Building a 3D Engine in Perl
by Geoff Broadwell
|
Pages: 1, 2, 3
Cumulative Transformations
Unfortunately, no dice. The white cube is two units to the left, but the
yellow cube is right on top of the axis lines again, not two units to the right
as intended. This happened because glTranslate calls (and the
other transformation calls I'll show later) are cumulative. Unlike routines
such as glColor that simply set the current state, most
transformation calls instead modify the current state in a certain way.
Because of this, the first cube starts at (-2, 0, 0), and the second starts at
(-2, 0, 0) + (2, 0, 0) = (0, 0, 0) -- right back at the origin again.
The solution to this problem requires peeking under the covers a little bit. OpenGL transformation calls really just set up a special matrix representing the effect that the requested transformation has on coordinates. OpenGL then multiplies the current matrix by this new transformation matrix and replaces the current matrix with the results of the multiplication.
What I need to fix this problem is some way to save the current matrix before performing a transformation, and then restore it after I'm done with it. Thankfully, OpenGL actually maintains a stack of matrices of each type. I just need to push a copy of the current matrix onto the stack before drawing the white cube, and pop that copy off again afterwards to get back to the state before I did my translation. I'm going to do this for both cubes:
sub draw_view
{
draw_axes();
glColor(1, 1, 1);
glPushMatrix;
glTranslate(-2, 0, 0);
draw_cube();
glPopMatrix;
glColor(1, 1, 0);
glPushMatrix;
glTranslate( 2, 0, 0);
draw_cube();
glPopMatrix;
}
That's a bit better. The yellow cube now has its origin at (2, 0, 0), just as intended.
Other Transformations
Earlier I referred to other transformation calls; let's take a look at a few
of them. First, I'll scale the boxes (change their size). I'm going to scale
the left (white) box uniformly -- in other words, scaling each of its
dimensions by the same amount. To show the difference, I'll scale the right
(yellow) box non-uniformly, with each dimension scaled differently. Here's the
new draw_view:
sub draw_view
{
draw_axes();
glColor(1, 1, 1);
glPushMatrix;
glTranslate(-4, 0, 0);
glScale( 2, 2, 2);
draw_cube();
glPopMatrix;
glColor(1, 1, 0);
glPushMatrix;
glTranslate( 4, 0, 0);
glScale(.2, 1, 2);
draw_cube();
glPopMatrix;
}
For the white box, I just doubled each dimension; the parameters to
glScale are X, Y, and Z multipliers. For the yellow box, I shrunk
the X dimension by a factor of 5 (multiplied by .2), left Y alone, and doubled
the Z dimension. The boxes are now big enough that I've also pushed them
farther apart, hence the updated values for glTranslate that place
them four units on either side of the scene origin.
Watch the Rotation
I've done translation and scaling; next up is rotation. To save space here, I'll demonstrate on the yellow cube alone. Here's the new code snippet:
glColor(1, 1, 0);
glPushMatrix;
glRotate( 40, 0, 0, 1);
glTranslate( 4, 0, 0);
glScale(.2, 1, 2);
draw_cube();
glPopMatrix;
The parameters to glRotate are the number of degrees to rotate
and the axis around which to do the rotation. In this case, I chose to rotate 40 degrees around the Z axis (0, 0, 1). The direction of rotation follows the
general pattern in OpenGL -- a positive value means counterclockwise when
looking down the rotation axis toward the origin.
Order of Transforms
This produces a flying yellow box in the upper-right quadrant. Remember
when I said that each new transformation is cumulative? The order matters. To
understand why, I like to imagine each transformation as moving, rotating, or
scaling the coordinate system in which I draw my objects. In this case, by
rotating first, I certainly rotated the box, but I really rotated the entire
coordinate system in which I defined the box. This meant the
glTranslate call that immediately follows the rotation translated
out along a rotated X axis, 40 degrees above the scene's X axis, to be
precise.
I'll move the rotation after the other two transformations to fix that:
glTranslate( 4, 0, 0);
glScale(.2, 1, 2);
glRotate( 40, 0, 0, 1);
Now the box isn't flying, but it does appear squashed in an odd way. The problem here is that because the nifty, non-uniform scaling happens before the rotation, I'm now trying to rotate through a space where the dimensions are different sizes. Putting the rotation in the middle fixes it:
glTranslate( 4, 0, 0);
glRotate( 40, 0, 0, 1);
glScale(.2, 1, 2);
If you compare this rendering from this version with the program with no
glRotate call, you should see that it does the right thing
now.
Whoa, Deep!
The last item I wanted to bring up is what to do when something near the
back draws after something near the front. To see what I mean, I'll move the
white box so that instead of being four units to the left of the scene origin,
it is four units behind it (along the negative Z axis). That merely involves
changing the white box's glTranslate call from this:
glTranslate(-4, 0, 0);
to this:
glTranslate( 0, 0, -4);
As you can see, even though the white box should appear behind the axis lines, it instead appears in front because OpenGL drew it after the axis lines. By default, OpenGL assumes you intended to do this (it is more efficient to make this assumption), but I didn't. To fix this, I need to tell OpenGL to pay attention to the depth of the various objects in the scene and not to overwrite near objects with far ones.
To do this, I need to enable OpenGL's depth buffer. This is similar to the color buffer, which stores the color of every pixel drawn. Instead of storing the color, however, it stores the depth (distance from the viewpoint along the viewing direction) of every pixel. Just like the color buffer, I need to clear the depth buffer each frame. Instead of clearing it to black, OpenGL clears it to the maximum depth value, so that any later rendering within the visible scene will be closer.
I also need to tell OpenGL that it should perform a test each time it wants
to draw a pixel, comparing the depth of the new pixel with what's already in
the depth buffer. If the new pixel is farther from the viewer than the pixel
it is about to replace, it's safe to ignore the new pixel and to leave alone
the old color. Here's the updated prep_frame:
sub prep_frame
{
glClear(GL_COLOR_BUFFER_BIT |
GL_DEPTH_BUFFER_BIT );
glEnable(GL_DEPTH_TEST);
}
In this version, I tell glClear to clear both the color buffer
and the depth buffer. You can now see why the constant names end with
_BIT; they are, in fact, bit masks. The reason for
this odd interface is purely efficiency -- some OpenGL implementations can very
rapidly clear all requested buffers simultaneously, and making the request for
all needed buffers in just one call allows this optimization. As for the
choice of bit mask rather than a list of constants, SDL_Perl reflects the
underlying C interface, so that people comfortable with that can more easily
cross over to using OpenGL under Perl.
The second routine I call, glEnable, is actually one of the
most commonly used OpenGL routines, despite the fact that this is the first
we've seen of it. Much of the OpenGL current state is a set of flags that tell
OpenGL when to do (or not do) certain things. glEnable and the
corresponding glDisable set these flags as desired. In this case,
I turn on the flag that tells OpenGL to perform the depth test, throwing away
pixels drawn in the wrong order.
With these changes, we can now once again see the axis lines, this time in front of the white box where they belong.
Conclusion
The final results may look simple, but we've come a long way. I started with some basic boilerplate and a simple main loop. I didn't even load SDL or OpenGL or open a window. By the end, I'd added a window to draw on; projection and viewing setup; multiple objects of different types, built using different OpenGL primitives, drawn in different colors, and transformed several different ways; and correct handling of out-of-order drawing.
That's a lot for now, but we're just starting. Next time I'll cover moving the viewpoint, SDL keyboard handling, and compensating for frame rate variations. I'll build on the example source code built in this article, so feel free to download it and use it for your own applications.
In the meantime, if you'd like to learn more visit the OpenGL and SDL websites; each contains (and links to) mountains of information.
You must be logged in to the O'Reilly Network to post a talkback.
Showing messages 1 through 7 of 7.
- SDL_Perl
2005-03-09 21:00:48 Rom [Reply]
Any chance of pointing the now so skilled :) to the sdl_perl please
- SDL installation on Mac OS X
2004-12-28 13:23:32 jbellew [Reply]
I can't seem to get the SDL to install on Mac OS X. I'm currently running perl 5.8.6. Does anyone have any links about getting it installed correctly?
- SDL Setup on Win32
2004-12-22 21:49:33 cadayton12 [Reply]
I've broken my pick trying to get the SDL components and SDL_Perl 1.2.6 working on Win32.
www.libsdl.org is basically broken.
Perhaps the next article can provide links to the proper installation of the components.
Merry Christmas
- more! i want more!
2004-12-21 03:12:44 timor [Reply]
this tutorial is so much fun to play with, when is the next part coming?
- Screenshots!
2004-12-13 11:54:14 sozin [Reply]
Every good gaming article demands screenshots. Show us the money! :-)
- Example source code
2004-12-07 08:38:30 carbon [Reply]
It seems the example source code is not currently available. Any chance we can get that resolved?- Example source code
2004-12-07 08:55:16 cvaldez [Reply]
Sorry about that. The correct link is:
http://www.perl.com/2004/12/01/examples/perl_opengl_examples.tar.gz
and has been corrected in the article.
-Chris
Perl.com Producer
- Example source code



