iOS Reference Library Apple Developer
Search

Best Practices for Working with Vertex Data

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.

Simplify Your Geometry

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:

Avoid Storing Constants in Arrays

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.

Use Interleaved Vertex Data

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.

Use the Smallest Acceptable Types for Attributes.

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.”

Use Triangle Strips to Batch Geometry

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.

Use Vertex Buffers

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.

Vertex Buffer Usage

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:

OpenGL ES 2.0 offers an additional option:

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);
}

Consolidate Vertex Array State Changes Using Vertex Array Objects

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.

Align Vertex Structures

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

Did this document help you? Yes It's good, but... Not helpful...