Home > Articles > Programming > Graphic Programming

  • Print
  • + Share This
This chapter is from the book

This chapter is from the book

2.2 Specifying Vertex Data

OpenGL allows the application to specify primitives in several ways. OpenGL® Distilled briefly covers the glBegin ()/ glEnd () paradigm for illustrative purposes only. You should avoid using glBegin ()/ glEnd () because of its inherent call overhead, which inhibits application performance. Instead, use buffer objects and vertex arrays, presented later in this chapter. Vertex arrays dramatically reduce function call overhead compared with glBegin ()/ glEnd () and also allow vertex sharing. Using buffer objects causes OpenGL to store vertex data in high-performance server memory, which allows your application to avoid expensive data copies at render time.

2.2.1 Drawing Primitives Using glBegin()/glEnd()

OpenGL version 1.0 features a flexible interface for primitive rendering called the glBegin ()/ glEnd () paradigm. Contemporary OpenGL features more efficient rendering mechanisms, which are the focus of this chapter. Because OpenGL is backward compatible with older versions, however, many applications still use the glBegin ()/ glEnd () paradigm in spite of its inherent performance issues. This chapter covers glBegin ()/ glEnd () briefly because it illustrates the OpenGL concept of per-vertex state.

Applications render primitives by surrounding vertices with a pair of functions, glBegin () and glEnd (). Applications specify the primitive type by passing it as a parameter to glBegin ().


void glBegin( GLenum mode );
void glEnd( void );

glBegin () and glEnd () surround commands that specify vertices and vertex data. mode specifies the primitive type to draw and must be one of GL_POINTS, GL_LINES, GL_LINE_STRIP, GL_LINE_LOOP, GL_TRIANGLES, GL_TRIANGLE_STRIP, GL_TRIANGLE_FAN, GL_QUADS, GL_QUAD_STRIP, or GL_POLYGON.

U25B8.GIF OpenGL version: 1.0 and later.

Between glBegin () and glEnd (), applications specify vertices and vertex states such as the current primary color, current normal and material properties (for lighting), and current texture coordinates (for texture mapping). [1] Applications specify colors, normals, material properties, texture coordinates, and vertices each with individual function calls. OpenGL® Distilled doesn't cover these function calls in detail, but their names are self-explanatory, as you'll see in the following examples.

The glBegin ()/ glEnd () paradigm serves as an excellent illustration of OpenGL per-vertex state. The following code, for example, demonstrates how to draw a red triangle:

glBegin( GL_TRIANGLES );
  glColor3f( 1.f, 0.f, 0.f ); // Sets current primary color to red
  glVertex3f( 0.f, 0.f, 0.f ); // Specify three vertices
  glVertex3f( 1.f, 0.f, 0.f );
  glVertex3f( 0.f, 1.f, 0.f );
glEnd();

Recall that OpenGL is a state machine. The glColor3f () command sets the current primary color state using the RGBA value (1.0, 0.0, 0.0, 1.0) for red (the alpha value 1.0 is implicit in the glColor3f () command). As OpenGL receives glVertex3f () commands, it copies the current primary color state into the state associated with each vertex.

An application can set different states at each vertex. Consider the following example, which draws a triangle with red, green, and blue vertices:

glBegin( GL_TRIANGLES );
  glColor3f( 1.f, 0.f, 0.f );
  glVertex3f( 0.f, 0.f, 0.f );
  glColor3f( 0.f, 1.f, 0.f );
  glVertex3f( 1.f, 0.f, 0.f );
  glColor3f( 0.f, 0.f, 1.f );
  glVertex3f( 0.f, 1.f, 0.f );
glEnd();

In this case, the current primary color state is different at the time OpenGL receives each glVertex3f () command. As a result, the state associated with the first vertex has red stored in its primary color, with green and blue stored in the state for the subsequent vertices.

OpenGL doesn't limit the number of vertices an application can specify between glBegin () and glEnd (). If your application calls glBegin ( GL_QUADS ), followed by four million vertices and glEnd (), for example, OpenGL renders 1 million individual quadrilaterals from your vertex data.

