JavaScript Canvas - WebGL 3D
Written by Ian Elliot   
Monday, 06 December 2021
Article Index
JavaScript Canvas - WebGL 3D
Vertex Shader
Fragment Shader
Connecting With Shaders
Vertex Data

Fragment Shader

The most basic shape in WebGL is the triangle. The vertex shader works out where the points in the buffer are to be plotted on the canvas. Usually groups of three points are taken to define a triangle, the interior of which is to be colored by the fragment shader. This means that when the fragment shader is called it works out the colors to be assigned to each of the pixels within each of the triangles. This can be a sophisticated calculation that involves the colors assigned to each of the vertices and the angle that the 3D fragment makes with a given direction – this is how lighting effects are added. You can also specify bitmaps which are processed by the shader to render a texture on the fragment, see the next chapter. For our first example, however, the simplest possible fragment shader just assigns a constant color:

void main(void) {
  gl_FragColor = vec4(0.0, 1.0, 0.0, 1.0);
}

Setting the standard variable gl_FragColor to an RGBA value sets every pixel within the triangle to that color. In this case the color is green. Now that gl_FragColor has been deprecated in GLSL ES 3.0, you should use your own variable defined using:

#version 300 es\n
 precision mediump float;
 out vec4 outColor;
 void main(void) {
   outColor= vec4(0.0, 1.0, 0.0, 1.0);
 }

Although triangles are the fundamental fragment in WebGL, you can also use lines and points and how these are handled is slightly different. But for most 3D graphics, the triangle is the workhorse.

Shader practice

Now we have our two shaders how do we get them into the WebGL object? The answer to this question is specific to JavaScript and WebGL as we have to write the shaders within JavaScript and somehow load them into the GPU.

The shader code is first stored as a string in a suitable variable. It is then stored in a shader object of the correct type and compiled. This has to be done for both the vertex and fragment shader. Then the two shaders are combined into a single program that the GPU can run to render our 3D model.

So starting with the vertex shader, first we have to store the code into a string:

var vsScript = `attribute vec3 vertexPosition;
                uniform mat4 modelViewMatrix;
                uniform mat4 perspectiveMatrix;
                void main(void) {
                  gl_Position = perspectiveMatrix * 
           modelViewMatrix * vec4(vertexPosition, 1.0);
                }`;

Notice that we are using a template string, which lets us write a multi-line string, and the “quotes” are back-ticks or grave accent characters.

The fragment shader, using a string template, is simply:

var fsScript = `void main(void) {
               gl_FragColor = vec4(0.0, 1.0, 0.0, 1.0);
                }`;

Now we have both shaders as strings, we have to create a shader object of the correct type from each string:

var vertexShader = gl.createShader( gl.VERTEX_SHADER);

Next add it with the code to the WebGL object and compile the shader:

gl.shaderSource(vertexShader, vsScript);
gl.compileShader(vertexShader);

As long as there are no syntax errors in the code, we now have a compiled shader ready to be used. However, syntax errors are common so we need to check that it worked and print any errors to the console:

if(!gl.getShaderParameter(vertexShader,
gl.COMPILE_STATUS)) { alert("Error in vertex shader"); var compilationLog = gl.getShaderInfoLog(vertexShader); console.log('Shader compiler log: ' + compilationLog); gl.deleteShader(vertexShader); }

We have to repeat the whole thing over again to enter and compile the fragment shader. Rather than splitting the steps down, it is more reasonable simply to present the code that creates both shaders in a function which will be used to compile both shaders from now on:

function createShaders(gl, vs, fs) {
  var vertexShader = gl.createShader(gl.VERTEX_SHADER);
  gl.shaderSource(vertexShader, vs);
  gl.compileShader(vertexShader);
  if (!gl.getShaderParameter(vertexShader, 
gl.COMPILE_STATUS)) { alert("Error in vertex shader"); var compilationLog =
gl.getShaderInfoLog(vertexShader); console.log('Shader compiler log: ' +
compilationLog); gl.deleteShader(vertexShader); return; } var fragmentShader =
gl.createShader(gl.FRAGMENT_SHADER); gl.shaderSource(fragmentShader, fs); gl.compileShader(fragmentShader); if (!gl.getShaderParameter(fragmentShader,
gl.COMPILE_STATUS)) { alert("error in fragment shader"); var compilationLog =
gl.getShaderInfoLog(fragmentShader); console.log('Shader compiler log: ' +
compilationLog); gl.deleteShader(fragmentShader); return; } return [vertexShader, fragmentShader]; }

This function accepts the drawing context and two strings that define the vertex and fragment shader. It returns an array with the first element (0) the vertexShader and the second (1) the fragmentShader.

Now we are almost done with the shaders. All that remains is to link them together into a single program that the GPU can run. This always follows the same steps. First create a program object, attach the compiled shaders to it and then link them together. It makes sense to write a function that does the job using the array of shaders returned by the previous function:

function createProgram(gl,shaders) {
  var program = gl.createProgram();
  gl.attachShader(program, shaders[0]);
  gl.attachShader(program, shaders[1]);
  gl.linkProgram(program);
  if (!gl.getProgramParameter(
program, gl.LINK_STATUS)) { alert("Error in shaders"); gl.deleteProgram(program); gl.deleteProgram(vertexShader); gl.deleteProgram(fragmentShader); return; } return program; }

Finally we have to tell the GPU to use the program:

gl.useProgram(program);

Setting up the shaders always follows these fairly tedious steps and you can mostly forget how these function do their jobs. You always define and create two shaders and use these to construct the GPU’s program object. In more advanced uses you can define multiple shaders and multiple programs. You can tell the GPU to use a given program before you render an object, so varying how the object is rendered.



Last Updated ( Monday, 06 December 2021 )