OpenGL, a computer-industry standard based on SGI's graphics library, lets you create some amazingly complicated and realistic scenes on Windows-based PCs.
July 01, 1995
URL:http://www.drdobbs.com/open-source/programming-with-opengl/184409593
Copyright © 1995, Dr. Dobb's Journal
Copyright © 1995, Dr. Dobb's Journal
Copyright © 1995, Dr. Dobb's Journal
Copyright © 1995, Dr. Dobb's Journal
Ron is a principal software developer at Lotus Development, where he researches and develops graphical and interactive techniques for data analysis and exploration. Ron can be contacted at [email protected].
If you've worked with the Windows GDI, you're painfully aware of its limitations, particularly when trying to create anything other than a flat, 2-D, static scene. And whether you program games or business graphics, you know that in Windows, attempting to create any effects beyond a simple gradient fill usually means some complicated programming. Recognizing these shortcomings, Microsoft has added to Windows NT 3.5 (and promised for all Microsoft 32-bit operating systems) a graphics library called "OpenGL," which provides the advanced 3-D rendering and animation that is difficult to do with GDI.
OpenGL is a computer-industry standard based upon Silicon Graphics' internal graphics library. OpenGL was designed and is maintained by an industry-wide review board composed of SGI, Microsoft, IBM, Intel, and DEC. Until recently, OpenGL was usually found only on UNIX workstations. However, with the availability of a standardized (and well-known) interface for 3-D graphics, along with advances in dedicated 3-D rendering hardware, it's possible to create some amazingly complicated and realistic scenes in Windows and render them quickly. In this article, I'll provide an overview of OpenGL and illustrate how you can start writing your own OpenGL programs.
OpenGL provides primitives for points, lines, and polygons. Everything you create is based on these three primitives. The library also provides support routines that draw curves, surfaces, or text; you can also create filled polygons (thereby creating a surface). Once you've created a scene out of primitives, you can specify lighting effects, specialized effects (fog or transparency), and viewing angle. OpenGL takes care of the rest: shading, hidden-surface removal, and perspective rendering. If you don't like the viewpoint, simply change it and OpenGL will recalculate the scene for you. In fact, once the objects are created, you can dynamically alter their location and rotation, your viewpoint, the lighting effects, shading, and so on; these, too, will be recalculated for you. The hard part is locating and describing the objects themselves.
OpenGL is designed to run efficiently as a state machine in a client/server model. For instance, in a typical environment, you might have one powerful computer generating the drawing commands (the server), while a networked client workstation receives these commands and does the actual rendering on its screen. While there's nothing in NT 3.5 preventing you from creating such a program using remote-procedure calls (RPCs), OpenGL works just as well if the same computer is both client and server. Again, the tricky part is learning how to create an OpenGL scene and trying to interface between OpenGL and Windows, since OpenGL (as a hardware-independent library) knows nothing about Windows, device contexts, pens, or brushes.
Three libraries are provided with the NT version of OpenGL, the main one being opengl32.lib. By convention, functions in this library (such as glDrawPixels()) use the prefix "gl". Next is the OpenGL utility library, glu32.lib, containing functions such as gluBeginPolygon(), which use the prefix "glu". These are helper routines for OpenGL that provide services such as creating a sphere, performing matrix manipulations, and tessellation. If you think of opengl32.lib as the workhorse of OpenGL, then the utility library provides higher-level functionality. The final library is the auxiliary library written for the OpenGL Programming Guide. Routines in this library contain an "aux" prefix, as in auxInitWindow(). These functions are not strictly part of OpenGL, and needn't be included for most OpenGL programs. However, you will likely find them in most OpenGL implementations, including NT's. I'll use the auxiliary library, since it allows you to ignore the Windows-specific portions of a program and concentrate on the OpenGL parts.
Finally, six new, implementation-specific interface routines allow OpenGL to work on a Windows platform. Interface routines like wglGetCurrentContext() use a "wgl" prefix and are referred to as "wiggle" routines. These routines provide the interface between straight OpenGL and Windows and are analogous to the "glx" interface functions (X Windows' interface for OpenGL) in an X Window System implementation.
In addition, four Win32 functions allow access to the pixel formats. These are important since you have to try to match your program's needs with your system's hardware. Finally, there's one Win32 function that deals with swapping the buffers in a double-buffered window.
Listing One (page 106) is an OpenGL program that displays a bouncing ball on a checkerboard surface; see Figure 1. I'm using the auxiliary library, which lets me ignore Windows and concentrate on OpenGL. It also lets me write more-traditional C code rather than Windows-style code. The first three aux functions in the localInitialize() procedure of Listing One initialize the display mode, set the window's size and position, and open the window. Clearly, using the aux library hides a lot of the Windows code.
After initialization, the next few routines in main() take function names as arguments and show how the auxiliary library operates. auxReshapeFunc is called when the window needs to be reshaped; auxKeyFunc, when a specified key is pressed; auxIdleFunc, when you have idle time; and auxMainLoop is the main loop of the program. In the Windows implementation, these functions all hide the Windows messaging system, making OpenGL programming straightforward. Of course, when you write an OpenGL program for Windows, you have to worry about all the other nastiness that accompanies writing for the Windows API, plus the additional worries of an OpenGL Windows app.
The biggest change that comes from rendering a 3-D scene is learning how to specify both object and a viewing volume. In the 2-D world, you could just specify a line to be drawn from, say, 100,100 to 200,300, and there it would be, on your screen. Things aren't that simple in 3-D, because 3-D objects are described by their vertices using x-, y-, and z-coordinates. The difficulty is compounded by the fact that you must specify the coordinates of both an object and the viewpoint.
When a vertex is rendered to the screen, it goes through a couple of transformation matrices. Figure 2 shows the steps that a single point goes through. An object is initially specified in what's usually called "object" coordinates, which are considered local for each object. The object is then usually translated, rotated, and scaled into "world" coordinates. Objects in world coordinates are positioned with respect to all other objects in the world. When everything is set, all of the viewing and projection calculations are performed to render your object to a collection of pixels on the screen.
When an object is created, its default origin is 0,0,0. Any initial transformations to an object are called "modeling transformations." For example, if you create a rendering of a car, you'd probably have one routine to draw a wheel in object coordinates, and then just call the routine four times with four different transformations that would place the wheels in the correct location and orientation about the car body in world coordinates. In this way, you can create complex objects out of simply rendered objects. When all of the objects are correctly positioned in world coordinates, we can specify the viewing transformation. This will determine the viewpoint from which we "see" the objects.
As a performance improvement, OpenGL combines the modeling transformations with the viewing transformation since the viewing and modeling matrices can be combined at this point. What this means for the programmer is that the viewing transformation is applied first and the modeling transformations follow. This is one of the trickier issues about 3-D graphics, particularly OpenGL's implementation.
Next, the projection matrix is applied to take the specified viewing volume and clip out everything outside it, along with parts of any object obscured by another object. The perspective division adjusts the results from the projection matrix (3-D coordinates) and gives you 2-D device coordinates. Finally, these 2-D coordinates are mapped to the physical screen by the viewport transformation. Fortunately, the only complex part of this whole procedure is the specification of the modelview matrix and the projection matrix. For now, I'll just use a simple set for both. The localReshape function in Listing One selects, initializes, then sets up the projection so that the result is a simple perspective projection. This is done each time the window is resized to maintain the correct aspect ratio. The localIdle function controls the modelview matrix, which is selected and initialized, and then translates our viewpoint along the z-axis. Next, rotations are applied along all three axes. In Listing One, all these values are controlled by the user, so that you can manipulate the view.
The real substance of the Listing One program is contained in two areas. The first is the visible part--the program functions that render the ball and the surface. The functions localDrawSurface and localDrawSphere are straightforward. The localDrawSphere function simply draws a white (glColor3f) solid sphere (auxSolidSphere) along the y-axis (glTranslatef). Since OpenGL is a state machine, you must first modify the state (in this case, the color and position). Hence, you set the color and position and then draw a sphere. Note that I've taken advantage of the aux library function to draw a sphere, rather than the more-complicated gluSphere.
Drawing the surface is similar, except that you have to explicitly create the surface out of polygons, and the polygons out of vertices. Inside the two nested for loops that divide up the surface into squares, OpenGL primitives are created between calls to glBegin and glEnd (auxSolidSphere handled this in localDrawSphere). This is similar to a WM_PAINT message, where you call BeginPaint, do some painting, then call EndPaint. In the case of OpenGL primitives, you signal OpenGL that you are going to create an object out of some vertices, construct the object, then signal you're done.
For the localDrawSurface function, the glBegin(GL_QUADS) call tells OpenGL that we are going to construct a four-sided polygon (quad). You set the color of a vertex with glColor3fv, then the position of the vertex with glVertex3fv. After the fourth vertex, glEnd signals that you are done. You do this for each square in the checkerboard surface.
Note that a more powerful form of glBegin could have been used for a simple checkerboard, making up the top side of a surface. However, I decided to add an interesting feature to the program: For the underside of the surface, I only draw alternating squares. If you rotate the view around the x- or z-axis, you can flip the scene over so that you are looking at it from underneath. From this viewpoint you can see the bouncing ball through the checkerboard! Try to imagine how many calculations are required to perform this feat and you'll quickly appreciate what OpenGL can do.
In my original plan, I was just going to render a scene of a ball bouncing on a red and blue checkered surface. With just a few additional lines of code, I added the capability to spin the scene around the y-axis, then the x- and z-axes. At this point, the bottom of the surface was visible, so I added code to change its color and place holes in it. The ease with which I added the code created a classic example of feature creep; I had to stop or I'd never finish. Here's how the scene is animated.
The main() procedure of Listing One makes a call to auxMainLoop(localDisplay). localDisplay is normally your display routine for nonanimated scenes. However, if you're creating an animated scene, the localIdle function is where the rendering is done. In the localIdle function the call to glClear clears out the display buffer, then adjusts the animation parameters in the call to localAdjustParameters. This adds spin to each axis and moves the ball along its trajectory. Next, set up the modelview matrix by first initializing it, then accounting for the rotations and translations. Then call the routines to draw the surface and sphere, and finally, since this is a double buffered window (as specified in the call to auxInitDisplayMode), swap the front and rear buffers, which sends the rendering to the screen.
With the program running in its startup state, you'll just see the ball hovering above the checkered surface. If you press the "a" key, you'll start the ball bouncing. The "a" key toggles the animation on and off. Eventually the bounces will diminish in magnitude, reaching a cutoff point at which the ball is reset to above the surface. The "x," "y," and "z" keys increase the rotation of their respective axes for each time through the animation loop. The more times you press a key, the larger each change will be through the loop. Press the uppercase letter to subtract from the rotation. The up and down arrow keys will move you closer and further away (add to or subtract from the initial z-axis translation). Actually, the center of the animation moves relative to the specified viewpoint. You'll see this when moving the surface away from you till it's beyond the far clipping plane of the viewing volume. If you just start the y-axis spinning from the initial position, you can see the far corner disappear when it rotates into the clipping plane. The "f" key will freeze the display, and the "r" key will reset it to the initial viewing parameters.
One thing you'll immediately notice about the animation is its constant speed. Since all of the matrix calculations are done each time through the loop, the values of the matrices are not important. In other words, the scene renders at the same rate even when the animation is running and spinning about the axes. The only thing that will affect the scene-rendering rate (aside from different hardware or a different scene) is the size of the window. If you make the scene full screen, the render rate drops off. If you make the window smaller, the speed picks up.
OpenGL is not a high-level toolkit, but it provides excellent capabilities that are pretty hard to program yourself. The low level of functionality provides opportunities for both video-hardware manufacturers, to provide dedicated OpenGL hardware, and third-party developers to provide high-level wrappers for 3-D graphics applications. We may, in fact, see an explosion of OpenGL applications including games, virtual reality, analytical graphics, and architectural applications.
Crain, Dennis. Windows NT OpenGL: Getting Started. Microsoft Developer Network CD-ROM, Disk #8, July 1994.
OpenGL Architecture Review Board. OpenGL Reference Manual. Reading, MA: Addison-Wesley, 1992.
OpenGL Architecture Review Board. OpenGL Programming Guide. Reading, MA: Addison-Wesley, 1992.
Prosie, Jeff. "Advanced 3-D Graphics for Windows NT 3.5: Introducing the OpenGL Interface, Part 1." Microsoft Systems Journal (October 1994).
Figure 1: An OpenGL program displaying the underside of a checkerboard surface.
Figure 2: The path from 3-D coordinate space to screen pixel.
// MS supplied file to turn off compiler warnings #include "glos.h" // OpenGL, utility, and aux header files #include <windows.h> #include <GL/gl.h> #include <GL/glu.h> #include <GL/glaux.h> // local functions static void localInitialize(int argc, char** argv); static void localDrawSurface( void ); static void localDrawSphere( void ); static void localAdjustParameters( void ); static void localAdjustRotationalParameters( float * , float * ); // These functions are called by the AUX library // These are CALLBACK, which just means treat them as cdecl functions static void CALLBACK localDisplay(void); static void CALLBACK localReshape(GLsizei w, GLsizei h); static void CALLBACK localIdle(void); static void CALLBACK Key_a(void); static void CALLBACK Key_Z(void); static void CALLBACK Key_z(void); static void CALLBACK Key_X(void); static void CALLBACK Key_x(void); static void CALLBACK Key_Y(void); static void CALLBACK Key_y(void); static void CALLBACK Key_r(void); static void CALLBACK Key_f(void); static void CALLBACK Key_up(void); static void CALLBACK Key_down(void); #define INITIAL_HEIGHT (7.0) #define INITIAL_ACCEL (0.1) #define INITIAL_ANGLE (15.0) #define SPHERE_RADIUS (0.5) #define ANGULAR_CHANGE (1.0) #define INITIAL_TRANSLATION (-15.0) #define MAX_DIMENSION (5.0) #define SUBDIVISION (10.0) // State variables int animate = 0; int freeze = 0; float sphere_height = INITIAL_HEIGHT; float sphere_drop_speed = 0; float sphere_drop_accel = INITIAL_ACCEL; float z_axis_rotation = 0; float z_axis_rotational_speed = 0; float y_axis_rotation = 0; float y_axis_rotational_speed = 0; float x_axis_rotation = INITIAL_ANGLE; float x_axis_rotational_speed = 0; float z_axis_translation = INITIAL_TRANSLATION; // main -- just like any other main you've seen before void main(int argc, char** argv) { // Initialize our program and OpenGL localInitialize(argc, argv); // if the window is resized, call this function auxReshapeFunc(localReshape); // Assign some keys to some functions auxKeyFunc(AUX_a, Key_a); auxKeyFunc(AUX_z, Key_z); auxKeyFunc(AUX_Z, Key_Z); auxKeyFunc(AUX_x, Key_x); auxKeyFunc(AUX_X, Key_X); auxKeyFunc(AUX_y, Key_y); auxKeyFunc(AUX_Y, Key_Y); auxKeyFunc(AUX_r, Key_r); auxKeyFunc(AUX_f, Key_f); auxKeyFunc(AUX_UP, Key_up); auxKeyFunc(AUX_DOWN, Key_down); // what to do with our idle time auxIdleFunc(localIdle); // which function to call when the window needs to be repainted auxMainLoop(localDisplay); } // localReshape -- called whenever the window is resized, moved, or // uncovered. The two arguments are the new windows width & height. static void CALLBACK localReshape(GLsizei w, GLsizei h) { GLfloat adjust_height, adjust_width; // Resize the viewport to the new window's size glViewport(0, 0, w, h); // scale the width/height by the size of the window so that // aspect ratio is retained, i.e., a sphere remains a sphere if ( w <= h ) { adjust_height = 1.0; adjust_width = (GLfloat)h/(GLfloat)w; } else { adjust_height = (GLfloat)w/(GLfloat)h; adjust_width = 1.0; } // Set up a projection matrix glMatrixMode(GL_PROJECTION); glLoadIdentity(); gluPerspective( 60.0, // field of view in degrees (GLfloat) w/(GLfloat) h, // aspect ratio 1.0, 20.0); } // Adjust a rotational value, keeping it between 0 and 360 degrees static void localAdjustRotationalParameters(float*rot_value,float*rot_rate ) { *rot_value += *rot_rate; *rot_value = *rot_value < 0 ? (*rot_value + 360) : (*rot_value > 360 ? (*rot_value - 360) : *rot_value ); } static void localAdjustParameters( void ) { if ( freeze ) return; localAdjustRotationalParameters(&x_axis_rotation,&x_axis_rotational_speed); localAdjustRotationalParameters(&y_axis_rotation,&y_axis_rotational_speed); localAdjustRotationalParameters(&z_axis_rotation,&z_axis_rotational_speed); if ( animate ) { sphere_drop_speed += sphere_drop_accel;// effect of gravity sphere_drop_speed *= 0.95; // effect of resistance sphere_height -= sphere_drop_speed; // Detect when we've hit the floor if ( sphere_height < 0 ) { sphere_height = -sphere_height*.95; sphere_drop_speed *= -0.95; sphere_drop_accel *= 0.95; // battle roundoff errors } // Detect when we've stopped bouncing if ( sphere_height <= 0.001 && abs(sphere_drop_speed) <= 0.001 ) { sphere_height = INITIAL_HEIGHT; // Start it over again sphere_drop_speed = 0.0; sphere_drop_accel = INITIAL_ACCEL; } } } // localDrawSphere -- Draw a sphere above (+Y) the surface static void localDrawSphere( void ) { if ( sphere_height <= 0 ) return; // Now create a sphere along the +Y axis glColor3f (.9, .9, 0.9); glTranslatef (0.0, SPHERE_RADIUS+sphere_height, 0.0); auxSolidSphere(SPHERE_RADIUS); } // localDrawSurface -- Draw a checkerboard surface, explicitly creating each // square make one side (the "front") two color, and the other side (the // "back") alternating squares and blanks. The surface is centered about // 0,0,0 and is perpendiculr to the Y axis static void localDrawSurface( void ) { int x,y; GLfloat vertices[4][3]; GLfloat red_color[3] = {0.8, 0.0, 0.0}; GLfloat blue_color[3] = {0.0, 0.0, 0.8}; GLfloat *color1, *color2, *color3, *color4; GLfloat mesh_delta = 2.0*MAX_DIMENSION/SUBDIVISION; for ( x=1 ; x <= SUBDIVISION ; x++ ) { for ( y=1 ; y <= SUBDIVISION ; y++ ) { // Orient them counter clockwise // vertice 1 vertices[0][0] = -MAX_DIMENSION+mesh_delta*(x-1); // x vertices[0][1] = 0.0; // y vertices[0][2] = -MAX_DIMENSION+mesh_delta*(y-1); // z // vertice 2 vertices[3][0] = -MAX_DIMENSION+mesh_delta*(x-0); // x vertices[3][1] = 0.0; // y vertices[3][2] = -MAX_DIMENSION+mesh_delta*(y-1); // z // vertice 3 vertices[2][0] = -MAX_DIMENSION+mesh_delta*(x-0); // x vertices[2][1] = 0.0; // y vertices[2][2] = -MAX_DIMENSION+mesh_delta*(y-0); // z // vertice 4 vertices[1][0] = -MAX_DIMENSION+mesh_delta*(x-1); // x vertices[1][1] = 0.0; // y vertices[1][2] = -MAX_DIMENSION+mesh_delta*(y-0); // z // Color squares such that four squares form a pattern if ( x%2 == 1 && y%2 == 1 ) // quadrant ul { color1 = blue_color; // ul color2 = blue_color; // ll color3 = red_color; // lr color4 = blue_color; // ur } else if ( x%2 == 1 && y%2 == 0 ) // quadrant ll { color1 = blue_color; color2 = blue_color; color3 = blue_color; color4 = red_color; } else if ( x%2 == 0 && y%2 == 1 ) // quadrant ur { color1 = blue_color; color2 = red_color; color3 = blue_color; color4 = blue_color; } else // quadrant lr { color1 = red_color; color2 = blue_color; color3 = blue_color; color4 = blue_color; } glBegin(GL_QUADS); glColor3fv ( color1 ); glVertex3fv(vertices[0]); glColor3fv ( color2 ); glVertex3fv(vertices[1]); glColor3fv ( color3 ); glVertex3fv(vertices[2]); glColor3fv ( color4 ); glVertex3fv(vertices[3]); glEnd(); // now, draw alternating back faces in different colors if ( (x+y)%2 ) { glBegin(GL_QUADS); // note that these "face" a different direction glColor3f (0.4, 0.4, 0.6); // blue-grey glVertex3fv(vertices[3]); glColor3f (0.0, 1.0, 0.0); // green glVertex3fv(vertices[2]); glColor3f (0.4, 0.4, 0.6); // blue-grey glVertex3fv(vertices[1]); glColor3f (1.0, 1.0, 0.); // yellow glVertex3fv(vertices[0]); glEnd(); } } } } // localIdle -- Called whenever there is idle time. Use it // for rendering frames when using double buffering static void CALLBACK localIdle(void) { // clear the viewport buffers, in this case the color & depth buffers // (there are other buffers we could include) glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); // Select the modelview matrix glMatrixMode(GL_MODELVIEW); // Initialize it glLoadIdentity(); // adjuset all of the animation and rotation parameters localAdjustParameters(); // apply Z axis translation. i.e. move viewpoint along the Z axis // (by default we're facing -Z with Y+ as the up vector) glTranslatef (0.0, 0.0, z_axis_translation); // apply the rotations glRotatef(x_axis_rotation, 1.0, 0.0, 0.0); glRotatef(y_axis_rotation, 0.0, 1.0, 0.0); glRotatef(z_axis_rotation, 0.0, 0.0, 1.0); // draw the objects localDrawSurface(); localDrawSphere(); // New Win32 function to swap buffers in a double-buffered window auxSwapBuffers(); } // localDisplay -- Called whenever we need to redisplay the scene static void CALLBACK localDisplay(void) { ; // do nothing for now, the Idle function takes care of it } // localInitialize -- Initializes program and sets up the initial OpenGL state static void localInitialize(int argc, char** argv) { // double buffering, RGBA mode, 16 bit depth buffer auxInitDisplayMode (AUX_DOUBLE | AUX_RGBA | AUX_DEPTH16 ); // create a default window centered at 0,0, that's 400 pixels wide // (note that this is shifted to Windows coordinate system) auxInitPosition (0, 0, 400, 400); // Open the window, specify the title auxInitWindow ("OPENGL DEMO"); // turn on depth testing glEnable( GL_DEPTH_TEST ); // turn on back face removal glEnable( GL_CULL_FACE ); } // These are the misc key functions static void CALLBACK Key_z(void) { z_axis_rotational_speed += ANGULAR_CHANGE; } static void CALLBACK Key_Z(void) { z_axis_rotational_speed -= ANGULAR_CHANGE; } static void CALLBACK Key_y(void) { y_axis_rotational_speed += ANGULAR_CHANGE; } static void CALLBACK Key_Y(void) { y_axis_rotational_speed -= ANGULAR_CHANGE; } static void CALLBACK Key_x(void) { x_axis_rotational_speed += ANGULAR_CHANGE; } static void CALLBACK Key_X(void) { x_axis_rotational_speed -= ANGULAR_CHANGE; } static void CALLBACK Key_up(void) { z_axis_translation += 1; } static void CALLBACK Key_down(void) { z_axis_translation -= 1; } static void CALLBACK Key_f(void) { // toggle all movement freeze = !freeze; } static void CALLBACK Key_a(void) { // toggle animation animate = !animate; } static void CALLBACK Key_r(void) { x_axis_rotation = INITIAL_ANGLE; z_axis_translation = INITIAL_TRANSLATION; z_axis_rotation = y_axis_rotation = 0; z_axis_rotational_speed = y_axis_rotational_speed = x_axis_rotational_speed = 0; } DDJ
Copyright © 1995, Dr. Dobb's Journal
Terms of Service | Privacy Statement | Copyright © 2024 UBM Tech, All rights reserved.