Sign In/My Account | View Cart  
advertisement


Listen Print Discuss

Building a 3D Engine in Perl, Part 3

by Geoff Broadwell
February 17, 2005

This article is the third in a series aimed at building a full 3D engine in Perl. The first article started with basic program structure and worked up to displaying a simple depth-buffered scene in an OpenGL window. The second article followed with a discussion of time, view animation, SDL events, keyboard handling, and a nice chunk of refactoring.

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
Best of the Perl Journal

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

Pages: 1, 2, 3, 4, 5

Next Pagearrow