WebGL Essentials

Vertex Array Elements Lookup

The drawArray() method of the WebGL context is the main routine for creating geometric primitives from a sequence of vertex array elements. The method, however, causes the redundant processing of vertex data when a single vertex is used to define dimensions of two or more primitives. To skip unnecessary array entries, the drawElements() command should be employed: it deals with indices pointing into vertex arrays. Element buffer objects are initialized similar to vertex buffer objects and are bound to the ELEMENT_ARRAY_BUFFER target.

The example below will demonstrate the use of the drawElements() method while building a pyramid of four triangles. The pyramid will rotate about the y axis. Clicking the canvas will stop the animation effect.

Vertex and Fragment Shader

Our vertex shader is based on the GLSL code from the previous articles:

<script id="vertex-shader" type="x-shader/x-vertex">
 attribute vec3 position;
 varying highp vec4 color;
 mat4 projectionMatrix;
 mat4 viewMatrix;
 mat4 modelMatrix;
 uniform float angle;
 uniform float l, r, b, t, n, f;
 float m11, m12, m13, m14, m21, m22, m23, m24, m31, m32, m33, m34, m41, m42, m43, m44;

 // function declarations
 mat4 computeProjectionMatrix();
 mat4 computeViewMatrix();
 mat4 computeModelMatrix();

 void main() {
  projectionMatrix = computeProjectionMatrix();
  viewMatrix = computeViewMatrix();
  modelMatrix = computeModelMatrix();

  if(position.y == 1.0) {
   color = vec4(1.0, 1.0, 1.0, 1.0);
  } else {
   color = vec4(0.2, 0.39, 1.0, 1.0);
  }

  gl_Position = projectionMatrix * viewMatrix * modelMatrix * vec4(position, 1.0);
 }

 . . . function definitions . . .

</script>

The shader does not receive color values from JavaScript any more: instead, it specifies them directly depending on the vertex position. The OpenGL Shading Language supports conditional control flow, so the shader uses if/else to define two vectors containing red, green, blue and alpha components: the top of the pyramid (y = 1.0) will be white, and the rest of the object will have a hue of the blue color.

The transformation matrices are calculated in the vertex shader. The shader has seven uniforms: these are the angle of rotation and parameters of the viewing volume. To reduce the size of the shader, all matrix entries are declared as global variables.

The computeProjectionMatrix() function works out the viewing volume of the scene:

mat4 computeProjectionMatrix() {
 m11 = (2.0 * n) / (r - l);
 m12 = 0.0;
 m13 = 0.0;
 m14 = 0.0;

 m21 = 0.0;
 m22 = (2.0 * n) / (t - b);
 m23 = 0.0;
 m24 = 0.0;

 m31 = (r + l) / (r - l);
 m32 = (t + b) / (t - b);
 m33 = - (f + n) / (f - n);
 m34 = -1.0;

 m41 = 0.0;
 m42 = 0.0;
 m43 = - (2.0 * f * n) / (f - n);
 m44 = 0.0;

 return mat4(
  vec4(m11, m12, m13, m14),
  vec4(m21, m22, m23, m24),
  vec4(m31, m32, m33, m34),
  vec4(m41, m42, m43, m44)
 );
}

The viewing transformation both pushes the pyramid down the z axis and stretches it uniformly by the x and y factors:

mat4 computeViewMatrix() {
 float x, y, z;
 x = 0.0;
 y = 0.0;
 z = -3.0;

 m11 = 2.0;
 m12 = 0.0;
 m13 = 0.0;
 m14 = 0.0;

 m21 = 0.0;
 m22 = 2.0;
 m23 = 0.0;
 m24 = 0.0;

 m31 = 0.0;
 m32 = 0.0;
 m33 = 1.0;
 m34 = 0.0;

 m41 = x;
 m42 = y;
 m43 = z;
 m44 = 1.0;

 return mat4(
  vec4(m11, m12, m13, m14),
  vec4(m21, m22, m23, m24),
  vec4(m31, m32, m33, m34),
  vec4(m41, m42, m43, m44)
 );
}

