WebGL Essentials

Vertex and Fragment Shaders

The first version of OpenGL was based on immediate mode rendering and the Begin/End paradigm: an application had to issue lots of commands running on the CPU to create even a simple 3D scene. Vertices, colors, texture coordinates and normals were specified for geometric objects between Begin/End pairs:

glBegin(GL_TRIANGLES);
 glColor3f(1.0f, 0.0f, 0.0f);
 glVertex3f(-1.0f, -1.0f, 0.0f);
 glColor3f(0.0f, 1.0f, 0.0f);
 glVertex3f(1.0f, -1.0f, 0.0f);
 glColor3f(0.0f, 0.0f, 1.0f);
 glVertex3f(0.0f, 1.0f, 0.0f);
glEnd();

To solve the problem of the redundant vertex processing, display lists and client vertex arrays were proposed. However, these techniques can not improve application performance in a highly structured 3D scene: passing large blocks of client data from the system RAM to the GPU to draw every single frame has a negative impact on 3D rendering and makes an application too slow.

Evolution of GPU hardware allowed OpenGL to divide the graphics workload properly between the client and the server. OpenGL 3.1 removed a lot of immediate mode functions: instead, vertex buffer objects residing in the GPU memory are generated to initialize vertex data, and GPU shaders are used to process the data and draw primitives.

A shader is a small program written in GLSL - OpenGL ES Shading Language resembling C/C++. In a typical scenario for a Web application, a vertex shader consumes array values provided by the JavaScript code and prepares vertices for rasterization. The rasterizer produces a series of framebuffer addresses and values. Each fragment resulting from rasterization is passed to the fragment shader. Shaders are compiled and uploaded to the graphics hardware.

Vertex Shader

A number of elementary GLSL constructs are shown below:

<script id="vertex-shader" type="x-shader/x-vertex">
 attribute vec3 position;
 void main() {
  gl_Position = vec4(position, 1.0);
 }
</script>

The shader code in the example is embedded in the Web page as the text content of the <script> element with the MIME type different from ECMAScript/JavaScript.

In the attribute vec3 position code line the attribute is a storage qualifier for the position variable. Attribute variables are used to hold data received from JavaScript. The vec3 is the data type of the variable: this is a three component floating-point vector.

The main() function is the entry point to the shader. The function cannot be overloaded.

The gl_Position = vec4(position, 1.0) assignment sets the value of the built-in gl_Position variable as a four-component vector. The vec4() vector constructor will consume x, y and z coordinates from the attribute variable. The w coordinate is default (1.0).

Fragment Shader

The fragment shader below is also represented as an inline script. The GLSL code uses the built-in gl_FragColor variable to apply the deepskyblue CSS color to each fragment. The value of the variable is a four-component vector:

<script id="fragment-shader" type="x-shader/x-fragment">
 void main() {
  gl_FragColor = vec4(0.0, 0.75, 1.0, 1.0);
 }
</script>

Compiling Shader Source

The createShader() command invoked with the appropriate shader type instantiates a shader object in JavaScript:

var canvas = document.querySelector('canvas');
var gl = canvas.getContext('webgl');

var vsSource = document.querySelector('#vertex-shader').textContent;
var fsSource = document.querySelector('#fragment-shader').textContent;

var vs = gl.createShader(gl.VERTEX_SHADER);
var fs = gl.createShader(gl.FRAGMENT_SHADER);

Shader objects are initially empty. The shaderSource() method of the WebGLRenderingContext loads the GLSL code into the shaders:

gl.shaderSource(vs, vsSource);
gl.shaderSource(fs, fsSource);

Once the GLSL code has been loaded, shader objects can be compiled:

gl.compileShader(vs);
gl.compileShader(fs);

Program Object

Shader objects are attached to a program:

var program = gl.createProgram();
gl.attachShader(program, vs);
gl.attachShader(program, fs);

The program object must be linked:

gl.linkProgram(program);

If neither compilation nor linking has raised errors, the program can be used for rendering:

gl.useProgram(program);

The validateProgram() is an auxiliary command validating the program object against the current WebGL state:

gl.validateProgram(program);

When an application does not need a program or a shader any more, it can dispose of them to release system resources:

gl.detachShader(program, vs);
gl.deleteShader(vs);

gl.detachShader(program, fs);
gl.deleteShader(fs);

gl.deleteProgram(program);

Shader and Program Queries

Compilation and linking are monitored with the help of functions requesting information about shaders and programs. The most straightforward way to check the validity of the objects is to call the isShader() and isProgram():

console.log(gl.isShader(vs));
console.log(gl.isProgram(program));

The GLSL code is listed by invoking the getShaderSource():

console.log('%c%s', 'color: black', gl.getShaderSource(vs));

The range and precision used by the GLSL compiler to store floating point and integer variables are retrieved by the getShaderPrecisionFormat():

// WebGLShaderPrecisionFormat object: rangeMin is 127, rangeMax is 127, precision is 23
console.log(gl.getShaderPrecisionFormat(gl.VERTEX_SHADER, gl.HIGH_FLOAT));

The getAttachedShaders() command returns an array of WebGLShader objects attached to the program:

console.log(gl.getAttachedShaders(program).length);

More detailed information about shaders is obtained through the use of the getShaderParameter() method:

// boolean comparison returns true
console.log(gl.getShaderParameter(vs, gl.SHADER_TYPE) == gl.VERTEX_SHADER);

// if there are no compilation errors, the command returns true
console.log(gl.getShaderParameter(vs, gl.COMPILE_STATUS));

// if the shader is not flagged for deletion, the command returns false
console.log(gl.getShaderParameter(vs, gl.DELETE_STATUS));

The getShaderInfoLog() method of the WebGLRenderingContext allows the developer to create custom handlers of compilation errors:

if(gl.getShaderParameter(vs, gl.COMPILE_STATUS) == true) {
 console.info('Vertex shader has been compiled successfully');
} else {
 console.log('%cVertex shader compilation failed:\n%s', 'color: red' , gl.getShaderInfoLog(vs));
}

Similar functionality for program queries is provided by the getProgramParameter() and getProgramInfoLog() functions:

// 1: the program has one attribute variable
console.log(gl.getProgramParameter(program, gl.ACTIVE_ATTRIBUTES));

// 0: there are no variables with the uniform storage qualifier
console.log(gl.getProgramParameter(program, gl.ACTIVE_UNIFORMS));

// 2: two shaders have been attached to the program
console.log(gl.getProgramParameter(program, gl.ATTACHED_SHADERS));

// false if the program is not flagged for deletion
console.log(gl.getProgramParameter(program, gl.DELETE_STATUS));

if(gl.getProgramParameter(program, gl.LINK_STATUS) == true) {
 console.info('A valid program executable is created');
} else {
 console.log('%cProgram linking failed:\n%s', 'color: red' , gl.getProgramInfoLog(program));
}

. . . issuing commands to manage WebGL state . . .

gl.validateProgram(program);
if(gl.getProgramParameter(program, gl.VALIDATE_STATUS) == true) {
 console.info('Given the current WebGL state, the program object is guaranteed to execute');
} else {
 console.log('Program validation failed');
}