Chapter 11. It's All About the Pipeline: Faster Geometry Throughput

by Richard S. Wright, Jr.

WHAT YOU'LL LEARN IN THIS CHAPTER:

How To

Functions You'll Use

Assemble polygons to create 3D objects

glBegin/glEnd/glVertex

Optimize object display with display lists

glNewList/glEndList/glCallList

Store and transfer geometry more efficiently

glEnableClientState/

 

glDisableClientState/

 

glVertexPointer/glNormalPointer/

 

glTexCoordPointer/glColorPointer/

 

glEdgeFlagPointer/

 

glFogCoordPointer/

 

glSecondaryColorPointer/

 

glArrayElement/glDrawArrays/

 

glInterleavedArrays

Reduce geometric bandwidth

glDrawElements/

 

glDrawRangeElements/

 

glMultiDrawElements

In the preceding chapters, we covered the basic OpenGL rendering techniques and technologies. Using this knowledge, there are few 3D scenes you can envision that cannot be realized using only the first half of this book. Now, however, we turn our attention to the techniques and practice of rendering complex geometric models using your newfound knowledge of OpenGL rendering capabilities.

We begin with a basic overview of the many complex models assembled from simpler pieces. We then progress to some new OpenGL functionality that helps you more quickly move geometry and other OpenGL commands to the hardware renderer (graphics card). Finally, we introduce you to some higher level ideas to eliminate costly drawing commands and geometry that is outside your field of view.

Model Assembly 101

Generally speaking, all 3D objects rendered with OpenGL are composed of some number of the 10 basic OpenGL rendering primitives: GL_POINTS, GL_LINES, GL_LINE_STRIP, GL_LINE_LOOP, GL_TRIANGLES, GL_TRIANGLE_STRIP, GL_TRIANGLE_FAN, GL_QUADS, GL_QUAD_STRIPS, or GL_POLYGON. Managing all the individual vertices and primitive groups can become quite complex when the geometry grows in size or complexity. The usual way of dealing with highly complex objects is simply “divide and conquer”—break it down into many smaller, simpler pieces.

For example, the SNOWMAN sample in Chapter 10, “Curves and Surfaces,” was simply made out of spheres, cylinders, cones, and disks cleverly arranged with a few geometric transformations. We begin this chapter with another simple model that is composed of more than one individual part: a model of a metallic bolt (such as those holding your disk drive together). Although this particular bolt might not exist in any hardware store, it does have the essential features of our end goal.

The bolt will have a six-sided head and a threaded shaft, as do many typical steel bolts. Because this is a learning exercise, we simplify the threads by making them raised on the surface of the bolt shaft rather than carved out of the shaft. Figure 11.1 provides a rough sketch of what we're aiming for. We build the three major components of this bolt—the head, shaft, and threads—individually and then put them together to form the final object.

The hex bolt to be modeled in this chapter.

Figure 11.1. The hex bolt to be modeled in this chapter.

Pieces and Parts

Any given programming task can be separated into smaller, more manageable tasks. Breaking down the tasks makes the smaller pieces easier to handle and code, and introduces some reusability into the code base as well. Three-dimensional modeling is no exception; you will typically create large, complex systems out of many smaller and more manageable pieces.

As previously mentioned, we have decided to break down the bolt into three pieces: head, shaft, and thread. Certainly, breaking down the tasks makes it much simpler for us to consider each section graphically, but it also gives us three objects that we can reuse. In more complex modeling applications, this reusability is of crucial importance. In a CAD-type application, for example, you would probably have many different bolts to model with various lengths, thicknesses, and thread densities. Instead of, say, a RenderHead function that draws the head of the bolt, you might want to write a function that takes parameters specifying the number of sides, thickness, and diameter of the bolt head.

Another thing we do is model each piece of our bolt in coordinates that are most convenient for describing the object. Most often, individual objects or meshes are modeled around the origin and then translated and rotated into place. Later, when composing the final object, we can translate the components, rotate them, and even scale them if necessary to assemble our composite object. We do this for two very good reasons. First, this approach enables rotations to take place around the object's geometric center instead of some arbitrary point off to one side (depending on the whim of the modeler). The second reason will become more obvious later in the chapter. For now, suffice it to say that it is very useful to know the distance from any given point to the center of an object and to be able to more easily calculate an object's 3D extents (how big it is).

The Head

The head of our bolt has six smooth sides and is smooth on top and bottom. We can construct this solid object with two hexagons that represent the top and bottom of the head and a series of quadrilaterals around the edges to represent the sides. We could use GL_QUAD_STRIP to draw the head with a minimum number of vertices; however, as we discussed previously in Chapter 6, “More on Colors and Materials,” this approach would require that each edge share a surface normal. By using individual quads (GL_QUADS), we can at least cut down on sending one additional vertex to OpenGL per side (as opposed to sending down two triangles). For a small model such as this, the difference is negligible. For larger models, this step could mean a significant savings.

Figure 11.2 illustrates how the bolt head is constructed with the triangle fan and quads. We use a triangle fan with six triangles for the top and bottom sections of the head. Then we compose each face of the side of the bolt with a single quad.

Primitive outline of bolt head.

Figure 11.2. Primitive outline of bolt head.

We used a total of 18 primitives to draw the bolt head: 6 triangles (or one fan) each on the top and bottom and 6 quads to compose the sides of the bolt head. Listing 11.1 contains the function that renders the bolt head. Figure 11.3 shows what the bolt head looks like when rendered by itself (the completed program is the BOLT sample on the CD). This code contains only functions we've already covered, but it's more substantial than any of the simpler chapter examples. Also, note that the origin of the coordinate system is in the exact center of the bolt head.

Example 11.1. Code to Render the Bolt Head

///////////////////////////////////////////////////////////////////////////
// Creates the head of the bolt
void RenderHead(void)
    {
    float x,y,angle;                    // Calculated positions
    float height = 25.0f;               // Thickness of the head
    float diameter = 30.0f;             // Diameter of the head
    GLTVector3 vNormal,vCorners[4];     // Storage of vertices and normals
    float step = (3.1415f/3.0f);        // step = 1/6th of a circle = hexagon

    // Set material color for head of bolt
    glColor3f(0.0f, 0.0f, 0.7f);

    // Begin a new triangle fan to cover the top
    glFrontFace(GL_CCW);
    glBegin(GL_TRIANGLE_FAN);

        // All the normals for the top of the bolt point straight up
        // the z axis.
        glNormal3f(0.0f, 0.0f, 1.0f);

        // Center of fan is at the origin
        glVertex3f(0.0f, 0.0f, height/2.0f);

        // Divide the circle up into 6 sections and start dropping
        // points to specify the fan. We appear to be winding this
        // fan backwards. This has the effect of reversing the winding
        // of what would have been a CW wound primitive. Avoiding a state
        // change with glFrontFace().

        // First and Last vertex closes the fan
        glVertex3f(0.0f, diameter, height/2.0f);

        for(angle = (2.0f*3.1415f)-step; angle >= 0; angle -= step)
            {
            // Calculate x and y position of the next vertex
            x = diameter*(float)sin(angle);
            y = diameter*(float)cos(angle);

            // Specify the next vertex for the triangle fan
            glVertex3f(x, y, height/2.0f);
            }

        // Last vertex closes the fan
        glVertex3f(0.0f, diameter, height/2.0f);

    // Done drawing the fan that covers the bottom
    glEnd();

    // Begin a new triangle fan to cover the bottom
    glBegin(GL_TRIANGLE_FAN);

        // Normal for bottom points straight down the negative z axis
        glNormal3f(0.0f, 0.0f, -1.0f);

        // Center of fan is at the origin
        glVertex3f(0.0f, 0.0f, -height/2.0f);

        // Divide the circle up into 6 sections and start dropping
        // points to specify the fan
        for(angle = 0.0f; angle < (2.0f*3.1415f); angle += step)
            {
            // Calculate x and y position of the next vertex
            x = diameter*(float)sin(angle);
            y = diameter*(float)cos(angle);

            // Specify the next vertex for the triangle fan
            glVertex3f(x, y, -height/2.0f);
            }

        // Last vertex, used to close the fan
        glVertex3f(0.0f, diameter, -height/2.0f);

    // Done drawing the fan that covers the bottom
    glEnd();


    // Build the sides out of triangles (two each). Each face
    // will consist of two triangles arranged to form a
    // quadrilateral
    glBegin(GL_QUADS);

        // Go around and draw the sides
        for(angle = 0.0f; angle < (2.0f*3.1415f); angle += step)
            {
            // Calculate x and y position of the next hex point
            x = diameter*(float)sin(angle);
            y = diameter*(float)cos(angle);

            // start at bottom of head
            vCorners[0][0] = x;
            vCorners[0][1] = y;
            vCorners[0][2] = -height/2.0f;

            // extrude to top of head
            vCorners[1][0] = x;
            vCorners[1][1] = y;
            vCorners[1][2] = height/2.0f;

            // Calculate the next hex point
            x = diameter*(float)sin(angle+step);
            y = diameter*(float)cos(angle+step);

            // Make sure we aren't done before proceeding
            if(angle+step < 3.1415*2.0)
                {
                // If we are done, just close the fan at a
                // known coordinate.
                vCorners[2][0] = x;
                vCorners[2][1] = y;
                vCorners[2][2] = height/2.0f;

                vCorners[3][0] = x;
                vCorners[3][1] = y;
                vCorners[3][2] = -height/2.0f;
                }
            else
                {
                // We aren't done, the points at the top and bottom
                // of the head.
                vCorners[2][0] = 0.0f;
                vCorners[2][1] = diameter;
                vCorners[2][2] = height/2.0f;

                vCorners[3][0] = 0.0f;
                vCorners[3][1] = diameter;
                vCorners[3][2] = -height/2.0f;
                }

            // The normal vectors for the entire face will
            // all point the same direction
            gltGetNormalVector(vCorners[0], vCorners[1], vCorners[2], vNormal);
            glNormal3fv(vNormal);

            // Specify each quad separately to lie next
            // to each other.
            glVertex3fv(vCorners[0]);
            glVertex3fv(vCorners[1]);
            glVertex3fv(vCorners[2]);
            glVertex3fv(vCorners[3]);
            }

    glEnd();
    }
Output from the head program.

Figure 11.3. Output from the head program.

The Shaft

The shaft of the bolt is nothing more than a cylinder with a bottom on it. We compose a cylinder by plotting x,z values around in a circle and then take two y values at these points and get polygons that approximate the wall of a cylinder. This time, however, we compose this wall entirely out of a quad strip because each adjacent quad can share a normal for smooth shading (see Chapter 5, “Color, Materials, and Lighting: The Basics”). Figure 11.4 shows the rendered cylinder.

The shaft of the bolt, rendered as a quad strip around the shaft body.

Figure 11.4. The shaft of the bolt, rendered as a quad strip around the shaft body.

We also create the bottom of the shaft with a triangle fan, as we did for the bottom of the bolt head previously. Notice now, however, that the smaller step size around the circle yields smaller flat facets, which make the cylinder wall more closely approximate a smooth curve. The step size also matches that used for the shaft wall so that they match evenly.

Listing 11.2 provides the code to produce this cylinder. Notice that the normals are not calculated for the quads using the vertices of the quads. We usually set the normal to be the same for all vertices, but here, we break with this tradition to specify a new normal for each vertex. Because we are simulating a curved surface, the normal specified for each vertex is normal to the actual curve. (If this description seems confusing, review Chapter 5 on normals and lighting effects.)

Example 11.2. Code to Render the Shaft of the Bolt

