Tutorials


These tutorials are targeted at people with a reasonable web programming knowledge (HTML and JavaScript languages) and a basic web 3D programming knowledge (WebGL API).


Tutorial 5 : Animations And Transformation Stack

This tutorial shows how to implement animations with SpiderGL. It also introduces the library component Space, and in particular one of his classes that, in similar way as in the fixed-pipeline version of OpenGL, provides the matrix stack data structure.

View DemoDownload Source

In the previous tutorial we showed how to draw on the HTML5 canvas element with the help of SpiderGL . As and example, two simple 2D static objects were rendered, a triangle and a square, with different color.
The aim of this tutorial is to draw a 3D object (a colored cube) and animate it.
While to instantiate a 3D object is practically the same that creating a 2D one, we need some new concept to animate a 3D scene (which, in Computer Graphics, means just to draw it repeatedly, each time in a different position ) .

SpiderGL provides a couple of tools that allow to operate with animations in a handy and simple manner: the first one is the User Interface routine "onAnimate" (the core of the animations in SpiderGL), and the second one is the Space component class "TransformationStack" (which is helpful in handling the various matrix stacks and greatly simplifies the WebGL coding).

Taking a look at the codewe can see that the HTML structure of the page, like other secondary JavaScript methods, are almost unchanged w.r.t the previous tutorials. Instead there is some modification in the "onInitialize" and "onDraw" routines. Additionally, a pair of event handler appears, the ("onKeyPress" and the aforementioned "onAnimate") to manage user interactions and animation, respectively.

Let's browse the code from the top: at the beginning we find the "onInitialize" routine. The first code lines are relative to the WebGL context variable assignment, to the vertex and fragment shaders definition, and to the shader program setup. All these entities are exactly the same as in the previous lessons (with the exception of the uniform variable "uColorCorrection", not used in this tutorial and hence removed).
Afterwards, we can find the 3D object buffers declarations. Like for the square example of the previous tutorial, our cube will be here rendered as a set of primitives, making use of an index buffer to re-use the vertex position specified in the vertex buffer:

var cubeVertices = [
  -1.0, -1.0,  1.0, //0
   1.0, -1.0,  1.0, //1
  -1.0,  1.0,  1.0, //2
   1.0,  1.0,  1.0, //3
  -1.0, -1.0, -1.0, //4
   1.0, -1.0, -1.0, //5
  -1.0,  1.0, -1.0, //6
   1.0,  1.0, -1.0  //7
];

this.cubeVertexPositionBuffer = new SglVertexBuffer(gl, {
    data  : new Float32Array(cubeVertices), 
    usage : gl.STATIC_DRAW 
});

In the above listing we can see the usual SpiderGL vertex buffer definition, with our object eight vertex spatial position outlined in the JavaScript list "cubeVertices".
Through a second vertex buffer and a second JavaScript array, different colors (as set of RGB values) are associated to each vertex:

var cubeColors = [			
  0.0, 0.0, 1.0, //0
  1.0, 0.0, 1.0, //1
  0.0, 1.0, 1.0, //2
  1.0, 1.0, 1.0, //3			
  0.0, 0.0, 0.0, //4
  1.0, 0.0, 0.0, //5
  0.0, 1.0, 0.0, //6
  1.0, 1.0, 0.0  //7
];

this.cubeVertexColorBuffer = new SglVertexBuffer(gl, {
    data  : new Float32Array(cubeColors), 
    usage : gl.STATIC_DRAW 
});

Looking at the index buffer, note that in the block of code below there are two of them: this is because we want to render the cube making use of different primitives (TRIANGLES, LINES and POINTS), that obviously need different sequence of indexes to link to the right vertexes in the buffer.
So you will have an index buffer that defines each cube face as composition of two filled triangles and another index buffer that defines the same cube faces as composition of the four segments that connect the vertexes, by sketching the perimeter (no specific index buffer is needed for the POINTS primitive, since is possible to re-use any of the previous):

var cubeIndicesTriangles = [
  0, 1, 2, 2, 1, 3, // Front
  5, 4, 7, 7, 4, 6, // Back
  4, 0, 6, 6, 0, 2, // Left
  1, 5, 3, 3, 5, 7, // Right
  2, 3, 6, 6, 3, 7, // Top
  4, 5, 0, 0, 5, 1  // Bottom
];

this.cubeVertexIndexBufferT = new SglIndexBuffer(gl, {
    data  : new Uint16Array(cubeIndicesTriangles), 
    usage : gl.STATIC_DRAW 
});

this.cubeVertexIndexBufferT.numItems = 36;

var cubeIndicesLines = [

  0, 1, 1, 3, 3, 2, 2, 0, // Front
  5, 4, 4, 6, 6, 7, 7, 5, // Back
  0, 4, 1, 5, 3, 7, 2, 6  // Middle
];