Individual function calls to specify vertices and vertex states provide great flexibility for the application developer. The function call overhead inherent in this interface, however, dramatically limits application performance. Even though display lists (see "Performance Issues" later in this chapter) allow implementations to optimize data sent using the glBegin ()/ glEnd () paradigm, many OpenGL vendors expect that developers will simply use mechanisms that are inherently more efficient and easier to optimize, such as buffer objects and vertex arrays. For this reason, avoid using the glBegin ()/ glEnd () paradigm.

Regardless, many applications use glBegin ()/ glEnd () extensively, possibly for historical reasons. Before OpenGL version 1.1 was released, the glBegin ()/ glEnd () paradigm was the only option available. Performance may be another reason; glBegin ()/ glEnd () is perfectly adequate for applications that have extremely light rendering requirements or are not performance critical.

2.2.2 Drawing Primitives Using Vertex Arrays

Vertex arrays allow applications to render primitives from vertex and vertex state data stored in blocks of memory. Under some circumstances, OpenGL implementations can process the vertices and cache the results for efficient reuse. Applications specify primitives by indexing into the vertex array data.

Vertex arrays, a common OpenGL extension in version 1.0, became part of the OpenGL core in version 1.1, with additional feature enhancements through version 1.4. Before version 1.5, applications using vertex arrays could store data only in client storage. Version 1.5 introduced the buffer object feature, allowing applications to store data in high-performance server memory.

This book focuses primarily on using buffer objects. The section "Vertex Array Data" later in this chapter, however, shows how to specify blocks of data with and without buffer objects. The example source code uses buffer objects when the OpenGL runtime version is 1.5 or later and uses the pre-1.5 interface otherwise.

2.2.2.1 Buffer Objects

Applications use buffer objects to store vertex data [2] in server memory.

Each buffer object requires a unique identifier. Obtain a buffer object identifier with glGenBuffers ().


void glGenBuffers( GLsizei n, GLuint* buffers );
GLboolean glIsBuffer( GLuint buffer );

glGenBuffers () obtains n buffer identifiers from its pool of unused buffers. n is the number of buffer identifiers desired by the application. glGenBuffers () stores the identifiers in buffers.

glIsBuffer () returns GL_TRUE if buffer is an existing buffer object.

U25B8.GIF OpenGL version: 1.5 and later.

You might need to create four buffer objects to store vertex data, normal data, texture coordinate data, and indices in vertex arrays. The following code obtains identifiers for four buffer objects:

GLuint bufObjects[4];
glGenBuffers( 4, bufObjects );

Before you can store data in the buffer object, your application must bind it using glBindBuffer ().


void glBindBuffer( GLenum target, GLuint buffer );

Specifies the active buffer object and initializes it if it's new. Pass a target value of GL_ARRAY_BUFFER to use the buffer object as vertex array data, and pass a target value of GL_ELEMENT_ARRAY_BUFFER to use the buffer object as vertex array indices. buffer is the identifier of the buffer object to bind.

U25B8.GIF OpenGL version: 1.5 and later.

The command glBindBuffer ( GL_ARRAY_BUFFER, bufObjects[0] ) binds the first buffer object ID obtained in the previous code listing, bufObjects[0], for use with vertex array data. If a buffer object is already bound, glBindBuffer () unbinds it and then binds buffer.

To unbind a buffer object, call glBindBuffer () with a buffer ID of zero. After glBindBuffer ( GL_ARRAY_BUFFER, 0 ) is called, for example, no buffer is associated with vertex array data.

When an application binds a buffer object for the first time, OpenGL creates an empty buffer object. To load the buffer object with data, call glBufferData ().


void glBufferData( GLenum target, GLsizeiptr size, const GLvoid* data,
  GLenum usage );

Copies data from host memory to the active buffer object. target must be GL_ARRAY_BUFFER or GL_ELEMENT_ARRAY_BUFFER; size indicates the size of the data in bytes; and data points to the data.

usage is a hint to OpenGL, stating the application's intended usage of the buffer data. A usage parameter of GL_STATIC_DRAW indicates that the application intends to specify the data once and use it to draw several times. Other values for usage are described in Section 2.9, "Buffer Objects," of The OpenGL Graphics System.

U25B8.GIF OpenGL version: 1.5 and later.

