Chapter 6: Camera - The Player s View of the World


In the last few chapters, you have seen me refer to a Camera class. In this chapter, we will take an in-depth look at the Camera class and the important part it plays in the game engine. Three matrices are used to convert three-dimensional object definitions into the two-dimensional data needed to render images to the computer screen. The first is the world matrix that we have been using in the Object3D and derivative classes. That matrix translates positions in object-relative coordinates to world coordinates.

The other two matrices are the view matrix and the projection matrix. These are used to translate the three-dimensional data into two-dimensional screen coordinates. Since these matrices combine to define the view rendered to the screen, let s use the analogy of a video camera for the class that manages them. The view matrix represents the placement and orientation of the camera. The projection matrix represents the lens of the camera.

Looking Through the Lens

The projection matrix is the lens of our camera. Four values define a projection matrix: field of view, aspect ratio, near clipping plane, and far clipping plane. These are used to create not only the projection matrix, but also the viewing frustum. Figure 6 “1 is an illustration of a frustum . Only objects that are within the area enclosed by the frustum will be visible on the screen. The smaller square in the foreground is the near clipping plane. Any objects that are closer to the camera origin than the near clipping plane are considered to be behind the lens. Any objects beyond the far clipping plane are not included in the scene.

click to expand
Figure 6 “1: Viewing frustum

The width of the frustum is defined by the field of view variable. This is typically set to 45 degrees (or a quarter pi radians, if you prefer). The height of the frustum is defined by the combination of the field of view and the aspect ratio. The aspect ratio is the ratio of the width of the frustum to the height. The standard aspect ratio ( assuming that you are rendering a full-screen image) is 1.33 (i.e., 640/480 or 1280/1024).

The near and far clipping planes are ranges in whatever database units that we are using. For example, if all of our positions are defined in meters, the clipping planes will also be defined in meters . The viewing frustum encloses the area that will be rendered to the screen, which is why we use the mathematical definition of the frustum in our culling process. Any object that is completely outside the frustum will not be visible. We would be wasting valuable processing resources rendering those objects.

Adjusting the field of view acts like the zoom feature on a camera. Narrowing the field of view has the same effect as zooming in. Objects in the distance become larger. Widening the field of view is the same as zooming out (up to a point). Widening the field of view much past the 45-degree point will start to create a fishbowl look.

Managing the Camera

Cameras can be used within the game in numerous ways. A camera could be attached to a moving object so that we have a driver s eye view. We could attach a camera so that it is always behind an object, providing a chase view of the action. A camera could also be placed in a fixed location or moved manually by the player. Finally, we can have multiple cameras and switch between them based on player selections or automated switching.

The game engine must provide the flexibility necessary so that the game programmer has all of the tools needed to build the game. This means providing the interfaces required to manage multiple cameras and switch between them.

Remember that any object that is not at least partially within the viewing frustum need not be rendered, since it will not be visible anyway. The camera class will assist in achieving improved efficiency by assisting in the culling process. It will include a method that tests an object or a bounding volume against the camera s frustum. The result returned by this method will be an enumeration value defined with the Camera class. This enumeration (shown in Listing 6 “1) includes the three possible results of the test. Any object is either completely within the frustum, completely outside the frustum, or partially inside.

