WebGL Lesson 4 – some real 3D objects

<< Lesson 3Lesson 5 >>

Welcome to my number four in my series of WebGL tutorials. This time we’re going to display some 3D objects. The lesson is based on number 5 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, second, or third tutorials already, you should probably do so before reading this one — here I will only explain the differences between the code for lesson 3 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.

The differences between the code for this lesson and the previous one are entirely concentrated in the animate, the initBuffers, and the drawScene functions. If you scroll down to animate now, you’ll see one first, very minor change: the variables that remember the current rotation state of the two objects in the scene have been renamed; they used to be rTri and rSquare, and now we have:

      rPyramid += (90 * elapsed) / 1000.0;
      rCube -= (75 * elapsed) / 1000.0;
 

That’s all for that function; let’s move up to drawScene. Just above the function declaration, we have definitions for the new variables:

  var rPyramid = 0;
  var rCube = 0;

Next comes the function header, followed by our setup code and the code to move into position for drawing the pyramid. Once that’s all done, we rotate it about the Y axis just as we did for the triangle in the previous lesson:

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

…and then we draw it. The only difference between the code in the last lesson that drew the colourful triangle and the new code to draw our equally-pretty pyramid is that there are more vertices, and equally more colours, so that will all be handled in initBuffers (which we’ll look at in a moment). This means that apart from a change in the names of the buffers we use, the code is identical:

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

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

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

Right, that was easy. Let’s look at the code for the cube. The first step is to rotate it; this time, instead of just rotating on the X axis, we’ll rotate around an axis that is (from the perspective of the viewer) upwards, to the right, and towards you:

    mvRotate(rCube, [1, 1, 1]);

Next, we draw the cube. This is a little more involved. There are three ways we can draw a cube:

  1. Use a single triangle strip. If the whole cube was one colour, this would be reasonably easy — we could use the vertex positions we’ve been using until now to draw a front face, then add another two points to add another face, and another two for another, and so on. This would be very efficient. Unfortunately, we want every face to have a different colour. Because each vertex specifies a corner of the cube, and each corner is shared between three faces, we’d need to specify each vertex three times, and doing this would be so tricky that I won’t even try to explain it…
  2. We could cheat, and draw our cube by drawing six separate squares, one for each face, with separate sets of vertex positions and colours for each. The first version of this lesson (prior to 30 October 2009) actually did this, and it worked just fine. However, it wouldn’t be good practice; because it costs a certain amount in terms of time every time you tell WebGL to draw another object in your scene, it’s much better to have a minimum number of calls to drawArrays.
  3. The final option is to specify the cube as six squares, each made up of of two triangles, but to send that all over to WebGL to be drawn in one go. This is similar to the way we would done it with a triangle strip, but because we’re specifying the triangles in their entirety each time rather than simply defining each triangle by adding a single point on to the previous one, it’s easier to specify the per-side colours. It also has the advantage that the neatest way to code it lets me introduce a new function, drawElements — so it’s the way we’re going to do it :-)

The first step is to associate the buffers containing the cube’s vertex positions and colours that we’ll be creating in initBuffers with the appropriate attributes, just as we did with the pyramid:

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

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

The next step is to draw the triangles. There’s a bit of a problem here. Let’s consider the front face; we have four vertex positions for it, and each of them has an associated colour. However, it needs to be drawn using two triangles, and because we’re using simple triangles, which need their own vertices specified individually, rather than triangle strips, which can share vertices, we have to specify six vertices in total for it. But we only have four for it in our array buffer.

What we want to do is specify something like “draw a triangle made up of the first three vertices in the array buffer, then draw another made out of the first one, the third, and the fourth”. This would draw our front face; drawing the rest of the cube would be similar. And this is exactly what we do.

We use something called an element array buffer and a new call, drawElements, for this. Just like the array buffers we’ve been using until now, the element array buffer will be populated with appropriate values in initBuffers, and it will hold a list of vertices using a zero-based index into the arrays we used for the positions and the colours; we’ll take a look at that in a moment.

In order to use it, we make our cube’s element array buffer the current one (WebGL keeps different current array buffers and element array buffers, so we must specify which one we’re binding in the call to gl.bindBuffer), then we do the normal code to push our model-view and projection matrices up to the graphics card, then and call drawElements to draw the triangles:

    gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, cubeVertexIndexBuffer);
    setMatrixUniforms();
    gl.drawElements(gl.TRIANGLES, cubeVertexIndexBuffer.numItems, gl.UNSIGNED_SHORT, 0);

That’s if for drawScene. The remainder of the code is in initBuffers, and is pretty obvious. We define buffers with new names to reflect the new kinds of objects we’re dealing with, and we add a new one in for the cube’s vertex index buffer:

  var pyramidVertexPositionBuffer;
  var pyramidVertexColorBuffer;
  var cubeVertexPositionBuffer;
  var cubeVertexColorBuffer;
  var cubeVertexIndexBuffer;

