Part B - Direct3D

Lighting

Describe the lighting algorithm used in computer graphics
Describe the approximations used to add shininess to a material

Sample | Framework | Design | Coordinator | Lighting | Object | Graphic | Exercises



Assigning a specific colour to each vertex of a graphics primitive precludes variations in its colour, which produces a rather stiff representation.  To improve the sense of realism, we enable variations in the colour of a vertex by introducing variations in lighting that is incident on a surface passing through the vertex.  We define light sources within the scene and determine the colour of each vertex by combining the light incident on the vertex with the reflectivity of the material at that vertex.  As the light source changes, colour of each target vertex changes.  We model the light source and the reflector as independent of one another.  Materials with different reflective properties reflect the light impinging on their vertices differently. 

In this chapter, we incorporate lighting using this two-part model.  We create independent light sources and define the colour of each vertex in terms of reflective surface passing through that vertex and the material reflectivity of the parent object. 


Lighting Sample

The Lighting Sample introduces

  • a green box on the left rotating about an axis parallel to the world z axis
  • a white, translucent box on the right rotating about an axis parallel to the world y axis
  • a point source of red light in the front left of the scene at [-100 100 -100]
  • a yellow spot light source in the front left of the scene at [-20 -20 -100]
  • a directional light source directed downwards and slightly to the left
  • a medium-grey background light source

The user speeds up the rotations of the boxes by pressing the 'Roll Left Box' or 'Spin Right Box' keys and toggles the states of the three lights by pressing the 'Toggle Point Light', 'Toggle Spot Light', and 'Toggle Distant Light' keys. 

Lighting Sample

The right spinning box is translucent with an alpha value of 0.5. 


Framework

Four components involve upgrades to accommodate the lighting algorithm:

  • Design - the Design class defines the point light, the spot light, and the distance lights and toggles them at the user's initiative.
  • Coordinator - manages all of the lighting created by the Design object
  • Object - the Object class holds the material reflectivity data.
  • Graphic - the VertexList object holds the reflection surface properties and sets the material reflectivity before drawing the primitives.

The new component included with this sample is:

  • Lighting - holds the light source information and manages their on and off states.

Lighting Components

The lighting information is spread across three distinct classes.  Each Light object holds the information describing a specific light source.  Each Reflectivity object holds the material reflectivity.  Each Vertex object holds the orientation of the reflecting surface. 

Lighting Theory

The theory associated with this model of lighting identifies the following elements:

  • components - distinguishing purely reflective, roughly reflective, and background lighting components
  • types - different types of light sources
  • orientation - describing the orientation of a reflecting surface?
  • shading - shading a graphics primitive
  • colour - describing colour
  • performance - what are the principal design decisions that affect efficiency?

Components

The model distinguishes lighting into three separate components:

  • ambient
  • diffuse
  • specular

This distinction applies at the light source and at the material reflector.  We pair each component of a light source with its corresponding component at the material reflector to determine the colour associated with that component.  The colour of a vertex is the sum of the ambient light on the ambient material property, the diffuse light on the diffuse material property, and the specular light on the specular material property.  We assume no significant cross-component influences. 

Ambient light lacks any directional feature.  We use it to simulate background lighting.  The ambient property of a material surface reflects the ambient component of each light source. 

Diffuse light and specular light have directional features.  We use diffuse lighting to simulate rough-surface reflection: the diffuse property of a material reflects the diffuse component of each light source in a range of directions as shown below.  We use specular lighting to simulate smooth-surface reflection: the specular property of a material reflects the specular component of each light source in a well-defined direction. 

reflection

Types of Light Sources

In computer graphics, we distinguish light sources according to their type:

  • directional - has direction, but no position, attenuation, or range
  • point - has position, attenuation and range, but no direction
  • spot - has position, direction, attenuation, range, and falloff from the center of its target

Directional, Point and Spot Light Types

Each type exhibits some or all of the following properties:

  • direction - direction of travel from the source in world space (all)
  • position - location in world space (point, spot)
  • range - distance from the source over which the light illuminates material (point, spot)
  • attenuation - how intensity decreases across the specified range (point, spot)

Surface Orientation

The amount of diffuse and specular light that a material surface reflects depends on both the orientation of the surface and the angle at which the light impinges upon the surface.  The amount of reflected light is the greatest if the angle of incidence is normal to the plane of reflection - that is, perpendicular to the reflecting surface. 

The graphics primitive display paradigm defines the orientation of the reflecting surface at a vertex using the normal vector to that surface at that vertex.  The Graphics API uses this normal, the vertex's position in world space, and the light source's direction to calculate the intensity of the reflected light at the vertex.

normal

The normal to the reflecting surface at each vertex of a graphics primitive forms part of the description of the graphics primitive that represents an object in a scene. 

Specular Lighting

In more realistic depictions of a scene, the amount of light that a material surface reflects depends not only on its orientation and the amount of incident light, but also on the direction to the viewer's eye and the shininess of the surface itself.  If we don't model these contributions, our surfaces tend to look dull.

Specular lighting accounts for both of these contributions.  In 1973, Bui Thuong Phong (1942-1975) proposed the following general expression as one possible approximation of specular lighting

 intensity = ks Ls cosαφ

where ks is the fraction between 0 and 1, Ls is the intensity of the light source itself, α is the shininess coefficient, and φ is the angle between a perfect reflector and the viewer.  The shininess coefficient is a scalar value between 0 and +infinity, which we also call the specular power value.  As α increases the reflected light concentrates in a narrower region centered on the angle of the perfect reflector. 

We include the shininess coefficient in our description of material reflectivity.

Shading

Variations of material reflectivity across a graphics primitive may be calculated in a variety of ways.  The shading mode defines the nature of this calculation:

  • Flat - the properties of the entire primitive are the properties of a single vertex
  • Gouraud - the properties of the primitive are interpolated linearly from the properties of each vertex
  • Phong - the normals across the primitive are interpolated from the normals at each vertex

