OpenGL Rendering and Drawing

by Chris Halsall

Related Articles:

Creating Real Time 3D Graphics With OpenGL

Preparing to Build an OpenGL Application

Utah GLX

Now that you've read the preceding companion piece to this article, "Preparing to Build a OpenGL Application," you're ready to tackle rendering and drawing. Let's jump right in to the details of rendering.

Rendering in OpenGL

The function registered as the Display (and usually Idle) callback is where control is passed whenever it's time to draw the next frame of the display. For our program this is the cbRenderScene() function, and it's also where our motion calculations are done.

Figure 1

Figure 1. With different drawing mode settings, a wide range of visual effects can be created.

In more advanced applications, physics calculations may be executing in another process or be based on the actual amount of time it took to render the last frame. This is necessary for objects' motions to be independent of frame-rate.

If you take a look at the function, you'll see it first simply enables or sets different modes depending on several global variables. This is where lighting, textures, and blending are enabled and disabled as appropriate. Take a close look at the settings requests for the filtering; it only affects textures.

Next we're asked to work with the GL_MODELVIEW matrix, which lets us make transformations that work on the model, or objects, of our scene. Into this matrix we load the Identity matrix, which is simply a way of resetting things to the origin, so there's no rotation, translation or scaling. Lastly, we translate the model out Z_Off distance from the screen so it can be seen and rotate the model as appropriate.

Let the drawing begin!

Now we're ready to start the actual drawing. (Yippee!) The first thing we do is clear both the color (frame) buffer and the depth buffer. It should be pretty obvious why we do this, but it can be both amusing and enlightening to try not clearing either or both buffers. Some applications can even guarantee that they'll be painting the whole screen themselves, so you can save a few cycles by not clearing the color buffer; the depth buffer must still be cleared, however.

Figure 2

Figure 2. By turning double-buffering off, you can watch OpenGL draw each frame. Note that OpenGL draws from the bottom up, and each Quad is made up of two triangles.

To start the faces of our cube, we first call glBegin(), passing GL_QUADS to tell OpenGL that any vertex data received from this point until the next glEnd() should be interpreted as four-vertex polygons. Next we call glNormal3f(), passing in the normal vector for our first polygon. The normal vector is simply the direction facing right angles to the surface, and is needed for OpenGL to be able to calculate lighting correctly. For our application, all the vertices of each polygon share the same normal, but for curved surfaces approximated with many polygons, adjusting the normal on a per-vertex basis can allow the object to appear smooth even though it isn't.

After setting the normal, we next call glColor4f() with the RGBA settings for a red face with a 75% level of opaque. Lastly, to complete our first face, we make four calls to glTexCoord2f() interleaved with four calls to glVertex3f() to define the position of the texture on the polygon and the position of the polygon in our 3D world: the floor.

We next do the same type of thing for the top-most grey face, but note that the texture coordinates are different. The third face rendered, painted green, also has unusual texture coordinates, but this time they're non-regular. Compare the code to the visual results, and tweak to see what kinds of effects can be achieved. Both the grey and green faces are set to be 50% opaque.

The fourth face, blue, is the first face with the full texture being requested across its face, and it is adjusted to be only 25% opaque. The second-to-last face to be drawn demonstrates how OpenGL can smoothly blend colors across the face of a polygon, ranging from 90% red, green or blue in three corners to black in the fourth.

The last face, painted yellow, shows that the same kind of blending applies for the alpha-channel of a polygon. The yellow face ranges from full transparent at one corner to fully opaque at another. After we're finished drawing the yellow face, we call glEnd() to tell OpenGL we're finished drawing Quads (at least, for now).

Drawing text

After drawing the cube, we next want to render some text. However, we want it to stay on the screen at the same location, and not spin around with the cube (although that might be neat). To do this, we first reload the Identity matrix, so we're back to the origin again. Then we call glMatrixMode() with GL_PROJECTION, which tells OpenGL we want to work with our projection matrix.

