WebGL Lesson 9 – improving the code structure with lots of moving objects

<< Lesson 8Lesson 10 >>

Welcome to my number nine in my series of WebGL tutorials, based on number 9 in the NeHe OpenGL tutorials. In it, we’ll use JavaScript objects so that we can have a number of independently-animated objects in our 3d scene. We’ll also touch on how to change the colour of a loaded texture and what happens when you blend textures together.

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. You should see a large number of differently-coloured stars, spiraling around.

You can use the checkbox underneath the canvas to switch on a “twinkling” effect, which we’ll look at later. You can also use the cursor keys to spin the animation around its X axis, and you can zoom in and out using the Page Up and Page Down keys.

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 previous tutorials already, you should probably do so before reading this one — here I will only explain the differences between the code for lesson 8 and the new code.

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.

The best way to show how the code for this example differs from lesson 8’s is to start at the bottom of the file and work our way up, kicking off with webGLStart. Here’s what the function looks like this time:

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

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

    gl.clearDepth(1.0);

    document.onkeydown = handleKeyDown;
    document.onkeyup = handleKeyUp;

    setInterval(tick, 15);
  }

I’ve highlighted one change in red; the call to the new initWorldObjects function. This function creates JavaScript objects to represent the scene, and we’ll come to it shortly, but before we do it’s also worth noting another change here; all of our previous webGLStart functions had two lines to set up depth-testing:

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

These have been removed for this example. You will probably remember from last time that blending and depth-testing don’t play well together, and we use blending all the time in this example. Depth-testing is off by default, so by removing those lines from our usual boilerplate we’re all set.

The next big change is in the animate function. Previously we used this to update the various global variables that represented the changing aspects of our scene — for example, the angle by which we should rotate a cube before drawing it. The change we’ve made here is quite simple; instead of updating variables directly, we loop through all of the objects in our scene and tell each one to animate itself:

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

      for (var i in stars) {
        stars[i].animate(elapsed);
      }
    }
    lastTime = timeNow;

  }

Working our way up, drawScene comes next. This has changed enough that I won’t highlight the specific changes; instead, we can go through it bit by bit. Firstly:

  function drawScene() {
    gl.viewport(0, 0, gl.viewportWidth, gl.viewportHeight);
    gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);

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

This is just our usual setup code, unchanged since lesson 1.

    gl.blendFunc(gl.SRC_ALPHA, gl.ONE);
    gl.enable(gl.BLEND);

Next, we switch on blending. We’re using the same blending as we did in the last lesson; you will remember that this allows objects to “shine through” each other. Usefully, it also means that black parts of an object are drawn as if they were transparent; to see how that works, take a look at the description of the blending function in the last lesson. What this means is that when we are drawing the stars that make up our scene, the black bits will seem transparent; indeed, the less bright a part of the star is, the more transparent it will seem. And as the star is drawn using this texture:

The star texture image

…this gets us just the effect we want.

On to the next part of the code:

    loadIdentity();
    mvTranslate([0.0, 0.0, zoom]);
    mvRotate(tilt, [1.0, 0.0, 0.0]);

So, here we just move to the centre of the scene, and then zoom in appropriately. We also tilt the scene around the X axis; the zoom and the tilt are still global variables controlled from the keyboard. We’re now pretty much ready to draw the scene, so first we check whether the “twinkle” checkbox is checked:

    var twinkle = document.getElementById("twinkle").checked;

…and then, just as we did when animating them, we iterate over the list of stars and tell each one to draw itself. We pass in the current tilt of the scene and the twinkle value. We also tell it what the current “spin” is — this is used to make the stars spin around their centres as they orbit the centre of the scene.

    for (var i in stars) {
      stars[i].draw(tilt, spin, twinkle);
      spin += 0.1;
    }

  }

So, that’s drawScene. We’ve now seen that the stars are clearly capable of drawing and animating themselves; the next code is the bit that creates them:

  var stars = [];
  function initWorldObjects() {
    var numStars = 50;

    for (var i=0; i < numStars; i++) {
      stars.push(new Star((i / numStars) * 5.0, i / numStars));
    }
  }