shading

Flat shading is the least realistic and the most efficient.  Phong shading is the least efficient and more realistic than Gouraud shading.  We need Phong shading wherever specular highlights are concentrated entirely within a primitive. 

Colour Description

We describe the colour of each component of a light source and a material reflector using the additive colour model, which consists of three elements.  Each element holds the intensity of one primary colour: red, green, or blue.  The weighted combination of these three elements defines the colour either incident upon any reflecting surface or the reflectivity of that surface. 

We add a fourth element to our description of a material reflector to account for its translucency: red, green, blue, and alpha.  'alpha' defines the degree of opacity: zero indicates perfect transparency, one indicates perfect opacity, and an intermediate value indicates some degree of translucency. 

For translucent surfaces, the effect of lights incident on the surface is a weighted combination of the reflected colour and the colour behind the translucent surface.  That is, the effect is a blending of the reflected colour and the colour being replaced.  We call this weighting process alpha-blending.  Typically, we use the alpha value as the weight of the reflected, foreground colour and one minus this value as the contribution of the background colour. 

Performance

The design of the lighting within a game can impact performance significantly.  Lighting involves per-vertex calculations in each rendered frame.  As a general rule, we keep the number of light sources to a minimum. 

Directional lights are the most efficient.  Spotlights can be more efficient than point lights if the spotlights do not shine on a many parts of a scene.  We use the range property of each light source to limit the lighting calculations. 

Specular lighting is computationally intensive because the calculations depend upon the direction of viewing, which varies across a graphics primitive with each pixel.  Typically, we use specular lighting only when absolutely necessary; for instance, to give important visual clues about the material surface itself. 

The per-pixel evaluations of the power relation in the expression for the intensity of specular light are computationally intensitive.  The advantage of the Phong model of specular lighting is that we can reduce the trigonometric part of the expression to a dot product

 intensity = ks Ls max((r·n)α,0)φ

r is the normalized direction of a perfect reflector and n is the normal to the surface.  Since r varies across a graphics primitive, this power expression still needs to be evaluated for each pixel.  There are several approximations that simplify this calculation. 

The first approximation introduces a halfway vector between the vector from each vertex to the light source and the vector from that vertex to the viewpoint and replaces the dot product with the dot product of this halfway vector and the normal to the surface.  Under this approximation, the power expression only requires evaluation at each vertex and we can interpolate the results linearly within the graphics primitive.  This is called the modified Phong or Blinn-Phong shading model. 

The second approximation assumes that the viewer is sufficiently far from the vertex so that we can approximate the viewpoint as being inifinitely far away along the z-axis.  Under this approximation, the dot product calculation is even simpler, but of course, less accurate.

Direct3D Implementation

Direct3D implements most of the elements of this theory.

Colour Descriptions

Direct3D stores colour values in a D3DCOLOR synonym type.  This type holds the red, green, blue, and alpha values in packed unsigned integer form (a DWORD), using a single byte for each value, which ranges from 0 to 255.  The D3DCOLOR_RGBA() macro packs a set of red, green, blue, and alpha colour values (DWORDs), into the D3DCOLOR type.  The D3DCOLOR_XRGB() macro packs a set of red, green, and blue colour values (DWORDs), into the D3DCOLOR type assuming perfect opacity. 

Direct3D supports continuous descriptions of colour through its D3DXCOLOR and D3DCOLORVALUE types using floating-point numbers that range from 0.0f to 1.0f.  These structs contain float members for red, green, blue and alpha values. 

The D3DCOLOR_COLORVALUE() macro converts a set of floating-point colour values into the D3DCOLOR type. 

Sources

Direct3D supports three types of light sources:

  • D3DLIGHT_POINT
  • D3DLIGHT_DIRECTIONAL
  • D3DLIGHT_SPOT

Direct3D also supports a separate ambient background source.  This background source is omnipresent and reflects from all surfaces in the same way.  We define it by setting the render state D3DRS_AMBIENT to a D3DCOLOR value.

Shading

Direct3D supports Flat and Gouraud shading, but does not support Phong shading.  Gouraud shading is the default.  We specify the shading method by setting the render state D3DRS_SHADEMODE to D3DSHADE_FLAT or D3DSHADE_GOURAUD

D3DSHADE_FLAT shading applies the colour and specular component of the first vertex to the entire primitive without interpolation.  Only the specular alpha value is interpolated. 

D3DSHADE_GOURAUD shading interpolates the colour and specular components across the primitive from the values at the vertices. 

Specular Lighting

Direct3D provides the D3DRS_SPECULARENABLE render state constant to turn specular lighting off and on.  The default state is off.  The shininess coefficient should be set to 0 if specular lighting has been turned off. 

Direct3D supports either the first approximation or the first and second approximations in calculating the amount of specular lighting.  We select the first approximation by setting the render state constant D3DRS_LOCALVIEWER is set to TRUE.  We select the second approximation by setting the render state constant D3DRS_LOCALVIEWER is set to FALSE

Indexing

Direct3D uses a zero-based index to identify each light source.  The application itself sets the index.  Direct3D stores lighting properties for every index that has not been used.  Direct3D lets us disable and enable lights at run time.  Not all lights that are stored need be active. 

The maximum number of active lights varies with physical devices and depends upon the vertex processing flag passed to CreateDevice().  For instance, hardware vertex processing, when available, may return a maximum value of say 10, while software vertex processing may return a maximum value of say 255.  The number of active lights should not exceed the maximum specified in the MaxActiveLights member of the capabilities of the display device.  We obtain this maximum value by calling the GetDeviceCaps() on the display device. 

Settings

Translation Layer

