Building Solid Objects
Composing a solid object out of triangles (or any other polygon) involves more than assembling a series of vertices in a 3D coordinate space. Let's examine the sample program TRIANGLE, which uses two triangle fans to create a cone in our viewing volume. The first fan produces the cone shape, using the first vertex as the point of the cone and the remaining vertices as points along a circle further down the z-axis. The second fan forms a circle and lies entirely in the xy plane, making up the bottom surface of the cone.
The output from TRIANGLE is shown in Figure 3.18. Here, you are looking directly down the z-axis and can see only a circle composed of a fan of triangles. The individual triangles are emphasized by coloring them alternately green and red.
The code for the SetupRC and RenderScene functions is shown in Listing 3.8. (This listing contains some unfamiliar variables and specifiers that are explained shortly.) This program demonstrates several aspects of composing 3D objects. Right-click in the window, and you will notice an Effects menu; it will be used to enable and disable some 3D drawing features so we can explore some of the characteristics of 3D object creation. We describe these features as we progress.
Figure 3.18 Initial output from the TRIANGLE sample program.
Listing 3.8 SetupRC and RenderScene Code for the TRIANGLE Sample Program
// This function does any needed initialization on the rendering // context. void SetupRC() { // Black background glClearColor(0.0f, 0.0f, 0.0f, 1.0f ); // Set drawing color to green glColor3f(0.0f, 1.0f, 0.0f); // Set color shading model to flat glShadeModel(GL_FLAT); // Clockwise-wound polygons are front facing; this is reversed // because we are using triangle fans glFrontFace(GL_CW); } // Called to draw scene void RenderScene(void) { GLfloat x,y,angle; // Storage for coordinates and angles int iPivot = 1; // Used to flag alternating colors // Clear the window and the depth buffer glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); // Turn culling on if flag is set if(bCull) glEnable(GL_CULL_FACE); else glDisable(GL_CULL_FACE); // Enable depth testing if flag is set if(bDepth) glEnable(GL_DEPTH_TEST); else glDisable(GL_DEPTH_TEST); // Draw the back side as a wireframe only, if flag is set if(bOutline) glPolygonMode(GL_BACK,GL_LINE); else glPolygonMode(GL_BACK,GL_FILL); // Save matrix state and do the rotation glPushMatrix(); glRotatef(xRot, 1.0f, 0.0f, 0.0f); glRotatef(yRot, 0.0f, 1.0f, 0.0f); // Begin a triangle fan glBegin(GL_TRIANGLE_FAN); // Pinnacle of cone is shared vertex for fan, moved up z-axis // to produce a cone instead of a circle glVertex3f(0.0f, 0.0f, 75.0f); // Loop around in a circle and specify even points along the circle // as the vertices of the triangle fan for(angle = 0.0f; angle < (2.0f*GL_PI); angle += (GL_PI/8.0f)) { // Calculate x and y position of the next vertex x = 50.0f*sin(angle); y = 50.0f*cos(angle); // Alternate color between red and green if((iPivot %2) == 0) glColor3f(0.0f, 1.0f, 0.0f); else glColor3f(1.0f, 0.0f, 0.0f); // Increment pivot to change color next time iPivot++; // Specify the next vertex for the triangle fan glVertex2f(x, y); } // Done drawing fan for cone glEnd(); // Begin a new triangle fan to cover the bottom glBegin(GL_TRIANGLE_FAN); // Center of fan is at the origin glVertex2f(0.0f, 0.0f); for(angle = 0.0f; angle < (2.0f*GL_PI); angle += (GL_PI/8.0f)) { // Calculate x and y position of the next vertex x = 50.0f*sin(angle); y = 50.0f*cos(angle); // Alternate color between red and green if((iPivot %2) == 0) glColor3f(0.0f, 1.0f, 0.0f); else glColor3f(1.0f, 0.0f, 0.0f); // Increment pivot to change color next time iPivot++; // Specify the next vertex for the triangle fan glVertex2f(x, y); } // Done drawing the fan that covers the bottom glEnd(); // Restore transformations glPopMatrix(); // Flush drawing commands glFlush(); }
Setting Polygon Colors
Until now, we have set the current color only once and drawn only a single shape. Now, with multiple polygons, things get slightly more interesting. We want to use different colors so we can see our work more easily. Colors are actually specified per vertex, not per polygon. The shading model affects whether the polygon is solidly colored (using the current color selected when the last vertex was specified) or smoothly shaded between the colors specified for each vertex.
The line
glShadeModel(GL_FLAT);
tells OpenGL to fill the polygons with the solid color that was current when the polygon's last vertex was specified. This is why we can simply change the current color to red or green before specifying the next vertex in our triangle fan. On the other hand, the line
glShadeModel(GL_SMOOTH);
would tell OpenGL to shade the triangles smoothly from each vertex, attempting to interpolate the colors between those specified for each vertex. You'll learn much more about color and shading in Chapter 5.
Hidden Surface Removal
Hold down one of the arrow keys to spin the cone around, and don't select anything from the Effects menu yet. You'll notice something unsettling: The cone appears to be swinging back and forth plus and minus 180°, with the bottom of the cone always facing you, but not rotating a full 360°. Figure 3.19 shows this effect more clearly.
Figure 3.19 The rotating cone appears to be wobbling back and forth.
This wobbling happens because the bottom of the cone is drawn after the sides of the cone are drawn. No matter how the cone is oriented, the bottom is drawn on top of it, producing the "wobbling" illusion. This effect is not limited to the various sides and parts of an object. If more than one object is drawn and one is in front of the other (from the viewer's perspective), the last object drawn still appears over the previously drawn object.
You can correct this peculiarity with a simple feature called depth testing. Depth testing is an effective technique for hidden surface removal, and OpenGL has functions that do this for you behind the scenes. The concept is simple: When a pixel is drawn, it is assigned a value (called the z value) that denotes its distance from the viewer's perspective. Later, when another pixel needs to be drawn to that screen location, the new pixel's z value is compared to that of the pixel that is already stored there. If the new pixel's z value is higher, it is closer to the viewer and thus in front of the previous pixel, so the previous pixel is obscured by the new pixel. If the new pixel's z value is lower, it must be behind the existing pixel and thus is not obscured. This maneuver is accomplished internally by a depth buffer with storage for a depth value for every pixel on the screen. Most all of the samples in this book use depth testing.
To enable depth testing, simply call
glEnable(GL_DEPTH_TEST);
Depth testing is enabled in Listing 3.8 when the bDepth variable is set to True, and it is disabled if bDepth is False:
// Enable depth testing if flag is set if(bDepth) glEnable(GL_DEPTH_TEST); else glDisable(GL_DEPTH_TEST);
The bDepth variable is set when you select Depth Test from the Effects menu. In addition, the depth buffer must be cleared each time the scene is rendered. The depth buffer is analogous to the color buffer in that it contains information about the distance of the pixels from the observer. This information is used to determine whether any pixels are hidden by pixels closer to the observer:
// Clear the window and the depth buffer glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
A right-click with the mouse opens a pop-up menu that allows you to toggle depth testing on and off. Figure 3.20 shows the TRIANGLE program with depth testing enabled. It also shows the cone with the bottom correctly hidden behind the sides. You can see that depth testing is practically a prerequisite for creating 3D objects out of solid polygons.
Figure 3.20 The bottom of the cone is now correctly placed behind the sides for this orientation.
Culling: Hiding Surfaces for Performance
You can see that there are obvious visual advantages to not drawing a surface that is obstructed by another. Even so, you pay some performance overhead because every pixel drawn must be compared with the previous pixel's z value. Sometimes, however, you know that a surface will never be drawn anyway, so why specify it? Culling is the term used to describe the technique of eliminating geometry that we know will never be seen. By not sending this geometry to your OpenGL driver and hardware, you can make significant performance improvements. One culling technique is backface culling, which eliminates the backsides of a surface.
In our working example, the cone is a closed surface, and we never see the inside. OpenGL is actually (internally) drawing the back sides of the far side of the cone and then the front sides of the polygons facing us. Then, by a comparison of z buffer values, the far side of the cone is either overwritten or ignored. Figures 3.21a and 3.21b show our cone at a particular orientation with depth testing turned on (a) and off (b). Notice that the green and red triangles that make up the cone sides change when depth testing is enabled. Without depth testing, the sides of the triangles at the far side of the cone show through.
Figure 3.21a With depth testing.
Figure 3.21b Without depth testing.
Earlier in the chapter, we explained how OpenGL uses winding to determine the front and back sides of polygons and that it is important to keep the polygons that define the outside of our objects wound in a consistent direction. This consistency is what allows us to tell OpenGL to render only the front, only the back, or both sides of polygons. By eliminating the back sides of the polygons, we can drastically reduce the amount of necessary processing to render the image. Even though depth testing will eliminate the appearance of the inside of objects, internally OpenGL must take them into account unless we explicitly tell it not to.
Backface culling is enabled or disabled for our program by the following code from Listing 3.8:
// Clockwise-wound polygons are front facing; this is reversed // because we are using triangle fans glFrontFace(GL_CW); ... ... // Turn culling on if flag is set if(bCull) glEnable(GL_CULL_FACE); else glDisable(GL_CULL_FACE);
Note that we first changed the definition of front-facing polygons to assume clockwise winding (because our triangle fans are all wound clockwise).
Figure 3.22 demonstrates that the bottom of the cone is gone when culling is enabled. The reason is that we didn't follow our own rule about all the surface polygons having the same winding. The triangle fan that makes up the bottom of the cone is wound clockwise, like the fan that makes up the sides of the cone, but the front side of the cone's bottom section is then facing the inside (see Figure 3.23).
Figure 3.22 The bottom of the cone is culled because the front-facing triangles are inside.
Figure 3.23 How the cone was assembled from two triangle fans.
We could have corrected this problem by changing the winding rule, by calling
glFrontFace(GL_CCW);
just before we drew the second triangle fan. But in this example, we wanted to make it easy for you to see culling in action, as well as set up for our next demonstration of polygon tweaking.
Why Do We Need Backface Culling?
You might wonder, "If backface culling is so desirable, why do we need the ability to turn it on and off?" Backface culling is useful when drawing closed objects or solids, but you won't always be rendering these types of geometry. Some flat objects (such as paper) can still be seen from both sides. If the cone we are drawing here were made of glass or plastic, you would actually be able to see the front and the back sides of the geometry. (See Chapter 6 for a discussion of drawing transparent objects.)
Polygon Modes
Polygons don't have to be filled with the current color. By default, polygons are drawn solid, but you can change this behavior by specifying that polygons are to be drawn as outlines or just points (only the vertices are plotted). The function glPolygonMode allows polygons to be rendered as filled solids, as outlines, or as points only. In addition, you can apply this rendering mode to both sides of the polygons or only to the front or back. The following code from Listing 3.8 shows the polygon mode being set to outlines or solid, depending on the state of the Boolean variable bOutline:
// Draw back side as a polygon only, if flag is set if(bOutline) glPolygonMode(GL_BACK,GL_LINE); else glPolygonMode(GL_BACK,GL_FILL);
Figure 3.24 shows the back sides of all polygons rendered as outlines. (We had to disable culling to produce this image; otherwise, the inside would be eliminated and you would get no outlines.) Notice that the bottom of the cone is now wireframe instead of solid, and you can see up inside the cone where the inside walls are also drawn as wireframe triangles.
Figure 3.24 Using glPolygonMode to render one side of the triangles as outlines.