////////////////////////////////////////////////////////////////////////
// Creates the shaft of the bolt as a cylinder with one end
// closed.
void RenderShaft(void)
    {
    float x,z,angle;                    // Used to calculate cylinder wall
    float height = 75.0f;               // Height of the cylinder
    float diameter = 20.0f;             // Diameter of the cylinder
    GLTVector3 vNormal,vCorners[2];     // Storage for vertex calculations
    float step = (3.1415f/50.0f);       // Approximate the cylinder wall with
                                        // 100 flat segments.

    // Set material color for head of screw
    glColor3f(0.0f, 0.0f, 0.7f);

    // First assemble the wall as 100 quadrilaterals formed by
    // placing adjoining Quads together
    glFrontFace(GL_CCW);
    glBegin(GL_QUAD_STRIP);

        // Go around and draw the sides
        for(angle = (2.0f*3.1415f); angle > 0.0f; angle -= step)
            {
            // Calculate x and y position of the first vertex
            x = diameter*(float)sin(angle);
            z = diameter*(float)cos(angle);

           // Get the coordinate for this point and extrude the
           // length of the cylinder.
           vCorners[0][0] = x;
           vCorners[0][1] = -height/2.0f;
           vCorners[0][2] = z;

           vCorners[1][0] = x;
           vCorners[1][1] = height/2.0f;
           vCorners[1][2] = z;

           // Instead of using real normal to actual flat section
           // Use what the normal would be if the surface was really
           // curved. Since the cylinder goes up the Y axis, the normal
           // points from the Y axis out directly through each vertex.
           // Therefore we can use the vertex as the normal, as long as
           // we reduce it to unit length first and assume the y component
           // to be zero
           vNormal[0] = vCorners[1][0];
           vNormal[1] = 0.0f;
           vNormal[2] = vCorners[1][2];

           // Reduce to length of one and specify for this point
           gltNormalizeVector(vNormal);
           glNormal3fv(vNormal);
           glVertex3fv(vCorners[0]);
           glVertex3fv(vCorners[1]);
           }

        // Make sure there are no gaps by extending last quad to
        // the original location
        glVertex3f(diameter*(float)sin(2.0f*3.1415f), -height/2.0f,
                                       diameter*(float)cos(2.0f*3.1415f));

        glVertex3f(diameter*(float)sin(2.0f*3.1415f), height/2.0f,
                                       diameter*(float)cos(2.0f*3.1415f));

    glEnd();    // Done with cylinder sides

    // Begin a new triangle fan to cover the bottom
    glBegin(GL_TRIANGLE_FAN);
        // Normal points down the Y axis
        glNormal3f(0.0f, -1.0f, 0.0f);

        // Center of fan is at the origin
        glVertex3f(0.0f, -height/2.0f, 0.0f);

        // Spin around matching step size of cylinder wall
        for(angle = (2.0f*3.1415f); angle > 0.0f; angle -= step)
            {
            // Calculate x and y position of the next vertex
            x = diameter*(float)sin(angle);
            z = diameter*(float)cos(angle);

           // Specify the next vertex for the triangle fan
           glVertex3f(x, -height/2.0f, z);
           }

        // Be sure loop is closed by specifying initial vertex
        // on arc as the last too
        glVertex3f(diameter*(float)sin(2.0f*3.1415f), -height/2.0f,
                                    diameter*(float)cos(2.0f*3.1415f));
    glEnd();
    }

The Thread

The thread is the most complex part of the bolt. It's composed of two planes arranged in a V shape that follows a corkscrew pattern up the length of the shaft. Figure 11.5 shows the rendered thread, and Listing 11.3 provides the OpenGL code used to produce this shape.

Example 11.3. Code to Render the Threads

//////////////////////////////////////////////////////////////////////////////
// Spiraling thread
void RenderThread(void)
    {
    float x,y,z,angle;                 // Calculate coordinates and step angle
    float height = 75.0f;              // Height of the threading
    float diameter = 20.0f;            // Diameter of the threading
    GLTVector3 vNormal, vCorners[4];   // Storage for normal and corners
    float step = (3.1415f/32.0f);      // one revolution
    float revolutions = 7.0f;          // How many times around the shaft
    float threadWidth = 2.0f;          // How wide is the thread
    float threadThick = 3.0f;          // How thick is the thread
    float zstep = .125f;               // How much does the thread move up
                                       // the Z axis each time a new segment
                                       // is drawn.

    // Set material color for head of screw
    glColor3f(0.0f, 0.0f, 0.4f);

    z = -height/2.0f+2.0f;    // Starting spot almost to the end

    // Go around and draw the sides until finished spinning up
    for(angle = 0.0f; angle < GLT_PI * 2.0f *revolutions; angle += step)
        {
        // Calculate x and y position of the next vertex
        x = diameter*(float)sin(angle);
        y = diameter*(float)cos(angle);

        // Store the next vertex next to the shaft
        vCorners[0][0] = x;
        vCorners[0][1] = y;
        vCorners[0][2] = z;

        // Calculate the position away from the shaft
        x = (diameter+threadWidth)*(float)sin(angle);
        y = (diameter+threadWidth)*(float)cos(angle);

        vCorners[1][0] = x;
        vCorners[1][1] = y;
        vCorners[1][2] = z;

        // Calculate the next position away from the shaft
        x = (diameter+threadWidth)*(float)sin(angle+step);
        y = (diameter+threadWidth)*(float)cos(angle+step);

        vCorners[2][0] = x;
        vCorners[2][1] = y;
        vCorners[2][2] = z + zstep;

        // Calculate the next position along the shaft
        x = (diameter)*(float)sin(angle+step);
        y = (diameter)*(float)cos(angle+step);

        vCorners[3][0] = x;
        vCorners[3][1] = y;
        vCorners[3][2] = z+ zstep;

       // We'll be using triangles, so make
       // counterclockwise polygons face out
       glFrontFace(GL_CCW);
       glBegin(GL_TRIANGLES);  // Start the top section of thread

           // Calculate the normal for this segment
           gltGetNormalVector(vCorners[0], vCorners[1], vCorners[2], vNormal);
           glNormal3fv(vNormal);

           // Draw two triangles to cover area
           glVertex3fv(vCorners[0]);
           glVertex3fv(vCorners[1]);
           glVertex3fv(vCorners[2]);

           glVertex3fv(vCorners[2]);
           glVertex3fv(vCorners[3]);
           glVertex3fv(vCorners[0]);
        glEnd();


        // Move the edge along the shaft slightly up the z axis
        // to represent the bottom of the thread
        vCorners[0][2] += threadThick;
        vCorners[3][2] += threadThick;

        // Recalculate the normal since points have changed, this
        // time it points in the opposite direction, so reverse it
        gltGetNormalVector(vCorners[0], vCorners[1], vCorners[2], vNormal);
        vNormal[0] = -vNormal[0];
        vNormal[1] = -vNormal[1];
        vNormal[2] = -vNormal[2];

        // Switch to clockwise facing out for underside of the
        // thread.
        glFrontFace(GL_CW);

        // Draw the two triangles
        glBegin(GL_TRIANGLES);
            glNormal3fv(vNormal);

            glVertex3fv(vCorners[0]);
            glVertex3fv(vCorners[1]);
            glVertex3fv(vCorners[2]);

            glVertex3fv(vCorners[2]);
            glVertex3fv(vCorners[3]);
            glVertex3fv(vCorners[0]);
        glEnd();

        // Creep up the Z axis
        z += zstep;
        }
    }
The bolt threads winding up the shaft (shown without shaft).

Figure 11.5. The bolt threads winding up the shaft (shown without shaft).

Putting It Together

We assemble the bolt by drawing all three sections in their appropriate location. All sections are translated and rotated appropriately into place. The shaft is not modified at all, and the threads must be rotated to match the shaft. Finally, the head of the bolt must be rotated and translated to put it in its proper place. Listing 11.4 provides the rendering code that manipulates and renders the three bolt components. Figure 11.6 shows the final output of the bolt program.

Example 11.4. Code to Render All the Pieces in Place

// Called to draw scene
void RenderScene(void)
    {
    // Clear the window with current clearing color
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

    // Save the matrix state
    glMatrixMode(GL_MODELVIEW);
    glPushMatrix();

    // Rotate about x and y axes
    glRotatef(xRot, 1.0f, 0.0f, 0.0f);
    glRotatef(yRot, 0.0f, 0.0f, 1.0f);

    // Render just the Thread of the nut
    RenderShaft();

    glPushMatrix();
    glRotatef(-90.0f, 1.0f, 0.0f, 0.0f);
    RenderThread();

    glTranslatef(0.0f,0.0f,45.0f);
    RenderHead();
    glPopMatrix();

    glPopMatrix();

    // Swap buffers
    glutSwapBuffers();
    }
Output from the bolt program.

Figure 11.6. Output from the bolt program.

So why all the gymnastics to place all these pieces together? We easily could have adjusted our geometry so that all the pieces would be drawn in their correct location. The point shown here is that many pieces modeled about their own local origins can be arranged together in a scene quite easily to create a more sophisticated model or environment. This basic principle and technique form the foundation of creating your own scene graph (see Chapter 1, “Introduction to 3D Graphics and OpenGL”) or virtual environment. We have been putting this principle into practice all along with the SPHEREWORLD samples in each chapter. In a complex and persistent (saved to disk) 3D environment, each piece's position and orientation could be stored individually using a GLTFrame structure from the glTools library.

Display Lists

The BOLT program produces a reasonable representation of the metal bolt we set out to model. Consisting of more than 1,700 triangles, this bolt is the most complex manually generated example in this book so far in terms of geometry. Comparatively speaking, however, this number of triangles isn't anywhere close to the largest number of polygons you'll encounter when composing larger scenes and more complex objects. In fact, the latest 3D accelerated graphics cards are rated at millions of triangles per second, and that's for the cheap ones! One of the goals of this chapter is to introduce you to some more efficient ways to store and render your geometry. One of the simplest and most effective ways to do this is to use OpenGL display lists.

Batch Processing

OpenGL has been described as a software interface to graphics hardware. As such, you might imagine that OpenGL commands are somehow converted into some specific hardware commands or operators by the driver and then sent on to the graphics card for immediate execution. If so, you would be mostly correct. Most OpenGL rendering commands are, in fact, converted into some hardware-specific commands, but these commands are not dispatched immediately to the hardware. Instead, they are accumulated in a local buffer until some threshold is reached, at which point they are flushed to the hardware.

The primary reason for this type of arrangement is that trips to the graphics hardware take a long time, at least in terms of computer time. To a human being, this process might take place very quickly, but to a CPU running at many billions of cycles per second, this is like waiting for a cruise ship to sail from North America to Europe and back. You certainly would not put a single person on a ship and wait for the ship to return before loading up the next person. If you have many people to send to Europe, you are going to cram as many people on the ship as you can! This analogy is very accurate: It is faster to send a large amount of data (within some limits) over the system bus to hardware all at once than to break it down into many bursts of smaller packages.

