Vertex Attributes, Vertex Arrays, and Buffer Objects - OpenGL ES 3.0: Programming Guide, Second Edition (2014)

OpenGL ES 3.0: Programming Guide, Second Edition (2014)

Chapter 6. Vertex Attributes, Vertex Arrays, and Buffer Objects

This chapter describes how vertex attributes and data are specified in OpenGL ES 3.0. We discuss what vertex attributes are, how to specify them and their supported data formats, and how to bind vertex attributes for use in a vertex shader. After reading this chapter, you will have a good grasp of what vertex attributes are and how to draw primitives with vertex attributes in OpenGL ES 3.0.

Vertex data, also referred to as vertex attributes, specify per-vertex data. This per-vertex data can be specified for each vertex, or a constant value can be used for all vertices. For example, if you want to draw a triangle that has a solid color (for the sake of this example, suppose the color is black, as shown in Figure 6-1), you would specify a constant value that will be used by all three vertices of the triangle. However, the position of the three vertices that make up the triangle will not be the same, so we will need to specify a vertex array that stores three position values.

Image

Figure 6-1 Triangle with a Constant Color Vertex and Per-Vertex Position Attributes

Specifying Vertex Attribute Data

Vertex attribute data can be specified for each vertex using a vertex array, or a constant value can be used for all vertices of a primitive.

All OpenGL ES 3.0 implementations must support a minimum of 16 vertex attributes. An application can query the exact number of vertex attributes that are supported by a particular implementation. The following code shows how an application can query the number of vertex attributes an implementation actually supports:

GLint maxVertexAttribs; // n will be >= 16
glGetIntegerv(GL_MAX_VERTEX_ATTRIBS, &maxVertexAttribs);

Constant Vertex Attribute

A constant vertex attribute is the same for all vertices of a primitive, so only one value needs to be specified for all the vertices of a primitive. It is specified using any of the following functions:

void glVertexAttriblf(GLuint index, GLfloat x);
void glVertexAttrib2f(GLuint index, GLfloat x, GLfloat y);
void glVertexAttrib3f(GLuint index, GLfloat x, GLfloat y,
GLfloat z);
void glVertexAttrib4f(GLuint index, GLfloat x, GLfloat y,
GLfloat z, GLfloat w);
void glVertexAttriblfv(GLuint index, const GLfloat *values);
void glVertexAttrib2fv(GLuint index, const GLfloat *values);
void glVertexAttrib3fv(GLuint index, const GLfloat *values);
void glVertexAttrib4fv(GLuint index, const GLfloat *values);

The glVertexAttrib* commands are used to load the generic vertex attribute specified by index. The functions glVertexAttriblf and glVertexAttriblfv load (x, 0.0, 0.0, 1.0) into the generic vertex attribute. glVertexAttrib2f and glVertexAttrib2fv load (x,y, 0.0, 1.0) into the generic vertex attribute. glVertexAttrib3f and glVertexAttrib3fv load (x, y, z, 1.0) into the generic vertex attribute. glVertexAttrib4f and glVertexAttrib4fv load (x, y, z, w) into the generic vertex attribute. In practice, constant vertex attributes provide equivalent functionality to using a scalar/vector uniform, and using either is an acceptable choice.

Vertex Arrays

Vertex arrays specify attribute data per vertex and are buffers stored in the application’s address space (what OpenGL ES calls the client space). They serve as the basis for vertex buffer objects that provide an efficient and flexible way for specifying vertex attribute data. Vertex arrays are specified using the glVertexAttribPointer or glVertexAttribIPointer function.

Image

Image

Next, we present a few examples that illustrate how to specify vertex attributes with glVertexAttribPointer. Two methods are commonly used for allocating and storing vertex attribute data:

• Store vertex attributes together in a single buffer—a method called an array of structures. The structure represents all attributes of a vertex, and we have an array of these attributes per vertex.

• Store each vertex attribute in a separate buffer—a method called a structure of arrays.

Suppose each vertex has four vertex attributes—position, normal, and two texture coordinates—and these attributes are stored together in one buffer that is allocated for all vertices. The vertex position attribute is specified as a vector of three floats (x, y, z), the vertex normal is also specified as a vector of three floats, and each texture coordinate is specified as a vector of two floats. Figure 6-2 gives the memory layout of this buffer. In this case, the stride of the buffer is the combined size of all attributes that make up the vertex (one vertex is equal to 10 floats or 40 bytes – 12 bytes for Position, 12 bytes for Normal, 8 bytes for Tex0, and 8 bytes for Tex1).

Image

Figure 6-2 Position, Normal, and Two Texture Coordinates Stored as an Array

Example 6-1 describes how these four vertex attributes are specified with glVertexAttribPointer. Note that we are illustrating how to use client-side vertex arrays here so that we can explain the concept of specifying per-vertex data. We recommend that applications use vertex buffer objects (described later in the chapter) and avoid client-side vertex arrays to achieve best performance. Client-side vertex arrays are supported in OpenGL ES 3.0 only for backward compatibility with OpenGL ES 2.0. In OpenGL ES 3.0, vertex buffer objects are always recommended.

Example 6-1 Array of Structures


#define VERTEX_POS_SIZE 3 // x, y, and z
#define VERTEX_NORMAL_SIZE 3 // x, y, and z
#define VERTEX_TEXCOORD0_SIZE 2 // s and t
#define VERTEX_TEXCOORDl_SIZE 2 // s and t

#define VERTEX_POS_INDX 0
#define VERTEX_NORMAL_INDX 1
#define VERTEX_TEXCOORD0_INDX 2
#define VERTEX_TEXCOORDl_INDX 3

// the following 4 defines are used to determine the locations
// of various attributes if vertex data are stored as an array
// of structures
#define VERTEX_POS_OFFSET 0
#define VERTEX_NORMAL_OFFSET 3
#define VERTEX_TEXCOORD0_OFFSET 6
#define VERTEX_TEXC00RD1_0FFSET 8