The settings for the Translation Layer include enumerations of light toggles and light types, added enumerations for colour dithering and specularity, and macros for the toggling actions: 

 // Translation.h
 // ...
 typedef enum Action {
     // ...
     LIGHT_POINT,
     LIGHT_SPOT,
     LIGHT_DISTANT,
 } Action;

 #define ACTION_DESCRIPTIONS {\
     // ...
     L"Toggle Point Light",   \
     L"Toggle Spot Light",    \
     L"Toggle Distant Light", \
 }

 #define ACTION_MAPPINGS {\
     // ...
     KEY_J, KEY_K, KEY_L, \
 }

 typedef enum RenderState {
     ALPHA_BLEND    = 1,
     LIGHTING       = 2,
     WIRE_FRAME     = 3,
     Z_BUFFERING    = 4,
     W_BUFFERING    = 5,
     DITHERING      = 6,
     SPECULARITY    = 7,
 } RenderState;

 typedef enum LightType {
     POINT_LIGHT,
     SPOT_LIGHT,
     DIRECTIONAL_LIGHT
 } LightType;

 #define MAX_ACTIVE_LIGHTS 8

Model Layer Settings

The settings for the Model Layer identifies two new categories: 

 // Model.h
 // ...
 typedef enum Category {
     LIT_OBJECT,
     SPRITE,
     OPAQUE_OBJECT,
     TRANSLUCENT_OBJECT,
 } Category;

Math Library

The math library defines the Reflectivity struct and default macros for determining each of the components of a material's reflectivity from a single colour value:

 #define AMBIENT 0.3f
 #define DIFFUSE 0.9f
 #define SPECULAR 0.8f
 #define POWER 10.0f

 struct Reflectivity {
     Colour ambient;
     Colour diffuse;
     Colour specular;
     float  power;
     Reflectivity(Colour c = Colour(), float p = 0) {
         ambient  = AMBIENT * c;
         diffuse  = DIFFUSE * c;
         specular = SPECULAR * Colour(1, 1, 1);
         power    = p  ? p  : POWER;
     }
     bool translucent() const {
         return ambient.a != 1 || diffuse.a != 1 || specular.a != 1;
     }
 };

Note that a shininess coefficient 0 resets to the default value.


Design

The Design component defines the light sources in the scene and the material reflectivity of its drawable objects and processes changes in the orientations of the objects and in the states of the light sources. 

The Design class now includes instance pointers to the three Lights and the two new Objects:

 class Design : public Coordinator {
     // ...
     iObject* rollLeft;     // points to left spinner
     iObject* rollRight;    // points to right spinner
     iLight*  pointLight;   // points to the point light
     iLight*  spotLight;    // points to the spot light
     iLight*  distantLight; // points to the directional light
     // ...
};

Implementation

Construct

The constructor initializes the pointers to the design units: 

 Design::Design(void* h, int s) : Coordinator(h, s) {
     // pointers to the objects
     // ...
     rollLeft  = nullptr;
     spinRight = nullptr;
     // pointers to the lights
     pointLight   = nullptr;
     spotLight    = nullptr;
     distantLight = nullptr;
 }

Initialize

The initialize() method defines the ambient background lighting, creates the two boxes, creates the three light sources and translates and rotates them into their initial positions and orientations: 

 void Design::initialize() {
     // ...
     setAmbientLight(0.2f, 0.2f, 0.2f);
     // ...
     iGraphic* box = CreateBox(-10, -10, -10, 10, 10, 10);
     Reflectivity greenish = Reflectivity(green);
     rollLeft = CreateObject(box, &greenish);
     rollLeft->translate(-40, -20, 30);
     objectCamera->attachTo(rollLeft);

     Reflectivity whiteish = Reflectivity(Colour(1, 1, 1, 0.5f));
     spinRight = CreateObject(box, &whiteish);
     spinRight->translate(30, -15, 30);

     Colour white(1, 1, 1);
     Colour yellow(1, 1, 0);
     Colour turquoise(0, 0.8f, 0.6f);
     Colour black(0, 0, 0);

     // create far away red point-source light
     pointLight = CreatePointLight(red, red, white, 1000.0f, true);
     pointLight->translate(-100.f, 100.f, -100.f);

     // create yellow/yellow/white spotlight (in the world z direction)
     spotLight = CreateSpotLight(yellow, yellow, white, 300.0f, true, 1,
      0.00005f, 0.00001f, .40f, .25f, 0.999f);
     spotLight->translate(-20.f, -20.f, -100.f);

     // create turquoise directional light (in the world -y direction)
     distantLight = CreateDistantLight(turquoise, turquoise, black, false);
     distantLight->translate(10, 1000, 0);
     distantLight->rotatex(1.57f); // rotate into the world -y direction
     distantLight->rotatez(0.20f); // rotate 11 degrees towards world +x
 }

Update

The update() method rolls and spins the boxes, speeding up the rotation at the user's initiative, and toggles the light sources at the user's initiative:

 void Design::update() {
     int delta = now - lastUpdate;
     // ...
     int dr = 0;  // roll the left box about an axis || to world z axis
     int ds = 0;  // spin the right box about an axis || to world y axis
     // ...
     // boxes ------------------------------------------------------------
     // add changes introduced by user input
     if (pressed(ROLL_LEFT_BOX))
         dr += delta;
     if (pressed(SPIN_RIGHT_BOX))
         ds += delta;
     if (spinRight)
          spinRight->rotatey(ds * ROT_SPEED + CONSTANT_ROLL);
     if (rollLeft)
         rollLeft->rotatez(dr * ROT_SPEED + CONSTANT_ROLL);
     // lighting ---------------------------------------------------------
     if (pressed(LIGHT_POINT) && pointLight)
         pointLight->toggle();
     if (pressed(LIGHT_SPOT) && spotLight)
         spotLight->toggle();
     if (pressed(LIGHT_DISTANT) && distantLight)
         distantLight->toggle();
 }

