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 it’s at point Y, and then next time that it’s 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 sample code so far has been using a function called drawScene to draw everything, and has been using this code:

    setInterval(drawScene, 15);

…to tell JavaScript to call drawScene at regular 15ms intervals, all we need to do to animate the scene and get the triangle and the square moving is change the code so that every time drawScene is called, it draws stuff slightly differently.

What that means is that the bulk of the changes from the lesson 2 code are in the drawScene function, so let’s start there — it’s 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:

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

    mvTranslate([-1.5, 0.0, -7.0])

    mvPushMatrix();
    mvRotate(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:

    mvRotate(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 second parameter). This means that when the triangle is drawn, it will be rotated by rTri degrees. mvRotate, just like the mvTranslate function we looked at in lesson 1, is coded in JavaScript — we’ll take a look at it later.

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:

    mvTranslate([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();
    mvRotate(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. We do this with a new function called animate which, like drawScene, will be called regularly (you’ll see the code that arranges that in a moment). It 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 add on 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 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.

The next change is the one we have to get animate called regularly, just like drawScene. We do this by creating a new function called tick, which calls both of them and itself is scheduled to be called every 15 milliseconds instead of drawScene:

  function tick() {
    drawScene();
    animate();
  }

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

    gl.clearColor(0.0, 0.0, 0.0, 1.0);
    gl.clearDepth(1.0);

    gl.enable(gl.DEPTH_TEST);
    gl.depthFunc(gl.LEQUAL);

    setInterval(tick, 15);
  }

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

  var mvMatrixStack = [];

  function mvPushMatrix(m) {
    if (m) {
      mvMatrixStack.push(m.dup());
      mvMatrix = m.dup();
    } else {
      mvMatrixStack.push(mvMatrix.dup());
    }
  }

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

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

Now, let’s look at mvRotate

  function mvRotate(ang, v) {
    var arad = ang * Math.PI / 180.0;
    var m = Matrix.Rotation(arad, $V([v[0], v[1], v[2]])).ensure4x4();
    multMatrix(m);
  }

Again, pretty simple — all of the hard work of creating a matrix to represent a rotation is handled by the Sylvester library.

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, mvPopMatrix, and mvRotate comes from Vladimir Vukićević’s spore creature viewer. 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.

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

  1. mike says:

    Hello,
    Thanks for the great tutorials. I got up and running in no time. I ran your javascripts through JSLint and got some errors. I thought this was important:

    var timeNow = (new Date).getTime();

    should be:
    var timeNow = new Date().getTime();

    All for now. Thanks again!

  2. giles says:

    @mike — glad you like the tutorials! Many thanks for the correction, I don’t know how it slipped through: it’s obviously wrong now that you point it out. I’ll fix it.

  3. Ray says:

    There is only one Colorful Triangle moving, no quad.

  4. giles says:

    @Ray — do you see both the triangle and the square on this page http://learningwebgl.com/bug-repro/singlevertexattr-minrepro.html on the same machine?

    If you can’t, then I think it must be an ATI driver bug I’m trying to work out a fix for.

    If you can see both the triangle and the square on the repro page, but don’t see the moving square for this tutorial, then I’m totally confused!

  5. giles says:

    @Ray — OK, I’ve changed the code in this lesson so that I no longer use the WebGL feature (vertexAttrib4fv) that I think was causing the problem you’re seeing.

  6. ed says:

    I’m a little confused when you write “we used setInterval to make sure that we’d be called every 40 milliseconds; “. Did you mean 15ms ?

  7. giles says:

    @ed — I’m beginning to think I need a proofreader… yes, it should say 15ms and I’ve corrected it now. That was left over from an earlier version of the lesson where I actually did use 40ms.

  8. Jos Hirth says:

    You should stick to one indent style. Since only “hugging brackets” aka 1tbs works in all cases, you should use that one. Always.

    Also note that you shouldn’t put “–” into html comments.

    Excellent work though. I really appreciate your effort.

  9. giles says:

    Jos, thanks for the advice and the kind words! I personally prefer the Allman indentation/brace style (perhaps because I spent a lot of time writing Java before I discovered Python); the reason the variation is that some of the matrix code came from elsewhere and I forgot to reformat all of it — a terrible excuse, I know. I’ll search for “) {” and make it all Allman-style, so that at least it’s consistent :-)

    Oh, and re: the problem with “–” in HTML comments — I didn’t know that, thanks for pointing it out. I’ll fix that too.

  10. Jos Hirth says:

    Heh. That “–” thing should have been “minus minus”. I always forget that Wordpress likes to convert this stuff automatically.

  11. giles says:

    Yup, don’t worry — I figured that was what had happened.

  12. Jos Hirth says:

    http://en.wikipedia.org/wiki/Indent_style#Variant:_1TBS

    That’s the only style which works for JavaScript. You see, the problem with JS is that the semicolons are optional. If something doesn’t make sense, the parser jumps back, adds a semicolon, and tries again.

    So, if you write something like:

    return
    {
    foo: ‘bar’
    };

    It will return undefined!

    Not putting the opening brace in a new line is the only indent style you can *always* use.

    [JavaScript: The Good Parts, Appendix A: Awful Parts, Semicolon Insertion]

  13. giles says:

    That’s a very convincing argument.

    I’d noticed the semicolon-handling — having been coding a lot of Python recently I’d got out of the habit of putting them in, and was rather confused (and a little annoyed) that JavaScript didn’t seem to mind and didn’t warn me. I’ve not been bitten by the problem you show, but it’s probably only a matter of time…

    So, OK: I’ll switch over to 1TBS. It may take a while, though, because I’d like to update the old lessons too.

  14. Jos Hirth says:

    >[JavaScript] didn’t warn me.

    Get Komodo Edit (open source, available for Windows, Mac, and Linux) and the kjslint addon.

    Head over to http://jslint.com/ and get a settings comment like this one:

    /*jslint white: true, onevar: true, undef: true, nomen: true, eqeqeq: true, bitwise: true, regexp: true, newcap: true, immed: true */

    (”The Good Parts” without requiring “use strict” and allowing ++ and –.)

    If you have a comment like that in your files, jslint (or kjslint) will use those options for validation. You can also use kjslint’s options, but with a comment like this it’s a tad nicer for full automation.

  15. giles says:

    Thanks!

  16. giles says:

    Right, lessons 1-5 all now use 1TBS; the remainder should be done by the end of today.

  17. Skofo says:

    First let me say that the way you explain things is -awesome-. I’ve been struggling with O3D for a long time because there aren’t any good tutorials out there for it, but with this one everything just ‘clicks’ in my mind even though I haven’t ever touched low-level graphics programming before.

    I’m dubious about your style of coding, however. You seem to make many functions that you use only once or could be considerably shortened, combined with other functions or taken out altogether. For example, wouldn’t your code be more efficient if you combined the ‘tick’, ‘animate’ and ‘drawScene’ functions together, and simply defining a variable as the current state of the model-view matrix instead of creating a couple functions to store it in a perpetually one-item list?

  18. giles says:

    Skofo — glad you like the explanations!

    Re: the structure of the code: I agree it’s not perfect — I’ve used global variables much more than I would in normal code, for example — but in general this is because I’ve tried to aim for readability and explainability over elegance, and to avoid forcing people into a particular style, be it functional or object-oriented.

    Regarding the functions: I prefer using short functions with well-defined tasks — so, while you’re right that animate() and drawScene() could be merged into one, they actually do quite different things and so IMO they’re better separated out. This also allows you to separate out repainting and animation at a later stage, so that (for example) in a MORPG, animation can be pushed out from a server while repainting is handled locally.

    Regarding the matrices — obviously my explanation there isn’t up to scratch! It’s not a perpetually one-item list, it’s a stack. mvPushMatrix puts a matrix onto the stack, mvPopMatrix restores it. This allows you to call nested functions, each of which can maintain its own model-view matrix state. Is that any clearer?

  19. Skofo says:

    Ahh, that makes perfect sense. Thanks!

  20. oskude says:

    Thanks for these nice tutorials, but why is the CPU usage at 100% in this simple animation example ? (1.8Ghz AMD Sempron / NVIDIA GeForce 6100)

    Or is this only like that on MS-Windows and Firefox 3.7alpha ?
    (couldn’t test Chrome as i only found an installer that doesn’t work with a proxy that needs authentication…)

    Cause when i check the “original” example, http://nehe.gamedev.net/data/lessons/lesson.asp?lesson=04 the objects rotate smooth and CPU usage is at ~5%.

    I’m just a “web monkey” and a hobby coder, but looking at the NeHe (c++) example code, i don’t see anything that says how fast the screen is being updated, i assume as fast as possible ?

    With this WebGL thing (in MS-Windows/Firefox) i needed to go low as setInterval(100) (10FPS) to get CPU usage down to 5%, but then the animation is jerky…

    So where does this massive CPU usage come from ?
    Will the CPU usage ever be as low as in the NeHe (c++) example and still stay smooth ?

    I really hope this is just a temporary “show stopper” ;)

  21. giles says:

    Hi oskude, glad you like the tutorials. I don’t think WebGL is ever going to be quite as efficient as C++, as it’s use JavaScript, but it should get better. The current implementation is in need of optimisation, and I would expect it to be faster by the time it’s finally released.

    On the other hand, there is one worry; because WebGL needs to be “composited” with the other elements in a web page, so that (for example) other HTML stuff can overlay it like it can overlay the 2D canvas, it may take a while to get it up to speed. As I understand it (and I may have the wrong end of the stick here), in order to composite the 3D graphics with the other HTML stuff, after the 3D buffer is generated by the graphics card it is copied back down into main memory, then the composition happens using the CPU rather than the GPU, and then it’s pushed back up to the graphics card. If this really is what is happening, it is obviously a non-optimal process… though it’s hard to see how it could be addressed. Rendering the HTML with Direct2D or an equivalent API could help — you might be able to keep everything on the GPU — though I’m not sure how much work that would involve. Something between “a lot” and “an impossible amount”, I suspect…

  22. Aetas says:

    These are great tutorials, been working through them over the past day and everything’s been explained very well. Thanks ^^.

    The only error I ran into I didn’t even know was an error (Never used JavaScript before) and it was the

    var timeNow = (new Date).getTime();

    line. I didn’t notice that until I saw the comments. You fixed it in the code, just not the lesson. =)

    Thank you again ^^

    On to lesson 4.

  23. giles says:

    “You fixed it in the code, just not the lesson. =)”

    Crikey, so I did! Thanks for pointing that out, I’ve fixed it in the lesson too now :-)

    Glad you like the tutorials, hope the later ones are as useful for you!

  24. Thomas says:

    Great write-up. You’ve structured it really well and I can’t even begin to tell you how incredible useful it is that the code changes only very slightly from lesson to lesson while still introducing and illustrating new concepts gradually.

    It would be nice if you could add an explanation about what the purpose is of the parameter m in mvPushMatrix, and what specifically the method call m.dup() does.

    I’ve found it in Sylvester’s documentation ( http://sylvester.jcoglan.com/api/matrix#dup ) eventually. In hindsight, the name is pretty obvious, I suppose. :p

  25. giles says:

    Thanks, Thomas — glad you found the tutorial useful! I’m considering moving to the MJS or EWGL matrix library instead of Sylvester (they’re both stripped down for WebGL use) but if I don’t, I’ll add an explanation of the unexplained bits in the matrix code.

  26. [...] and see what we can do with just canvas.  So, I took the source code for one of the Learning WebGL examples and used it for my test case.  I was having more problems with it and was almost ready to give [...]

  27. msirin says:

    hi, thank you for these awesome tutorials. seems to be a lot of work..
    tell me if i understood correctly: we define our vertices in the model coordinate system, then translating the coordinate system of our vertex buffer object so that we can see e.g. the triangle. now when we say rotate around the y-axis, it is the y-axis of the above mentioned coordinate system.
    but what to do if i don’t want to turn around the y-axis, instead use an arbitrary one? let’s say the chosen new rotate axis is 2 units right to the y-axis. how would you realize that with webgl?

    regards

  28. giles says:

    Hi msirin,

    Glad you like the tutorials! It sounds like you understand the concepts, and answering your question is just a simple step on from there. Right now, before we draw the triangle, we move 1.5 units to the left, and seven units into the screen, so that we’re positioned at the centre of the place where the triangle will be — that is, our translated Y axis is seven units deeper and 1.5 to left of where it was:

    mvTranslate([-1.5, 0.0, -7.0]);
    

    Next, we rotate around our translated Y axis:

    mvRotate(rTri, [0, 1, 0]);
    

    Now, if what we wanted to do is rotate around a a vertical axis 2 units to the right of the centre of the triangle, what we’d need to do is translate such that the point 2 units to the right was the Y axis, then do the rotate. So a first approximation of what we’d do would be:

    # Move to the centre of the triangle
    mvTranslate([-1.5, 0.0, -7.0]);
    
    # Move two units to the right
    mvTranslate([2.0, 0.0, 0.0]);
    
    # Rotate around the translated Y axis
    mvRotate(rTri, [0, 1, 0]);
    
    # Move back to where the triangle now is
    mvTranslate([2.0, 0.0, 0.0]);
    

    Now, obviously, the first two translates can be combined, like this:

    # Move two units to the right of the centre of the triangle
    mvTranslate([0.5, 0.0, -7.0]);
    
    # Rotate around the translated Y axis
    mvRotate(rTri, [0, 1, 0]);
    
    # Move back to where the triangle now is
    mvTranslate([2.0, 0.0, 0.0]);
    

    …and you’re done!

  29. Ted says:

    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 it’s at point Y, and then next time that it’s at point Z” and so on.

    You mean WebGL is more stateless than you expected?

  30. giles says:

    @Ted — well, it was OpenGL when I learned about it, but yes — that’s right.

  31. jed says:

    I’m a confused.
    elapsed represent how long time it has been since the function was last called,
    but a fixed during is 15s by setInterval(tick, 15);
    Are they equal elapsed and 15s ?

  32. giles says:

    @jed — they might be! setInterval is a request to be called every 15ms, but there’s no guarantee that tick will be called precisely that frequently. All we can be sure of is that it won’t be called *more* frequently than that.

  33. 80063r says:

    I don’t think you can combine the first two mvTranslate’s into one because of the mvPushMatrix and the second value of 2.0 I think should be negative.

    I think it should be this:

    mvTranslate([-1.5, 0.0, -7.0]);

    mvPushMatrix();

    mvTranslate([2.0, 0.0, 0.0]);

    mvRotate(rTri, [0, 1, 0]);

    mvTranslate([-2.0, 0.0, 0.0]);

    Am I on the right track?

  34. 80063r says:

    Sorry for not specifying what I was referring to. I’m referring to the comments about rotating the triangle on a Y-axis 2 units to the right of the triangle.

  35. deathy/Brainstorm says:

    I’ve been doing 3D code for long enough to be able to tell what was going on there, but it may be worth a line or two about the conversion from angle to radians for rotation, since that’s something that will frequently bite newbs.

    Thanks for the tutes!

  36. giles says:

    @80063er — hmm, I think you may be right there. I guess I was just thinking in terms of doing the triangle alone, not the square too.

    @deathy/Brainstorm — good point, I’ll make a note.

  37. neo says:

    Just as you wanted to let you know about one of the first paragraphs: it could be worth mentioning that there is also a system which let you draw things once and then move them around – it’s svg.

Leave a Reply

Subscribe to RSS Feed Follow me on Twitter!