Lesson 17: Display Lists

Display lists are used in OpenGL as stored versions of a series of OpenGL calls. These lists are then accessed with a display list handle.

Theory

Okay... great. Access them with a handle. So... why use display lists anyways?

Actually, there is absolutely no reason to. Display lists are useless.

No, not really. I was just pulling your leg. First of all, display lists are more convenient to use - you get to replace all those OpenGL rendering calls with a single glCallList(). But, even better - when display lists are stored, lots of extra calculations that are normally done each time you call, say glVertex3f() or glTexCoord2f(), are done once and then stored. So, by avoiding the unnecessary calculations, the use of display lists can speed up your rendering code.

Basically, all you have to do is find an unused handle, then reference it and tell OpenGL to start collecting commands to put into the display list. When you're done, tell OpenGL that you're done compiling, and then all you have to do is call it with glCallList(). It's pretty simple!

Implementation

Remember the Cube class? Well, now it's time to go to the next step: Spheres!!! The Sphere class constructs a geodisic sphere and stores it in a display list.

Listing 17-1: sphere.h
// sphere.h - interface of Sphere class.

#ifndef SPHERE_H
#define SPHERE_H

#include <alleggl.h>

class Sphere
{
private:
    float rad;
    int stacks, slices;
    GLuint dl_handle;
    float shade;

    void ConstructDisplayList();
    void DeleteDisplayList();

public:
    float xp, yp, zp;
    float dxp, dyp, dzp;
    float xr, yr, zr;
    float dxr, dyr, dzr;

    Sphere(float rad, int st, int sl, float shade,
           float x, float y, float z,
           float xr = 0.0, float yr = 0.0, float zr = 0.0);
    ~Sphere();

    void Update();
    void Render();
};

#endif  // SPHERE_H

As you can see, the Sphere class is quite extensive and has many parameters that affect how it works. The member dl_handle is, as you might have guessed, the display list handle. This is what is used to reference the display list once it has been compiled. stacks and slices are, respectively, the number of subdivisions along the Y axis and the number of divisions around the Y axis. These are similar to lines of longitude and latitude on a globe. shade is the "colour" of the sphere(level of gray), rad is the radius, and the other variables represent the position, orientation, and speeds of the sphere.

Now, for the implementation of the Sphere class:

Listing 17-2: sphere.cpp
// sphere.cpp - implementation of Sphere class

#include "sphere.h"
#include <cmath>
using namespace std;

// The magic number
const double Pi = 3.1415926535897932384626433832795;

Sphere::Sphere(float rad, int st, int sl, float shade,
               float x, float y, float z,
               float xr = 0.0, float yr = 0.0, float zr = 0.0)
 : rad(rad), stacks(st), slices(sl), dl_handle(0), shade(shade),
   xp(x), yp(y), zp(z), dxp(0.0), dyp(0.0), dzp(0.0),
   xr(xr), yr(yr), zr(zr), dxr(0.0), dyr(0.0), dzr(0.0)
{
    ConstructDisplayList();
}

Sphere::Sphere(const Sphere& s)
 : rad(s.rad), stacks(s.stacks), slices(s.slices),
   dl_handle(s.dl_handle), shade(s.shade),
   xp(s.xp), yp(s.yp), zp(s.zp), dxp(s.dxp), dyp(s.dyp), dzp(s.dzp),
   xr(s.xr), yr(s.yr), zr(s.zr), dxr(s.dxr), dyr(s.dyr), dzr(s.dzr)
{
    ConstructDisplayList();
}

Sphere& Sphere::operator=(const Sphere& rhs)
{
    *this = Sphere(rhs);
}

Sphere::~Sphere()
{
    DeleteDisplayList();
}