OpenGL doesn't limit the amount of data you can store in a buffer object. Some implementations, however, can provide maximum performance only if buffer object data is below an implementation-specific size and must fall back to a slower rendering path when buffer objects are too large. Currently, OpenGL doesn't provide a mechanism to query this implementation-specific limit. OpenGL vendors often make this type of information available to developers in implementation-specific documentation, however.

Applications typically create buffer objects and load them with data at initialization time. You might create a single buffer object and load it with three vertices by using the following code:

// Obtain a buffer identifier from OpenGL
GLuint bufferID;
glGenBuffers( 1, &bufferID );

// Define three vertices to draw a right triangle.
const GLfloat vertices[] = {
    0.f, 0.f, 0.f,
    1.f, 0.f, 0.f,
    0.f, 1.f, 0.f };

// Bind the buffer object, OpenGL initially creates it empty.
glBindBuffer( GL_ARRAY_BUFFER, bufferID );
// Tell OpenGL to copy data from the 'vertices' pointer into
//   the buffer object.
glBufferData( GL_ARRAY_BUFFER, 3*3*sizeof(GLfloat), vertices,
    GL_STATIC_DRAW );

To render the three vertices stored in this buffer object as a triangle, your application must bind the buffer object before issuing the appropriate vertex array pointer commands. See the next section, "Vertex Array Data," for additional information.

Some applications need to specify dynamic data. Because buffer objects exist in server memory, OpenGL provides an interface that allows applications to map the buffer object in client memory. This interface isn't covered in this book. For more information, see Section 2.9, "Buffer Objects," of The OpenGL Graphics System. Applications can replace all data in a buffer object with the glBufferData () command.

When your application no longer needs the buffer object, call glDeleteBuffers (). This command empties the specified buffers and places their IDs in OpenGL's pool of unused buffer object IDs.


void glDeleteBuffers( GLsizei n, const GLuint* buffers );

Returns buffer object identifiers to the unused pool and deletes buffer object resources. The parameters are the same as for glGenBuffers ().

U25B8.GIF OpenGL version: 1.5 and later.

Applications typically delete buffers when the application exits but should delete buffers to conserve server memory whenever the application no longer needs the buffer object.

2.2.2.2 Vertex Array Data

When rendering primitives with vertex arrays, your application must tell OpenGL where to obtain vertex array data. You can either submit the data directly or bind a buffer object, which tells OpenGL to obtain the data from that buffer. Both methods use the same interface.


void glVertexPointer( GLint size, GLenum type, GLsizei stride, const
  GLvoid* pointer );
void glNormalPointer( GLenum type, GLsizei stride, const GLvoid* pointer );
void glTexCoordPointer( GLint size, GLenum type, GLsizei stride, const
  GLvoid* pointer );

Submits arrays of vertices, normals, and texture coordinates to OpenGL for use with vertex arrays. type indicates the type of data being submitted. Applications typically use GL_FLOAT for single-precision vertices, normals, and texture coordinates. If your application uses buffer objects, most OpenGL implementations optimize for GL_FLOAT data. [3]

stride lets you interleave data. If your data is tightly packed (noninter-leaved), specify a stride of zero. Otherwise, specify a byte distance between the vertices, normals, or texture coordinates.

pointer points to your data or indicates an offset into a buffer object.

glVertexPointer () and glTexCoordPointer () additionally take a size parameter. Use a size of 3 when sending 3D (xyz) vertices with glVertexPointer (). For 2D (st) texture coordinates, specify a size of 2 to glTexCoordPointer (). Because normals always consist of three elements, glNormalPointer () doesn't require a size parameter.

OpenGL supports sending other vertex data besides normals and texture coordinates. See OpenGL® Programming Guide for more information.

U25B8.GIF OpenGL version: 1.1 and later.

To enable and disable vertex, normal, and texture coordinate arrays, call glEnableClientState () and glDisableClientState () with the parameters GL_VERTEX_ARRAY, GL_NORMAL_ARRAY, and GL_TEXTURE_COORD_ARRAY, respectively. If your application renders a vertex array primitive without enabling the normal or texture coordinate arrays, OpenGL renders the primitive without that data. Note that if your application fails to call glEnableClientState ( GL_VERTEX_ARRAY ), OpenGL will render nothing; the vertex array must be enabled to render geometry with vertex arrays.

