WebGL Lesson 3 – a bit of movement

<< Lesson 2Lesson 4 >>

Welcome to my number three in my series of WebGL tutorials. This time we’re going to start making things move around. It’s based on number 4 in the NeHe OpenGL tutorials.

Here’s what the lesson looks like when run on a browser that supports WebGL:

Click here and you’ll see the live WebGL version, if you’ve got a browser that supports it; here’s how to get one if you don’t.

More on how it all works below…

The usual warning: these lessons are targeted at people with a reasonable amount of programming knowledge, but no real experience in 3D graphics; the aim is to get you up and running, with a good understanding of what’s going on in the code, so that you can start producing your own 3D Web pages as quickly as possible. If you haven’t read the first and second tutorials already, you should probably do so before reading this one — here I will only explain the differences between the code for lesson 2 and the new code.

As before, there may be bugs and misconceptions in this tutorial. If you spot anything wrong, let me know in the comments and I’ll correct it ASAP.

There are two ways you can get the code for this example; just “View Source” while you’re looking at the live version, or if you use GitHub, you can clone it (and the other lessons) from the repository there. Either way, once you have the code, load it up in your favourite text editor and take a look.

Before I get into describing the code, I’ll clarify one thing. The way you animate a 3D scene in WebGL is very simple — you just draw repeatedly, drawing it differently each time. This may well be totally obvious to a lot of readers, but it was a bit of a surprise to me when I was learning OpenGL, and might surprise others who are coming to 3D graphics for the first time with WebGL. The reason I was confused originally was that I was imagining that it would use a higher-level abstraction, which would work in terms of “tell the 3D system that there’s (say) a square at point X the first time I draw it, and then to move the square, tell the 3D system that the square I told it about earlier has moved to point Y.” Instead, what happens is more that you “tell the 3D system that there’s a square at point X, then next time you draw it, tell the 3D system that there’s a square at point Y, and then next time that there’s a square at point Z” and so on.

I hope that last paragraph has made things clearer for at least some people (let me know in the comments if it’s just confusing matters and I’ll delete it :-)

Anyway, what this means is that because our code so far has been using a function called drawScene to draw everything, to animate things we need to arrange matters such that this function is called repeatedly, and draws something slightly different each time. Let’s start at the bottom of the index.html file and see how that’s done. Firstly, let’s take a look at the function that kicks everything off when the page is loaded, webGLStart:

  function webGLStart() {
    var canvas = document.getElementById("lesson03-canvas");
    initGL(canvas);
    initShaders()
    initBuffers();

    gl.clearColor(0.0, 0.0, 0.0, 1.0);
    gl.enable(gl.DEPTH_TEST);

    tick();
  }

The only change here is that instead of calling drawScene at the end to draw the scene, we call a new function, tick. This is the function that needs to be called regularly; it updates the scene’s animation state (eg. the triangle has moved from being 81 degrees rotated to 82 degrees) draws the scene, and also arranges for itself to be called again in an appropriate time. It’s the next function up in the file, so let’s look at it next.

  function tick() {
    requestAnimFrame(tick);

The first line is where tick arranges to be called again when a repaint is needed next. requestAnimFrame is a function in some Google-provided code that we’re including into this web page with a <script> tag at the top, webgl-utils.js. It gives us a browser-independent way of asking the browser to call us back next time it wants to repaint the WebGL scene — for example, next time the computer’s display is refreshing itself. Right now, functions to do this exist in all WebGL-supporting browsers, but they have different names for it (for example, Firefox has a function called mozRequestAnimationFrame, while Chrome and Safari have webkitRequestAnimationFrame). In the future they are expected to all use just requestAnimationFrame. Until then, we can use the Google WebGL utils to have just one call that works everywhere.

It’s worth noting that you could get a similar effect to using requestAnimFrame by asking JavaScript to call the drawScene function regularly, for example by using the built-in JavaScript setInterval function. A lot of early WebGL code (including earlier versions of these tutorials) did just that, and it worked fine — right up until people had more than one WebGL page open, in different browser tabs. Because functions scheduled with setInterval are called regardless of whether the browser tab they belong to is showing, using it meant that computers were doing all the work of displaying every open WebGL tab all the time, hidden or not. This was obviously a Bad Thing, and was the reason why requestAnimationFrame was introduced; functions scheduled using it are only called when the tab is visible.

On to the remainder of tick:

    drawScene();
    animate();
  }