Keeping to the analogy, you also do not have to wait for the first cruise ship to return before you can begin filling the next ship with passengers. Sending the buffer to the graphics hardware (a process called flushing) is an asynchronous operation. This means that the CPU can move on to other tasks and does not have to wait for the batch of rendering commands just sent to be completed. You can literally have the hardware rendering a given set of commands while the CPU is busy calling a new set of commands for the next graphics image (typically called a frame when you're creating an animation). This type of parallelization between the graphics hardware and the host CPU is highly efficient and often sought after by performance-conscious programmers.

Three events trigger a flush of the current batch of rendering commands. The first occurs when the driver's command buffer is full. You do not have access to this buffer, nor do you have any control over the size of the buffer. The hardware vendors work hard to tune the size and other characteristics of this buffer to work well with their devices. A flush also occurs when you execute a buffer swap. The buffer swap cannot occur until all pending commands have been executed (you want to see what you have drawn!), so the flush is initiated, followed by the command to perform the buffer swap. A buffer swap is an obvious indicator to the driver that you are done with a given scene and that all commands should be rendered. However, if you are doing single-buffered rendering, OpenGL has no real way of knowing when you're done sending commands and thus when to send the batch of commands to the hardware for execution. To facilitate this process, you can call the following function to manually trigger a flush:

void glFlush(void);

Some OpenGL commands, however, are not buffered for later execution—for example, glReadPixels and glDrawPixels. These functions directly access the framebuffer and read or write data directly. Therefore, it is useful to be able not only to flush the buffer, but also to wait for all the commands to be executed before calling one of these functions. For this reason, you also do not put these commands in a display list. For example, if you render an image that you want to read back with glReadPixels, you could read the framebuffer before the command batch has even been flushed. To both force a flush and wait for the all previous rendering commands to finish, call the following function:

void glFinish(void);

Preprocessed Batches

The work done every time you call an OpenGL command is not inconsequential. Commands are compiled, or converted, from OpenGL's high-level command language into low-level hardware commands understood by the hardware. For complex geometry, or just large amounts of vertex data, this process is performed many thousands of times, just to draw a single image onscreen. Often, the geometry or other OpenGL data remains the same from frame to frame. A solution to this needlessly repeated overhead is to save a chunk of data from the command buffer that performs some repetitive rendering task. This chunk of data can later be copied back to the command buffer all at once, saving the many function calls and compilation work done to create the data.

OpenGL provides a facility to create a preprocessed set of OpenGL commands (the chunk of data) that can then be quickly copied to the command buffer for more rapid execution. This precompiled list of commands is called a display list, and creating one or more of them is an easy and straightforward process. Just as you delimit an OpenGL primitive with glBegin/glEnd, you delimit a display list with glNewList/glEndList. A display list, however, is named with an integer value that you supply. The following code fragment represents a typical example of display list creation:

glNewList(<unsigned integer name>,GL_COMPILE);
...
...
// Some OpenGL Code
...
...
glEndList();

The named display list now contains all OpenGL rendering commands that occur between the glNewList and glEndList function calls. The GL_COMPILE parameter tells OpenGL to compile the list but not to execute it yet. You can also specify GL_COMPILE_AND_EXECUTE to simultaneously build the display list and execute the rendering instructions. Typically, however, display lists are built (GL_COMPILE only) during program initialization and then executed later during rendering.

The display list name can be any unsigned integer. However, if you use the same value twice, the second display list overwrites the previous one. For this reason, it is convenient to have some sort of mechanism to keep you from reusing the same display list more than once. This is especially helpful when you are incorporating libraries of code written by someone else who may have incorporated display lists and may have chosen the same display list names.

OpenGL provides built-in support for allocating unique display list names. The following function returns the first of a series of display list integers that are unique:

GLuint glGenLists(GLsizei range);

The display list names are reserved sequentially, with the first name being returned by the function. You can call this function as often as you want and for as many display list names at a time as you may need. A corresponding function frees display list names and releases any memory allocated for those display lists:

void glDeleteLists(GLuint list, GLsizei range);

A display list, containing any number of precompiled OpenGL commands, is then executed with a single command:

void glCallList(GLuint list);

You can also execute a whole array of display lists with this command:

void glCallLists(GLsizei n, GLenum type, const GLvoid *lists);

The first parameter specifies the number of display lists contained by the array lists. The second parameter contains the data type of the array; typically, it is GL_UNSIGNED_BYTE.

Display List Caveats

A few important points about display lists are worth mentioning here. Although on most implementations, a display list should improve performance, your mileage may vary depending on the amount of effort the vendor puts into optimizing display list creation and execution. It is rare, however, for display lists not to offer a noticeable boost in performance, and they are widely relied on in applications that use OpenGL.

Display lists are typically good at creating precompiled lists of OpenGL commands, especially if the list contains state changes (turning lighting on and off, for example). If you do not create a display list name with glGenLists first, you might get a working display list on some implementations, but not on others. Some commands simply do not make sense in a display list. For example, reading the framebuffer into a pointer with glReadPixels makes no sense in a display list. Likewise, calls to glTexImage2D would store the original image data in the display list, followed by the command to load the image data as a texture. Basically, your textures stored this way would take up twice as much memory! Display lists excel, however, at precompiled lists of geometry, with texture objects bound either inside or outside the display lists. Finally, display lists cannot contain calls that create display lists. You can have one display list call another, but you cannot put calls to glNewLists/glEndList inside a display list.

Converting to Display Lists

Converting the BOLT sample to use display lists requires only a few additional lines of code. First, we add three variables that contain the display list identifiers for the three pieces of the bolt:

// Display list identifiers
GLuint  headList, shaftList, threadList;

Then, in the SetupRC function, we request three display list names and assign them to our display list variables:

// Get Display list names
headList = glGenLists(3);
shaftList = headList + 1;
threadList = headList + 2;

Next, we add the code to generate the three display lists. Each display list simply calls the function that draws that piece of geometry:

// Prebuild the display lists
glNewList(headList, GL_COMPILE);
    RenderHead();
glEndList();

glNewList(shaftList, GL_COMPILE);
    RenderShaft();
glEndList();

glNewList(threadList, GL_COMPILE);
    RenderThread();
glEndList();

Finally, in the Render function, we simply replace each function call for the bolt pieces with the appropriate display list call:

// Render just the Thread of the nut
//RenderShaft();
glCallList(shaftList);

glPushMatrix();
glRotatef(-90.0f, 1.0f, 0.0f, 0.0f);
//RenderThread();
glCallList(threadList);

glTranslatef(0.0f,0.0f,45.0f);
//RenderHead();
glCallList(headList);

In this example, we have created three display lists, one for each component of the bolt. We also could have placed the entire bolt in a single display list or even created a fourth display list that contained calls to the other three. You can find the complete code for the display list version of the bolt in the BOLTDL sample program on the CD.

Measuring Performance

It is difficult to demonstrate the performance enhancements made by using display lists with something as simple as the BOLT example. To demonstrate the advantages of using display lists (or vertex arrays, for that matter), we need two things. First, we need a sample program with a more serious amount of geometry. Second, we need a way to measure performance besides some subjective measure of how fast an animation appears to be running.

Most chapters have included a SPHEREWORLD sample program that demonstrates an immersive 3D environment, enhanced using techniques presented in that particular chapter. By this point in the book, the SphereWorld contains a lot of geometry. A highly tessellated ground and torus and a number of high-resolution spheres inhabit the plane. In addition, the planar shadow algorithm we used requires that nearly all the geometry be processed twice (once for the object, once for the shadow). Certainly, this sample program should see some visible benefit from a retrofit using display lists.

A simple and meaningful measure of rendering performance is the measure of how many frames (individual images) can be rendered per second. In fact, many games and graphical benchmarking programs have options to display the frame rate prominently, as a frames per second (fps) indicator. For this chapter's installment of SPHEREWORLD, we will add both a frame rate indication and the option to use or not use display lists. The difference measured in fps should give us a reasonable indication of the type of performance improvement that display lists can frequently contribute to our applications.

The frame rate is simply the number of frames rendered over some period of time, divided by the amount of time elapsed. Counting buffer swaps is relatively simple…counting seconds, as it turns out, is not as easy as it sounds. High-resolution time keeping is unfortunately an operating system and hardware platform feature that is not very portable. Time-keeping functions are also documented poorly and can imply misleading performance characteristics. For example, most standard C runtime functions that can return time to the nearest millisecond often have a resolution of many, many milliseconds. Subtracting two times should give you the difference between events, but sometimes the minimum amount of time you can actually measure is as much as 1/20th of a second. With timer resolution this poor, you can sometimes render several frames and buffer swaps without being able to see any difference in time pass at all!

The glTools library contains a time data structure and two time functions that isolate operating system dependencies and give fairly good timing resolution. On the PC, it is often on the order of millionths of a second, and on the Mac, you will get at least 10ms resolution. These functions work something like a stopwatch. In fact, the data structure that contains the last sampled time is defined as such:

GLTStopwatch frameTimer;

These next two functions reset the stopwatch and read the number of elapsed seconds (as a floating-point value) since the last time the stopwatch was reset:

void gltStopwatchReset(GLTStopwatch *pTimer);
float gltStopwatchRead(GLTStopwatch *pTimer);

You must reset the stopwatch at least one time before any read time values will have any meaning.

A Better Example

Because SPHEREWORLD is a fairly long program and has already been introduced in earlier chapters, Listing 11.5 shows only the new RenderScene function, which contains some noteworthy changes.

Example 11.5. Main Rendering Function for SPHEREWORLD

void RenderScene(void)
    {
    static int iFrames = 0;             // Frame count
    static GLTStopwatch frameTimer;     // Render time

    // Reset the stopwatch on first time
    if(iFrames == 0)
        {
        gltStopwatchReset(&frameTimer);
        iFrames++;
        }

    // Clear the window with current clearing color
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);

    glPushMatrix();
        gltApplyCameraTransform(&frameCamera);

        // Position light before any other transformations
        glLightfv(GL_LIGHT0, GL_POSITION, fLightPos);

        // Draw the ground
        glColor3f(1.0f, 1.0f, 1.0f);
        if(iMethod == 0)
            DrawGround();
        else
            glCallList(groundList);

        // Draw shadows first
        glDisable(GL_DEPTH_TEST);
        glDisable(GL_LIGHTING);
        glDisable(GL_TEXTURE_2D);
        glEnable(GL_BLEND);
        glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
        glEnable(GL_STENCIL_TEST);
        glPushMatrix();
            glMultMatrixf(mShadowMatrix);
            DrawInhabitants(1);
        glPopMatrix();
        glDisable(GL_STENCIL_TEST);
        glDisable(GL_BLEND);
        glEnable(GL_LIGHTING);
        glEnable(GL_TEXTURE_2D);
        glEnable(GL_DEPTH_TEST);

        // Draw inhabitants normally
        DrawInhabitants(0);

    glPopMatrix();

    // Do the buffer Swap
    glutSwapBuffers();

    // Increment the frame count
    iFrames++;

    // Do periodic frame rate calculation
    if(iFrames == 101)
        {
        float fps;
        char cBuffer[64];

        fps = 100.0f / gltStopwatchRead(&frameTimer);
        if(iMethod == 0)
            sprintf(cBuffer,
                 "OpenGL SphereWorld without Display Lists %.1f fps", fps);
        else
            sprintf(cBuffer,
                 "OpenGL SphereWorld with Display Lists %.1f fps", fps);

        glutSetWindowTitle(cBuffer);
        gltStopwatchReset(&frameTimer);
        iFrames = 1;
        }

    // Do it again
    glutPostRedisplay();
    }

First, we have two static variables that will retain their values from one function call to the next. They are the frame counter and frame timer:

static int iFrames = 0;             // Frame count
static GLTStopwatch frameTimer;     // Render time

Because the timer must be initialized before use, we use the frame counter as a sentinel. The iFrames variable contains only the value 0 the first time the scene is rendered, so here we perform the initial stopwatch reset:

// Reset the stopwatch on first time
if(iFrames == 0)
    {
    gltStopwatchReset(&frameTimer);
    iFrames++;
    }

Next, we render the scene almost as usual. Note this change to the place where the ground is drawn:

if(iMethod == 0)
    DrawGround();
else
    glCallList(groundList);

The iMethod variable is set to 0 when the pop-up menu selection is Without Display Lists and 1 when With Display Lists is selected. A display list is generated in the SetupRC function for the ground, the torus, and a sphere. The DrawInhabitants function, likewise, switches between the display list and base function call as indicated by the iMethod variable. After the buffer swap, we simply increment the frame counter:

// Do the buffer Swap
glutSwapBuffers();

// Increment the frame count
iFrames++;

The frame rate is not calculated every single frame. Instead, we count some number of frames and then divide the total time by the number of frames. This approach serves two purposes. First, if the timer resolution is not terribly great, spreading out the time helps mitigate this problem by reducing the percentage of the total time represented by the error. Second, the process of calculating and displaying the frame rate takes time and will slow down the rendering, as well as adversely affect the accuracy of the measurement.

