Being derived from OpenGL ES 3.0, WebGL uses the OpenGL shading language (GLSL) for vertex and fragment manipulation in the graphics pipeline. As stated in previous materials, WebGL requires a shader program (consisting of a vertex shader and a fragment shader) every time it is asked to draw something. The GLSL is different from WebGL's JavaScript API, in both its purpose and syntax, and therefore a separate section dedicated to it feels to be appropriate.
It is also worth mentioning that there are different versions of OpenGL, WebGL and GLSL. In fact, WebGL uses OpenGL ES, which is a subset of OpenGL. The two languages are related but not directly compatible. Attention should also be paid to the version of WebGL, since 2.0 is still being implemented into some browsers while 1.0 is already widely supported. The good news is that WebGL2 is nearly 100% backwards compatible with WebGL1. Therefore some older solutions to a problem can still be viable nowadays, eventhough it may not be the most efficient option at this point.
The GLSL itself might not be the most exciting part of WebGL and because of that only basics will be explained in the following paragraphs. You can always refer to Khronos' official WebGL2 reference card and learn more about GLSL on pages 6&7.
GLSL is designed to do math that is commonly needed to compute things for rasterizing graphics. It allows for three basic types of data:
bool
: boolean values (true, false)int
: integer values, a whole number (0, 1, 10, -5, ...)float
: floating point values, a number with at least one decimal (0.0, 0.1, 1.0, -10.8, ...)
GLSL facilitates the manipulation of vectors and matrices. Those are always composed of values of the same basic data type. If the type name starts with b
, it contains boolean values, if it starts with i
it contains integer values, if it starts with anything else it contains floating point values. The vector and matrix data types are:
bvec2
,bvec3
,bvec4
: 2, 3, and 4-component boolean vectorsivec2
,ivec3
,ivec4
: 2, 3, and 4-component integer vectorsvec2
,vec3
,vec4
: 2, 3, and 4-component floating point vectorsmat2
,mat3
,mat4
: 2x2, 3x3, and 4x4 floating point matrices
There are three other specialized data types:
sampler2D
: a reference to aTEXTURE_2D
texture unitsamplerCube
: a reference to aSAMPLER_CUBE
texture unitvoid
: used to identify functions that do not return a value or parameter lists to a function that are empty
Compared to some other programming languages which define the range of values that can be stored in a specific data type, GLSL allows the user to select from three different precisions levels for the basic data types:
- For integers:
lowp
: range of values (-28, 28), specific range -+256mediump
: range of values (-210, 210), specific range -+1024highp
: range of values (-216, 216), specific range -+65536
- For floats:
lowp
: range of values (-2, 2), fractional accuracy 2-8mediump
: range of values (-214, 214), fractional accuracy 2-10highp
: range of values (-262, 262), fractional accuracy 2-16
Individual variables can have different precisions as well as all variables of particular type can have set the same precision:
precision highp int;
precision mediump float;
Integers can be specified in decimal, octal or hexidecimal base on their leading character:
int alpha = 176; // base 10, starts with a non-zero digit
int beta = 0176; // base 8, starts with 0
int gamma = 0x176; // base 16, starts with 0x
Floats are always in base 10, specified using a series of digits that include a decimal point, an exponent or both:
float delta = 1.;
float epsilon = 0.3421;
float phi = 2e4;
float theta = 2.45e-2;
If declared as constant using the const
identifier, the compilar guarantees that the value will not be changed during shader execution:
const float pi = 3.141592653589793;
const int number_lights = 5;
Variable names must start with a letter or underscore, and it can contain letters, digits and underscores. User defined variable names are not allowed to start with gl_
. Variables can be declared with or without an initialization value:
float alpha;
float beta = 5.0;
Variables that are used to pass data between browser and a shader program or between a shader program and object buffers must be designated with a storage qualifier:
uniform
: the variable is assigned a value from the JavaScript code before agl.drawArrays()
is called. The value is accessible in both the vertex and fragment shaderattribute
: the variable is assigned a value from a object buffer as a series of graphics primitives are rendered. It is accessible only in the vertex shadervarying
: the variable is assigned a value by a vertex shader and automatically interpolated across the surface of a graphics primitive before a fragment shader receives it. The value can be read in a fragment shader
A user can create new data types that contain a combination of values:
-
struct
can contain values of different data types:struct my_light { float intensity; vec3 position; vec4 color; };
-
array
requires that all values in it are the same data type. It's size must be a constant and individual elements of an array must be assigned individually:float frequencies[3]; const int numLights = 2; my_light lights[numLights]; frequencies[0] = 0.23; frequencies[1] = 0.67; frequencies[2] = 0.82;
Similar to JavaScript array or objects, an individual element of a vector can be accessed using array [0]
or dotted .a
notation. The names of the vector components are usually x, y, z, w
for geometric data, r, g, b, a
for color data, and s, t, p, q
for texture data. The array notation returns a single component while the dotted notation can return a single component or a new vector depending on the number of field names used:
vec3 alpha = vec3(1.0, 2.0, 3.0);
vec4 a;
vec3 b;
vec2 c;
float d;
b = alpha.xyz; // b is now (1.0, 2.0, 3.0)
d = alpha[2]; // d is now 3.0
a = alpha.xxxx; // a is now (1.0, 1.0, 1.0, 1.0)
c = alhpa.zx; // c is now (3.0, 1.0)
b = alpha.rgb; // b is now (1.0, 2.0, 3.0)
b = alpha.stp; // b is now (1.0, 2.0, 3.0)
a = alpha.yy; // error - trying to assign a 2-component vector to a 4-component vector
Using multiple property names to create a new vector is called swizzle notation. Swizzling can also be used on the left-hand side of an assigment, each field name can only be used once in this case though:
alpha.zxy = vec3(3.0, 4.0, 5.0); // alpha is now (4.0, 5.0, 3.0)
alpha.zx = vec2(10.0, 20.0); // alpha is now (20.0, 5.0, 10.0)
alpha.xx = vec2(10.0, 20.0); // error
alpha.xyz = vec2(10.0, 20.0); // error
You can convert data from one type to another using a conversion a cast, which is a conversion function with the same name as the data type. Since GLSL does not support mixed data types, casting is essential feature:
int a = 123;
float b = float(a) * 0.1;
Same as casts, constructors also have the same name as their associated data types. A call to a constructor creates a value of the indicated data type and requires the correct number of initial values:
vec3 alpha = vec3(1.0, 2.0, 3.0);
vec4 beta = vec4(4.0, 5.0, 6.0, 7.0);
vec3 delta = vec3(alpha.xy, beta.w); // delta is now (1.0, 2.0, 7.0)
vec4 gamma = vec4(alpha[2], beta.rrr); // gamma is now (3.0, 4.0, 4.0, 4.0)
A shader program is composed of one or more functions. Its execution always begins with the main
fucntion which receives no parameters and returns no value:
void main() {
...
}
Custom functions must be defined first before they can be called. A function header defines its name, parameter list and data type of its return value:
vec3 customFunction(float x, bool beta) {
...
}
All parameters are pass by value by default, but there are more parameter qualifiers available:
in
: pass by value - if the parameter's value is changed in the function, the actual parameter from the calling statement is unchangedout
: pass by reference - the parameter is not initialized when the function is called, any changes in the parameter's value change the actual parameter from the calling statementinout
: the parameter's value is initialized by the calling statement and any changes made by the function change the actual parameter from the calling statement
vec3 customFunction(in float x, in bool beta, inout int gamma, out int theta) {
...
}
vec3 phi = customFunction(3.5, true, delta, chi);
// At call: 3.5 is copied into x,
// true is copied into beta,
// delta is copied into gamma,
// chi is not copied into theta
// After the call: the value of delta might be changed,
// the value of chi has changed,
// phi contains the returned value
The classic conditional statements if
, else if
, else
are available in GLSL. Its frequent usage is discouraged though, since it can reduce the ability to execute operations in parallel on 3D graphics processors. All of this is dependent on hardware.
Repeating a group of statements can be done in multiple ways:
for (int j = 0; j < 5; j += 1) {
...
}
int j = 0;
while (j < 5) {
...
j += 1;
}
int j = 0;
do {
...
j += 1;
} while (j < 5);
If the loop control variable is declared in the loop, its scope is limited to the loop. There are many restrictions on the looping constructs:
- There can be only one loop control variable of type int or float
- The initialization of the
for
statement must be of the form oftype-specifier identifier = constant-expression
- The test for loop termination of the
for
statement must have the form ofloop_control_variable relational_operator constant_expression
where the operator is one of the following:>
,>=
,<
,<=
,==
,!=
- The update of the loop control variable in the
for
statement must have the one of the following forms:loop_control_variable++
.loop_control_variable--
,loop_control_variable += constant_expression
,loop_control_variable -= constant_exoression
The flow of control inside a loop can be modified through:
break
: immediately terminates a loop and jumps to the first statement after the loopcontinue
: skips any remaining statements in the loop and jumps to the next iteration in the loopreturn
: immediately exits the current function, terminating the active loop
GLSL is designed for efficient vector and matrix processing. Most of its operators perform standard vector and matrix operations as defined in linear algebra. In cases where an operation is not defined in linear algebra, the operation is typically done component-wise, where the operation is performed on each element of the vector or matrix.
As explained before, GLSL does not perform any automatic casting of data types. The user needs to make sure all of the operands in an expression are the same type.
GLSL only supports square matrices. A vector is treaded as either a row or a column vector whenever it is multiplied by a matrix, whichever makes the operation valid. The user does not have to transpose a vector as he would in normal matrix algebra.
A list of GLSL operators available in precedence order:
b
: boolean (scalar)i
: integer (scalar)f
: float (scalar)bv
: bvec2, bvec3, bvec4 (boolean vector)iv
: ivec2, ivec3, ivec4 (integer vector)v
: vec2, vec3, vec4 (floating point vector)m
: mat2, mat3, mat4 (floating point matrix)
Order | Operator | Description | Examples | Type of operation |
---|---|---|---|---|
1 | () |
Grouping | () |
|
2 | [] |
Array subscription | v[2] |
|
() |
Function call | func() |
||
. |
Field selector, swizzle | v.xyz |
||
++ , -- |
Postfix increment, decrement | i++ , f++ , i-- , f-- |
Scalar | |
iv++ , v++ , m++ , iv-- , v-- , m-- |
Component-wise | |||
3 | ++ , -- |
Prefix increment, decrement | ++i , ++f , --i , --f |
Scalar |
++iv , ++v , ++m , --iv , --v , --m |
Component-wise | |||
+ , - |
Unary positive, negative | +i , +f , -i , -f |
Scalar | |
+iv , +v , +m , -iv , -v , -m |
Component-wise | |||
! |
Boolean negation | !b |
Boolean | |
4 | * |
Multiplication | i * i , f * f |
Scalar |
v * v , iv * iv , i * iv , f * v , f * m |
Component-wise | |||
v * m , m * v , m * m |
Linear algebra | |||
/ |
Division | i / i , f / f |
Scalar | |
v / v , iv / iv , iv / i , v / f , m / f |
Component-wise | |||
% |
Modulus | -Not implemented- | ||
5 | + , - |
Addition, subtraction | i + i , f + f , i - i , f + f |
Scalar |
iv + iv , v + v , m + m , iv - iv , v - v , m - m |
Component-wise | |||
6 | << , >> |
Bitwise shift | -Not implemented- | |
7 | < , > , <= , >= |
Less than, greater than, less than or equal to, greater than or equal to | i < i , f < f , ... |
Boolean |
8 | == , != |
Equality, not equality | i == i , f == f , bv == bv , iv == iv , v == v , m == m , ... |
Boolean |
9 | & |
Bitwise AND | -Not implemented- | |
10 | ^ |
Bitwise EXCLUSIVE OR | -Not implemented- | |
11 | | |
Bitwise INCLUSIVE OR | -Not implemented- | |
12 | && |
Logical AND | b && b |
Boolean |
13 | ^^ |
Logical EXCLUSIVE OR | b ^^ b |
Boolean |
14 | || |
Logical INCLUSIVE OR | b || b |
Boolean |
15 | ? : |
Selection | b ? n : m |
Component-wise |
16 | = |
Assignment | b = b |
Boolean |
i = i , f = f |
Scalar | |||
bv = bv , iv = iv , v = v |
Vector | |||
m = m |
Matrix | |||
+= , -= |
Addition, subtraction assignment | i += i , f += f , ... |
Scalar | |
iv += iv , v += v , m += m , ... |
Component-wise | |||
+= , /= |
Multiplication, division assignment | i += i , f += f , ... |
Scalar | |
v *= v , iv *= iv , v *= f , iv *= i , m *= f |
Component-wise | |||
17 | , |
Sequence |
GLSL provides a list of built-in functions which would be too long to go over in here. You can find them all on pages 7&8 of WebGL2 Reference Card. In brief it is worth mentioning though, that the majority of the functions only work on floating point scalars or vectors.
Shader programs communicate with the graphics pipeline using predefined input and output variables.
A vertex shader has two outputs:
gl_Position
: avec4
position of a vertex in clip coordinatesgl_PointSize
: a float value of pixels to use to render a point, applied only to the rendering of single points, not vertices of lines and triangles
Inputs to a fragment shader are following:
gl_FragCoord
: avec4
value that holds (x, y, z, w) value of the fragment- This is the value of
gl_Position
after transformation by the viewport transofrm and the perspective divide
- This is the value of
gl_FrontFacing
: a boolean value that is true if this fragment is part of a front-facing primitive- This only applies to triangles, determined on whether its normal vector is pointing toward the camera
gl_PointCoord
:vec2
value indicating the relative location of the fragment within the renedering of a point
A WebGL fragment shader has one single output:
gl_FragColor
: avec4
RGBA value placed into color buffer for the fragment is is processing
GLSL programs are compiled using functionality in the WebGL API. If an error occurs during the compilation, it can be captured and displayed. It is important not to pollute the GPU memory with invalid or unused object since the GPU does not do automatic garbage collection. A shader should be therefore returned only if it is valid:
const createShader = (gl, type, source) => {
const shader = gl.createShader(type);
gl.shaderSource(shader, source);
gl.compileShader(shader);
const success = gl.getShaderParameter(shader, gl.COMPILE_STATUS);
if (success) {
return shader;
}
console.log(gl.getShaderInfoLog(shader));
gl.deleteShader(shader)
};
Linking vertex and fragment shaders into a single program makes sure that both reference to the same global variables. Again, only valid objects should be returned:
const createProgram = (gl, vertexShader, fragmentShader) => {
const program = gl.createProgram();
gl.attachShader(program, vertexShader);
gl.attachShader(program, fragmentShader);
gl.linkProgram(program);
const success = gl.getProgramParameter(program, gl.LINK_STATUS);
if (success) {
return program;
}
console.log(gl.getProgramInfoLog(program));
gl.deleteProgram(program);
};
Since shader programs are just strings of text before being compiled, it is possible to create shader programs on the fly. You can have a set of strings that define various shader commands and combine those in different ways to create a specific shader for a specific rendering situations. While this concept might be very powerful, it is also quite advanced and beyond scope of this material.
Title | Author | Link |
---|---|---|
WebGL2 from WebGL1 | Greggman | Link |
WebGL Shader Language | Dr. Wayne Brown | Link |
WebGL2 Reference Card | Khronos | Link |