#define VERTEX_ATTRIB_SIZE (VERTEX_POS_SIZE + \
VERTEX_NORMAL_SIZE + \
VERTEX_TEXCOORD0_SIZE + \
VERTEX_TEXC00RD1_SIZE)

float *p = (float*) malloc(numVertices * VERTEX_ATTRIB_SIZE
* sizeof(float));

// position is vertex attribute 0
glVertexAttribPointer(VERTEX_POS_INDX, VERTEX_POS_SIZE,
GL_FLOAT, GL_FALSE,
VERTEX_ATTRIB_SIZE * sizeof(float),
p);

// normal is vertex attribute 1
glVertexAttribPointer(VERTEX_NORMAL_INDX, VERTEX_NORMAL_SIZE,
GL_FLOAT, GL_FALSE,
VERTEX_ATTRIB_SIZE * sizeof(float),
(p + VERTEX_NORMAL_OFFSET));

// texture coordinate 0 is vertex attribute 2
glVertexAttribPointer(VERTEX_TEXCOORDO_INDX,
VERTEX_TEXCOORD0_SIZE,
GL_FLOAT, GL_FALSE,
VERTEX_ATTRIB_SIZE * sizeof(float),
(p + VERTEX_TEXCOORD0_OFFSET));

// texture coordinate 1 is vertex attribute 3
glVertexAttribPointer(VERTEX_TEXCOORDl_INDX,

VERTEX_TEXC00RD1_SIZE,

GL_FLOAT, GL_FALSE,

VERTEX_ATTRIB_SIZE * sizeof(float),

(p + VERTEX_TEXC00RD1_0FFSET));


In Example 6-2, position, normal, and texture coordinates 0 and 1 are stored in separate buffers.

Example 6-2 Structure of Arrays


float *position = (float*) malloc(numVertices *
VERTEX_POS_SIZE * sizeof(float));
float *normal = (float*) malloc(numVertices *
VERTEX_NORMAL_SIZE * sizeof(float));
float *texcoordO = (float*) malloc(numVertices *
VERTEX_TEXCOORD0_SIZE * sizeof(float));
float *texcoordl = (float*) malloc(numVertices *
VERTEX_TEXC00RD1_SIZE * sizeof(float));

// position is vertex attribute 0
glVertexAttribPointer(VERTEX_POS_INDX, VERTEX_POS_SIZE,
GL_FLOAT, GL_FALSE,
VERTEX_POS_SIZE * sizeof(float),
position);

// normal is vertex attribute 1
glVertexAttribPointer(VERTEX_NORMAL_INDX, VERTEX_NORMAL_SIZE,
GL_FLOAT, GL_FALSE,
VERTEX_NORMAL_SIZE * sizeof(float),
normal);

// texture coordinate 0 is vertex attribute 2
glVertexAttribPointer(VERTEX_TEXCOORDO_INDX,
VERTEX_TEXCOORD0_SIZE,
GL_FLOAT, GL_FALSE,
VERTEX_TEXCOORD0_SIZE *
sizeof(float), texcoordO);

// texture coordinate 1 is vertex attribute 3
glVertexAttribPointer(VERTEX_TEXCOORDl_INDX,
VERTEX_TEXC00RD1_SIZE,
GL_FLOAT, GL_FALSE,
VERTEX_TEXC00RD1_SIZE * sizeof(float),
texcoordl);


Performance Hints

How to Store Different Attributes of a Vertex

We described the two most common ways of storing vertex attributes: an array of structures and a structure of arrays. The question to ask is which allocation method would be the most efficient for OpenGL ES 3.0 hardware implementations. In most cases, the answer is an array of structures. The reason is that the attribute data for each vertex can be read in sequential fashion, which will most likely result in an efficient memory access pattern. A disadvantage of using an array of structures becomes apparent when an application wants to modify specific attributes. If a subset of vertex attribute data needs to be modified (e.g., texture coordinates), this will result in strided updates to the vertex buffer. When the vertex buffer is supplied as a buffer object, the entire vertex attribute buffer will need to be reloaded. You can avoid this inefficiency by storing vertex attributes that are dynamic in nature in a separate buffer.

Which Data Format to Use for Vertex Attributes

The vertex attribute data format specified by the type argument in glVertexAttribPointer can affect not only the graphics memory storage requirements for vertex attribute data, but also the overall performance, which is a function of the memory bandwidth required to render the frame(s). The smaller the data footprint, the lower the memory bandwidth required. OpenGL ES 3.0 supports a 16-bit floating-point vertex format named GL_HALF_FLOAT (described in detail in Appendix A). Our recommendation is that applications use GL_HALF_FLOAT wherever possible. Texture coordinates, normals, binormals, tangent vectors, and so on are good candidates to be stored using GL_HALF_FLOAT for each component. Color could be stored as GL_UNSIGNED_BYTE with four components per vertex color. We also recommend GL_HALF_FLOAT for vertex position, but recognize that this choice might not be feasible for quite a few cases. For such cases, the vertex position could be stored as GL_FLOAT.

How the Normalized Flag in gIVertexAttribPointer Works

Vertex attributes are internally stored as a single-precision floating-point number before being used in a vertex shader. If the data type indicates that the vertex attribute is not a float, then the vertex attribute will be converted to a single-precision floating-point number before it is used in a vertex shader. The normalized flag controls the conversion of the non-float vertex attribute data to a single precision floating-point value. If the normalized flag is false, the vertex data are converted directly to a floating-point value. This would be similar to casting the variable that is not a float type to float. The following code gives an example:

GLfloat f;
GLbyte b;
f = (GLfloat)b; // f represents values in the range [-128.0,
// 127.0]

If the normalized flag is true, the vertex data is mapped to the [–1.0, 1.0] range if the data type is GL_BYTE, GL_SHORT, or GL_FIXED, or to the [0.0, 1.0] range if the data type is GL_UNSIGNED_BYTE or GL_UNSIGNED_SHORT.

Table 6-1 describes conversion of non-floating-point data types with the normalized flag set. The value c in the second column of Table 6-1 refers to a value of the format specified in the first column.

Image

Table 6-1 Data Conversions

