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

Let There Be Lighting!

Okay, so maybe that's not much more interesting, admittedly. This scene needs a little mood lighting instead of the flat colors I've used so far (especially because they make it hard to see the shape of each object clearly). As a first step, I turned on OpenGL's lighting system with a new line at the end of prep_frame:

    glEnable(GL_LIGHTING);

Far from lighting the scene, the view is now almost black. If you look very carefully and your monitor and room lighting are forgiving, you should be able to just make out the objects, which are very dark gray on the black background. In order to see anything, I must enable both GL_LIGHTING and one or more lights to provide light to the scene. Without a light, the objects are dark gray instead of true black because OpenGL, by default, applies a very small amount of light to the entire scene, known as ambient light. To show the objects more brightly, I turned on the first OpenGL light with another new line at the end of prep_frame:

    glEnable(GL_LIGHT0);

Now the objects are brighter, but they're still just gray. When calculating colors with lighting enabled, OpenGL uses a completely different set of parameters from the colors used when lighting is disabled. Together these new parameters make up a material. Complex interactions between the parameters that make up a material can result in very interesting color effects, but in this case, I'm not trying to create a complex effect. I want my objects to have their old colors back without worrying about the full complexity that materials provide. Thankfully, OpenGL provides a way to state that the current material should default to the current color. To do this, I add yet another line to the end of prep_frame:

    glEnable(GL_COLOR_MATERIAL);

At this point, the objects once again have color, but each of the faces is still the same shade rather than appearing to be lit by a single light source somewhere. The problem is that OpenGL does not know whether each face points toward or away from the light and, if so, by how much. The angle between the face and the light determines how much light falls on the surface and, therefore, how bright it should appear. It is possible to calculate the angle of each face in my scene from the location of its vertices, but this is not always the right thing to do (especially when dealing with curved surfaces), so OpenGL does not calculate this internally. Instead, the program needs to do the direction calculations and tell OpenGL the result, known as the normal vector.

Luckily, in draw_cube the faces align with the coordinate axes so that each face points down one of them (positive or negative X, Y, or Z). I don't have to do any calculation here, just tell OpenGL which normal vector to associate with each face:

sub draw_cube
{
    # A simple cube
    my @indices = qw( 4 5 6 7   1 2 6 5   0 1 5 4
                      0 3 2 1   0 4 7 3   2 3 7 6 );
    my @vertices = ([-1, -1, -1], [ 1, -1, -1],
                    [ 1,  1, -1], [-1,  1, -1],
                    [-1, -1,  1], [ 1, -1,  1],
                    [ 1,  1,  1], [-1,  1,  1]);
    my @normals = ([0, 0,  1], [ 1, 0, 0], [0, -1, 0],
                   [0, 0, -1], [-1, 0, 0], [0,  1, 0]);

    glBegin(GL_QUADS);

    foreach my $face (0 .. 5) {
        my $normal = $normals[$face];
        glNormal(@$normal);

        foreach my $vertex (0 .. 3) {
            my $index  = $indices[4 * $face + $vertex];
            my $coords = $vertices[$index];
            glVertex(@$coords);
        }
    }
    glEnd;
}

The new lines are the definition of the @normals array and the two lines at the top of the $face loop that select the correct normal for each face and pass it to OpenGL using glNormal.

The boxes are now shaded reasonably and it's clear that the light is coming from somewhere behind the viewer; the front faces are brighter than the sides. Unfortunately, the axes are now dark again:

I did not specify any normal for the axis lines because the concept doesn't make a whole lot of sense for lines or points. However, with lighting enabled, OpenGL needs a set of normals for every lit object, so it goes back to the current state and uses the most recently defined normal. For the very first frame this is the default normal, which happens to point towards the default first light, but for succeeding frames it will be the last normal set in draw_cube. The latter definitely does not point toward the light, and the axes end up dark.

I'd rather the axis lines didn't take part in lighting calculations at all and kept their original bright colors, regardless of any lighting (or lack thereof) in the scene. To do this, I removed the line that enables GL_LIGHTING in prep_frame and inserted two new lines near the top of draw_view:

sub draw_view
{
    glDisable(GL_LIGHTING);

    draw_axes();

    glEnable(GL_LIGHTING);

Now lighting is off before drawing the axis lines and back on afterward. The axis lines have bright colors again, but rotating the view exposes a new problem. When the view rotates, the direction of the light changes as well:

Because of the way that OpenGL calculates light position and direction, any lights defined before the view is set are fixed to the viewer like the light on a miner's helmet. To fix a light relative to the simulated world, define the light instead after setting the view. I removed the line enabling GL_LIGHT0 in prep_frame and moved it to the new routine set_world_lights:

sub set_world_lights
{
    glEnable(GL_LIGHT0);
}

I then updated draw_frame to call the new routine after setting the view:

sub draw_frame
{
    my $self = shift;

    $self->set_projection_3d;
    $self->set_view_3d;
    $self->set_world_lights;
    $self->draw_view;
}

Unfortunately, this doesn't work. OpenGL only updates its internal state with the light's position and direction when they change explicitly, not when the light is enabled or disabled. I've never set the light's parameters explicitly, so the original default still stands. This issue is easy to fix with another 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);
}

In one of the few OpenGL interface decisions that actively annoys me, the new line sets the direction of the light, not its position. OpenGL defines all lights as one of two types: directional or positional. OpenGL assumes directional lights are very far away so that anywhere in the scene the direction from the light to each object is effectively the same. Positional lights are nearer and OpenGL must calculate the direction from the light to every vertex of every object in the scene independently. As you can imagine, this is much slower, but produces more interesting lighting effects.

The key to choosing between these two types is the last parameter of the glLight call above. If this parameter is 0, the light is directional and the other three coordinates specify the direction from which the light comes. In this case, I've specified that the light should come from the +Z direction. If the last parameter is 1, then OpenGL makes the light positional and uses the other three coordinates to set the light's position within the scene. For now, I'll skip the gory details of what happens when a value other than 0 or 1 is used, but in short, the light will be positional and extra calculations determine the actual position used. Most of the time it's best to ignore that case.

You may wonder why I explicitly specified 0.0 and 1.0 instead of 0 and 1. This is a workaround for a bug in glLight in some versions of SDL_Perl when it is presented with integer arguments instead of floating-point arguments.

With this line added, the light now stays fixed in the world, even when the user moves and rotates the view:

Pages: 1, 2, 3, 4, 5

Next Pagearrow