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.
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.