Building a 3D Engine in Perl, Part 2
by Geoff Broadwell
|
Pages: 1, 2, 3, 4, 5
Animating the View
To understand what's really going on with an odd transformation, it helps me to turn it into a short animation. I start the animation with a very small transformation and keep increasing it until well past the intended level. This way, I can see the effect of both smaller and larger changes.
To do this, I need a few more frames in the animation. I can do this by
changing the last line in draw_frame:
$done = 1 if $frame == 10;
I also want the rotation to animate with each frame:
sub set_view_3d
{
glTranslate(-6, -2, -10);
glRotate(18 * $frame, 0, 1, 0);
}
This chops the rotation into 18 degree increments, starting at frame 1 with an 18 degree rotation and ending at frame 10 with a 180 degree rotation.
Running this program shows what is happening. The scene rotates counterclockwise around its origin, the intersection point of the axis lines. I wanted to rotate the viewpoint, but I rotated the objects instead. Just reversing the sign won't do the trick. That will rotate the scene the other direction (clockwise), but it won't rotate around the viewpoint--it will still rotate around the scene origin.
In the first article, I described how to visualize a series of transforms by thinking about transforming the local coordinate system of the objects in a series of steps. Looking at the code above, you can see that it first translates the scene origin away and then rotates around that new origin. To rotate around the viewpoint, I need to rotate first and then translate the scene away:
sub set_view_3d
{
glRotate(18 * $frame, 0, 1, 0);
glTranslate(-6, -2, -10);
}
This now rotates around the viewpoint, but because it rotates 180 degrees starting from dead ahead, the scene ends up behind the viewpoint. To start the view so that the scene is on one side and then rotates to be on the other, I simply offset the angle:
sub set_view_3d
{
glRotate(-90 + 18 * $frame, 0, 1, 0);
glTranslate(-6, -2, -10);
}
At frame 1, the rotation angle is -90 + 18 * 1 = -72 degrees. At frame 10, the angle is -90 + 18 * 10 = 90 degrees. Perfect.
Stop and Turn Around
There's only one little problem: it's going the wrong way! I wanted to do a counterclockwise rotation of the view, but that should make the scene appear to rotate clockwise around the viewpoint. Imagine standing in front of a landmark, taking a picture. Looking through the viewfinder, you might notice that the landmark is a bit left of center. To center it, turn slightly left (counterclockwise as seen from above, or around +Y in the default OpenGL coordinate system). This would make the landmark appear to move clockwise around you (again as seen from above), moving it from the left side of the viewfinder to the center.
In this case, reverse the angle's sign:
sub set_view_3d
{
glRotate(90 - 18 * $frame, 0, 1, 0);
glTranslate(-6, -2, -10);
}
In fact, every transformation of the view is equivalent to the opposite transformation of every object in the scene. You must reverse the sign of the coordinates in a translation, reverse the sign of the angle in a rotation, or take the inverse of the factors in a scaling (shrinking the viewer to half size makes everything appear twice as big). As we saw before, you must reverse the order of rotation and translation as well.
Scaling is a special case. Inverting the factors works, but you must still do the scaling after the translation to achieve the expected effect, rather than following the rule for rotation and translation and reversing the transformation order completely. The reason is that scaling before the translation scales the translation also. Scaling by (2, 2, 2) would double the size of all of the objects in the scene, but it would also put them twice as far away, making them appear the same size. I'll skip the code for this and leave it as an exercise for the reader. Go ahead, have fun.
If you decide to give view scaling a try, remember that all distances will
change. This affects some non-obvious things such as the third and fourth
arguments to gluPerspective (the distance to the nearest and
farthest objects OpenGL will render).
Smoothing It Out
After watching these animations for a while, the jerkiness really begins to
bother me, and because I doubled the number of animation frames, it takes twice
as long to finish a run. Both of these problems relate to the second-long
sleep at the end of draw_frame. I should be able to fix them by
shortening the sleep to half a second:
sleep .5;
Chances are, that doesn't yield quite the respected result. On my system,
there's a blur for a fraction of a second, and the whole run is done.
Unfortunately, the builtin Perl sleep function only handles
integer seconds, so .5 truncates to 0 and the sleep returns almost
instantly.
Luckily, SDL provides a millisecond-resolution delay function,
SDL::Delay. To use it, I add another subroutine to handle the
delay, translating between seconds and milliseconds:
sub delay
{
my $seconds = shift;
SDL::Delay($seconds * 1000);
}
Now, changing the sleep call to delay fixes it:
delay(.5);
The movement is faster and it only takes five seconds to complete the entire animation again, but this code still wastes the available performance of the system. I want the animation to be as smooth as the system allows, while keeping the rotation speed (and total time) constant. To implement this, I need to give the code a sense of time. First, I add another global to keep the current time:
my ($time);
At this point, my editor likely just sprayed his screen with whatever he's drinking and coughed "Another global?!?" I'll address that later in this article during the refactoring.
To update the time, I need a couple more functions:
sub update_time
{
$time = now();
}
sub now
{
return SDL::GetTicks() / 1000;
}
now calls SDL's GetTicks function, which returns
the time since SDL initialization in milliseconds. It converts the result back
to seconds for convenience elsewhere. update_time uses
now to keep the global $time up to date.
main_loop uses this to update the time before rendering the
frame:
sub main_loop
{
while (not $done) {
$frame++;
update_time();
do_frame();
}
}
Because this version won't artificially slow the animation, I make two
changes to draw_frame. I remove the delay call and
change the animation end test to check whether the time has reached five seconds,
instead of whether frame ten has been drawn.
sub draw_frame
{
set_projection_3d();
set_view_3d();
draw_view();
print '.';
$done = 1 if $time >= 5;
}
Finally, set_view_3d must base its animation on the current
time instead of the current frame. Our current rotation speed is 18 degrees per
frame. With 2 frames per second, that comes to 36 degrees per second:
sub set_view_3d
{
glRotate(90 - 36 * $time, 0, 1, 0);
glTranslate(-6, -2, -10);
}
This version should appear much smoother. On my system, the dots printed for each frame scroll up the terminal window. If you run this program multiple times, you'll notice the number of frames (and hence dots) varies. Small variations in timing from numerous sources cause a frame now and then to take more or less time. Over the course of a run, this adds up to being able to complete a few frames more or less before hitting the five second deadline. Visually, the rotation speed should appear nearly constant because it calculates the current angle from the current time, whatever that may be, rather than the frame number.