It is also possible to access integer vertex attribute data as integers in the vertex shader rather than having them be converted to floats. In this case, the glVertexAttribIPointer function should be used and the vertex attribute should be declared to be of an integer type in the vertex shader.

Selecting Between a Constant Vertex Attribute or a Vertex Array

The application can enable OpenGL ES to use either the constant data or data from vertex array. Figure 6-3 describes how this works in OpenGL ES 3.0.

Image

Figure 6-3 Selecting Constant or Vertex Array Vertex Attribute

The commands glEnableVertexAttribArray and glDisableVertexAttribArray are used to enable and disable a generic vertex attribute array, respectively. If the vertex attribute array is disabled for a generic attribute index, the constant vertex attribute data specified for that index will be used.

Image

Example 6-3 illustrates how to draw a triangle where one of the vertex attributes is constant and the other is specified using a vertex array.

Example 6-3 Using Constant and Vertex Array Attributes


int Init ( ESContext *esContext )
{
UserData *userData = (UserData*) esContext->userData;
const char vShaderStr[] =
"#version 300 es \n"
"layout(location = 0) in vec4 a_color; \n"
"layout(location = 1) in vec4 a_position; \n"
"out vec4 v_color; \n"
"void main() \n"
"{ \n"
" v_color = a_color; \n"
" gl_Position = a_position; \n"
"}";

const char fShaderStr[] =
"#version 300 es \n"
"precision mediump float; \n"
"in vec4 v_color; \n"
"out vec4 o_fragColor; \n"
"void main() \n"
"{ \n"
" o_fragColor = v_color; \n"
"}" ;

GLuint programObject;

// Create the program object
programObject = esLoadProgram ( vShaderStr, fShaderStr );

if ( programObject == 0 )
return GL_FALSE;

// Store the program object
userData->programObject = programObject;

glClearColor ( 0.0f, 0.0f, 0.0f, 0.0f );
return GL_TRUE;
}

void Draw ( ESContext *esContext )
{
UserData *userData = (UserData*) esContext->userData;
GLfloat color[4] = { 1.0f, 0.0f, 0.0f, 1.0f };
// 3 vertices, with (x, y, z) per-vertex
GLfloat vertexPos[3 * 3] =
{
0.0f, 0.5f, 0.0f, // v0
-0.5f, -0.5f, 0.0f, // v1
0.5f, -0.5f, 0.0f // v2
};

glViewport ( 0, 0, esContext->width, esContext->height );

glClear ( GL_COLOR_BUFFER_BIT );

glUseProgram ( userData->programObject );

glVertexAttrib4fv ( 0, color );
glVertexAttribPointer ( 1, 3, GL_FLOAT, GL_FALSE, 0,
vertexPos );
glEnableVertexAttribArray ( 1 );

glDrawArrays ( GL_TRIANGLES, 0, 3 );

glDisableVertexAttribArray ( 1 );
}


The vertex attribute color used in the code example is a constant value specified with glVertexAttrib4fv without enabling the vertex attribute array 0. The vertexPos attribute is specified by using a vertex array with glVertexAttribPointer and enabling the array withglEnableVertexAttribArray. The value of color will be the same for all vertices of the triangle(s) drawn, whereas the vertexPos attribute could vary for vertices of the triangle(s) drawn.

Declaring Vertex Attribute Variables in a Vertex Shader

We have looked at what a vertex attribute is, and considered how to specify vertex attributes in OpenGL ES. We now discuss how to declare vertex attribute variables in a vertex shader.

In a vertex shader, a variable is declared as a vertex attribute by using the in qualifier. Optionally, the attribute variable can also include a layout qualifier that provides the attribute index. A few example declarations of vertex attributes are given here:

layout(location = 0) in vec4 a_position;
layout(location = 1) in vec2 a_texcoord;
layout(location = 2) in vec3 a_normal;

The in qualifier can be used only with the data types float, vec2, vec3, vec4, int, ivec2, ivec3, ivec4, uint, uvec2, uvec3, uvec4, mat2, mat2x2, mat2x3, mat2x4, mat3, mat3x3, mat3x4, mat4, mat4x2, and mat4x3. Attribute variables cannot be declared as arrays or structures. The following example declarations of vertex attributes are invalid and should result in a compilation error:

in foo_t a_A; // foo_t is a structure
in vec4 a_B[10];

An OpenGL ES 3.0 implementation supports GL_MAX_VERTEX_ATTRIBS four-component vector vertex attributes. A vertex attribute that is declared as a scalar, two-component vector, or three-component vector will count as a single four-component vector attribute. Vertex attributes declared as two-dimensional, three-dimensional, or four-dimensional matrices will count as two, three, or four 4-component vector attributes, respectively. Unlike uniform and vertex shader output/fragment shader input variables, which are packed automatically by the compiler, attributes do not get packed. Please consider your choices carefully when declaring vertex attributes with sizes less than a four-component vector, as the maximum number of vertex attributes available is a limited resource. It might be better to pack them together into a single four-component attribute instead of declaring them as individual vertex attributes in the vertex shader.

Variables declared as vertex attributes in a vertex shader are read-only variables and cannot be modified. The following code should cause a compilation error:

in vec4 a_pos;
uniform vec4 u_v;

void main()
{
a_pos = u_v; <--- cannot assign to a_pos as it is read-only
}

An attribute can be declared inside a vertex shader—but if it is not used, then it is not considered active and does not count against the limit. If the number of attributes used in a vertex shader is greater than GL_MAX_VERTEX_ATTRIBS, the vertex shader will fail to link.

Once a program has been successfully linked, we may need to find out the number of active vertex attributes used by the vertex shader attached to this program. Note that this step is necessary only if you are not using input layout qualifiers for attributes. In OpenGL ES 3.0, it is recommended that you use layout qualifiers; thus you will not need to query this information after the fact. However, for completeness, the following line of code shows how to get the number of active vertex attributes:

glGetProgramiv(program, GL_ACTIVE_ATTRIBUTES, &numActiveAttribs);

