So far we’ve been using the
`glDrawArrays`

function for all our rendering. OpenGL ES
offers another way of kicking off a sequence of triangles (or lines or
points) through the use of the `glDrawElements`

function.
It has much the same effect as `glDrawArrays`

, but
instead of simply plowing forward through the vertex list, it first reads
a list of indices from an *index buffer* and then uses
those indices to choose vertices from the vertex buffer.

To help explain indexing and how it’s useful,
let’s go back to the simple “square from two triangles” example from the
previous chapter (Figure 2-3). Here’s one way of
rendering the square with `glDrawArrays`

:

vec2 vertices[6] = { vec2(0, 0), vec2(0, 1), vec2(1, 1), vec2(1, 1), vec2(1, 0), vec2(0, 0) }; glVertexPointer(2, GL_FLOAT, sizeof(vec2), (void*) vertices); glDrawArrays(GL_TRIANGLES, 0, 6);

Note that two vertices—(0, 0) and (1, 1)—appear twice in the vertex list. Vertex indexing can eliminate this redundancy. Here’s how:

vec2 vertices[4] = { vec2(0, 0), vec2(0, 1), vec2(1, 1), vec2(1, 0) }; GLubyte indices[6] = { 0, 1, 2, 2, 3, 0}; glVertexPointer(2, GL_FLOAT, sizeof(vec2), vertices); glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_BYTE, (void*) indices);

So, instead of sending 6 vertices to OpenGL (8
bytes per vertex), we’re now sending 4 vertices plus 6 indices (one byte
per index). That’s a total of 48 bytes with `glDrawArrays`

and 38 bytes with
`glDrawIndices`

.

You might be thinking “But I can just use a
triangle strip with `glDrawArrays`

and save just as much
memory!” That’s true in this case. In fact, a triangle strip is the best
way to draw our lonely little square:

vec2 vertices[6] = { vec2(0, 0), vec2(0, 1), vec2(1, 0), vec2(1, 1) }; glVertexPointer(2, GL_FLOAT, sizeof(vec2), (void*) vertices); glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);

That’s only 48 bytes, and adding an index buffer would buy us nothing.

However, more complex geometry (such as our
cone model) usually involves even more repetition of vertices, so an index
buffer offers much better savings. Moreover, `GL_TRIANGLE_STRIP`

is great in certain
cases, but in general it isn’t as versatile as `GL_TRIANGLES`

. With
`GL_TRIANGLES`

, a single draw call can be used to render
multiple disjoint pieces of geometry. To achieve best performance with
OpenGL, execute as few draw calls per frame as possible.

Let’s walk through the process of updating
Touch Cone to use indexing. Take a look at these two lines in the class
declaration of `RenderingEngine1`

:

vector<Vertex> m_cone; vector<Vertex> m_disk;

Indexing allows you to combine these two
arrays, but it also requires a new array for holding the indices. OpenGL
ES supports two types of indices: `GLushort`

(16 bit) and
`GLubyte`

(8 bit). In this case, there are fewer than 256
vertices, so you can use `GLubyte`

for best efficiency.
Replace those two lines with the following:

vector<Vertex> m_coneVertices; vector<GLubyte> m_coneIndices; GLuint m_bodyIndexCount; GLuint m_diskIndexCount;

Since the index buffer is partitioned into two
parts (body and disk), we also added some counts that will get passed to
`glDrawElements`

, as you’ll see later.

Next you need to update the code that generates
the geometry. With indexing, the number of required vertices for our cone
shape is *n*2+1*, where *n* is the
number of slices. There are *n* vertices at the apex,
another *n* vertices at the rim, and one vertex for the
center of the base. Example 3-4 shows
how to generate the vertices. This code goes inside the
`Initialize`

method of the rendering engine class; before
you insert it, delete everything between ```
m_pivotPoint =
ivec2(width / 2, height / 2);
```

and ```
// Create the depth
buffer
```

.

Example 3-4. Vertex generation

const float coneRadius = 0.5f; const float coneHeight = 1.866f; const int coneSlices = 40; const float dtheta = TwoPi / coneSlices; const int vertexCount = coneSlices * 2 + 1; m_coneVertices.resize(vertexCount); vector<Vertex>::iterator vertex = m_coneVertices.begin(); // Cone's body for (float theta = 0; vertex != m_coneVertices.end() - 1; theta += dtheta) { // Grayscale gradient float brightness = abs(sin(theta)); vec4 color(brightness, brightness, brightness, 1); // Apex vertex vertex->Position = vec3(0, 1, 0); vertex->Color = color; vertex++; // Rim vertex vertex->Position.x = coneRadius * cos(theta); vertex->Position.y = 1 - coneHeight; vertex->Position.z = coneRadius * sin(theta); vertex->Color = color; vertex++; } // Disk center vertex->Position = vec3(0, 1 - coneHeight, 0); vertex->Color = vec4(1, 1, 1, 1);

In addition to the vertices, you need to store
indices for *2n* triangles, which requires a total of
*6n* indices.

Figure 3-2
uses exploded views to show the tessellation of a cone with *n =
10*. The image on the left depicts the ordering of the vertex
buffer; the image on the right depicts the ordering of the index buffer.
Note that each vertex at the rim is shared between four different
triangles; that’s the power of indexing! Remember, the vertices at the
apex cannot be shared because each of those vertices requires a unique
color attribute, as discussed in the previous chapter (see Figure 2-17).