When the frame counter reaches 101, we have rendered 100 frames (recall, we start at 1, not 0). So, we create a string buffer, fill it with our frame rate calculation, and simply pop it into the window title bar. Finally, we must reset the timer and our counter to 1:

// Do periodic frame rate calculation
if(iFrames == 101)
    {
    float fps;
    char cBuffer[64];

    fps = 100.0f / gltStopwatchRead(&frameTimer);
    if(iMethod == 0)
        sprintf(cBuffer,
               "OpenGL SphereWorld without Display Lists %.1f fps", fps);
    else
        sprintf(cBuffer,
               "OpenGL SphereWorld with Display Lists %.1f fps", fps);

    glutSetWindowTitle(cBuffer);
    gltStopwatchReset(&frameTimer);
    iFrames = 1;
    }

Switching to display lists can have an amazing impact on performance. Some OpenGL implementations even try to store display lists in memory on the graphics card, if possible, further reducing the work required to get the data to the graphics processor. Figure 11.7 shows the SPHEREWORLD sample running without using display lists. The frame rate is fairly high already on a modern consumer graphics card. However, in Figure 11.8, you can see the frame rate is far and away higher.

SPHEREWORLD without display lists.

Figure 11.7. SPHEREWORLD without display lists.

SPHEREWORLD with display lists.

Figure 11.8. SPHEREWORLD with display lists.

Why should you care about rendering performance? The faster and more efficient your rendering code, the more visual complexity you can add to your scene without dragging down the frame rate too much. Higher frame rates yield smoother and better-looking animations. You can also use the extra CPU time to perform other tasks such as physics calculations or lengthy I/O operations on a separate thread.

Vertex Arrays

Display lists are frequently used for precompiling sets of OpenGL commands. In our BOLT example, display lists were perhaps a bit underused because all we really encapsulated was the creation of the geometry. In the same vein, SphereWorld's many spheres required a great deal of trigonometric calculations saved by placing the geometry in display lists. You might consider that we could just as easily have created some arrays to store the vertex data for the models and thus saved all the computation time just as easily as with the display lists.

You might be right about this way of thinking—to a point. Some implementations store display lists more efficiently than others, and if all you're really compiling is the vertex data, you can simply place the model's data in one or more arrays and render from the array of precalculated geometry. The only drawback to this approach is that you must still loop through the entire array moving data to OpenGL one vertex at a time. Depending on the amount of geometry involved, taking this approach could be a substantial performance penalty. The advantage, however, is that, unlike with display lists, the geometry does not have to be static. Each time you prepare to render the geometry, some function could be applied to all the geometry data and perhaps displace or modify it in some way. For example, say a mesh used to render the surface of an ocean could have rippling waves moving across the surface. A swimming whale or jellyfish could also be cleverly modeled with deformable meshes in this way.

With OpenGL, you can, in fact, have the best of both scenarios by using vertex arrays. With vertex arrays, you can precalculate or modify your geometry on the fly, but do a bulk transfer of all the geometry data at one time. Basic vertex arrays can be almost as fast as display lists, but without the requirement that the geometry be static. It might also simply be more convenient to store your data in arrays for other reasons and thus also render directly from the same arrays (this approach could also potentially be more memory efficient).

Using vertex arrays in OpenGL involves four basic steps. First, you must assemble your geometry data in one or more arrays. You can do this algorithmically or perhaps by loading the data from a disk file. Second, you must tell OpenGL where the data is. When rendering is performed, OpenGL pulls the vertex data directly from the arrays you have specified. Third, you must explicitly tell OpenGL which arrays you are using. You can have separate arrays for vertices, normals, colors, and so on, and you must let OpenGL know which of these data sets you want to use. Finally, you execute the OpenGL commands to actually perform the rendering using your vertex data.

To demonstrate these four steps, we revisit an old sample from another chapter. We've rewritten the SMOOTHER sample from Chapter 6 for the STARFIELD sample in this chapter. The STARFIELD sample creates three arrays that contain randomly initialized positions for stars in a starry sky. We then use vertex arrays to render directly from these arrays, bypassing the glBegin/glEnd mechanism entirely. Figure 11.9 shows the output of the STARFIELD sample program, and Listing 11.6 shows the important portions of the source code.

Example 11.6. Setup and Rendering Code for the STARFIELD Sample

// Array of small stars
#define SMALL_STARS 150
GLTVector2  vSmallStars[SMALL_STARS];

#define MEDIUM_STARS   40
GLTVector2 vMediumStars[MEDIUM_STARS];

#define LARGE_STARS 15
GLTVector2 vLargeStars[LARGE_STARS];

#define SCREEN_X    800
#define SCREEN_Y    600

// This function does any needed initialization on the rendering
// context.
void SetupRC()
    {
    int i;

    // Populate star list
    for(i = 0; i < SMALL_STARS; i++)
        {
        vSmallStars[i][0] = (GLfloat)(rand() % SCREEN_X);
        vSmallStars[i][1] = (GLfloat)(rand() % (SCREEN_Y - 100))+100.0f;
        }

    // Populate star list
    for(i = 0; i < MEDIUM_STARS; i++)
        {
        vMediumStars[i][0] = (GLfloat)(rand() % SCREEN_X * 10)/10.0f;
        vMediumStars[i][1] = (GLfloat)(rand() % (SCREEN_Y - 100))+100.0f;
        }

    // Populate star list
    for(i = 0; i < LARGE_STARS; i++)
        {
        vLargeStars[i][0] = (GLfloat)(rand() % SCREEN_X*10)/10.0f;
        vLargeStars[i][1] =
                  (GLfloat)(rand() % (SCREEN_Y - 100)*10.0f)/ 10.0f +100.0f;
        }


    // Black background
    glClearColor(0.0f, 0.0f, 0.0f, 1.0f );

    // Set drawing color to white
    glColor3f(0.0f, 0.0f, 0.0f);
    }


///////////////////////////////////////////////////
// Called to draw scene
void RenderScene(void)
    {
    GLfloat x = 700.0f;     // Location and radius of moon
    GLfloat y = 500.0f;
    GLfloat r = 50.0f;
    GLfloat angle = 0.0f;   // Another looping variable

    // Clear the window
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

    // Everything is white
    glColor3f(1.0f, 1.0f, 1.0f);

    // Using vertex arrays
    glEnableClientState(GL_VERTEX_ARRAY);

    // Draw small stars
    glPointSize(1.0f);
//  This code is no longer needed
//    glBegin(GL_POINTS);
//        for(i = 0; i < SMALL_STARS; i++)
//            glVertex2fv(vSmallStars[i]);
//    glEnd();

    // Newer vertex array functionality
    glVertexPointer(2, GL_FLOAT, 0, vSmallStars);
    glDrawArrays(GL_POINTS, 0, SMALL_STARS);



    // Draw medium sized stars
    glPointSize(3.05f);
    glVertexPointer(2, GL_FLOAT, 0, vMediumStars);
    glDrawArrays(GL_POINTS, 0, MEDIUM_STARS);

    // Draw largest stars
    glPointSize(5.5f);
    glVertexPointer(2, GL_FLOAT, 0, vLargeStars);
    glDrawArrays(GL_POINTS, 0, LARGE_STARS);

    // Draw the "moon"
    glBegin(GL_TRIANGLE_FAN);
        glVertex2f(x, y);
        for(angle = 0; angle < 2.0f * 3.141592f; angle += 0.1f)
            glVertex2f(x + (float)cos(angle) * r, y + (float)sin(angle) * r);
            glVertex2f(x + r, y);
    glEnd();

    // Draw distant horizon
    glLineWidth(3.5);
    glBegin(GL_LINE_STRIP);
        glVertex2f(0.0f, 25.0f);
        glVertex2f(50.0f, 100.0f);
        glVertex2f(100.0f, 25.0f);
        glVertex2f(225.0f, 125.0f);
        glVertex2f(300.0f, 50.0f);
        glVertex2f(375.0f, 100.0f);
        glVertex2f(460.0f, 25.0f);
        glVertex2f(525.0f, 100.0f);
        glVertex2f(600.0f, 20.0f);
        glVertex2f(675.0f, 70.0f);
        glVertex2f(750.0f, 25.0f);
        glVertex2f(800.0f, 90.0f);
    glEnd();


    // Swap buffers
    glutSwapBuffers();
    }
Output from the STARFIELD program.

Figure 11.9. Output from the STARFIELD program.

Loading the Geometry

The first prerequisite to using vertex arrays is that your geometry must be stored in arrays. In Listing 11.6, you see three globally accessible arrays of two-dimensional vectors. They contain x and y coordinate locations for the three groups of stars:

// Array of small stars
#define SMALL_STARS 150
GLTVector2  vSmallStars[SMALL_STARS];

#define MEDIUM_STARS   40
GLTVector2 vMediumStars[MEDIUM_STARS];

#define LARGE_STARS 15
GLTVector2 vLargeStars[LARGE_STARS];

Recall that this sample program uses an orthographic projection and draws the stars as points at random screen locations. Each array is populated in the SetupRC function with a simple loop that picks random x and y values that fall within the portion of the window we want the stars to occupy. The following few lines from the listing show how just the small star list is populated:

// Populate star list
    for(i = 0; i < SMALL_STARS; i++)
        {
        vSmallStars[i][0] = (GLfloat)(rand() % SCREEN_X);
        vSmallStars[i][1] = (GLfloat)(rand() % (SCREEN_Y - 100))+100.0f;
        }

Enabling Arrays

In the RenderScene function, we enable the use of an array of vertices with the following code:

// Using vertex arrays
glEnableClientState(GL_VERTEX_ARRAY);

This is the first new function for using vertex arrays, and it has a corresponding disabling function:

void glEnableClientState(GLenum array);
void glDisableClientState(GLenum array);

These functions accept the following constants, turning on and off the corresponding array usage: GL_VERTEX_ARRAY, GL_COLOR_ARRAY, GL_SECONDARY_COLOR_ARRAY, GL_NORMAL_ARRAY, GL_FOG_COORDINATE_ARRAY, GL_TEXURE_COORD_ARRAY, and GL_EDGE_FLAG_ARRAY. For our STARFIELD example, we are sending down only a list of vertices. As you can see, you can also send down a corresponding array of normals, texture coordinates, colors, and so on.

Here's one question that commonly arises with the introduction of this function: Why did the OpenGL designers add a new glEnableClientState function instead of just sticking with glEnable. A good question. The reason has to do with how OpenGL is designed to operate. OpenGL was designed using a client/server model. The server is the graphics hardware, and the client is the host CPU and memory. On the PC, for example, the server would be the graphics card, and the client would be the PC's CPU and main memory. Because this state of enabled/disabled capability specifically applies to the client side of the picture, a new set of functions was derived.

Where's the Data?

Before we can actually use the vertex data, we must still tell OpenGL where to fetch the data. The following single line in the STARFIELD example does this:

glVertexPointer(2, GL_FLOAT, 0, vSmallStars);

Here, we find our next new function. The glVertexPointer function tells OpenGL where it can fetch the vertex data. There are also corresponding functions for the other types of vertex array data:

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

void glFogCoordPointer(GLenum type, GLsizei stride, const void *pointer);
void glEdgeFlagPointer(GLenum type, GLsizei stride, const void *pointer);

These functions are all closely related and take nearly identical arguments. All but the normal, fog coordinate, and edge flag functions take a size argument first. This argument tells OpenGL the number of elements that make up the coordinate type. For example, vertices can consist of 2 (x,y), 3 (x,y,z), or 4 (x,y,z,w) components. Normals, however, are always three components, and fog coordinates and edge flags are always one component; thus, it would be redundant to specify the argument for these arrays.

The type parameter specifies the OpenGL data type for the array. Not all data types are valid for all vertex array specifications. Table 11.1 lists the seven vertex array functions (index pointers are used for color index mode and are thus excluded here) and the valid data types that can be specified for the data elements.