A detailed description of glGetProgramiv is given in Chapter 4, “Shaders and Programs.”

The list of active vertex attributes used by a program and their data types can be queried using the glGetActiveAttrib command.

Image

Image

The glGetActiveAttrib call provides information about the attribute selected by index. As detailed in the description of glGetActiveAttrib, index must be a value between 0 and GL_ACTIVE_ATTRIBUTES – l. The value of GL_ACTIVE_ATTRIBUTES is queried usingglGetProgramiv. An index of 0 selects the first active attributes, and an index of GL_ACTIVE_ATTRIBUTES – 1 selects the last vertex attribute.

Binding Vertex Attributes to Attribute Variables in a Vertex Shader

We discussed earlier that in a vertex shader, vertex attribute variables are specified by the in qualifier, the number of active attributes can be queried using glGetProgramiv, and the list of active attributes in a program can be queried using glGetActiveAttrib. We also described how generic attribute indices that range from 0 to (GL_MAX_VERTEX_ATTRIBS – 1) are used to enable a generic vertex attribute and specify a constant or per-vertex (i.e., vertex array) value using the glVertexAttrib* and glVertexAttribPointer commands. Now we consider how to map this generic attribute index to the appropriate attribute variable declared in the vertex shader. This mapping will allow appropriate vertex data to be read into the correct vertex attribute variable in the vertex shader.

Figure 6-4 describes how generic vertex attributes are specified and bound to attribute names in a vertex shader.

Image

Figure 6-4 Specifying and Binding Vertex Attributes for Drawing One or More Primitives

In OpenGL ES 3.0, three approaches may be used to map a generic vertex attribute index to an attribute variable name in the vertex shader. These approaches can be categorized as follows:

• The index can be specified in the vertex shader source code using the layout(location = N) qualifier (recommended).

• OpenGL ES 3.0 will bind the generic vertex attribute index to the attribute name.

• The application can bind the vertex attribute index to an attribute name.

The easiest way to bind attributes to a location is to simply use the layout(location = N) qualifier; this approach requires the least amount of code. However, in some cases, the other two options might be more desirable. The glBindAttribLocation command can be used to bind a generic vertex attribute index to an attribute variable in a vertex shader. This binding takes effect when the program is linked the next time—it does not change the bindings used by the currently linked program.

Image

If name was bound previously, its assigned binding is replaced with an index. glBindAttribLocation can be called even before a vertex shader is attached to a program object. As a consequence, this call can be used to bind any attribute name. Attribute names that do not exist or are not active in a vertex shader attached to the program object are ignored.

Another option is to let OpenGL ES 3.0 bind the attribute variable name to a generic vertex attribute index. This binding is performed when the program is linked. In the linking phase, the OpenGL ES 3.0 implementation performs the following operation for each attribute variable:

For each attribute variable, check whether a binding has been specified via glBindAttribLocation. If a binding is specified, the appropriate attribute index specified is used. If not, the implementation will assign a generic vertex attribute index.

This assignment is implementation specific; that is, it can vary from one OpenGL ES 3.0 implementation to another. An application can query the assigned binding by using the glGetAttribLocation command.

Image

glGetAttribLocation returns the generic attribute index that was bound to the attribute variable name when the program object defined by program was last linked. If name is not an active attribute variable, or if program is not a valid program object or was not linked successfully, then –1 is returned, indicating an invalid attribute index.

Vertex Buffer Objects

The vertex data specified using vertex arrays are stored in client memory. This data must be copied from client memory to graphics memory when a draw call such as glDrawArrays or glDrawElements is made. These two commands are described in detail in Chapter 7, “Primitive Assembly and Rasterization.” It would, however, be much better if we did not have to copy the vertex data on every draw call, but instead could cache the data in graphics memory. This approach can significantly improve the rendering performance and also reduce the memory bandwidth and power consumption requirements, both of which are quite important for handheld devices. This is where vertex buffer objects can help. Vertex buffer objects allow OpenGL ES 3.0 applications to allocate and cache vertex data in high-performance graphics memory and render from this memory, thereby avoiding resending data every time a primitive is drawn. Not only the vertex data, but also the element indices that describe the vertex indices of the primitive and are passed as an argument to glDrawElements, can be cached.

OpenGL ES 3.0 supports two types of buffer objects that are used for specifying vertex and primitive data: array buffer objects and element array buffer objects. The array buffer objects specified by the GL_ARRAY_BUFFER token are used to create buffer objects that will store vertex data. The element array buffer objects specified by the GL_ELEMENT_ARRAY_BUFFER token are used to create buffer objects that will store indices of a primitive. Other buffer object types in OpenGL ES 3.0 are described elsewhere in this book: uniform buffers (Chapter 4), transform feedback buffers (Chapter 8), pixel unpack buffers (Chapter 9), pixel pack buffers (Chapter 11), and copy buffers (the Copying Buffer Objects section later in this chapter). For now, we will focus on the buffer objects used for specifying vertex attributes and element arrays.


Note

To get best performance, we recommend that OpenGL ES 3.0 applications use vertex buffer objects for vertex attribute data and element indices.


Before we can render using buffer objects, we need to allocate the buffer objects and upload the vertex data and element indices into appropriate buffer objects. This is demonstrated by the sample code in Example 6-4.

Example 6-4 Creating and Binding Vertex Buffer Objects


void initVertexBufferObjects(vertex_t *vertexBuffer,
GLushort *indices,
GLuint numVertices,
GLuint numlndices,
GLuint *vboIds)

{
glGenBuffers(2, vboIds);

glBindBuffer(GL_ARRAY_BUFFER, vboIds[0]);
glBufferData(GL_ARRAY_BUFFER, numVertices *
sizeof(vertex_t), vertexBuffer,
GL_STATIC_DRAW);

// bind buffer object for element indices
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, vboIds[1]);
glBufferData(GL_ELEMENT_ARRAY_BUFFER,
numIndices * sizeof(GLushort),
indices, GL_STATIC_DRAW);
}


