Sign In/My Account | View Cart  
advertisement


Listen Print Discuss

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.