Building a 3D Engine in Perl, Part 3
by Geoff BroadwellFebruary 17, 2005
Editor's note: see also the next article in the series, profiling your application.
Later in this article, I'll discuss movement of the view position,
continue the refactoring work by cleaning up draw_view, and begin
to improve the look of our scene using OpenGL lighting and materials.
Before I cover that, your feedback to the previous articles has
included a couple of common requests: screenshots and help with porting
issues. If you're having problems running SDL_Perl and the
sample code
from these articles on your system, or might be able to help
the Mac OS X and Win32 readers, take a look at the next section.
Otherwise, skip down to the Screenshots section, where the main
article begins.
Known Porting Issues
General
Some versions of SDL_Perl require that the program load SDL::Constants
to recognize SDL_QUIT and other constants. As this change should
be transparent to other users, I have merged that into the latest
version of the sample code, retroactive to the first use of an SDL
constant.
FreeBSD
See the suggestions at the beginning of the second article.
|
Related Reading
Games, Diversions & Perl Culture |
Mac OS X
I spent some time research the porting issues on Mac OS X but am as yet unable
to figure out a simple procedure for building SDL_Perl from scratch. Recent
emails on the sdl-devel mailing list seem to indicate that Mac OS X builds
for recent SDL_Perl sources are problematic right now, but older releases seem
to be even worse. There have been some packaging attempts in the past, but none
that I have found so far install a fully configured set of SDL_Perl libraries
into the system perl. I'm no Mac porting expert, so I appreciate any help on
this; please post a comment in this month's article discussion if you have a
suggestion or solution.
Slackware
According to comments by Federico (ironfede) in last month's
article discussion,
Slackware ships with a version of SDL_Perl that requires
SDL::Constants. This is not an issue for the current version
of the sample code, which I fixed as mentioned above in the
General issues paragraph.
Win32
Win32 porting went as did Mac OS X porting. I was quite excited when chromatic
pointed me to some old Win32 PPM packages, but sadly they don't include a
working version of SDL::OpenGL. Building manually was "interesting" at best, as
I have no access to a Microsoft compiler and precious little experience using
gcc under Win32. As with the Mac folks, I appreciate any help from the
readers. Please post a comment in this month's article discussion if you have
a suggestion or solution for your fellows.
Screenshots
Thankfully, screenshots are much easier to handle than porting issues. I'd like
the user to be able to take a screenshot whenever desired. The obvious way to
accomplish that is to bind the screenshot action to a key; I chose function key
F4 at random. First I added it to the bind hash:
bind => {
escape => 'quit',
f4 => 'screenshot',
left => '+yaw_left',
right => '+yaw_right',
tab => '+look_behind',
}
The new key must have an action routine, so I altered that lookup hash as well:
$self->{lookup}{command_action} = {
quit => \&action_quit,
screenshot => \&action_screenshot,
'+yaw_left' => \&action_move,
'+yaw_right' => \&action_move,
'+look_behind' => \&action_move,
};
I need to wait until drawing completes for the entire scene before I can take a snapshot, but event processing happens before drawing begins. To work around this, I set a state variable marking that the user has requested a screenshot, rather than perform the screenshot immediately:
sub action_screenshot
{
my $self = shift;
$self->{state}{need_screenshot} = 1;
}
The code checks this state variable in a new line at the end of end_frame,
after the drawing has completed and it has synced the screen with the
image written into OpenGL's color buffer:
sub end_frame
{
my $self = shift;
$self->{resource}{sdl_app}->sync;
$self->screenshot if $self->{state}{need_screenshot};
}
The screenshot routine is surprisingly short but dense:
sub screenshot
{
my $self = shift;
my $file = 'screenshot.bmp';
my $w = $self->{conf}{width};
my $h = $self->{conf}{height};
glReadBuffer(GL_FRONT);
my $data = glReadPixels(0, 0, $w, $h, GL_BGR,
GL_UNSIGNED_BYTE);
SDL::OpenGL::SaveBMP($file, $w, $h, 24, $data);
$self->{state}{need_screenshot} = 0;
}
The routine starts by specifying a filename for the screenshot and
gathering the width and height of the screen. The real work begins
with the call to glReadBuffer. Depending on the OpenGL driver, the
hardware, and a number of advanced settings, OpenGL may have provided
several color buffers in which to draw and read images. In fact,
the default behavior on most systems is to draw onto one buffer, known
as the back buffer, and display a separate buffer, known as the
front buffer. After completing the drawing for each frame, the
SDL::App::sync call moves the image from the back buffer to the
front buffer so the user can see it. Behind the scenes, OpenGL
generally handles this in one of two different ways, depending on the
underlying implementation. Software OpenGL implementations, such as
Mesa, copy the data from the back buffer to the front buffer. Hardware-accelerated systems can swap internal pointers so that the
back buffer becomes the front buffer and vice versa. As you can
imagine, this is much faster.
This extra work brings a great benefit. Without double buffering, as soon as one frame completes, the next frame immediately clears the screen to black and starts drawing again from scratch. Depending on the relative speed difference between the user's monitor and the application, this would probably appear to the user as a flickering, dark, perpetually half-drawn scene. With double buffering, this problem is almost gone. The front buffer shows a solid stable image while all of the drawing is done on the back buffer. Once the drawing completes, it takes at most a few milliseconds to sync up and start displaying the new frame. To the human eye, the animation appears solid, bright, and (hopefully) smooth.
In this case, I want to make sure that I take a screenshot of exactly
the same image the user sees, so I tell OpenGL that I want to read the
image in the front buffer (GL_FRONT).
At this point, it's safe to read the image data into a Perl buffer in
the proper format. The first four arguments to glReadPixels specify
the lower-left corner and size of the sub-image to read. The next two
arguments together tell OpenGL what format I would like for the data.
I specify that I want to read the entire window and that I want the
data in the correct format for a BMP file--one unsigned byte for
each of the red, green, and blue color channels for each pixel, but in
reverse order.
Once I have the data from OpenGL I use the SDL_Perl utility routine
SaveBMP to save the image into a file. The arguments are the
filename, image width, image height, color depth (24 bits per pixel),
and data buffer. Finally, the routine resets the need_screenshot
state flag and returns.
At this point you should be able to take a screenshot each time you press the
F4 key. Of course, I'd like to show several screenshots during this article as
the code progresses. The current code overwrites the previous screenshot file
every time I request a new one. Because I number each runnable version of the
code, I used a quick workaround resulting in a different screenshot filename
for each code step. I first load one of the core Perl modules to strip
directories from a path:
use File::Basename;
Then I use the filename of the script itself as part of my screenshot filename:
my $file = basename($0) . '.bmp';
This may be all you need for your application, or you may want to add some code to number each file uniquely. This code is enough to fix my problem, so I've left the more powerful version as an exercise for the reader.
Here then is the first screenshot:

The observant reader will notice that this image is not a BMP file; it's a
PNG image, which is both much smaller than a BMP and more friendly to web
standards. There are many tools available that can perform this conversion.
Any good image editor can do it. In this case that's overkill--I instead used
the convert program from the ImageMagick suite of utilities:
convert step042.bmp step042.png

