CS315 Lab 6: Display Lists and Vertex Arrays


Highlights of this lab:

This lab is an introduction to Display Lists and Vertex Arrays. Click here to downlad the lab demo.

Assignment:

Online OpenGL Manual


Seminar Notes

In large 3D scenes it becomes very computationally expensive to keep calling all those functions over and over again. OpenGL provides ways to optimise this. For static objects we can pre-compute all sorts of things and draw a prerecorded sequence of calls called a display list. For dynamic objects you can't precompute nearly as much, but you can still save time by reducing how often you make certain function calls by using a vertex array

A. Creating, Recording and Executing a Display List

The limited commands that are acceptable between a glBegin() / glEnd() pair only describe an OpenGL primitive. Therefore if you are going to repeatedly execute the same sequence of OpenGL commands, you can create and store a display list and then have this cached sequence of calls repeated with minimal overhead, since all of the vertices, lighting calculations, textures, and matrix operations stored in that list are calculated only when the list is created, not when it is replayed.

Creating a display list is very simple. Let's suppose that we have some code that creates a static model. The important part of the code is within the glBegin() / glEnd() pair. The function for the stock scene looks like this:

    // start rendering the stock scene
    glBegin (GL_POLYGON);
       ... ... details
    // finish rendering the stock scene
    glEnd ();

A display list records the results of one or more glBegin() / glEnd() sequences. When you record a display list, the current state is used to create the display list; when the list ends, the current state is whatever the original state was, plus any changes made during the recording of the displaying list. Replaying the display list uses the current state, not the state that was in effect when the list was recorded. When the replay ends, the current state is the state when the replay started plus any any changes to the state that were replayed as part of the display list.

To use a display list you will need to use the following commands:

When creating a display list, you use the glNewList() and glEndList() commands as separators. The first function – glNewList() – takes two arguments: the first is a unique integer argument used to identify the display list; the second is a flag indicating whether to simply compile the display list or to compile and immediately execute the display list. The glEndList() function takes no argument and simply marks the end of the list.

Here is how to create a display list:

    // get a unique identifier for the display list
    GLuint myList;
    myList = glGenLists(1);
    // start recording the display list
    glNewList(myList, GL_COMPILE); // just record for now
       // start rendering the display list
       glBegin (GL_POLYGON);
          ... ... details
          // finish rendering the stock scene
       glEnd ();
    glEndList();
    //finish creating the display list

The commands executed between the pair use the parameter values in effect when the list was created, not values when the list is replayed. Thus if you generate a display list that's based on a vector pointer, only the values of the pointer are used, not the pointer itself. In other words, you can not set up the array of vertices, create the display list using the array, change the vertices in the array, and then execute the display list and expect the new array values to be effective.

The following commands are not complied into the display list but are executed immediately, regardless of the display list.

Commands not Compiled into Display Lists:

    glIsList( )            glGenLists( )        glDeleteLists( )

    glFeedbackBuffer( )    glSelectBuffer( )    glRenderMode( )

    glReadPixels( )        glPixelstore( )      glFlush( )

    glFinish( )            glIsEnabled( )       glGet*( )

You should note that calling glNewList() while inside another glNewList() will generate an error.

Executing a display list is done with one of two functions. One is a specialized form that is used for sequences of display lists. The other is the basic method of calling a single display list. The glCallList( ) command takes a single argument, the unique integer ID that you gave a previously defined display list. When a display list is executed, it is as if the display list commands were inserted into the place where you made the glCallList( ) command, except that any state variables that have changed between the display list creation and execution times are ignored. However, if any state variables are changed from within the display list, they remain changed after the display list completes execution. glPushAttrib() and glPopAttrib() were created for such cases. These commands save and restore state variables. Refer to the manual for a full list of attributes you can save.

To call the display list, the following command is used:

    glCallList(myList);