We put values in the pyramid’s vertex position buffer for all of the faces, with an appropriate change to the numItems:

    pyramidVertexPositionBuffer = gl.createBuffer();
    gl.bindBuffer(gl.ARRAY_BUFFER, pyramidVertexPositionBuffer);
    var vertices = [
        // Front face
         0.0,  1.0,  0.0,
        -1.0, -1.0,  1.0,
         1.0, -1.0,  1.0,
        // Right face
         0.0,  1.0,  0.0,
         1.0, -1.0,  1.0,
         1.0, -1.0, -1.0,
        // Back face
         0.0,  1.0,  0.0,
         1.0, -1.0, -1.0,
        -1.0, -1.0, -1.0,
        // Left face
         0.0,  1.0,  0.0,
        -1.0, -1.0, -1.0,
        -1.0, -1.0,  1.0
    ];
    gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertices), gl.STATIC_DRAW);
    pyramidVertexPositionBuffer.itemSize = 3;
    pyramidVertexPositionBuffer.numItems = 12;

…likewise for the pyramid’s vertex colour buffer:

    pyramidVertexColorBuffer = gl.createBuffer();
    gl.bindBuffer(gl.ARRAY_BUFFER, pyramidVertexColorBuffer);
    var colors = [
        // Front face
        1.0, 0.0, 0.0, 1.0,
        0.0, 1.0, 0.0, 1.0,
        0.0, 0.0, 1.0, 1.0,
        // Right face
        1.0, 0.0, 0.0, 1.0,
        0.0, 0.0, 1.0, 1.0,
        0.0, 1.0, 0.0, 1.0,
        // Back face
        1.0, 0.0, 0.0, 1.0,
        0.0, 1.0, 0.0, 1.0,
        0.0, 0.0, 1.0, 1.0,
        // Left face
        1.0, 0.0, 0.0, 1.0,
        0.0, 0.0, 1.0, 1.0,
        0.0, 1.0, 0.0, 1.0
    ];
    gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(colors), gl.STATIC_DRAW);
    pyramidVertexColorBuffer.itemSize = 4;
    pyramidVertexColorBuffer.numItems = 12;

…and for the cube’s vertex position buffer:

    cubeVertexPositionBuffer = gl.createBuffer();
    gl.bindBuffer(gl.ARRAY_BUFFER, cubeVertexPositionBuffer);
    vertices = [
      // Front face
      -1.0, -1.0,  1.0,
       1.0, -1.0,  1.0,
       1.0,  1.0,  1.0,
      -1.0,  1.0,  1.0,

      // Back face
      -1.0, -1.0, -1.0,
      -1.0,  1.0, -1.0,
       1.0,  1.0, -1.0,
       1.0, -1.0, -1.0,

      // Top face
      -1.0,  1.0, -1.0,
      -1.0,  1.0,  1.0,
       1.0,  1.0,  1.0,
       1.0,  1.0, -1.0,

      // Bottom face
      -1.0, -1.0, -1.0,
       1.0, -1.0, -1.0,
       1.0, -1.0,  1.0,
      -1.0, -1.0,  1.0,

      // Right face
       1.0, -1.0, -1.0,
       1.0,  1.0, -1.0,
       1.0,  1.0,  1.0,
       1.0, -1.0,  1.0,

      // Left face
      -1.0, -1.0, -1.0,
      -1.0, -1.0,  1.0,
      -1.0,  1.0,  1.0,
      -1.0,  1.0, -1.0,
    ];
    gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertices), gl.STATIC_DRAW);
    cubeVertexPositionBuffer.itemSize = 3;
    cubeVertexPositionBuffer.numItems = 24;

The colour buffer is marginally more complex, because we use a loop to create a list of vertex colours so that we don’t have to specify each colour four times, one for each vertex:

    cubeVertexColorBuffer = gl.createBuffer();
    gl.bindBuffer(gl.ARRAY_BUFFER, cubeVertexColorBuffer);
    var colors = [
      [1.0, 0.0, 0.0, 1.0],     // Front face
      [1.0, 1.0, 0.0, 1.0],     // Back face
      [0.0, 1.0, 0.0, 1.0],     // Top face
      [1.0, 0.5, 0.5, 1.0],     // Bottom face
      [1.0, 0.0, 1.0, 1.0],     // Right face
      [0.0, 0.0, 1.0, 1.0],     // Left face
    ];
    var unpackedColors = []
    for (var i in colors) {
      var color = colors[i];
      for (var j=0; j < 4; j++) {
        unpackedColors = unpackedColors.concat(color);
      }
    }
    gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(unpackedColors), gl.STATIC_DRAW);
    cubeVertexColorBuffer.itemSize = 4;
    cubeVertexColorBuffer.numItems = 24;

Finally, we define the element array buffer (note again the difference in the first parameter to gl.bindBuffer and gl.bufferData):

    cubeVertexIndexBuffer = gl.createBuffer();
    gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, cubeVertexIndexBuffer);
    var cubeVertexIndices = [
      0, 1, 2,      0, 2, 3,    // Front face
      4, 5, 6,      4, 6, 7,    // Back face
      8, 9, 10,     8, 10, 11,  // Top face
      12, 13, 14,   12, 14, 15, // Bottom face
      16, 17, 18,   16, 18, 19, // Right face
      20, 21, 22,   20, 22, 23  // Left face
    ]
    gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Uint16Array(cubeVertexIndices), gl.STATIC_DRAW);
    cubeVertexIndexBuffer.itemSize = 1;
    cubeVertexIndexBuffer.numItems = 36;