So, once we’ve scheduled tick to be called again next time the browser wants a frame to be painted, we simply draw this one, and update our state for the next. Let’s look at the drawScene and animate functions in turn.

drawScene is about two thirds of the way down index.html. The first thing to note is that just before the function declaration, we’re now defining two new global variables.

  var rTri = 0;
  var rSquare = 0;

These are used to track the rotation of the triangle and the square respectively. They both start off rotated by zero degrees, and then over time these numbers will increase — you’ll see how later — making them rotate more and more. (A side note — using global variables for things like this in a 3D program that is not a simple demo like this would be really bad practice. I show how to structure things in a more elegant manner in lesson 9.)

The next change in drawScene comes at the point where we draw the triangle. I’ll show all of the code that draws it by way of context, the new lines are the ones in red:

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

    mat4.identity(mvMatrix);

    mat4.translate(mvMatrix, [-1.5, 0.0, -7.0]);

    mvPushMatrix();
    mat4.rotate(mvMatrix, degToRad(rTri), [0, 1, 0]);

    gl.bindBuffer(gl.ARRAY_BUFFER, triangleVertexPositionBuffer);
    gl.vertexAttribPointer(shaderProgram.vertexPositionAttribute, triangleVertexPositionBuffer.itemSize, gl.FLOAT, false, 0, 0);

    gl.bindBuffer(gl.ARRAY_BUFFER, triangleVertexColorBuffer);
    gl.vertexAttribPointer(shaderProgram.vertexColorAttribute, triangleVertexColorBuffer.itemSize, gl.FLOAT, false, 0, 0);

    setMatrixUniforms();
    gl.drawArrays(gl.TRIANGLES, 0, triangleVertexPositionBuffer.numItems);

    mvPopMatrix();

In order to explain what’s going on here, let’s go back to lesson 1. There, I said:

In OpenGL, when you’re drawing a scene, you tell it to draw each thing you draw at a “current” position with a “current” rotation — so, for example, you say “move 20 units forward, rotate 32 degrees, then draw the robot”, the last bit being some complex set of “move this much, rotate a bit, draw that” instructions in itself. This is useful because you can encapsulate the “draw the robot” code in one function, and then easily move said robot around just by changing the move/rotate stuff you do before calling that function.

You’ll remember that this current state is stored in a model-view matrix. Given all that, the purpose of the call to:

    mat4.rotate(mvMatrix, degToRad(rTri), [0, 1, 0]);

Is probably pretty obvious; we’re changing our current rotation state as stored in the model-view matrix, rotating by rTri degrees around the vertical axis (which is specified by the vector in the third parameter). This means that when the triangle is drawn, it will be rotated by rTri degrees. Note that mat4.rotate takes angles in radians; personally I find degrees easier to deal with, so I’ve written a simple conversion function degToRad to use here.

Now, what about the calls to mvPushMatrix and mvPopMatrix? As you would expect from the function names, they’re also related to the model-view matrix. Going back to my example of drawing a robot, let’s say your code at the highest level needs to move to point A, draw the robot, then move to some offset from point A and draw a teapot. The code that draws the robot might make all kinds of changes to the model-view matrix; it might start with a body, then move down for the legs, then up for the head, and finish off with the arms. The problem is that if after this you tried to move to your offset, you’d move not relative to point A but instead relative to whatever you last drew, which would mean that if your robot lifted its arms, the teapot would start levitating. Not a good thing.

Obviously what is required is some way of storing the state of the model-view matrix before you start drawing the robot, and restoring it afterwards. This is, of course, what mvPushMatrix and mvPopMatrix do. mvPushMatrix puts the matrix onto a stack, and mvPopMatrix gets rid of the current matrix, takes one from the top of the stack, and restores it. Using a stack means that we can have any number of bits of nested drawing code, each of which manipulates the model-view matrix and then restores it afterwards. So once we’ve finished drawing our rotated triangle, we restore the model-view matrix with mvPopMatrix so that this code:

    mat4.translate(mvMatrix, [3.0, 0.0, 0.0]);

