Optimization: Base Vertex

Using VAOs can dramatically simplify code. However, VAOs are not always the best case for performance, particularly if you use a lot of separate buffer objects.

Binding a VAO for rendering can be an expensive proposition. Therefore, if there is a way to avoid binding one, then it can provide a performance improvement, if the program is currently bottlenecked on the CPU.

Our two objects have much in common. They use the same vertex attribute indices, since they are being rendered with the same program object. They use the same format for each attribute (3 floats for positions, 4 floats for colors). The vertex data even comes from the same buffer object.

Indeed, the only difference between the two objects is what offset each attribute uses. And even this is quite minimal, since the difference between the offsets is a constant factor of the size of each attribute.

Look at the vertex data in the buffer object:

Example 5.5. Vertex Attribute Data Abridged

//Object 1 positions
LEFT_EXTENT,    TOP_EXTENT,       REAR_EXTENT,
LEFT_EXTENT,    MIDDLE_EXTENT,    FRONT_EXTENT,
RIGHT_EXTENT,   MIDDLE_EXTENT,    FRONT_EXTENT,

...

RIGHT_EXTENT,   TOP_EXTENT,       REAR_EXTENT,
RIGHT_EXTENT,   BOTTOM_EXTENT,    REAR_EXTENT,

//Object 2 positions
TOP_EXTENT,     RIGHT_EXTENT,     REAR_EXTENT,
MIDDLE_EXTENT,  RIGHT_EXTENT,     FRONT_EXTENT,
MIDDLE_EXTENT,  LEFT_EXTENT,      FRONT_EXTENT,

...

TOP_EXTENT,     RIGHT_EXTENT,     REAR_EXTENT,
TOP_EXTENT,     LEFT_EXTENT,      REAR_EXTENT,
BOTTOM_EXTENT,  LEFT_EXTENT,      REAR_EXTENT,

//Object 1 colors
GREEN_COLOR,
GREEN_COLOR,
GREEN_COLOR,

...

BROWN_COLOR,
BROWN_COLOR,

//Object 2 colors
RED_COLOR,
RED_COLOR,
RED_COLOR,

...

GREY_COLOR,
GREY_COLOR,

Notice how the attribute array for object 2 immediately follows its corresponding attribute array for object 1. So really, instead of four attribute arrays, we really have just two attribute arrays.

If we were doing array drawing, we could simply have one VAO, which sets up the beginning of both combined attribute arrays. We would still need 2 separate draw calls, because there is a uniform that is different for each object. But our rendering code could look like this:

Example 5.6. Array Drawing of Two Objects with One VAO

glUseProgram(theProgram);

glBindVertexArray(vaoObject);
glUniform3f(offsetUniform, 0.0f, 0.0f, 0.0f);
glDrawArrays(GL_TRIANGLES, 0, numTrianglesInObject1);

glUniform3f(offsetUniform, 0.0f, 0.0f, -1.0f);
glDrawArrays(GL_TRIANGLES, numTrianglesInObject1, numTrianglesInObject2);

glBindVertexArray(0);
glUseProgram(0);

This is all well and good for array drawing, but we are doing indexed drawing. And while we can control the location we are reading from in the element buffer by using the count and indices parameter of glDrawElements, that only specifies which indices we are reading from the element buffer. What we would need is a way to modify the index data itself.

This could be done by simply storing the index data for object 2 in the element buffer. This changes our element buffer into the following:

Example 5.7. MultiObject Element Buffer

const GLshort indexData[] =
{
//Object 1
0, 2, 1,        3, 2, 0,
4, 5, 6,        6, 7, 4,
8, 9, 10,       11, 13, 12,
14, 16, 15,     17, 16, 14,

//Object 2
18, 20, 19,     21, 20, 18,
22, 23, 24,     24, 25, 22,
26, 27, 28,     29, 31, 30,
32, 34, 33,     35, 34, 32,
};

This would work for our simple example here, but it does needlessly take up room. What would be great is a way to simply add a bias value to the index after it is pulled from the element array, but before it is used to access the attribute data.

I'm sure you'll be surprised to know that OpenGL offers such a mechanism, what with me bringing it up and all.

The function glDrawElementsBaseVertex provides this functionality. It works like glDrawElements has one extra parameter at the end, which is the offset to be applied to each index. The tutorial project Base Vertex With Overlap demonstrates this.

The initialization changes, building only one VAO.

Example 5.8. Base Vertex Single VAO

glGenVertexArrays(1, &vao);
glBindVertexArray(vao);

size_t colorDataOffset = sizeof(float) * 3 * numberOfVertices;
glBindBuffer(GL_ARRAY_BUFFER, vertexBufferObject);
glEnableVertexAttribArray(0);
glEnableVertexAttribArray(1);
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 0, 0);
glVertexAttribPointer(1, 4, GL_FLOAT, GL_FALSE, 0, (void*)colorDataOffset);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, indexBufferObject);

glBindVertexArray(0);

This simply binds the beginning of each array. The rendering code is as follows:

Example 5.9. Base Vertex Rendering

glUseProgram(theProgram);

glBindVertexArray(vao);

glUniform3f(offsetUniform, 0.0f, 0.0f, 0.0f);
glDrawElements(GL_TRIANGLES, ARRAY_COUNT(indexData), GL_UNSIGNED_SHORT, 0);

glUniform3f(offsetUniform, 0.0f, 0.0f, -1.0f);
glDrawElementsBaseVertex(GL_TRIANGLES, ARRAY_COUNT(indexData),
	GL_UNSIGNED_SHORT, 0, numberOfVertices / 2);

glBindVertexArray(0);
glUseProgram(0);

The first draw call uses the regular glDrawElements function, but the second uses the BaseVertex version.

Note

This example of BaseVertex's use is somewhat artificial, because both objects use the same index data. The more compelling way to use it is with objects that have different index data. Of course, if objects have different index data, you may be wondering why you would bother with BaseVertex when you could just manually add the offset to the indices themselves when you create the element buffer.

There are several reasons not to do this. One of these is that GL_UNSIGNED_INT is twice as large as GL_UNSIGNED_SHORT. If you have more than 65,536 entries in an array, whether for one object or for many, you would need to use ints instead of shorts for indices. Using ints can hurt performance, particularly on older hardware with less bandwidth. With BaseVertex, you can use shorts for everything, unless a particular object itself has more than 65,536 vertices.

The other reason not to manually bias the index data is to more accurately match the files you are using. When loading indexed mesh data from files, the index data is not biased by a base vertex; it is all relative to the model's start. So it makes sense to keep things that way where possible; it just makes the loading code simpler and faster by storing a per-object BaseVertex with the object rather than biasing all of the index data.

Fork me on GitHub