Listing 6.1: Camera Culling Enumeration
start example
 public class Camera  {     public enum CullState     {        AllInside,        AllOutside,        PartiallyIn,     } 
end example
 

Defining the Camera

The Camera class must know certain things about itself. This information is held within the attribute members of the class (shown in Listing 6 “2a). First the camera needs to know about any objects that it is either attached to or will be looking at. These may very well be the same object. When a camera is attached to an object, it is likely that it will not be attached to the reference point of the object. The camera will be attached with a positional offset. This offset is held within the class as a vector. If the camera is not attached to an object, it will need its own position and attitude. We will default the camera to a position of one unit north and east of the corner of the terrain (to ensure that it is somewhere within the terrain). The class will also hold the two matrices that it must manage: the view matrix and the projection matrix.

Listing 6.2a: Camera Class Definition (Attributes)
start example
 #region Attributes     private Object3D  m_AttachedObject = null;     private Object3D  m_LookAtObject = null;     private Vector3   m_Offset;     private Attitude m_Attitude;     private Matrix m_matProj;     private Matrix m_matView;     private float m_X = 1.0f;     private float m_Y = 1.0f;     private float m_Z = 1.0f;     private Vector3[] vecFrustum; // Corners of the view frustum     private Plane[] planeFrustum; // Planes of the view frustum     private Vector3 m_Eye;     private Vector3 m_LookAt;     private string m_name = "default";     private float m_fFOV = (float)Math.PI/4.0f;     private float m_fAspect = 1.33f;     private float m_fNearPlane = 1.0f;     private float m_fFarPlane = 800.0f;     private ArrayList m_VisibleObjects = null; 
end example
 

Three vectors are used in the creation of the view matrix. One of the vectors is the Up vector, and this defines which way is up for the camera. This vector is useful only if we wish to have the camera be able to achieve attitudes in which the image is rolled to the side or upside down. For this simple engine, we will constrain the camera so that it is always right-side up. Because our Up vector is always pointing straight up, we do not need to have an attribute for it. We will need the other two vectors , which are the Eye and the LookAt vectors. The Eye vector defines the position from which the camera is looking. For an attached camera, this will be the position of the object to which the camera is attached (with any specified offsets). The LookAt vector specifies the direction in which the camera is pointing. This can be used to point the camera at an object or just a specific direction, depending on how the camera is being used.

Each camera must also be given a unique name. This allows the user of the game engine to retrieve a reference to a camera by specifying its name. The default camera that is always created by the game engine will be given the name default.

The next four attributes of the class will be the values used to create the projection matrix. They will default to the standard 45-degree field of view and an aspect ratio of 1.33. The near clipping plane will default to 1 unit in front of the camera, and the far clipping plane will default to 800 units. Anything that is more than 800 units from the camera will not be visible. The final attribute is a collection of objects that are currently in view for the camera.

All of the attributes of the class will be declared as private so that the game engine or any user of the game engine may not directly access them. Public properties will be defined to provide access in a controlled manner as shown in Listing 6 “2b.

Listing 6.2b: Camera Class Definition (Properties)
start example
 Public ArrayList VisibleObjects { get { return m_VisibleObjects; } }  public float Heading {     get { return (float)(m_Attitude.Heading*180.0/Math.PI); } }  public float Pitch {     get { return (float)(m_Attitude.Pitch*180.0/Math.PI); } }  public string Name { get { return m_name; } }  public float X { get { return m_X; } }  public float Y { get { return m_Y; } }  public float Z { get { return m_Z; } }  public Matrix View { get { return m_matView; } }  public Vector3 EyeVector { get { return m_Eye; } }  public Vector3 LookAtVector { get { return m_LookAt; } }  public float FieldOfView  {     set     {        m_fFOV = value;        m_matProj = Matrix.PerspectiveFovLH(m_fFOV, m_fAspect,           m_fNearPlane, m_fFarPlane);     }  }  public float AspectRatio  {     set     {        m_fAspect = value;        m_matProj = Matrix. PerspectiveFovLH(m_fFOV, m_fAspect,           m_fNearPlane, m_fFarPlane);     }  }  public float NearClipPlane  {     set     {        m_fNearPlane = value;        m_matProj = Matrix. PerspectiveFovLH(m_fFOV, m_fAspect,           m_fNearPlane, m_fFarPlane);     }  }  public float FarClipPlane  {     set     {        m_fFarPlane = value;        m_matProj = Matrix. PerspectiveFovLH(m_fFOV, m_fAspect,           m_fNearPlane, m_fFarPlane);     }  }  #endregion 
end example
 

The first two properties concern the attitude of the camera. While the heading and pitch of the camera appear within the Camera class in radians, that is not the most readable format for us humans . These properties are read-only and provide the heading and pitch in degrees. The name of the camera and the position of the camera are exposed as read-only properties as well. We will allow the game engine or the engine s user to look at these values, but the name must not be changed, and there are other ways to alter the position of the camera.

The view matrix and the two vectors used to create it are also made available as read-only properties. You can see how we are using the features of properties to control access to our attributes. In the same way, we will be making the last four properties of the class write-only. These properties are used in setting the values that form the projection matrix. By making them properties, we are able to add the automatic recalculation of the projection matrix anytime one of the four values is changed.

Creating a Camera

There are two constructors for our Camera class, shown in Listing 6 “3. The first constructor is the default that should be used only by the game engine. It creates the default camera (i.e., it leaves the name of the camera as default). The second constructor is the one that will normally be used by anyone building a game with the game engine. It allows us to specify the name for the camera. Both constructors initialize the two matrices that are held by the class. The view matrix is set to the identity matrix. The identity matrix is a safe default, since it equates to a position and attitude of all zeros. The projection matrix is built from the four values described earlier in the chapter. The only other thing required by the constructors is to allocate the arrays that will be used to hold the points defining the corners of the viewing frustum and the six planes that define the sides of the frustum.

Listing 6.3: Camera Constructors
start example
 public Camera()  {     m_matView = Matrix.Identity;     m_Offset = new Vector3(0.0f, 0.0f, 0.0f);     vecFrustum = new Vector3[8];     // Corners of the view frustum     planeFrustum = new Plane[6];     // Planes of the view frustum     m_matProj = Matrix.PerspectiveFovLH(fFOV, fAspect,                                        fNearPlane, fFarPlane);  }  public Camera(string name)  {     m_matView = Matrix.Identity;     m_Offset = new Vector3(0.0f, 0.0f, 0.0f);     vecFrustum = new Vector3[8];     // Corners of the view frustum     planeFrustum = new Plane[6];     // Planes of the view frustum     m_matProj = Matrix.PerspectiveFovLH(fFOV, fAspect,                                 fNearPlane, fFarPlane);     m_name = name;  } 
end example
 

Positioning the Camera

Since the camera will be flexible in the way it is positioned and pointed, we need to include a number of methods to provide for all options. The first three methods are concerned with manual control of camera position and attitude. The first of these, AdjustHeading , is used for pointing the camera. The AdjustHeading method (shown in Listing 6 “4) takes a heading adjustment value in degrees. This value is converted to radians and added to the camera s current heading. We want to keep the range of the heading within 0 to 360 degrees. Whenever adjusting an attitude angle like this, it is a good idea to ensure that the angles stay within a reasonable range. Microsoft s trigonometric routines tend to lose accuracy when the values get too far from this nominal range.

Listing 6.4: Camera AdjustHeading Method
start example
 public void AdjustHeading(float deltaHeading)  {     m_Attitude.Heading += (deltaHeading * (float)Math.PI / 180.0f);     if (m_Attitude.Heading > (2.0f * Math.PI))     {        m_Attitude.Heading -= (float)(2.0f * Math.PI);     }     if (m_Attitude.Heading < 0.0f)     {        m_Attitude.Heading += (float)(2.0f * Math.PI);     }  } 
end example
 

Just like the method for adjusting the heading, we have the AdjustPitch method (shown in Listing 6 “5) for adjusting the pitch angle up and down. The only difference (other than which attribute is being adjusted) is the range that the angle is kept within. The valid range for a pitch angle is plus or minus 90 degrees, or one-half pi radians.

Listing 6.5: Camera AdjustPitch Method
start example
 public void AdjustPitch(float deltaPitch)  {     m_Attitude.Pitch += (deltaPitch * (float)Math.PI / 180.0f);     if (m_Attitude.Pitch > (0.5f * Math.PI))     {        m_Attitude.Pitch = (float)(0.5f * Math.PI);     }     if (m_Attitude.Pitch < (   0.5f * Math.PI))     {        m_Attitude.Pitch = (float)(   0.5f * Math.PI);     }  } 
end example
 

The last method for manually controlling the camera is the MoveCamera method (shown in Listing 6 “6). The three arguments to this method dictate the amount to adjust the camera position in the forward, vertical, and sideways directions. The amount to move the camera in the world X direction (east) is based on the heading and the forward and sideways movement distances. The movement in the world Z direction (north) works the same way, only at right angles to the heading. We also want to make sure that the camera is not moved below the surface of the terrain. We query the terrain for the current terrain height at the camera s position. If the camera is less than one unit above the terrain, it is raised to be one unit above the ground. Notice that the query of the terrain height is placed in a Try/Catch block. If the camera is moved so that it is not over the terrain, it is possible that the HeightOfTerrain method would throw an exception. If the exception is thrown, we will assume a terrain height of zero.

Listing 6.6: Camera MoveCamera Method
start example
 public void MoveCamera(float x, float y, float z)  {     float ty;     m_X += x * (float)Math.Cos(m_Attitude. Heading) + z *                   (float)Math.Sin(m_Attitude.Heading);     m_Y += y;     m_Z += z * (float)Math.Cos(m_Attitude. Heading) + x *                  (float)Math.Sin(m_Attitude.Heading);     try     {        ty = CGameEngine.Ground.HeightOfTerrain(new Vector3(m_X, m_Y, m_Z));     }     catch {ty=0;}        m_Y = ty + 1.0f;  } 
end example
 

The next two methods relate to associating the camera to one or more objects. The camera may be attached to an object so that it moves along with the object. Normally, the camera is attached to the player-controlled vehicle or character. The Attach method (shown in Listing 6 “7) associates the camera with an object, with an offset from the object s reference position. It is a simple method that just saves the object reference and offset to attribute variables for use by the rendering method.

Listing 6.7: Camera Attach Method
start example
 public void Attach(Object3 D parent, Vector3  offset)  {     m_AttachedObject = parent;     m_Offset = offset;  } 
end example
 

The second object- related method is LookAt (shown in Listing 6 “8). This method saves the reference to the object that is passed to it and into an attribute variable. The rendering method will orient the camera so that it points toward this object. There are two good uses for this feature. The first is for the camera that is attached to an object. Setting the camera to look at the same object as it is attached to works well for a chase camera that is offset behind the object. No matter how the object moves, the camera will always being centered on the object. The second is for cameras that are at fixed points around the path that the objects will follow (like camera towers at a racetrack). By having cameras sprinkled around the course and always switching to the closest camera, we would get an effect similar to television coverage of a race.

Listing 6.8: Camera LookAt Method
start example
 public void LookAt(Object3 D obj)  {     m_LookAtObject = obj;  } 
end example
 

Culling with the Camera

As I mentioned earlier in the chapter, the camera is a vital part of the culling process. It is the camera, after all, that knows whether an object will be in view or not. The camera will actually have two methods dedicated to culling. One will be oriented towards working with objects. The other will just check a position and radius against the frustum. Both methods will be called CheckFrustum , and the difference will be in the arguments supplied to the method (its signature). The method that works with a position and radius (shown in Listing 6 “9) will check to see if any point on the sphere defined by the position and radius lies within the culling frustum.

Listing 6.9: Camera CheckFrustum Method 1
start example
 public CullState CheckFrustum(Vector3pos, float radius)  {     float distance;     int count = 0;      // Dont check against top and bottom.     for(int iPlane = 0; iPlane < 4; iPlane++)     {        distance = planeFrustum[iPlane].Dot(pos);        if(distance <=   radius)        {           return CullState.AllOutside;        }        if (distance > radius) count++;     }     if (count == 4) return CullState.AllInside;     return CullState.PartiallyIn;  } 
end example
 

These methods will only check to see if the spheres are within the horizontal footprint of the frustum. They will not check to see if the points are above or below the frustum. Due to the nature of the game we are developing, it is unlikely that any object will be above or below the frustum. Any objects that are will be rendered, but this is a small price to pay when balanced against a 30 percent increase in culling speed.

To determine if a sphere intersects the frustum, we will take the dot product of the position versus each of the vertical planes of the frustum. The value returned by the dot product is actually the distance from the surface of the plane to that point, with a positive value indicating a position inside the frustum. Therefore, if the distance is negative and greater than the radius away from any of the planes, it must be completely outside of the frustum. If all of the distances are positive and greater than the radius of the sphere, the sphere is completely inside of the frustum. If the sphere is not completely inside or outside, it defaults to being partially inside the frustum.

The version of CheckFrustum that checks objects (shown in Listing 6 “10) works much like the previous version. It gets its position and radius information from the object s position and bounding radius. The fourth plane of the frustum is the near clipping plane. It just so happens that the distance returned by the dot product for the near clipping plane is also the distance that the object is from the camera (minus the near clipping distance).

Listing 6.10: Camera CheckFrustum Method 2
start example
 public CullState CheckFrustum(Object3D obj)  {     float distance = 0.0f;     int count = 0;      // Dont check against top and bottom.     for(int iPlane = 0; iPlane < 4; iPlane++)     {        distance = planeFrustum[iPlane].Dot(obj.Position);        if(distance <=   obj.Radius)        {           return CullState.AllOutside;        }        if  (distance > obj.Radius) count++;     }     if (count == 4) return CullState.AllInside;     return CullState.PartiallyIn;  } 
end example
 

There may be times when we need to get the distance between an object and the camera. One possible scenario in which this would happen is for objects that are attached to another object as children. The parent object will have its range set automatically by the culling routines. The same is not true for the child objects. The GetDistance method (shown in Listing 6 “11) provides the means of getting this distance. This method performs the same dot product operation used in the CheckFrustum method.

Listing 6.11: Camera GetDistance Method
start example
 public float GetDistance(Object3D obj)  {     return (planeFrustum[3].Dot(obj.Position) + m_fNearPlane);  } 
end example
 

Using the Camera

We have covered all of the methods required to put the camera where we need it to be and to extract necessary information from the camera. The one thing that we have not done with the camera is to actually use it in the rendering process. The Render method (shown in Listings 6 “12a through 6 “12d in this section) takes the current state of the camera and builds the matrices required for rendering the scene.

Listing 6.12a: Camera Render Method
start example
 public void Render()  {     Vector3 Up = new Vector3(0.0f, 1.0f, 0.0f);     if (m_AttachedObject != null)     {        if (m_LookAtObject != null)        {           m_LookAt = m_LookAtObject.Position;        }        else        {           m_LookAt = m_AttachedObject.Position;           m_LookAt.X += (float)Math.Sin(m_Attitude.Heading)*10.0f;           m_LookAt.Y += (float)Math.Sin(m_Attitude.Pitch)*10.0f;           m_LookAt.Z += (float)Math.Cos(m_Attitude.Heading)*10.0f;        }        Matrix transpose = Matrix.Identity;        m_Attitude.Heading = Attitude.Aepc(m_AttachedObject.Heading);        m_Attitude. Pitch = m_AttachedObject.Pitch;        m_Attitude.Roll = 0.0f;        transpose.RotateYawPitchRoll(m_Attitude. Heading,           m_Attitude. Pitch, m_Attitude. Roll);        m_Eye = m_AttachedObject.Position +           Vector3.TransformCoordinate(m_Offset, transpose);     }     } 
end example
 
Listing 6.12b: Camera Render Method
start example
 else  {     m_Eye = new Vector3(m_X, m_Y, m_Z);     if (m_LookAtObject != null)     {        m_LookAt = m_LookAtObject.Position;     }     else     {        m_LookAt = m_Eye;        m_LookAt.X += (float)Math.Sin(m_Attitude.Heading)*10.0f;        m_LookAt.Y += (float)Math.Sin(m_Attitude.Pitch)*10.0f;        m_LookAt.Z += (float)Math.Cos(m_Attitude.Heading)*10.0f;     }  } 
end example
 
Listing 6.12c: Camera Render Method
start example
 // Set the app view matrix for normal viewing.   m_matView = Matrix.LookAtLH (m_Eye, m_LookAt, Up);   CGameEngine.Device3D.Transform.View = m_matView;   CGameEngine.Device3D.Transform.Projection = m_matProj; 
end example
 
Listing 6.12d: Camera Render Method (Conclusion)
start example
 Matrix mat = Matrix.Multiply(m_matView, m_matProj);        mat.Invert();        vecFrustum[0] = new Vector3(   1.0f,   1.0f, 0.0f);   // xyz        vecFrustum[1] = new Vector3(1.0f,   1.0f, 0.0f);   // Xyz        vecFrustum[2] = new Vector3(   1.0f,  1.0f, 0.0f);   // xYz        vecFrustum[3] = new Vector3(1.0f,  1.0f, 0.0f);   // XYz        vecFrustum[4] = new Vector3(   1.0f,   1.0f, 1.0f);   // xyZ        vecFrustum[5] = new Vector3(1.0f,   1.0f, 1.0f);   // XyZ        vecFrustum[6] = new Vector3(   1.0f,  1.0f, 1.0f);   // xYZ        vecFrustum[7] = new Vector3(1.0f,  1.0f, 1.0f);   // XYZ        for(int i = 0; i < 8; i++)           vecFrustum[i] = Vector3.TransformCoordinate(vecFrustum[i],mat);        planeFrustum[0] = Plane.FromPoints(vecFrustum[7],                 vecFrustum[3],vecFrustum[5]); // Right        planeFrustum[1] = Plane.FromPoints(vecFrustum[2],                 vecFrustum[6],vecFrustum[4]); // Left        planeFrustum[2] = Plane.FromPoints(vecFrustum[6],                 vecFrustum[7],vecFrustum[5]); // Far        planeFrustum[3] = Plane.FromPoints(vecFrustum[0],                 vecFrustum[1],vecFrustum[2]); // Near        planeFrustum[4] = Plane.FromPoints(vecFrustum[2],                 vecFrustum[3],vecFrustum[6]); // Top        planeFrustum[5] = Plane.FromPoints(vecFrustum[1],                 vecFrustum[0],vecFrustum[4]); // Bottom     }  } 
end example
 

In order to create the two matrices, it is necessary to first create the vectors that are used to generate the view matrix. The Up matrix is easy. It only needs to be a simple vector pointing up in the Y-axis. The Eye and LookAt vectors depend on whether the camera is attached to an object or not. The code for creating the vector when attached to an object is shown in Listing 6 “12a. If an object has been specified as the object to look at, its position is used as the LookAt vector. The object s pitch and heading constitute the camera s attitude. A transpose matrix is created through this attitude for use later in the method. If the camera is not looking at an object, then the vector is set to the camera s position, offset ten units in the camera s current attitude. The Eye vector is set to the object s position with an offset. The offset is transposed to the proper place relative to the attached object using the transpose matrix created earlier.

If the camera is not attached to an object, the vectors are set based on the camera s position and attitude (shown in Listing 6 “12b). The Eye vector is set to the camera s position. If the camera is set to look at an object, then that object s position is used for the LookAt vector. Otherwise, the LookAt vector is set to the Eye vector s position with a ten-unit offset in the direction that the camera is pointing.

Once the vectors are created, it is time to generate the view matrix and to pass the matrices to the device as shown in Listing 6 “12c. The view matrix is created using a static method of the Matrix class called LookAtLH . This method employs the three vectors to create a matrix based on a left-hand coordinate system. This is the most commonly used coordinate system and the one that we will apply throughout this game engine. Once we have created the view matrix, we pass it and the projection matrix to the device using its view and projection transformation properties. The device now has all of the transformation information that it needs from the camera.

Even though we are done using the camera to prepare for rendering, we still have one more thing to do. We need to set up the frustum information that will be used in the culling process (shown in Listing 6 “12d). To create the frustum data, we will need a matrix that is a combination of the view matrix and projection matrix. Multiplying them together combines matrices. The eight corners of the frustum are initialized as a cube just in front of the camera. Transforming each of these points using the combined view/projection matrix creates the transformed corners. The final step in the process is to generate the six planes that form the sides of the frustum using the points at the corners. The Plane class s FromPoints method is used to create each of the planes.

You will see in Chapter 8 that it can be useful to know what objects are currently within the camera s view. The visible object collection held by the camera contains this information. It is populated during the culling process for a given camera. To manage this collection, we will need two methods to clear and populate it with objects. The Reset method (shown in Listing 6 “13) handles the clearing part. It empties the collection by calling the ArrayList Clear method.

Listing 6.13: Camera Reset Method
start example
 public void Reset()  {     m_VisibleObjects.Clear();  } 
end example
 

The population of the collection is done using the AddVisibleObject method shown in Listing 6 “14. It takes a reference to an object as an argument. We are not concerned with pieces of the terrain for this collection. If the object is not a TerrainQuad , it is added to the collection.

Listing 6.14: Camera AddVisibleObject Method
start example
 public void AddVisibleObject(Object3 D obj)  {     if (! (obj is TerrainQuad))     {        m_VisibleObjects.Add(obj);     }  } 
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