glEnableClientState () and glDisableClientState () are similar in concept to glEnable () and glDisable (), except that the former enables and disables OpenGL client state features, whereas the latter enables and disables OpenGL server state features. See "glEnableClientState" in OpenGL® Reference Manual for more information.

Without Buffer Objects

Most programmers will code their applications to use buffer objects but also need to allow for the case in which buffer objects are unavailable, such as when running on a pre-1.5 version of OpenGL. The example code that accompanies this book demonstrates both methods.

When not using buffer objects, the pointer parameter is a simple GLvoid* address of array data. The following code demonstrates how to enable and specify a vertex array by using glVertexPointer ():

const GLfloat data[] = {
    -1.f, -1.f, 0.f,
    1.f, -1.f, 0.f,
    0.f, 1.f, 0.f };

// Enable the vertex array and specify the data.
glEnableClientState( GL_VERTEX_ARRAY );
glVertexPointer( 3, GL_FLOAT, 0, data );

Although this may be simpler than using buffer objects, it requires OpenGL to copy the vertex array from client memory each time the application renders a primitive that uses it. This is illustrated in Figure 2-2.

02fig02.gif

Figure 2-2 Rendering with vertex arrays before OpenGL version 1.5. OpenGL must copy the vertex array data each time the application renders primitives that use it.

The next section discusses buffer objects (the preferred method), which eliminate the copy from client memory to server memory. Buffer objects allow vertex array rendering commands to source vertex array data directly from high-performance server memory.

With Buffer Objects

As shown in the preceding section, "Without Buffer Objects," before OpenGL version 1.5, applications could submit data only directly with these functions; the pointer parameter could only be an address in application-accessible memory, and OpenGL accessed the data from client memory to render primitives. OpenGL version 1.5 overloads these functions, however. If your application has bound a buffer object to GL_ARRAY_BUFFER, OpenGL interprets pointer as an offset into the buffer object data.

Because C doesn't allow function overloading, pointer must still be a GLvoid* address, even though the buffer object feature requires an offset. When obtaining data from a buffer object, OpenGL computes this offset by subtracting a pointer to NULL from pointer, where both addresses are treated as char*.

Applications commonly direct OpenGL to use buffer object data from the start of the buffer, for example. In this case, the offset into the buffer is 0. If you store vertices in the buffer object as tightly packed single-precision floats, call glVertexPointer () as follows:

glVertexPointer( 3, GL_FLOAT, 0, (GLvoid*)((char*)NULL) );

Here, pointer is a char* pointer to NULL, typecast as a GLvoid*. To determine the offset into the buffer object, OpenGL subtracts a pointer to NULL from pointer, and in this case, the result is an offset of 0.

As a second example, consider how you would call glVertexPointer () if you wanted to skip the first vertex in the buffer object. Because a single vertex is 12 bytes long (3 * sizeof( GLfloat)) on most 32-bit systems, this requires an offset of 12 bytes into the buffer object. Your application would call glVertexPointer () as follows:

glVertexPointer( 3, GL_FLOAT, 0, (GLvoid*)(((char*)NULL) + 12) );

pointer is a char* pointer to NULL plus 12 bytes, so when OpenGL subtracts a pointer to NULL from pointer, it obtains a result of 12. When rendering, OpenGL uses this as an offset into the buffer object before obtaining vertex data, effectively skipping the first vertex.

The following code shows a convenience routine that takes the desired offset in bytes as a parameter and returns a GLvoid* value to use as the pointer parameter to the vertex array pointer functions:

inline GLvoid* bufferObjectPtr( unsigned int idx )
{
    return (GLvoid*)( ((char*)NULL) + idx );
}

The example code uses this function when using vertex arrays with buffer objects. Using this function, your application would call glVertexPointer () as follows to specify a 12-byte buffer object offset:

glVertexPointer( 3, GL_FLOAT, 0, bufferObjectPtr( 12 ) );

Without buffer objects, the vertex array pointer commands incur the expense of copying vertex array data each time the vertex array rendering commands are issued. With buffer objects, OpenGL copies this data at initialization time, when the application calls glBufferData (). At render time, the vertex array pointer commands simply pass in an offset to the bound buffer object, eliminating render-time copies from client memory to server memory. This is illustrated in Figure 2-3.

02fig03.gif