void Sphere::ConstructDisplayList()
{
    // Delete the old list
    DeleteDisplayList();

    // Create a new list name
    dl_handle = glGenLists(1);
    if(!glIsList(dl_handle))
    {
        allegro_message("Fatal error: Could not create a display list!");
        exit(1);
    }

    // Make sure stacks and slices are at reasonable values
    stacks = MAX(2, stacks);
    slices = MAX(3, slices);

    // Precalculate some needed information
    double stack_angle = Pi / 2 + (Pi / stacks);
    double stack_angle_step = -(Pi / stacks);
    double stack_height = 0.0;
    double stack_height_next = 0.0;
    double stack_width = 0.0;
    double stack_width_next = 0.0;
    double slice_angle = 0.0;
    double slice_angle_step = -((Pi * 2) / slices);

    // Compile the display list
    glNewList(dl_handle, GL_COMPILE);
        for(int i = 0; i < stacks; i++)
        {
            stack_angle += stack_angle_step;
            stack_height = sin(stack_angle);
            stack_width = cos(stack_angle);
            stack_height_next = sin(stack_angle + stack_angle_step);
            stack_width_next = cos(stack_angle + stack_angle_step);
            glBegin(GL_QUAD_STRIP);
                glColor3f(shade, shade, shade);
                for(int j = 0; j < slices + 1; j++)
                {
                    slice_angle += slice_angle_step;
                    glNormal3d(cos(slice_angle) * stack_width,
                               stack_height,
                               sin(slice_angle) * stack_width);
                    glVertex3d(cos(slice_angle) * rad * stack_width,
                               stack_height * rad,
                               sin(slice_angle) * rad * stack_width);
                    glNormal3d(cos(slice_angle) * stack_width_next,
                               stack_height_next,
                               sin(slice_angle) * stack_width_next);
                    glVertex3d(cos(slice_angle) * rad * stack_width_next,
                               stack_height_next * rad,
                               sin(slice_angle) * rad * stack_width_next);
                }
            glEnd();
        }
    glEndList();
}

void Sphere::DeleteDisplayList()
{
    if(glIsList(dl_handle))
        glDeleteLists(dl_handle, 1);
}

void Sphere::Update()
{
    xp += dxp; yp += dyp; zp += dzp;
    xr += dxr; yr += dyr; zr += dzr;
}

void Sphere::Render()
{
    glPushMatrix();
    glTranslatef(xp, yp, zp);
    glRotatef(xr, 1.0, 0.0, 0.0);
    glRotatef(yr, 0.0, 1.0, 0.0);
    glRotatef(zr, 0.0, 0.0, 1.0);
    glCallList(dl_handle);
    glPopMatrix();
}

The constructor and destructor should be easy enough to follow, so I'll jump right in to Sphere::ConstructDisplayList(). After any old lists which may exist are deleted, we get a display list handle by calling glGenLists() with the number of display lists we want to generate(in this case, 1). We then make sure that the handle actually "points" to a real display list by checking it with glIsList().

GLuint glGenLists(int range)
Generates a range of contiguous display list handles. Returns the integer that represents the first display list; if this is zero, then there was an error in trying to allocate enough display lists. Otherwise, you can start using display lists with the handles return value through return value + range - 1.

int glIsList(GLuint list)
Returns 1 if the specified display list handle points to a valid display list; otherwise, returns 0.

After the display list handle is allocated, we can compile it. This is done by using the function glNewList() and glEndList to border the OpenGL calls that we want to be stored in the display list. All the OpenGL function calls that are made between these two functions are recorded to the display list supplied to glNewList().

void glNewList(GLuint list, int mode)
Starts the compilation of the display list with the handle list in mode mode. If mode is GL_COMPILE, the display list is merely compiled and not executed. If mode is GL_COMPILE_AND_EXECUTE, the OpenGL commands that you call are executed as they are stored in the display list. Any OpenGL calls can be made between glNewList() and glEndList(), with the following special rules(these are taken from the OpenGL 1.1 Reference):
  • glNewList() or glEndList() cannot be added to a display list.
  • The following functions are will not be added to a display list, but will be executed immediately: glColorPointer(), glDeleteLists(), glDisableClientState(), glEdgeFlagPointer(), glEnableClientState(), glFeedbackBuffer(), glFinish(), glFlush(), glGenLists(), glIndexPointer(), glInterleavedArrays(), glIsEnabled(), glIsList(), glNormalPointer(), glPopClientAttrib(), glPixelStore(), glPushClientAttrib(), glReadPixels(), glRenderMode(), glSelectBuffer(), glTexCoordPointer(), glVertexPointer(), and all of the glGet() commands.
  • glTexImage2D() will be executed immediately if the first argument is GL_PROXY_TEXTURE_2D.
  • glTexImage1D() will be executed immediately if the first argument is GL_PROXY_TEXTURE_1D.

void glEndList(void)
Finishes the compilation of a display list which was started with a call to glNewList().

In between glNewList() and glEndList() is the sphere generating code, which uses some nifty trigonometry to make a sphere. The sphere is made up of a series of quad strips running from the top of the sphere to the bottom, following the "lines of latitude", as it were. Just ignore the calls to glNormal() in the display list compilation code - those will be used in the next lesson, and I didn't want to have to re-write the sphere class just to add those function calls :)