The code in Example 6-4 creates two buffer objects: a buffer object to store the actual vertex attribute data, and a buffer object to store the element indices that make up the primitive. In this example, the glGenBuffers command is called to get two unused buffer object names in vboIds. The unused buffer object names returned in vboIds are then used to create an array buffer object and an element array buffer object. The array buffer object is used to store vertex attribute data for vertices of one or more primitives. The element array buffer object stores the indices of one or more primitives. The actual array or element data are specified using glBufferData. Note that GL_STATIC_DRAW is passed as an argument to glBufferData. This value is used to describe how the buffer is accessed by the application and will be described later in this section.

Image

glGenBuffers assigns n buffer object names and returns them in buffers. The buffer object names returned by glGenBuffers are unsigned integer numbers other than 0. The value 0 is reserved by OpenGL ES and does not refer to a buffer object. Attempts to modify or query the buffer object state for buffer object 0 will generate an error.

The glBindBuffer command is used to make a buffer object current. The first time a buffer object name is bound by calling glBindBuffer, the buffer object is allocated with the default state; if the allocation is successful, this allocated object is bound as the current buffer object for the target.

Image

Note that glGenBuffers is not required to assign a buffer object name before it is bound using glBindBuffer. Alternatively, an application can specify an unused buffer object name with glBindBuffer. However, we recommend that OpenGL ES applications call glGenBuffersand use buffer object names returned by glGenBuffers instead of specifying their own buffer object names.

The state associated with a buffer object can be categorized as follows:

• GL_BUFFER_SIZE. This refers to the size of the buffer object data that is specified by glBufferData. The initial value when the buffer object is first bound using glBindBuffer is 0.

• GL_BUFFER_USAGE. This is a hint as to how the application will use the data stored in the buffer object. It is described in detail in Table 6-2. The initial value is GL_STATIC_DRAW.

Image

Image

Table 6-2 Buffer Usage

As mentioned earlier, GL_BUFFER_USAGE is a hint to OpenGL ES—not a guarantee. Therefore, an application could allocate a buffer object data store with usage set to GL_STATIC_DRAW and frequently modify it.

The vertex array data or element array data storage is created and initialized using the glBufferData command.

Image

glBufferData will reserve appropriate data storage based on the value of size. The data argument can be a NULL value, indicating that the reserved data store remains uninitialized. If data is a valid pointer, then the contents of data are copied to the allocated data store. The contents of the buffer object data store can be initialized or updated using the glBufferSubData command.

Image

After the buffer object data store has been initialized or updated using glBufferData or glBufferSubData, the client data store is no longer needed and can be released. For static geometry, applications can free the client data store and reduce the overall system memory consumed by the application. This might not be possible for dynamic geometry.

We now look at drawing primitives with and without buffer objects. Example 6-5 describes drawing primitives with and without vertex buffer objects. Notice that the code to set up the vertex attributes is very similar. In this example, we use the same buffer object for all attributes of a vertex. When a GL_ARRAY_BUFFER buffer object is used, the pointer argument in glVertexAttribPointer changes from being a pointer to the actual data to being an offset in bytes into the vertex buffer store allocated using glBufferData. Similarly, if a validGL_ELEMENT_ARRAY_BUFFER object is used, the indices argument in glDrawElements changes from being a pointer to the actual element indices to being an offset in bytes to the element index buffer store allocated using glBufferData.

Example 6-5 Drawing with and without Vertex Buffer Objects


#define VERTEX_POS_SIZE 3 // x, y, and z
#define VERTEX_COLOR_SIZE 4 // r, g, b, and a

#define VERTEX_POS_INDX 0
#define VERTEX_COLOR_INDX 1
//
// vertices - pointer to a buffer that contains vertex
// attribute data
// vtxStride - stride of attribute data / vertex in bytes
// numIndices - number of indices that make up primitives
// drawn as triangles
// indices - pointer to element index buffer
//
void DrawPrimitiveWithoutVBOs(GLfloat *vertices,
GLint vtxStride,
GLint numIndices,
GLushort *indices)
{
GLfloat *vtxBuf = vertices;

glBindBuffer(GL_ARRAY_BUFFER, 0);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);

glEnableVertexAttribArray(VERTEX_POS_INDX);
glEnableVertexAttribArray(VERTEX_COLOR_INDX);

glVertexAttribPointer(VERTEX_POS_INDX, VERTEX_POS_SIZE,
GL_FLOAT, GL_FALSE, vtxStride,
vtxBuf);
vtxBuf += VERTEX_POS_SIZE;

glVertexAttribPointer(VERTEX_COLOR_INDX,
VERTEX_COLOR_SIZE, GL_FLOAT,
GL_FALSE, vtxStride, vtxBuf);

glDrawElements(GL_TRIANGLES, numIndices, GL_UNSIGNED_SHORT,
indices);

glDisableVertexAttribArray(VERTEX_POS_INDX);
glDisableVertexAttribArray(VERTEX_COLOR_INDX);

}

void DrawPrimitiveWithVBOs(ESContext *esContext,
GLint numVertices, GLfloat *vtxBuf,
GLint vtxStride, GLint numIndices,
GLushort *indices)
{
UserData *userData = (UserData*) esContext->userData;
GLuint offset = 0;
// vboIds[0] - used to store vertex attribute data
// vboIds[l] - used to store element indices
if ( userData->vboIds[0] == 0 && userData->vboIds[1] == 0 )
{
// Only allocate on the first draw
glGenBuffers(2, userData->vboIds);

glBindBuffer(GL_ARRAY_BUFFER, userData->vboIds[0]);
glBufferData(GL_ARRAY_BUFFER, vtxStride * numVertices,
vtxBuf, GL_STATIC_DRAW);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER,
userData->vboIds[1]);
glBufferData(GL_ELEMENT_ARRAY_BUFFER,
sizeof(GLushort) * numIndices,
indices, GL_STATIC_DRAW);
}

glBindBuffer(GL_ARRAY_BUFFER, userData->vboIds[0]);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, userData->vboIds[1]);

glEnableVertexAttribArray(VERTEX_POS_INDX);
glEnableVertexAttribArray(VERTEX_COLOR_INDX);

