Sending data to a shader using per-vertex attributes and vertex buffer objects
The vertex shader is invoked once per vertex. Its main job is to process the data associated with the vertex, and pass it (and possibly other information) along to the next stage of the pipeline. In order to give our vertex shader something to work with, we must have some way of providing (per-vertex) input to the shader. Typically, this includes the vertex position, normal vector, and texture coordinate (among other things). In earlier versions of OpenGL (prior to 3.0), each piece of vertex information had a specific "channel" in the pipeline. It was provided to the shaders using functions such as glVertex
, glTexCoord
, and glNormal
(or within vertex arrays using glVertexPointer
, glTexCoordPointer
, or glNormalPointer
).The shader would then access these values via built-in variables such as gl_Vertex
and gl_Normal
. This functionality was deprecated in OpenGL 3.0 and later removed. Instead, now vertex information must be provided using
generic vertex attributes, usually in conjunction with (vertex) buffer objects. The programmer is now free to define an arbitrary set of per-vertex attributes to provide as input to the vertex shader. For example, in order to implement normal mapping, we might decide that position, normal vector, and tangent vector should be provided along with each vertex. With OpenGL 4.0, it's easy to define this as the set of input attributes. This gives us a great deal of flexibility to define our vertex information in any way that is appropriate for our application, but may require a bit of getting used to for those of us who are used to the old way of doing things.
In the vertex shader, per-vertex input attributes are declared by using the GLSL qualifier in
. For example, to define a 3-component vector input attribute named VertexColor
, we use the following code:
Of course, the data for this attribute must be supplied by the OpenGL program. To do so, we make use of vertex buffer objects. The buffer object contains the values for the input attribute and in the main OpenGL program we make the connection between the buffer and the input attribute, and define how to "step through" the data. Then, when rendering, OpenGL pulls data for the input attribute from the buffer for each invocation of the vertex shader.
For this recipe, we'll draw the simplest OpenGL shape, a triangle. Our vertex attributes will include the position and color. We'll use a fragment shader to blend the colors of each vertex across the triangle to produce an image similar to the one shown in the following screenshot. The vertices of the triangle are red, green, and blue, and the interior of the triangle has those three colors blended together.
We'll start with a simple, empty OpenGL program, and the following shaders.
The vertex shader (basic.vert
):
Note that there are two input attributes in the vertex shader:
VertexPosition
and VertexColor
. Our program needs to provide the data for these two attributes for each vertex. We will do so by mapping our polygon data to these variables.
It also has one output variable named Color
, which is sent to the fragment shader. In this case, Color
is just an unchanged copy of VertexColor
. Also, note that the attribute VertexPosition
is simply expanded and passed along to the built-in output variable gl_Position
for further processing.
The fragment shader (basic.frag
):
There is just one input variable for this shader, Color
. This links to the corresponding output variable in the vertex shader, and will contain a value that has been interpolated across the triangle based on the values at the vertices. We simply expand and copy this color to the output variable FragColor
(more about fragment shader output variables in later recipes).
Write code to compile and link these shaders into a shader program (see Compiling a Shader and Linking a Shader Program). In the following code, I'll assume that the handle to the shader program is programHandle
.
Use the following steps to set up your buffer objects and render the triangle.
Just prior to linking the shader program, define the mapping between vertex attributes and shader input variables using glBindAttribLocation
.
Create a global (or private instance) variable to hold our handle to the vertex array object:
Within the initialization function, create and populate the vertex buffer objects for each attribute.
Create and bind to a vertex array object, which stores the relationship between the buffers and the input attributes.
In the render function, bind to the vertex array object and call glDrawArrays
to initiate rendering.
Vertex attributes are the input variables to our vertex shader. In the vertex shader above, our two attributes are VertexPosition
and VertexColor
. Since we can give these variables any name we like, OpenGL provides a way to refer to vertex attributes in the OpenGL program by associating each (active) input variable with a generic attribute index. These generic indices are simply integers between 0 and GL_MAX_VERTEX_ATTRIBS
– 1. We refer to the vertex attributes in our OpenGL code by referring to the corresponding generic vertex attribute index.
The first step above involves making connections between the shader input variables VertexPosition
and VertexColor
and the generic vertex attribute indexes 0 and 1 respectively, using the function glBindAttribLocation
. If this is done within the OpenGL application, we have to do this before the program is linked.
Note
It is not strictly necessary to explicitly specify the mappings between attribute variables and generic attribute indexes, because OpenGL will automatically map active vertex attributes to generic indexes when the program is linked. We could then query for the mappings and determine the indexes that correspond to the shader's input variables. It may be somewhat clearer, however, to explicitly specify the mapping as we do in this example.
The next step involves setting up a pair of buffer objects to store our position and color data. As with most OpenGL objects, we start by acquiring handles to two buffers by calling glGenBuffers
. We then assign each handle to a separate descriptive variable to make the following code clearer.
For each buffer object, we first bind the buffer to the GL_ARRAY_BUFFER
binding point by calling glBindBuffer
. The first argument to glBindBuffer
is the target binding point. For vertex attribute data, we use GL_ARRAY_BUFFER
. Examples of other kinds of targets (such as GL_UNIFORM_BUFFER
, or GL_ELEMENT_ARRAY_BUFFER
) will be seen in later examples. Once our buffer object is bound, we can populate the buffer with vertex/color data by calling glBufferData
. The second and third arguments to this function are the size of the array and a pointer to the array containing the data. Let's focus on the first and last argument. The first argument indicates the target buffer object. The data provided in the third argument is copied into the buffer that is bound to this binding point. The last argument is one that gives OpenGL a hint about how the data will be used so that it can determine how best to manage the buffer internally. For full details about this argument, take a look at the OpenGL documentation (http://www.opengl.org/sdk/docs/man4/). In our case, the data specified once will not be modified, and will be used many times for drawing operations, so this usage pattern best corresponds to the value GL_STATIC_DRAW
.
Now that we have set up our buffer objects, we tie them together into a
vertex array object (VAO). The VAO contains information about the connections between the data in our buffers and the input vertex attributes. We create a VAO using the function glGenVertexArrays
. This gives us a handle to our new object, which we store in the (global) variable vaoHandle
. Then we enable the generic vertex attribute indexes 0 and 1 by calling glEnableVertexAttribArray
. Doing so indicates that the values for the attributes will be accessed and used for rendering.
The next step makes the connection between the buffer objects and the generic vertex attribute indexes.
First we bind the buffer object to the GL_ARRAY_BUFFER
binding point, and then we call glVertexAttribPointer
, which tells OpenGL which generic index the data should be used with, the format of the data stored in the buffer object, and where it is located within the buffer object that is bound to the GL_ARRAY_BUFFER
binding point. The first argument is the generic attribute index. The second is the number of components per vertex attribute (1, 2, 3, or 4). In this case, we are providing 3-dimensional data, so we want 3 components per vertex. The third argument is the data type of each component in the buffer. The fourth is a Boolean which specifies whether or not the data should be automatically normalized (mapped to a range of [-1,1] for signed integral values or [0,1] for unsigned integral values). The fifth argument is the stride, which indicates the byte offset between consecutive attributes. Since our data is tightly packed, we use a value of zero. The last argument is a pointer, which is not treated as a pointer! Instead, its value is interpreted as a byte offset from the beginning of the buffer to the first attribute in the buffer. In this case, there is no additional data in either buffer prior to the first element, so we use a value of zero (NULL
).
Note
The vertex array object stores all of the OpenGL state related to the relationship between buffer objects and the generic vertex attributes, as well as the information about the format of the data in the buffer objects. This allows us to quickly return all of this state when rendering.
In the render function, it is simply a matter of clearing the color buffer using glClear
, binding to the vertex array object, and calling glDrawArrays
to draw our triangle. The function glDrawArrays
initiates rendering of primitives by stepping through the buffers for each enabled attribute array, and passing the data down the pipeline to the vertex shader. The first argument is the render mode (in this case we are drawing triangles), the second is the starting index in the enabled arrays, and the third argument is the number of indices to be rendered (3 vertexes for a single triangle).
To summarize, rendering with vertex buffer objects (VBOs) involves the following steps:
Before linking the shader program, define the mappings between generic vertex attribute indexes and shader input variables by calling glBindAttribLocation
.
Create and populate the buffer objects for each attribute.
Create and define the vertex array object by calling glVertexAttribPointer
while the appropriate buffer is bound.
When rendering, bind to the vertex array object and call glDrawArrays
, or other appropriate rendering function (for example, glDrawElements
).
You may have noticed that I've neglected saying anything about the output variable FragColor
in the fragment shader. This variable receives the final output color for each fragment (pixel). Like vertex input variables, this variable also needs to be associated with a location. Of course, we typically would like this to be linked to the back color buffer, which by default (in double buffered systems) is "color number" zero. (The relationship of the color numbers to render buffers can be changed by using glDrawBuffers
.) In this program we are relying on the fact that the linker will automatically link our only fragment output variable to color number zero. To explicitly do so, we could (and probably should) have used the following command prior to program linking:
We are free to define multiple output variables for a fragment shader, thereby enabling us to render to multiple output buffers. This can be quite useful for specialized algorithms such as deferred shading (see Chapter 5).
We can avoid the need to call glBindAttribLocation
within the OpenGL program by using layout qualifiers to define the attribute index within the shader itself. For example, we could remove the two calls to glBindAttribLocation
, and change the input variable declarations in our vertex shader to:
This would indicate to the linker that VertexPosition
should correspond to generic attribute index 0 and VertexColor
to index 1.
We can use a layout qualifier to define the color number for our fragment output variables as well:
This would tell the linker to bind the output variable FragColor
to color number 0, avoiding the need to call glBindFragDataLocation
within our OpenGL program.
It is often the case that we need to step through our vertex arrays in a non-linear fashion. In other words we may want to "jump around" the data rather than just moving through it from beginning to end, as we did in this example. For example, we might want to draw a cube where the vertex data consists of only eight positions (the corners of the cube). In order to draw the cube, we would need to draw 12 triangles (2 for each face), each of which consists of 3 vertices. All of the required position data is in the original 8 positions, but to draw all the triangles, we'll need to jump around and use each position for at least three different triangles.
To jump around in our vertex arrays, we can make use of element arrays. The element array is another buffer that defines the indices used when stepping through the vertex arrays. For details on using element arrays, take a look at the function glDrawElements
in the OpenGL documentation (http://www.opengl.org/sdk/docs/man4/).
In this example, we used two buffers (one for color and one for position). Instead, we could have used just a single buffer and combined all of the data. The data for multiple attributes can be interleaved within an array, such that all of the data for a given vertex is grouped together within the buffer. Doing so just requires careful use of the arguments to glVertexAttribPointer
(particularly the fifth argument: the stride). Take a look at the OpenGL documentation for full details (http://www.opengl.org/sdk/docs/man4/).
The decision about when to use interleaved arrays, and when to use separate arrays, is highly dependent on the situation. Interleaved arrays may bring better results due to the fact that data is accessed together and resides closer in memory (so-called locality of reference), resulting in better caching performance.