Example 3-5
shows the code for generating indices (again, this code lives in our
`Initialize`

method). Note the usage
of the modulo operator to wrap the indices back to the start of the
array.

Example 3-5. Index generation

m_bodyIndexCount = coneSlices * 3; m_diskIndexCount = coneSlices * 3; m_coneIndices.resize(m_bodyIndexCount + m_diskIndexCount); vector<GLubyte>::iterator index = m_coneIndices.begin(); // Body triangles for (int i = 0; i < coneSlices * 2; i += 2) { *index++ = i; *index++ = (i + 1) % (2 * coneSlices); *index++ = (i + 3) % (2 * coneSlices); } // Disk triangles const int diskCenterIndex = vertexCount - 1; for (int i = 1; i < coneSlices * 2 + 1; i += 2) { *index++ = diskCenterIndex; *index++ = i; *index++ = (i + 2) % (2 * coneSlices); }

Now it’s time to enter the new
`Render()`

method, shown in Example 3-6. Take a close look at the core of the
rendering calls (in bold). Recall that the body of the cone has a
grayscale gradient, but the cap is solid white. The
`draw`

call that renders the body should heed the color
values specified in the vertex array, but the draw call for the disk
should not. So, between the two calls to
`glDrawElements`

, the `GL_COLOR_ARRAY`

attribute is turned off with `glDisableClientState`

, and
the color is explicitly set with `glColor4f`

. Replace the
definition of `Render()`

in its entirety with the code in
Example 3-6.

Example 3-6. RenderingEngine1::Render()

void RenderingEngine1::Render() const { GLsizei stride = sizeof(Vertex); const GLvoid* pCoords = &m_coneVertices[0].Position.x; const GLvoid* pColors = &m_coneVertices[0].Color.x; glClearColor(0.5f, 0.5f, 0.5f, 1); glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); glPushMatrix(); glRotatef(m_rotationAngle, 0, 0, 1); glScalef(m_scale, m_scale, m_scale); glVertexPointer(3, GL_FLOAT, stride, pCoords); glColorPointer(4, GL_FLOAT, stride, pColors); glEnableClientState(GL_VERTEX_ARRAY); const GLvoid* bodyIndices = &m_coneIndices[0]; const GLvoid* diskIndices = &m_coneIndices[m_bodyIndexCount];glEnableClientState(GL_COLOR_ARRAY);glDrawElements(GL_TRIANGLES, m_bodyIndexCount, GL_UNSIGNED_BYTE, bodyIndices);glDisableClientState(GL_COLOR_ARRAY);glColor4f(1, 1, 1, 1);glDrawElements(GL_TRIANGLES, m_diskIndexCount, GL_UNSIGNED_BYTE, diskIndices);glDisableClientState(GL_VERTEX_ARRAY); glPopMatrix(); }

You should be able to build and run at this
point. Next, modify the ES 2.0 backend by making the same changes we just
went over. The only tricky part is the `Render`

method,
shown in Example 3-7. From a 30,000-foot
view, it basically does the same thing as its ES 1.1 counterpart, but with
some extra footwork at the beginning for setting up the transformation
state.

Example 3-7. RenderingEngine2::Render()

void RenderingEngine2::Render() const { GLuint positionSlot = glGetAttribLocation(m_simpleProgram, "Position"); GLuint colorSlot = glGetAttribLocation(m_simpleProgram, "SourceColor"); mat4 rotation = mat4::Rotate(m_rotationAngle); mat4 scale = mat4::Scale(m_scale); mat4 translation = mat4::Translate(0, 0, -7); GLint modelviewUniform = glGetUniformLocation(m_simpleProgram, "Modelview"); mat4 modelviewMatrix = scale * rotation * translation; GLsizei stride = sizeof(Vertex); const GLvoid* pCoords = &m_coneVertices[0].Position.x; const GLvoid* pColors = &m_coneVertices[0].Color.x; glClearColor(0.5f, 0.5f, 0.5f, 1); glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); glUniformMatrix4fv(modelviewUniform, 1, 0, modelviewMatrix.Pointer()); glVertexAttribPointer(positionSlot, 3, GL_FLOAT, GL_FALSE, stride, pCoords); glVertexAttribPointer(colorSlot, 4, GL_FLOAT, GL_FALSE, stride, pColors); glEnableVertexAttribArray(positionSlot); const GLvoid* bodyIndices = &m_coneIndices[0]; const GLvoid* diskIndices = &m_coneIndices[m_bodyIndexCount];glEnableVertexAttribArray(colorSlot);glDrawElements(GL_TRIANGLES, m_bodyIndexCount, GL_UNSIGNED_BYTE, bodyIndices);glDisableVertexAttribArray(colorSlot);glVertexAttrib4f(colorSlot, 1, 1, 1, 1);glDrawElements(GL_TRIANGLES, m_diskIndexCount, GL_UNSIGNED_BYTE, diskIndices);glDisableVertexAttribArray(positionSlot); }

That covers the basics of index buffers; we managed to reduce the memory footprint by about 28% over the nonindexed approach. Optimizations like this don’t matter much for silly demo apps like this one, but applying them to real-world apps can make a big difference.

Start Free Trial

No credit card required