Sphere::DeleteDisplayList() uses glDeleteLists() to get rid of the display list used by the class.

void glDeleteLists(GLuint list, int range)
Frees resources for range lists starting at list.

In Sphere::Update(), the position and rotations of the sphere are updated based on the positional and angular velocities of the sphere. Then, in Sphere::Render(), the needed transformations are applied to the modelview matrix, and then glCallList() is used to execute the display list. (Finally!)

void glCallList(GLuint list)
Executes the display list named list. Commands in the display list are executed in the same order in which they were compiled.

There is another way to execute display lists, using glCallLists(). An explanation of it can be found here.

And now, we finally get to use the class in a nice little example program!

Listing 17-3: agl17.cpp
#include <allegro.h>
#include <alleggl.h>
#include "sphere.h"

// Timer interrupt system
volatile int Time = 0;
void TimerFunc()
{
    Time++;
}
END_OF_FUNCTION(TimerFunc);

void Init()
{
    // Start up Allegro and AllegroGL systems
    allegro_init();
    install_allegro_gl();
    install_timer();
    install_keyboard();

    // Set up the timer interrupts
    LOCK_VARIABLE(Time);
    LOCK_FUNCTION(TimerFunc);
    install_int_ex(TimerFunc, BPS_TO_TIMER(50));

    // Set some AllegroGL options
    allegro_gl_set(AGL_DOUBLEBUFFER, true);
    allegro_gl_set(AGL_COLOR_DEPTH, 16);
    allegro_gl_set(AGL_Z_DEPTH, 16);
    allegro_gl_set(AGL_RENDERMETHOD, true);
    allegro_gl_set(AGL_SUGGEST, AGL_DOUBLEBUFFER | AGL_COLOR_DEPTH | AGL_Z_DEPTH | AGL_RENDERMETHOD);

    // Set up a suitable viewing window
    set_color_depth(16);
    set_gfx_mode(GFX_OPENGL_WINDOWED, 640, 480, 0, 0);

    // Enable face culling + depth testing
    glCullFace(GL_BACK);
    glEnable(GL_CULL_FACE);
    glEnable(GL_DEPTH_TEST);

    // Set up a perspective projection
    glMatrixMode(GL_PROJECTION);
    glFrustum(-1.0, 1.0, -1.0, 1.0, 1.0, 100.0);
    glMatrixMode(GL_MODELVIEW);

    // Set the background color to black
    glClearColor(0.0, 0.0, 0.0, 0.0);

    // Seed the RNG
    srand(time(0));
}

int main()
{
    Init();

    // Make some spheres
    Sphere sphere[5] =
    {
        Sphere(5.0, 16, 20, 0.8, 0.0, 0.0, -12.0),
        Sphere(4.0, 16, 20, 0.7, -9.0, 6.0, -16.0),
        Sphere(3.0, 16, 20, 0.6, -8.0, -7.0, -18.0),
        Sphere(2.0, 16, 20, 0.5, 7.0, 7.0, -10.0),
        Sphere(1.0, 16, 20, 0.4, 6.0, -6.0, -8.0)
    };

    // Randomize their rotations
    for(int i = 0; i < 5; i++)
    {
        sphere[i].dxr = float(rand() % 1000) / 1000;
        sphere[i].dyr = float(rand() % 1000) / 1000;
        sphere[i].dzr = float(rand() % 1000) / 1000;
    }

    while(!key[KEY_ESC])
    {
        while(Time > 0)
        {
            // Update the spheres
            for(int i = 0; i < 5; i++)
            {
                sphere[i].Update();
            }
            Time--;
        }

        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

        // Draw the spheres
        for(int i = 0; i < 5; i++)
        {
            sphere[i].Render();
        }

        glFlush();
        allegro_gl_flip();
    }
}
END_OF_MAIN();

Compile this program along with sphere.cpp and you will see five nice spinning single-coloured spheres on a black background.

The Guts of agl17.cpp

After writing this long lesson, I don't feel like explaining much more, and luckily, I don't have to! :) There is pretty much nothing new at all in this example program, except for the use of the Sphere class.

Now, how about an exercise for the reader? I can hear the anxious cheering now! ;)

The example I wrote was okay, but it generated five separate display lists when really you could have gotten away with only one display list, shared between all the spheres. Maybe one could generate a sphere with a radius of one, then add a call to glScale() in Sphere::Update() to scale the sphere by its radius? Hint, hint.

In the Next Lesson...

Yes, this article is almost over! Don't fret, though, the next lesson is complicated and difficult to understand! That's right, it's about lighting! Yay!