So, a simple loop creating 50 stars (you might want to try experimenting with larger or smaller numbers). Each star is given a first parameter specifying a starting distance from the centre of the scene and a second specifying a speed to orbit the centre of the scene, both of which come from their position in the list.

The next code to look at is the class definition for the stars. If you're not used to JavaScript, it will look really odd. (If you know JavaScript well, click here to skip my explanation of its object model.)

JavaScript's object model is very different to other languages'. The way I've found it easiest to understand is that each object is created as a dictionary (or hashtable, or associative array) and then is turned into a fully-fledged object by putting values into it. The object's fields are simply entries in the dictionary that map to values, and the methods are entries that map to functions. Now, we add to that the fact that for single-word keys, the syntax foo.bar is a valid JavaScript shortcut for foo["bar"], and you can see how you get syntax similar to other languages' from a very different starting point.

Next, when you're in any JavaScript function, there is an implicitly-bound variable called this which refers to the function's "owner". For global functions this is a global per-page "window" object, but if you put the keyword new before it then it will be a brand-new object instead. So, if you have a function that sets this.foo to 1 and this.bar to a function, and then you make sure you always call it with the new keyword, it's basically the same as a constructor combined with a class definition.

Next, we can note that if a function is called using method invocation-like syntax (that is, foo.bar()), then this will be bound to the function's owner (foo), just as we'd expect, so the object's methods can do stuff to the object itself.

Finally, there's one special attribute associated with a function, prototype. This is a dictionary of values that are associated with every object that is created using the new keyword with that function; this is a good way of setting values that will be the same for every object of that "class" — for example, methods.

[Thanks to murphy and doug in the comments and to this page by Sergio Pereira for helping me correct the original version of that explanation.]

Let's take a look at the function we write to define a Star object for this scene.

  function Star(startingDistance, rotationSpeed) {
    this.angle = 0;
    this.dist = startingDistance;
    this.rotationSpeed = rotationSpeed;

    // Set the colors to a starting value.
    this.randomiseColors();
  }

So, in the constructor function, the star is initialised with the values we provide and a starting angle of zero, and then a method is called. The next step is to bind the methods to the Star function's associated prototype so that all new Stars have the same methods. First, the draw method:

  Star.prototype.draw = function(tilt, spin, twinkle) {
    mvPushMatrix();
 

So, draw is defined to take the parameters we passed in to it back in the main drawScene function. We start it off by pushing the current model-view matrix onto the matrix stack so that we can move around without fear of having side-effects elsewhere.

    // Move to the star's position
    mvRotate(this.angle, [0.0, 1.0, 0.0]);
    mvTranslate([this.dist, 0.0, 0.0]);

Next we rotate around the Y axis by the star's own angle, and move out by the star's distance from the centre. This puts us in the correct position to draw the star.

    // Rotate back so that the star is facing the viewer
    mvRotate(-this.angle, [0.0, 1.0, 0.0]);
    mvRotate(-tilt, [1.0, 0.0, 0.0]);

These lines are required so that when we alter the tilt of the scene using the cursor keys, the stars still look right. They are drawn as a 2D texture on a square, which looks right when we're looking at it straight on, but would just look like a line if we tilted the scene so that we were viewing it from the side. For similar reasons, we also need to back out the rotation required to position the star. When you "undo" rotations like this, you need to do so in the reverse of the order you did them in the first place, so first we undo the rotation from our positioning, and then the tilt (which was done in drawScene).

The next lines are to draw the star:

    if (twinkle) {
      // Draw a non-rotating star in the alternate "twinkling" color
      gl.uniform3f(shaderProgram.colorUniform, this.twinkleR, this.twinkleG, this.twinkleB);
      drawStar();
    }

    // All stars spin around the Z axis at the same rate
    mvRotate(spin, [0.0, 0.0, 1.0]);

    // Draw the star in its main color
    gl.uniform3f(shaderProgram.colorUniform, this.r, this.g, this.b);
    drawStar();

Let's ignore the code that's executed for the twinkling effect for a moment. The star is drawn by first rotating around the Z axis by the spin that was passed in, so that it rotates around its own centre while it orbits the centre of the scene. We then push the star's colour up to the graphics card in a shader uniform, and then call a global drawStar function (which we'll come to in a moment).