Coordinator

The Corrdinator component manages the lighting defined by the Design object and the rendering of objects with light source dependent vertices. 

The iCoordinator interface exposes two virtual methods to the framework:

 // iCoordinator.h

 class iCoordinator {
   public:
     // ...
     virtual bool add(const iLight* l)    = 0;
     virtual bool remove(const iLight* l) = 0;
     // ...
 };
 iCoordinator* CoordinatorAddress();

The class definition holds a vector of pointers to light source objects and the ambient background colour:

 // Coordinator.h

 class Coordinator : public iCoordinator {
     // ...
     std::vector<iLight*> light; // points to light sources
     Colour ambient;             // background lighting
     // ...
   protected:
     // ...
     void setAmbientLight(float, float, float);
     // ...
   public:
     // ...
     bool add(const iLight*);
     bool remove(const iLight*);
     // ...
 };

Implementation

Get Configuration

The getConfiguration() method stores the received values as the ambient background colour:

 bool Coordinator::getConfiguration() {
     bool rc = false;

     if (userInput->getConfiguration()) {
         // ...
         if (window->setup()) {
             // ...
             if (display->setup()) {
                 projection = ::projection(fov, (float) width / height,
                  nearcp, farcp);
                 display->setProjection(&projection);
                 display->set(DITHERING, true);
                 display->set(SPECULARITY, true);
                 Light::alloc();
                 rc = true;
             } else
                 window->release();
         }
     }
     // ...
 }

Set Ambient Light

The setAmbientLight() method stores the received values as the ambient background colour:

 void Coordinator::setAmbientLight(float r, float g, float b) {
    ambient = Colour(r, g, b);
 }

Render

The render() method updates the light sources, sets the ambient light, and draws the objects with light source dependent vertices: 

 void Coordinator::render() {
     Coordinator::update();
     update();
     for (unsigned i = 0; i < light.size(); i++)
         if (light[i]) light[i]->update();
     // draw the frame
     view = *((Matrix*)Camera::getView());
     display->beginDrawFrame(&view);
     Light::setAmbient(ambient);
     render(SPRITE);
     display->set(LIGHTING, false);
     display->set(ALPHA_BLEND, true);
     render(LIT_OBJECT);
     display->set(ALPHA_BLEND, false);
     display->set(LIGHTING, true);
     render(OPAQUE_OBJECT);
     display->set(ALPHA_BLEND, true);
     render(TRANSLUCENT_OBJECT);
     display->set(ALPHA_BLEND, false);
     display->endDrawFrame();
 }

Suspend, Restore and Release

The suspend() method suspends each Light source in preparation of loss of focus: 

 void Coordinator::suspend() {
     // ...
     for (unsigned i = 0; i < light.size(); i++)
         if (light[i]) light[i]->suspend();
     // ...
 }

The restore() method restores each Light source to its pre-suspension state and resets the colour dithering and specularity states:

 void Coordinator::restore() {
     // ...
     display->set(DITHERING, true);
     display->set(SPECULARITY, true);
     for (unsigned i = 0; i < light.size(); i++)
         if (light[i]) light[i]->restore();
     // ...
 }

The release() method deallocates the memory for light indexing: 

 void Coordinator::release() {
     // ...
     Light::dealloc();
     // ...
 }

Destroy

The destructor deletes all Light objects that still exist:

 Coordinator::~Coordinator() {
     // ...
     for (unsigned i = 0; i < light.size(); i++)
         if (light[i]) light[i]->Delete();
     // ...
 }

Lighting

The Lighting component manages all of the light sources in the framework.  This component consists of the Light class in the Model Layer and the APILight class in the Translation Layer.  The Design component refers to light sources through Light objects alone.  These objects communicate with the supporting API through the underlying APILight objects. 

Lighting Component

The APILight objects accesses the graphics hardware through the IDirect3DDevice9 interface. 

Lighting classes

Each Light object has its own reference frame in world space, which we can reposition and reorient.

Light Class