glVertexAttribPointer(VERTEX_POS_INDX, VERTEX_POS_SIZE,
GL_FLOAT, GL_FALSE, vtxStride,
(const void*)offset);

offset += VERTEX_POS_SIZE * sizeof(GLfloat);
glVertexAttribPointer(VERTEX_COLOR_INDX,
VERTEX_COLOR_SIZE,
GL_FLOAT, GL_FALSE, vtxStride,
(const void*)offset);

glDrawElements(GL_TRIANGLES, numIndices, GL_UNSIGNED_SHORT,
0);


glDisableVertexAttribArray(VERTEX_POS_INDX);
glDisableVertexAttribArray(VERTEX_COLOR_INDX);

glBindBuffer(GL_ARRAY_BUFFER, 0);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);
}

void Draw ( ESContext *esContext )
{
UserData *userData = (UserData*) esContext->userData;

// 3 vertices, with (x, y, z),(r, g, b, a) per-vertex
GLfloat vertices[3 * (VERTEX_POS_SIZE + VERTEX_COLOR_SIZE)] =
{
-0.5f, 0.5f, 0.0f, // v0
1.0f, 0.0f, 0.0f, 1.0f, // c0
-1.0f, -0.5f, 0.0f, // v1
0.0f, 1.0f, 0.0f, 1.0f, // c1
0.0f, -0.5f, 0.0f, // v2
0.0f, 0.0f, 1.0f, 1.0f, // c2
};
// index buffer data
GLushort indices[3] = { 0, 1, 2 };

glViewport ( 0, 0, esContext->width, esContext->height );
glClear ( GL_COLOR_BUFFER_BIT );
glUseProgram ( userData->programObject );
glUniform1f ( userData->offsetLoc, 0.0f );

DrawPrimitiveWithoutVBOs ( vertices,
sizeof(GLfloat) * (VERTEX_POS_SIZE + VERTEX_COLOR_SIZE),
3, indices );

// offset the vertex positions so both can be seen
glUniform1f ( userData->offsetLoc, 1.0f );

DrawPrimitiveWithVBOs ( esContext, 3, vertices,
sizeof(GLfloat) * (VERTEX_POS_SIZE + VERTEX_COLOR_SIZE),
3, indices );
}


In Example 6-5, we used one buffer object to store all the vertex data. This demonstrates the array of structures method of storing vertex attributes described in Example 6-1. It is also possible to have a buffer object for each vertex attribute—that is, the structure of arrays method of storing vertex attributes described in Example 6-2. Example 6-6 illustrates how drawPrimitiveWithVBOs would look with a separate buffer object for each vertex attribute.

Example 6-6 Drawing with a Buffer Object per Attribute


#define VERTEX_POS_SIZE 3 // x, y, and z
#define VERTEX_COLOR_SIZE 4 // r, g, b, and a

#define VERTEX_POS_INDX 0
#define VERTEX_COLOR_INDX 1

void DrawPrimitiveWithVBOs(ESContext *esContext,
GLint numVertices, GLfloat **vtxBuf,
GLint *vtxStrides, GLint numIndices,
GLushort *indices)
{
UserData *userData = (UserData*) esContext->userData;
// vboIds[0] - used to store vertex position
// vboIds[1] - used to store vertex color
// vboIds[2] - used to store element indices
if ( userData->vboIds[0] == 0 && userData->vboIds[1] == 0 &&
userData->vboIds[2] == 0)
{
// allocate only on the first draw
glGenBuffers(3, userData->vboIds);

glBindBuffer(GL_ARRAY_BUFFER, userData->vboIds[0]);
glBufferData(GL_ARRAY_BUFFER, vtxStrides[0] * numVertices,
vtxBuf[0], GL_STATIC_DRAW);
glBindBuffer(GL_ARRAY_BUFFER, userData->vboIds[1]);
glBufferData(GL_ARRAY_BUFFER, vtxStrides[1] * numVertices,
vtxBuf[1], GL_STATIC_DRAW);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER,
userData->vboIds[2]);
glBufferData(GL_ELEMENT_ARRAY_BUFFER,
sizeof(GLushort) * numIndices,
indices, GL_STATIC_DRAW);
}

glBindBuffer(GL_ARRAY_BUFFER, userData->vboIds[0]);
glEnableVertexAttribArray(VERTEX_POS_INDX);
glVertexAttribPointer(VERTEX_POS_INDX, VERTEX_POS_SIZE,
GL_FLOAT, GL_FALSE, vtxStrides[0], 0);

glBindBuffer(GL_ARRAY_BUFFER, userData->vboIds[1]);
glEnableVertexAttribArray(VERTEX_COLOR_INDX);
glVertexAttribPointer(VERTEX_COLOR_INDX,
VERTEX_COLOR_SIZE,
GL_FLOAT, GL_FALSE, vtxStrides[1], 0);

glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, userData->vboIds[2]);

glDrawElements(GL_TRIANGLES, numIndices,
GL_UNSIGNED_SHORT, 0);

glDisableVertexAttribArray(VERTEX_POS_INDX);
glDisableVertexAttribArray(VERTEX_COLOR_INDX);

glBindBuffer(GL_ARRAY_BUFFER, 0);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);
}


After the application has finished using the buffer objects, they can be deleted using the glDeleteBuffers command.

Image

glDeleteBuffers deletes the buffer objects specified in buffers. Once a buffer object has been deleted, it can be reused as a new buffer object that stores vertex attributes or element indices for a different primitive.

As you can see from these examples, using vertex buffer objects is very easy and requires very little extra work to implement over vertex arrays. The minimal extra work involved in supporting vertex buffer objects is well worth it, considering the performance gain this feature provides. In the next chapter, we discuss how to draw primitives using commands such as glDrawArrays and glDrawElements, and how the primitive assembly and rasterization pipeline stages in OpenGL ES 3.0 work.

Vertex Array Objects