Suppose the list is created as follows:

    glNewList(myList, GL_COMPILE);
       glBegin(GL_QUAD);
          glVertex3f(1, 0, -1);
          glVertex3f(1, 0, 1);
          glVertex3f(-1, 0, 1);
          glVertex3f(-1, 0, -1);
       glEnd( );
       glTranslatef(1, 0, 1);
    glEndList( );

Note there is a translation after the vertices have been defined. This causes the vertices following the execution of this display list to be translated. To draw five connected squares, the following commands is used:

    glCallList(myList);
    glCallList(myList);
    glCallList(myList);
    glCallList(myList);
    glCallList(myList);

This sequence also leaves the ModelView matrix translated five units in the +z and +x directions.

B. Hierarchical Display Lists

When glNewList( ) encounter a call to glCallList( ), the command that is inserted is a call to that display list, not the commands represented by that display list. For example, consider the following code:

    GLuint lists;

    // create three display lists
    lists = glGenLists(3);

    glNewList(lists+0, GL_COMPILE);
        ... ...
    glEndList( );

    glNewList(lists+1, GL_COMPILE);
        ... ...
    glEndList( );

    glNewList(lists+2, GL_COMPILE);
        ... ...
    glEndList( );

This creates three unique display lists. To generate a hierarchical display list:

    GLuint nested_list;

    // create a hierarchical display list
    nested_list = glGenLists(1);
    glNewList(nested_list, GL_COMPILE);
       glCallList(lists); // +0 not really needed
       glCallList(lists+1);
       glCallList(lists+2);
    glEndList( );

The following line will execute the nested display list and thus call the three display lists nested inside:

    glCallList(nested_list);

The advantage of hierarchical display list is that you can modify the commands associated with a display list ID and have the new commands executed in other display lists that use this ID.

    // modify the second list
    glNewList(lists+1, GL_COMPILE);
       ... ... // some different code
    glEndList( );
    // now execute the modified hierarchical list
    glCallList(nested_list);

You can see the power of associated with the feature. If you are writing an application that uses many static objects in nonstatic ways, such as an architectural program that creates buildings out of standard parts, the ability to cache objects and to replay them whenever you need, plus the ability to nest them, provides the basis for a very powerful modeling feature.

C. Vertex Arrays

Vertex arrays are another way of speeding up rendering. You keep more control over what you are rendering - it is stored in memory you allocate - but less is precalculated for you.

Vertex arrays allow you to use arrays of vertices, colours, texture coordinates and normals, and more, to control what is drawn. As a minimum requirement you must have an array of vertices. Other arrays can be provided if they are needed. When you are done using vertex array drawing commands, the current colour, normal and texture coordinates will be those used for the last vertex drawn. It is as if you had called a sequence of glTexCoord, glColor, glNormal and glVertex calls, but without the overhead of making the actual function calls. This leads to significant speed boosts.

Vertex arrays are the only way to draw with OpenGL ES 1, so you will want to know how to use them. These notes will only cover what is necessary to know for OpenGL ES 1.1 which you will be using next lab. If you want to know more you can learn about vertex arrays in detail in the OpenGL RedBook.

What Arrays to Use

All arrays are not necessarily needed for every object. You enable or disable arrays with the glEnableClientState and glDisableClientState commands.

Client States
void glEnableClientState(GLenum array);
void glDisableClientState(GLenum array);
array
      Specifies the capability to enable or disable. Values include
      GL_COLOR_ARRAY, GL_NORMAL_ARRAY, GL_TEXTURE_COORD_ARRAY, and
      GL_VERTEX_ARRAY

Internal Array Formats

Most arrays may be specified in multiple data types, number of components, and distances between entries. You tell the OpenGL state machine what format you are using with a gl*Pointer command. The function descriptions below represent formats common to both OpenGL ES 1.1 and OpenGL 1.1.

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

size
      Specifies the number of coordinates per vertex. Must be 2,3, or 4.

type 
      Specifies the data type of each vertex coordinate in the array.  Values
      include GL_SHORT and GL_FLOAT.

stride
      Number of bytes between one vertex and the next in the array. 0 represents
      a tightly packed array.

