Using glDrawElements

There are two ways to use glDrawElements. Let's illustrate with a simple example. Consider the cube shown below. Note that its xmax face is on the right; its ymax face is on top, and its zmax face is towards the front. Suppose we wish to draw its 6 faces. We create a VBO with the eight vertices whose order in the VBO is as indicated in the figure.

Clearly the zmax, xmax, and zmin faces can be rendered, in that order, as:

glBindVertexArray(theVAO);
// zmax face:
glVertexAttrib3f(shaderIF->pvaLoc("mcNormal"), 0.0, 0.0, 1.0);
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
// xmax face:
glVertexAttrib3f(shaderIF->pvaLoc("mcNormal"), 1.0, 0.0, 0.0);
glDrawArrays(GL_TRIANGLE_STRIP, 2, 4);
// zmin face:
glVertexAttrib3f(shaderIF->pvaLoc("mcNormal"), 0.0, 0.0, -1.0);
glDrawArrays(GL_TRIANGLE_STRIP, 4, 4);

The remaining three faces must be done differently since the vertices are not in the order they would need to be in order to draw those faces with glDrawArrays. This is where glDrawElements comes in.

The xmin face needs to use vertices {6, 7, 0, 1}. The ymin face needs to use vertices {6, 0, 4, 2}. The ymax face needs to use vertices {1, 7, 3, 5}. Assuming the VAO is still bound (e.g., we pick up right where the rendering of the first three faces ended), the xmin face can be rendered as:

// xmin face:
GLuint xminIndices[] = {6, 7, 0, 1};
glVertexAttrib3f(shaderIF->pvaLoc("mcNormal"), -1.0, 0.0, 0.0);
glDrawElements(GL_TRIANGLE_STRIP, 4, GL_UNSIGNED_INT, xminIndices);

The code for the ymin and ymax faces is analogous and is left as an exercise. Note that – like glDrawArrays – if there are multiple VBOs associated with the VAO, then the index list passed to glDrawElements is used when extracting per-vertex data from each VBO.

Element Buffers

The code shown above represents the simplest way to use glDrawElements, and it should be perfectly adequate for anything done in this course. Nevertheless, it is important to note that the index list itself is sent to the GPU every time glDrawElements is called. For small to moderately-sized models, this is not a big deal. However, if the index lists are very long and/or a moderately large index list is referenced thousands of times or more during each display callback, you may start to see some performance degradation. This leads to an alternative approach: place the index lists into so-called "element buffers" that, like the VBOs we have seen to date, reside permanently on the GPU.

If we were to draw the final three faces of our block in that way, we would do something more like the following:

  1. At model creation time (i.e., when the constructor for a ModelView subclass is called):
    GLuint indexList[3][4] = {
    { 6, 7, 0, 1 }, // xmin face
    { 6, 0, 4, 2 }, // ymin face
    { 1, 7, 3, 5 }  // ymax face
    };
    glGenBuffers(3, ebo); // ebo is assumed to be appropriately declared as an instance variable
    for (int i=0 ; i<3 ; i++)
    {
        glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ebo[i]);
        glBufferData(GL_ELEMENT_ARRAY_BUFFER, 4*sizeof(GLuint), indexList[i], GL_STATIC_DRAW);
    }
    GL_ELEMENT_ARRAY_BUFFERS are different than the GL_ARRAY_BUFFERS we have seen to date. Among other things, this means we use neither glVertexAttribPointer nor glEnableVertexAttribArray with element buffers.
  2. At render time (and just showing rendering of the xmin face):
    // xmin face
    glVertexAttrib3f(shaderIF->pvaLoc("mcNormal"), -1.0, 0.0, 0.0);
    glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ebo[0]);
    glDrawElements(GL_TRIANGLE_STRIP, 4, GL_UNSIGNED_INT, nullptr);
  3. In the destructor: glDeleteBuffers(3, ebo);

Final Important Notes:

  1. Since we need to use different element buffers with a common VBO, we must re-bind the appropriate element buffer before each glDrawElements call. Review the code above under "2. At render time…"
  2. Passing nullptr as the final parameter to glDrawElements tells the vertex fetch processor to use the currently bound element buffer object when extracting per-vertex data for vertex shader executions.
  3. While this facility is most useful for large element buffers, it is important for Macintosh OpenGL developers to know that – at least as of fall 2019 under Mac OSX 10.14.5 – glDrawElements calls that do not use element buffers do not work. An "invalid operation" error message is generated on any glDrawElements call that passes the element array as the final parameter, and the object that you are trying to draw simply does not appear. The solution is to always use element buffers and pass nullptr as the final parameter to the glDrawElements call when running on the Macintosh under Mac OSX.

Other common use cases for glDrawElements

Consider drawing spheres, Bezier surfaces, and other similar doubly-curved surfaces. The most common way to draw them is to create adjacent rows of points sampled along the surface and render the surface as a series of GL_TRIANGLE_STRIP primitives, using corresponding points in each pair of adjacent rows. Using only glDrawArrays, interior rows of points would have to be stored twice (make sure you understand why!), hence the total number of points that must be stored in VBOs is roughly twice the number required. This can lead to significant wasted storage on the GPU.

Using glDrawElements (typically with element buffers) allows you to create and store in VBOs the data for each point in the piecewise linear approximation exactly once. Of course the cost is that you need to store the element buffer, but the storage for the element buffer will be much less than would be required to store the data for interior points twice since each requires at least 6 floating point numbers per vertex: (x, y, z) & (nx, ny, nz). For example, if we were drawing the Bezier surface on the right using a 50 x 50 grid of points and only storing coordinates and normals per-vertex, using only glDrawArrays would require 117,600 bytes, whereas using element buffers and glDrawElements would require only 79,600 bytes – 38 KB (32.3%) less GPU storage.