<< 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:
…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 Star
s 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 :-)
Acknowledgments: As always, I'm deeply in debt to NeHe for his OpenGL tutorial for the script for this lesson.
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 :)
Oh, and as for nano-Doom: I am very excited about that! We don’t get any weapons yet, do we?
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.
@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.
@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 :-)
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!
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) { … }
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.
The zoom in/out keyboard mapping seems to be inconsistent with the previous lessons. It may be nice to keep the same mapping throughout…
Good point, I’ll fix that.
Fixed.
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.
@Neppord — still looks OK for me on Minefield and Chrome on Windows. What way is it going wrong for you with WebKit?
Now its working. maybe new ver of webkit.
Hmm, weird! Perhaps there was just a bad build.
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 :)
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
var distances = []
This varible is never called. The program works without this line.
@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.
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…
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.
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.
Thanks, deathy/Brainstorm — I’ll make that change soon.