Browser Physics Using the new web standards to produce physics

7Sep/114

Using shaders to accelerate WebGL

webglopt

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 vec3 aVertexPosition;
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):

// display + camera settings etc
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 vec3 aVertexPosition;
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:

gl.uniform3f(myUniformLocation, x, y, z);

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.

// display + camera settings etc
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.clear(...);
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.

Comments (4) Trackbacks (3)
  1. 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!

  2. 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.

  3. 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?


Leave a comment


*