Best Practices for Working with Vertex Data
To render a frame using OpenGL ES your application configures the graphics pipeline and submits graphics primitives to be drawn. In some applications, all primitives are drawn using the same pipeline configuration; other applications may render different elements of the frame using different techniques. But no matter which primitives you use in your application or how the pipeline is configured your application provides vertices to OpenGL ES. This chapter provides a refresher on vertex data and follows it with targeted advice for how to efficiently process vertex data.
A vertex consists of one or more attributes, such as the position, the color, the normal, or texture coordinates. An OpenGL ES 2.0 application is free to define its own attributes; each attribute in the vertex data corresponds to an attribute variable that acts as an input to the vertex shader. An OpenGL 1.1 application uses attributes defined by the fixed-function pipeline.
Your application defines an attribute as a vector consisting of one to four components. All components in the attribute share a common data type. For example, a color might be defined as four GLbyte components (alpha, red, green, blue). When an attribute is loaded into a shader variable, any components that are not provided in the application data are filled in with default values by OpenGL ES. The last component is filled with 1, while other unspecified components are filled with 0.

Your application may configure an attribute to be a constant, which means the same values are used for all vertices submitted as part of a draw command, or an array, which means that each vertex a value for that attribute. When your application calls a function in OpenGL ES to draw a set of vertices, the vertex data is copied from your application to the graphics hardware. The graphics hardware than acts on the vertex data, processing each vertex in the shader, assembling primitives and rasterizing them out into the framebuffer. One advantage of OpenGL ES is that it standardizes on a single set of functions to submit vertex data to OpenGL ES, removing older and less efficient mechanisms that were provided by OpenGL.
Applications that must submit a large number of primitives to render a frame need to carefully manage their vertex data and how they provide it to OpenGL ES. The practices described in this chapter can be summarized in a few basic principles:
Reduce the size of your vertex data.
Reduce the pre-processing that must occur before OpenGL ES can transfer the vertex data to the graphics hardware.
Reduce the time spent copying vertex data to the graphics hardware.
Reduce computations performed for each vertex.
Simplify Your Models
The graphics hardware of iOS-based devices is very powerful, but the images it displays are often very small. You don’t need extremely complex models to present compelling graphics on iOS. Reducing the number of vertices used to draw a model directly reduces the size of the vertex data and the calculations performed on your vertex data.
You can reduce the complexity of a model by using some of the following techniques:
Provide multiple versions of your model at different levels of detail, and choose an appropriate model at runtime based on the distance of the object from the camera and the dimensions of the display.
Use textures to eliminate the need for some vertex information. For example, a bump map can be used to add detail to a model without adding more vertex data.
Some models add vertices to improve lighting details or rendering quality. This is usually done when values are calculated for each vertex and interpolated across the triangle during the rasterization stage. For example, if you directed a spotlight at the center of a triangle, its effect might go unnoticed because the brightest part of the spotlight is not directed at a vertex. By adding vertices, you provide additional interpolant points, at the cost of increasing the size of your vertex data and the calculations performed on the model. Instead of adding additional vertices, consider moving calculations into the fragment stage of the pipeline instead:
If your application uses OpenGL ES 2.0, then your application performs the calculation in the vertex shader and assigns it to a varying variable. The varying value is interpolated by the graphics hardware and passed to the fragment shader as an input. Instead, assign the calculation’s inputs to varying variables and perform the calculation in the fragment shader. Doing this changes the cost of performing that calculation from a per-vertex cost to a per-fragment cost, reduces pressure on the vertex stage and more pressure on the fragment stage of the pipeline. Do this when your application is blocked on vertex processing, the calculation is inexpensive and the vertex count can be significantly reduced by the change.
If your application uses OpenGL ES 1.1, you can perform per-fragment lighting using DOT3 lighting. You do this by adding a bump map texture to hold normal information and applying the bump map using a texture combine operation with the
GL_DOT3_RGBmode.
Avoid Storing Constants in Attribute Arrays
If your models include attributes that uses data that remains constant across the entire model, do not duplicate that data for each vertex. OpenGL ES 2.0 applications can either set a constant vertex attribute or use a uniform shader value to hold the value instead. OpenGL ES 1.1 application should use a per-vertex attribute function such as glColor4ub or glTexCoord2F instead.
Use the Smallest Acceptable Types for Attributes.
When specifying the size of each of your attribute’s components, choose the smallest data type that provides acceptable results. Here are some guidelines:
Specify vertex colors using four unsigned byte components (
GL_UNSIGNED_BYTE).Specify texture coordinates using 2 or 4 unsigned bytes (
GL_UNSIGNED_BYTE) or unsigned short (GL_UNSIGNED_SHORT). Do not pack multiple sets of texture coordinates into a single attribute.Avoid using the OpenGL ES
GL_FIXEDdata type. It requires the same amount of memory asGL_FLOAT, but provides a smaller range of values. All iOS devices support hardware floating-point units, so floating point values can be processed more quickly.
If you specify smaller components, be sure you reorder your vertex format to avoid misaligning your vertex data. See “Avoid Misaligned Vertex Data.”
Use Interleaved Vertex Data
OpenGL ES allows your application to either specify vertex data as a series of arrays (also known as a struct of arrays) or as an array where each element includes multiple attributes (an array of structs). The preferred format on iOS is an array of structs with a single interleaved vertex format. Interleaved data provides better memory locality for each vertex.

