Any OpenGL ES application must submit geometry to the OpenGL ES API to render the final scene. Whenever your data changes, you redraw the scene by submitting the changed vertex information. In all cases, OpenGL must efficiently process these commands to render your images quickly. Every time your application submits geometry, it must be processed and transferred to the hardware to be rendered. This chapter covers many common techniques for managing your vertex data so that it can be processed efficiently by the hardware of iOS-based devices.
If you are familiar with traditional OpenGL on Mac OS X and other platforms, you know that there are many different functions used to submit geometry to OpenGL, each with widely different uses and performance characteristics. As OpenGL matured, so did the techniques for working with vertex data. OpenGL ES dispenses with the older mechanisms in order to provide a simple interface that provides great performance.
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 needed to draw an object can result in a direct reduction in the time it takes to transmit and process vertex information.
You can often reduce the complexity of your geometry by using some of the following techniques:
Provide multiple versions of your geometry at different levels of detail, and choose an appropriate model based on the distance of the object from the camera.
If you are using OpenGL ES 1.1 and added additional vertices to improve lighting details, you can simplify the geometry by using DOT3 lighting. To do this, you’ll create a bump map texture for your object to hold normal information, and use the texture unit to apply lighting using a texture combine operation with the GL_DOT3_RGB
mode.
If your application is written to use OpenGL ES 2.0 shaders, you can easily move lighting and other per-vertex calculations into the fragment shader. Although processing these calculations for every fragment can be more expensive, it can result in a higher quality image. Moving calculations into the fragment shader should be used when the data changes frequently.
If your geometry uses data that remains constant across an object, you should not duplicate that data inside your vertex format. Instead, you should disable that array entirely and use a per-vertex attribute state call such as glColor4ub
or glTexCoord2F
instead. OpenGL ES 2.0 applications can either set a constant vertex attribute or use a uniform shader value to hold information that is constant across an object and changes infrequently.
In OpenGL ES, you enable each attribute your application needs and provide a pointer to an array of that data type. OpenGL ES allows you to specify a stride, which offers your application the flexibility of providing multiple arrays (also known as a struct of arrays) or a single array with a single vertex format (an array of structs).
Your application should use an array of structs with a single interleaved vertex format. Interleaved data provides better memory locality than using a separate array for each attribute.
You may want to separate out attribute data that is updated at a frequency different from the rest of your vertex data (as described in “Vertex Buffer Usage”). Similarly, if attribute data can be shared between two or more pieces of geometry, separating it out may reduce your memory usage.
Memory bandwidth is limited on iOS-based devices, so reducing the size of your vertex data can provide a significant boost to performance. When specifying the size of each of your components, you should use the smallest type that gives you acceptable results. Specify vertex colors using 4 unsigned byte values. Specify texture coordinates with 2 or 4 unsigned byte or short values, instead of floating-point values.
Avoid the use of the OpenGL ES GL_FIXED
data type. It uses the same amount of bandwidth as GL_FLOAT
, but provides a smaller range of values and requires additional processing.
If you specify smaller components, be sure you reorder your vertex format to avoid any misalignment penalties. See “Align Vertex Structures.”
Using triangle strips significantly reduces the number of vertex calculations that OpenGL ES must perform on your geometry. Your performance is best if an object (or even a group of objects) can be submitted in a single unindexed triangle strip using glDrawArrays
. This may involve adding degenerate triangles to merge multiple smaller triangle strips into a single large strip.
If your geometry duplicates a large number of vertices (because vertices are shared by many triangles or because a large number of duplicate vertices were added to merge multiple triangle strips), you may obtain better performance by submitting an indexed triangle strip using glDrawElements
. For performance, your application should sort the drawing order so that triangles that share the same vertex are drawn reasonably close to one another in the strip. Graphics hardware often caches recent vertex calculations, so submitting all the triangles that use the same vertex can allow the hardware to used the cached calculations, rather than repeating vertex calculations.
For best results, you should test your geometry using both indexed and unindexed triangle strips, and use the one that performs the fastest.
By default, an application maintains its own vertex data and submits it to the hardware to be rendered. When you submit geometry, it is copied to the hardware to be rendered. Listing 4-1 shows the code a simple application would use to provide position and color information to OpenGL ES 1.1.
Listing 4-1 Submitting vertex data to OpenGL ES 1.1.
typedef struct _vertexStruct |
{ |
GLfloat position[2]; |
GLubyte color[4]; |
} vertexStruct; |
void DrawGeometry() |
{ |
const vertexStruct vertices[] = {...}; |
const GLubyte indices[] = {...}; |
glEnableClientState(GL_VERTEX_ARRAY); |
glVertexPointer(2, GL_FLOAT, sizeof(vertexStruct), &vertices[0].position); |
glEnableClientState(GL_COLOR_ARRAY); |
glColorPointer(4, GL_UNSIGNED_BYTE, sizeof(vertexStruct), &vertices[0].color); |
glDrawElements(GL_TRIANGLE_STRIP, sizeof(indices)/sizeof(GLubyte), GL_UNSIGNED_BYTE, indices); |
} |
This code works, but is inefficient. Each time glDrawElements
is called, the data is retransmitted to the graphics hardware to be rendered. If the data did not change, those additional copies are unnecessary. To avoid this, your application should store its geometry in a vertex buffer object (VBO). Data stored in a vertex buffer object is owned by OpenGL ES and may be cached by the hardware or driver to improve performance.
Listing 4-2 modifies the previous example to create vertex buffer objects to store the vertices and indices. In this example, the buffers are created when the application is initialized. Listing 4-3 shows how to use the vertex buffers to submit the geometry for rendering.
Listing 4-2 Creating vertex buffers in OpenGL ES 1.1
typedef struct _vertexStruct |
{ |
GLfloat position[2]; |
GLubyte color[4]; |
} vertexStruct; |
const vertexStruct vertices[] = {...}; |
const GLubyte indices[] = {...}; |
GLuint vertexBuffer; |
GLuint indexBuffer; |
void CreateVertexBuffers() |
{ |
glGenBuffers(1, &vertexBuffer); |
glGenBuffers(1, &indexBuffer); |
glBindBuffer(GL_ARRAY_BUFFER, vertexBuffer); |
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW); |
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, indexBuffer); |
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW); |
} |
Listing 4-3 Drawing using Vertex Buffers in OpenGL ES 1.1
void DrawUsingVertexBuffers() |
{ |
glBindBuffer(GL_ARRAY_BUFFER, vertexBuffer); |
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, indexBuffer); |
glEnableClientState(GL_VERTEX_ARRAY); |
glVertexPointer(2, GL_FLOAT, sizeof(vertexStruct), (void*)offsetof(vertexStruct,position)); |
glEnableClientState(GL_COLOR_ARRAY); |
glColorPointer(4, GL_UNSIGNED_BYTE, sizeof(vertexStruct), (void*)offsetof(vertexStruct,color)); |
glDrawElements(GL_TRIANGLE_STRIP, sizeof(indices)/sizeof(GLubyte), GL_UNSIGNED_BYTE, (void*)0); |
} |
The key difference in this code is that glVertexPointer
and glColorPointer
no longer directly point to the vertex data. Instead, the code creates a vertex buffer object and copies the vertex data into it by calling glBufferData
. The functions glVertexPointer
and glColorPointer
are called with offsets into the vertex buffer object instead. This code also creates a buffer to hold the index information. Because this buffer is bound when you call glDrawElements
, it is used as the source for indices.
Another big advantage of vertex buffers is that the application can hint how it uses the data in each vertex buffer. For example, in Listing 4-2, the code informed OpenGL ES that the contents of both buffers were not expected to change (GL_STATIC_DRAW
). The usage parameter allows OpenGL ES to alter its strategy for processing different types of vertex data to improve performance.
OpenGL ES supports the following usage cases:
GL_STATIC_DRAW
should be used for vertex data that is specified once and never changed. Your application should create these vertex buffers during initialization and use them repeatedly until your application shuts down.
GL_DYNAMIC_DRAW
should be used when data are expected to change after they are created. Your application should still allocate these buffers during initialization and periodically update them by calling glBufferSubData
.
OpenGL ES 2.0 offers an additional option:
GL_STREAM_DRAW
is 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 change vertex data every frame in a way that cannot be performed in a vertex shader. To use a stream vertex buffer, your application initially fills the buffer using glBufferData
, then alternates between drawing geometry from the buffer and altering the content by calling glBufferSubData
.
If different data in your vertex format has different usage characteristics, you may want to split the vertex data into one structure for each usage case, and allocate a vertex buffer for each. Listing 4-4 modifies the previous example to hint that the color data may change.
Listing 4-4 Geometry with various usage patterns
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() |
{ |
glGenBuffers(1, &staticBuffer); |
glGenBuffers(1, &dynamicBuffer); |
glGenBuffers(1, &indexBuffer); |
// Static position data |
glBindBuffer(GL_ARRAY_BUFFER, staticBuffer); |
glBufferData(GL_ARRAY_BUFFER, sizeof(staticVertexData), staticVertexData, GL_STATIC_DRAW); |
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, indexBuffer); |
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW); |
// Dynamic color data |
// While not shown here, the expectation is that the data in this buffer changes between frames. |
glBindBuffer(GL_ARRAY_BUFFER, dynamicBuffer); |
glBufferData(GL_ARRAY_BUFFER, sizeof(dynamicVertexData), dynamicVertexData, GL_DYNAMIC_DRAW); |
} |
void DrawUsingVertexBuffers() |
{ |
glBindBuffer(GL_ARRAY_BUFFER, staticBuffer); |
glEnableClientState(GL_VERTEX_ARRAY); |
glVertexPointer(2, GL_FLOAT, sizeof(vertexStatic), (void*)offsetof(vertexStatic,position)); |
glBindBuffer(GL_ARRAY_BUFFER, dynamicBuffer); |
glEnableClientState(GL_COLOR_ARRAY); |
glColorPointer(4, GL_UNSIGNED_BYTE, sizeof(vertexDynamic), (void*)offsetof(vertexDynamic,color)); |
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, indexBuffer); |
glDrawElements(GL_TRIANGLE_STRIP, sizeof(indices)/sizeof(GLubyte), GL_UNSIGNED_BYTE, (void*)0); |
} |
iOS 4 adds the OES_vertex_array_object
extension on all platforms. Vertex array objects provide two key optimizations. First, the call overhead to set series of attribute pointers can be reduced to a single function call inside the rendering loop. Second, because the attributes associated with a vertex array object are in a known and well-defined state, OpenGL ES can cache the layout of the vertex data and process the vertex data more quickly.
When you design your vertex formats, you should make sure that all components are properly aligned to their native alignment. 32-bit values such as GL_FLOAT
should be aligned on a 32-bit boundary. 16-bit values should be aligned on a 16-bit boundary. Unaligned data requires significantly more processing, particularly when your application uses vertex buffers.
Last updated: 2010-07-09