Figure 2-3 Vertex arrays with buffer objects. Initialization operations are shown in (a). The application allocates a buffer object ID with glGenBuffers()glGenBuffers() and stores vertex array data with glBufferData(). At render time (b), the application binds the buffer object and sends vertex array pointer and rendering commands.

2.2.2.3 Vertex Array Rendering Commands

So far, this chapter has covered creating and using buffer objects, and specifying vertex arrays with and without buffer objects. OpenGL doesn't draw anything, however, until your application issues vertex array rendering commands that tell OpenGL what primitives to draw from the vertex array data. This section covers those commands.

OpenGL provides several commands for drawing primitives with vertex arrays; OpenGL® Distilled covers three of them. (See OpenGL® Programming Guide, OpenGL® Reference Manual, and The OpenGL Graphics System for information on other vertex array commands.) These three commands are:

  • glDrawElements ()— This command renders a single OpenGL primitive with vertices and vertex data specified by vertex array indices.
  • glDrawRangeElements ()— This optimized form of glDrawElements () restricts selected vertex data to a specified range of index values.
  • glMultiDrawElements ()— This command renders multiple OpenGL primitives of the same type. It, too, uses indices to obtain vertex data from vertex arrays.

All three commands use indices to obtain vertices, normals, and texture coordinates from the data specified by your application in glVertex-Pointer (), glNormalPointer (), and glTexCoordPointer (). Before OpenGL version 1.5, copying these indices was a potential performance bottleneck for large blocks of static indices. Buffer objects, however, allow the application to store the indices in high-performance server memory. The following sections demonstrate this technique.

Using glDrawElements()

Applications should use glDrawElements () to draw a single OpenGL primitive. glDrawElements () was added to OpenGL in version 1.1. The example code that comes with this book falls back to using this command in versions of OpenGL that don't support more-preferred interfaces.


void glDrawElements( GLenum mode, GLsizei count, GLenum type,
  const GLvoid* indices );

Draws a single primitive from vertex array data. mode is any valid OpenGL primitive type, as listed in the section "Primitive Types" earlier in this chapter and illustrated in Figure 2-1. count is the number of vertices to render.

indices is an array of indices into the enabled vertex arrays. type is the data type of the indices array and must be GL_UNSIGNED_BYTE, GL_UNSIGNED_SHORT, or GL_UNSIGNED_INT.

U25B8.GIF OpenGL version: 1.1 and later.

This command causes OpenGL to read count indices from the indices array and use them to index into the enabled vertex arrays to obtain vertices and vertex data. Then it uses that data to render a primitive of type mode. Using glDrawElements () vastly reduces the number of function calls made by applications using the glBegin ()/ glEnd () paradigm.

If a buffer object is bound to GL_ELEMENT_ARRAY_BUFFER when the glDrawElements () command is issued, OpenGL interprets indices as an offset into that buffer and obtains the indices from the bound buffer object. This is more common with glDrawRangeElements () and glMultiDrawElements (), because an OpenGL implementation that supports buffer objects also supports these more efficient interfaces.

Calling glDrawElements () with enabled normal and texture coordinate arrays leaves the current normal and current texture-coordinate states undefined.

Using glDrawRangeElements()

The glDrawRangeElements () command is identical to glDrawElements () but requires the application to specify the minimum and maximum values of the indices. Using this command often results in better performance than glDrawElements () because it allows OpenGL to make better use of internal vertex array data caches.


void glDrawRangeElements( GLenum mode, GLuint start, GLuint end,
  GLsizei count, GLenum type, const GLvoid* indices );

Draws a single primitive using a range of vertex array data. mode, count, type, and indices mean the same as in glDrawElements (). start and end denote lower and upper limits on the count index values that are used in the indices array.

U25B8.GIF OpenGL version: 1.2 and later.

When glDrawRangeElements () was introduced in OpenGL version 1.2, it clearly outperformed glDrawElements () because it allowed OpenGL to process a single block of vertex data rather than element by element. Although buffer objects somewhat level the performance of these two commands, glDrawRangeElements () still largely replaces glDrawElements () in cases where the application knows the range of its indices.

If a buffer object is bound to GL_ELEMENT_ARRAY_BUFFER when the glDrawRangeElements () command is issued, OpenGL interprets indices as an offset into that buffer and obtains the indices from the bound buffer object. The example source code makes extensive use of this feature. See the section "Vertex Arrays Example Code" later in this chapter.