this.cubeVertexIndexBufferL = new SglIndexBuffer(gl, {
    data  : new Uint16Array(cubeIndicesLines), 
    usage : gl.STATIC_DRAW 
});

this.cubeVertexIndexBufferL.numItems = 24;

The properties "numItems", which tell us the number of elements located in their list (the usefulness of this will be clarify later), are associated to both the index buffers (thanks to the JavaScript possibility to add and remove dynamically attributes to an object)

Towards the end of the initialization stage we set different parameters:

this.primitives            = gl.TRIANGLES;
this.cubeVertexIndexBuffer = this.cubeVertexIndexBufferT;

this.angle                 = -45.0;
this.rate                  =  60.0;	

this.ui.animateRate        =   0.0;

These variables refers, respectively, to the WebGL primitives constant value (initially set to "gl.TRIANGLES", i.e. the triangles value), the previously allocated index buffers (here set to the triangles index buffer "cubeVertexIndexBufferT", according to the selected primitives), the value of the current angle of rotation (initialized to "-45.0" degrees), and the value that will be used to modify the animation frequency (set to "60.0" by default).
The last variable initialization deserves more attention, because sets an internal field attribute ("animateRate", located in the User Interface class CanvasHandler) that defines the calls per second (here setted to "0.0") used to emit the "onAnimate" event (if zero, the animation event will be disabled, if greater than zero, it specifies how many times the event is emitted per second, if less than zero the event is emitted as fast as possible).

Finally the last line code creates the SpiderGL "TransformationStack" object:

this.xform = new SglTransformationStack();

As anticipated at the beginning of this tutorial, the purpose of this object is to provide transformation stack similar to the one in the fixed-pipeline versions of OpenGL. But, unlike to OpenGL, which has two stacks (namely MODELVIEW and PROJECTION), the SpiderGL transformation stack is composed of three matrix stacks: MODEL, VIEW and PROJECTION. All these three stacks can be directly accessed, as well as utility getters, to obtain compositions of matrices, e.g. model-view-projection, model-view-inverse, etc. (note that to avoid recalculation of several products, matrix compositions and variations are cached and updated when stack operations occur).

After the SpiderGL initialization procedure, we can jump to the first event listener, the "onKeyPress":

onKeyPress: function (key) {
  var gl = this.ui.gl;

  if (key == "1") {
    this.primitives = gl.TRIANGLES;
    this.cubeVertexIndexBuffer = this.cubeVertexIndexBufferT;
    this.ui.postDrawEvent();
    log("Selected primitives TRIANGLES");
  }
  if (key == "2") {
    this.primitives = gl.LINES;
    this.cubeVertexIndexBuffer = this.cubeVertexIndexBufferL;
    this.ui.postDrawEvent();
    log("Selected primitives LINES");
  }
  if (key == "3") {
    this.primitives = gl.POINTS;
    this.ui.postDrawEvent();
    log("Selected primitives: POINTS");
  }
  if (key == "b" || key == "B") {
    this.ui.animateRate = this.rate;	
    log("Start animation (" + this.ui.animateRate + " FPS)");
  }
  if (key == "s" || key == "S") {
    this.ui.animateRate	= 0.0;	
    log("Stop animation");
  }
  if (key == "a" || key == "A") {
    this.rate++;
    log("Increase animation rate (" + this.rate + "FPS)");
    if(this.ui.animateRate > 0) this.ui.animateRate = this.rate;
  }
  if (key == "z" || key == "Z") {
    if (this.rate > 1) {				
      this.rate--;
      log("Decrease animation rate (" + this.rate + " FPS)");
      if(this.ui.animateRate > 0) this.ui.animateRate = this.rate;
    }	
  }
}

This routine, as shown in Tutorial 3, is used to manage the user interaction with the canvas, and in more detail, as can be easily inferred by its name, it is the one that connects the pressure of a key with the desired action.
In this case it is possible for the user to control the cube animation, changing the animation parameters, and to switch from a drawing primitive to another.
The first three keys associations are used for handling the animation: pressing the numeric keys "1", "2" or "3" is possible to change the value of the variables related to the primitives constant value and to the different index buffers. The call to the SpiderGL method "postDrawEvent" (located in the User Interface class CanvasHandler) sends a draw event which will calls the "onDraw" method of the registered handler, refreshing in this manner the scene), prints a log message, and then terminates (note that when the POINTS primitives are selected, pressing the numeric key "3", since using both triangles and lines index buffers yields the same result, no index buffers associations are performed).
The "B" and "S" keys permit to modify the value of the boolean variable that controls the animation, in order to Begin and Stop it.
Finally, the last two key actions enable to change the frequency used to emit the "onAnimate" event, by increasing ("A") or decreasing ("Z") it. The current animation rate is also printed.

