- OpenGL Graphics Primitives
- Data in OpenGL Buffers
- Vertex Specification
- OpenGL Drawing Commands
- Instanced Rendering
Vertex Specification
Now that you have data in buffers, and you know how to write a basic vertex shader, it’s time to hook the data up to the shader. You’ve already read about vertex array objects, which contain information about where data is located and how it is laid out, and functions like glVertexAttribPointer(). It’s time to take a deeper dive into vertex specifications, other variants of glVertexAttribPointer(), and how to specify data for vertex attributes that aren’t floating point or aren’t enabled.
VertexAttribPointer in Depth
The glVertexAttribPointer() command was briefly introduced in Chapter 1. The prototype is as follows:
The state set by glVertexAttribPointer() is stored in the currently bound vertex array object (VAO). size is the number of elements in the attribute’s vector (1, 2, 3, or 4), or the special token GL_BGRA, which should be specified when packed vertex data is used. The type parameter is a token that specifies the type of the data that is contained in the buffer object. Table 3.6 describes the token names that may be specified for type and the OpenGL data type that they correspond to:
Table 3.6. Values of Type for glVertexAttribPointer()
Token Value |
OpenGL Type |
GL_BYTE |
GLbyte (signed 8-bit bytes) |
GL_UNSIGNED_BYTE |
GLubyte (unsigned 8-bit bytes) |
GL_SHORT |
GLshort (signed 16-bit words) |
GL_UNSIGNED_SHORT |
GLushort (unsigned 16-bit words) |
GL_INT |
GLint (signed 32-bit integers) |
GL_UNSIGNED_INT |
GLuint (unsigned 32-bit integers) |
GL_FIXED |
GLfixed (16.16 signed fixed point) |
GL_FLOAT |
GLfloat (32-bit IEEE single-precision floating point) |
GL_HALF_FLOAT |
GLhalf (16-bit S1E5M10 half-precision floating point) |
GL_DOUBLE |
GLdouble (64-bit IEEE double-precision floating point) |
GL_INT_2_10_10_10_REV |
GLuint (packed data) |
GL_UNSIGNED_INT_2_10_10_10_REV |
GLuint (packed data) |
Note that while integer types such as GL_SHORT or GL_UNSIGNED_INT can be passed to the type argument, this tells OpenGL only what data type is stored in memory in the buffer object. OpenGL will convert this data to floating point in order to load it into floating-point vertex attributes. The way this conversion is performed is controlled by the normalize parameter. When normalize is GL_FALSE, integer data is simply typecast into floating-point format before being passed to the vertex shader. This means that if you place the integer value 4 into a buffer and use the GL_INT token for the type when normalize is GL_FALSE, the value 4.0 will be placed into the shader. When normalize is GL_TRUE, the data is normalized before being passed to the vertex shader. To do this, OpenGL divides each element by a fixed constant that depends on the incoming data type. When the data type is signed, the following formula is used:
Whereas, if the data type is unsigned, the following formula is used:
In both cases, f is the resulting floating-point value, c is the incoming integer component, and b is the number of bits in the data type (i.e., 8 for GL_UNSIGNED_BYTE, 16 for GL_SHORT, and so on). Note that unsigned data types are also scaled and biased before being divided by the type-dependent constant. To return to our example of putting 4 into an integer vertex attribute, we get:
which works out to about 0.000000009313—a pretty small number!
Integer Vertex Attributes
If you are familiar with the way floating-point numbers work, you’ll also realize that precision is lost as numbers become very large, and so the full range of integer values cannot be passed into a vertex shader using floating-point attributes. For this reason, we have integer vertex attributes. These are represented in vertex shaders by the int
, ivec2
, ivec3
, or ivec4
types or their unsigned counterparts—uint
, uvec2
, uvec3
, and uvec4
.
A second vertex-attribute function is needed in order to pass raw integers into these vertex attributes—one that doesn’t automatically convert everything to floating point. This is glVertexAttribIPointer()—the I stands for integer.
Notice that the parameters to glVertexAttribIPointer() are identical to the parameters to glVertexAttribPointer(), except for the omission of the normalize parameter. normalize is missing because it’s not relevant to integer vertex attributes. Only the integer data type tokens, GL_BYTE, GL_UNSIGNED_BYTE, GL_SHORT, GL_UNSIGNED_SHORT, GL_INT, and GL_UNSIGNED_INT may be used for the type parameter.
Double-Precision Vertex Attributes
The third variant of glVertexAttribPointer() is glVertexAttribLPointer()—here the L stands for “long”. This version of the function is specifically for loading attribute data into 64-bit double-precision floating-point vertex attributes.
Again, notice the lack of the normalize parameter. In glVertexAttribPointer(), normalize was used only for integer data types that aren’t legal here, and so the parameter is not needed. If GL_DOUBLE is used with glVertexAttribPointer(), the data is automatically down-converted to 32-bit single-precision floating-point representation before being passed to the vertex shader—even if the target vertex attribute was declared using one of the double-precision types double
, dvec2
, dvec3
, or dvec4
, or one of the double-precision matrix types such as dmat4
. However, with glVertexAttribLPointer(), the full precision of the input data is kept and passed to the vertex shader.
Packed Data Formats for Vertex Attributes
Going back to the glVertexAttribPointer() command, you will notice that the allowed values for the size parameter are 1, 2, 3, 4, and the special token GL_BGRA. Also, the type parameter may take one of the special values GL_INT_2_10_10_10_REV or GL_UNSIGNED_INT_2_10_10_10_REV, both of which correspond to the GLuint data type. These special tokens are used to represent packed data that can be consumed by OpenGL. The GL_INT_2_10_10_10_REV and GL_UNSIGNED_INT_2_10_10_10_REV tokens represent four-component data represented as ten bits for each of the first three components and two for the last, packed in reverse order into a single 32-bit quantity (a GLuint). GL_BGRA could just have easily been called GL_ZYXW.5 Looking at the data layout within the 32-bit word, you would see the bits divided up as shown in Figure 3.3.
Figure 3.3. Packing of elements in a BGRA-packed vertex attribute
In Figure 3.3, the elements of the vertex are packed into a single 32-bit integer in the order w, x, y, z—which when reversed is z, y, x, w, or b, g, r, a when using color conventions. In Figure 3.4, the coordinates are packed in the order w, z, y, x, which reversed and written in color conventions is r, g, b, a.
Figure 3.4. Packing of elements in a RGBA-packed vertex attribute
Vertex data may be specified only in the first of these two formats by using the GL_INT_2_10_10_10_REV or GL_UNSIGNED_INT_2_10_10_10_REV tokens. When one of these tokens is used as the type parameter to glVertexAttribPointer(), each vertex consumes one 32-bit word in the vertex array. The word is unpacked into its components and then optionally normalized (depending on the value of the normalize parameter before being loaded into the appropriate vertex attribute. This data arrangement is particularly well suited to normals or other types of attributes that can benefit from the additional precision afforded by the 10-bit components but perhaps don’t require the full precision offered by half-float data (which would take 16-bits per component). This allows the conservation of memory space and bandwidth, which helps improve performance.
Static Vertex-Attribute Specification
Remember from Chapter 1 where you were introduced to glEnableVertexAttribArray() and glDisableVertexAttribArray(). These functions are used to tell OpenGL which vertex attributes are backed by vertex buffers. Before OpenGL will read any data from your vertex buffers, you must enable the corresponding vertex attribute arrays with glEnableVertexAttribArray(). You may wonder what happens if you don’t enable the attribute array for one of your vertex attributes. In that case, the static vertex attribute is used. The static vertex attribute for each vertex is the default value that will be used for the attribute when there is no enabled attribute array for it. For example, imagine you had a vertex shader that would read the vertex color from one of the vertex attributes. Now suppose that all of the vertices in a particular mesh or part of that mesh had the same color. It would be a waste of memory and potentially of performance to fill a buffer full of that constant value for all the vertices in the mesh. Instead, you can just disable the vertex attribute array and use the static vertex attribute to specify color for all of the vertices.
The static vertex attribute for each attribute may be specified using one of glVertexAttrib*() functions. When the vertex attribute is declared as a floating-point quantity in the vertex shader (i.e., it is of type float
, vec2
, vec3
, vec4
, or one of the floating-point matrix types such as mat4
), the following glVertexAttrib*() commands can be used to set its value.
All of these functions implicitly convert the supplied parameters to floating-point before passing them to the vertex shader (unless they’re already floating-point). This conversion is a simple typecast. That is, the values are converted exactly as specified as if they had been specified in a buffer and associated with a vertex attribute by calling glVertexAttribPointer() with the normalize parameter set to GL_FALSE. For the integer variants of the functions, versions exist that normalize the parameters to the range [0, 1] or [–1, 1] depending on whether the parameters are signed or unsigned. These are:
Even with these commands, the parameters are still converted to floating-point before being passed to the vertex shader. Thus, they are suitable only for setting the static values of attributes declared with one of the single-precision floating-point data types. If you have vertex attributes that are declared as integers or double-precision floating-point variables, you should use one of the following functions:
Furthermore, if you have vertex attributes that are declared as one of the double-precision floating-point types, you should use one of the L variants of glVertexAttrib*(), which are:
Both the glVertexAttribI*() and glVertexAttribL*() variants of glVertexAttrib*() pass their parameters through to the underlying vertex attribute just as the I versions of glVertexAttribIPointer() do.
If you use one of the glVertexAttrib*() functions with less components than there are in the underlying vertex attribute (e.g., you use glVertexAttrib*() 2f to set the value of a vertex attribute declared as a vec4
), default values are filled in for the missing components. For w, 1.0 is used as the default value, and for y and z, 0.0 is used.6 If you use a function that takes more components than are present in the vertex attribute in the shader, the additional components are simply discarded.