Thursday, December 5, 2013

Implementing Multiple Types of Lights

This post continues my series about rewriting the code from Game and Graphics Programming for iOS and Android with OpenGL ES 2.0 to better leverage C++ abilities.  Finally, in Chapter 10 I get to write some code which uses real object oriented polymorphism.  This module from the book was my favorite to rewrite.  A lot of conditional logic which used to be exposed in the application code has been hidden from the application programmer inside this class hierarchy which implements lighting.

To get a copy of the code as described in this post pull a copy from GitHub using the tag chapter10.

I need to start by describing how the original code implements the LAMP type to support multiple types of light sources.  The author implements five different types of lighting in this chapter.  Each of these types requires a different set of data members be passed to the OpenGL programmable shaders.  Along with the various lighting parameters needed to implement each light source type the data structure has a data member called type which holds an integer value 0 through 4.  One of the first things I did was create a new enumerated data type (LampType) to give each of these integer constants a symbolic name.  You can find this new type near the top of the file SDK/chapter10-1/templateApp.cpp.

The author's final data structure holding all of the values needed for each of the light types is:

    typedef struct {
        char    name[MAX_CHAR];
        vec4    color;
        vec3    direction;
        vec4    position;   // Position of the lamp in world
                            // coordinates
        float   linear_attenuation;
        float   quadratic_attenuation;
        float   distance;
        /* The cosine of half the field of view of the spot
         * (in radians).
         */
       float   spot_cos_cutoff;
        /* Factor ranging from 0 to 1 to smooth the edge of
         * the spot circle.
         */
        float   spot_blend;
        /* The spot direction is calculated by multiplying
         * the direction vector by the invert of the
         * modelview matrix of the camera.
         */
        vec3    spot_direction;
        unsigned char type;
    } LAMP;

As you can see this data structure has eleven data members.  The code never sends the name datum to the OpenGL shaders.  The code always* sends the type data member because the OpenGL shaders need that information to know how to process the rest of the data members.  The following table shows which data members are needed for each type of light source:


TypeColorDirectionPositionAttenuationSpot
LinearQuadraticDistanceAngleBlendDirection
Directional
Point
Point w/Attentuation
Spherical Point
Spot

* That is the author's code ought to always send the type data member.  See below for details.

The author's original code has a multi-branch if statement which was executed each time the code called the program_draw() function.  The if statement looks like:

    if (lamp->type == 0) {
        .
        .
        .
    } else if (lamp->type == 1) {
        .
        .
        .
    } else if (lamp->type == 2) {
        .
        .
        .
    } else if (lamp->type == 3) {
        .
        .
        .
    } else {    // lamp->type == 4
        .
        .
        .
    }

The original code uses this if statement to only send the data members of the LAMP type which are needed for each particular light source type.

My rewrite of the code using polymorphism accomplishes the same result but moves the burden for managing which data needs to be transferred to the OpenGL shader code onto the LAMP class hierarchy.  LAMP and its subclasses have the following organization:
  • LAMP
    • DirectionalLamp
    • PointLamp
      • AttenuatedPointLamp
      • PointSphereLamp
      • SpotLamp
The parent class LAMP declares a virtual function push_to_shader().  For the LAMP class this function has the responsibility sending its type† and color data members to the OpenGL shader programs.

† This is actually a bug in the code.  The author's original code, and, consequently my code which mimics the behavior of his code, never passes the type value to the OpenGL shaders. Both the author's code and my code should be doing this so that the shader programs can implement conditional logic to dynamically calculate the lighting values for the corresponding light source type.

[Note:  The class hierarchy I've implemented is an artifact of the order in which the book presented the various light source types.  Since the data members of the PointSphereLamp are a subset of the data members of AttenuatedPointLamp, I could have made AttenuatedPointLamp a subclass of PointSphereLamp.  Also, if I had used multiple inheritance I could have re-used the direction data member by making SpotLamp be a subclass of both DirectionalLamp, and PointLamp.  But, to do this I would probably have used virtual inheritance since these two classes (DirectionalLamp, and PointLamp) share a common super class (LAMP) in the class hierarchy.]

Each subclass also implements its own version of the virtual function push_to_shader().  The first responsibility of each of these functions is to invoke the push_to_shader() method of its parent class, and then "push to the OpenGL shader" the data members which are unique to its own class.

This implementation of push_to_shader() as a virtual function means that the entire multi-branch if statement shown above can be replaced with the single line of code:

    lamp->push_to_shader(program);

So, for example, if the code has created a light source using "new SpotLamp(…)" the line of code above would invoke SpotLamp::push_to_shader().  Upon receiving control this method would invoke the corresponding method from its parent class, that is, it invokes PointLamp::push_to_shader().  Likewise, PointLamp::push_to_shader() invokes LAMP::push_to_shader().  LAMP::push_to_shader() sends its data member color to the shader program, and returns control to the method which called it.  In this case that would be PointLamp::push_to_shader() which, upon regaining execution control, pushes its data member position to the shader program, and then returns control SpotLamp::push_to_shader(), etc.  Again, as you can see from this example, the burden for managing which data members get pushed to the OpenGL shader is assumed by the LAMP class, and its subclasses, hopefully, making the job of the application programmer just that much easier.

For the sample programs from Chapters 11, and 12 the class LAMP, and its subclasses will be renamed as LIGHT, DirectionalLight, etc. and their code will be moved into the files SDK/common/light.h, and SDK/common/light.cpp.

No new modules are introduced in Chapter 11 so my next post will cover rewriting the MD5 module used in Chapter 12.

No comments:

Post a Comment