Table 11.1. Valid Vertex Array Sizes and Data Types

Command

Elements

Valid Data Types

glColorPointer

3, 4

GL_BYTE, GL_UNSIGNED_BYTE, GL_SHORT, GL_UNSIGNED_SHORT, GL_INT, GL_UNSIGNED_INT, GL_FLOAT, GL_DOUBLE

glEdgeFlagPointer

1

None specified (always GLboolean)

glFogCoordPointer

1

GL_FLOAT, GL_DOUBLE

glNormalPointer

3

GL_BYTE, GL_SHORT, GL_INT, GL_FLOAT, GL_DOUBLE

glSecondaryColorPointer

3

GL_BYTE, GL_UNSIGNED_BYTE, GL_SHORT, GL_INT, GL_UNSIGNED_INT, GL_FLOAT, GL_DOUBLE

glTexCoordPointer

1, 2, 3, 4

GL_SHORT, GL_INT, GL_FLOAT, GL_DOUBLE

glVertexPointer

2, 3, 4

GL_SHORT, GL_INT, GL_FLOAT, GL_DOUBLE

The stride parameter specifies the space in bytes between each array element. Typically, this value is just 0, and array elements have no data gaps between values. Finally, the parameter is a pointer to the array of data. For arrays, this is simply the name of the array.

Draw!

Finally, we're ready to render using our vertex arrays. We can actually use the vertex arrays in two different ways. For illustration, first look at the nonvertex array method that simply loops through the array and passes a pointer to each array element to glVertex:

glBegin(GL_POINTS);
   for(i = 0; i < SMALL_STARS; i++)
       glVertex2fv(vSmallStars[i]);
glEnd();

Because OpenGL now knows about our vertex data, we can have OpenGL look up the vertex values for us with the following code:

glBegin(GL_POINTS);
   for(i = 0; i < SMALL_STARS; i++)
       glArrayElement(i);
glEnd();

The glArrayElement function looks up the corresponding array data from any arrays that have been enabled with glEnableClientState. If an array has been enabled, and a corresponding array has not been specified (glVertexPointer, glColorPointer, and so on), an illegal memory access will likely cause the program to crash. The advantage to using glArrayElement is that a single function call can now replace several function calls (glNormal, glColor, glVertex, and so forth) needed to specify all the data for a specific vertex. Sometimes you might want to jump around in the array in nonsequential order as well.

Most of the time, however, you will find that you are simply transferring a block of vertex data that needs to be traversed from beginning to end. In these cases (as is the case with the STARFIELD sample), OpenGL can transfer a single block of any enabled arrays with a single function call:

void glDrawArrays(GLenum mode, GLint first, GLint count);

In this function, mode specifies the primitive to be rendered (one primitive batch per function call). The first parameter specifies where in the enabled arrays to begin retrieving data, and the count parameter tells how many array elements to retrieve. In the case of the STARFIELD example, we rendered the array of small stars as follows:

glDrawArrays(GL_POINTS, 0, SMALL_STARS);

OpenGL implementations can optimize these block transfers, resulting in significant performance gains over multiple calls to the individual vertex functions such as glVertex, glNormal, and so forth.

Indexed Vertex Arrays

Indexed vertex arrays are vertex arrays that are not traversed in order from beginning to end, but are traversed in an order that is specified by a separate array of index values. This may seem a bit convoluted, but actually indexed vertex arrays can save memory and reduce transformation overhead. Under ideal conditions, they can actually be faster than display lists!

The reason for this extra efficiency is the array of vertices can be smaller than the array of indices. Adjoining primitives such as triangles can share vertices in ways not possible by just using triangle strips or fans. For example, using ordinary rendering methods or vertex arrays, there is no other mechanism to share a set of vertices between two adjacent triangle strips. Figure 11.10 shows two triangle strips that share one edge. Although triangle strips make good use of shared vertices between triangles in the strip, there is no way to avoid the overhead of transforming the vertices shared between the two strips because each strip must be specified individually.

Two triangle strips in which the vertices share an edge.

Figure 11.10. Two triangle strips in which the vertices share an edge.

Now let's look at a simple example; then we'll look at a more complex model and examine the potential savings of using indexed arrays.

A Simple Cube

In the thread example, we repeated many normals and vertices. We can save a considerable amount of memory if we can reuse a normal or vertex in a vertex array without having to store it more than once. Not only is memory saved, but also a good OpenGL implementation is optimized to transform these vertices only once, saving valuable transformation time.

Instead of creating a vertex array containing all the vertices for a given geometric object, you can create an array containing only the unique vertices for the object. Then you can use another array of index values to specify the geometry. These indices reference the vertex values in the first array. Figure 11.11 shows this relationship.

An index array referencing an array of unique vertices.

Figure 11.11. An index array referencing an array of unique vertices.

Each vertex consists of three floating-point values, but each index is only an integer value. A float and an integer are 4 bytes on most machines, which means you save 8 bytes for each reused vertex for the cost of 4 extra bytes for every vertex. For a small number of vertices, the savings might not be great; in fact, you might even use more memory using an indexed array than you would have by just repeating vertex information. For larger models, however, the savings can be substantial.

Figure 11.12 shows a cube with each vertex numbered. For our next sample program, CUBEDX, we create a cube using indexed vertex arrays.

A cube containing six unique numbered vertices.

Figure 11.12. A cube containing six unique numbered vertices.

Listing 11.7 shows the code from the CUBEDX program to render the cube using indexed vertex arrays. The six unique vertices are in the corners array, and the indices are in the indexes array. In RenderScene, we set the polygon mode to GL_LINE so that the cube is wireframed.

Example 11.7. Code from the CUBEDX Program to Use Indexed Vertex Arrays

// Array containing the six vertices of the cube
static GLfloat corners[] = { -25.0f, 25.0f, 25.0f, // 0 // Front of cube
                              25.0f, 25.0f, 25.0f, // 1
                              25.0f, -25.0f, 25.0f,// 2
                             -25.0f, -25.0f, 25.0f,// 3
                             -25.0f, 25.0f, -25.0f,// 4  // Back of cube
                              25.0f, 25.0f, -25.0f,// 5
                              25.0f, -25.0f, -25.0f,// 6
                             -25.0f, -25.0f, -25.0f };// 7

// Array of indexes to create the cube
static GLubyte indexes[] = { 0, 1, 2, 3,     // Front Face
                             4, 5, 1, 0,     // Top Face
                             3, 2, 6, 7,     // Bottom Face
                             5, 4, 7, 6,     // Back Face
                             1, 5, 6, 2,     // Right Face
                             4, 0, 3, 7 };   // Left Face

// Rotation amounts
static GLfloat xRot = 0.0f;
static GLfloat yRot = 0.0f;

// Called to draw scene
void RenderScene(void)
    {
    // Clear the window with current clearing color
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

    // Make the cube a wire frame
    glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);

    // Save the matrix state
    glMatrixMode(GL_MODELVIEW);
    glPushMatrix();
    glTranslatef(0.0f, 0.0f, -200.0f);

    // Rotate about x and y axes
    glRotatef(xRot, 1.0f, 0.0f, 0.0f);
    glRotatef(yRot, 0.0f, 0.0f, 1.0f);

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

    // Using Drawarrays
    glDrawElements(GL_QUADS, 24, GL_UNSIGNED_BYTE, indexes);

    glPopMatrix();

    // Swap buffers
    glutSwapBuffers();
    }

OpenGL has native support for indexed vertex arrays, as shown in the glDrawElements function. The key line in Listing 11.7 is

glDrawElements(GL_QUADS, 24, GL_UNSIGNED_BYTE, indexes);

This line is much like the glDrawArrays function mentioned earlier, but now we are specifying an index array that determines the order in which the enabled vertex arrays are traversed. Figure 11.13 shows the output from the program CUBEDX.

A wireframe cube drawn with an indexed vertex array.

Figure 11.13. A wireframe cube drawn with an indexed vertex array.

A variation on glDrawElement is the glDrawRangeElements function. This function is documented in the reference section and simply adds two parameters to specify the range of indices that will be valid. This hint can enable some OpenGL implementations to prefetch the vertex data, a potentially worthwhile performance optimization. A further enhancement is glMultiDrawArrays, which allows you to send multiple arrays of indices with a single function call.

One last vertex array function you'll find in the reference section is glInterleavedArrays. It allows you to combine several arrays into one aggregate array. There is no change to your access or traversal of the arrays, but the organization in memory can possibly enhance performance on some hardware implementations.

Getting Serious

With a few simple examples behind us, it's time to tackle a more sophisticated model with more vertex data. For this example, we use a model created by Full Sail student Stephen Carter, generously provided by the school's gaming department. We also use a product called Deep Exploration from Right Hemisphere that has a handy feature of exporting models as OpenGL code! A demo version of this product is available on the CD with this book. Figure 11.14 shows Deep Exploration running and displaying the model that we will be working with.

Sample model to be rendered with OpenGL.

Figure 11.14. Sample model to be rendered with OpenGL.