An exception to this rule is when your application needs to update some vertex data at a rate different from the rest of the vertex data, or if some data can be shared between two or more models. In either case, you may want to separate the attribute data into two or more structures.

Avoid Misaligned Vertex Data
When you are designing your vertex structure, align the beginning of each attribute to an offset that is either a multiple of its component size or 4 bytes, whichever is larger. When an attribute is misaligned, iOS must perform additional processing before passing the data to the graphics hardware.
In Figure 9-3, the position and normal data are each defined as three short integers, for a total of six bytes. The normal data begins at offset 6, which is a multiple of the native size (2 bytes), but is not a multiple of 4 bytes. If this vertex data were submitted to iOS, iOS would have to take additional time to copy and align the data before passing it to the hardware. To fix this, explicitly add two bytes of padding after each attribute.

Use Triangle Strips to Batch Vertex Data
Using triangle strips significantly reduces the number of vertex calculations that OpenGL ES must perform on your models. On the left side of Figure 9-4, three triangles are specified using a total of nine vertices. C, E and G actually specify the same vertex! By specifying the data as a triangle strip, you can reduce the number of vertices from nine to five.

Sometimes, your application can combine more than one triangle strip into a single larger triangle strip. All of the strips must share the same rendering requirements. This means:
An OpenGL ES 2.0 application must use the same shader to draw all of the triangle strips.
An OpenGL ES 1.1 application must be able to render all of the triangle strips without changing any OpenGL state.
The triangle strips must share the same vertex attributes.
To merge two triangle strips, duplicate the last vertex of the first strip and the first vertex of the second strip, as shown in Figure 9-5. When this strip is submitted to OpenGL ES, triangles DEE, EEF, EFF, and FFG are considered degenerate and not processed or rasterized.