Remember, each number in this buffer is an index into the vertex position and colour buffers. So, the first line, combines with the instruction to draw triangles in drawScene, means that we get a triangle using vertices 0, 1, and 2, and then another using 0, 2 and 3. Because both triangles are the same colour and they are adjacent, the result is a square using vertices 0, 1, 2 and 3. Repeat for all faces of the cube, and you're done!

Now you know how to make WebGL scenes using 3D objects, and you know how to re-use the vertices you've specified in array buffers by using element array buffers and drawElements. If you have any questions, comments, or corrections, please do leave a comment below.

Next time, we'll go over texture mapping.

<< Lesson 3Lesson 5 >>

Acknowledgments: As always, I'm deeply in debt to NeHe for his OpenGL tutorial for the script for this lesson. Chris Marrin's WebKit spinning box was the inspiration for adapting this lesson to introduce element array buffers.

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

11 Responses to “WebGL Lesson 4 – some real 3D objects”

  1. Jeff says:

    First off, just found this site recently and love the tutorials/demos. My question might be a bit off topic, but I thought this would be the best lesson to comment the question under.

    How does this relate to mesh normals, and the difference values that can be exported out of a blender model, specifically blenders DirectX(.x) format? And what about animation of only certain vertexes, like the vertexes of an arm or leg?

    Seems to me that does something like that should follow this same ideas you present here, but I feel like that makes it incredibly more complex. Maybe there is just a lot more to it than I am thinking.

    Any input on that would be great! Thanks!

  2. giles says:

    Hi Jeff, glad you like the tutorials!

    You specify vertex normals just like you do their positions, as vertex attributes; similarly with the texture coordinates. You can see both of those in action in lesson 7. There’s work underway to provide a Blender “export to JSON” module, in which you’ll (eventually) be able to select which attributes you export.

    Animating certain vertices individually isn’t something I’ve covered in the tutorials yet, and it might be a while before I do. It does indeed make things quite a lot more complex…

  3. Oliver Vornberger says:

    Hi Giles,

    first of all: these are excellent tutorials. Thumb up !

    Question: Instead of storing 24 vertices in the CubeVertexPositionBuffer, would it be possible to store just 8 vertices and define the resulting 6 faces by listing the appropriate indices in cubeVertexIndexBuffer ?

  4. giles says:

    Hi Oliver — glad you like the tutorials! You could do that, but then they’d all have to be the same colour. Each vertex is a bundle of attributes — in this case a location and a colour, but in more complex models it might be location, colour, texture coordinates, and many other things.

  5. moo says:

    Don’t forget to change the animate() function!!
    rTri becomes rPyramid etc.

  6. giles says:

    Thanks, well spotted! I’ve updated the text appropriately.

  7. Phinehas says:

    I have one idea on generate the pyramid but I encounter some problem when implement it. Can you help me to solve the problem?

    I can draw a triangle to the screen first. Then, rotate the screen 90 degree and draw the triangle again. Using this method to generate four identical triangle can form a pyramid. When I implement it, I find that only one triangle can be drawn in the screen. How can I rotate the screen and place the same triangle correctly?

    for (var j = 0; j < n; j++) {
    mvPushMatrix();
    mvRotate(1, [0, 1, 0]);
    setMatrixUniforms();
    gl.drawElements(gl.TRIANGLES, pyramidVertexIndexBuffer.numItems, gl.UNSIGNED_SHORT, 0);
    mvPopMatrix();

  8. giles says:

    @Phinehas — I’m not sure if it will help, but there are two oddities in the code you pasted:

    1. You’re rotating by 1 degree rather than 90
    2. In your loop, you’re pushing the matrix in each iteration and popping it at the end, which means that you’re rotating 1 degree, drawing a triangle, going back to where you were, rotating 1 degree, drawing the triangle, going back to where you were again, and so on.

    So, if you set n to 4, pass “90″ in as the first parameter in your call to mvRotate, and remove the mvPushMatrix and mvPopMatrix calls, you may get better results.

  9. abresas says:

    Two notices:
    * You redeclare var colors and
    * the declaration of var unpackedcolors has a missing semicolon.
    Thank you for the otherwise decent javascript code.

  10. WarrenFaith says:

    I enjoyed the first 3 lessons, but here you start to jump to often between pyramid and cube changes. Maybe its because I’m not a native speaker.

    Anyway I am doing my bachelor thesis about WebGL (making a simple Benchmark and testing all possible browser and their performance).

    Your lessons help me on that way!

  11. giles says:

    @abresas — thanks! I’ll fix that.

    @Warren — glad you’re finding the lessons useful. Which bit in particular did you find confusing here?

Leave a Reply

Subscribe to RSS Feed Follow me on Twitter!