Now, we've already got a nice projection matrix loaded, which we need to render our rotated cube. However, we also need to reset this with an orthogonal projection matrix to be able to render in the true coordinate system of the display. For just this type of situation, OpenGL lets you save matrices by using the glPushMatrix() call. After doing this, we can change the projection matrix as needed, and we can glPopMatrix() the saved matrix back later.

Next, we disable texturing and lighting in case they were activated, since lit or textured text isn't what we want. The color for the text is then set, including a 75% level of transparency, just for fun. Then our code simply renders a text string using sprintf(), which is then painted on the screen with a call to glRasterPos2i() and then another to ourPrintString(). The latter is a simple support function which loops through a string, calling the GLUT library to render a bitmap for each character as needed. Note that in the OpenGL system, (0,0) is the bottom-left of the screen.

The next section of code renders the frames-per-second value and the current frame count in a similar fashion as above. One extra bit of code first renders a small, mostly opaque dark rectangle for the FPS value to be rendered onto. This technique is often used to ensure that some important bit of text can always be seen, regardless of what might be rendered behind it.

After all the text rendering is complete, we call glPopMatrix() to recover our saved projection matrix. Then, since we've completed all the drawing we're going to, we call glutSwapBuffers() to display our just-drawn image to the user. It's at this point that we do our motion calculations, and then we make a call to ourDoFPS() in order to collect the FPS stats. Then we exit the function, returning control back to OpenGL.

Do that about 50 times a second or so, and you've got a pretty good idea what's involved in an OpenGL application loop. Obviously, for an application to be useful (or at least amusing), a great deal more should be going on in the scene. But what we've covered here is the core basis for most OpenGL applications.

Notes, and bug!

Figure 3

Figure 3. A ha! Transparent objects must be drawn depth sorted, or errors may occur.

Readers should be aware that for optimization reasons, objects are now usually defined all at once by way of something called Vertex Arrays. This is used instead of what we're doing in this demonstration program, which is passing vertex attribute data one function-call at a time.

Even though they're now a rather universal extension to OpenGL, vertex arrays are beyond the scope of this article. The concepts covered here are still applicable, and discrete function-call methods are still common during prototyping.

I must also point out an intentional "bug" that's living in our sample program, which sharp-eyed readers may have already noticed. It has to do with transparent objects and how they must be rendered in descending distance from the camera (back to front) in order for them to appear correctly.

This isn't a problem when the objects aren't transparent, as the z-buffer is used to eliminate hidden surfaces (by OpenGL simply refusing to draw anything farther from the camera than what's already drawn). But in the case of transparency, each object contributes to the final image. Thus, the order in which the objects are rendered matter. To see this, turn on blending (which turns off z-buffering), and then rotate the cube so the red, green and blue faces are closest to you. These are drawn first, and so it will be clear something's not quite right when the faces "behind" these are next drawn on top of them.

Figure 4

Figure 4. Or you can cheat, and use an exclusively additive blend function.

There are several solutions to this problem, all of which are compromises. (Welcome to computer science). You can:

Final thoughts

OpenGL is the lowest-level, cross-platform, common hardware accelerated 3D API available. The demo program we've provided here should compile and run with only minor tweaking under any Unix with OpenGL or a clone installed. It should also work without much effort in a Windows- or Mac-based development environment, although we haven't tried that.

We've done our best to explore some of the more interesting and useful aspects of OpenGL with these articles and demo program, but in truth we've only scratched the surface of what can be done with the library. Interested readers are encouraged to visit the site to learn more. The "OpenGL Programming Guide," published by Addison-Wesley, and the "OpenGL SuperBible," published by the Waite Group, are both excellent reference books as well.

XFree86 4.0 will soon be included in major Linux distributions, and when that happens it will deliver off-the-CD access to Linux users' 3D accelerated hardware. With such hardware becoming commonplace in even the least expensive machines, I look forward to an explosion in 3D applications under Linux in the near future. Could your software project use a 3D interface?

Chris Halsall is the Managing Director of Ideas 4 Lease (Barbados). Chris is a specialist... at automating information gathering and presentation systems.

Discuss this article in the O'Reilly Network Forum.

Return to the O'Reilly Network Hub.

Copyright © 2017 O'Reilly Media, Inc.