Now, what about that twinkling stuff? Well, the star has two colours associated with it — its normal colour, and its "twinkling colour". To make it twinkle, before we draw the star itself we draw a non-spinning star in a different colour. This means that the two stars are blended together, making a nice bright colour, but also means that rays that come out of the first star to be drawn are stationary while the ones that come out of the second star are rotating, giving a nice effect. That's our twinkling.

So, once we've drawn the star, we just pop our model-view matrix from the stack and we're done:

    mvPopMatrix();
  };

The next method we bind to the prototype is the one to animate the star:

  Star.prototype.animate = function(elapsedTime) {
    var effectiveFPMS = 60 / 1000;

As in the previous lessons, instead of just getting the scene to update as fast as it can, I've chosen to make it change at a steady pace for everyone, so people with faster computers get smoother animations and people with slower ones jerkier. Now, I think that the numbers for the angular speed at which the stars orbit the centre of the scene and at which they move towards the centre were carefully calculated by NeHe, so rather than messing around with them I decided that it was best to assume that the numbers were calibrated for 60 frames per second and then use that and the elapsedTime (which you'll remember is the time between calls to the animate function) to scale the amount we move at each animation "tick" appropriately. elapsedTime is in milliseconds, and so we want an effective frames-per-millisecond of 60 / 1000.

So, now we have this number we can adjust the star's angle — that is, how far around its orbit of the centre of the scene it is:

    this.angle += this.rotationSpeed * effectiveFPMS * elapsedTime;

...and we can adjust its distance from the centre, moving it out to the outside of the scene and resetting its colours to something random when it finally reaches the centre:

    // Decrease the distance, resetting the star to the outside of
    // the spiral if it's at the center.
    this.dist -= 0.01 * effectiveFPMS * elapsedTime;
    if (this.dist < 0.0) {
      this.dist += 5.0;
      this.randomiseColors();
    }

  };

The final bit of code that makes up the Star's prototype is that code we saw used in the constructor and just now in the animation code to randomise its twinkling and non-twinkling colours:

  Star.prototype.randomiseColors = function() {
    // Give the star a random color for normal
    // circumstances...
    this.r = Math.random();
    this.g = Math.random();
    this.b = Math.random();

    // When the star is twinkling, we draw it twice, once
    // in the color below (not spinning) and then once in the
    // main color defined above.
    this.twinkleR = Math.random();
    this.twinkleG = Math.random();
    this.twinkleB = Math.random();
  };

...and we're done with the star's prototype. So, that's how a star object is created, complete with methods to draw and animate it. Now, just above these functions, you can see the (rather dull) code that draws the star: it just draws a square in a manner that will be familiar from the first lesson, using an appropriate texture and vertex position/texture coordinate buffers:

  function drawStar() {
    gl.activeTexture(gl.TEXTURE0);
    gl.bindTexture(gl.TEXTURE_2D, starTexture);
    gl.uniform1i(shaderProgram.samplerUniform, 0);

    gl.bindBuffer(gl.ARRAY_BUFFER, starVertexTextureCoordBuffer);
    gl.vertexAttribPointer(shaderProgram.textureCoordAttribute, starVertexTextureCoordBuffer.itemSize, gl.FLOAT, false, 0, 0);

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

    setMatrixUniforms();
    gl.drawArrays(gl.TRIANGLE_STRIP, 0, starVertexPositionBuffer.numItems);
}

A little further up, you can see initBuffers, which sets up those vertex position and texture coordinate buffers, and then the appropriate code in handleKeys to manipulate the zoom and tilt functions when the user presses the up/down cursor keys or the Page Up/Page Down keys. A little further up again, and you will see the initTexture and handleLoadedTexture functions have been updated to load the new texture. All of that is so simple that I won't bore you by describing it :-)

