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. We’ve also reversed the direction of spin of the cube (just because it looks prettier), so 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:

    mat4.rotate(mvMatrix, degToRad(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:

    mat4.rotate(mvMatrix, degToRad(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 have 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 it 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);
    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.

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

  1. athom says:

    hi, @giles one question,
    can the line in drawScene() be removed?
    gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, cubeVertexIndexBuffer);
    I think it has already done in initBuffer()

  2. pravin mali says:

    you must have used 8 different vertices of cube and should have used only 8 indices to refer to vertices instead of 23 in cubeVertexIndices

  3. And1 says:

    this line is not necessary since size doesn’t need to be supplied for index buffers:
    cubeVertexIndexBuffer.itemSize = 1;

    Also, in drawScene you shouldn’t compute the perspective matrix every time, it should just be done once.

    Generally I think you could’ve used more objects or at least breaked down some functions into smaller ones instead of having everything in one script in large functions, it got really big till this point. (I know probably the reason for doing it so was to have everything in one place focusing more on the webgl stuff but at the end it cost on being able to look through on the code – maybe having one object instead of two would have helped this.

    One last thing is that I personally dont like writing code that I dont understand 100%, so here and there I could have used more explanation, like the usage parameter in gl.bufferData which I had to look up.

    Sorry for picking on so many things, its meant to be constructive, I like this tutorial by the way!

  4. And1 says:

    pravin mali:
    he could have used 8 vertices but then how do you achieve to have one solid color for each side instead of an interpolated one?
    he used 36 vertex indexes not 23 as I see it, but how can you do this with just 8 ? I dont see it. If you do a triangle strip for each 6 faces you can get it down to 24, if you use one big triangle strip you can lower it to 16 but you shouldn’t do that because of texturing and light issues.

  5. Sharma says:

    How does WebGL figure out which array is to be indexed into when you say gl.drawElements giving cubeVertexIndexBuffer?

  6. Bismita Sahu says:

    how should i decide where in the co ordinate system i should draw the cube.suppose i want to draw the cube connecting two points like (-5,5,1) and (5,5,1).the cube need not rotate.

  7. [...] 以下の文章は、WebGL Lesson 5 – introducing texturesの日本語訳です。 WebGLのチュートリアルシリーズ第五回へようこそ。このチュートリアルは、第六回NeHeさんのOpenGLチュートリアルの基礎になります。チュートリアルは、分離したファイルから画像を読み込むといったものです。これは、複雑なオブジェクトを描画せずに、リアルな3D空間を表現するのにお手軽な手法です。ダンジョンゲームで岩が落下するところを想像してみてください。おそらく、ひとつひとつの岩をダンジョンの壁と別のオブジェクトとしてモデリングしたくはないでしょう。よって、壁全体はひとつのオブジェクトとして扱い、岩の画像を用意して壁を補うでしょう。 レッスンの内容をWebGLをサポートするブラウザで動かした様子です。 WebGLをサポートするブラウザ環境であれば、リンクをクリックして、WebGLの現行版をチェックしてみてください。もし持っていなければ、リンクから導入方法がわかります。 より詳しい原理を下記します。 注意点:レッスンは、プログラミングの知識を持っており、3Dグラフィックスの経験がないひとを対象にしています。できる限り早く個人の3D Webページを持てるよう、深く理解していただけると幸いです。すでに前回のチュートリアルをご覧になられていたら、この記事を読む前に読んでいただきたいです。なぜなら、このチュートリアルでは、レッスン4と新しいコードとの違いしか説明していないからです。 このチュートリアルには、バグや誤りがあるかもしれません。なにか誤りがあれば、コメントや訂正をできる限り早く教えていただきたいです。 例で紹介しているコードを手に入れるには、2つのやり方があります。表示されているものを読むようなまさにソースを見るやり方。Githubを使って、リンクのリポジトリからcloneしてくる(他のレッスンも)方法もあります。どちらかのやり方でコードを手に入れて、お気に入りのテキストエディタに読み込んで読んでみてください。 テクスチャがどのように機能しているか理解するには、3Dオブジェクト上で色の点がセットされる特有の仕組みを知ることです。レッスン2で説明したように、色がどのようにフラグメントシェーダによって指定されるかを覚えていたら、画像を読み込んで、画像をフラグメントシェーダに送るには何が必要かわかりますか。フラグメントシェーダは、フラグメントを利用して画像のどの部分を取得するのか知る必要がありましたね。 テクスチャを読み込むコードを見ていきましょう。ページ下部にあるWebGLStartのJavaScriptの実行結果を見てください。 [...]

  8. erilem says:

    I wonder why gl.bindBuffer should be called prior to calling gl.drawElements. (Sorry I have to say that I haven’t actually tried to remove that call and see what the result is.)

  9. [...] Lesson 4: Some Real 3D Objects builds on lesson 3, bringing us into the third dimension fully by replacing the triangle with a pyramid and the square with a cube. [...]

  10. Dima says:

    Good tutorial.

    // 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,
    ];
    Remove last comma after last number.

    I would suggest to use more red remarks, it helps. I didn’t change new Float32Array(colors) to unpackedColors at first. And spend some time to find out why my cube didn’t print.

  11. frnkschwns says:

    hey,

    thanks for this great tutorial!

    btw, you forgot a semicolon after the declaration of cubeVertexIndices.

    cheers :)

  12. LanfeustXIII says:

    Hello,

    I have difficulties to understand what contains cubeVertexIndexBuffer, could ou give me some explanation please ?

    Thanks :)

  13. Pehat says:

    Hello,

    great tutorial! But when I read it, I see something like that: http://imageshack.us/photo/my-images/141/nonamede.png/ Gray navigation bar gives me no chance to read some lines of code :( Fix it if you can, please.

  14. Allen says:

    To get rid of the sidebar, open the Chrome Developer Tools by right-clicking the sidebar and selecting “Inspect Element.” Then, under the Element tab in the Developer Tools look to the right you see element.style {}. Write “display: none;” in there, and viola, right side-panel gone. :-)

  15. Allen says:

    @Giles, you need to add in the css a selector for the pre tags that contain your code:

    word-wrap: break-word;

  16. Potomac says:

    Great lessons! Thank you for taking the time and sharing as you learn! Really – thank you.

    I have one constructive criticism though, which may help out others as well; instead of labeling cubeVertexIndexBuffer as cubeVertexIndexBuffer, maybe cubeIndexBuffer or cubeVertexAndColorIndexBuffer or cubeVertexColorTextureIndexBuffer.

    This removes ambiguity and better indicates what the IndexBuffer is doing – it’s tying together the VertexBufferArray and ColorBufferArray (here in Lesson 4).

    You did mention this in the paragraph following the IndexBuffer code above, but my left-hemisphere-brain argued with my right-hemisphere-brain for a while ! ;-)

  17. Debbie says:

    Can you tell me how do I get a bottom in the pyramid ..? I thought to create 2 triangles and connect them but I do not know how to connect the points of indexbuffer..

  18. virtual stranger says:

    @Debbie

    to get a bottom to the pyramid we need to recognize that this is a square pyramid and not a tetrahedron. that means it has a square for the base (fifth side) and not just another triangle.

    so… we have to add not one but two triangles to complete the pyramid:

    pyramidVertexPositionBuffer = gl.createBuffer();
    gl.bindBuffer(gl.ARRAY_BUFFER, pyramidVertexPositionBuffer);
    var vertices =
    [
    0.0, 1.0, 0.0,
    -1.0, -1.0, 1.0,
    1.0, -1.0, 1.0,

    0.0, 1.0, 0.0,
    1.0, -1.0, 1.0,
    1.0, -1.0,-1.0,

    0.0, 1.0, 0.0,
    1.0, -1.0,-1.0,
    -1.0, -1.0,-1.0,

    0.0, 1.0, 0.0,
    -1.0, -1.0,-1.0,
    -1.0, -1.0, 1.0,

    1.0, -1.0, 1.0, // square bottom, triangle 1
    1.0, -1.0,-1.0,
    -1.0, -1.0,-1.0,

    1.0, -1.0, 1.0, // square bottom, triangle 2
    -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 = 18;

    and we have to appropriately expand the color vector and color our new vertices to match the other sides:

    pyramidVertexColorBuffer = gl.createBuffer();
    gl.bindBuffer(gl.ARRAY_BUFFER, pyramidVertexColorBuffer);
    var colors =
    [
    1.0, 0.0, 0.0, 1.0,
    0.0, 1.0, 0.0, 1.0,
    0.0, 0.0, 1.0, 1.0,

    1.0, 0.0, 0.0, 1.0,
    0.0, 0.0, 1.0, 1.0,
    0.0, 1.0, 0.0, 1.0,

    1.0, 0.0, 0.0, 1.0,
    0.0, 1.0, 0.0, 1.0,
    0.0, 0.0, 1.0, 1.0,

    1.0, 0.0, 0.0, 1.0,
    0.0, 0.0, 1.0, 1.0,
    0.0, 1.0, 0.0, 1.0,

    0.0, 0.0, 1.0, 1.0, // 2: blue
    0.0, 1.0, 0.0, 1.0, // 3: green
    0.0, 0.0, 1.0, 1.0, // 4: blue

    0.0, 0.0, 1.0, 1.0, // 2: blue
    0.0, 0.0, 1.0, 1.0, // 4: blue
    0.0, 1.0, 0.0, 1.0 // 1: green
    ];
    gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(colors), gl.STATIC_DRAW);
    pyramidVertexColorBuffer.itemSize = 4;
    pyramidVertexColorBuffer.numItems = 18;

    one last touch, we can’t really see the bottom unless we rotate only around the x-axis to give ourselves a good view of the new bottom:

    mat4.identity(mvMatrix);
    mat4.translate(mvMatrix, [-1.5, 0.0, -7.0]);
    mvPushMatrix();
    mat4.rotate(mvMatrix, degToRad(rPyramid), [1, 0, 0]);

    et voila!

  19. kevin says:

    this is great tutorial.
    I make some changes and make the cube circle around the pyramid.
    try the link

    http://echofromfuture.com/webglsamples.html

  20. Gabrielle says:

    Cool one. I’ve added two triangles to the pyramid to close it at the bottom.

Leave a Reply

Subscribe to RSS Feed Follow Learning WebGL on Twitter