Like glDrawElements (), vertex states that are modified by enabled vertex arrays (such as the current normal and current texture coordinate) are left undefined after a call to glDrawRangeElements ().

Using glMultiDrawElements()

The glMultiDrawElements () command draws multiple primitives of the same type. It's comparable to calling glDrawElements () repeatedly with the same mode parameter.


void glMultiDrawElements( GLenum mode, const GLsizei* count,
  GLenum type, const GLvoid** indices, GLsizei primcount );

Draws multiple primitives of the same type from vertex array data. primcount is the number of primitives to draw. All primitives drawn are of type mode.

indices is a two-dimensional array of type type. Each subarray in indices contains the indices for one of the primitives. The number of indices in the subarray is specified by the corresponding element of the count array.

U25B8.GIF OpenGL version: 1.4 and later.

This command is simple in concept, yet difficult to explain in plain English text. As an illustration, consider that glMultiDrawElements () behaves as though it were executing the following code:

for ( int i=0; i<primcount; i++ )
  if (count[i] > 0)
    glDrawElements( mode, count[i], type, indices[i] );

If a buffer object is bound to GL_ELEMENT_ARRAY_BUFFER when the glMultiDrawElements () command is issued, OpenGL interprets indices as an offset into that buffer and obtains the indices from the bound buffer object. The example source code makes extensive use of this feature. See the next section, "Vertex Arrays Example Code," for an example.

Like glDrawElements () and glDrawRangeElements (), vertex states that are modified by enabled vertex arrays (such as the current normal and current texture coordinate) are left undefined after a call to glMultiDrawElements ().

2.2.2.4 Vertex Arrays Example Code

The example code, available from this book's Web site, contains C++ objects for rendering cylinders, spheres, planes, and tori. The code uses different rendering paradigms and issues different OpenGL commands depending on the OpenGL version; your application needs to do the same to run on a wide variety of OpenGL implementations:

  • If the OpenGL version is 1.5 or later, the example code creates and uses buffer objects. Otherwise, it passes vertex data directly when issuing the vertex pointer commands.
  • All objects require either glDrawRangeElements () or glMultiDrawElements () and use this interface if the OpenGL version is 1.2 or 1.4 or later, respectively. Otherwise, the code uses the glDrawElements () interface.
  • The example code requires at least OpenGL version 1.1; it never falls back to using the glBegin ()/ glEnd () paradigm. This should be sufficient because OpenGL version 1.0 implementations are rarely encountered today.

To branch efficiently in the OpenGL version, the example code uses a singleton instance that queries the OpenGL version string by using glGetString ( GL_VERSION ) once and encodes it as an enum: Ver11 for version 1.1, Ver12 for version 1.2, and so on.

