Getting Your Data Automatically
When you call glDrawArraysInstanced or glDrawElementsInstanced, the built-in variable gl_InstanceID
will be available in your shaders to tell you which instance you’re
working on, and it will increment by one for each new instance of the
geometry that you’re rendering. It’s actually available even when you’re
not using one of the instanced drawing functions—it’ll just be zero in
those cases. This means that you can use the same shaders for instanced
and noninstanced rendering.
You can use gl_InstanceID
to index into arrays that are the same length as the number of
instances that you’re rendering. For example, you can use it to look up
texels in a texture or to index into a uniform array. Really, what you’d
be doing though is treating the array as if it were an “instanced
attribute.” That is, a new value of the attribute is read for each
instance you’re rendering. OpenGL can feed this data to your shader
automatically using a feature called instanced arrays.
To use instanced arrays, declare an input to your shader as normal. The
input attribute will have an index that you would use in calls to
functions like glVertexAttribPointer.
Normally, the vertex attributes would be read per vertex and a new value
would be fed to the shader. However, to make OpenGL read attributes
from the arrays once per instance, you can call
void glVertexAttribDivisor(GLuint index, GLuint divisor);
Pass the index of the attribute to the function in index and set divisor to the number of instances you’d like to pass between each new value being read from the array. If divisor is zero, then the array becomes a regular vertex attribute array with a new value read per vertex. If divisor is nonzero, however, then new data is read from the array once every few instances. For example, if you set divisor to one, you’ll get a new value from the array for each instance. If you set divisor
to two, you’ll get a new value for every second instance, and so on.
You can mix and match the divisors, setting different values for each
attribute.
An example of using
this functionality would be when you want to draw a set of objects with
different colors. Consider the simple vertex shader in Listing 5.
Listing 5. Simple Vertex Shader with Per-Vertex Color
#version 150
precision highp float;
in vec4 position;
in vec4 color;
out Fragment
{
vec4 color;
} fragment;
uniform mat4 mvp;
void main(void)
{
gl_Position = mvp * position;
fragment.color = color;
}
|
Normally, the attribute color
would be read once per vertex, and so every vertex would end up having a
different color. The application would have to supply an array of
colors with as many elements as there were vertices in the model. Also
it wouldn’t be possible for every instance of the object to have a
different color because the shader doesn’t know anything about
instancing. We can make color an instanced array if we call
glVertexAttribDivisor(index_of_color, 1);
where index_of_color is the index of the slot to which the color attribute has been bound.
Now, a new value of color will be fetched from the vertex array once per instance.
Every vertex within any particular instance will receive the same value
for color, and the result will be that each instance of the object will
be rendered in a different color. The size of the vertex array holding
the data for color only needs to be as
long as the number of indices we want to render. If we increase the
value of the divisor, new data will be read from the array with less and
less frequency. If the divisor is two, a new value of color will be
presented every second instance; if the divisor is three, color will be
updated every third instance; and so on.
If we render geometry
using this simple shader, each instance will be drawn on top of the
others. We need to modify the position of each instance so that we can
see each one. We can use another instanced array for this. Listing 6 shows a simple modification to the vertex shader in Listing 5.
Listing 6. Simple Instanced Vertex Shader
#version 150
precision highp float;
in vec4 position;
in vec4 instance_color;
in vec4 instance_position;
out Fragment
{
vec4 color;
} fragment;
uniform mat4 mvp;
void main(void)
{
gl_Position = mvp * (position + instance_position);
fragment.color = instance_color;
}
|
Now,
we have a per-instance position as well as a per-vertex position. We
can add these together in the vertex shader before multiplying with the
model-view-projection matrix. We can set the instance_position input attribute to an instanced array by calling
glVertexAttribDivisor(index_of_instance_position, 1);
Again, index_of_instance_position is the index of the location to which the instance_position attribute has been bound. Any type of input attribute can be made instanced using glVertexAttribDivisor. This example is simple and only uses a translation (the value held in instance_position).
A more advanced application could use matrix vertex attributes or pack
some transformation matrices into uniforms and pass matrix weights in
instanced arrays. The application can use this to render an army of
soldiers, each with a different pose, or a fleet of spaceships all
flying in different directions.
Now let’s hook this simple
shader up to a real program. First, we load our shaders and set the
attribute positions like normal before linking the program as shown in Listing 7.
Listing 7. Setting Up Instanced Attributes
instancingProg = gltLoadShaderPair("instancing.vs", "instancing.fs");
glBindAttribLocation(instancingProg, 0, "position");
glBindAttribLocation(instancingProg, 1, "instance_color");
glBindAttribLocation(instancingProg, 2, "instance_position");
glLinkProgram(instancingProg);
|
In Listing 8, we declare some data and load it into a vertex buffer (attached to a vertex array object).
Listing 8. Getting Ready for Instanced Rendering
static const GLfloat square_vertices[] =
{
-1.0f, -1.0f, 0.0f, 1.0f,
1.0f, -1.0f, 0.0f, 1.0f,
1.0f, 1.0f, 0.0f, 1.0f,
-1.0f, 1.0f, 0.0f, 1.0f
};
static const GLfloat instance_colors[] =
{
1.0f, 0.0f, 0.0f, 1.0f,
0.0f, 1.0f, 0.0f, 1.0f,
0.0f, 0.0f, 1.0f, 1.0f,
1.0f, 1.0f, 0.0f, 1.0f
};
static const GLfloat instance_positions[] =
{
-2.0f, -2.0f, 0.0f, 0.0f,
2.0f, -2.0f, 0.0f, 0.0f,
2.0f, 2.0f, 0.0f, 0.0f,
-2.0f, 2.0f, 0.0f, 0.0f
};
GLuint offset = 0;
glGenVertexArrays(1, &square_vao);
glGenBuffers(1, &square_vbo);
glBindVertexArray(square_vao);
glBindBuffer(GL_ARRAY_BUFFER, square_vbo);
glBufferData(GL_ARRAY_BUFFER,
sizeof(square_vertices) +
sizeof(instance_colors) +
sizeof(instance_positions), NULL, GL_STATIC_DRAW);
glBufferSubData(GL_ARRAY_BUFFER, offset,
sizeof(square_vertices),
square_vertices);
offset += sizeof(square_vertices);
glBufferSubData(GL_ARRAY_BUFFER, offset,
sizeof(instance_colors), instance_colors);
offset += sizeof(instance_colors);
glBufferSubData(GL_ARRAY_BUFFER, offset,
sizeof(instance_positions), instance_positions);
offset += sizeof(instance_positions);
glVertexAttribPointer(0, 4, GL_FLOAT, GL_FALSE, 0, 0);
glVertexAttribPointer(1, 4, GL_FLOAT, GL_FALSE, 0,
(GLvoid *)sizeof(square_vertices));
glVertexAttribPointer(2, 4, GL_FLOAT, GL_FALSE, 0,
(GLvoid *)(sizeof(square_vertices) +
sizeof(instance_colors)));
glEnableVertexAttribArray(0);
glEnableVertexAttribArray(1);
glEnableVertexAttribArray(2);
|
Now all that remains is to set the vertex attrib divisors for the instance_color and instance_position attribute arrays.
glVertexAttribDivisor(1, 1);
glVertexAttribDivisor(2, 1);
Now we draw four instances
of the geometry we put into our vertex buffer. Each instance consists
of four vertices, each with its own position. The same vertex in each
instance has the same position. However, all of the vertices in a single
instance see the same value of instance_color and instance_position, and a new value of each is presented at each instance. Our rendering loop looks like this:
glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
glClear(GL_COLOR_BUFFER_BIT);
glUseProgram(instancingProg);
glBindVertexArray(square_vao);
glDrawArraysInstanced(GL_TRIANGLE_FAN, 0, 4, 4);
What we get is shown in Figure 6.
In Figure 6,
you can see that four squares have been rendered. Each is at a
different position, and each has a different color. This can be extended
to thousands or even millions of instances, and modern graphics
hardware should be able to handle this without any issue.
|