WebGL Basics 3 – Rotating triangle

We continue this pure WebGL tutorial with animation.

Introduction

The previous post was dedicated to all the function calls necessary to render a simple triangle. In this post, we will animate this triangle and make it rotate around the three axes. The web page used in the previous post must be changed as following:

  • Addition of a mechanism to periodically refresh the canvas and manually control the rotation
  • Modification of the shader code to include a matrix-based transformation of vertices
  • Creation of a function used to pre-compute the transformation matrix

The part about the transformation matrix necessitates some basic linear algebra but should be understandable with high-school level mathematics knowledge.

Refactoring the code

First of all, in order to make the program more readable, we move some code around. The code of the shaders is placed at the beginning of the Javascript part:

// GLSL ES code to be compiled as fragment shader
fragmentShaderCode=
'void main(void) {'+
'  gl_FragColor = vec4(1.0, 1.0, 1.0, 1.0);'+
'}';

// GLSL ES code to be compiled as vertex shader
vertexShaderCode=
'attribute vec2 ppos;'+
'void main(void) {'+
'  gl_Position = vec4(ppos.x, ppos.y, 0.0, 1.0);'+
'}';

Note that the vertex shader code above is not modified compared to the previous post, just re-arranged. It will be modified in the next section.

The second step is to separate the initialization code from the code called at each refresh. Hence the new method draw() is created and filled with the end of the start() function. The new global flag running activates or deactivates the refresh commands (the control of this flag is described in a section below) and we define the variable program as a global one:

var running = true; // True when the canvas is periodically refreshed
var program; // The program object used in the GL context

// Function called periodically to draw the scene
function draw()
{
  // Tests if canvas should be refreshed
  if(!running || !gl)
    return;

  // Sets clear color to non-transparent dark blue and clears context
  gl.clearColor(0.0, 0.0, 0.5, 1.0);
  gl.clear(gl.COLOR_BUFFER_BIT);

  // Draws the object
  gl.drawArrays(gl.TRIANGLES, 0, 3);
  gl.flush();
}

Periodic Refresh

The draw() function is now prepared. At the end of start(), the standard Javascript function setInterval is used to indicate to the browser that the function draw() must be called periodically:

  // The function draw() will be called every 40 ms
  setInterval('draw();', 40);  

Using setInterval is not a very good solution for animation because this function is completely independent of the refresh cycles of the screen (no smooth animation). Furthermore, it is called even if the page or tab is not currently visible, and a "best-guess" value for the delay in milliseconds must be set. There is an alternative to setInterval called requestAnimationFrame, but it is still in a draft stage and we won’t use it for now (it maybe addressed in a future post).

Matrix multiplication in shader

The next change is in the vertex shader. We add a transformation implemented as a multiplication of a 4×4 floats matrix (type "mat4" in GLSL) by the 4D homogeneous coordinates (x, y, z, 1) of each vertex. The matrix is given in the variable named mvp:

// GLSL ES code to be compiled as vertex shader
vertexShaderCode=
'attribute vec2 ppos;'+
'uniform mat4 mvp;'+
'void main(void) {'+
'  gl_Position = mvp * vec4(ppos.x, ppos.y, 0.0, 1.0);'+
'}';

The modifier "uniform" means that the value of this variable won’t change during the rendering operations. The product of a matrix by a vector is simply stated using the operator "*".

As a side note, standard operators are used in GLSL for common matrix or vector operations ("+", "-", "*" of course the vector or matrix sizes must match) and additional functions implement other operations: dot(v1,v2) is the dot product of two vectors (i.e. x1*y1+x2*y2+x3*y3 equal to 0 for perpendicular vectors), cross(v1,v2) the cross product (i.e. a vector perpendicular to v1 and v2 with a length equal to 0 for parallel vectors) and matrixCompMult(m1,m2) the component-wise product of two matrices.

Animation control

Note: this section deals more with HTML and DOM than WebGL.

We want to control the animation with the mouse, more precisely we want to toggle the refresh flag "running" and change the increments of the rotation angles. The flag "running" is already defined (see section "Refactoring the code"), and it is controlled directly as a click into the <canvas>:

<canvas id='glcanvas' width=320 height=240 onclick='running = !running;'>
    Your browser may not support HTML5
</canvas>

The rotation angle increments are set by the user with drop down lists (<select> elements with identifiers dx, dy, dz) and the current angles are stored as <div> elements (ax, ay, az). The HTML code for the control of the rotation around the X axis is given hereafter:

<div style='display:inline-block;'>RX:</div>
<div style='display:inline-block; width:1.5em;'id='ax'>0</div>
<div style='display:inline-block;'>
  <select id='dx'>
    <option>-2<optio>-1<option>0<option selected="selected">+1<option>+2
 </select>
</div>

In the draw() function, the rotation angles and increments are read, and the current angles get updated (EDIT: thanks canta I changed "innerText" by "innerHTML" for better compatibility e.g. with Firefox):

  // Gets control value angles from HTML page via DOM
  var ax = parseInt(document.getElementById('ax').innerHTML, 10);
  var ay = parseInt(document.getElementById('ay').innerHTML, 10);
  var az = parseInt(document.getElementById('az').innerHTML, 10);
  
  // Use increments via DOM to update angles (still in degrees)
  ax = (ax + parseInt(document.getElementById('dx').value, 10) + 360) % 360;
  ay = (ay + parseInt(document.getElementById('dy').value, 10) + 360) % 360;
  az = (az + parseInt(document.getElementById('dz').value, 10) + 360) % 360;
  
  // Update HTML page with new values
  document.getElementById('ax').innerHTML = ax.toString();
  document.getElementById('ay').innerHTML = ay.toString();
  document.getElementById('az').innerHTML = az.toString();