pointer
      Specifies a pointer to the first coordinate of the first vertex in the
      array.  
Colour Array
void glColorPointer(GLint size, GLenum type, GLsizei stride, 
                    const GLvoid *pointer);

size
      Specifies the number of components per color. Must be 4.

type
      Specifies the data type of each color component in the array. Values
      include GL_UNSIGNED_BYTE and GL_FLOAT

stride and pointer are similar to glVertexPointer.
Normal Array

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

Size is not a parameter. Each normal has size 3.

type
      Specifies the data type of each coordinate in the array. Values include
      GL_BYTE, GL_SHORT, and GL_FLOAT.

stride and pointer are similar to glVertexPointer.
Texture Coordinate Array

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

size
      Specifies the number of coordinates per array element. Must be 2, 3 or 4.

type
      Specifies the data type of each texture coordinate. Values include
      GL_SHORT and GL_FLOAT.

stride and pointer are similar to glVertexPointer.

Prepare Your Data

To use vertex arrays you must place your data in arrays or pointers to arrays. One set of arrays represents all the data you would send in a single glBegin/glEnd pair. A set of vertex arrays usually consists of one array for each of the drawing properties you plan to change. You do not need to specify an array for all drawing properties, but you must provide a vertex array. The other arrays should have exactly one entry per vertex. ie: the arrays should be the same length.

Before drawing remember to set up the arrays with the appropriate gl*Pointer call and be sure to only enable the Client States that you need.

Render Your Data

It is most common to draw all the vertices and their attributes in order from beginning to end with the glDrawArrays command, but you can draw vertices in any order with the glDrawElements command.

Draw Arrays

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

mode
      Specifies what kind of primitives to render. Values include GL_POINTS,
      GL_LINE_STRIP, GL_LINE_LOOP, GL_LINES, GL_TRIANGLE_STRIP, GL_TRIANGLE_FAN,
      and GL_TRIANGLES

first
      Specifies the starting index in the enabled arrays. 

count
      Specifies the number of indices to be rendered.

An Example

The following arrays would specify a cube like the one in the Lab 4 demo in a vertex array friendly format:

    //Vertex array for faces of a cube
    GLfloat faces[6][4][3] =
    {
	{ { 1, 1, 1}, { 1,-1, 1}, { 1,-1,-1}, { 1, 1,-1} },
	{ { 1, 1,-1}, { 1,-1,-1}, {-1,-1,-1}, {-1, 1,-1} },
	{ {-1, 1,-1}, {-1,-1,-1}, {-1,-1, 1}, {-1, 1, 1} },
	{ {-1, 1, 1}, {-1,-1, 1}, { 1,-1, 1}, { 1, 1, 1} },
	{ {-1, 1,-1}, {-1, 1, 1}, { 1, 1, 1}, { 1, 1,-1} },
	{ { 1,-1, 1}, { 1,-1,-1}, {-1,-1,-1}, {-1,-1, 1} },
    };

    //Vertex array for normals of the cube
    GLfloat normals[6][4][3] =
    {
	{ { 1, 0, 0},{ 1, 0, 0},{ 1, 0, 0},{ 1, 0, 0} },
	{ { 0, 0,-1},{ 0, 0,-1},{ 0, 0,-1},{ 0, 0,-1} },
	{ {-1, 0, 0},{-1, 0, 0},{-1, 0, 0},{-1, 0, 0} },
	{ { 0, 0, 1},{ 0, 0, 1},{ 0, 0, 1},{ 0, 0, 1} },
	{ { 0, 1, 0},{ 0, 1, 0},{ 0, 1, 0},{ 0, 1, 0} },
	{ { 0,-1, 0},{ 0,-1, 0},{ 0,-1, 0},{ 0,-1, 0} }
    };

    #define green{0,1,0,1}
    #define cyan{0,1,1,1}
    #define yellow{1,1,0,1}
    #define red{1,0,0,1}
    #define blue{0,0,1,1}
    #define magenta{1,0,1,1}

    //Vertex array of Colours for the cube
    GLfloat colors[6][4][4] =
    {
	{green,green,green,green},
	{cyan,cyan,cyan,cyan},
	{yellow,yellow,yellow,yellow},
	{red,red,red,red},
	{blue,blue,blue,blue},
	{magenta,magenta,magenta,magenta},
    };