…moves across the scene in an unrotated frame of reference. (If it’s still not clear what this all means, I recommend copying the code and seeing what happens if you remove the push/pop code, then running it again; it will almost certainly “click” pretty quickly.)

So, these three changes make the triangle rotate around the vertical axis through its centre without affecting the square. There are also three similar lines to make the square rotate around the horizontal axis through its centre:

    mvPushMatrix();
    mat4.rotate(mvMatrix, degToRad(rSquare), [1, 0, 0]);

    gl.bindBuffer(gl.ARRAY_BUFFER, squareVertexPositionBuffer);
    gl.vertexAttribPointer(shaderProgram.vertexPositionAttribute, squareVertexPositionBuffer.itemSize, gl.FLOAT, false, 0, 0);

    gl.bindBuffer(gl.ARRAY_BUFFER, squareVertexColorBuffer);
    gl.vertexAttribPointer(shaderProgram.vertexColorAttribute, squareVertexColorBuffer.itemSize, gl.FLOAT, false, 0, 0);

    setMatrixUniforms();
    gl.drawArrays(gl.TRIANGLE_STRIP, 0, squareVertexPositionBuffer.numItems);

    mvPopMatrix();
  }

…and that’s all of the changes to the drawing code in drawScene.

Obviously, the other thing we need to do to animate our scene is to change the values of rTri and rSquare over time, so that each time the scene is drawn, it’s slightly different. This, of course, happens in our new animate function, which looks like this:

  var lastTime = 0;
  function animate() {
    var timeNow = new Date().getTime();
    if (lastTime != 0) {
      var elapsed = timeNow - lastTime;

      rTri += (90 * elapsed) / 1000.0;
      rSquare += (75 * elapsed) / 1000.0;
    }
    lastTime = timeNow;
  }

A simple way of animating a scene would be to just rotate our triangle and our square by a fixed amount each time animate was called (which is what the original OpenGL lesson on which this one was based does), but here I’ve chosen to do something I think is slightly better practice; the amount by which we rotate the objects is determined by how long it has been since the function was last called. Specifically, the triangle is rotating by 90 degrees per second, and the square by 75 degrees per second. The nice thing about doing it this way is that everyone sees the same rate of motion in the scene regardless of how fast their machine is; people with slower machines (for whom functions scheduled with requestAnimFrame will be called less frequently) just see jerkier images. This doesn’t matter so much for a simple demo like this, but obviously can be a bigger deal with games and the like.

So, that’s all of the code that actually animates and draws the scene. Let’s look at the supporting code that we had to add, mvPushMatrix and mvPopMatrix:

  var mvMatrix = mat4.create();
  var mvMatrixStack = [];
  var pMatrix = mat4.create();

  function mvPushMatrix() {
    var copy = mat4.create();
    mat4.set(mvMatrix, copy);
    mvMatrixStack.push(copy);
  }

  function mvPopMatrix() {
    if (mvMatrixStack.length == 0) {
      throw "Invalid popMatrix!";
    }
    mvMatrix = mvMatrixStack.pop();
  }

There shouldn’t be anything surprising there. We have a list to hold our stack of matrices, and define push and pop appropriately.

There’s only one new thing left to explain — the degToRad function I mentioned earlier. If you remember anything from your maths at school, it won’t hold any surprises…

    function degToRad(degrees) {
        return degrees * Math.PI / 180;
    }

And… that’s it! There are no more changes to go through. Now you know how to animate simple WebGL scenes. If you have any questions, comments, or corrections, please do leave a comment below.

Next time, (to quote NeHe’s preface to his lesson 5) we’ll “make the object into TRUE 3D object, rather than 2D objects in a 3D world”. Click here to find out how.

<< Lesson 2Lesson 4 >>

Acknowledgments: The code for mvPushMatrix and mvPopMatrix is adapted from Vladimir Vukićević’s spore creature viewer. Thanks also to Google for publishing their very useful webgl-utils.js helper file, and, of course, I’m deeply in debt to NeHe for his OpenGL tutorial for the script for this lesson.

You can leave a response, or trackback from your own site.

