Sign In/My Account | View Cart  
advertisement


Listen Print Discuss

Building a 3D Engine in Perl, Part 3
by Geoff Broadwell | Pages: 1, 2, 3, 4, 5

Softening the Edges

Clearly, increasing the number of subdivisions only goes so far to improve the rendering, while simultaneously costing dearly in performance. I'll try a different tack and go back to what I know about a flashlight. Most flashlights cast a beam that is brighter in the center than at the edge. (Some have a dark circle in the very center, but I'm ignoring that for now.) I can take advantage of this to create a more accurate image and also soften the large jaggies considerably. First, I backed out my change to the draw_quad_face call:

        draw_quad_face(normal    => $normal,
                       corners   => \@corners);

Then I changed one spotlight parameter for the flashlight in set_eye_lights and added another:

    glLight(GL_LIGHT1, GL_SPOT_CUTOFF,   30.0);
    glLight(GL_LIGHT1, GL_SPOT_EXPONENT, 80.0);

With the change to GL_SPOT_CUTOFF, I've widened the beam to twice its original angle. At the same time, I've told OpenGL to make it quite a bit dimmer at the edges using GL_SPOT_EXPONENT, hopefully hiding any jaggies. The new parameter has a somewhat confusing name that refers to the details of the equation that determines the strength of the off-center dimming effect. In a theme seen throughout the mathematics of computer graphics, the dimming is a function of the cosine of the angle between the center line and the vertex being lit. In fact, the dimming factor is the cosine raised to the exponent specified by GL_SPOT_EXPONENT. Why use the cosine of the angle? It turns out to be cheap to calculate--cheaper than calculating the angle itself--and also gives a nice smooth effect.

With luck, the new beam will appear about the same width to the eye as the old one:

Good enough. The image looks better without the massive performance strain of high subdivision levels.

Refactoring Drawing

There's still something not right, but it will take a few more objects in the scene to show it. draw_view is already a repetitive hardcoded mess and it's been on the "to be refactored" list for a while, so now seems a good time to clean it up before I add to the mess.

draw_view performs a series of transformations and state settings for each object drawn. I want to move to a more data-driven design, with each object in the simulated world represented by a data structure specifying the needed transformations and settings. Eventually, these structures may become full-fledged blessed objects, but I'll start simple for now.

I initialized the data structures in init_objects:

sub init_objects
{
    my $self = shift;

    my @objects = (
        {
            draw        => \&draw_axes,
        },
        {
            lit         => 1,
            color       => [ 1, 1,  1],
            position    => [12, 0, -4],
            scale       => [ 2, 2,  2],
            draw        => \&draw_cube,
        },
        {
            lit         => 1,
            color       => [ 1, 1, 0],
            position    => [ 4, 0, 0],
            orientation => [40, 0, 0, 1],
            scale       => [.2, 1, 2],
            draw        => \&draw_cube,
        },
    );

    $self->{world}{objects} = \@objects;
}

Each hash includes the arguments to the various transformations to apply to it, along with a reference to the routine that actually draws the object and a flag indicating whether the object should be subject to OpenGL lighting. The object array then becomes a new part of the world hash for easy access later.

I called this routine at the end of init as usual:

    $self->init_objects;

I also replaced draw_view with a version that interprets the data into a series of OpenGL calls:

sub draw_view
{
    my $self    = shift;

    my $objects = $self->{world}{objects};

    foreach my $o (@$objects) {
        $o->{lit} ? glEnable (GL_LIGHTING)
                  : glDisable(GL_LIGHTING);

        glColor(@{$o->{color}})        if $o->{color};

        glPushMatrix;

        glTranslate(@{$o->{position}}) if $o->{position};
        glRotate(@{$o->{orientation}}) if $o->{orientation};
        glScale(@{$o->{scale}})        if $o->{scale};

        $o->{draw}->();

        glPopMatrix;
    }
}

The new routine iterates over the world object array, performing each requested operation. It either skips or defaults any unspecified values. First up is the choice to enable or disable GL_LIGHTING, followed by setting the current color if requested. The code next checks for and applies the usual transformations and finally, calls the object draw routine.

For simplicity and robustness, I've unconditionally wrapped the transformations and draw routine in a matrix push/pop pair rather than trying to detect whether they need the push and pop. OpenGL implementations tend to be highly optimized with native code, and any detection I did would be Perl. Chances are good that such an "optimization" would instead slow things down. This way, my code stays cleaner and even a misbehaving draw routine that performed transformations internally without cleaning up afterwards will not affect the next object drawn.

A quick test showed that this refactored version still worked. Now I could add a few more objects to demonstrate the remaining lighting issue. I specified several more boxes programmatically by inserting a new loop before the end of init_objects:

    foreach my $num (1 .. 5) {
        my $scale =   $num * $num / 15;
        my $pos   = - $num * 2;
        push @objects, {
            lit         => 1,
            color       => [ 1, 1,  1],
            position    => [$pos, 2.5, 0],
            orientation => [30, 1, 0, 0],
            scale       => [1, 1, $scale],
            draw        => \&draw_cube,
        };
    }

    $self->{world}{objects} = \@objects;
}

For each box, just two parameters vary: position and Z scale. I chose the position to set each box next to the last, progressing along the -X axis. The scale is set so that the height and width of each box remains the same, but the depths vary from very shallow for the first box to fairly deep for the last.

The loop specifies five boxes in total and begins by calculating the X position and Z scaling (depth) for the current box. The next few lines simply create a new hash for the new box and push it onto the object array.

Finally, there was one last change--the bright world light overwhelms the problematic effect from the flashlight. This is an easy fix; I commented out the line that enables it:

sub set_world_lights
{
    glLight(GL_LIGHT0, GL_POSITION, 0.0, 0.0, 1.0, 0.0);

#     glEnable(GL_LIGHT0);
}

By panning to the left across the scene until the viewpoint is in front of the new boxes, the problem becomes obvious:

The brightness of the lighting varies immensely depending on the depth of the box! This rather unintuitive outcome is an unfortunate side effect of how OpenGL must handle normals. A normal specifies the direction of the surface associated with a vertex. If a rigid object rotates, its surfaces rotate, so all of its normals must rotate as well. OpenGL handles this by transforming normal coordinates as it would vertex coordinates. This runs into trouble with any transformations other than translation and rotation. OpenGL calculations assume that normals are normalized (have unit length). Scaling the normal breaks this assumption and results in the effect seen above.

To fix this, I told OpenGL that normals may not have unit length and that OpenGL must normalize them before other calculations are performed. This is not the default behavior because of the performance cost of normalizing each vector. An application that can ensure normals are always unit length after transformation can keep the default and run a little faster. I want to allow arbitrary scaling of objects, so I enabled automatic normalization with another line at the end of prep_frame:

    glEnable(GL_NORMALIZE);

That fixed the problem:

With that bug killed, I reenabled the world light by uncommenting the glEnable line in set_world_lights:

sub set_world_lights
{
    glLight(GL_LIGHT0, GL_POSITION, 0.0, 0.0, 1.0, 0.0);

    glEnable(GL_LIGHT0);
}

Conclusion

During this article I've moved pretty quickly, covering screenshots, movement of the viewpoint, the beginnings of lighting in OpenGL, and subdivided faces for the boxes. Along the way, I took the chance to refactor draw_view into a more data-driven design and made the scene a little more interesting.

Unfortunately, these new changes have slowed things down quite a bit. OpenGL has several features that can improve performance considerably. Next time, I'll talk about one of the most powerful of these: display lists. I'll also introduce basic font handling and run with the performance theme by adding an FPS display to the engine.

Until next time, have fun and keep hacking!