Section 2.3
Drawing in 3D
Finally, we are ready to create some three-dimensional scenes. We will look first at how to create three-dimensional objects and how to place them into the world. This is geometric modeling in 3D, and is not so different from modeling in 2D. You just have to deal with the extra coordinate.
2.3.1 Geometric Modeling
We have already seen how to use glBegin/glEnd to draw primitives such as lines and polygons in two dimensions. These commands can be used in the same way to draw primitives in three dimensions. To specify a vertex in three dimensions, you can call gl.glVertex3f(x,y,z) (or gl.glVertex3d(x,y,z)). You can also continue to use glVertex2f and glVertex2d to specify points with z-coordinate equal to zero. You can use glPushMatrix and glPopMatrix to implement hierarchical modeling, just as in 2D.
Modeling transformations are an essential part of geometric modeling. Scaling and translation are pretty much the same in 3D as in 2D. Rotation is more complicated. Recall that the OpenGL command for rotation is gl.glRotatef(d,x,y,z) (or glRotated), where d is the measure of the angle of rotation in degrees and x, y, and z specify the axis of rotation. The axis passes through (0,0,0) and the point (x,y,z). However, this does not fully determine the rotation, since it does not specify the direction of rotation. Is it clockwise? Counterclockwise? What would that even mean in 3D?
The direction of rotation in 3D follows the right-hand rule (assuming that you are using a right-handed coordinate system). To determine the direction of rotation for gl.glRotatef(d,x,y,z), place your right hand at (0,0,0), with your thumb pointing at (x,y,z). The direction in which your fingers curl will be the direction of a positive angle of rotation; a negative angle of rotation will produce a rotation in the opposite direction. (If you happen to use a left-handed coordinate system, you would use your left hand in the same way to determine the positive direction of rotation.) When you want to apply a rotation in 3D, you will probably find yourself literally using your right hand to figure out the direction of rotation.
Let's do a simple 3D modeling example. Think of a paddle wheel, with a bunch of flat flaps that rotate around a central axis. For this example, we will just draw the flaps, with nothing physical to support them. Three paddle wheels, with different numbers of flaps, are drawn in the following applet:
You can make the wheels rotate by clicking on the checkbox below the image. The animation can improve the illusion of three dimensions. You can also drag the mouse on this image to rotate the view. This lets you look at the paddle wheels from different points of view, which can also make them look more three-dimensional. The mouse action in this applet is enabled by an object of type TrackBall, another of the classes defined in the glutil source directory.
To produce a model of a paddle wheel, we can start with one flap, drawn as a trapezoid lying in the xy-plane above the origin:
gl.glBegin(GL.GL_POLYGON); gl.glVertex2d(-0.7,1); gl.glVertex2d(0.7,1); gl.glVertex2d(1,2); gl.glVertex2d(-1,2); gl.glEnd();
We can make the complete wheel by making copies of this flap, rotated by various amounts around the x-axis. If paddles is an integer giving the number of flaps that we want, then the angle between each pair of flaps will be 360/paddles. Recalling that successive transforms are multiplied together, we can draw the whole set of paddles with a for loop:
for (int i = 0; i < paddles; i++) { gl.glRotated(360.0/paddles, 1, 0, 0); gl.glBegin(GL.GL_POLYGON); gl.glVertex2d(-0.7,1); gl.glVertex2d(0.7,1); gl.glVertex2d(1,2); gl.glVertex2d(-1,2); gl.glEnd(); }
The first paddle is rotated by 360/paddles degrees. By the time the second paddle is drawn, two such rotations have been applied, so the second paddle is rotated by 2*(360/(paddles). As we continue, the paddles are spread out to cover a full circle. If we want to make an animation in which the paddle wheel rotates, we can apply an additional rotation, depending on the frameNumber, to the wheel as a whole.
To draw the set of three paddle wheels, we can write a method to draw a single wheel and then call that method three times, with different colors and translations in effect for each call. You can see how this is done in the source code for the program PaddleWheels.java.
2.3.2 Some Complex Shapes
Complex shapes can be built up out of large numbers of polygons, but it's not always easy to see how to do so. OpenGL has no built-in complex shapes, but there are utility libraries that provide subroutines for drawing certain shapes. One of the most basic libraries is GLUT, the OpenGL Utilities Toolkit. Jogl includes a partial implementation of GLUT in the class com.sun.opengl.util.GLUT. GLUT includes methods for drawing spheres, cylinders, cones, and regular polyhedra such as cubes and dodecahedra. The curved shapes are actually just approximations made out of polygons. To use the drawing subroutines, you should create an object, glut, of type GLUT. You will only need one such object for your entire program, and you can create it in the init method. Then, you can draw various shapes by calling methods such as
glut.glutSolidSphere(0.5, 40, 20);
This draws a polyhedral approximation of a sphere. The sphere is centered at the origin, with its axis lying along the z-axis. The first parameter gives the radius of the sphere. The second gives the number of "slices" (like lines of longitude or the slices of an orange), and the third gives the number of "stacks" (divisions perpendicular to the axis of the sphere, like lines of latitude). Forty slices and twenty stacks give quite a good approximation for a sphere. You can check the GLUT API documentation for more information.
Because of some limitations to the GLUT drawing routines (notably the fact that they don't support textures), I have written a few shape classes of my own. Three shapes -- sphere, cylinder, and cone -- are defined by the classes UVSphere, UVCylinder, and UVCone in the sample source package glutil. To draw a sphere, for example, you should create an object of type UVSphere. For example,
sphere = new UVSphere(0.5, 40, 20);
Similarly to the GLUT version, this represents a sphere with radius 0.5, 40 slices, and 20 stacks, centered at the origin and with its axis along the z-axis. Once you have the object, you can draw the sphere in an OpenGL context gl by saying
sphere.render(gl);
Cones and cylinders are used in a similar way, except that they have a height in addition to a radius:
cylinder = new UVCylinder(0.5,1.5,15,10); // radius, height, slices, stacks
The cylinder is drawn with its base in the xy-plane, centered at the origin, and with its axis along the positive z-axis. By default, both the top and bottom ends of the cylinder are drawn, but you can turn this behavior off by calling cylinder.setDrawTop(false) and/or cylinder.setDrawBottom(false). Using a UVCone is very similar, except that a cone doesn't have a top, only a bottom.
I will use these three shape classes in several examples throughout the rest of this chapter.
2.3.3 Optimization and Display Lists
The drawing commands that we have been using work fine, but they have a problem with efficiency. To understand why, you need to understand something of how OpenGL works. The calculations involved in computing and displaying an image of a three-dimensional scene can be immense. Modern desktop computers have graphics cards -- dedicated, specialized hardware that can do these calculations at very high speed. In fact, the graphics cards that come even in inexpensive computers today would have qualified as supercomputers a decade or so ago.
It is possible, by the way, to implement OpenGL on a computer without any graphics card at all. The computations and drawing can be done, if necessary, by the computer's main CPU. When this is done, however, graphics operations are much slower -- slow enough to make many 3D programs unusable. Using a graphics card to perform 3D graphics operations at high speed is referred to as 3D hardware acceleration.
OpenGL is built into almost all modern graphics cards, and most OpenGL commands are executed on the card's specialized hardware. Although the commands can be executed very quickly once they get to the graphics card, there is a problem: The commands, along with all the data that they require, have to transmitted from your computer's CPU to the graphics card. This can be a significant bottleneck, and the time that it takes to transmit all that information can be a real drag on graphics performance. Newer versions of OpenGL have introduced various techniques that can help to overcome the bottleneck. In this section, we will look at one of these optimization strategies, display lists. Display lists have been part of OpenGL from the beginning.
Display lists are useful when the same sequence of OpenGL commands will be used several times. A display list is a list of graphics commands that can be stored on a graphics card (though there is no guarantee that they actually will be). The contents of the display list only have to be transmitted once from the CPU to the card. Once a list has been created, it can be "called," very much like a subroutine. The key point is that calling a list requires only one OpenGL command. Although the same list of commands still has to be executed, only one command has to be transmitted from the CPU to the graphics card, and the full power of hardware acceleration can be used to execute the commands at the highest possible speed.
One difference between display lists and subroutines is that display lists don't have parameters. This limits their usefulness, since its not possible to customize their behavior by calling them with different parameter values. However, calling a display list twice can result in two different effects, since the effect can depend on the OpenGL state at the time the display list is called. For example, a display list that generates the geometry for a sphere can draw spheres in different locations, as long as different modeling transforms are in effect each time the list is called. The list can also produce spheres of different colors, as long as the drawing color is changed between calls to the list.
Here, for example is an applet that shows 1331 spheres, arranged in a cube that has eleven spheres along each edge.
Each sphere in the picture is a different color and is in a different location. Exactly the same OpenGL commands are used to draw each sphere, but before drawing each sphere, the transform and color are changed. This is a natural place to use a display list. We can store the commands for drawing one sphere in a display list, and call that list once for each sphere that we want to draw. The graphics commands that draw the sphere only have to be sent to the card once. Then each of the 1331 spheres can be drawn by sending a single command to the card to call the list (plus a few commands to change the color and transform). Note that we are also saving some significant time on the main CPU, since it only has to generate the sphere-drawing commands once instead of 1331 times.
In the applet, you can rotate the cube of spheres by dragging the mouse over the image. You can turn the use of display lists on and off using the checkbox below the image. You are likely to see a significant change in the time that it takes to render the image, depending on whether or not display lists are used. However, the exact results will depend very much on the OpenGL implementation that you are using. On my computer, with a good graphics card and hardware acceleration, the rendering with display lists is fast enough to produce very smooth rotation. Without display lists, the rendering is about five times slower, and the rotation is a bit jerky but still usable. Without hardware acceleration, the rendering might take so long that you get no feeling at all of natural rotation.
I hope that this has convinced you that display lists can be very worthwhile. Fortunately, they are also fairly easy to use.
Remember that display lists are meant to be stored on the graphics card, so it is the graphics card that has to manage the collection of display lists that have been created. Display lists are identified by integer code numbers. If you want to use a display list, you first have to ask the graphics card for an integer to identify the list. (You can't just make one up, since the one that you pick might already be in use.) This is done with a command such as
listID = gl.glGenLists(1);
The return value is an int which will be the identifier for the list. The parameter to glGenLists is also an int. You can actually ask for several list IDs at once; the parameter tells how many you want. The list IDs will be consecutive integers, so that if listA is the return value from gl.glGenLists(3), then the identifiers for the three lists will be listA, listA + 1, and listA + 2.
Once you've allocated a list in this way, you can store commands into it. If listID is the ID for the list, you would do this with code of the form:
gl.glNewList(listID, GL.GL_COMPILE); ... // Generate OpenGL commands to store in the list. gl.glEndList();
Note that the previous contents of the list, if any, will be deleted and relaced. The parameter GL.GL_COMPILE means that you only want to store commands into the list, not execute them. If you use the alternative parameter GL.GL_COMPILE_AND_EXECUTE, then the commands will be executed immediately as well as stored in the list for later reuse.
Most, but not all, OpenGL commands can be placed into a display list. In particular, commands for generating geometry, applying transforms, changing the color, and enabling and disabling various features can be placed into lists. You can even add commands for calling other lists. In addition to OpenGL commands, you can have other Java code between glNewList and glEndList. For example, you can use a for loop to generate a large number of vertices. But remember that the list only stores OpenGL commands, not the Java code that generates them For example, if you use a for loop to call glVertex3f 10,000 times, then the display list will contain 10,000 individual glVertex3f commands, not a for loop. Furthermore, as the parameters to these commands, the list only stores numerical values, not references to variables. So if you call
gl.glVertex3d(x,y,z);
between glNewList and glEndList, only the values of x, y, and z at the time glVertex3d is called are stored into the list. If you change the values of the variables later, the change has no effect on the list or what it does.
Once you have a list, you can call the list with the command
gl.glCallList(listID);
The effect of this command, remember, is to tell the graphics card to execute a list that it has already stored.
If you are going to reuse a list many times, it can make sense to create it in the init method and keep it around as long as your program will run. You can also, of course, make them on the fly, as needed, in the display method. However, a graphics card has only a limited amount of memory for storing display lists, so you shouldn't keep a list around longer than you need it. You can tell the graphics card that a list is no longer needed by calling
gl.glDeleteLists(listID, 1);
The second parameter in this method call plays the same role as the parameter in glGenLists; that is, it allows you delete several sequentially numbered lists; deleting one list at a time is probably the most likely use. Deleting a list when you are through with it allows the graphics card to reuse the memory used by that list.
As an example of using display lists, you can look at the source code for the program that draws the ``cube of spheres,'' ColorCubeOfSpheres.java. That program is complicated somewhat by the option to use or not use the display list. Let's look at some slightly modified code that always uses lists.
The program uses an object of type UVSphere to draw a sphere. The display list is created in the init method, and the UVSphere object is used to generate the commands that go into the list. The integer identifier for the list is displayList:
UVSphere sphere = new UVSphere(0.4); // For drawing a sphere of radius 0.4. displayList = gl.glGenLists(1); // Allocate the list ID. gl.glNewList(displayList, GL.GL_COMPILE); sphere.render(gl); gl.glEndList();
When sphere.render(gl) is called, all the OpenGL commands that it generates are channeled into the list, instead of being executed immediately. In the display method, the display list is called 1331 times in a set of triply nested for loops:
for (int i = 0; i <= 10; i++) { float r = i/10.0f; // Red component of sphere color. for (int j = 0; j <= 10; j++) { float g = j/10.0f; // Green component of sphere color. for (int k = 0; k <= 10; k++) { float b = k/10.0f; // Blue component of sphere color. gl.glColor3f(r,g,b); // Set the color for the sphere. gl.glPushMatrix(); gl.glTranslatef(i-5,j-5,k-5); // Translate the sphere. gl.glCallList(displayList); // Call display list to draw sphere. gl.glPopMatrix(); } } }
The glCallList in this code replaces a call to sphere.render that would otherwise be used to draw the sphere directly.