The second (and last) event handler is the above cited "onAnimate" routine, which manages the animation code.
This routine is the core of animations in SpiderGL, because is the one called, with the specified frequency, when the CanvasHandler field attribute "animateRate" assumes a value other than zero. The "postDrawEvent" method placed here becomes the animation controller, due to the fact that it calls the "onDraw" routine of the registered handler with the appropriate rate.
As we can see in the following code, this event handler passes an useful parameter ("dt") which stores the elapsed time, in seconds, between the last and the current call.

onAnimate: function (dt) {
  this.angle += 90.0 * dt;
  this.ui.postDrawEvent();
}

Thanks to this parameter, the "angle" variable modifications can be modified ensuring an homogeneous animation behaviour independently on the power of the graphics hardware.

Finally, concerning the "onDraw" routine, many lines of code are exactly the same of the previous lesson, so we can safely leave them away and jump to the new ones, related to the setting of the model view projection matrix transformation.
These matrix operations are performed in easy and quick way thanks to the SpiderGL transformation stack object "xform" earlier defined in the "onInitialize" method. This object can dispose of three different kind of matrix stacks (PROJECTION, VIEW and MODEL), that allow a direct access to the related transformation matrices, avoiding annoying matrix allocations and compositions:

this.shaderProgram.bind();

xform.projection.loadIdentity();
xform.projection.perspective(sglDegToRad(45.0), w/h, 0.1, 100.0);

xform.view.loadIdentity();
xform.view.lookAt([0.0,2.0,3.0], [0.0,0.0,0.0], [0.0,1.0,0.0]);

xform.model.loadIdentity();
xform.model.rotate(sglDegToRad(this.angle), [0.0, 1.0, 0.0]);
xform.model.scale([0.5, 0.5, 0.5]);

this.shaderProgram.setUniforms({
  uModelViewProjectionMatrix: xform.modelViewProjectionMatrix
});

The two lines of code next to the shader program binding act on the projection matrix stack: first we set the matrix at the top of the stack as the identity matrix, and then it is post-multiplied with a perspective projection matrix (with is created by providing the vertical field-of-view angle in radians, the projection plane aspect ratio and the distances of the near and far clipping planes).
The next two code lines operate on the view matrix stack, first placing the identity matrix at the top of the stack, and then post-multiplying the same with a look-at matrix (the column major 4x4 matrix with in input the viewer spatial position as 3D vector, the viewer look-at target point as 3D vector, and the viewer up 3D vector).
The following three lines of code work with the model matrix stack: the initial identity matrix loading is performed, then this matrix is post-multiplied with a rotation matrix (the rotate function takes in input the counter-clockwise rotation angle in radians and a 3D vector representing the rotation axis), and at the end the same matrix is post-multiplied with a scaling matrix. Note that, in this example, it is the rotation applied, which changes during the time, that realizes the animation of the cube.
Afeter completing the matricx transformations, we can pass the final model view projection matrix obtained as a uniform variable to the shader, through the appropriate method "setUniforms". For this last operation you can use the transformation stack attribute field "modelViewProjectionMatrix", that gets a copy of the transformation matrix T = P * V * M (where P is the projection matrix, V the view matrix and M the model matrix) automatically computed.

This is all about how to use the SpiderGL transformation stack. Now, to render the cube on the canvas is sufficient to latch the attributes pointer with the suitable buffers (in the same way as explained in the previous tutorial).

this.cubeVertexIndexBuffer.drawElements({
  glMode : this.primitives, 
  count  : this.cubeVertexIndexBuffer.numItems, 
  glType : gl.UNSIGNED_SHORT, 
  offset : 0 
});

Referring to the above code, we point out that the first two parameters of this method (related to the primitives type and to the number of elements to draw) are here referred not to two constants (like in the last lesson) but as to two variables, previously defined in the "onInitialize" routine, and also modified in the key press event listener.
This kind of implementation permits to dynamically change the index buffer setting, making possible some particular effect during the rendering. For example, here we use it to switch among three different types of primitives to draw our cube. Thanks to the JavaScript property previously mentioned, we can change the SpiderGL index buffer object linked to the variable called "cubeVertexIndexBuffer" and automatically change also the value referred by the attribute "numItems", in a very clean and comfortable way (and this explain the attribute instantiation in the previous "onInitialize" routine).

Well, it is all for this tutorial!
Now the reader should be able to use the transformation stack provided and implement an animation easily using the SpiderGL provided tools.
In the next tutorial we will show how to create and handle a 2D texture with SpiderGL.