82 Responses to “WebGL Lesson 3 – a bit of movement”

  1. giles says:

    That’s just weird. I was going to suggest that it might be an old driver or something, but I see from the video that you’ve checked all that already. Everything *should* be working fine. Wish I could help, but I’m out of ideas :-(

  2. jlabanca says:

    Excellent tutorial! I have almost no experience with OpenGL or matrix transforms, and this is the first tutorial I’ve found that explains the code easily enough for me to understand.

    One potential bug I found is that in animate(), you should clip rTri and rSquare so they don’y grow out of control. It would take a long while before you overflow, but its good habit, especially for gaming where people can play for hours.
    rTri = rTri % 360;
    rSquare = rSquare % 360;

  3. giles says:

    Thanks, that’s a good point — probably best not to get people into bad habits. I don’t have time to do it right now, but I’ll fix the problem soon.

  4. Peter Thelander says:

    Hi and thank you for the excellent work in setting up these lessons.

    One suggestion, you should change the line:

    gl = canvas.getContext(“experimental-webgl”);

    to instead be this, as recommended by the webgl-utils comments:

    gl = WebGLUtils.setupWebGL(canvas);

    That way it will keep working once they change the string to remove the “experimental”. Also, it has better handling for the case when webgl is not supported in the browser, with links to the getwebgl site etc, and you don’t need the surrounding try/catch.

    Thanks again!

  5. strcat says:

    Thanks for the great tutorials!

    I decided just to convert from milliseconds -> degrees, avoiding the lastTime, rTri and rSquare global variables:

    function drawScene() {
    var timeNow = new Date().getTime();
    var rTri = timeNow * 0.06;
    var rSquare = timeNow * 0.18;

    gl.viewport(0, 0, canvas.width, canvas.height);
    gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);

    mat4.perspective(45, canvas.width / canvas.height, 0.1, 100.0, pMatrix);

    mat4.identity(mvMatrix);

    mat4.translate(mvMatrix, [-1.5, 0.0, -7.0]);



    }

  6. strcat says:

    Note: the speeds are different in my example code because I was messing around with them :P .

  7. Catalin says:

    Hi.
    If i use your link with demo lesson3, the example working fine.
    If i try to run the downloaded zip example i got “Could not initialise WebGL, sorry :-( ” !
    I try with “gl = WebGLUtils.setupWebGL(canvas);” but not working …
    I try with firefox ver. 5.0 and i have this settings on about:support
    Graphics
    Adapter DescriptionMobile Intel(R) 945 Express Chipset Family
    Vendor ID8086Device ID27ae
    Adapter RAMUnknown
    Adapter Driversigxprd32
    Driver Version6.14.10.4926
    Driver Date2-15-2008
    Direct2D Enabledfalse
    DirectWrite Enabledfalse (0.0.0.0, font cache n/a)
    WebGL RendererGoogle Inc. — ANGLE — OpenGL ES 2.0 (ANGLE 0.0.0.611)
    GPU Accelerated Windows0/2
    Can you tell me what is wrong ?
    Thank you .

  8. Diogo says:

    Hey,

    I’m trying to figure out how I can control the time interval… In my case I need a square to move 33px in the x axis per second (instantly, I need it to jump from 0 to 33, 33 to 66, 66 to 99, etc.), how can I do this?

    Thanks.

  9. unsweet says:

    Hi,

    I used your example and on mac it was really, really slow. Actually I have no idea why it was happening. I am javascript developer ans I was trying to implement functions in the different way without using global variables and so on. In the end simple using:

    var rotate = function() {
    drawScene(gl, squareBuffers, shaderProgram, rotation || (rotation = 0));
    rotation–;

    }

    is working good. Any idea why your example was so slow in my mac? I really appreciate your tutorials. Thank you very much for writing them :)

    Cheers :)

  10. Bunzaga says:

    A couple quick ideas…

    var gl;
    function initGL(canvas) {
    try{
    gl = WebGLUtils.setupWebGL(canvas); // awesomeness
    gl.viewportWidth = canvas.width;
    gl.viewportHeight = canvas.height;
    return true; // success
    }
    catch(er){return false;} // failure
    }

    function webGLStart() {
    var canvas = document.getElementById(“lesson03-canvas”);
    // gets rid of ‘ugly’ js errors in IE
    if(initGL(canvas)){ // if successful…
    initShaders()
    initBuffers();

    gl.clearColor(0.0, 0.0, 0.0, 1.0);
    gl.enable(gl.DEPTH_TEST);

    tick();
    }
    }

    I haven’t ever used OpenGL, thanks a million for these tutorials.

  11. [...] De nuevo, este artículo es una traducción casi literal de su respectivo tutorial de la web Learning WebGL, que a su vez está basado en el número 4 de los tutoriales de openGL de [...]

  12. There should not be any initTexture in this lesson I think.
    Thanks for the awesome tutorials!
    Cheers!

  13. Jesse Good says:

    Thanks for the great articles. I had a suggestion for your code:

    1) Get rid of tick() and change webGLStart()to this:
    var timeNow = new Date().getTime();
    animate(timeNow);

    2)Change the animate function like this:
    function animate(previousTime) {
    var timeNow = new Date().getTime();
    var elapsed = timeNow – previousTime;
    rTri += (90 * elapsed) / 1000.0;
    rSquare += (75 * elapsed) / 1000.0;
    renderAnimFrame(function() { animate(timeNow);});
    drawScene();
    }

    I think its a little more elegant this way.
    This idea was taken from here.

  14. Choons says:

    Hi – I’ve been translating these lessons into haXe as I like having the IDE code-completion and type-checking. I’m using a different matrix library and the perspective matrix was giving me problems until I moved it out of the drawScene() function. I notice both here and in the Mozilla webGL tutes that the perspective matrix is regenerated on every draw call. Is that necessary? It seems it never changes.

  15. pravin mali says:

    requestAnimFrame(tick);

    this statement should come at the end of tick function…..
    because initially ” requestAnimFrame(tick);” is not called….

    firstly drawscene would be called and then animate…
    variables used in drawscene have changed their values .hence refreshing is needed….and there’s no function after animate….
    so it will not be able to call itself

  16. Paul says:

    Great tutorial!

    Please can you replace:
    initTexture();
    by
    initBuffers();

    Thanks!

  17. giles says:

    @Peter — thanks for the suggestion, I should probably do that.

    @strcat — interesting idea!

    @Catalin — unfortunately it looks like your setup isn’t WebGL-compatible yet — a lot of older graphics drivers don’t work. Check out this page.

    @Diogo — you could keep a “ticker” — an integer variable that holds the number of milliseconds since your animation started — and then in the animate() function do something like

    xPos = Math.floor(ticker / 1000) * 33
    

    @unsweet — sorry, I really have no idea! I wonder if its something to do with garbage collection of global functions?

    @Buzunga — nice ideas, thanks! I’ll look at putting something like that in there.

    @Stanislas Polu, @Paul — great point! I did a bulk edit of the tutorials, and got that one wrong. The demo code and the github versions have the correct stuff.

    @Jesse — interesting idea, but I think it’s a bit more clear — at least for JavaScript beginners — the way it currently works.

    @Choons — you’re right, it’s not necessary in this example. I might actually move it out, it could be confusing people. Thanks for pointing it out!

    @pravin — I’m not sure I understand the problem. Could you clarify?

  18. Kyle says:

    I might suggest reordering the tick() function contents. Right now drawScene() is called, then animate(). If you are switching between tabs on a browser and you return to the tab of the animation, the older position of the objects is drawn for one frame, then it jumps to the new position. This causes a bit of a jutter which calling animate() before drawScene() fixes.

    Also I believe pravin was saying above that the requestAnimFrame(tick) which sets up tick to be called again should be last in the function. I’m not sure of how the concurrency issues resolve but I could imagine a scenario where tick is repeatedly called by the request of an animation frame without either drawscene() or animate() successfully completing. Thus my suggestion is 1) animate 2) drawscene then 3) requestAnimFrame(tick)

  19. bils says:

    hi guys
    i have a big problem with the tick function.
    I changed the design of the program to be more OOP oriented using the javascript prototype. It worked very well until now but i wanted to do the same with the tick function with something like that:
    tick:function() {
    this.animate();
    this.drawScene();
    requestAnimFrame(this.tick);
    },
    i got caught up with a “Uncaught RangeError: Maximum call stack size exceeded” exception and i don’t know how to fix it if somebody on this i would be grateful
    Thx

  20. bils says:

    Me again
    i managed to get it worked and get rid of the exception but even though
    i’m sure that the tick function is called every frame thanks to a console.log in the animate function, nothing is moving just the iniitial rotation as if the drawingScene is called just once.
    Any idea ?
    Thx

  21. Manu says:

    Nice tutorials!
    But about push and pop, …
    I looked at the code and the only place where the matrix stack was used in the push and pop operations themselves!
    I don’t see why the mvMatrix must be “pushed” when it isn’t used when inside the stack.
    All we do is push it, and pop it. And the stack isn’t used in between?
    I don’t see the use.
    How about setting the mvMatrix to Identity matrix right after the job is done.
    Won’t that work.

    I’m really thankful for your tutorials, and would appreciate it if someone helped me here.
    Thanks!

  22. [...] is the Dart code for “Lesson 3 – A bit of movement” from learningwebgl.com’s lesson series. For those of you who don’t know, Dart is Google’s new programming [...]

  23. Nick says:

    Thank you for an excellent, easy-to-follow, explanation!

    By the way – if you triple the size of the square and triangle, so that they overlap each other as they go round, it is even more impressive.

  24. Thor says:

    The paragraph on animation was/is required! Too many developers assume that animation is a matter of “nudging the thing on” and assuming that GL (Web or Open) should magically take over, not so.
    On the other hand, this “feature” of GL allows intermediate interventions on an animated object. Say: a bird in flight in a game, can be made to change its flightpath when the player comes to stand “in the way”…
    It’s just how you look at it, really…

  25. Nathon Fowlie says:

    It appears there’s been a breaking change in the WebGL api. In your mvPushMatrix function, I had to change:

    var copy = mat4.create();
    mat4.set(mvMatrix, copy);

    to:

    var copy = mat4.create();
    mat4.copy(copy, mvMatrix);

    to get the scene to render correctly. Seems the “set” function has been renamed to “copy” and the parameter order has been reversed.

  26. Nathon Fowlie says:

    It appears there’s been a breaking change in the WebGL api. In your mvPushMatrix function, I had to change:

    var copy = mat4.create();
    mat4.set(mvMatrix, copy);

    to:

    var copy = mat4.create();
    mat4.copy(copy, mvMatrix);

    to get the scene to render correctly. Seems the “set” function has been renamed to “copy” and the parameter order has been reversed.

  27. sid says:

    hi on my android mobile in opera the objects disappeared after few frames

  28. lethjakman says:

    Hey, I noticed that you’re using the gl-transform library, and I think it might be helpful to point out which one. It seems you’re using: https://code.google.com/p/glmatrix/wiki/Usage instead of http://glmatrix.net/docs/2.2.0/index.html

  29. tony says:

    Yes this uses glMatrix 1.0 at the moment. We are in the middle of converting the lessons to use the latest (2.2) Stay tuned!

  30. Matt says:

    Hi! Thanks for theses tutorials, I am really enjoying them.

    I can’t get this lesson to work, though, unfortunately. :(

    I’ve gone through the lessons since #1 and edited the .html file each time changes were required to achieve new functionality. I finished lessons 1 and 2 successfully, but lesson 3 just isn’t working.

    After making the edits in the code myself with no success, I tried copying and pasting your code to see if it would work.

    … Nope.

    And, yes, I do have both .js files properly located in the same directory as the .html file.

    Does anyone else have this problem?

    BTW, I downloaded all of the .js files and copied all of the code from GitHub.

  31. Gabrielle says:

    Unfortunately I have the exact same problems & situation as Matt posted, no idea why :(

  32. Gabrielle says:

    Okay I got the problem fixed. For Matt and others having this issue: open the new .js file in an editor and check that it is the correct code inside: https://github.com/gpjt/webgl-lessons/blob/master/lesson03/webgl-utils.js.

    I also thought that I took the file properly from GitHub but I didn’t the content was not the good one. Once it is, all works like a charm!

    Thank you to the authors for those tutorials!

Leave a Reply

Subscribe to RSS Feed Follow Learning WebGL on Twitter