The world coordinates of the pyramid are based on rotation about the ray from the scene origin through the point (0, 1, 0):

mat4 computeModelMatrix() {
 m11 = cos(radians(angle));
 m12 = 0.0;
 m13 = -sin(radians(angle));
 m14 = 0.0;

 m21 = 0.0;
 m22 = 1.0;
 m23 = 0.0;
 m24 = 0.0;

 m31 = sin(radians(angle));
 m32 = 0.0;
 m33 = cos(radians(angle));
 m34 = 0.0;

 m41 = 0.0;
 m42 = 0.0;
 m43 = 0.0;
 m44 = 1.0;

 return mat4(
  vec4(m11, m12, m13, m14),
  vec4(m21, m22, m23, m24),
  vec4(m31, m32, m33, m34),
  vec4(m41, m42, m43, m44)
 );
}

As the color variable is declared as a vector of four elements (vec4), the fragment shader applies it to fragments without further conversions:

<script id="fragment-shader" type="x-shader/x-fragment">
 varying highp vec4 color;
 void main() {
  gl_FragColor = color;
 }
</script>

Array Indices in Buffer Objects

The client script initializing the WebGL context and compiling the shaders is similar to JavaScript code from the earlier example. This time, however, a new global variable for holding an element buffer object is declared:

var gl;
var vs, fs, program;
var vbo;
var ebo;
var handle;

function createWebGLContext() {
 var canvas = document.querySelector('canvas');
 gl = canvas.getContext('webgl') || canvas.getContext('experimental-webgl');
 if(gl == null) {
  console.error('WebGL is not supported by the browser');
 } else {
  compileVertexShader();
 }
}

function compileVertexShader() {
 var vsSource = document.querySelector('#vertex-shader').textContent;
 vs = gl.createShader(gl.VERTEX_SHADER);
 gl.shaderSource(vs, vsSource);
 gl.compileShader(vs);
 if(gl.getShaderParameter(vs, gl.COMPILE_STATUS) == true) {
  console.info('Vertex shader has been compiled successfully');
  compileFragmentShader();
 } else {
  console.log('%cVertex shader compilation failed:\n%s', 'color: red' , gl.getShaderInfoLog(vs));
 }
}

function compileFragmentShader() {
 var fsSource = document.querySelector('#fragment-shader').textContent;
 fs = gl.createShader(gl.FRAGMENT_SHADER);
 gl.shaderSource(fs, fsSource);
 gl.compileShader(fs);
 if(gl.getShaderParameter(fs, gl.COMPILE_STATUS) == true) {
  console.info('Fragment shader has been compiled successfully');
  createProgram();
 } else {
  console.log('%cFragment shader compilation failed:\n%s', 'color: red' , gl.getShaderInfoLog(fs));
 }
}

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

In the createVertexBufferObject() function the primary array of vertices is declared without colors and redundant coordinate values. Besides, the function creates an array of indices pointing to the elements that should be selected to build 4 surfaces of the pyramid. Two buffer objects hold vertex array elements and their indices in the GPU memory:

function createVertexBufferObject() {
 var vertices = [
  0.0, 1.0, 0.0,
  -1.0, -1.0, 1.0,
  1.0, -1.0, 1.0,
  1.0, -1.0, -1.0,
  -1.0, -1.0, -1.0,
  -1.0, -1.0, 1.0
 ];

 var indices = [
  0, 1, 2,
  0, 2, 3,
  0, 3, 4,
  0, 4, 5
 ];

 var elementSize = Float32Array.BYTES_PER_ELEMENT;

 var vertexArray = new Float32Array(vertices);
 var indexArray = new Uint8Array(indices);

 vbo = gl.createBuffer();
 ebo = gl.createBuffer();

 gl.bindBuffer(gl.ARRAY_BUFFER, vbo);
 gl.bufferData(gl.ARRAY_BUFFER, vertexArray, gl.STATIC_DRAW);

 gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, ebo);
 gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, indexArray, gl.STATIC_DRAW);

 var position = gl.getAttribLocation(program, 'position');
 gl.vertexAttribPointer(position, 3, gl.FLOAT, false, 3 * elementSize, 0);
 gl.enableVertexAttribArray(position);

 gl.bindBuffer(gl.ARRAY_BUFFER, null);

 if(gl.getError() == gl.NO_ERROR) {
  prepareScene();
 } else {
  console.error('An error occurred while creating a vertex buffer object');
 }
}