Let's just go right up to the shaders, where you can see the last change that was required for this lesson. All of the lighting-related stuff has been removed from the vertex shader, which is now just the same as it was for lesson 5. The fragment shader is a little bit more interesting:

  #ifdef GL_ES
  precision highp float;
  #endif

  varying vec2 vTextureCoord;

  uniform sampler2D uSampler;

  uniform vec3 uColor;

  void main(void) {
    vec4 textureColor = texture2D(uSampler, vec2(vTextureCoord.s, vTextureCoord.t));
    gl_FragColor = textureColor * vec4(uColor, 1.0);
}

...but not that much more interesting :-) All we're doing is picking up that colour uniform that was pushed up here by the code in the star's draw method and using it to tint the texture, which is monochrome. This means that our stars appear in the appropriate colour.

And that's it! Now you know all there is to learn from this lesson: how to create JavaScript objects to represent things in your scene, and to give them methods that allow them to be separately animated and drawn.

Next time, we'll show how to load up a scene from a simple file, and take a look at how you can have a camera moving through it, combining these to make kind of nano-Doom :-)

<< Lesson 8Lesson 10 >>

Acknowledgments: As always, 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.

23 Responses to “WebGL Lesson 9 – improving the code structure with lots of moving objects”

  1. murphy says:

    That was a nice lesson, I can see that Christmas is approaching :)

    Your explanations about “this” are not fully correct though. “this” is not pointing to the function, but to the receiver of the function call, which is determined by the calling statement obj.fun(). The function isn’t copied.

    By the way, you could also hide most of the members of Star by using the closure scope; only draw() and animate() need to be visible from the outside. Here’s a patch: http://pastie.textmate.org/716668. But maybe I just like closure variables more than member variables :)

  2. murphy says:

    Oh, and as for nano-Doom: I am very excited about that! We don’t get any weapons yet, do we?

  3. Paul says:

    First off this is an excellent addition to a brilliant resource. I’ve learned so much from this site in the last couple of weeks. I have discovered one little issue with the code which shows up in several tutorials. It isn’t effecting anything yet, but the following line may cause you trouble down the line:

    gl.uniform1f(gl.getUniformLocation(shaderProgram, “uSampler”), 0);

    This works fine when you only have one texture in your shader; but, if you want to use more then one, it won’t complain or error, just not work as it’s expecting an int not a float:

    gl.uniform1i(gl.getUniformLocation(shaderProgram, “uSampler”), 0);

    Keep up the great work.

  4. giles says:

    @murphy — thanks for explaining that! I’ll read up a little on JavaScript functions and receivers and fix the description appropriately. Unfortunately the nano-Doom is a bit too primitive for weapons, or indeed monsters. It does have a bobbing-head effect, though :-)

    @Paul — glad you like the posts! That’s a great point about the texture uniforms, I’ve tweaked this and the previous tutorials so that future readers don’t get caught out.

  5. giles says:

    @murphy — OK, hopefully that’s better now! I’ve not put in the use of the closure scope, I think I need to think a bit more about that before using it — to a Python guy it looks very odd :-)

  6. murphy says:

    Yes, I see…you certainly wouldn’t do that in Python! I just happen to play around with closures a lot, and there are fascinating things you can do with them – but this is a WebGL tutorial, not a functional programming tutorial :) Keep up the great work!

  7. doug says:

    It’s not much of an issue with only 50 stars, but your code is creating a new instance of the draw and animate functions per star instance. They’re all the same, so you don’t need to have separate copies per star. Instead of creating the draw and animate functions in the constructor and assigning them to fields of the newly-constructed instance, you should create and assign them to the prototype, so that all Stars can use the same ones:

    function Star(startingDistance, rotationSpeed) {
    this.angle = 0;
    this.dist = startingDistance;
    this.rotationSpeed = rotationSpeed;
    };

    Star.prototype.draw = new function(tilt, spin, twinkle) { … }

    Star.prototype.animate = new function(elapsedTime) { … }

  8. giles says:

    doug — that sounds like a good plan, I don’t want to get people new to JavaScript into bad habits. I’ll update the lesson.

  9. Hao says:

    The zoom in/out keyboard mapping seems to be inconsistent with the previous lessons. It may be nice to keep the same mapping throughout…

  10. giles says:

    Good point, I’ll fix that.

  11. giles says:

    Fixed.

  12. Neppord says:

    Hi the WebGL version seems to be non working on newest webkitt nightly. it starts but renders incorrectly. It appears to be at first, but the cpu dose not seem to be overloaded. Have something changed in the way WebGL renders scenes since this lesson was made? I remember that i it didn’t lag when it was new.

  13. giles says:

    @Neppord — still looks OK for me on Minefield and Chrome on Windows. What way is it going wrong for you with WebKit?

  14. Neppord says:

    Now its working. maybe new ver of webkit.

  15. giles says:

    Hmm, weird! Perhaps there was just a bad build.

  16. m.sirin says:

    hi giles!
    I’m using objects that have almost the same structure as yours. for each instantiated object (for example spheres) a buffer is created via createBuffer() and data is appended,too then it is stored as a object attribute. but this is only for the first time of drawing this object, then when the object-draw-function is called at least a second time the buffer is just used as usual with bindBuffer etc. Each object has a drawing method that contains such things like mvPushMatrix, drawArrays (…) and mvPopMatrix. In case the object is called let’s say 250 times the performance is really bad (something like 1-2 fps). removing all the matrix-operations mvPopMatrix, mvTranslate, mvRotate,PushMatrix helps a lot improving performance. also fps are increased when removing drawArrays. but of course i need these things to run the stuff..
    so the speed-killers are : mv-Matrix-Functions (or is it the slow matrix operations?), drawArrays (drawElements) and i think bindBuffer,too.

    what would you suggest to do for get rid of this performance-problem?

    And yes, it is important to use such independent objects, because I want to do stuff like transform a single object or change color of a single certain object etc.

    I look forward for your answer :)

  17. m.sirin says:

    hi, now i tried another matrix-framework which you mentioned earlier. it is tojiros glMatrix-0.9.4 and is really much faster than sylvester’s matrices. so my above mentioned mvMatrix performance problem is solved. but still remaining is the drawArray issue..i’ll look around to find a solution :)

    bye

  18. vavo says:

    var distances = []

    This varible is never called. The program works without this line.

  19. giles says:

    @m.sirin — for some reason I never saw your comments, sorry for not replying! Thanks for the suggestion of using Tojiro’s matrix library, I might port the lessons over to it. re: drawArrays — not sure how you can work around that problem…

    @vavo — thanks for spotting that, I’ll remove it.

  20. WarrenFaith says:

    Was a bit shocked at the beginning because my performance was multiple times slower than you sample. This sample was the first time I recognized that the debugging option from khronos ( http://www.khronos.org/webgl/wiki/Debugging ) has a huge impact on the performance… thought it wouldn’t be so much…

    I also thought that “improving the code structure” will be a bit more improved… but I guess I am just fastidious (is this the right word?!) by Java and not used to the JavaScript way of programming…

    Nice sample, thought it will be colored light instead of blended textures at first glance…

  21. giles says:

    That does sound like a big hit from the debugger! That said, this demo does hit the GPU quite frequently, and presumably the problem is a per-call cost.

    Re: improving the code structure — some of it might be differences in what’s regarded as good structure in JavaScript versus what’s regarded as good in Java. Neither’s necessarily better than the other (and I speak as someone who coded very fastidious [yup, good word!] Java from 1995-2005 and then even-more-fastidious Python from 2005-2011), but the natural “idiomatic” style is different in a strongly-typed class-based language versus a dynamically-typed primarily functional language with non-class-based OO support. OTOH some of the stuff that looks like ill-structured code in this might just be me writing bad code, it’s been known to happen :-) If you get a chance to point out any bits that look particularly messy I’d be glad to hear about them.

  22. deathy/Brainstorm says:

    Minor suggestion for performance:

    effectiveFPMS is calculated for every single star every frame. This is a pretty static value and could be set as a global value so that it only gets calculated once.

    It’s true that it’s something that won’t likely get noticed on modern hardware, but these things will still add up and thinking about code optimization is going to be more important with WebGL than other modern languages just because of its nature.

  23. giles says:

    Thanks, deathy/Brainstorm — I’ll make that change soon.

Leave a Reply

Subscribe to RSS Feed Follow me on Twitter!