The iLight interface exposes six virtual methods to the framework (in addition to those exposed by Frame and iSwitch:

 class iLight : public Frame, public iSwitch, public Base {
     virtual bool toggle()          = 0; // toggles on/off state of source
     virtual void update()          = 0; // updates the light source
     virtual void suspend()         = 0; // suspends the light source
     virtual void restore()         = 0; // restores the light source
     virtual void release()         = 0; // releases the light source
     virtual void Delete() const    = 0; // deletes the light source
     friend class Coordinator;
     friend class Design;
   public:
     virtual iLight* clone() const = 0;
 };
 iLight* CreateDistantLight(Colour, Colour, Colour, bool = true);
 iLight* CreatePointLight(Colour, Colour, Colour, float, bool, float = 1,
  float = 0, float = 0);
 iLight* CreateSpotLight(Colour, Colour, Colour, float, bool, float = 1,
  float = 0, float = 0, float = 0, float = 0, float = 0);
 iLight* Clone(const iLight*);

The first three global functions create the three types of Light.  The fourth global function creates a clone of any Light.

The Light class holds the properties of a light source.  The instance variables include:

 class Light : public iLight {
     iAPILight* apiLight;   // points to the APILight object
     bool       on;         // light is on?
     LightType  type;       // directional, point, or spot?
     bool       turnOn;     // turn on this light at update?
     bool       turnOff;    // turn off this light at update?
     int        lastToggle; // time of the last toogle

     Light(const Light&);
     virtual ~Light();
   public:
     static void alloc();
     static void dealloc();
     Light(LightType, Colour, Colour, Colour, float, bool, float = 1,
      float = 0, float = 0, float = 0, float = 0, float = 0);
     Light& operator=(const Light&);
     iLight* clone() const { return new Light(*this); }
     static void setAmbient(const Colour&);
     bool isOn() const     { return on; }
     bool toggle();
     void update();
     void suspend();
     void restore();
     void release() { }
     void Delete() const   { delete this; }
 };

Create a Light

Each Create*Light() function creates a different type of light source on dynamic memory. 

 iLight* CreateDistantLight(Colour d, Colour a, Colour s, bool o) {
     return new Light(DIRECTIONAL_LIGHT, d, a, s, 0, o);
 }

 iLight* CreatePointLight(Colour d, Colour a, Colour s, float r, bool o,
  float a0, float a1, float a2) {
     return new Light(POINT_LIGHT, d, a, s, r, o, a0, a1, a2);
 }

 iLight* CreateSpotLight(Colour d, Colour a, Colour s, float r, bool o,
  float a0, float a1, float a2, float ph, float th, float f) {
     return new Light(SPOT_LIGHT, d, a, s, r, o, a0, a1, a2, ph, th, f);
 }

 iLight* Clone(const iLight* src) {
     return src->clone();
 }

Class Methods

The alloc() method allocates memory for the light indexing:

 void Light::alloc() { APILight::alloc(); }

The dealloc() method deallocates memory for the light indexing:

 void Light::dealloc() { APILight::dealloc(); }

The setAmbient() method sets the ambient lighting for a scene:

 void Light::setAmbient(const Colour& ambient) {
     APILight::setAmbient(ambient);
 }

The set() method sets the render states for lighting on the scene:

 void Light::set(RenderState state, bool b) { APILight::set(set, b); }

Construct

The constructor stores the source properties, adds its address to the coordinator, creates the APILight object that communicates with the API and initializes the time of the last toogle:

 Light::Light(LightType t, Colour d, Colour a, Colour s, float r, bool o,
  float a0, float a1, float a2, float ph, float th, float f) : on(false),
  turnOn(o), turnOff(false), type(t) {
     coordinator->add(this);
     apiLight = CreateAPILight(t, d, a, s, r, o, a0, a1, a2, ph, th, f);
     lastToggle  = 0;
 }

Toggle

The toggle() method resets the state change flag for updating:

 bool Light::toggle() {
     bool rc = on;

     if (now - lastToggle > KEY_LATENCY) {
         if (on) turnOff = true;
         else    turnOn  = true;
         rc         = !rc;
         lastToggle = now;
     }
     return rc;
 }

Update

The update() method turns on, turns off or updates the light source through the APILight object: 

 void Light::update() {
     Vector pos;
     Vector dir;
     if (turnOn || on) {
         pos = position();
         dir = orientation('z');
     }
     if (turnOn) {
         apiLight->turnOn(pos, dir);
         turnOn = false;
         on     = true;
     }
     else if (turnOff) {
         apiLight->turnOff();
         turnOff = false;
         on      = false;
     }
     else if (on) {
         apiLight->update(pos, dir);
     }
 }

Suspend and Restore

The suspend() method suspends the APILight and saves the state for subsequent restoration: 

 void Light::suspend() {
     if (apiLight)
         apiLight->suspend(this);
     turnOn  = on || turnOn;
     turnOff = false;
 }

The restore() method resets the time of the last update: 

 void Light::restore() { lastToggle = now; }

Destroy

The destructor removes the light source's address from the Coordinator object:

 Light::~Light() { coordinator->remove(this); }

APILight Class

The APILight class connects the Light class to the supporting API. 

The iAPILight interface exposes the seven virtual methods that wrap the lighting calls to the Graphics API: 

 // iAPILight.h

 class iAPILight {
     virtual iAPILight* clone() const                  = 0;
     virtual void turnOn(const Vector&, const Vector&) = 0;
     virtual void update(const Vector&, const Vector&) = 0;
     virtual void turnOff()                            = 0;
     virtual void suspend()                            = 0;
     virtual void Delete() const                       = 0;
     friend class Light;
 };
 iAPILight* CreateAPILight(LightType, Colour, Colour, Colour, float,
  bool, float, float, float, float = 0, float = 0, float = 0);

The class variables of the APILight class are:

  • d3dd - points to the display device
  • lightIndex - points to the array of flags identifying light indices that are available
  • maxLights - the maximum number of active lights

The instance variables of the APILight class are:

  • index - the index by which the Graphics API identifies this light source
  • isSetup - the flag that identifies whether this light source is connected to the Graphics API
 // APILight.h
 class APILight : public iAPILight, public APIBase {
     static bool* lightIndex;       // list of available device lights
     static int   maxLights;        // maximum number of lights
     int          index;        // identifier for this light
     bool         isSetup;      // this api light has been setup?
     LightType    type;         // light type
     Colour       diffuse;      // diffuse component
     Colour       ambient;      // ambient component
     Colour       specular;     // specular component
     float        range;        // beyond which light ceases
     float        attenuation0; // constant attenuation
     float        attenuation1; // linear attenuation
     float        attenuation2; // quadratic attenuation
     float        phi;          // angle of outer spot in radians
     float        theta;        // angle of inner spot in radians
     float        falloff;      // falloff factor [0,1]

     APILight(const APILight&);
     bool setup();
   public:
     static void alloc();
     static void dealloc();
     APILight(LightType, Colour, Colour, Colour, float, bool, float,
      float, float, float, float, float);
     iAPILight* clone() const { return new APILight(*this); }
     APILight& operator=(const APILight& src);
     static void setAmbient(const Colour&);
     static void set(RenderState, bool);
     void turnOn(const Vector&, const Vector&);
     void update(const Vector&, const Vector&);
     void turnOff();
     void suspend();
     void Delete() const { delete this; }
 };

Implementation

The class variables initially point nowhere: 

 bool* APILight::lightIndex = nullptr;
 int   APILight::maxLights  = 0;

Class Methods

The alloc() class method allocates dynamic memory for the activity flags and initializes them as available: 

 void APILight::alloc() {
     // maximum number of lights supported by the display device
     D3DCAPS9 caps;
     d3dd->GetDeviceCaps(&caps);
     maxLights = caps.MaxActiveLights ?
      caps.MaxActiveLights : MAX_ACTIVE_LIGHTS;
     if (maxLights > (unsigned)MAX_ACTIVE_LIGHTS)
         maxLights = MAX_ACTIVE_LIGHTS;
     if (lightIndex)
         delete [] lightIndex;
     lightIndex = new bool[maxLights];
     for (int i = 0; i < maxLights; i++)
         lightIndex[i] = true;
 }

The dealloc() class method deallocates the dynamic memory previously allocated: 

 void APILight::dealloc() {
     if (lightIndex)
         delete [] lightIndex;
     lightIndex = nullptr;
 }

The set() class method sets the render state for the lighting on the scene: 

 void APILight::set(RenderState state, bool b) {
     if (d3dd) {
         switch (state) {
             case DITHERING:
                 d3dd->SetRenderState(D3DRS_DITHERENABLE, b); break;
             case SPECULARITY:
                 d3dd->SetRenderState(D3DRS_SPECULARENABLE, b); break;
         }
     }
 }

The SetRenderState() method on the display device sets the render states for lighting.  (D3DRS_DITHERENABLE, TRUE) turns on colour dithering.  Dithering is a colouring technique that creates the illusion of colour depth.  (D3DRS_SPECULARENABLE, TRUE) turns on specular lighting. 

The setAmbient() class method sets the ambient lighting on the scene: 

 void APILight::setAmbient(const Colour& a) {
     if (d3dd) d3dd->SetRenderState(D3DRS_AMBIENT,
          D3DCOLOR_COLORVALUE(a.r, a.g, a.b, 1.0f));
 }

D3DRS_AMBIENT is the enumeration constant for global ambient lighting. 

Construct

The constructor initializes the instance variables with the received parameter values: 

 APILight::APILight(LightType t, Colour d, Colour a, Colour s, float r,
  bool o, float a0, float a1, float a2, float p, float th, float f) :
  type(t), diffuse(d), ambient(a), specular(s), range(r), attenuation0(a0),
  attenuation1(a1), attenuation2(a2), phi(p), theta(th), falloff(f),
  isSetup(false), index(0) {}

Setup

The setup() method on the APILight object finds an available index for the object and implements a new light at that index: 

 bool APILight::setup() {
     bool rc = false;

     index = -1;
     for (int i = 0; i < maxLights; i++) {
         if (lightIndex && lightIndex[i]) {
             lightIndex[i] = false;
             index = i;
             i = maxLights;
         }
     }
     if (index == -1)
         error(L"APILight::21 No more room for lights on this device");
     else {
         D3DLIGHT9 d3dLight;
         ZeroMemory(&d3dLight, sizeof d3dLight);
         switch (type) {
           case POINT_LIGHT:
             d3dLight.Type = D3DLIGHT_POINT;
             break;
           case SPOT_LIGHT:
             d3dLight.Type = D3DLIGHT_SPOT;
             break;
           case DIRECTIONAL_LIGHT:
             d3dLight.Type = D3DLIGHT_DIRECTIONAL;
             break;
         }
         d3dLight.Diffuse      = D3DXCOLOR(diffuse.r, diffuse.g,
          diffuse.b, diffuse.a);
         d3dLight.Ambient      = D3DXCOLOR(ambient.r, ambient.g,
          ambient.b, ambient.a);
         d3dLight.Specular     = D3DXCOLOR(specular.r, specular.g,
          specular.b, specular.a);
         d3dLight.Attenuation0 = attenuation0;
         d3dLight.Attenuation1 = attenuation1;
         d3dLight.Attenuation2 = attenuation2;
         d3dLight.Phi          = phi;
         d3dLight.Theta        = theta;
         d3dLight.Falloff      = falloff;
         d3dLight.Range        = range;
         d3dLight.Position     = D3DXVECTOR3(0, 0, 0);
         d3dLight.Direction    = D3DXVECTOR3(0, 0, 0);

         if (!d3dd || FAILED(d3dd->SetLight(index, &d3dLight)))
             error(L"APILight::22 Failed to create device light");
         else
             rc = true;
     }
     return rc;
 }

The SetLight() method on the display device stores the instance of the D3DLIGHT9 struct as the properties associated with light index

Turn On

The turnOn() method on the APILight object updates and turns on light index

 void APILight::turnOn(const Vector& p, const Vector& o) {
     if (!isSetup) isSetup = setup();
     if (isSetup) {
         D3DLIGHT9 d3dLight;
         if (FAILED(d3dd->GetLight(index, &d3dLight)))
             error(L"APILight::23 Failed to find a device light");
         else {
             d3dLight.Position  = D3DXVECTOR3(p.x, p.y, p.z);
             d3dLight.Direction = D3DXVECTOR3(o.x, o.y, o.z);
             if (FAILED(d3dd->SetLight(index, &d3dLight)))
                 error(L"APILight:24 Failed to update position");
         }
         d3dd->LightEnable(index, true);
     }
 }

The LightEnable() method on the display device turns on light index

Update

The update() method on the APILight object updates the position and direction of light index

 void APILight::update(const Vector& p, const Vector& o) {
     if (!isSetup) isSetup = setup();
     if (isSetup) {
         D3DLIGHT9 d3dLight;
         if (FAILED(d3dd->GetLight(index, &d3dLight)))
             error(L"APILight::23 Failed to find a device light");
         else {
             d3dLight.Position  = D3DXVECTOR3(p.x, p.y, p.z);
             d3dLight.Direction = D3DXVECTOR3(o.x, o.y, o.z);
             if (FAILED(d3dd->SetLight(index, &d3dLight)))
                 error(L"APILight:24 Failed to update position");
         }
         d3dd->LightEnable(index, true);
     }
 }

Turn Off

The turnOff() method on the APILight object turns off light index

 void APILight::turnOff() {
     if (isSetup && d3dd)
         d3dd->LightEnable(index, false);
 }

Suspend

The suspend() method on the APILight object turns off the light and releases its index: 

 void APILight::suspend() {
     turnOff();
     if (isSetup) {
         lightIndex[index] = true;
         isSetup = false;
     }
 }

Object

The Object component handles the material reflectivity for opaque and translucent objects. 

class describes the structure of design units that represent specific objects in a scene.  These objects consist of the following properties:

  • a material relfectivity
  • a graphic representation

These objects can also belong to different categories or subsets for the purpose of drawing sets or subsets of objects together.

The iObject interface exposes a global function that takes material reflectivity as one of its parameters:

 // iObject.h

 class iObject : public Frame, public Base {
     // ...
 };
 iObject* CreateObject(iGraphic* v, const Reflectivity* r);
 iObject* Clone(const iObject*);

The class definition includes the reflectivity as an instance variable:

The new method compares the current object's Category to that received.

 // Object.h

 class Object : public iObject {
     // ...
     Reflectivity   reflectivity;   // material reflectivity
     // ...
   public:
     Object(Category, iGraphic*, const Reflectivity* = nullptr);
     // ...
 };

Implementation

The CreateObject() function determines from the reflectivity whether the object is opaque or translucent and calls the constructor with the corresponding category:

 iObject* CreateObject(iGraphic* v, const Reflectivity* r) {
     Category category = OPAQUE_OBJECT;
     if (r->translucent())
         category = TRANSLUCENT_OBJECT;
     return new Object(category, v, r);
 }

Construct

The constructor stores the current object's material reflectivity and sets its category to TRANSLUCENT_OBJECT or OPAQUE_OBJECT depending on the alpha value of its reflectivity:

 Object::Object(Category c, iGraphic* v, const Reflectivity* r) : category(c),
  graphic(v) {
     coordinator->add(this);
     reflectivity = *r;
     texture = nullptr;
 }

Render

The render() method sets the material reflectivity on the display device before drawing the graphical representation:

 void Object::render() {
     if (graphic) {
         if (category == SPRITE) {
             // ...
         }
         else if (category == LIT_OBJECT) {
             // ...
         }
         else {
             graphic->setWorld(&world());
             graphic->set(&reflectivity);
             graphic->render();
         }
     }
  }

Graphic

The Graphic component handles the combination of lighting and material properties for graphic representations composed of unlit vertices. 

The iGraphic interface includes prototypes for global functions that create light source sensitive graphic representations.  The interface exposes a virtual method for setting the material reflectivity:

 // iGraphic.h

 class iGraphic : public Base {
     // ...
     virtual void set(void*) = 0;
     // ...
 };
 // ...
 iGraphic* CreateBox(float, float, float, float, float, float);
 iGraphic* CreateGrid(float, float, int);
 iGraphic* TriangleList(const wchar_t* file);
 iGraphic* CreateRectangleList(float, float, float, float);

The class definition includes a stub on the base class and a call to the APIVertexList object on the derived class:

 // Graphic.h

 class Graphic : public iGraphic {
     // ...
   public:
     // ...
     void set(void*) {}
     // ...
 };

 template <class T = Vertex>
 class VertexList : public Graphic {
     // ...
   public:
     // ...
     void set(void* r) { apiVertexList->setReflectivity(r); }
     // ...
 };

Create Functions

The CreateBox() function now creates a primitive set using lit vertices:

 iGraphic* CreateBox(float minx, float miny, float minz, float maxx,
  float maxy, float maxz) {
     VertexList<Vertex>* vertexList =
      (VertexList<Vertex>*)CreateVertexList<Vertex>(TRIANGLE_LIST, 12);
     float x = (minx + maxx) / 2;
     float y = (miny + maxy) / 2;
     float z = (minz + maxz) / 2;
     minx -= x;
     miny -= y;
     minz -= z;
     maxx -= x;
     maxy -= y;
     maxz -= z;
     // bounding sphere
     float max;
     max = maxx > maxy ? maxx : maxy;
     max = maxz > max  ? maxz : max;
     vertexList->setRadius(1.73205f * max);
     // locate centroid at origin
     Vector p1 = Vector(minx, miny, minz),
            p2 = Vector(minx, maxy, minz),
            p3 = Vector(maxx, maxy, minz),
            p4 = Vector(maxx, miny, minz),
            p5 = Vector(minx, miny, maxz),
            p6 = Vector(minx, maxy, maxz),
            p7 = Vector(maxx, maxy, maxz),
            p8 = Vector(maxx, miny, maxz);
     add(vertexList, p1, p2, p3, p4, Vector(0, 0, -1)); // front
     add(vertexList, p4, p3, p7, p8, Vector(1, 0,  0)); // right
     add(vertexList, p8, p7, p6, p5, Vector(0, 0,  1)); // back
     add(vertexList, p6, p2, p1, p5, Vector(-1, 0, 0)); // left
     add(vertexList, p1, p4, p8, p5, Vector(0, -1, 0)); // bottom
     add(vertexList, p2, p6, p7, p3, Vector(0, 1,  0)); // top
     return vertexList;
 }

The CreateGrid() function now creates a primitive set using lit vertices: 

 iGraphic* CreateGrid(float min, float max, int n) {
     VertexList<Vertex>* vertexList =
      (VertexList<Vertex>*)CreateVertexList<Vertex>(LINE_LIST, 2*n+2);
     float x = (min + max) / 2;
     min -= x;
     max -= x;
     // bounding sphere
     vertexList->setRadius(1.73205f * max);
     float cur = min, inc = (max - min) / float(n - 1);
     for (int i = 0; i < n; i++, cur += inc) {
         // in the local x direction
         vertexList->add(Vertex(Vector(min, 0, cur), Vector(0, 1, 0)));
         vertexList->add(Vertex(Vector(max, 0, cur), Vector(0, 1, 0)));
         // in the local z direction
         vertexList->add(Vertex(Vector(cur, 0, min), Vector(0, 1, 0)));
         vertexList->add(Vertex(Vector(cur, 0, max), Vector(0, 1, 0)));
     }
     return vertexList;
 }

The CreateRectangleList() function now creates a rectangle using lit vertices: 

 iGraphic* CreateRectangleList(float minx, float miny, float maxx,
  float maxy) {
     VertexList<Vertex>* vertexList =
      (VertexList<Vertex>*)CreateVertexList<Vertex>(TRIANGLE_LIST, 2);
     float x = (minx + maxx) / 2, y = (miny + maxy) / 2;
     minx -= x;
     miny -= y;
     maxx -= x;
     maxy -= y;
     // bounding sphere
     float max;
     max = maxx > maxy ? maxx : maxy;
     vertexList->setRadius(1.73205f * max);
     // locate centroid at origin
     Vector p1 = Vector(minx, miny, 0),
            p2 = Vector(minx, maxy, 0),
            p3 = Vector(maxx, maxy, 0),
            p4 = Vector(maxx, miny, 0);
     add(vertexList, p1, p2, p3, p4, Vector(0, 0, -1));
     return vertexList;
 }

The TriangleList() function now creates a rectangle using lit vertices: 

 iGraphic* TriangleList(const wchar_t* file) {
     iGraphic* graphic = nullptr;
     // construct filename with path
     int len = strlen(file) + strlen(ASSET_DIRECTORY) + 1;
     wchar_t* absFile = new wchar_t[len + 1];
     nameWithDir(absFile, ASSET_DIRECTORY, file, len);
     // open file for input
     std::wifstream in(absFile, std::ios::in);
     delete [] absFile;
     float x, y, z, nx, ny, nz, tu, tv, xc = 0, yc = 0, zc = 0;
     unsigned no = 0;
     // count the number of records
     while (in) {
         in >> x >> y >> z >> nx >> ny >> nz >> tu >> tv;
         if (in.good()) {
             xc += x;
             yc += y;
             zc += z;
             no++;
         }
     }
     in.clear();
     in.seekg(0);
     if (no) {
         float max = 0;
         VertexList<Vertex>* vertexList =
          (VertexList<Vertex>*)CreateVertexList<Vertex>(TRIANGLE_LIST,
          no / 3);
         xc /= no;
         yc /= no;
         zc /= no;
         for (unsigned i = 0; i < no; i++) {
             in >> x >> y >> z >> nx >> ny >> nz >> tu >> tv;
             vertexList->add(Vertex(Vector(x - xc, y - yc, (z - zc)),
                 Vector(nx, ny, nz)));
             if (x - xc > max) max = x - xc;
             if (y - yc > max) max = y - yc;
             if (z - zc > max) max = z - zc;
         }
         graphic = vertexList;
         // bounding sphere
         vertexList->setRadius(1.73205f * max);
     }
     return graphic;
 }

Vertex Class

The Vertex object holds the position in local coordinates and the direction of the normal to the reflective surface at that location. 

The instance variables include the coordinates of the vertex and the coordinates of the normal in local space:

 class Vertex {
     float x;   // x coordinate in the local frame
     float y;   // y coordinate in the local frame
     float z;   // z coordinate in the local frame
     float nx;  // x coordinate of normal in the local frame
     float ny;  // y coordinate of normal in the local frame
     float nz;  // z coordinate of normal in the local frame
   public:
     Vertex();
     Vertex(const Vector& p, const Vector& n);
     void   populate(void**) const;
     Vector position() const;
 };

The instance variables include the coordinates of the vertex and the coordinates of the normal in local space:

 template <>
 D3DVERTEXELEMENT9 APIVertexDeclaration::fmt[MAXD3DDECLLENGTH + 1]
  = {
  { 0,  0, D3DDECLTYPE_FLOAT3, D3DDECLMETHOD_DEFAULT,
   D3DDECLUSAGE_POSITION, 0},
  { 0, 12, D3DDECLTYPE_FLOAT3, D3DDECLMETHOD_DEFAULT,
    D3DDECLUSAGE_NORMAL, 0},
  D3DDECL_END()};

 template<>
 unsigned APIVertexDeclaration<Vertex>::vertexSize = 24;

Construct

The constructor for the Vertex object fills the vertex buffer with the position and normal for the vertex: 

 Vertex::Vertex() : x(0), y(0), z(0), nx(0), ny(0), nz(0) {}

 Vertex::Vertex(const Vector& p, const Vector& n) :
  x(p.x), y(p.y), z(p.z), nx(n.x), ny(n.y), nz(n.z) {}

Populate

The populate() method on the Vertex object fills the vertex buffer with the position and normal for the vertex: 

 void Vertex::populate(void** pv) const {
     float* p = *(float**)pv;
     *p++ = x;
     *p++ = y;
     *p++ = z;
     *p++ = nx;
     *p++ = ny;
     *p++ = nz;
     *pv  = p;
 }

Exercises

  • Read the Wikipedia article on Gouraud Shading.
  • Read the Wikipedia article on Phong Shading.
  • Read the DirectX Documentation on Lighting and Materials:
    • Lights and Materials
    • Mathematics of Lighting
    • Light Properties
    • Light Types
    • Shading Modes
  • Introduce another light - a spot light - into the Design component.
  • Challenge: introduce a distant light that revolves about the z-axis of world space.



Previous Reading  Previous: Heads Up Display Next: Texturing   Next Reading


  Designed by Chris Szalwinski   Copying From This Site   
Logo