Aspect Ratio

The prepareScene() function declares values for clearing the canvas:

gl.clearColor(0.0, 0.0, 0.0, 0.0);
gl.clearDepth(1.0);
gl.enable(gl.DEPTH_TEST);
gl.depthFunc(gl.LEQUAL);

Another preliminary step is the attaching of the anonymous click handler to the canvas. The event listener implemented as an arrow function stops animation:

gl.canvas.addEventListener('click', () => {
 if (handle != undefined) {
  cancelAnimationFrame(handle);
 }
}, false);

The initial value of the angle of rotation is 0.0:

gl.uniform1f(gl.getUniformLocation(program, 'angle'), 0.0);

Volume parameters required to compute projection matrix are uniform variables, so they are loaded from JavaScript:

if(gl.drawingBufferWidth <= gl.drawingBufferHeight) {
 gl.uniform1f(gl.getUniformLocation(program, 'l'), -1.5);
 gl.uniform1f(gl.getUniformLocation(program, 'r'), 1.5);
 gl.uniform1f(gl.getUniformLocation(program, 'b'), -1.5 * gl.drawingBufferHeight/gl.drawingBufferWidth);
 gl.uniform1f(gl.getUniformLocation(program, 't'), 1.5 * gl.drawingBufferHeight/gl.drawingBufferWidth);
} else {
 gl.uniform1f(gl.getUniformLocation(program, 'l'), -1.5 * gl.drawingBufferWidth/gl.drawingBufferHeight);
 gl.uniform1f(gl.getUniformLocation(program, 'r'), 1.5 * gl.drawingBufferWidth/gl.drawingBufferHeight);
 gl.uniform1f(gl.getUniformLocation(program, 'b'), -1.5);
 gl.uniform1f(gl.getUniformLocation(program, 't'), 1.5);
}

gl.uniform1f(gl.getUniformLocation(program, 'n'), 1.0);
gl.uniform1f(gl.getUniformLocation(program, 'f'), 6.0);

Using the aspect ratio of a 3D scene in a way illustrated above allows the application to preserve dimensions of three-dimensional objects even if the canvas size is changed.

Now that the scene has been prepared, the first animation frame can be requested:

handle = requestAnimationFrame(renderScene);

Rendering 3D Scene

The drawElements() command builds the pyramid by successively transferring 12 array elements to the graphics engine:

function renderScene() {
 gl.clear(gl.COLOR_BUFFER_BIT | gl.GL_DEPTH_BUFFER_BIT);
 gl.drawElements(gl.TRIANGLE_FAN, 12, gl.UNSIGNED_BYTE, 0);
 var angle = gl.getUniform(program, gl.getUniformLocation(program, 'angle'));
 angle = angle + 1.1;
 if (angle > 360.0) {
  angle = angle - 360.0;
 }
 gl.uniform1f(gl.getUniformLocation(program, 'angle'), angle);
 handle = requestAnimationFrame(renderScene);
}

The drawElements() method has the following parameters:

  • mode determining the type of a primitive to be drawn;
  • count - the number of elements to be rendered;
  • type - the data type of the values in element indices; valid types are UNSIGNED_BYTE and UNSIGNED_SHORT;
  • offset - an offset within the array of indices.

If the canvas has a background image, rotation of the pyramid against the background might be rendered as