Sign In/My Account | View Cart  
advertisement


Listen Print Discuss

Building a 3D Engine in Perl

by Geoff Broadwell
December 01, 2004

This article is the first in a series aimed at building a full 3D engine. It could be the underlying technology for a video game, the visualization system for a scientific application, the walkthrough program for an architectural design suite, or whatever.

Editor's note: see also the rest of the series, events and keyboard handling, lighting and movement, and profiling your application.

First, I'll set some goals and ground rules to help guide the design. I'm all for agile programming, but even the most agile development process needs some basic goals at the outset:

  • I'm not going to make little demos. Early on, the engine won't have much functionality, but it should always be a good foundation for future growth.
  • The engine must be portable across architectures and operating systems. I will use OpenGL for 3D rendering and SDL for general, operating system interaction, such as input handling and window creation. The engine itself should contain almost no OS-specific code.
  • The engine should be operational at every step of the way, from the very beginning. I will flesh it out over time, and there may be some complex concepts that take some time to work through, but at the very least, every article should end with the whole engine working again.
  • I'll leave out most error checking to save space and make the central concepts more clear. For the same reasons, there is no included test library. In your own engine, you will want to have both!
  • Don't be afraid to experiment. The best way to learn this stuff is to play with it. Start with what's in the articles and add to it. Any time you spend now will repay itself many times over later, because it's easier to understand advanced topics when you have a solid understanding of the earlier topics.

As a final note before we begin, some versions of SDL_Perl have bugs that will affect the engine. If I know of any issues to watch out for, I'll let you know; conversely, if you find any bugs, let me know, and I'll include a note in a following article.

Getting Started

The first step is to rough out a simple structure and make a runnable program right off. Bear with me; there's a fair bit of code here for what it does, but that will simplify things later on. Here's my starting point:

#!/usr/bin/perl

use strict;
use warnings;

my ($done, $frame);

START: main();

sub main
{
    init();
    main_loop();
    cleanup();
}

sub init
{
    $| = 1;
}

sub main_loop
{
    while (not $done) {
        $frame++;
        do_frame();
    }
}

sub do_frame
{
    print '.';
    sleep 1;
    $done = 1 if $frame == 5;
}

sub cleanup
{
    print "\nDone.\n";
}

The first few lines are the usual strict boilerplate, especially important since I'm working without a net (a test library). Then I declare a couple of state variables (a "done" flag and a frame counter), and jump to the main program.

The main program is pretty simple -- initialize, run the main loop for a while, and then clean up. It's typical of how I structure a potentially complex program. The top-level routines should be very simple, clear, and self-documenting. Each conceptual piece is a separate routine, wherein reside all the gritty bits that actually do the real work. I've seen huge programs (hundreds of thousands of lines) where the main procedures started with several hundred lines of initialization before finally branching to the "real" main body at the end. That style is hard to debug, hard to profile, and just plain hard to understand. I avoid it religiously.

Back to the program at hand. init sets autoflush on STDOUT so that partial lines print immediately, which I use later in do_frame.

The main_loop simply loops until $done is true, producing one finished animation frame per loop. Each loop increments the frame counter and calls the actual routine that does the work, do_frame.

do_frame prints a single dot to indicate a frame has begun, and sleeps for a second. When it wakes up, it checks if five frames have completed, flagging $done if so.

With $done set, main_loop ends and control returns to main, which calls the final cleanup. cleanup just notifies the user of a clean exit and ends.

That's a fair amount of code to print two lines of text (over the course of five seconds) and exit; it doesn't even open a rendering window! I'll do that next.

Creating a Window

First, I need to pull in the SDL and OpenGL libraries:

use SDL::App;
use SDL::OpenGL; 

and add a couple more state variables (a config hash and an SDL::App object):

my ($conf, $sdl_app); 

Initialization

I'm going to do two new types of initialization, so I create routines for them and call them from init:

sub init
{
    $| = 1;
    init_conf();
    init_window();
}

sub init_conf
{
    $conf = {
        title  => 'Camel 3D',
        width  => 400,
        height => 400,
    };
}

sub init_window
{
    my ($title, $w, $h) = @$conf{qw( title width height )};

    $sdl_app = SDL::App->new(-title  => $title,
                             -width  => $w,
                             -height => $h,
                             -gl     => 1,
                            );
    SDL::ShowCursor(0);
}

At this point, init_conf just defines some configuration properties used immediately in init_window, which contains the first real SDL meat.

init_window performs two important actions. First, it asks SDL::App to create a new window, with the appropriate title, width, and height. The -gl option tells SDL::App to attach an OpenGL 3D-rendering context to this window instead of the default 2D-rendering context. Second, it hides the mouse cursor (while it's within the new window's border) using SDL::ShowCursor(0).

Three Phases of Drawing

Now that I have a nice new window, I'd like do_frame to do something with it. I'll start by breaking the rendering into three phases: prepare, draw, and finish.

sub do_frame
{
    prep_frame();
    draw_frame();
    end_frame();
}

For now, draw_frame contains exactly what do_frame used to contain:

sub draw_frame
{
    print '.';
    sleep 1;
    $done = 1 if $frame == 5;
}

The new code is in prep_frame and end_frame; let's look at prep_frame first:

sub prep_frame
{
    glClear(GL_COLOR_BUFFER_BIT);
}

This is the first actual OpenGL call. Before I explain the details, it's worth pointing out the OpenGL naming conventions. OpenGL's design allows it to work with programming languages that have no concept of namespaces or packages. In order to work around this, all OpenGL routine names look like glFooBar (CamelCase, no underscores, gl prepended), and all OpenGL constant names look like GL_FOO_BAR (UPPERCASE, underscores between words, GL_ prepended). In older languages, this prevents the OpenGL names from colliding with names used in other libraries. In the Perl world, this isn't an issue for object-oriented modules. Because OpenGL is not object-oriented, SDL_Perl takes advantage of this convention and simply imports all of the names into the current package when you write use SDL::OpenGL.

Note: If you read OpenGL code written in C, you may notice a short string of characters appended to the routine names, such as 3fv. This convention differentiates variants that have different numbers of parameters or whose parameters are different types. In Perl, values know their own type and a function's parameters can vary in number, so this is unnecessary. The Perl bindings simply drop these extra characters and SDL::OpenGL do the right thing for you.

The OpenGL call in prep_frame clears the rendering area to black by calling glClear -- the general OpenGL "clear a buffer" routine -- with a constant that indicates it should clear the color buffer. As the name indicates, the color buffer stores the color for each pixel and is what the user sees. Several other OpenGL buffers exist; I'll describe those later.

The alert reader may wonder why the code clears the color buffer to black as opposed to white or some other color. OpenGL relies heavily on the concept of current state. Many OpenGL routines do not actually request any rendering, instead altering one or more variables in the current state so that the next rendering command will perform its action differently. When a program prepares to use OpenGL, which SDL::App::new does for us, the current state is set to (mostly) reasonable defaults. One of these state variables is the color to use when clearing the color buffer. Its default is black, which I haven't bothered to override.

The remaining routine is end_frame :

sub end_frame
{
    $sdl_app->sync;
}

This asks the SDL::App object to synchronize the window contents with those held in OpenGL's color buffer, so that the user can see the rendered image. In this case, it's a black window for five seconds.

Pages: 1, 2, 3

Next Pagearrow