Chapter 7: Adding Some Atmosphere - Lighting and Fog


So far, all of the rendering that we have done has been with DirectX lighting turned off. With lighting turned off, every pixel drawn is colored strictly based on the texture or material colors defined for each object. Lighting and fog adjust the rendered color of each pixel based on different factors.

The lighting system combines the colors of any lights illuminating a pixel with the base colors specified for the pixel by the material and texture definitions. Fog, on the other hand, integrates the color of the pixels between this base color (as altered by any lighting) and a selected fog color. The integration is based on the range between the pixel s position and the viewpoint.

Shedding Some Light on the Subject

We can define several types of lights within our game scene. Each type of light behaves somewhat differently in how it illuminates the pixels. The four types of light are ambient, directional, point, and spot. We will discuss each type of light in the text that follows .

Ambient Light

Ambient light is unique among the four light types. A given scene may include multiple lights of the other three types, but it can have only one ambient light. As such, it is the one type of light that is implemented as a rendering state rather than a light. This rendering state is simply the color of the ambient light. Ambient light by definition exists everywhere and illuminates in all directions.

The color of the ambient light is used to modulate the base color of the pixels. If the ambient color for a scene is set to black and there are no other lights, then the scene will be completely black. This reflects a total lack of illumination . Figure 7 “1 shows a scene from our game engine with the ambient light set to a dark gray.

click to expand
Figure 7 “1: Low ambient light

As you can see from the picture, the entire scene is quite dark. This is a good basis for working with the other lights as we progress through the rest of the chapter. Although keeping the three components of the color the same (i.e., shades of gray from black to white) is most natural, this is not a requirement. Let s say that we are doing a game based on Mars instead of Earth. We might want the ambient light to have a reddish tint to it so that everything takes on a reddish cast. Only objects that are illuminated by a separate white light would lose the tinted look.

Note

For any of the lighting to work at all, there must be a material active in the current rendering state. For the mesh objects, this is done for us. Looking back at the other objects, you will see that a default white material is set for the billboards and skybox objects.

Directional Light

While ambient light is the same everywhere at once and does not come from any one direction, it does not provide everything we need for realistic lighting. Directional light behaves like light from the sun. It is light coming from a specified direction, with all of the rays of light traveling parallel. Since directional light behaves like light from the sun, a scene tends to have just one directional light. Again, if we were doing a science fiction game set on another planet, we might simulate the effects of multiple suns with different colors. Since each of these suns would give light from a different direction, we would get changes in object color depending on the angle from which we are looking.

Figure 7 “2 shows our scene lit by a directional light. One thing we must do when adding lights other than ambient light is to render the skybox with the lights deactivated. Since only ambient light is directionless, we do not want the other lights to affect the skybox. Otherwise, we will begin to see the seams where the sides of the skybox meet. This breaks the illusion the skybox is there to provide. Therefore, remember that the ambient light sets the basic lighting conditions for the scene, including the skybox, and all other lights add to the look of the terrain and other objects within the scene.

click to expand
Figure 7 “2: Directional light

Point Light

Ambient and directional lights provide the illumination for the simulated world at large. The two remaining light types provide localized light sources within the scene. The first of these is the point light, which is a point in space where light originates in all directions equally. Think of point lights as things like bare lightbulbs and torches. The parameters that configure a point light determine how far the light shines and how quickly the light intensity falls off as we get farther from the light. Unlike with the first two light types, it is quite possible to have a large number of point lights within a scene. Unfortunately, there are no video cards on the market that can support an unlimited number of such lights within a scene.

To cope with the limitations of video cards, we will need to do two things. The first is to know exactly what these limitations are. We need to find how many lights we can have at once. It is possible to query the capabilities of the video card to get this information. The other thing that our game engine must do is select which lights to include in the scene on a given pass so that we do not exceed the limits of the video card. You will see later in the chapter, when we get into the implementation of the GameLights class, how we will choose the lights to use.

Figure 7 “3 shows our desert scene in a low-light condition with a number of point lights distributed around the scene. You can t see the lights themselves . What you will see is the illumination of the terrain surface below each light. Notice that not all areas around the lights are illuminated in the circular pattern that we would expect from a point light. This is due to the fact that the fixed function graphics pipeline performs vertex-based lighting. The colors are determined at each vertex based on the lighting information and then integrated between the vertices. To get pixel-based lighting (which is far more precise and realistic looking) would require the use of a pixel shader. Since the use of shaders is not being covered in this book, we will stick with what the fixed function pipeline provides.

