## Using shaders to accelerate WebGL

The WebGL matrix libraries for JavaScript are all trying to satisfy the needs of WebGL developers. The matrix libs needs to be really well performing since they will be handling all the heavy calculations in the rendering loop.

By using the GLSL shaders, we can just skip the matrix operations in the rendering loop and let the GPU work for us instead. This will for sure relax your JavaScript. In this post I'll show you a simple example on how to move translation operations to the vertex shader instead of using JavaScript matrix operations.

The graph above shows three lines, "WebGL", "WebGL + optimization" and "OSG (C++)". The first corresponds to the usual rendering procedure with matrix operations in JavaScript context, the second one is after the translation operations were moved to the shader (and other tricks applied, see the section "more tricks" below). The third is for comparison, the corresponding C++ results using OpenSceneGraph.

# The JavaScript way

First, I'll show you a quick example on how to do this "the old way". Your vertex shader may look like this. What it does is using projection matrices to transform **aVertexPosition** to some other desired position **gl_Position** in the world.

attribute vec4 aVertexColor;

uniform mat4 uMVMatrix;

uniform mat4 uPMatrix;

varying vec4 vColor;

void main(void) {

gl_Position = uPMatrix * uMVMatrix * vec4(aVertexPosition,

1.0);

vColor = aVertexColor;

}

Your render procedure may look like below (note: I have used glMatrix.js for matrix transformations):

gl.viewport(0, 0, viewportWidth, viewportHeight);

gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);

mat4.perspective(45, viewportWidth / viewportHeight, 0.1, 100.0, pMatrix);

mat4.identity(mvMatrix);

mat4.translate(mvMatrix, [cam.x, cam.y, cam.z]);

// Translate the render object

mvPushMatrix();

mat4.translate(mvMatrix, position);

// Bind buffers

gl.bindBuffer(gl.ARRAY_BUFFER, vertexPositionBuffer);

gl.vertexAttribPointer(shaderProgram.vertexPositionAttribute, vertexPositionBuffer.itemSize, gl.FLOAT, false, 0, 0);

gl.bindBuffer(gl.ARRAY_BUFFER, vertexColorBuffer);

gl.vertexAttribPointer(shaderProgram.vertexColorAttribute, vertexColorBuffer.itemSize, gl.FLOAT, false, 0, 0);

gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, vertexIndexBuffer);

setMatrixUniforms();

// Draw

gl.drawElements(gl.TRIANGLES, vertexIndexBuffer.numItems, gl.UNSIGNED_SHORT, 0);

mvPopMatrix();

This approach is heavy for our browser. For each frame, we have to do many matrix operations including

- push
- translate
- rotate
- pop
- etc

These operations are simple but expensive when doing them in JavaScript context. The thing is that we can put these operations in the shader if we want to.

# The shader way

We want to move the translation part (mat4.translate()) to the shader. In JavaScript, we know the position of the object we want to render. Before, we provided this position in the modelview matrix so the shader could render it in the right place. Now we are going to pass the "raw" position data to the shader and skip the JavaScript matrix translating operation.

Begin with defining a new Uniform in the shader called **uPosition**. Also, add this vector when calculating **gl_Position** (this will translate our rendered model). See code below.

attribute vec4 aVertexColor;

uniform mat4 uMVMatrix;

uniform mat4 uPMatrix;

uniform vec3 uPosition;

varying vec4 vColor;

void main(void){

gl_Position = uPMatrix * uMVMatrix * vec4(aVertexPosition

+ uPosition,

1.0);

vColor = aVertexColor;

}

If we can change the value of this **uPosition** from the JavaScript context, we can translate our rendering object however we want. I can tell you that this procedure is already built into WebGL. This is the usage:

I assume you already know how to get the uniform location. It's the same procedure as with the projection matrices.

Our optimized rendering procedure code is shown below.

gl.viewport(0, 0, viewportWidth, viewportHeight);

gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);

mat4.perspective(45, viewportWidth / viewportHeight, 0.1, 100.0, pMatrix);

mat4.identity(mvMatrix);

mat4.translate(mvMatrix, [cam.x, cam.y, cam.z]);

// Send translation data to the shader

gl.uniform3f(shaderProgram.positionUniform,

position[0],

position[1],

position[2]);

// Bind buffers

// [See last code snippet]

setMatrixUniforms();

// Draw

gl.drawElements(gl.TRIANGLES, vertexIndexBuffer.numItems, gl.UNSIGNED_SHORT, 0);

That's it! We got rid of translate()! And since push() and pop() are not needed anymore they could also be removed. Note that almost all matrix operations in the rendering loop can be moved to the vertex shader. It is just a matter of GLSL coding.

# More tricks

You can get even more performance if you want. For example

- Rotation operations can be moved to the shader in the same way as the translation, e.g. by using quaternions.
- Camera operations can also be moved to the shader. Camera position, field of view etc can be sent to the shader using uniforms.
- If you are going to render the same object many times, you only have to bind the buffers once and reuse them for all render objects. This is called pseudo-instancing and saves a lot of memory and increases performance.

If you are running a scene with 1000 cubes in motion and use all tricks I have explained, then you may only need these lines in your render procedure:

gl.uniform3f(camPosUniform, camx, camy, camz); // ...and similar for other cam settings

for(var i=0; i<1000; i++){

gl.uniform3f(positionUniform, x[i], y[i], z[i]);

gl.uniform3f(rotationUniform, q[i][0], q[i][1], q[i][2], q[i][3]);

gl.drawElements(...);

}

# Conclusions

By using the shader approach to WebGL, we can get more performance, even close to C++ performance.

Brandon JonesApril 19th, 2012 - 21:57

This is a great optimization to use in many cases, but a small caveat to be aware of: Any time you do this type of calculation in a shader you are repeating the math once per vertex, whereas when you do in in Javascript you’re doing it once per mesh. In many cases the pre-vertex case will still win (usually by a large margin) because the GPUs are so much faster than the Javascript VM, but for complex meshes on lower-power GPUs (mobile devices) you may actually find a case where the Javascript variant is faster.

For most desktops this will never be a problem and as such this is a perfectly valid technique for > 90% of the current market, but it’s something to be aware of nonetheless. The real answer, as always, is to benchmark your own apps and see where the bottlenecks actually are!

kyleApril 22nd, 2012 - 14:49

Apparently, its better to do wasteful computations on GPU then touch Javascript. If anything, this optimization just shows how crippled the technology is, too bad chances for getting something better are pretty slim.

TimFebruary 16th, 2014 - 14:02

Nice article! I have one question on how to compute the normal matrix when moving the rotation to the shader. At the moment I’m computing the normal matrix on the JS side using the mvMatrix. If I would like to compute the rotations in the shader, how would I compute the normal matrix? If I still had to compute a new mvMatrix just for computing the normal matrix afterwards, I guess I wouldn’t get that much more performance. And computing the normal matrix for every vertex in the shader is probably not the solution. Do you have any hint for me on you to do this?

wanhongboFebruary 25th, 2014 - 03:06

I think you can try to use the webcl to compute the normal matrix,the webcl program is also execute in the GPU.