For best performance, your models should be submitted as a single unindexed triangle strip using glDrawArrays with as few duplicated vertices as possible. If your models require many vertices to be duplicated (because many vertices are shared by triangles that do not appear sequentially in the triangle strip or because your application merged many smaller triangle strips), you may obtain better performance using a separate index buffer and calling glDrawElements instead. There is a trade off: an unindexed triangle strip must periodically duplicate entire vertices, while an indexed triangle list requires additional memory for the indices and adds overhead to look up vertices. For best results, test your models using both indexed and unindexed triangle strips, and use the one that performs the fastest.
Where possible, sort vertex and index data so triangles that share common vertices are drawn reasonably close to each other in the triangle strip. Graphics hardware often caches recent vertex calculations, so locality of reference may allow the hardware to avoid calculating a vertex multiple times.
Use Vertex Buffer Objects to Manage Copying Vertex Data
Listing 9-1 provides a function that a simple application might use to provide position and color data to the vertex shader. It enables two attributes and configures each to point at the interleaved vertex structure. Finally, it calls the glDrawElements function to render the model as a single triangle strip.
Listing 9-1 Submitting vertex data to OpenGL ES 2.0
typedef struct _vertexStruct |
{ |
GLfloat position[2]; |
GLubyte color[4]; |
} vertexStruct; |
enum { |
ATTRIB_POSITION, |
ATTRIB_COLOR, |
NUM_ATTRIBUTES }; |
void DrawModel() |
{ |
const vertexStruct vertices[] = {...}; |
const GLubyte indices[] = {...}; |
glVertexAttribPointer(ATTRIB_POSITION, 2, GL_FLOAT, GL_FALSE, sizeof(vertexStruct), &vertices[0].position); |
glEnableVertexAttribArray(ATTRIB_POSITION); |
glVertexAttribPointer(ATTRIB_COLOR, 4, GL_UNSIGNED_BYTE, GL_TRUE, sizeof(vertexStruct), &vertices[0].color); |
glEnableVertexAttribArray(ATTRIB_COLOR); |
glDrawElements(GL_TRIANGLE_STRIP, sizeof(indices)/sizeof(GLubyte), GL_UNSIGNED_BYTE, indices); |
} |
This code works, but is inefficient. Each time DrawModel is called, the index and vertex data are copied to OpenGL ES, and transferred to the graphics hardware. If the vertex data does not change between invocations, these unnecessary copies can impact performance. To avoid unnecessary copies, your application should store its vertex data in a vertex buffer object (VBO). Because OpenGL ES owns the vertex buffer object’s memory, it can store the buffer in memory that is more accessible to the graphics hardware, or pre-process the data into the preferred format for the graphics hardware.
Listing 9-2 creates a pair of vertex buffer objects, one to hold the vertex data and the second for the strip’s indices. In each case, the code generates a new object, binds it to be the current buffer, and fills the buffer. CreateVertexBuffers would be called when the application is initialized.
Listing 9-2 Creating vertex buffer objects
GLuint vertexBuffer; |
GLuint indexBuffer; |
void CreateVertexBuffers() |
{ |
glGenBuffers(1, &vertexBuffer); |
glBindBuffer(GL_ARRAY_BUFFER, vertexBuffer); |
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW); |
glGenBuffers(1, &indexBuffer); |
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, indexBuffer); |
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW); |
} |
Listing 9-3 modifies Listing 9-1 to use the vertex buffer objects. The key difference in Listing 9-3 is that the parameters to the glVertexPointer and glColorPointer functions no longer point to the vertex arrays. Instead, each is an offset into the vertex buffer object.
Listing 9-3 Drawing using Vertex Buffer Objects in OpenGL ES 2.0
void DrawModelUsingVertexBuffers() |
{ |
glBindBuffer(GL_ARRAY_BUFFER, vertexBuffer); |
glVertexAttribPointer(ATTRIB_POSITION, 2, GL_FLOAT, GL_FALSE, sizeof(vertexStruct), (void*)offsetof(vertexStruct,position)); |
glEnableVertexAttribArray(ATTRIB_POSITION); |
glVertexAttribPointer(ATTRIB_COLOR, 4, GL_UNSIGNED_BYTE, GL_TRUE, sizeof(vertexStruct), (void*)offsetof(vertexStruct,color); |
glEnableVertexAttribArray(ATTRIB_COLOR); |
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, indexBuffer); |
glDrawElements(GL_TRIANGLE_STRIP, sizeof(indices)/sizeof(GLubyte), GL_UNSIGNED_BYTE, (void*)0); |
} |
Buffer Usage Hints
The previous example assumed that the vertex data is initialized once, and never needs to change during rendering. However, OpenGL ES allows you to change the data in a buffer at runtime. A key part of the design of vertex buffer objects is that the application can inform OpenGL ES how it uses the data stored in the buffer. The usage parameter allows OpenGL ES to choose different strategies for storing the buffer depending on how your application intends to use the data. In Listing 9-2, each call to the glBufferData function provides a usage hint as the last parameter. Passing GL_STATIC_DRAW into glBufferData tells OpenGL ES that the contents of both buffers are never expected to change, which gives OpenGL ES more opportunities to optimize how and where the data is stored.
OpenGL ES supports the following usage cases:
GL_STATIC_DRAWshould be used for vertex data that is specified once and never changes. Your application should create these vertex buffer objects during initialization and use them until they are no longer needed.GL_DYNAMIC_DRAWshould be used when data stored in the buffer may change during the rendering loop. Your application should still initialize these buffers during initialization, and update the data as needed by calling theglBufferSubDatafunction.GL_STREAM_DRAWis used when your application needs to create transient geometry that is rendered a small number of times and then discarded. This is most useful when your application must dynamically calculate new vertex data every frame and you perform the calculations inside the shader. Typically, if your application is using stream drawing, you want to create two different buffers; while OpenGL ES is drawing using the contents of one buffer, your application is filling the other. See “Use Double Buffering to Avoid Resource Conflicts.” Stream drawing is not available in OpenGL ES 1.1; useGL_DYNAMIC_DRAWinstead.
If different attributes inside your vertex format require different usage patterns, split the vertex data into multiple structures, and allocate a separate vertex buffer object for each collection of attributes that share common usage characteristics. Listing 9-4 modifies the previous example to use a separate buffer to hold the color data. By allocating the color buffer using the GL_DYNAMIC_DRAW hint, OpenGL ES can allocate that buffer so that your application maintains reasonable performance.
Listing 9-4 Drawing a model with multiple vertex buffer objects
typedef struct _vertexStatic |
{ |
GLfloat position[2]; |
} vertexStatic; |
typedef struct _vertexDynamic |
{ |
GLubyte color[4]; |
} vertexDynamic; |
// Separate buffers for static and dynamic data. |
GLuint staticBuffer; |
GLuint dynamicBuffer; |
GLuint indexBuffer; |
const vertexStatic staticVertexData[] = {...}; |
vertexDynamic dynamicVertexData[] = {...}; |
const GLubyte indices[] = {...}; |
void CreateBuffers() |
{ |
// Static position data |
glGenBuffers(1, &staticBuffer); |
glBindBuffer(GL_ARRAY_BUFFER, staticBuffer); |
glBufferData(GL_ARRAY_BUFFER, sizeof(staticVertexData), staticVertexData, GL_STATIC_DRAW); |
// Dynamic color data |
// While not shown here, the expectation is that the data in this buffer changes between frames. |
glGenBuffers(1, &dynamicBuffer); |
glBindBuffer(GL_ARRAY_BUFFER, dynamicBuffer); |
glBufferData(GL_ARRAY_BUFFER, sizeof(dynamicVertexData), dynamicVertexData, GL_DYNAMIC_DRAW); |
// Static index data |
glGenBuffers(1, &indexBuffer); |
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, indexBuffer); |
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW); |
} |
void DrawModelUsingMultipleVertexBuffers() |
{ |
glBindBuffer(GL_ARRAY_BUFFER, staticBuffer); |
glVertexAttribPointer(ATTRIB_POSITION, 2, GL_FLOAT, GL_FALSE, sizeof(vertexStruct), (void*)offsetof(vertexStruct,position)); |
glEnableVertexAttribArray(ATTRIB_POSITION); |
glBindBuffer(GL_ARRAY_BUFFER, dynamicBuffer); |
glVertexAttribPointer(ATTRIB_COLOR, 4, GL_UNSIGNED_BYTE, GL_TRUE, sizeof(vertexStruct), (void*)offsetof(vertexStruct,color); |
glEnableVertexAttribArray(ATTRIB_COLOR); |
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, indexBuffer); |
glDrawElements(GL_TRIANGLE_STRIP, sizeof(indices)/sizeof(GLubyte), GL_UNSIGNED_BYTE, (void*)0); |
} |
Consolidate Vertex Array State Changes Using Vertex Array Objects
Take a closer look at the DrawModelUsingMultipleVertexBuffers function in Listing 9-4. It enables many attributes, binds multiple vertex buffer objects, and configures attributes to point into the buffers. All of that initialization code is essentially static; none of the parameters change from frame to frame. If this function is called every time the application renders a frame, there’s a lot of unnecessary overhead reconfiguring the graphics pipeline. If the application draws many different kinds of models, reconfiguring the pipeline may become a real bottleneck. To avoid this, use a vertex array object to store a complete attribute configuration. The OES_vertex_array_object extension is available on all iOS devices starting in iOS 4.0.
Figure 9-6 shows an example configuration with two vertex array objects. Each configuration is independent of the other; each points to a different number of vertices and even into different vertex buffer objects.