click to expand
Figure 7 “3: Point lights

Spot Light

The final light type is the spot light. Spot lights are used to provide light that shines in a cone shape in the specified direction from a given position. Actually, this type of light is implemented as two cones, one inside the other, which mimics the way that spotlights , flashlights, and vehicle headlights work in the real world. A point light has an inner cone within which the light is at its brightest. It also has an outer cone that surrounds the inner cone, and the intensity of the light falls off as we move from the inner cone to the outside edge of the outer cone.

Figure 7 “4 shows our night scene with a spot light configured as the headlight of our car. Again, notice that the footprint of the light on the ground is not the cone shape that we would expect in an ideal situation. This is the same vertex versus pixel lighting issue mentioned in the discussion of the point lights.

click to expand
Figure 7 “4: Spot lights

Implementing the GameLights Class

The game engine requires a class to manage the lights in the scene. The class will be organized into two sections. The first section will be the attributes, properties, and methods that configure an individual light. The second section will be the static attributes, properties, and methods that are used to create and manage the lights.

The attributes for the class (shown in Listing 7 “1) closely follow the attributes that Microsoft has defined for its Light class. This is not a coincidence . The rendering device s array of lights will be configured using the data in the GameLights instances. The only nonstatic attributes that are unique to the GameLights class are m_DirectionOffset and m_PositionOffset . These attributes will be used when we attach a GameLights object to another object. The class also inherits from the Object3D class so that each light can be treated like any other object in the scene when it comes to positioning and updating the lights.

