GLSL stands for OpenGL Shading Language. As the name indicates, it is a programming language designed specifically for writing shaders: small programs that are used to customize the behavior of the graphics pipeline. Because WebGL implements the graphics pipeline as a program that runs on the GPU, shaders also run on the GPU because they can be considered parts of the graphics pipeline.
Like other long living programming languages, GLSL has multiple versions. For this book, we will be using a version of GLSL called "GLSL ES 3.0," which is the latest function of GLSL support by WebGL 2.0. It also supports other version, GLSL ES 1.0, but these two versions are not compatible with each other. Thus, is is important to note that what is written in this book only applies to OpenGL ES 3.0 and not any other versions of the language.
In this chapter, we discuss basic features of the GLSL languages such as its data types, control statements, and functional abstraction. These are not all the features that we will use in this book because covering all of them would make this chapter exceedingly long. We will discuss other features as we need them in later chapters.
Both Jvavascript and GLSL are languages in the C famility, which means that their syntaxes are very similar.
;
).{
and }
).=
) while testing for equality is indicated with two equal signs (==
), and testing for inequality is indicated with !=
.<function-name>(arg-1
, arg-2
, ..., arg-n
)
.+
, -
, *
, /
, &&
, ||
, !
, ++
, --
, ++
, --
have the same semantics as the Javascript ones. For GLSL ES 3.0 (which is the version we will be using), the semantics of the modulo operator (%
) and bitwise operators (&
, |
, ^
, <<
, and >>
) are also the same.if
, while
, and for
are the same.As a result of the above similarities, it is easy to learn GLSL if you already can program in Javascript. However, the closest language to GLSL is C, so GLSL learning would be much easier if you are already familiar with the language.
One of the biggest differences between Javascript and GLSL is that Javascript is dynamically typed, but GLSL is statically typed. In a dynamically-typed programming languages, one can declare a variable without specifying the type of values that it can hold. For example, in Javascript, one can declare a variable with a let
statement such as:
let x = 1;
Now, x
holds an integer value, but there is no restruction on what value x
can hold. We can later assign a floating point number or a boolean value to it without any problems.
x = 2.0; // No problem.
x = false; // Again, no problem.
On the other hand, in a statically-typed programming language, the programmer must indicate the type of data that a variable can hold. After declaration, the type cannot be changed, and the variable can only hold values of that type.
In GLSL, a variable can be declared with a statement of the form:
<type-name> <variable-name>;
For example, the following statement
int x;
declare x
as a variable of type int
, can only store integer values. As a result, the following assignment statements are OK.
x = 1; // No problem.
x = -10; // No problem.
x = 255; // No problem.
However, x
cannot store floating point values or boolean values. Doing so would result in a compilation error.
x = 0.0; // Compilation error.
x = true; // Compilation error.
We will talk about how to detect these errors later. For now, let us return to other forms of variable declaration. Like Javascript, one can declare a variable and then immediately assign its value.
int y = 42;
int aPrimeNumber = 1 + 6*3;
Note that, when we declare a variable without immediately assigning a value to it, the value of the variable is "undefined," which means that it would contain a value that we do not know in advanced. Hence, the value should not be used because it is unpredictable.
GLSL also supports declaring multiple variables at once.
int a, b, c; // All variables are not initialized.
int d = 100, e = 200; // All variables are initialized.
int f = 99, g, h = 199; // Declaration with and without assignments and can be mixed.
The simplest types that GLSL have are scalar types. These types correspond to numerical and boolean values, and there are 4 of them.
bool
correspond to logical values of true
and false
.int
corresponds signed integer values.uint
corresponds unsigned integer values.float
corresponds to floating point values.Note that the type names, except for uint
, are the same as those in the C language. However, the semantics of the types are not the same. In C, each type has a fixed associated size in memeory. For example, a variable of type int
would occupy exactly 4 bytes in memory, so does a a variable of type float
. For numerical types, their sizes indicate how much information they can store. For types that store real numbers, more storage means more signficant figures or matissa in the represented numbers. For examples, C's float
type can store 24 significant figures in base 2, while C's double
type can store 53. As a result, the bigger the size, the more precise the numbers that can be represented. Even though this fact does not quite apply to integer types, we may say that each type has an associated "precision," and this precision is fixed in C.
In GLSL, the precisions of the three numerical types (int
, uint
, and float
) can be changed. This is done through the precision
statement.
precision <precision-qualifier> <type>;
Here, <precision-qualifier>
can be either highp
, mediump
, and lowp
, which signify high, medium, and low precision, respectively. <type>
is an applicable type, which includes the three numerical types above and other more complicated types that we will discusss later. The following listing contains some example usage of the statement.
precision mediump int;
precision highp float;
precision lowp uint;
The effect of the precision qualifiers are different when applied to integer types (int
and uint
) and the floating point type (float
). For integer types, the storage size and the range of values that can be stored are as follows.
Precision qualifier | Storage size | int value range |
uint value range |
---|---|---|---|
highp |
32 bits | $-2^{31}$ to $2^{31}-1$ | $0$ to $2^{32}-1$ |
mediump |
16 bits | $-2^{15}$ to $2^{15}-1$ | $0$ to $2^{16}-1$ |
lowp |
9 bits | $-2^{8}$ to $2^{8}-1$ | $0$ to $2^{9}-1$ |
The effects of the qualifiers on float
is more complicated because the GLSL ES 3.0 specification specifies the behavior of highp
very precisely but only specifies minimum requirements of other qualifiers. In particular, highp
means the IEEE 754 standard's single-precision floating point number, exactly the same as the float
type in C, C++, and many other major programming languages. The specification allows mediump
and lowp
to have different behavior from implementation to implementation, but they must satisfy certain minimum requirements below.
Precision qualifier | Mininum/maximum values | Minimum/maximum magnitudes | Precision |
---|---|---|---|
mediump |
($-2^{14}$, $2^{14}$) | $(2^{-14}, 2^{14})$ | $2^{-10}$ relative to store value |
lowp |
(-2,2) | $(2^{-8},2)$ | $2^{-8}$ absolute precision |
mediump
and lowp
precision qualifiers.It is important to remember the default precision qualifiers for scalar types because they take effect whenever we don't explicitly specify anything. They are also quite troublesome because the defaults are different for vertex shaders and fragment shaders. For vertex shaders, the default qualifiers are:
precition highp float;
precision highp int;
precision highp uint;
For fragment shaders, the default values are:
precision mediump int;
precision mediump uint;
In other words, in vertex shaders, integer types have 32 bits by default, but they have 16 bits by default in fragment shaders. Not being aware of this fact can cause subtle bugs that are hard to discover. For example, consider the following seemlingly innocuous statement.
int n = 40000;
Inside a vectex shader, the variable n
will store the value of $40\,000$. However, inside a fragment shader, the value will not be $40\,000$ because, by default, an int
only has 16 bits, so the largest positive value it can store is $2^{15}-1 = 32\,767$. The stored value will a negative value, and any subsequent code that uses n
will behave in unspected way unless we set the predision of int
inside a fragment shader to highp
.
While there are default precision qualifiers for integer types in fragment shaders, there is no precision qualifier for the float
type. This is why, in the last chapter, we have to declare a precision qualifier for it in every fragment shader.
We note in the passing that we can specify different precision qualifier to each variable, but we will not use this feature in this book. So, the precision statement is something we use at the beginning of a program once to change the default behavior of numerical types, and then we have to write the rest of the program accordingly.
One feature of the GLSL language that is not commonly found in other languages is support for vector types at the language level. Vector types are data types that are indended to use to model mathematical vectors that we previously discussed in Chapter 3. In the last chapter, we used vector types to store points and colors although we did not discuss what they are in details.
For GLSL ES 3.0, a vector types can have 2, 3, or 4 components. This means that they can be used to represent 2D, 3D, and 4D vectors and points. They can also represent RGB colors (3 components) and RGBA colors (4 components). Recall from Chapter 4 that homogeneous coordinates are also 4D vectors, and so we can use vectors types to store them too.
For each scalar type, there are three associated vectors types.
bool
type, there are bvec2
, bvec3
, and bvec4
.int
type, there are ivec2
, ivec3
, and ivec4
.uint
type, there are uvec2
, uvec3
, and uvec4
.float
type, there are vec2
, vec3
, and vec4
.The type of each of the above vetor type is the associated scalar type. The number at the end of each vector type's name is the number of components. The first letter of a vector type's name indicate the scalar type of its component, except for vec2
, vec3
, and vec4
. Most programs use vectors with floating-points components most of the time, so it makes sense that the names for the types are short.
The precisions of the components of vectors types are the same as the precision of the associated scalar type. So, when we issue precision float highp;
in a fragment shader, the associated vectors types (vec2
, vec3
, and vec4
) will have the precision highp
as well.
Values of vector types can be constructed by vector constructors. These are expressions of the form
<vector-type>(<scalar-value-1>, <scalar-value-2>, ..., <scalar-value-n>)
where we can specify the value for each component individually. We can use vector constructors to initialize vector variables.
vec2 a = vec2(1.0, 2.0);
ivec3 b = vec3(4,5,6);
bvec4 c = bvec4(true, false, true, false);
We can also use constructors in variable assignments.
vec3 p = vec3(0.0, 0.0, 0.0);
p = vec3(1.0, 1.0, 1.0); // Now, all components of p is one.
One feature that is not often found in other languages is that we can use vectors inside constructors too.
vec2 a = vec2(1.0, 2.0);
vec3 b = vec3(0.0, a); // b = (0.0, 1.0, 2.0)
vec4 c = vec4(b, 3.0); // c = (0.0, 1.0, 2.0, 3.0)
vec4 d = vec3(a, a); // d = (1.0, 2.0, 1.0, 2.0)
Components of vectors types can be accessed in two ways. The first is the bracket notion, commonly used with arrays and lists in other programming languages.
vec3 p = vec3(10.0, 20.0, 30.0);
float x = p[0]; // x = 10
float y = p[1]; // y = 20
float z = p[2]; // z = 30
float total = p[0] + p[1] + p[2]; // total = 60
float temp = p[2] / (2*p[0]) * p[1]; // temp = 30
The other ways is to use the dot notation, which treats the components as if it were fields. The field names that can be used to access the components are as follows:
.x
, .r
or .s
..y
, .g
or .t
..z
, .b
or .p
..w
, .a
or .q
.The above lists can be rewritten as a table, which is shown below. Through naming conventions, GLSL provides us 3 ways to look at a vector: as a point/vector in the Euclidean space, as a representation for color, as a texture coordiate (more on this in a later chapter).
Naming convention | 1st comp | 2nd comp | 3rd comp | 4th comp |
---|---|---|---|---|
Point/vector in Euclidean space | .x |
.y |
.z |
.w |
Color | .r |
.g |
.b |
.a |
Texture coordinate | .s |
.t |
.p |
.q |
Let look at some examples on how the dot notations can be used.
vec4 F = vec4(10.0, 20.0, 30.0, 40.0);
float f = (F.x + F.y * F.z) / F.w; // f = 7/4
float temp = (F.r + F.g + F.b) * F.a; // temp = 2400
vec2 G = vec2((F.s + F.t)/2.0, (F.p + F.q)/2.0); // G = (15,35)
F.x = 0.0; // F = (0,20,30,40)
F.a = F.r*F.r + F.g*F.b + F.a*F.a; // F = (0,20,30,2200)
Before we move on to the next section, I would like to comment on the field names. I recommend that the reader not mix the naming conventions together. More specifically, the reader should decide the naming covention to use for each variable based on its meaning and stick to this naming convention. For example, if p
is vec3
that is used to represent the position of a point in space, then we should not access its components like it represents a color or a texture coordinate. In other words, p.x
, p.y
, and p.z
are OK, but p.r
, p.g
, and p.b
are not. As a another example, let color
be a vec4
that is used to represent an RGBA color. Then, we should access its components with color.r
, color.g
, color.b
, and color.a
, not color.s
, color.y
, and so on. Maintaining this rule makes the code easier to understand and will prevent bugs that are hard to discover from materializing.
Swizzling is the unique feature of GLSL where we can construct vectors by stringing together field names in the last section. It is easier to see in action than to explain.
vec4 p = vec4(1.0, 2.0, 3.0, 4.0);
vec3 a = p.xyz; // a = (1, 2, 3)
vec2 b = p.ww; // b = (4,4)
vec4 c = vec4(p.zxx, 5.0); // c = (3, 1, 1, 5)
GLSL only have up 4-component vectors, so an expression like p.xxyyzzww
would not work because it would create an 8-component vector. Vector that is referred to by swizzling can appear on the left side of the assignment operator, and it behaves as you would expect from reading the code.
vec4 q = vec4(9.0, 8.0, 7.0, 6.0);
q.xy = 1.0; // q = (1,1,7,6)
q.yzw = vec3(2.0,3.0,4.0); // q = (1,2,3,4)
q.xyzw = q.wxyz; // q = (4,1,2,3)
Note that, when a swizzled vector appear on the LHS of an assignment, the same component cannot be used twice. For example q.yy = 1.0;
is not allowed.
We can do arithmetic operations on vector types, and this capability allows us to perform the operations we studied in Chapter 3 and Chapter 4. First, vectors types support addition, subtraction, and multiplication/division by scalars.
vec3 u = vec3(0.0, 1.0, 2.0);
vec3 v = vec3(40.0, 50.0, 60.0);
vec3 a = u + v; // a = (40,51,62)
vec3 b = u - v; // b = (-40,-49,-58)
vec3 c = 2.0 * u; // c = (0,2,4)
vec3 d = u * -1.0; // d = (0,-1,-2)
vec3 e = v / 10.0; // e = (4,5,6)
Vector types can also multiply and divide each other. Like addition and subtraction, these operations are apply the arithmetic operation on a per-component basis. It is important to keep in mind that they are not the same as the dot product or the cross product that we studied in Chapter 3.
vec3 f = u * v; // u = (0,50,120)
vec3 g = v / vec3(5.0,10.0,15.0); // g = (8,5,4)
On the other hand, the dot product and the cross product can be computed using the dot
and cross
built-in functions. Note that we can use dot
with all vector types, but we can only use cross
with only vector types that have 3 components. This is because the cross product as a mathematical operation is only defined on vectors in $\mathbb{R}^3$.
float temp = dot(u,v); // temp = 50+120 = 170
vec3 X = vec3(1.0, 0.0, 0.0);
vec3 Y = vec3(0.0, 1.0, 0.0);
vec3 Z = vec3(0.0, 0.0, 1.0);
vec3 XcY = cross(X,Y); // XcY = (0,0,1)
vec3 YcX = cross(Y,X); // YcX = (0,0,-1)
vec3 XcX = cross(X,X); // XcX = (0,0,0)
vec3 ZcY = cross(Z,Y); // ZcY = (-1,0,0)
Like Javascript, GLSL has array types. Arrays in GLSL differ from Javascript arrows in two different ways.
int
, float
, vec4
, or vec2i
. On the other hand, a Javascript arrays can hold values of different types in different cells. You can have a Javascript array whose first element is a number but the second element is a string.There are multiple ways to declare arrays in a GLSL shader, but we recommend the following where the element type and array size are written together before the name.
<element-type>[<array-size>] <variable-name>;
Here are some examples of array declaration.
int[5] a;
vec4[10] b;
bvec2[20] c;
We repeat that the value that appears inside the bracket must be a constant that the GLSL compilation must be able to determine at compile time. As a result, it cannot be a variable or a mathematical expression that involves variables.
Like Javascript, elements of arrays can be accessed by bracket notation.
float[5] a;
a[0] = 1.0;
a[1] = 10.0;
a[2] = 20.0;
float b = (a[0] + a[1]) * a[2]; // b = 220
a[3] = b / a[1]; // a[3] = 22
a[4] = a[0] + a[3]; // a[4] = 23
Multi-dimensional arrays can be declared with multiple brackets after the array type names.
int[10][10] a;
float[2][3][4] b;
vec3[5][4][3][2] c;
Elements of such arrays are accessed in the same way they are accessed in Javascript.
vec3[10][10] p;
p[0][0] = vec3(1,2,3);
p[1][1] = p[0][0].xxx; // p[1][1] = (1,1,1)
float dotProd = dot(p[1][1], p[0][0]); // dotProd = 6
Lastly, like vector types, array types of constructors that can be used to initialize their values.
int[3] a = int[3](1,2,3);
float[4] b = float[4](90.0, 80.0, 70.0, 60.0);
Alternatively, one can use curly braces to surround the values and avoid writing down the type name and the size.
int[3] a = {1, 2, 3};
float[4] b = {90.0, 80.0, 70.0, 60.0};
A struct is a data type whose values are collections of different values, which can have different types. Each of such values is called a field and is identified by a specific field name. The struct type is a staple of the C, and the most similar feature in Javascript to struct is probably the object type. However, GLSL structs do not have methods, and fields cannot be added or removed after declaration. Moreover, each field has a specific type that cannot be changed.
A struct type can be declared with the struct
keyword, followed by the name of the type and a code block, delineated by curly braces. Inside the block, we declare the fields like we declare variables. Lastly, we must put a semi-colon after the closing curly brace to make the declaration syntactically correct.
struct <type-name> {
<field-declaration-1>
<field-declaration-2>
:
:
<field-declaration-n>
};
The following in an example of a struct that represents a particle moving in 3D space.
struct Particle {
float mass;
vec3 position;
vec3 velocity;
};
After the struct type has been declared, we can use it to declare variables like other type names such as int
, float
, or vec3
. This means that arrays of structs can be declared.
Particle a;
Particle[10] particles;
Similar to Javascript objects, fields of a struct can be accessed with the dot notation.
Particle p;
p.mass = 10.0;
p.position = vec3(1,2,-1);
p.velocity = vec3(1,0,0);
vec3 force = vec3(0,-98,0);
vec3 acceleration = force / p.mass;
// acceleration = (0,-9.8,0)
vec3 newVelocity = p.velocity + 0.1 * acceleration;
// newVelocity = (1,-0.98,0)
vec3 newPosition = p.position + 0.1 * newVelocity;
// newPosition = (1.1,1.902,-1)
Structs can also be initalized with constructors in a similar way to vector types.
Particle q = Particle(20.0, vec3(0,0,0), vec3(1,-1,1));
Curly brace notations can also be used to shorten the constructor.
Particle q = {20.0, {0,0,0}, {1,-1,1}};
A major difference between Javascript and GLSL is behavior of variables. First, let us remind ourselves that Javascript variables are references to values. The values are stored somewhere in the computer's memory, and a variable simply holds the memory address. When we do an assignment statement in Javascript, say b = a;
, we simply make sure that b
would store the address of the value that a
refers to. As a result, b
would refer to the same value after the statement finishes. If we make any modification to the value that b
refers to, we would also see the change through a
as well. For example, consider the following Javascript code.
let a = {x:1, y:2, z:3};
let b = a;
b.x = 0;
console.log(a.x);
We would see that the value 0 is printed by the console.
On the other hand, GLSL variables are not references. A GLSL variable can be thought of as a fixed area in GPU memory with a definite size and a definite address that remains unchanged until the variable goes out of scope. Two different variables in GLSL are thus completely independent. When the statement b = a;
is executed, what happens is that the value in a
is copied to b
, and what happends to what is stored in b
afterwards would not be reflected on what is stored in a
. Let us following GLSL that looks pretty similar to the Javascript code above.
vec3 a = vec3(1,2,3);
vec3 b = a;
b.x = 0;
We can deduce that, after the last statement ends, b
would hold the value $(0,2,3)$ while a
holds $(1,2,3)$. This behavior is the complete opposite of Javascript's behavior, and it is worth keeping this in mind to avoid subtle programming errors.
Note also that the behavior we just discussed applies to all GLSL types of variables, and these include primitives, arrays, vectors, and structs. Javascript also have arrays, and Javascript objects are similar in appearance to GLSL vectors and structs. Again, it is important to remember that these similar concepts have totally different behavior in their respective languages.
GLSL has similar control statements to Javascript. The following control statements are exactly the same as those in Javascript.
if
switch
while ... do
do ... while
continue
break
return
So, we will not discuss these statements for brevity. The only statement that is different is the for
statement, and the difference is minimal. In Javascript, we can declare a new counter variable in the first clause of the for
statement. For examples, we may write
for (let i=0; i<10; i++) {
:
:
}
or
for (var i=0; i<10; i++) {
:
:
}
However, GLSL has neither the let
nor the var
keyword, so the above pieces of code are not valid GLSL code. The only change we need to do is to replace let
and var
with the type that the counter variable is supposed to have. In our case, because the counter is an integer, we shall use int
instead of let
and var
. So, the following is a valid GLSL for
statement.
for (int i=0; i<10; i++) {
:
:
}
Functions in GLSL are different from functions in Javascript in two major ways.
The GLSL syntax for function declaration is as follows.
<return-type> <function-name>(
<parameter-type> <parameter-name>,
<parameter-type> <parameter-name>,
:
<parameter-type> <parameter-name>) {
<statement>
<statement>
:
:
<statement>
}
If the function does not take any parameters, the declaration becomes:
<return-type> <function-name>() {
<statement>
<statement>
:
:
<statement>
}
Like in Javascript, we can exit a function with the return
statement. When we use it, we must specify a return value that has the same type as the one we declare as the return type of the function. (There is an exception to this, which we will talk about momentarily.)
For example, here is a GLSL function that takes in a float
and returns the square of it.
float squareFloat(float x) {
return x*x;
}
This function is different from the following function, which does the same thing but the types of the input and the output are integers.
int squareInt(int x) {
return x*x;
}
Notice how the return types and parameter types make functions that peform the same mathematical operation different.
Like in Javascript, a function does not have to return a value. In this case, the return type in the function declaration must be void
. We have seen functions that have this return type before. In particular, all the main
functions of the GLSL shaders in the last chaper all have void
return type. For example, the main
function of most vertex shaders in the last chapter are:
void main() {
gl_Position = vec4(position, 1);
}
Moreover, most main
functions of the fragment shaders are:
void main() {
fragColor = vec4(1.0, 1.0, 1.0, 1.0);
}
Notice that, when a function does not return a value, it should somehow have a side effect. Otherwise, there is no point in writing the function in the first place. We can see that the main
functions above modify global variables that are not declared inside the function. For the vertex shader's main
function, it modifies the gl_Position
variable, which stores the vertex position that is to be processed by the next step of the graphics pipeline. For the fragment shader's main
function, it modified the fragColor
variable that was declared just above the function. There is another way to have side effects, but we will discuss it when we shall need it in a future chapter.
While one must use the return
statement if the function is declared to return a value. The return statement is optional in a void
function. When it is used, though, we do not have to specify a return value. For example, we can modify the vertex shader so that its outputs are different based on the input position as follows.
void main() {
if (position.x < 0) {
gl_Position = vec4(0,0,0,1);
return;
}
gl_Position vec4(position, 1);
}
Function parameters are just variables that are receptacles of data that are supposed to be processed by the function. Because Javascript and GLSL have different variable semantics, the two languages' parameter passing semantics are different as well. Javascript variables are references, so function parameters are passed by reference. This means that a function parameter would hold a memory address to a value. Any modification made to the value would thus be visible through any other variables that also refer to it, and these include variables that are not in the function's scope.
For example, consider the following Javascript function that receives an array, extract its first element, and then replaces it by 0.
function f(arr) {
let output = arr[0];
arr[0] = 0;
return output;
}
If we call this function with an array, we would see that the array's first element would become zero after the function exits.
let a = [1,2,3];
let b = f(a);
console.log(b); // This would print 1.
console.log(a); // This would print [0,2,3];
On the other hand, a GLSL variable is a fixed area in GPU memory that stores a particular type of values. Thus, parameters are passed by value. This means that, when a function is called, the program would first allocate new areas of memory to store all the parameters. It would then copy the values that the user specify to the new memory areas and then execute the function's command. After the function exists, these memory areas would be free. This means that, in side the function, parameters are independent to any values or variables are specified to be the parmaeters. Any changes made to the parameters will not have any effect on any variables outside the function's scope. As an example, consider a GLSL function that is modeled after the Javascript function above.
int f(int[3] arr) {
int output = arr[0];
arr[0] = 0;
return output;
}
If we execute the function, its modification to the first element of the array would not affect the array that was passed in as the parameter.
int[3] a = {1,2,3};
int b = f(a);
We would see that b
would be 1, but the first element of a
would still be 1. Again, we stress that the reader remembers this crucial difference in order to avoid bugs.
int
, uint
, float
, and bool
.
bool
type is the same as Javascript boolean type.int
, uint
, and float
) have precision that can be set throught the precision
statement.vec2
, vec3
, vec4
, and so on are unique features of GLSL that make 3D progarmming simple and pleasant.
[...]
) or the dot (.x
, .y
, .z
) notations.for
loop where the type of the index variable must be exclitly declared.void
type. This is the return type of all main
functions of all shaders.