Listing 2-1 shows the draw() method of the Cylinder object. This code draws the cylinder body as a quad strip and optionally caps the ends of the cylinder with triangle fans. (Note that this example doesn't use display lists. Later in this chapter, "Performance Issues" covers this topic.)

Example 2-1. Drawing with vertex arrays and buffer objects

void Cylinder::draw()
{
    if (!_valid)
    {
        if (!init())
            return;
    }

    glPushClientAttrib( GL_CLIENT_VERTEX_ARRAY_BIT );

    glEnableClientState( GL_VERTEX_ARRAY );
    glEnableClientState( GL_NORMAL_ARRAY );

    if (OGLDif::instance()->getVersion() >= Ver15)
    {
        glBindBuffer( GL_ARRAY_BUFFER, _vbo[1] );
        glNormalPointer( GL_FLOAT, 0, bufferObjectPtr( 0 ) );
        glBindBuffer( GL_ARRAY_BUFFER, _vbo[0] );
        glVertexPointer( 3, GL_FLOAT, 0, bufferObjectPtr( 0 ) );

        glBindBuffer( GL_ELEMENT_ARRAY_BUFFER, _vbo[2] );
        glDrawRangeElements( GL_QUAD_STRIP, _idxStart,
                _cap1Idx, _numVerts, GL_UNSIGNED_SHORT,
                bufferObjectPtr( 0 ) );
        if (_drawCap1)
            glDrawRangeElements( GL_TRIANGLE_FAN, _cap1Idx,
                    _cap2Idx, _numCapVerts, GL_UNSIGNED_SHORT,
                    bufferObjectPtr((_cap1Start-_indices)
                    * sizeof(GLushort)) );
        if (_drawCap2)
            glDrawRangeElements( GL_TRIANGLE_FAN, _cap2Idx,
                    _idxEnd, _numCapVerts, GL_UNSIGNED_SHORT,
                    bufferObjectPtr((_cap2Start-_indices)
                    * sizeof(GLushort)) );
    }
    else
    {
        glVertexPointer( 3, GL_FLOAT, 0, _vertices );
        glNormalPointer( GL_FLOAT, 0, _normals );

        if (OGLDif::instance()->getVersion() >= Ver12)
        {
            glDrawRangeElements( GL_QUAD_STRIP, _idxStart,
                    _cap1Idx, _numVerts, GL_UNSIGNED_SHORT,
                    _indices );
            if (_drawCap1)
                glDrawRangeElements( GL_TRIANGLE_FAN, _cap1Idx,
                        _cap2Idx, _numCapVerts, GL_UNSIGNED_SHORT,
                        _cap1Start );
            if (_drawCap2)
                glDrawRangeElements( GL_TRIANGLE_FAN, _cap2Idx,
                        _idxEnd, _numCapVerts, GL_UNSIGNED_SHORT,
                        _cap2Start );
        }
        else
        {
            glDrawElements( GL_QUAD_STRIP, _numVerts,
                    GL_UNSIGNED_SHORT, _indices );
            if (_drawCap1)
                glDrawElements( GL_TRIANGLE_FAN, _numCapVerts,
                        GL_UNSIGNED_SHORT, _cap1Start );
            if (_drawCap2)
                glDrawElements( GL_TRIANGLE_FAN, _numCapVerts,
                        GL_UNSIGNED_SHORT, _cap2Start );
        }
    }

    glPopClientAttrib();

    OGLDIF_CHECK_ERROR;
}

The code first tests to see whether the Cylinder object has already been initialized, and if it hasn't, it calls the init() method, which generates the cylinder vertex and normal data. If the OpenGL version is 1.5 or later, it also allocates buffer objects and fills them with that data. Download the example source code to see the Cylinder::init() method.

The draw() method pushes the client attribute stack with a call to glPushClientAttrib (). When it calls glPopClientAttrib () at the end of the function, the client states changed by this function—such as the state of enabled arrays, bound buffer objects, and vertex array pointers—restore to their previous values.

The Cylinder object doesn't specify texture coordinates, so it needs only to enable the normal and vertex arrays. These arrays must be enabled regardless of the OpenGL version.

Next, the draw() method specifies the vertex data and issues the rendering commands. How draw() does this depends on the OpenGL version.

For OpenGL version 1.5, the Cylinder object uses three buffer objects: one for normal data, one for vertex data, and one for the indices into the arrays. Note that the normal and vertex buffer objects are bound to GL_ARRAY_BUFFER before calling glNormalPointer () and glVertexPointer (), respectively, and that the indices buffer object is bound to GL_ELEMENT_ARRAY_BUFFER before issuing the drawing commands.

The code draws three primitives—a quad strip and, optionally, two triangle fans—using three glDrawRangeElements () commands. The code doesn't use different buffer objects for each primitive. Instead, it passes in different offsets to each glDrawRangeElements () call (along with different minimum and maximum index range values) to access the specific data for each primitive.

If the OpenGL version is less than 1.5, the code passes the vertex and normal data directly in its calls to glNormalPointer () and glVertexPointer ().

The code uses glDrawRangeElements () if the version is 1.2 or greater, but when less than version 1.5, the code must pass the indices directly to these commands rather than pass offsets.

If the version is less than 1.2, the code assumes version 1.1 and uses glDrawElements (). This is the least-desired code path, because OpenGL doesn't know what data to process until after it copies and starts to process each index.

Before returning to the calling application, Cylinder::draw() pops the client attribute stack and tests for OpenGL errors. OGLDIF_CHECK_ERROR is a CPP macro, described in appendix D, "Troubleshooting and Debugging."

  • + Share This
  • 🔖 Save To Your Account