Listing 7.1: GameLights Attributes
start example
 public class GameLights : Object3D, IDisposable, IDynamic, IComparable  {     #region Attributes     private LightType m_Type = LightType.Point;     private Vector3   m_Direction = new Vector3(0.0f,0.0f,0.0f);     private Vector3   m_DirectionOffset = new Vector3(0.0f,0.0f,0.0f);     private Vector3   m_PositionOffset = new Vector3(0.0f,0.0f,0.0f);     private Color     m_Diffuse = Color.White;     private Color     m_Specular = Color.White;     private float     m_EffectiveRange = 1000.0f;     private float     m_Attenuation0 = 0.0f;     private float     m_Attenuation1 = 1.0f;     private float     m_Attenuation2 = 0.0f;     private float     m_FallOff = 1.0f;     private float     m_InnerConeAngle = 0.5f;     private float     m_OuterConeAngle = 1.0f;     private bool      m_Deferred = true;     private bool      m_Enabled = true;     // A static array that will hold all lights     private static Color     m_Ambient = Color.White;     private static ArrayList m_ActiveLights = new ArrayList();     private static ArrayList m_InactiveLights = new ArrayList();     private static int m_max_lights = 1;     private static int m_num_activated = 0;     #endregion 
end example
 

The interfaces that are used in the class should be familiar by now, with the exception of the IComparable interface. We are going to want to sort an array of lights as part of deciding which lights will be active. The IComparable interface declares the CompareTo method that is called by the ArrayList s Sort method.

The static attributes are used for managing the instances of the lights that we will create. We will keep two arrays of lights. One array is for the lights that are on and another array for lights that are currently turned off. We also want to keep track of how many lights have been activated within the video card and how many lights the card can manage. The color value for the ambient light is also managed as a static property since there can only be one value for the ambient light.

The properties of this class (shown in Listing 7 “2) provide controlled access to the attributes of the class. Most of the properties are basic Get/Set methods that simply provide access and nothing more. The Type property is one exception, since it is read-only. We wish to allow the light type to be queried, but we don t want anyone changing the light s type once it has been created. The one property that is significantly more complex is the Enabled property. When the enabled state of a light changes, we need to do more than change the state of the Boolean attribute. We must ensure that the light is on the proper list for its current state. We will remove the light from both lists prior to adding it back onto the proper list to ensure that a light does not end up on a list more than once. This is safe to do since the Remove method of the ArrayList class does not throw an exception if we ask it to remove something that it does not have. If we only remove the light from one list and add it to another, we would get into trouble if the light were set to the same state twice in a row.

Listing 7.2: GameLights Properties
start example
 #region Properties  public LightType Type { get { return m_Type; } }  public Vector3   Direction {     get { return m_Direction; }     set { m_Direction = value; }}  public Vector3   DirectionOffset {     get { return m_DirectionOffset; }      set { m_DirectionOffset = value; }}  public Vector3   PositionOffset {     get { return m_PositionOffset; }     set { m_PositionOffset = value; }}  public Color     Diffuse {     get { return m_Diffuse; }     set { m_Diffuse = value; }}  public Color     Specular {     get { return m_Specular; }     set { m_Specular = value; }}  public float     EffectiveRange {     get { return m_EffectiveRange; }     set { m_ EffectiveRange = value; }}  public float     Attenuation0 {     get { return m_Attenuation0; }     set { m_Attenuation0 = value; }}  public float     Attenuation1 {     get { return m_Attenuation1; }     set { m_Attenuation1 = value; }}  public float     Attenuation2 {     get { return m_Attenuation2; }     set { m_Attenuation2 = value; }}  public float     FallOff {     get { return m_FallOff; }     set { m_FallOff = value; }}  public float     InnerConeAngle {     get { return m_InnerConeAngle; }     set { m_InnerConeAngle = value; }}  public float     OuterConeAngle {     get { return m_OuterConeAngle; }     set { m_OuterConeAngle = value; }}  public bool      Deferred {     get { return m_Deferred; }     set { m_Deferred = value; }}  public bool Enabled  {     get { return m_Enabled; }     set     {        m_Enabled = value;        // Remove from both lists to ensure it does not get onto a list twice.        m_ActiveLights.Remove (this);        m_InactiveLights.Remove(this);       if (m_Enabled) // Move from inactive list to active list.        {           m_ActiveLights.Add(this);        }        else // Move from active list to inactive list.        {           m_InactiveLights.Add(this);        }     }  }  public static Color Ambient {                              get { return m_Ambient; }                         set { m_Ambient = value; } }  #endregion 
end example
 

The constructor for the class (shown in Listing 7 “3) is quite simple. It accepts the name of the light as an argument and passes that name on to the base class constructor for the Object3D base class. It sets a default range of a thousand units in the range attribute ( m_EffectiveRange ) and copies that same value to the radius. The bounding radius for a light will be the same as its range. In other words, if the light could be shining on polygons that are in the current view, it is a candidate for inclusion in the list of active lights.

Listing 7.3: GameLights Constructor
start example
 public GameLights(string name) :base(name)  {     m_EffectiveRange = 1000.0f;     m_fRadius = m_EffectiveRange;  } 
end example
 

Remember that we added the IComparable interface to our class so that we can easily sort a list of lights based on the criterion of our choice. The CompareTo method (shown in Listing 7 “4) is the required method for implementing this interface. This method returns an integer that is negative if the supplied object is less than the object owning the method, zero if the objects are equal, and positive if the supplied object is less than this one. In our case, we wish to be able to sort the lights based on the distance between the current camera and the origin of the light. This makes the result of the comparison just the integer value of the difference between the two ranges. When we get to the SetupLights method, you will see how implementing this interface simplifies our code.

Listing 7.4: GameLights CompareTo Method
start example
 public int CompareTo(object other)  {     GameLights other_light = (GameLights)other;     return (int) (Range   other_light.Range);  } 
end example
 

It is quite possible that the user of the game engine will need to get a reference to a light at some point based on the light s name. We will provide the GetLight method (shown in Listing 7 “5) to retrieve a reference based on a supplied name. We begin by setting a reference of the correct type to a null condition. If we do not find the requested light, we wish to return a null value. We then iterate through each of the lists (the light may be active or inactive) and set the reference if we find the light.

Listing 7.5: GameLights GetLight Method
start example
 public static GameLights GetLight(string name)  {     GameLights light_found = null;     foreach (GameLights light in m_Active Lights)     {        if (light. Name == name)        {           light_found = light;        }     }     foreach (GameLights light in m_InactiveLights)     {        if (light.Name == name)        {           light_found = light;        }     }     return light_found;  } 
end example
 

The next three methods will be used for the creation of lights. One static method exists for each type of light. Remember that we do not create lights for ambient light ”it just is. The first of these construction methods is AddDirectionalLight (shown in Listing 7 “6). For directional light, we will need to know the color of the light and a direction vector so that we know the direction in which the light is shining. We also need a string with the name for the light because every object in the game engine needs a name for later reference.

Listing 7.6: GameLights AddDirectionalLight Method
start example
 public static GameLights AddDirectionalLight(Vector3  direction,                      Color color, string name)  {     GameLights light = new GameLights(name);     light.m_Diffuse = color;     light.m_Direction = direction;     light.m_Type = LightType.Directional;     m_ActiveLights.Add(light);     return light;  } 
end example
 

We instantiate a new light using the supplied data and set its type to Directional . The light is then added to the active light list. All lights default to active. A reference to the new light is also returned so that the calling method has instant access to the light for additional configuration.

The AddPointLight method (shown in Listing 7 “7) is quite similar to the method used for directional lights. Instead of supplying a direction vector, we need to supply a position. The only other difference is the obvious change of setting the light s type to Point before adding the light to the active list.

Listing 7.7: GameLights AddPointLight Method
start example
 public static GameLights AddPointLight(Vector3  position,                       Color color, string name)  {     GameLights light = new GameLights(name);     light.m_Diffuse = color;     light.Position = position;     light.m_Type = LightType.Point;     m_ActiveLights.Add(light);     return light;  } 
end example
 

The AddSpot Light method (shown in Listing 7 “8) is the last of the light creation methods. Because spot lights have an origin location and a direction, both must be supplied as arguments to the method. The attenuation of the light as we go from the origin to its maximum range is based on the attenuation formula that follows. The Attenuation0 variable provides a constant attenuation. The other two attenuation variables are factors multiplied by the range from the origin and the square of the range from the origin.

click to expand

This equation is used for both the point and spot lights. It is important that all three attenuation factors are not zero at the same time, which, as you can see from the equation, would generate a divide-by-zero exception.

Listing 7.8: GameLights AddSpotLight Method
start example
 public static GameLights AddSpotLight(Vector3  position,                          Vector3 direction, Color color, string name)  {     GameLights light = new GameLights(name);     light.m_Diffuse = color;     light.m_Direction = direction;     light.Position = position;     light.m_Type = LightType.Spot;     light.Attenuation0 = 0.0f;     light.Attenuation1 = 1.0f;     m_ActiveLights.Add(light);     return light;  } 
end example
 

Before we can manage the lights, we need to call the InitializeLights method (shown in Listing 7 “9). This is where we will query the device capabilities to establish how many lights can be active at once. The rendering device has a property named DeviceCaps that exposes all of the capabilities of the video card. The MaxActiveLights property is the one that we are interested in.

Listing 7.9: GameLights InitializeLights Method
start example
 public static void InitializeLights()  {     m_max_lights = CGameEngine.Device3D.DeviceCaps.MaxActiveLights;  } 
end example
 

I mentioned earlier that we need to deactivate all active lights prior to rendering the skybox so that it is not affected by any lighting values other than ambient light. The DeactivateLights method (shown in Listing 7 “10) provides this functionality. It loops through all of the activated lights and disables them within the context of the rendering device. The lights within the device can be modified in two different ways. If the deferred flag for the light is set to false, all changes to that light take effect as the values are set. If the flag is true, none of the changes for that light are passed to the video card until the Commit method for that light is called. It is best to operate with the flag set to true. Otherwise, it is possible for an invalid set of parameters to corrupt the device. A prime example of this situation would be when altering the attenuation factors.

Listing 7.10: GameLights DeactivateLights Method
start example
 public static void DeactivateLights()  {     try     {        for (int i=0; i< m_num_activated; i++)        {           CGameEngine.Device3D.Lights[i].Enabled = false;           CGameEngine.Device3D.Lights[i].Commit();        }     }     catch (DirectXException d3de)     {        Console.AddLine("Unable to Deactivate lights ");        Console.AddLine(d3de.ErrorString);     }     catch (Exception e)     {        Console.AddLine("Unable to Deactivate lights ");        Console.AddLine(e.Message);     } } 
end example
 

The key to the GameLights class is found in the SetupLights static method (shown in Listing 7 “11). This is where we will decide which lights will be shown in the scene for this pass and transfer the information for those lights to the rendering device. The rendering device has an array of the Microsoft Light class, which is where we will store the information for each active light.

Listing 7.11: GameLights SetupLights Method
start example
 public static void SetupLights()  {     int num_active_lights = 0;     CGameEngine.Device3D.RenderState.Lighting = true;     CGameEngine.Device3D.RenderState.Ambient = m_Ambient;     CGameEngine.Device3D.RenderState.SpecularEnable = true;     // Sort lights to be in range order from closest to farthest.     m_ActiveLights.Sort();     try     {        foreach (GameLights light in m_ActiveLights)        {           if (!light.IsCulled && num_active_lights < m_max_lights)           {              Light this_light =                 CGameEngine.Device3D.Lights[num_active_lights];              this_light.Deferred = light.m_Deferred;              this_light.Type = light.m_Type;              this_light.Position = light.m_vPosition;              this_light.Direction = light.m_Direction;              this_light.Diffuse = light.m_Diffuse;              this_light.Specular = light.m_Specular;              this_light.Attenuation0 = light.m_Attenuation0;              this_light.Attenuation1 = light.m_Attenuation1;              this_light.Attenuation2 = light.m_Attenuation2;              this_light.InnerConeAngle = light.m_InnerConeAngle;              this_light.OuterConeAngle = light.m_OuterConeAngle;              this_light.Range = light. m_EffectiveRange;              this_light.Falloff = light.FallOff;              this_light.Enabled = true;              this_light.Commit();              num_active_lights++;           }        }        if (m_num_activated > num_active_lights)        {           for (int i=0; i< (m_num_activated - num_active_lights);           {              Light this_light =                 CGameEngine.Device3D.Lights[num_active_lights+i];              this_light.Enabled = false;              this_light.Commit();           }        }        m_num_activated = num_active_lights;     }     catch (DirectXException d3de)     {        Console.AddLine("dx Unable to setup lights ");        Console.AddLine(d3de.ErrorString);     }     catch (Exception e)     {        Console.AddLine("Unable to setup lights ");        Console.AddLine(e.Message);     }  } 
end example
 

The method begins by setting up the rendering state for lighting using the ambient light value set for the scene. We will set the specular enable flag so that any specular colors that are specified for a light will show up as specular highlights on the objects. If we knew that we were never going to use specular lighting within our game, we could change this to default to the disabled state, thereby providing a slight increase in rendering performance.

The next step is to sort the active lights array so that it goes from the closest light to the one that is the farthest away. This is why we implemented the IComparable interface earlier in the class. It is also why we split the lights up into two arrays. We want this sort to run as quickly as possible, and keeping the inactive lights in a separate array helps with this.

Once the sort is completed, it is just a matter of iterating through the active lights array and transferring data to the rendering device. We will transfer the information only if the light has not been culled and the video card can handle additional lights. We transfer all of the class attributes, regardless of whether or not they are needed for this particular type of light. It would take more processing time to include flow control to check for each light type and transfer a subset.

If there were more lights active during the last pass than we have active now, we need to deactivate the remaining lights. This is done using a simple loop that clears the enable flag for the extra lights and then commits the changes.

The SetupLights method checks the culling state of each light so that we don t use any lighting slots in the video card for lights that would not be visible in the current scene. This culling state is determined in the CheckCulling method (shown in Listing 7 “12), which is a static method of the class that is called prior to the SetupLights method. It takes a reference to the current camera as an argument, since the camera is needed to perform the actual culling check. The method loops through the array of active lights. There is no need to check the culling status of inactive lights.

Listing 7.12: GameLights CheckCulling Method
start example
 public static void CheckCulling (Camera cam)  {     foreach (GameLights light in m_ActiveLights)     {            if (light.m_Type == LightType.Directional)            {               light.Culled = false; // Cant cull a directional light.               light.Range = 0.0f;            }            else            {               if (cam.CheckFrustum(light) != Camera.CullState.AllOutside)               {                  light.Culled = false;                 // We want the absolute value of the range.                 light.m_fRange = Math.Abs(light.m_fRange);              }              else              {                light.Culled = true;              // Big range to sort to end of list                light.Range = 1000000000.0f;              }           }        }     }  } 
end example
 

If the light is directional, then it cannot be culled. We don t bother with checking against the camera. We just set the culling status to false and the range to zero so that it will be sorted to the beginning of the list. For the point lights and spot lights, we call the camera s CheckFrustum method to see if the light shines on any points within the current view of the scene. If the light is not completely outside of the frustum , then we set the culling state to false. Since we are sorting based on the range, we want to use the absolute value of the range provided by the CheckFrustum method. We don t want a light that is a thousand units behind us from taking preference over a light ten units in front of us.

If the light is completely outside of the frustum, we will set the culling state to true and the range to a large number. This will force the culled lights to the end of the list when it is sorted.

Since our GameLights class inherits from Object3D , our lights can be attached as children to another object. This allows us to attach a spot light to a vehicle to act as its headlights. The Update method (shown in Listing 7 “13) is used to keep the light updated properly as objects move around the scene.

Listing 7.13: GameLights Update Method
start example
 public override void Update(float DeltaT)  {     m_fRadius = m_EffectiveRange;     if (m_Parent != null)     {        Matrix matrix = Matrix.Identity;               matrix.RotateYawPitchRoll(m_Parent.Heading,                  m_Parent.Pitch,m_Parent.Roll);               Vector3 pos_offset =                           Vector3.TransformCoordinate(m_PositionOffset,matrix);               m_vPosition = m_Parent.Position + pos_offset;               m_Direction.X = (float)Math.Sin(m_Parent.Attitude.Heading);               m_Direction.Y = (float)Math.Sin(m_Parent.Attitude.Pitch);               m_Direction.Z = (float)Math.Cos(m_Parent.Attitude.Heading);               m_Direction +=  Vector3.TransformCoordinate(m_DirectionOffset,matrix);           }        }     }  } 
end example
 

Regardless of whether the light is attached to another object, we must ensure that the radius value for the light remains equal to the effective range of the light. The game may be adjusting the range of the light. If the radius were not changed to match, the culling would not return the proper state for the light.

If the light is attached to another object, then we need to update the position and direction vector of the light based on the position and attitude of the parent object. A rotation matrix is created using the attitude of the parent. This is used to rotate both the position offset and the directional offset, which are then applied to the parent s data and stored for this light.

Using the GameLights Class

So far in the chapter we have discussed some theory behind lights as used in the game engine as well as the implementation of the GameLights class for the game engine. Now we will see how easy it is to actually use the lights. The example code in Listing 7 “14 illustrates how the lighting is set up in the game application s LoadOptions routine. Setting the ambient light is done simply by setting the Ambient property of the GameLights class to the desired value. An RGB value of all 20s gives a fairly dark scene. We will then create a spot light and attach it to the ownship. The ownship is a reference to the vehicle model that we will be driving around the scene. The term comes from the simulation and training world, where the vehicle being simulated was traditionally an aircraft. Once the light is created, configured, and attached to a vehicle, there is nothing else we need to do with it. It will automatically travel around the scene with the vehicle and illuminate the terrain and other objects before it as it moves along.

Listing 7.14: GameLights Example
start example
 GameEngine.GameLights.Ambient = Color.FromArgb(20,20,20);  GameEngine.GameLights headlights =         GameEngine.GameLights.AddSpotLight(new Vector3(0.0f,0.0f,0.0f),         new Vector3(1.0f,0.0f,1.0f), Color.White, "headlight");  headlights.EffectiveRange = 200.0f;  headlights.Attenuation0 = 1.0f;  headlights.Attenuation1 = 0.0f;  headlights.InnerConeAngle = 1.0f;  headlights.OuterConeAngle = 1.5f;  headlights.PositionOffset = new Vector3(0.0f, 2.0f, 1.0f);  headlights.DirectionOffset = new Vector3(0.0f, 0.00f, 1.0f);  ownship.AddChild(headlights); 
end example
 



Introduction to 3D Game Engine Design Using DirectX 9 and C#
Introduction to 3D Game Engine Design Using DirectX 9 and C#
ISBN: 1590590813
EAN: 2147483647
Year: 2005
Pages: 98

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net