This is, of course, a ridiculous way to work - it is very rare to specify vertex arrays by hand. Either you generate the data with a function or load it with a mesh loader and ensure that the data is loaded into the arrays in a form that is compatible with OpenGL's vertex array format.

The next calls draw the array:

    //Enable vertex arrays we want to draw with
    glEnableClientState(GL_VERTEX_ARRAY);
    glEnableClientState(GL_NORMAL_ARRAY);
    glEnableClientState(GL_COLOR_ARRAY);

    //Connect the arrays themselves
    glVertexPointer(3, GL_FLOAT, 0, faces);
    glNormalPointer(GL_FLOAT, 0, normals);
    glColorPointer(4, GL_FLOAT, 0, colors);

    //Draw command - draw everything
    glDrawArrays(GL_QUADS, 0, 6*4);


    //Disable vertex arrays that are no longer in use
    glDisableClientState(GL_VERTEX_ARRAY);
    glDisableClientState(GL_NORMAL_ARRAY);
    glDisableClientState(GL_COLOR_ARRAY);

D. Basic Buffers

OpenGL has several buffers that store pixel data for various purposes. There's multiple Color Buffers for drawing, the Depth Buffer for depth tests, the Stencil Buffer for masking off parts of the rendering surface, and the Accumulation Buffer for combining many images for special effects. You have made use of various buffers throughout these lessons, so this section is offered only as a review.

The two most common special buffer uses are Double Buffering which allows the illusion of speed and smooth animation, and the Depth Test which allows us to draw 3D scenes without sorting all the polygons from back to front first. Using these techniques requires a few extra steps that you should be familiar with.

Double Buffering

Double buffering is important in two cases. The first is when you don't want the user to see the rendering being performed but would rather see the entire display updated at one time. The other time is when you need to perform animation or some other rapid updating of the OpenGL display.

If you select a pixel format that supports double buffering, glutInitDisplayMode(GLUT_DOUBLE), then by default OpenGL drawing commands will be directed to the "back" buffer. You can think of this buffer as a memory bitmap that sits off screen. What's displayed on the screen is called the "front" buffer. The glut call to swap the front and back buffers is called glutSwapBuffers(). This call exchanges the front and back buffers of the specified DC. This means the user can't see what is being drawn until the buffers are swapped. Then the display appears to be "instantly" updated.

If the particular video hardware has enough video memory to support both buffers, this swap can be very fast. It is simply a matter of resetting the beginning offset of the currently displayed memory. In theory this means you could swap buffers thousands of times per second if you have a simple scene. In practice your video card may be set to vsync which means that you can only swap buffers when your screen is ready to start drawing, so your framerate is limited by the update frequency of your monitor, which usually between 60 and 80 Hz. If you need to do a speed test on your scene you should disable vsync.

Note that double buffering isn't a method of speeding up your program. Rather it is a method of making it appear to run smoothly, simply by letting the user see the individual frames "after" they have been drawn. In some cases, double buffering will make your program run a bit faster, although the effect depends on the rendering time (or the complexity of the scene). For example, complex scenes will take longer to draw and slow down your frame rate.

Remember before you draw to a color buffer you should clear the buffer:
    glClear(GL_COLOR_BUFFER_BIT);
When you are in double buffering mode you can add somthing to the front buffer without redrawing the whole frame by switching the draw buffer like this:
    glDrawBuffer(GL_FRONT);
after that everything you draw will be drawn to the front buffer and will be immediately visible, no need to swap buffers. You can go back to regular double buffered rendering by calling:
    glDrawBuffer(GL_BACK);

Depth Testing