Listing 9-5 provides the code used to configure first vertex array object shown above. It generates an identifier for the new vertex array object and then binds the vertex array object to the context. After this, it makes the same calls to configure vertex attributes as it would if the code were not using vertex array objects. The configuration is stored to the bound vertex array object instead of to the context.
Listing 9-5 Configuring a vertex array object
void ConfigureVertexArrayObject() |
{ |
// Create and bind the vertex array object. |
glGenVertexArraysOES(1,&vao1); |
glBindVertexArrayOES(vao1); |
// Configure the attributes in the VAO. |
glBindBuffer(GL_ARRAY_BUFFER, vbo1); |
glVertexAttribPointer(ATT_POSITION, 3, GL_FLOAT, GL_FALSE, sizeof(staticFmt), (void*)offsetof(staticFmt,position)); |
glEnableVertexAttribArray(ATT_POSITION); |
glVertexAttribPointer(ATT_TEXCOORD, 2, GL_UNSIGNED_SHORT, GL_TRUE, sizeof(staticFmt), (void*)offsetof(staticFmt,texcoord)); |
glEnableVertexAttribArray(ATT_TEXCOORD); |
glVertexAttribPointer(ATT_NORMAL, 3, GL_FLOAT, GL_FALSE, sizeof(staticFmt), (void*)offsetof(staticFmt,normal)); |
glEnableVertexAttribArray(ATT_NORMAL); |
glBindBuffer(GL_ARRAY_BUFFER, vbo2); |
glVertexAttribPointer(ATT_COLOR, 4, GL_UNSIGNED_BYTE, GL_TRUE, sizeof(dynamicFmt), (void*)offsetof(dynamicFmt,color)); |
glEnableVertexAttribArray(ATT_COLOR); |
// Bind back to the default state. |
glBindBuffer(GL_ARRAY_BUFFER,0); |
glBindVertexArrayOES(0); } |
To draw, the code binds the vertex array object and then submits drawing commands as before.
For best performance, your application should configure each vertex array object once, and never change it at runtime. If you need to change a vertex array object in every frame, create multiple vertex array objects instead. For example, an application that uses double buffering might configure one set of vertex array objects for odd-numbered frames, and a second set for even numbered frames. Each set of vertex array objects would point at the vertex buffer objects used to render that frame. When a vertex array object’s configuration does not change, OpenGL ES can cache information about the vertex format and improve how it processes those vertex attributes.
© 2013 Apple Inc. All Rights Reserved. (Last updated: 2013-04-23)