We had to modify the code output by Deep Exploration so that it would work with our GLUT framework and run on both the Macintosh and PC platforms. You can find the code that renders the model in the MODELTEST sample program. We do not include the entire program listing here because it is quite lengthy and mostly meaningless to human beings. It consists of a number of arrays representing 2,248 individual triangles (that's a lot of numbers to stare at!).

The approach taken with this tool is to produce the smallest possible amount of code to represent the given model. Deep Exploration has done an excellent job of compacting the data. There are 2,248 individual triangles, but using a clever indexing scheme, Deep Exploration has encoded this as only 1,254 individual vertices, 1,227 normals, and 2,141 texture coordinates. The following code shows the DrawModel function, which loops through the index set and sends OpenGL the texture, normal, and vertex coordinates for each individual triangle:

void DrawModel(void)
    {
    int iFace, iPoint;
    glBegin(GL_TRIANGLES);
        for(iFace = 0; iFace < 2248; iFace++)  // Each new triangle starts here
            for(iPoint = 0; iPoint < 3; iPoint++) // Each vertex specified here
                {
                // Lookup the texture value
                glTexCoord2fv(textures[face_indices[iFace][iPoint+6]]);

                // Lookup the normal value
                glNormal3fv(normals[face_indices[iFace][iPoint+3]]);

                // Lookup the vertex value
                glVertex3fv(vertices[face_indices[iFace][iPoint]]);
                }
    glEnd();
    }

This approach is ideal when you must optimize the storage size of the model data—for example, to save memory in an embedded application, reduce storage space, or reduce bandwidth if the model must be transmitted over a network. However, for real-time applications where performance considerations can sometimes outweigh memory constraints, this code would perform quite poorly because once again you are back to square one, sending vertex data to OpenGL one vertex at a time.

The simplest and perhaps most obvious approach to speeding up this code is simply to place the DrawModel function in a display list. Indeed, this is the approach we used in the MODELTEST program that renders this model. Let's look at the cost of this approach and compare it to rendering the same model with indexed vertex arrays.

Measuring the Cost

First, we calculate the amount of memory required to store the original compacted vertex data. We can do this simply by looking at the declarations of the data arrays and knowing how large the base data type is:

static short face_indices[2248][9] = { ...

static GLfloat vertices [1254][3] = { ...

static GLfloat normals [1227][3] = { ...

static GLfloat textures [2141][2] = { ...

The memory for face_indices would be sizeof(short) × 2,248 × 9, which works out to 40,464 bytes. Similarly, we calculate the size of vertices, normals, and textures as 15,048, 14,724, and 17,128 bytes, respectively. This gives us a total memory footprint of 87,364 bytes or about 85KB.

But wait! When we draw the model into the display list, we copy all this data again into the display list, except that now we decompress our packed data so that many vertices are duplicated for adjacent triangles. We, in essence, undo all the work to optimize the storage of the geometry to draw it. We can't calculate exactly how much space the display list takes, but we can get a good estimate by calculating just the size of the geometry. There are 2,248 triangles. Each triangle has three vertices, each of which has a floating-point vertex (three floats), normal (three floats), and texture coordinate (two floats). Assuming four bytes for a float (sizeof(float)), we calculate this as follows:

  • 2,248 (triangle) × 3 (vertices) = 6,744 vertices.

Each vertex has three components (x, y, z):

  • 6,744 × 3 = 20,232 floating-point values for geometry.

Each vertex has a normal, meaning three more components:

  • 6,744 × 3 = 20,232 floating-point values for normals.

Each vertex has a texture, meaning two more components:

  • 6,744 × 2 = 13,488 floating-point values for texture coordinates.

This gives a total of 53,952 floats, at 4 bytes each = 215,808 bytes.

Total memory for the display list data and the original data is 311,736 bytes, just a tad more than 300KB. But don't forget the transformation cost—6,744 (2,248 × 3) vertices must be transformed by the OpenGL geometry pipeline. That's a lot of matrix multiplies!

Creating a Suitable Indexed Array

Just because the data in the MODELTEST sample is stored in arrays does not mean the data is ready to be used as any kind of OpenGL vertex array. In OpenGL, the vertex array, normal array, texture array, and any other arrays that you want to use must all be the same size. The reason is that all the array elements across arrays must be shared. For ordinary vertex arrays, as you march through the set of arrays, array element 0 from the vertex array must go with array element 0 from the normal array, and so on. For indexed arrays, we have the same limitation. Each index must address all the enabled arrays at the same corresponding array element.

For the sample program MODELIVA, we wrote a function that goes through the existing vertex array and reindexes the triangles so that all three arrays are the same size and all array elements correspond exactly one to another. The pertinent code is given in Listing 11.8.

Example 11.8. Code to Create a New Indexed Vertex Array

////////////////////////////////////////////////////////////////
// These are hard coded for this particular example
GLushort uiIndexes[2248*3];   // Maximum number of indexes
GLfloat vVerts[2248*3][3];  // (Worst case scenario)
GLfloat vText[2248*3][2];
GLfloat vNorms[2248*3][3];
int iLastIndex = 0;         // Number of indexes actually used



/////////////////////////////////////////////////////////////////
// Compare two floating point values and return true if they are
// close enough together to be considered the same.
inline bool IsSame(float x, float y, float epsilon)
    {
    if(fabs(x-y) < epsilon)
        return true;

    return false;
    }


///////////////////////////////////////////////////////////////
// Goes through the arrays and looks for duplicate vertices
// that can be shared. This expands the original array somewhat
// and returns the number of true unique vertices that now
// populate the vVerts array.
int IndexTriangles(void)
    {
    int iFace, iPoint, iMatch;
    float e = 0.000001; // How small a difference to equate

    // LOOP THROUGH all the faces
    int iIndexCount = 0;
    for(iFace = 0; iFace < 2248; iFace++)
        {
        for(iPoint = 0; iPoint < 3; iPoint++)
            {
            // Search for match
            for(iMatch = 0; iMatch < iLastIndex; iMatch++)
                {
                // If Vertex is the same...
                if(IsSame(vertices[face_indices[iFace][iPoint]][0],
                                                       vVerts[iMatch][0], e) &&
                   IsSame(vertices[face_indices[iFace][iPoint]][1],
                                                       vVerts[iMatch][1], e) &&
                   IsSame(vertices[face_indices[iFace][iPoint]][2],
                                                       vVerts[iMatch][2], e) &&

                   // AND the Normal is the same...
                   IsSame(normals[face_indices[iFace][iPoint+3]][0],
                                                       vNorms[iMatch][0], e) &&
                   IsSame(normals[face_indices[iFace][iPoint+3]][1],
                                                       vNorms[iMatch][1], e) &&
                   IsSame(normals[face_indices[iFace][iPoint+3]][2],
                                                       vNorms[iMatch][2], e) &&

                   // And Texture is the same...
                   IsSame(textures[face_indices[iFace][iPoint+6]][0],
                                                        vText[iMatch][0], e) &&
                   IsSame(textures[face_indices[iFace][iPoint+6]][1],
                                                       vText[iMatch][1], e))
                    {
                    // Then add the index only
                    uiIndexes[iIndexCount] = iMatch;
                    iIndexCount++;
                    break;
                    }
                }

            // No match found, add this vertex to the end of our list,
            // and update the index array
            if(iMatch == iLastIndex)
                {
                // Add data and new index
                memcpy(vVerts[iMatch], vertices[face_indices[iFace][iPoint]],
                                                            sizeof(float) * 3);
                memcpy(vNorms[iMatch], normals[face_indices[iFace][iPoint+3]],
                                                            sizeof(float) * 3);
                memcpy(vText[iMatch],  textures[face_indices[iFace][iPoint+6]],
                                                            sizeof(float) * 2);
                uiIndexes[iIndexCount] = iLastIndex;
                iIndexCount++;
                iLastIndex++;
                }
            }
        }
    return iIndexCount;
     }

/////////////////////////////////////////////
// Function to stitch the triangles together
// and draw the vehicle
void DrawModel(void)
    {
    static int iIndexes = 0;
    char cBuffer[32];


    // The first time this is called, reindex the triangles. Report the results
    // in the window title
    if(iIndexes == 0)
        {
        iIndexes = IndexTriangles();
        sprintf(cBuffer,"Verts = %d Indexes = %d", iLastIndex, iIndexes);
        glutSetWindowTitle(cBuffer);
        }

    // Use vertices, normals, and texture coordinates
    glEnableClientState(GL_VERTEX_ARRAY);
    glEnableClientState(GL_NORMAL_ARRAY);
    glEnableClientState(GL_TEXTURE_COORD_ARRAY);

    // Here's where the data is now
    glVertexPointer(3, GL_FLOAT,0, vVerts);
    glNormalPointer(GL_FLOAT, 0, vNorms);
    glTexCoordPointer(2, GL_FLOAT, 0, vText);

    // Draw them
    glDrawElements(GL_TRIANGLES, iIndexes, GL_UNSIGNED_SHORT, uiIndexes);
    }

First, we need to declare storage for our new indexed vertex array. Because we don't know ahead of time what our savings will be, or indeed whether there will be any savings, we allocate a block of arrays assuming the worst case scenario. If each vertex is unique, three floats for each vertex (which is 2,248 faces × 3 vertices each):

GLushort uiIndexes[2248*3];   // Maximum number of indexes
GLfloat vVerts[2248*3][3];  // (Worst case scenario)
GLfloat vText[2248*3][2];
GLfloat vNorms[2248*3][3];
int iLastIndex = 0;         // Number of indexes actually used

Looking for duplicates requires us to test many floating-point values for equality. This is usually a no-no because floats are notoriously noisy; their values can float around and vary slightly (forgive the pun!). You frequently can solve this problem by writing a special function that simply subtracts two floats and seeing whether the difference is small enough to call it even:

inline bool IsSame(float x, float y, float epsilon)
    {
    if(fabs(x-y) < epsilon)
        return true;

    return false;
    }

The IndexTriangles function is called only once; it goes through the existing array looking for duplicate vertices. For a vertex to be shared, all the vertex, normal, and texture coordinates must be exactly the same. If a match is found, that vertex is simply referenced in the new index array. If not, it is added to the end of the array of unique vertices and then referenced in the index array.

In the DrawModel function, the IndexTriangles function is called (only once), and the window caption reports how many unique vertices were identified and how many indices are needed to traverse the list of triangles:

// The first time this is called, reindex the triangles. Report the results
// in the window title
if(iIndexes == 0)
    {
    iIndexes = IndexTriangles();
    sprintf(cBuffer,"Verts = %d Indexes = %d", iLastIndex, iIndexes);
    glutSetWindowTitle(cBuffer);
    }

From here, rendering is straightforward. You enable the three sets of arrays:

// Use vertices, normals, and texture coordinates
glEnableClientState(GL_VERTEX_ARRAY);
glEnableClientState(GL_NORMAL_ARRAY);
glEnableClientState(GL_TEXTURE_COORD_ARRAY);

Next, you tell OpenGL where the data is:

// Here's where the data is now
glVertexPointer(3, GL_FLOAT,0, vVerts);
glNormalPointer(GL_FLOAT, 0, vNorms);
glTexCoordPointer(2, GL_FLOAT, 0, vText);

Then you fire off all the triangles:

// Draw them
glDrawElements(GL_TRIANGLES, iIndexes, GL_UNSIGNED_SHORT, uiIndexes);

Avoiding triangles for rendering might appear strange because they don't have the shared vertex advantage of strips and fans. However, with indexed vertex arrays, we can go back to large batches of triangles and still have the advantage of multiple shared vertex data—perhaps even beating the efficiency offered by strips and fans.

The final output of MODELIVA is shown in Figure 11.15.

A model rendered with indexed vertex arrays.

Figure 11.15. A model rendered with indexed vertex arrays.

Comparing the Cost

Now let's compare the cost of our two methods of rendering this model. From the output of the MODELIVA program, we see that 3,193 unique vertices were found; they can also share normals and texture coordinates. Rendering the entire model requires 6,744 indices still (this should come as no surprise!).

Each vertex has three components (x,y,z):

  • 3,193 vertices × 3 = 9,579 floats

Each normal also has three components:

  • 3,193 normals × 3 = 9,579 floats

Each texture coordinate has two components:

  • 3,193 texture coordinates × 2 = 6,386 floats

Multiplying each float by 4 bytes yields a memory overhead of 102,176 bytes. We still need to add in the index array of shorts. That's 6,744 elements times 2 bytes each = 13,488. This gives a grand total storage overhead of 115,664 bytes.

Table 11.2 shows these values side by side.

Table 11.2. Memory and Transformation Overhead for Three Rendering Methods

Rendering Mode

Memory

Vertices

Immediate Mode

95KB

6,744

Display List

300KB

6,744

Indexed Vertex Array

112KB

3,193

You can see that the immediate mode rendering used by the code output by Deep Exploration certainly has the smallest memory footprint. However, this transfers the geometry to OpenGL very slowly. If you put the immediate mode code into a display list, the geometry transfer takes place much faster, but the memory overhead for the model soars to three times that originally required. The indexed vertex array seems a good compromise at just more than twice the memory footprint, but less than half the transformation cost.

Of course, in this example, we actually allocated a much larger buffer to hold the maximum number of vertices that may have been required. In a production program, you might have tools that take this calculated indexed array and write it out to disk with a header that describes the required array dimensions. Reading this model back into the program then is a simple implementation of a basic model loader. The loaded model is then exactly in the format required by OpenGL.

Models with sharp edges and corners often have fewer vertices that are candidates for sharing. However, models with large smooth surface areas can stand to gain even more in terms of memory and transformation savings. With the added savings of less geometry to move through memory, and the corresponding savings in mathematical operations, indexed vertex arrays can sometimes dramatically outperform display lists, even for static geometry. For many real-time applications, indexed vertex arrays are often the method of choice for geometric rendering.

Summary

In this chapter, we slowed down the pace somewhat and just explained how to build a three-dimensional object, starting with using the OpenGL primitives to create simple 3D pieces and then assembling them into a larger and more complex object. Learning the API is the easy part, but your level of experience in assembling 3D objects and scenes will be what differentiates you from your peers. After you break down an object or scene into small and potentially reusable components, you can save building time by using display lists. You'll find many more functions for utilizing and managing display lists in the reference section.

The last half of the chapter was concerned not with how to organize your objects, but how to organize the geometry data used to construct these objects. By packing all the vertex data together in a single data structure (an array), you enable the OpenGL implementation to make potentially valuable performance optimizations. In addition, you can stream the data to disk and back, thus storing the geometry in a format that is ready for use in OpenGL. Although OpenGL does not have a “model format” as some higher level APIs do, the vertex array construct is certainly a good place to start if you want to build your own.

Generally, you can significantly speed up static geometry by using display lists, and you can use vertex arrays whenever you want dynamic geometry. Index vertex arrays, on the other hand, can potentially (but not always) give you the best of both worlds—flexible geometry data and highly efficient memory transfer and geometric processing. For many applications, vertex arrays are used almost exclusively. However, the old glBegin/glEnd construct still has many uses, besides allowing you to create display lists—any time the amount of geometry fluctuates dynamically from frame to frame, for example. There is little benefit to continually rebuilding a vertex array from scratch rather than letting the driver do the work with glBegin/glEnd.

Reference

glArrayElement

Purpose:

Specifies an array element used to render a vertex.

Include File:

<gl.h>

Syntax:

void glArrayElement(GLint index);

Description:

You use this function with a glBegin/glEnd pair to specify vertex data. The indexed element from any enabled vertex arrays are passed to OpenGL as part of the primitive definition.

Parameters:

index

GLintThe index of the array element to use.

Returns:

None.

See Also:

glDrawArrays, glDrawElements, glDrawRangeElements, glInterleavedArrays

glCallList

Purpose:

Executes a display list.

Include File:

<gl.h>

Syntax:

void glCallList(GLuint list);

Description:

This function executes the display list identified by list. The OpenGL state machine is not restored after this function is called, so it is a good idea to call glPushMatrix beforehand and glPopMatrix afterward. Calls to glCallList can be nested. The glGet function with the GL_MAX_LIST_NESTING argument returns the maximum number of allowable nests. For Microsoft Windows, this value is 64.

Parameters:

list

GLuintIdentifies the display list to be executed.

Returns:

None.

See Also:

glCallLists, glDeleteLists, glGenLists, glNewList

glCallLists

Purpose:

Executes a list of display lists.

Include File:

<gl.h>

Syntax:

void glCallLists(GLsizei n, GLenum type, const 
GLuint:GLvoid *lists);

Description:

This function calls the display lists listed in the *lists array sequentially. This array can be of nearly any data type. The result is converted or clamped to the nearest integer value to determine the actual index of the display list. Optionally, the list values can be offset by a value specified by the glListBase function.

Parameters:

n

GLsizeiThe number of elements in the array of display lists.

type

GLenumThe data type of the array stored at *lists. It can be any one of the following values: GL_BYTE, GL_UNSIGNED_BYTE, GL_SHORT, GL_UNSIGNED_SHORT, GL_INT, GL_UNSIGNED_INT, GL_FLOAT, GL_2_BYTES, GL_3_BYTES, and GL_4_BYTES.

*lists

GLvoidAn array of elements of the type specified in type. The data type is void to allow any of the preceding data types to be used.

Returns:

None.

See Also:

glCallList, glDeleteLists, glGenLists, glListBase, glNewList

glColorPointer

Purpose:

Defines an array of color data for OpenGL vertex array functionality.

Include File:

<gl.h>

Syntax:

void glColorPointer(GLint size, GLenum type, 
GLvoid:GLsizei stride, const GLvoid *pointer);

Description:

This function defines the location, organization, and type of data to be used for vertex color data when OpenGL is using the vertex array functions. The buffer pointed to by this function can contain dynamic data but must remain valid data. The data is read afresh from the vertex array buffer supplied here whenever OpenGL evaluates vertex arrays.

Parameters:

size

GLintThe number of components per color. Valid values are 3 and 4.

type

GLenumThe data type of the array. It can be any of the valid OpenGL data types for color component data: GL_BYTE, GL_UNSIGNED_BYTE, GL_SHORT, GL_UNSIGNED_SHORT, GL_INT, GL_UNSIGNED_INT, GL_FLOAT, and GL_DOUBLE.

stride

GLsizeiThe byte offset between colors in the array. A value of 0 indicates that the data is tightly packed.

pointer

GLvoid*A pointer that specifies the location of the beginning of the vertex array data.

Returns:

None.

See Also:

glVertexPointer, glNormalPointer, glTexCoordPointer, glEdgeFlagPointer, glFogCoordPointer, glInterleavedArrays

glDeleteLists

Purpose:

Deletes a continuous range of display lists.

Include File:

<gl.h>

Syntax:

void glDeleteLists(GLuint list, GLsizei range);

Description:

This function deletes a range of display lists. The range goes from an initial value and proceeds until the number of lists deleted as specified by range is completed. Deleting unused display lists can save considerable memory. Unused display lists in the range of those specified are ignored and do not cause an error.

Parameters:

list

GLuintThe integer name of the first display list to delete.

range

GLsizeiThe number of display lists to be deleted following the initially specified list.

Returns:

None.

See Also:

glCallList, glCallLists, glGenLists, glIsList, glNewList

glDrawArrays

Purpose:

Creates a sequence of primitives from any enabled vertex arrays.

Include File:

<gl.h>

Syntax:

void glDrawArrays(GLenum mode, GLint first, 
GLsizei:GLsizei count);

Description:

This function enables you to render a series of primitives using the data in the currently enabled vertex arrays. The function takes the primitive type and processes all the vertices within the specified range.

Parameters:

mode

GLenumThe kind of primitive to render. It can be any of the valid OpenGL primitive types: GL_POINTS, GL_LINES, GL_LINE_LOOP, GL_LINE_STRIP, GL_TRIANGLES, GL_TRIANGLE_STRIP, GL_TRIANGLE_FAN, GL_QUADS, GL_QUAD_STRIP, and GL_POLYGON

first

GLintThe first index of the enabled arrays to use.

count

GLsizeiThe number of indices to use.

Returns:

None.

See Also:

glDrawElements, glDrawRangeElements, glInterleavedArrays

glDrawElements

Purpose:

Renders primitives from array data, using an index into the array.

Include File:

<gl.h>

Syntax:

void glDrawElements(GLenum mode, GLsizei count, 
GLsizei:GLenum type, GLvoid *pointer);

Description:

Rather than traverse the array data sequentially, this function traverses an index array sequentially. This index array typically accesses the vertex data in a nonsequential and often repetitious way, allowing for shared vertex data.

Parameters:

mode

GLenumThe primitive type to be rendered. It can be GL_POINTS, GL_LINES, GL_LINE_LOOP, GL_LINE_STRIP, GL_TRIANGLES, GL_TRIANGLE_FAN, GL_TRIANGLE_STRIP, GL_QUAD, GL_QUAD_STRIP, or GL_POLYGON.

count

GLsizeiThe byte offset between coordinates in the array. A value of 0 indicates that the data is tightly packed.

type

GLenumThe type of data used in the index array. It can be any one of GL_UNSIGNED_BYTE, GL_UNSIGNED_SHORT, or GL_UNSIGNED_INT.

pointer

GLvoid*A pointer that specifies the location of the index array.

Returns:

None.

See Also:

glArrayElement, glDrawArrays, glDrawRangeElements, glDrawMultiRangeElements

glDrawRangeElements

Purpose:

Renders primitives from array data, using an index into the array and a specified range of valid index values.

Include File:

<gl.h>

Syntax:

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

Description:

Rather than traverse the array data sequentially, this function traverses an index array sequentially. This index array typically accesses the vertex data in a nonsequential and often repetitious way, allowing for shared vertex data. In addition to this shared functionality with glDrawElements, this function takes a range of valid index values. Some OpenGL implementations can use this information to prefetch the vertex data for higher performance.

Parameters:

mode

GLenumThe primitive type to be rendered. It can be GL_POINTS, GL_LINES, GL_LINE_LOOP, GL_LINE_STRIP, GL_TRIANGLES, GL_TRIANGLE_FAN, GL_TRIANGLE_STRIP, GL_QUAD, GL_QUAD_STRIP, or GL_POLYGON.

start

GLintThe first index of the index range that will be used.

end

GLintThe last index of the index range that will be used.

count

GLsizeiThe byte offset between coordinates in the array. A value of 0 indicates that the data is tightly packed.

type

GLenumThe type of data used in the index array. It can be any one of GL_UNSIGNED_BYTE, GL_UNSIGNED_SHORT, or GL_UNSIGNED_INT.

pointer

GLvoid*A pointer that specifies the location of the index array.

Returns:

None.

See Also:

glArrayElement, glDrawArrays, glDrawElements, glDrawMultiRangeElements

glEdgeFlagPointer

Purpose:

Defines an array of edge flags for OpenGL vertex array functionality.

Include File:

<gl.h>

Syntax:

void glEdgeFlagPointer(GLsizei stride, const 
GLvoid*:GLvoid *pointer);

Description:

This function defines the location of data to be used for the edge flag array when OpenGL is using the vertex array functions. The buffer pointed to by this function can contain dynamic data but must remain valid data. The data is read afresh from the vertex array buffer supplied here whenever OpenGL evaluates vertex arrays. Note that there is no type argument as in the other vertex array pointer functions. The data type for edge flags must be GLboolean.

Parameters:

stride

GLsizeiThe byte offset between edge flags in the array. A value of 0 indicates that the data is tightly packed.

pointer

GLvoid*A pointer that specifies the location of the beginning of the vertex array data.

Returns:

None.

See Also:

glColorPointer, glNormalPointer, glTexCoordPointer, glVertexPointer, glFogCoordPointer, glEdgeFlagPointer, glSecondaryColorPointer

glEnableClientState/glDisableClientState

Purpose:

Specify the array type to enable or disable for use with OpenGL vertex arrays.

Include File:

<gl.h>

Syntax:

void glEnableClientState(GLenum array);
void glDisableClientState(GLenum array);

Description:

These functions tell OpenGL that you will or will not be specifying vertex arrays for geometry definitions. Each array type can be enabled or disabled individually. The use of vertex arrays does not preclude use of the normal glVertex family of functions. The specification of vertex arrays cannot be stored in a display list.

Parameters:

array

GLenumThe name of the array to enable or disable. Valid values are GL_VERTEX_ARRAY, GL_COLOR_ARRAY, GL_SECONDARY_COLOR_ARRAY, GL_NORMAL_ARRAY, GL_FOG_COORDINATE_ARRAY, GL_TEXTURE_COORD_ARRAY, and GL_EDGE_FLAG_ARRAY.

Returns:

None.

See Also:

glVertexPointer, glNormalPointer, glTexCoordPointer, glColorPointer, glEdgeFlagPointer, glSecondaryColorPointer, glFogCoordPointer

glEndList

Purpose:

Delimits the end of a display list.

Include File:

<gl.h>

Syntax:

void glEndList( void);

Description:

Display lists are created by first calling glNewList. Thereafter, all OpenGL commands are compiled and placed in the display list. The glEndList function terminates the creation of this display list.

Returns:

None.

See Also:

glCallList, glCallLists, glDeleteLists, glGenLists, glIsList

glFogCoordPointer

Purpose:

Defines an array of fog coordinates for OpenGL vertex array functionality.

Include File:

<gl.h>

Syntax:

void glFogCoordPointer(GLenum type, GLsizei stride
GLenum:, const GLvoid *pointer);

Description:

This function defines the location, organization, and type of data to be used for fog coordinates when OpenGL is using the vertex array functions. The buffer pointed to by this function can contain dynamic data but must remain valid data. The data is read afresh from the vertex array buffer supplied here whenever OpenGL evaluates vertex arrays.

Parameters:

type

GLenumThe data type of the array. It can be any of the valid OpenGL data types for color component data: GL_BYTE, GL_UNSIGNED_BYTE, GL_SHORT, GL_UNSIGNED_SHORT, GL_INT, GL_UNSIGNED_INT, GL_FLOAT, and GL_DOUBLE.

stride

GLsizeiThe byte offset between colors in the array. A value of 0 indicates that the data is tightly packed.

pointer

GLvoid*A pointer that specifies the location of the beginning of the vertex array data.

Returns:

None.

See Also:

glColorPointer, glSecondaryColorPointer, glNormalPointer, glTexCoordpointer, glEdgeFlagPointer

glGenLists

Purpose:

Generates a continuous range of empty display lists.

Include File:

<gl.h>

Syntax:

GLuint glGenLists(GLsizei range);

Description:

This function creates a range of empty display lists. The number of lists generated depends on the value specified in range. The return value is then the first display list in this range of empty display lists. The purpose of this function is to reserve a range of display list values for future use.

Parameters:

range

GLsizeiThe number of empty display lists requested.

Returns:

The first display list of the range requested. The display list values following the return value up to range –1 are created empty.

See Also:

glCallList, glCallLists, glDeleteLists, glNewList

glInterleavedArrays

Purpose:

Enables and disables multiple vertex arrays simultaneously and specifies an address that points to all the vertex data contained in one aggregate array.

Include File:

<gl.h>

Syntax:

void glInterleavedArrays(GLenum format, GLsizei 
GLsizei:stride, GLvoid *pointer);

Description:

Similar to the glXXXPointer functions, this function enables and disables several vertex arrays simultaneously. All the enabled arrays are interleaved together in one aggregate array. This functionality could be achieved by careful use of the stride parameter in the other vertex array functions, but this function saves several steps and can be optimized by the OpenGL implementation.

Parameters:

format

GLenumThe packing format of the vertex data in the interleaved array. It can be any one of the values shown in Table 11.3.

stride

GLsizeiThe byte offset between coordinates in the array. A value of 0 indicates that the data is tightly packed.

pointer

GLvoid*A pointer that specifies the location of the interleaved array.

Returns:

None.

Table 11.3. Supported Interleaved Vertex Array Formats

Format

Details

GL_V2F

Two GL_FLOAT values for the vertex data.

GL_V3F

Three GL_FLOAT values for the vertex data.

GL_C4UB_V2F

Four GL_UNSIGNED_BYTE values for color data and two GL_FLOAT values for the vertex data.

GL_C4UB_V3F

Four GL_UNSIGNED_BYTE values for color data and three GL_FLOAT values for vertex data.

GL_C3F_V3F

Three GL_FLOAT values for color data and three GL_FLOAT values for vertex data.

GL_N3F_V3F

Three GL_FLOAT values for normal data and three GL_FLOAT values for vertex data.

GL_C4F_N3F_V3F

Four GL_FLOAT values for color data, three GL_FLOAT values for normal data, and three GL_FLOAT values for vertex data.

GL_T2F_V3F

Two GL_FLOAT values for texture coordinates, three GL_FLOAT values for vertex data.

GL_T4F_V4F

Four GL_FLOAT values for texture coordinates and four GL_FLOAT values for vertex data.

GL_T2F_C4UB_V3F

Two GL_FLOAT values for texture coordinates, four GL_UNSIGNED_BYTE values for color data, and three GL_FLOAT values for vertex data.

GL_T2F_C3F_V3F

Two GL_FLOAT values for texture data, three GL_FLOAT values for color data, and three GL_FLOAT values for vertex data.

GL_T2F_N3F_V3F

Two GL_FLOAT values for texture coordinates, three GL_FLOAT values for normals, and three GL_FLOAT values for vertex data.

GL_T2F_C4F_N3F_V3F

Two GL_FLOAT values for texture coordinates, four GL_FLOAT values for color data, three GL_FLOAT values for normals, and three GL_FLOAT values for vertex data.

GL_T4F_C4F_N3F_V4F

Four GL_FLOAT values for texture coordinates, four GL_FLOAT values for colors, three GL_FLOAT values for normals, and four GL_FLOAT for vertex data.

See Also:

glColorPointer, glEdgeFlagPointer, glSecondaryColorPointer, glFogCoordPointer, glNormalPointer, glTexCoordPointer, glVertexPointer

glIsList

Purpose:

Tests for the existence of a display list.

Include File:

<gl.h>

Syntax:

GLboolean glIsList(GLuint list);

Description:

This function enables you to find out whether a display list exists for a given identifier. You can use this function to test display list values before using them.

Parameters:

 

list

GLuintThe value of a potential display list. This function tests this value to see whether a display list is defined for it.

 

Returns:

GL_TRUE if the display list exists; otherwise, GL_FALSE.

 

See Also:

glCallList, glCallLists, glDeleteLists, glGenLists, glNewList

 

glListBase

Purpose:

Specifies an offset to be added to the list values specified in a call to glCallLists.

Include File:

<gl.h>

Syntax:

void glListBase(GLuint base);

Description:

The glCallLists function calls a series of display lists listed in an array. This function sets an offset value that can be added to each display list name for this function. By default, this value is 0. You can retrieve the current value by calling glGet(GL_LIST_BASE).

Parameters:

base

GLuintSets an integer offset value that will be added to display list names specified in calls to glCallLists. This value is 0 by default.

Returns:

None.

See Also:

glCallLists

glMultiDrawElements

Purpose:

Renders primitives from multiple arrays of data, using an array of indices into the arrays.

Include File:

<gl.h>

Syntax:

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

Description:

This function has the effect of multiple calls to glDrawElements. For each set of primitives, an array is passed in the count parameter that specifies the number of array elements for each primitive batch. The indices array contains an array of arrays; each array is the corresponding element array for each primitive batch.

Parameters:

mode

GLenumThe primitive type to be rendered. It can be GL_POINTS, GL_LINES, GL_LINE_LOOP, GL_LINE_STRIP, GL_TRIANGLES, GL_TRIANGLE_FAN, GL_TRIANGLE_STRIP, GL_QUAD, GL_QUAD_STRIP, or GL_POLYGON.

count

GLsizei*An array of the number of vertices contained in each array of elements.

type

GLenumThe type of data used in the index array. It can be any one of GL_UNSIGNED_BYTE, GL_UNSIGNED_SHORT, or GL_UNSIGNED_INT.

indices

GLvoid**An array of pointers to lists of array elements.

primcount

GLsizeiThe number of arrays of elements contained by the count and indices arrays.

Returns:

None.

See Also:

glDrawElements, glDrawRangeElements

glNewList

Purpose:

Begins the creation or replacement of a display list.

Include File:

<gl.h>

Syntax:

void glNewList(GLuint list, GLenum mode);

Description:

A display list is a group of OpenGL commands that are stored for execution on command. You can use display lists to speed up drawings that are computationally intensive or that require data to be read from a disk. The glNewList function begins a display list with an identifier specified by the integer list parameter. The display list identifier is used by glCallList and glCallLists to refer to the display list. If it's not unique, a previous display list may be overwritten. You can use glGenLists to reserve a range of display list names and glIsList to test a display list identifier before using it. Display lists can be compiled only or compiled and executed. After glNewList is called, all OpenGL commands are stored in the display list in the order they were issued until glEndList is called. The following commands are executed when called and are never stored in the display list itself: glIsList, glGenLists, glDeleteLists, glFeedbackBuffer, glSelectBuffer, glRenderMode, glReadPixels, glPixelStore, glFlush, glFinish, glIsEnabled, and glGet.

Parameters:

list

GLuintThe numerical name of the display list. If the display list already exists, it is replaced by the new display list.

mode

GLenumDisplay lists may be compiled and executed later or compiled and executed simultaneously. Specify GL_COMPILE to only compile the display list or GL_COMPILE_AND_EXECUTE to execute the display list as it is being compiled.

Returns:

None.

See Also:

glCallList, glCallLists, glDeleteLists, glGenLists, glIsList

glNormalPointer

Purpose:

Defines an array of normals for OpenGL vertex array functionality.

Include File:

<gl.h>

Syntax:

void glNormalPointer(GLenum type, GLsizei stride, 
GLenum:const GLvoid *pointer);

Description:

This function defines the location, organization, and type of data to be used for vertex normals when OpenGL is using the vertex array functions. The buffer pointed to by this function can contain dynamic data but must remain valid data. The data is read afresh from the vertex array buffer supplied here whenever OpenGL evaluates vertex arrays.

Parameters:

type

GLenumThe data type of the array. It can be any of the valid OpenGL data types for vertex normals: GL_BYTE, GL_SHORT, GL_INT, GL_FLOAT, and GL_DOUBLE.

stride

GLsizeiThe byte offset between normals in the array. A value of 0 indicates that the data is tightly packed.

pointer

GLvoid*A pointer that specifies the location of the beginning of the vertex normal array data.

Returns:

None.

See Also:

glColorPointer, glVertexPointer, glTexCoordPointer, glEdgeFlagPointer, glInterleavedArrays, glSecondaryColorPointer

glSecondaryColorPointer

Purpose:

Defines an array of secondary color data for OpenGL vertex array functionality.

Include File:

<gl.h>

Syntax:

void glSecondaryColorPointer(GLint size, GLenum 
GLvoid*:type, GLsizei stride,
                                    const GLvoid
GLvoid*: *pointer);

Description:

This function defines the location, organization, and type of data to be used for vertex secondary color data when OpenGL is using the vertex array functions. The buffer pointed to by this function can contain dynamic data but must remain valid data. The data is read afresh from the vertex array buffer supplied here whenever OpenGL evaluates vertex arrays.

Parameters:

size

GLintThe number of components per color. The only valid value is 3.

type

GLenumThe data type of the array. It can be any of the valid OpenGL data types for color component data: GL_BYTE, GL_UNSIGNED_BYTE, GL_SHORT, GL_UNSIGNED_SHORT, GL_INT, GL_UNSIGNED_INT, GL_FLOAT, and GL_DOUBLE.

stride

GLsizeiThe byte offset between colors in the array. A value of 0 indicates that the data is tightly packed.

pointer

GLvoid*A pointer that specifies the location of the beginning of the vertex array data.

Returns:

None.

See Also:

glColorPointer, glFogCoordPointer, glNormalPointer, glTexCoordPointer, glEdgeFlagPointer

glTexCoordPointer

Purpose:

Defines an array of texture coordinates for OpenGL vertex array functionality.

Include File:

<gl.h>

Syntax:

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

Description:

This function defines the location, organization, and type of data to be used for texture coordinates when OpenGL is using the vertex array functions. The buffer pointed to by this function can contain dynamic data but must remain valid data. The data is read afresh from the vertex array buffer supplied here whenever OpenGL evaluates vertex arrays.

Parameters:

size

GLintThe number of coordinates per array element. Valid values are 1, 2, 3, and 4.

type

GLenumThe data type of the array. It can be any of the valid OpenGL data types for texture coordinates: GL_SHORT, GL_INT, GL_FLOAT, and GL_DOUBLE.

stride

GLsizeiThe byte offset between coordinates in the array. A value of 0 indicates that the data is tightly packed.

pointer

GLvoid*A pointer that specifies the location of the beginning of the vertex array data.

Returns:

None.

See Also:

glColorPointer, glNormalPointer, glSecondaryColorPointer, glVertexPointer, glEdgeFlagPointer, glInterleavedArrays

glVertexPointer

Purpose:

Defines an array of vertex data for OpenGL vertex array functionality.

Include File:

<gl.h>

Syntax:

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

Description:

This function defines the location, organization, and type of data to be used for vertex data when OpenGL is using the vertex array functions. The buffer pointed to by this function can contain dynamic data but must remain valid data. The data is read afresh from the vertex array buffer supplied here whenever OpenGL evaluates vertex arrays.

Parameters:

size

GLintThe number of vertices per coordinate. Valid values are 2, 3, and 4.

type

GLenumThe data type of the array. It can be any of the valid OpenGL data types for vertex data: GL_SHORT, GL_INT, GL_FLOAT, and GL_DOUBLE.

stride

GLsizeiThe byte offset between vertices in the array. A value of 0 indicates that the data is tightly packed.

pointer

GLvoid*A pointer that specifies the location of the beginning of the vertex array data.

Returns:

None.

See Also:

glColorPointer, glNormalPointer, glSecondaryColorPointer, glTexCoordPointer, glEdgeFlagPointer, glInterleavedArrays

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset