So far, you have seen how to send blocks of data to OpenGL to render using functions such as glDrawArrays.
It’s possible to send huge numbers of vertices—millions if necessary—to
OpenGL using a single call to this function. However, this is only of
any use when the geometry is nicely arranged in a large contiguous
block. In any nontrivial application, there will be many different,
unrelated objects. There is likely to be a world or some kind of
background, and each of these may require several calls to one of the
drawing functions. It is not unusual to see a complex application making
thousands or even hundreds of thousands of calls to the various drawing
functions that OpenGL provides in every frame. In this section, we go
over a number of methods that you can use to draw a lot of independent
pieces of geometry with very few calls to OpenGL.
Combining Drawing Functions
If you have a lot of geometry to
send to OpenGL in a single application, it’s likely that you will have
one preferred method of drawing. This might be to use glDrawArrays or glDrawElements,
for example. If you were to pack all of the vertex data for all of your
objects into a single buffer, it would be reasonable to have a loop in
your code that looks something like this:
for (int i = 0; i < num_objects; i++) {
glDrawArrays(GL_TRIANGLES,
object[n]->first_vertex,
object[n]->vertex_count);
}
This
might produce a lot of calls into OpenGL, and each one carries some
overhead. If you have a large number of objects in your scene, and each
has a relatively small number of triangles, the cost of each of these
calls to glDrawArrays will start to
add up and could negatively affect the performance of your application. A
couple of functions that might help in this case are
void glMultiDrawArrays(GLenum mode, GLint *first, GLsizei *count, GLsizei primcount);
and
void glMultiDrawElements(GLenum mode, GLsizei *count, GLenum type, GLvoid **indices, GLsizei primcount);
These two functions operate similarly to the previous code. Each behaves as if its non-Multi versions had been called primcount times. For glMultiDrawArrays, first and count are arrays. Also, for glMultiDrawElements, count and indices
are arrays. This allows OpenGL to perform all of its setup once, check
that all the parameters are correct once, and if the driver supports it,
send a single command to the graphics hardware. This can allow a lot of
the overhead associated with calling OpenGL functions to be amortized
across the number of function calls the glMultiDraw function replaces.
By rewriting this example, we can see that only one function call to glMultiDrawArrays can be used to replace the many (potentially thousands) calls to glDrawArrays. This new version is shown in Listing 1. Although there is more code, there are fewer calls to OpenGL, which often translates to better performance.
Listing 1. Simple Example of glMultiDrawArrays
// These arrays are assumed to be sized large enough to hold enough data to
// represent all of the objects in the scene
GLint first[];
GLsizei count[];
// Build our lists of first vertex and vertex count
for (int i = 0; i < num_objects; i++) {
first[i] = object[n]->first_vertex;
count[i] = object[n]->vertex_count;
}
// Now make a single call to glDrawArrays
glMultiDrawArrays(GL_TRIANGLES, first, count, num_objects);
|
If the list of objects doesn’t change (or doesn’t change very often), you can build the first and count
arrays up front, removing the for-loop from the example entirely. For
example, if you have a simple game with enemies and bonus items in a
level, you may only need to update the first and count arrays when one of the enemies dies or a bonus item is collected by the player.
Combining Geometry Using Primitive Restart
There are many tools out there
that “stripify” geometry. The idea of these tools is that by taking
“triangle soup,” which means a large collection of unconnected
triangles, and attempting to merge it into a set of triangle strips,
performance can be improved. This works because individual triangles
each take three vertices to represent, but a triangle strip reduces this
to a single vertex per triangle (not counting the first triangle in the
strip). By converting the geometry from triangle soup to triangle
strips, there is less geometry data to process, and the system should
run faster. If the tool does a good job and produces a small number of
long strips containing many triangles each, this generally works well.
There has been a lot of research into this type of algorithm, and a new
method’s success is measured by passing some well-known models through
the new “stripifier” and comparing the number and average length of the
strips generated by the tool to that produced by current cutting-edge
stripifiers.
Despite all of this research, the reality is that a soup can be rendered with a single call to glDrawArrays or glDrawElements,
but unless the functionality that is about to be introduced is used, a
set of strips needs to be rendered with separate calls to OpenGL. This
means that there is likely to be a lot more function calls in a program
that uses stripified geometry, and if the stripping application hasn’t
done a decent job or if the model just doesn’t lend well to
stripification, this can eat any performance gains seen by using strips
in the first place. Even functions like glMultiDrawArrays and glMultiDrawElements
don’t always help because the graphics hardware may not implement these
functions directly, and so OpenGL essentially has to convert them to
multiple calls to glDrawArrays internally anyway.
A feature that is almost universally supported by recent graphics hardware and is part of OpenGL is primitive restart. Primitive restart applies to the GL_TRIANGLE_STRIP, GL_TRIANGLE_FAN, GL_LINE_STRIP, and GL_LINE_LOOP
geometry types. It is a method of informing OpenGL when one strip (or
fan or loop) has ended and that another should be started. To indicate
the position in the geometry where one strip ends and the next starts, a
special marker is placed as a reserved value in the element array. As
OpenGL either fetches vertex indices from the element array or generates
them internally, in the case of nonindexed draw commands like glDrawArrays,
it checks for this special index value and whenever it comes across it,
it ends the current strip and starts a new one with the next vertex.
This mode is disabled by default but can be enabled by calling
glEnable(GL_PRIMITIVE_RESTART);
and disabled again by calling
glDisable(GL_PRIMITIVE_RESTART);
When primitive restart mode
is enabled, OpenGL watches for the special index value as it fetches or
generates them and when it comes across it, stops the current strip and
starts a new one. To set the index that OpenGL should watch for, call
glPrimitiveRestartIndex(index);
OpenGL watches for the value specified by index
and uses that as the primitive restart marker. Because the marker is a
vertex index, primitive restart is best used with indexed drawing
functions such as glDrawElements. You can still use primitive restart with glDrawArrays,
for example. In this case, OpenGL may eventually generate the restart
index internally, and when it does, it restarts the primitive. For
example, if you set the restart index to ten and then draw 20 vertices
using the GL_TRIANGLE_STRIP mode, you get two separate strips.
The default value of the
primitive restart index is zero. Because that’s almost certainly the
index of a real vertex that will be contained in the model, it’s a good
idea to set the restart index to a new value whenever you’re using
primitive restart mode. A good value to use is 0xFFFFFFFF
because you can be almost certain that it will not be used as a valid
index of a vertex. Many stripping tools have an option to either create
separate strips or to create a single strip with the restart index in
it. The stripping tool may use a predefined index or output the index it
used when creating the stripped version of the model (for example, one
greater than the number of vertices in the model). You need to know this
and set it using the glPrimitiveRestartIndex function to use the output of the tool in your application.
The primitive restart feature is illustrated in Figure 1.