After that, ax, ay and az contain the current angles in degrees, and must be converted:

  // Convert values to radians
  ax *= 2*Math.PI/360; ay *= 2*Math.PI/360; az *= 2*Math.PI/360; 

Combining the rotation matrices

At this point, almost everything is set up:

  • The current rotation angles, in radian (i.e. 0 to 2*PI), are defined (in ax, ay, az)
  • They are incremented for each refresh
  • The refresh is done in draw() using the code of the previous post
  • The shader uses a 4×4 "uniform" matrix that is multiplied with all input vertices
  • This matrix must be defined in the Javascript code

In this section we look at the matrix itself, without a full explanation of the mathematics behind it (a future post will be dedicated to the complete transformation matrix). Hence we admit for now that the rotation around an axis is achieved by the product of a "rotation" matrix with the input coordinates. We will use the Computer Algebra System wxMaxima to pre-compute these matrix operations (the matrix formula won’t change during rendering). We use the variables rx, ry, rz for the angles. A rotation matrix around the Z axis has the following form (in wxMaxima: rotz:matrix([cos(rz),-sin(rz),0,0],[sin(rz),cos(rz),0,0],[0,0,1,0],[0,0,0,1])):

Note that after multiplication of the matrix above by a point with coordinates (x,y,z,1), the Z coordinate remains unchanged (as well as the 4th one), and the X and Y coordinates are rotated in the plane (0,X,Y) (hence it is a rotation around the Z axis):

The rotation matrices around the X and Y axes are obtained by replacing az by ax and ay, and by permuting the different coordinates. The rotation matrix around the X axis is given hereafter (rotx:matrix([1,0,0,0],[0,cos(rx),-sin(rx),0],[0,sin(rx),cos(rx),0],[0,0,0,1])):

The rotation around the Y axis (roty:matrix([cos(ry),0,sin(ry),0],[0,1,0,0],[-sin(ry),0,cos(ry),0],[0,0,0,1])):

The 3 rotations are combined by multiplying the 3 matrices (here again wxMaxima is of great help!). The order of the matrix multiplications is important, i.e. multiplying rotX.rotY.rotZ is not the same as rotZ.rotY.rotX (matrix product is not "commutative"). In our case, we choose to have the rotation around the Z axis as last operation, hence in wxMaxima, we get the result of the formula rotz.roty.rotx:

The matrix is quite complex now, and it only implements 3 rotations. The good thing is that this computation is necessary only once per refresh. It is implemented in the getTransformationMatrix(rx,ry,rz) function:

// Gets a transformation matrix given the rotation angles
function getTransformationMatrix(rx, ry, rz)
{
  // Pre-computes trigonometric values (mainly for better readability)
  var cx = Math.cos(rx), sx = Math.sin(rx);
  var cy = Math.cos(ry), sy = Math.sin(ry);
  var cz = Math.cos(rz), sz = Math.sin(rz);

  // Returns matrix
  return new Float32Array([cy*cz, (sx*sy*cz-cx*sz), (sx*sz+cx*sy*cz), 0,
                           cy*sz, (sx*sy*sz+cx*cz), (cx*sy*sz-sx*cz), 0,
                           -sy,   sx*cy,            cx*cy,            0,
                           0,     0,                0,                1]);
}

The object returned by this function is a one-dimensional array with 16 components, not a real 4×4 matrix. In the WebGL conventions, all matrices are represented as flat arrays when used from Javascript.

Transformation matrix set for rendering

The transformation is now defined and must be communicated to the shader. First the address of the mvp matrix in the shader must be determined. The function gl.getUniformLocation(program, variable-name) returns the desired reference. After that the function gl.uniformMatrix4fv(uniform-ref, false, matrix-as-array) sets the value of the matrix:

  // Gets reference on the &amp;quot;uniform&amp;quot; 4x4 matrix transforming coordinates
  var amvp = gl.getUniformLocation(program, 'mvp');
  if(amvp == -1)
  {alert('Error during uniform address retrieval');running=false;return;}  

  // Creates matrix using rotation angles
  var mat = getTransformationMatrix(ax, ay, az);
  
  // Sets the model-view-projections matrix in the shader
  gl.uniformMatrix4fv(amvp, false, mat);

As usual, you can have a look at the result online. It should look like this:

Summary

The main points regarding animation in WebGL:

  • Functions used for the refresh are put in a new function, which is called periodically through setInterval("code", refresh-period)
  • A "transformation matrix" is set as a "uniform" variable in the shader
  • A reference to such a variable is retrieved by the function gl.getUniformLocation(program, variable-name)
  • The vertex transformation operation in the shader is the product matrix * coordinates
  • Our transformation matrix is built as a product of three 2D rotations using three variable angles (formula found with wxMaxima)
  • Once computed, the matrix is set in the shader through gl.uniformMatrix4fv(uniform-ref, false, matrix-as-array)
Advertisements

,

  1. #1 by Canta on November 10, 2012 - 23:13

    Love the tutorial! :D

    Just one thing: i had to change innerText for innerHTML in order to make it also work in Firefox.

    Thanks!

    • #2 by blogoben on November 13, 2012 - 20:31

      Thanks, and thanks for the tip for Firefox compatibility.

  2. #3 by Artneiver on September 18, 2015 - 22:30

    this is fantastic tutorial about webgl..

  1. math help for kids
  2. wifi ready tv
  3. magnum result
  4. Log Rotations
  5. WebGL Basics 4 – Wireframe 3D object | The Blog-o-Ben

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: