You have seen a few short, simple examples of shader programs written in GLSL. In fact, shader programs are often fairly short, but they are not always so simple. To understand the more complex shaders that we will be using in the rest of this book, you will need to know more about GLSL. This section aims to give a short, but reasonably complete, introduction to the major features of the language. This is a rather technical section. You should read it to get some familiarity with GLSL, and then use it as a reference when needed.
The version of GLSL for WebGL 1.0 is GLSL ES 1.0. However, the specification for GLSL ES 1.0 lists a number of language features as being optional. The WebGL specification mandates that the optional features in GLSL ES 1.0 are not supported in WebGL. These unsupported features include some that you would probably consider pretty basic, such as while loops and certain kinds of array indexing. The justification for having optional features in GLSL ES is that GPUs vary in the set of features that can be efficiently implemented, and GPUs for embedded systems can be especially limited. The justification for eliminating those optional features in WebGL is presumably that WebGL programs are used on Web pages that can be accessed by any device, so they should work on the full range of devices.
Variables in GLSL must be declared before they are used. GLSL is a strictly typed language, and every variable is given a type when it is declared.
GLSL has built-in types to represent scalars (that is, single values), vectors, and matrices. The scalar types are float, int, and bool. A GPU might not support integers or booleans on the hardware level, so it is possible that the int and bool types are actually represented as floating point values.
The types vec2, vec3, and vec4 represent vectors of two, three, and four floats. There are also types to represent vectors of ints (ivec2, ivec3, and ivec4) and bools (bvec2, bvec3, and bvec4). GLSL has very flexible notation for referring to the components of a vector. One way to access them is with array notation. For example, if v is a four-component vector, then its components can be accessed as v[0], v[1], v[2], and v[3]. But they can also be accessed using the dot notation as v.x, v.y, v.z, andv.w. The component names x, y, z, and w are appropriate for a vector that holds coordinates. However, vectors can also be used to represent colors, and the components of v can alternatively be referred to as v.r, v.g, v.b, and v.a. Finally, they can be referred to as v.s, v.t, v.p, and v.q — names appropriate for texture coordinates.
Furthermore, GLSL allows you to use multiple component names after the dot, as in v.rgb or v.zx or even v.yyy. The names can be in any order, and repetition is allowed. This is called swizzling, and v.zx is an example of a swizzler. The notation v.zx can be used in an expression as a two-component vector. For example, if v is vec4(1.0,2.0,3.0,4.0), then v.zx is equivalent to vec2(3.0,1.0), and v.yyy is like vec3(2.0,2.0,2.0). Swizzlers can even be used on the left-hand side of an assignment, as long as they don't contain repeated components. For example,
vec4 coords = vec4(1.0, 2.0, 3.0, 4.0); vec3 point = vec3(5.0, 6.0, 7.0); coords.yzw = coords.wyz; // Now, coords is (1.0, 4.0, 2.0, 3.0) point.xy = coords.xx; // Now, point is (1.0, 1.0, 7.0)
A notation such as vec2(1.0, 2.0) is referred to as a "constructor," although it is not a constructor in the sense of Java or C++, since GLSL is not object-oriented, and there is no new operator. A constructor in GLSL consists of a type name followed by a list of expressions in parentheses, and it represents a value of the type specified by the type name. Any type name can be used, including the scalar types. The value is constructed from the values of the expressions in parentheses. An expression can contribute more than one value to the constructed value; we have already seen this in examples such as
vec2 v = vec2( 1.0, 2.0 ); vec4 w = vec4( v, v ); // w is ( 1.0, 2.0, 1.0, 2.0 )
Note that the expressions can be swizzlers:
vec3 v = vec3( 1.0, 2.0, 3.0 ); vec3 w = vec3( v.zx, 4.0 ); // w is ( 3.0, 1.0, 4.0 )
Extra values from the last parameter will be dropped. This makes is possible to use a constructor to shorten a vector. However, it is not legal to have extra parameters that contribute no values at all to the result:
vec4 rgba = vec4( 0.1, 0.2, 0.3, 0.4 ); vec3 rgb = vec3( rgba ); // takes 3 items from rgba; rgb is (0.1, 0.2, 0.3) float r = float( rgba ); // r is 0.1 vec2 v = vec2( rgb, rgba ); // ERROR: No values from rgba are used.
As a special case, when a vector is constructed from a single scalar value, all components of the vector will be set equal to that value:
vec4 black = vec4( 1.0 ); // black is ( 1.0, 1.0, 1.0, 1.0 )
When constructing one of the built-in types, type conversion will be applied if necessary. For purposes of conversion, the boolean values true/false convert to the numeric values zero and one; in the other direction, zero converts to false and any other numeric value converts to true. As far as I know, constructors are the only context in which GLSL does automatic type conversion. For example, you need to use a constructor to assign an int value to a float variable, and it is illegal to add an int to a float:
int k = 1; float x = float(k); // "x = k" would be a type mismatch error x = x + 1.0; // OK x = x + 1; // ERROR: Can't add values of different types.
The built-in matrix types are mat2, mat3, and mat4. They represent, respectively, two-by-two, three-by-three, and four-by-four matrices of floating point numbers. The elements of a matrix can be accessed using array notation, such as M[2][1]. If a single index is used, as in M[2], the result is a vector. For example, if M is of type mat4, then M[2] is a vec4. Arrays in GLSL, as in OpenGL, use column-major order. This means that M[2] is column number 2 in M rather than row number 2 (as it would be in Java), and M[2][1] is the element in column 2 and row 1.
A matrix can be constructed from the appropriate number of values, which can be provided as scalars, vectors or matrices. For example, a mat3 can be constructed from nine float or from three vec3 parameters:
mat3 m1 = mat3( 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0 ); vec3 v = vec3( 1, 2, 3 ); mat3 m2 = mat3( v, v, v );
Keep in mind that the matrix is filled in column-major order; that is, the first three numbers go into column 0, the next three into column 1, and the last three into column 2.
As a special case, if a matrix M is constructed from a single scalar value, then that value is put into all the diagonal elements of M (M[0][0], M[1][1], and so on). The non-diagonal elements are all set equal to zero. For example, mat4(1.0) constructs the four-by-four identity matrix.
The only other built-in types are sampler2D and samplerCube, which are used for accessing textures. The sampler types can be used only in limited ways. They are not numeric types and cannot be converted to or from numeric types. The will be covered in the next section.
A GLSL program can define new types using the struct keyword. The syntax is the same as in C, with some limitations. A struct is made up of a sequence of named members, which can be of different types. The type of a member can be any of the built-in types, an array type, or a previously defined struct type. For example,
struct LightProperties { vec4 position; vec3 color; float intensity; };
This defines a type named LightProperties. The type can be used to declare variables:
LightProperties light;
The members of the variable light are then referred to as light.position, light.color, and light.intensity. Struct types have constructors, but their constructors do not support type conversion: The constructor must contain a list of values whose types exactly match the types of the corresponding members in the struct. For example,
light = LightProperties( vec4(0.0, 0.0, 0.0, 1.0), vec3(1.0), 1.0 );
GLSL also supports arrays. Only one-dimensional arrays are allowed. The base type of an array can be any of the basic types or it can be a struct type. The size of the array must be specified in the variable declaration as an integer constant. For example
int A[10]; vec3 palette[8]; LightProperties lights[3];
There are no array constructors, and it is not possible to initialize an array as part of its declaration.
Array indexing uses the usual syntax, such as A[0] or palette[i+1] or lights[3].color. However, there are some strong limitations on the expressions that can be used as array indices. With one exception, an expression that is used as the index for an array can contain only integer constants and for loop variables (that is, variables that are used as loop control variables in for loops). For example, the expression palette[i+1] would only be legal inside a for of the form for (int i = .... The single exception is that arbitrary index expressions can be used for arrays of uniforms in a vertex shader (and then only if the array does not contain samplers).
Just as in C, there is no check for array index out of bounds errors. It is up to the programmer to make sure that array indices are valid.
Variable declarations can be modified by various qualifiers. You have seen examples of the qualifiers attribute, uniform, and varying. These are called storage qualifiers. The other possible storage qualifier is const, which means that the value of the variable cannot be changed after it has been initialized. In addition, it is not legal to assign a value to an attribute or uniform variable; their values come from the JavaScript side, and they are considered to be read-only. There are implementation-dependent limits on the numbers of attribute, uniform, and varying variables that can be used in a shader program; this is discussed in the last subsection of this section.
The attribute qualifier can only be used for global variables in the vertex shader, and it only applies to the built-in floating point types float, vec2, vec3, vec4, mat2, mat3, and mat4. (Matrix attributes are not supported directly on the JavaScript side. A matrix attribute has to be treated as a set of vector attributes, one for each column. Matrix attributes would be very rare, and I won't go into any detail about them here.)
Both the vertex shader and the fragment shader can use uniform variables. The same variable can occur in both shaders, as long as the types in the two shaders are the same. Uniform variables can be of any type, including array and structure types. Now, JavaScript only has functions for setting uniform values that are scalar variables, vectors, or matrices. There are no functions for setting the values of structs or arrays. The solution to this problem requires treating every component of a struct or array as a separate uniform value. For example, consider the declarations
struct LightProperties { vec4 position; vec3 color; float intensity; }; uniform LightProperties light[4];
The variable light contains twelve basic values, which are of type vec4, vec3, or float. To work with the light uniform in JavaScript, we need twelve variables to represent the locations of the 12 components of the uniform variable. When using gl.getUniformLocation to get the location of one of the 12 components, you need to give the full name of the component in the GLSL program. For example: gl.getUniformLocation(prog, "light[2].color"). It is natural to store the 12 locations in an array of JavaScript objects that parallels the structure of the array of structs on the GLSL side. Here is typical JavaScript code to create the structure and use it to initialize the uniform variables:
lightLocations = new Array(4); for (i = 0; i < light.length; i++) { lightLocations[i] = { position: gl.getUniformLocation(prog, "light[" + i + "].position" ); color: gl.getUniformLocation(prog, "light[" + i + "].color" ); intensity: gl.getUniformLocation(prog, "light[" + i + "].intensity" ); }; } for (i = 0; i < light.length; i++) { gl.uniform4f( lightLocations[i].position, 0, 0, 0, 1 ); gl.uniform3f( lightLocations[i].color, 1, 1, 1 ); gl.uniforma1f( lightLocations[i].intensity, 0 ); }
For uniform shader variables that are matrices, the JavaScript function that is used to set the value of the uniform is gl.uniformMatrix2fv for a mat2, gl.uniformMatrix3fv for a mat3, or gl.uniformMatrix4fv for a mat4. Even though the matrix is two-dimensional, the values are stored in a one dimensional array. The values are loaded into the array in column-major order. For example, if transform is a uniform mat3 in the shader, then JavaScript can set its value to be the identity matrix with
transformLoc = gl.getUniformLocation(prog, "transform"); gl.uniformMatrix3fv( transformLoc, false, [ 1,0,0, 0,1,0, 0,0,1 ] );
The second parameter must be false. (In some other versions of OpenGL, the second parameter can be set to true to indicate that the values are in row-major instead of column-major order, but WebGL requires column-major order.) Note that the 3 in uniformMatrix3fv refers to the number of rows and columns in the matrix, not to the length of the array, which must be 9. (By the way, it is OK to use a typed array rather than a normal JavaScript array for the value of a uniform.)
As for the varying qualifier, it can be used only for the built-in floating point types (float, vec2, vec3, vec4, mat2, mat3, and mat4) and for arrays of those types. A varying variable should be declared in both the vertex and fragment shader. (This is not actually a requirement; an error only occurs if the fragment shader tries to use the value of a varying variable that does not exist in the vertex shader.) A variable must have the same type in both shaders. The variable is read-only in the fragment shader. The vertex shader should write a value to the varying variable, and it can also read its value.
Variable declarations can also be modified by precision qualifiers. The possible precision qualifiers are highp, mediump, and lowp. A precision qualifier sets the minimum range of possible values for an integer variable or the minimum range of values and number of decimal places for a floating point variable. GLSL doesn't assign a definite meaning to the precision qualifiers, but mandates some minimum requirements. For example, lowp integers must be able to represent values in at least the range −28 to 28; mediump integers, in the range −210 to 210; and highp integers, in the range −216 to 216. It is possible—and on desktop computers it is likely—that all values are 32-bit values and the precision qualifiers have no real effect. But GPUs in embedded systems can be more limited.
A precision qualifier can be used on any variable declaration. If the variable also has a storage qualifier, the storage qualifier comes first. For example
lowp int n; varying highp float v; uniform mediump vec3 colors[3];
A varying variable can have different precisions in the vertex and in the fragment shader. The default precision for integers and floats in the vertex shader is highp. Fragment shaders are not required to support highp, although it is likely that they do so, except on older mobile hardware. In the fragment shader, the default precision for integers is mediump, but floats do not have a default precision. This means that every floating point variable in the fragment shader has to be explicitly assigned a precision. Alternatively, it is possible to set a default precision for floats with the statement
precision mediump float;
This statement was used at the start of each of the fragment shaders in the previous section. Of course, if the fragment shader does support highp, this restricts the precision unnecessarily. You can avoid that by using this code at the start of the fragment shader:
#ifdef GL_FRAGMENT_PRECISION_HIGH precision highp float; #else precision mediump float; #endif
This sets the default precision to highp if it is available and to mediump if not. The lines starting with "#" are preprocessor directives—an aspect of GLSL that I don't want to get into.
The last qualifier, invariant, is even more difficult to explain, and it has only a very limited use. Invariance refers to the requirement that when the same expression is used to compute the value of the same variable (possibly in different shaders), then the value that is assigned to the variable should be exactly the same in both cases. This is not automatically the case. For example, the values can be different if a compiler uses different optimizations or evaluates the operands in a different order in the two expressions. The invariant qualifier on the variable will force the compiler to use exactly the same calculations for the two assignment statements. The qualifier can only be used on declarations of varying variables. It must be the first qualifier in the declaration. For example,
invariant varying mediump vec3 color;
It can also be used to make the predefined variables such as gl_Position and gl_FragCoord invariant, using a statement such as
invariant gl_Position;
Invariance can be important in a multi-pass algorithm that applies two or more shader programs in succession to compute an image. It is important, for example, that both shaders get the same answer when they compute gl_Position for the same vertex, using the same expression in both vertex shaders. Making gl_Position invariant in the shaders will ensure that.
Expressions in GLSL can use the arithmetic operators +, −, *, /, ++ and −− (but %, <<, and >> are not supported). They are defined for the types int and float. There is no automatic type conversion in expressions. If x is of type float, the expression x+1 is illegal. You have to say x+1.0 or x+float(1).
The arithmetic operators have been extended in various ways to work with vectors and matrices. If you use * to multiply a matrix and a vector, in either order, it multiplies them in the linear algebra sense, giving a vector as the result. The types of the operands must match in the obvious way; for example, a vec3 can only be multiplied by a mat3, and the result is a vec3. When used with two matrices of the same size, * does matrix multiplication.
If +, −, *, or / is used on a vector and a scalar of the same basic type, then the operation is performed on each element of the vector. For example, vec2(3.0,3.0) / 2.0 is the vector vec2(1.5,1.5), and 2*ivec3(1,2,3) is the vector ivec3(2,4,6). When one of these operators is applied to two vectors of the same type, the operation is applied to each pair of components, and the result is a vector. For example, the value of
vec3( 1.0, 2.0, 3.0 ) + vec3( 4.2, -7.0, 1.7 )
is the vector vec3(5.2,-5.0,4.7). Note in particular that the usual vector arithmetic operations—addition and subtraction of vectors, multiplication of a vector by a scalar, and multiplication of a vector by a matrix—are written in the natural way is GLSL.
The relational operators <, >, <=, and >= can only be applied to ints and floats, and the types of the two operands must match exactly. However, the equality operators == and != have been extended to work on all of the built-in types except sampler types. Two vectors are equal only if the corresponding pairs of components are all equal. The same is true for matrices. The equality operators cannot be used with arrays, but they do work for structs, as long as the structs don't contain any arrays or samplers; again, every pair of members in two structs must be equal for the structs to be considered equal.
GLSL has logical operators !, &&, ||, and ^^ (the last one being an exclusive or operation). The operands must be of type bool.
Finally, there are the assignment operators =, +=, −=, *=, and /=, with the usual meanings.
GLSL also has a large number of predefined functions, more than I can discuss here. All of the functions that I will mention here require floating-point values as parameters, even if the function would also make sense for integer values.
Most interesting, perhaps, are functions for vector algebra. See Section 3.5 for the definitions of these operations. These functions have simple formulas, but they are provided as functions for convenience and because they might have efficient hardware implementations in a GPU. The function dot(x,y) computes the dot product x·y of two vectors of the same length. The return value is a float; cross(x,y) computes the cross product x×y, where the parameters and return value are of type vec3; length(x) is the length of the vector x and distance(x,y) gives the distance between two vectors; normalize(x) returns a unit vector that points in the same direction as x. There are also functions named reflect and refract that can be used to compute the direction of reflected and refracted light rays; I will cover them when I need to use them.
The function mix(x,y,t) computes x*(1−t) + y*t. If t is a float in the range 0.0 to 1.0, then the return value is a linear mixture, or weighted average, of x and y. This function might be used, for example, to do alpha-blending of two colors. The function clamp(x,low,high) clamps x to the range low to high; the return value could be computed as min(max(x,low),high). If rgb is a vector representing a color, we could ensure that all of the components of the vector lie in the range 0 to 1 with the command
rgb = clamp( rgb, 0.0, 1.0 );
If s and t are floats, with s < t, then smoothstep(s,t,x) returns 0.0 for x less than s and returns 1.0 for x greater than t. For values of x between s and t, the return value is smoothly interpolated from 0.0 to 1.0. Here is an example that might be used in a fragment shader for rendering a gl.POINTS primitive, with transparency enabled:
float dist = distance( gl_PointCoord, vec2(0.5) ); float alpha = 1.0 - smoothstep( 0.45, 0.5, dist ); if (alpha == 0.0) { discard; // discard fully transparent pixels } gl_FragColor = vec4( 1.0, 0.0, 0.0, alpha );
This would render the point as a red disk, with the color fading smoothly from opaque to transparent around the edge of the disk, as dist increases from 0.45 to 0.5. Note that for the functions mix, clamp, and smoothstep, the x and y parameters can be vectors as well as floats.
The usual mathematical functions are available in GLSL, including sin, cos, tan, asin, acos, atan, log, exp, pow, sqrt, abs, floor, ceil, min, and max. For these functions, the parameters can be any of the types float, vec2, vec3, or vec4. The return value is of the same type, and the function is applied to each component separately. For example, the value of sqrt(vec3(16.0,9.0,4.0)) is the vector vec3(4.0,3.0,2.0). For min and max, there is also a second version of the function in which the first parameter is a vector and the second parameter is a float. For those versions, each component of the vector is compared to the float; for example, max(vec3(1.0,2.0,3.0),2.5) is vec3(2.5,2.5,3.0).
The function mod(x,y) computes the modulus, or remainder, when x is divided by y. The return value is computed as x − y*floor(x/y). As with min and max, x can be either a vector or a float. The mod function can be used as a substitute for the % operator, which is not supported in GLSL.
There are also a few functions for working with sampler variables that I will discuss in the next section.
A GLSL program can define new functions, with a syntax similar to C. Unlike C, function names can be overloaded; that is, two functions can have the same name, as long as they have different numbers or types of parameters. A function must be declared before it is used. As in C, it can be declared by giving either a full definition or a function prototype.
Function parameters can be of any type. The return type for a function can be any type except for array types. A struct type can be a return type, as long as the structure does not include any arrays. When an array is used a formal parameter, the length of the array must be specified by an integer constant. For example,
float arraySum10( float A[10] ) { float sum = 0.0; for ( int i = 0; i < 10; i++ ) { sum += A[i]; } return sum; }
Function parameters can be modified by the qualifiers in, out, or inout. The default, if no qualifier is specified, is in. The qualifier indicates whether the parameter is used for input to the function, output from the function, or both. For input parameters, the value of the actual parameter in the function call is copied into the formal parameter in the function definition, and there is no further interaction between the formal and actual parameters. For output parameters, the value of the formal parameter is copied back to the actual parameter when the function returns. For an inout parameter, the value is copied in both directions. This type of parameter passing is referred to as "call by value/return." Note that the actual parameter for an out or inout parameter must be something to which a value can be assigned, such as a variable or swizzler. (All parameters in C, Java, and JavaScript are input parameters, but passing a pointer as a parameter can have an effect similar to an inout parameter. GLSL, of course, has no pointers.) For example,
void cumulativeSum( in float A[10], out float B[10]) { B[0] = A[0]; for ( int i = 1; i < 10; i++ ) { B[i] = B[i-1] + A[i]; } }
Recursion is not supported for functions in GLSL. This is a limitation of the type of processor that is typically found in GPUs. There is no way to implement a stack of activation records. Also, GLSL for WebGL does not support computations that can continue indefinitely.
The only control structures in GLSL for WebGL are the if statement and a very restricted form of the for loop. There is no while or do..while loop, and there is no switch statement.
If statements are supported with the full syntax from C, including else and else if.
In a for loop, the loop control variable must be declared in the loop, and it must be of type int or float. The initial value for the loop control variable must be a constant expression (that is, it can include operators, but all the operands must be literal constants or const variables) The code inside the loop is not allowed to change the value of the loop control variable. The test for ending the loop can only have the form var op expression, where var is the loop control variable, the op is one of the relational or equality operators, and the expression is a constant expression. Finally, the update expression must have one of the forms var++, var--, var+=expression, or var-=expression, where var is the loop control variable, and expression is a constant expression. Of course, this is the most typical form for for loops in other languages. Some examples of legal first lines for for loops:
for (int i = 0; i < 10; i++) for (float x = 1.0; x < 2.0; x += 0.1) for (int k = 10; k != 0; k -= 1)
For loops can include break and continue statements.
WebGL puts limits on certain resources that are used by WebGL and its GLSL programs, such as the number of attribute variables or the size of a texture image. The limits are due in many cases to hardware limits in the GPU, and they depend on the device on which the program is running, and on the implementation of WebGL on that device. The hardware limits will tend to be lower on mobile devices such as tablets and phones. Although the limits can vary, WebGL imposes a set of minimum requirements that all implementations must satisfy.
For example, any WebGL implementation must allow at least 8 attributes in a vertex shader. The actual limit for a particular implementation might be more, but cannot be less. The actual limit is available in a GLSL program as the value of a predefined constant, gl_MaxVertexAttribs. More conveniently, it is available on the JavaScript side as the value of the expression
gl.getParameter( gl.MAX_VERTEX_ATTRIBS )
Attribute variables of type float, vec2, vec3, and vec4 all count as one attribute against the limit. For a matrix-valued attribute, each column counts as a separate attribute as far as the limit goes.
Similarly, there are limits on varying variables, and there are separate limits on uniform variables in the vertex and fragment shaders. (The limits are on the number of four-component "vectors." There can be some packing of separate variables into a single vector, but the packing that is used does not have to be optimal. No packing is done for attribute variables.) The limits must satisfy
gl_MaxVertexAttribs >= 8; gl_MaxVertexUniformVectors >= 128; gl_MaxFragmentUniformVectors >= 16; gl_MaxVaryingVectors >= 8;
There are also limits in GLSL on the number of texture units, which means essentially the number of texture images that can be used simultaneously. These limits must satisfy
gl_MaxTextureImageUnits >= 8; // limit for fragment shader gl_MaxVertexTextureImageUnits >= 0; // limit for vertex shader gl_MaxCombinedTextureImageUnits >= 8; // total limit for both shaders
Textures are usually used in fragment shaders, but they can sometimes be useful in vertex shaders. Note however, that gl_MaxVertexTextureImageUnits can be zero, which means that implementations are not required to allow texture units to be used in vertex shaders.
There are also limits on other things, including viewport size, texture image size, line width for line primitives, and point size for the POINTS primitive. All of the limits can be queried from the JavaScript side using gl.getParameter().
The following demo shows the actual values of the resource limits on the device on which you are viewing this page. You can use it to check the capabilities of various devices on which you want your WebGL programs to run. In general, the actual limits will be significantly larger than the required minimum values.