CS 381 Fall 2012  >  Lecture Notes for Tuesday, October 2, 2012

CS 381 Fall 2012
Lecture Notes for Tuesday, October 2, 2012

Hierarchical Objects

We wish to draw a moving object with moving parts that have moving parts .... In order to do this, we adhere to the following two principles.

  1. A function that draws an object should draw it with its natural center at the origin, in its basic size and orientation, using the current transformation.
  2. The current transformation should be unaffected when the function exits.

We have already seen the first principle. The second principle is new, but it is easily followed: whenever we modify a transformation, push first and then pop when we are done using the modified transformation.

Recall the OpenGL matrix-stack commands:

glPushMatrix
No parameters. Pushes a duplicate of top-of-stack matrix onto the current matrix stack (set with glMatrixMode).
glPopMatrix
No parameters. Pops the current top-of-stack matrix off the current matrix stack.

Top of stack is the matrix affected by transformation commands and used in the pipeline.

Thus, a function that draws an object with 3 moving parts might look like this:

void drawObj()
{
    // MAIN PART OF OBJECT
    // Draw main part of object

    // MOVING PART 1
    glPushMatrix();
    // Set up transformation for part 1 (no glLoadIdentity!)
    // Draw part 1 (call another function?)
    glPopMatrix();

    // MOVING PART 2
    glPushMatrix();
    // Set up transformation for part 2 (no glLoadIdentity!)
    // Draw part 2 (call another function?)
    glPopMatrix();

    // MOVING PART 3
    glPushMatrix();
    // Set up transformation for part 3 (no glLoadIdentity!)
    // Draw part 3 (call another function?)
    glPopMatrix();
}

Notes

See arm.cpp for code that draws and animates a hierarchical object.

Vertex Arrays

Troubles with glBegin-glEnd

In the original OpenGL specification, the only way to draw general-geometry primitives was using glBegin-glEnd. But this mechanism turned out to have three problems.

  1. It has high overhead. If we use a separate function call to specify each vertex’s position, color, normal vector, and texture coordinates, then a scene with \(100,000\) vertices requires \(400,000\) function calls.
  2. Vertices must be specified again each time they are drawn. Very often we use the same vertex coordinates repeatedly, moving them with transformations. It would be convenient to be able to say, “Use the same vertices as last time.”
  3. Surfaces are often specified using multiple triange strips. A vertex that lies in two adjacent strips—as most vertices will—must have its coordinates specified twice.

Later versions of OpenGL added mechanisms to deal with each of these problems.

For the first problem OpenGL now allows for data to be specified using a vertex array: a data structure holding coordinates and other attributes for arbitrarily large numbers of vertices. A large vertex array can be passed to OpenGL with just a few function calls.

For the second problem, a vertex array can be stored in memory managed by OpenGL, in the form of a vertex buffer objects (VBO). The vertex data stored in such an object can be reused repeatedly.

For the third problem, we can specify the structure of a primitive independently of the coordinates and other attributes of the vertices using an element array, stored in an element buffer object (EBO). (Note: alternative names are index array, index buffer object.)

Using the VBO/EBO mechanism is considerably more complicated than glBegin-glEnd. In this class, you may use whichever method you want. Note, however, that the glBegin and glEnd commands have been officially removed from the latest OpenGL versions. Further, OpenGL’s descendants, OpenGL ES and WebGL, have never allowed for the glBegin-glEnd mechanism.

Client & Server

In any number of computing contexts, the client-server paradigm is used. The idea is that some task needs to be done. The module that needs the task performed is the client. The module that can perform the task for the client is the server.

For example, a web browser is an example of a web client. It wants webpage data, and a web server can provide such data.

The client-server approach allows for:

OpenGL was designed using a client-server approach. The client is the application code. The server is whatever does the rendering; these days, it will be the GPU. These might be on the same computer, or they might be on different computers, different continents, or perhaps one day different planets. (Strangely, despite technological advances, rendering on a different machine seems to have become less common. In the early 1990s, I did it often; in the past five years, I don’t recall doing it at all. -GGC-)

This client-server design is what glFlush is all about. When the connection between the client and the server is slow, we want to avoid using it. We save up data to be sent in a single burst. The glFlush command directs the system to send any saved-up data to the server.

OpenGL Objects

OpenGL makes a clear distinction between client-side data and server-side data. The latter is managed by OpenGL, in the form of objects. An OpenGL object has nothing to do with C++ classes; it is a chunk of server-side storage.

When we use an OpenGL object:

VBOs and EBOs are examples of OpenGL objects. Later, when we discuss textures, we will see another kind: texture objects.

Using a VBO

What to Do with a VBO

Recall that a vertex buffer object is a chunk of OpenGL server-side data that holds a vertex array: coordinates and other attributes of arbitrarily large numbers of vertices. Here is how we deal with such an object in our code.

Sample VBO Code

Suppose we want to draw two triangles with multicolored vertices. We could do it like this.

glBegin(GL_TRIANGLES);
    glColor3d(1., 0., 0.);  // First triangle
    glVertex2d(0., 0.);
    glColor3d(0., 1., 0.);
    glVertex2d(1., 0.);
    glColor3d(0., 0., 1.);
    glVertex2d(0., 1.);
    glColor3d(0., 1., 1.);  // Second triangle
    glVertex2d(-1., -1.);
    glColor3d(1., 0., 1.);
    glVertex2d(0., -1.);
    glColor3d(1., 1., 0.);
    glVertex2d(-1., 0.);
glEnd();

Here is complete code to use a VBO to draw the same triangles.

We declare a global GLuint to hold the name.

GLuint trianglevbo;  // Name of VBO holding data for triangles

In our initialization, generate, bind, set the data, send it to OpenGL, and describe it.