Depth testing allows us to draw the primitives that make up a scene in any order. Without it we would need to break the objects in the scene into their individual triangles and sort them from back to front before rendering. This sorting can be computationally expensive and adds complexity to the renderign process. Depth testing simplfies rendering. You can send whole objects to be rendered all at once and no sorting is needed. It requires more memory however. Every pixel, or fragment, needs to store a depth value.

To get depth testing you need a pixel format with a depth buffer. Many modern graphics cards have depth buffering in all available pixel formats. You should still ask for the depth buffer just in case – glutInitDisplayMode(GLUT_DEPTH).

To turn on depth testing you enable it with glEnable(GL_DEPTH_TEST). Before drawing each frame you should clear the depth buffer – glClear(GL_DEPTH_BUFFER_BIT).

You can change the depth test to achieve various effects by using glDepthFunc. It takes the following paramenters: GL_NEVER, GL_ALWAYS, GL_LESS, GL_LEQUAL, GL_EQUAL, GL_GEQUAL, GL_GREATER, or GL_NOTEQUAL.


D. Lab Exercise

This exercise is broken into two parts:
  1. Display List Practice
  2. Performance Comparison

Part 1-Display List Practice

Goals:

Instructions:

  1. Create an empty GLUT project
  2. Add the following code to the project:
    list.cpp
  3. Build and Run this code
  4. How many triangles are there? What color are the triangles? What color is the line?
  5. Use GL_COMPILE_AND_EXECUTE instead of GL_COMPILE in glNewList. How many triangles are there now? Why? (2 marks)
  6. Fix the code by moving the code that creates the display list so that you do see 11 triangles. (1 mark)
  7. When you are defining your display list, use glPushAttrib(GL_CURRENT_BIT) at the beginning and glPopAttrib() at the end. What color is the line now? (1 mark)

Part 2-Performance Comparison: Basic Drawing vs. Display Lists vs. Vertex Arrays

Goals:

Instructions:

Download the PC version or the Mac version of this lab. The cool demonstration model in this lab is from the OpenGL SuperBible. You may develop the lab on whatever platform you like, but please record your results on the Macs in the UDML. If your code is clean it should compile on both platforms.

  1. Restart your computer.
  2. Baseline Test:
    1. Build and Run the lab code.
    2. Look in the console to see what speed the model runs at on a Mac in the lab. Record the approximate FPS the animation stabilizes at.
  3. Display List Test:
    1. Add code in SetupRC to generate and compile display lists for the body and glass of the F-16.
    2. Call the lists instead of DrawBody and DrawGlass in the appropriate places in RenderScene.
    3. Run the code again and record the approximate FPS the animation stabilizes at.
  4. Vertex Array Test:
    1. Make a copy of DrawBody called GenBodyArrays
    2. Make a copy of DrawGlass called GenGlassArrays
    3. Rewrite GenBodyArrays and GenGlassArrays so they load Vertex, Normal and Texture values into global arrays instead of drawing.
      HINTS:
      • You will need 6 global arrays: 3 for body, and 3 for glass.
      • In this model, normals and vertices have 3 components, but textures have only 2 components.
      • It is possible to use 1D, 2D and 3D arrays to solve this problem.
      • If you wish to store the plane body normals in a 3D array, you might do it like this:
        bodyNorm[iFace][iPoint][0] = normalsBody[face_indicesBody[iFace][iPoint+3]][0];
        bodyNorm[iFace][iPoint][1] = normalsBody[face_indicesBody[iFace][iPoint+3]][1];
        bodyNorm[iFace][iPoint][2] = normalsBody[face_indicesBody[iFace][iPoint+3]][2];
        the other arrays are similar.
      • See if you can do this with 1D arrays.
    4. Place the code necessary to draw the arrays into two functions called DrawBodyArrays and DrawGlassArrays.
    5. Call your new functions in the appropriate place instead of using display lists or the DrawBody and DrawGlass functions.
    6. Run the code again and record the approximate FPS the animation stabilizes at.
  5. If you have not already done so, add keys to your program that allow switching between the three drawing modes.

Deliverables

Part 1

Part 2