So far, we have covered how to load vertex attributes in two different ways: using client vertex arrays and using vertex buffer objects. Vertex buffer objects are preferred to client vertex arrays because they can reduce the amount of data copied between the CPU and GPU and, therefore, have better performance. In OpenGL ES 3.0, a new feature was introduced to make using vertex arrays even more efficient: vertex array objects (VAOs). As we have seen, setting up drawing using vertex buffer objects can require many calls to glBindBuffer, glVertexAttribPointer, and glEnableVertexAttribArray. To make it faster to switch between vertex array configurations, OpenGL ES 3.0 introduced vertex array objects. VAOs provide a single object that contains all of the state required to switch between vertex array/vertex buffer object configurations.

In fact, there is always a vertex array object that is active in OpenGL ES 3.0. All of the examples so far in this chapter have operated on the default vertex array object (the default VAO has the ID of 0). To create a new vertex array object, you use the glGenVertexArrays function.

Image

Once created, the vertex array object can be bound for use using glBindVertexArray.

Image

Each VAO contains a full state vector that describes all of the vertex buffer bindings and vertex client state enables. When the VAO is bound, its state vector provides the current settings of the vertex buffer state. After binding the vertex array object using glBindVertexArray, subsequent calls that change the vertex array state (glBindBuffer, glVertexAttribPointer, glEnableVertexAttribArray, and glDisableVertexAttribArray) will affect the new VAO.

In this way, an application can quickly switch between vertex array configurations by binding a vertex array object that has been set with state. Rather than having to make many calls to change the vertex array state, all of the changes can be made in a single function call. Example 6-7demonstrates the use of a vertex array object at initialization time to set up the vertex array state. The vertex array state is then set in a single function call at draw time using glBindVertexArray.

Example 6-7 Drawing with a Vertex Array Object


#define VERTEX_POS_SIZE 3 // x, y, and z
#define VERTEX_COLOR_SIZE 4 // r, g, b, and a

#define VERTEX_POS_INDX 0
#define VERTEX_COLOR_INDX 1

#define VERTEX_STRIDE ( sizeof(GLfloat) * \
( VERTEX_POS_SIZE + \
VERTEX_COLOR_SIZE ) )

int Init ( ESContext *esContext )
{
UserData *userData = (UserData*) esContext->userData;
const char vShaderStr[] =
"#version 300 es \n"
"layout(location = 0) in vec4 a_position; \n"
"layout(location = 1) in vec4 a_color; \n"
"out vec4 v_color; \n"
"void main() \n"
"{ \n"
" v_color = a_color; \n"
" gl_Position = a_position; \n"
"}";

const char fShaderStr[] =
"#version 300 es \n"
"precision mediump float; \n"
"in vec4 v_color; \n"
"out vec4 o_fragColor; \n"
"void main() \n"
"{ \n"
" o_fragColor = v_color; \n"
"}" ;

GLuint programObject;

// 3 vertices, with (x, y, z),(r, g, b, a) per-vertex
GLfloat vertices[3 * (VERTEX_POS_SIZE + VERTEX_COLOR_SIZE)] =
{
0.0f, 0.5f, 0.0f, // v0
1.0f, 0.0f, 0.0f, 1.0f, // c0
-0.5f, -0.5f, 0.0f, // v1
0.0f, 1.0f, 0.0f, 1.0f, // c1
0.5f, -0.5f, 0.0f, // v2
0.0f, 0.0f, 1.0f, 1.0f, // c2
};

// Index buffer data
GLushort indices[3] = { 0, 1, 2 };

// Create the program object
programObject = esLoadProgram ( vShaderStr, fShaderStr );

if ( programObject == 0 )
return GL_FALSE;

// Store the program object
userData->programObject = programObject;

// Generate VBO Ids and load the VBOs with data
glGenBuffers ( 2, userData->vboIds );

glBindBuffer ( GL_ARRAY_BUFFER, userData->vboIds[0] );
glBufferData ( GL_ARRAY_BUFFER, sizeof(vertices),
vertices, GL_STATIC_DRAW);
glBindBuffer ( GL_ELEMENT_ARRAY_BUFFER, userData->vboIds[1]);
glBufferData ( GL_ELEMENT_ARRAY_BUFFER, sizeof ( indices ),
indices, GL_STATIC_DRAW );

// Generate VAO ID
glGenVertexArrays ( 1, &userData->vaoId );

// Bind the VAO and then set up the vertex
// attributes
glBindVertexArray ( userData->vaoId );

glBindBuffer(GL_ARRAY_BUFFER, userData->vboIds[0]);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, userData->vboIds[1]);

glEnableVertexAttribArray(VERTEX_POS_INDX);
glEnableVertexAttribArray(VERTEX_COLOR_INDX);

glVertexAttribPointer ( VERTEX_POS_INDX, VERTEX_POS_SIZE,
GL_FLOAT, GL_FALSE, VERTEX_STRIDE, (const void*) 0 );

glVertexAttribPointer ( VERTEX_COLOR_INDX, VERTEX_COLOR_SIZE,
GL_FLOAT, GL_FALSE, VERTEX_STRIDE,
(const void*) ( VERTEX_POS_SIZE * sizeof(GLfloat) ) );

// Reset to the default VAO
glBindVertexArray ( 0 );

glClearColor ( 0.0f, 0.0f, 0.0f, 0.0f );
return GL_TRUE;
}

void Draw ( ESContext *esContext )
{
UserData *userData = (UserData*) esContext->userData;

glViewport ( 0, 0, esContext->width, esContext->height );
glClear ( GL_COLOR_BUFFER_BIT );
glUseProgram ( userData->programObject );

// Bind the VAO
glBindVertexArray ( userData->vaoId );

// Draw with the VAO settings
glDrawElements ( GL_TRIANGLES, 3, GL_UNSIGNED_SHORT,
(const void*) 0 );

// Return to the default VAO
glBindVertexArray ( 0 );
}


When an application is finished with one or more vertex array objects, they can be deleted using glDeleteVertexArrays.

Image

Mapping Buffer Objects

So far, we have shown how to load data into buffer objects using glBufferData or glBufferSubData. It is also possible for applications to map and unmap a buffer object’s data storage into the application’s address space. There are several reasons why an application might prefer to map a buffer rather than load its data using glBufferData or glBufferSubData:

• Mapping the buffer can reduce the memory utilization of the application because potentially only a single copy of the data needs to be stored.

• On architectures with shared memory, mapping the buffer returns a direct pointer into the address space where the buffer will be stored for the GPU. By mapping the buffer, the application can avoid the copy step, thereby realizing better performance on updates.

The glMapBufferRange command returns a pointer to all of or a portion (range) of the data storage for the buffer object. This pointer can be used by the application to read or update the contents of the buffer object. The glUnmapBuffer command is used to indicate that the updates have been completed and to release the mapped pointer.

Image

Image

Image

glMapBufferRange returns a pointer to the buffer data storage range requested. If an error occurs or an invalid request is made, the function will return NULL. The glUnmapBuffer command unmaps a previously mapped buffer.

Image

glUnmapBuffer returns GL_TRUE if the unmap operation is successful. The pointer returned by glMapBufferRange can no longer be used after a successful unmap has been performed. glUnmapBuffer returns GL_FALSE if the data in the vertex buffer object’s data storage have become corrupted after the buffer has been mapped. This can occur due to a change in the screen resolution, multiple screens being used by OpenGL ES context, or an out-of-memory event that causes the mapped memory to be discarded.1

1. If the screen resolution changes to a larger width, height, and bits per pixel at runtime, the mapped memory may have to be released. Note that this is not a very common issue on handheld devices. A backing store is rarely implemented on most handheld and embedded devices. Therefore, an out-of-memory event will result in memory being freed and becoming available for reuse for critical needs.

The code in Example 6-8 demonstrates the use of glMapBufferRange and glUnmapBuffer to write the contents of vertex buffer objects.

Example 6-8 Mapping a Buffer Object for Writing


GLfloat *vtxMappedBuf;
GLushort *idxMappedBuf;

glGenBuffers ( 2, userData->vboIds );

glBindBuffer ( GL_ARRAY_BUFFER, userData->vboIds[0] );
glBufferData ( GL_ARRAY_BUFFER, vtxStride * numVertices,
NULL, GL_STATIC_DRAW );

vtxMappedBuf = (GLfloat*)
glMapBufferRange ( GL_ARRAY_BUFFER, 0,
vtxStride * numVertices,
GL_MAP_WRITE_BIT |
GL_MAP_INVALIDATE_BUFFER_BIT );
if ( vtxMappedBuf == NULL )
{
esLogMessage( "Error mapping vertex buffer object." );
return;
}

// Copy the data into the mapped buffer
memcpy ( vtxMappedBuf, vtxBuf, vtxStride * numVertices );

// Unmap the buffer
if ( glUnmapBuffer( GL_ARRAY_BUFFER ) == GL_FALSE )
{
esLogMessage( "Error unmapping array buffer object." );
return;
}

// Map the index buffer
glBindBuffer ( GL_ELEMENT_ARRAY_BUFFER,
userData->vboIds[1] );
glBufferData ( GL_ELEMENT_ARRAY_BUFFER,
sizeof(GLushort) * numIndices,
NULL, GL_STATIC_DRAW );

idxMappedBuf = (GLushort*)
glMapBufferRange ( GL_ELEMENT_ARRAY_BUFFER, 0,
sizeof(GLushort) * numIndices,
GL_MAP_WRITE_BIT |
GL_MAP_INVALIDATE_BUFFER_BIT );
if ( idxMappedBuf == NULL )
{
esLogMessage( "Error mapping element buffer object." );
return;
}

// Copy the data into the mapped buffer
memcpy ( idxMappedBuf, indices,
sizeof(GLushort) * numIndices );

// Unmap the buffer
if ( glUnmapBuffer( GL_ELEMENT_ARRAY_BUFFER ) == GL_FALSE )
{
esLogMessage( "Error unmapping element buffer object." );
return;
}


Flushing a Mapped Buffer

An application may wish to map a range (or all) of a buffer object using glMapBufferRange, but update only discrete subregions of the mapped range. To avoid the potential performance penalty for flushing the entire mapped range when calling glUnmapBuffer, the application can map with the GL_MAP_FLUSH_EXPLICIT_BIT access flag (along with GL_MAP_WRITE_BIT). When the application has finished updating a portion of the mapped range, it can indicate this fact using glFlushMappedBufferRange.

Image

If an application maps with GL_MAP_FLUSH_EXPLICIT_BIT but does not explicitly flush a modified region with glFlushMappedBufferRange, its contents will be undefined.

Copying Buffer Objects

So far, we have shown how to load buffer objects with data using glBufferData, glBufferSubData, and glMapBufferRange. All of these techniques involve transferring data from the application to the device. It is also possible with OpenGL ES 3.0 to copy data from one buffer object to another entirely on the device. This can be done using the function glCopyBufferSubData.

Image

Calling glCopyBufferSubData will copy the specified bytes from the buffer bound to the readtarget to the writetarget. The buffer binding is determined based on the last call to glBindBuffer for each target. Any type of buffer object (array, element array, transform feedback, and so on) can be bound to the GL_COPY_READ_BUFFER or GL_COPY_WRITE_BUFFER target. These two targets are provided as a convenience so that the application doesn’t have to change any of the true buffer bindings to perform a copy between buffers.

Summary

This chapter explored how vertex attributes and data are specified in OpenGL ES 3.0. Specifically, it covered the following topics:

• How to specify constant vertex attributes using the glVertexAttrib* functions and vertex arrays using the glVertexAttrib[I]Pointer functions

• How to create and store vertex attribute and element data in vertex buffer objects

• How vertex array state is encapsulated in vertex array objects and how to use VAOs to improve performance

• The variety of methods for loading buffer objects with data: glBuffer[Sub]Data, glMapBufferRange, and glCopyBufferSubData

Now that we know how vertex data are specified, the next chapter covers all of the primitives that can be drawn in OpenGL ES using vertex data.