// Generate name
glGenBuffers(1, &trianglevbo);

// Bind name
glBindBuffer(GL_ARRAY_BUFFER, trianglevbo);

// Set data
GLdouble triangledata[6*2+6*3] = {
     0.,  0.,   // Vertex 0
     1.,  0.,   // Vertex 1
     0.,  1.,   // Vertex 2
    -1., -1.,   // Vertex 3
     0., -1.,   // Vertex 4
    -1.,  0.,   // Vertex 5
    1., 0., 0.  // Color 0
    0., 1., 0.  // Color 1
    0., 0., 1.  // Color 2
    0., 1., 1.  // Color 3
    1., 0., 1.  // Color 4
    1., 1., 0.  // Color 5
};

// Send data
glBufferData(GL_ARRAY_BUFFER,               // Target
             (6*2+6*3) * sizeof(GLdouble),  // Data size (bytes)
             triangledata,                  // Pointer to data
             GL_STATIC_DRAW);               // Storage hint

// Describe data
glVertexPointer(2,                     // Components per vertex
                GL_DOUBLE,             // Type of vertex component
                2 * sizeof(GLdouble),  // Stride (bytes)
                (GLvoid *)(0));        // Byte index of first vertex
                                       //  (pointer, for some reason)

glColorPointer(3,                      // Components per color
               GL_DOUBLE,              // Type of color component
               3 * sizeof(GLdouble),   // Stride (bytes)
               (GLvoid *)(6*2));       // Byte index of first color

Note that, after the above call to glBufferData, the data are on the server, and we can do whatever we want with array triangledata: destroy it, use it for something else, etc.

To render our triangles, bind, enable, and draw in the display function.

// Bind name
glBindBuffer(GL_ARRAY_BUFFER, trianglevbo);

// Enable functionality
glEnableClientState(GL_VERTEX_ARRAY);
glEnableClientState(GL_COLOR_ARRAY);

// Render using VBO
glDrawArrays(GL_TRIANGLES,  // Primitive
             0,             // Index of start vertex
             6);            // How many vertices

Optionally, we can delete our VBO when we are sure we never want to use it again. This probably should not be part of the regular update-display cycle. Put it in a destructor, perhaps.

glDeleteBuffers(1, &trianglevbo);

The above code looks hideous, of course. But there are advantages. The next time we want to draw the same triangles, we simply bind, enable, and call glDrawArrays. And this would work even for \(100,000\) triangles.

See vbo.h for a (hopefully) convenient wrapper class for VBOs. See vbodemo.cpp for a simple application that uses this class.

Using an EBO

What to Do with an EBO

An element buffer object holds integers, which are used as indices into whatever VBO is bound when we render. Here is how we deal with such an object in our code.

Sample EBO Code

Suppose we have set up trianglevbo, as above. We want to draw a single triangle using vertices \(0\), \(2\), \(5\). In other words, we want to do this:

glBegin(GL_TRIANGLES);
    glColor3d(1., 0., 0.);  // Color+vert 0
    glVertex2d(0., 0.);
    glColor3d(0., 0., 1.);  // Color+vert 2
    glVertex2d(0., 1.);
    glColor3d(1., 1., 0.);  // Color+vert 5
    glVertex2d(-1., 0.);
glEnd();

Here is code to use an EBO with our VBO to draw this triangle. We assume that the set up code for the VBO is done as above, except for the rendering.

We declare a global GLuint to hold the name.

GLuint niftyebo;  // Name of EBO holding indices for new triangle

In our initialization, generate, bind, set the data, and send it to OpenGL.

// Generate name
glGenBuffers(1, &niftyebo);

// Bind name
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, niftyebo);

// Set data
GLuint niftyindices[3] = { 0, 2, 5 };

// Send data
glBufferData(GL_ELEMENT_ARRAY_BUFFER,  // Target
             3 * sizeof(GLuint),       // Data size (bytes)
             niftyindices,             // Pointer to data
             GL_STATIC_DRAW);          // Storage hint

Once again, after the above call to glBufferData, the data are on the server, and we can do whatever we want with array niftyindices.

To render the triangle in the display function, bind the VBO and enable, then bind the EBO and draw.

// Bind VBO name
glBindBuffer(GL_ARRAY_BUFFER, trianglevbo);

// Enable VBO functionality
glEnableClientState(GL_VERTEX_ARRAY);
glEnableClientState(GL_COLOR_ARRAY);

// Bind EBO name
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, niftyebo);

// Render using EBO
glDrawElements(GL_TRIANGLES,     // Primitive
               3,                // Number of elements
               GL_UNSIGNED_INT,  // Type of element
               (GLvoid *)(0));   // Start element

Lastly, the optional delete.

glDeleteBuffers(1, &niftytebo);

See ebo.h for a (hopefully) convenient wrapper class for EBOs. See ebodemo.cpp for a simple application that uses this class.

Notes on VBOs & EBOs

OpenGL does little or no range checking. It is your responsibility to make sure that you do not reference data that lies beyond the end of the stored data.

Each EBO handles a single primitive. To do multiple primitives, use multiple EBOs. You could use one gigantic VBO to hold all vertex data, then lots of EBOs, one for each primitive.

The hint GL_STATIC_DRAW says that we do not intend to change the data, and we intend to use it a lot. We can also use GL_STREAM_DRAW, which indicates no changing and little use. Lastly, there is GL_DYNAMIC_DRAW, which indicates frequent alterations in the data. But these are merely hints; they do not affect the operations we are allowed to perform.

You may wish to look into display lists, which allow you to store, not just vertex data, but arbitrary rendering commands.


CS 381 Fall 2012: Lecture Notes for Tuesday, October 2, 2012 / Updated: 6 Oct 2012 / Glenn G. Chappell / ggchappell@alaska.edu