Traveling the Rolling Landscape


Now that we have our horizon and sky, it is time to put some ground under our feet. The class that will provide the ground for our game will be the Terrain class. There are many different ways to create the ground surface in a game, ranging from a detailed mesh created in a 3D modeling package at the high end down to a single textured square at the low end. We are going to use a solution that is somewhere in the middle of these two extremes by creating a regular grid mesh. This means that each point in the rectangular mesh will be a constant distance from each of its neighbors.

The easiest way to model the shape of the terrain in a regular grid system like this is to use an image as a height map. Each pixel in the image will represent one of the points in the grid. The blue color channel for each pixel will represent the height at that point. In a normal image where each color is represented by an 8-bit value, this gives us 256 possible values for each height. A vertical scale factor will define how many feet each value represents. A horizontal scale factor will determine how far apart each of the points will be. In Chapter 11, during our investigation of applicable tools, we will look at how this height map is created.

The Building Blocks for the Terrain

Each square area within the grid will be represented by an instance of the TerrainQuad class. Breaking the terrain up in this manner allows us to display only those portions of the terrain that are currently visible in the viewing frustum .

The TerrainQuad class (shown in Listing 4 “10) indicates that the class inherits from the Object3D class to give each quad the basic characteristics of a 3D object. The class also has an array of vertices that will be used in rendering the quad. It also has a validity flag and two vectors (one for each triangle of the quad) for the normal vectors. A property called FaceNormals returns the average of the two triangle normal vectors as the average normal for the quad.

Listing 4.10: TerrainQuad Class Definition
start example
 public class TerrainQuad : Object3D, IDisposable  {      #region Attributes      private CustomVertex.PositionNormalTextured[] m_Corners;      private bool        m_bValid = false;      public Vector3      m_Face1Normal;      public Vector3      m_Face2Normal;      public bool Valid { get { return m_bValid; } }      public Vector3 FaceNormals { get {        Vector3 sum = Vector3.Add(m_Face1Normal, m_Face2Normal);        sum.Normalize();        return sum; } }  #endregion 
end example
 
Note

A shortcut for averaging two vectors is to add them and then normalize ( assuming the original vectors were normalized in the first place).

Each terrain quad is going to be made up of two triangles . While some would argue that it is more efficient to do this with four points and a triangle strip, we will not. Because we will be batching the triangles together, it is actually more efficient to use the larger buffer a triangle list requires than to break the rendering calls up so that there is one call per strip. Therefore the constructor (shown in Listing 4 “11) will define six vertices using the four positions that are passed as arguments. These vertices will use the PositionNormalTextured vertex format. As the name implies, this format holds the three components of the position, the three components of the normal vector, and the two texture coordinates for each vertex. A helper method in the GameMath class ComputeFaceNormal is used to calculate the normal vector for each triangle.

Listing 4.11: TerrainQuad Constructor
start example
 public TerrainQuad(string sName, Vector3 p1, Vector3 p2,     Vector3 p3, Vector3 p4) : base(sName)  {      m_sName = sName;      // Create the vertices for the box.      m_Corners = new CustomVertex.PositionNormalTextured[6];      m_Corners[0].X = p3.X; // nw      m_Corners[0].Y = p3.Y;      m_Corners[0].Z = p3.Z;      m_Corners[0].Tu = 0.0f;      m_Corners[0].Tv = 1.0f;      m_Corners[1].X = p1.X; // sw      m_Corners[1].Y = p1.Y;      m_Corners[1].Z = p1.Z;      m_Corners[1].Tu = 0.0f;      m_Corners[1].Tv = 0.0f;      m_Corners[2].X = p4.X; // ne      m_Corners[2].Y = p4.Y;      m_Corners[2].Z = p4.Z;      m_Corners[2].Tu = 1.0f;      m_Corners[2].Tv = 1.0f;      m_Corners[3].X = p2.X; // ne      m_Corners[3].Y = p2.Y;      m_Corners[3].Z = p2.Z;      m_Corners[3].Tu = 1.0f;      m_Corners[3].Tv = 0.0f;      m_vPosition.X = (p4.X + p3.X) / 2. 0f;      m_vPosition.Y = (p1.Y + p2.Y + p3.Y + p4.Y) / 4. 0f;      m_vPosition.Z = (p1.Z + p3.Z) / 2. 0f;      double dx = p4.X   p3.X;      double dz = p3.Z   p1.Z;      m_fRadius = (float)Math.Sqrt(dx * dx + dz * dz) / 2. 0f;      m_Face1Normal = GameMath.ComputeFaceNormal(new Vector3(m_Corners[0].X,m_Corners[0].Y,m_Corners[0].Z),         new Vector3 (m_Corners[1] .X,m_Corners[1].Y,m_Corners[1].Z),         new Vector3(m_Corners[2].X,m_Corners[2].Y,m_Corners[2].Z));      m_Face2Normal = GameMath.ComputeFaceNormal(new Vector3(m_Corners[1].X,m_Corners[1].Y,m_Corners[1].Z),         new Vector3 (m_Corners [3].X, m_Corners[3].Y, m_Corners[3].Z) ,         new Vector3(m_Corners[2].X,m_Corners[2].Y,m_Corners[2].Z));      // Default the vertex normals to the face normal value.      m_Corners[0].SetNormal(m_Face1Normal);      m_Corners[1].SetNormal(FaceNormals);      m_Corners[2].SetNormal(FaceNormals);      m_Corners[3].SetNormal(m_Face2Normal);      m_Corners[4].SetNormal(FaceNormals);      m_Corners[5].SetNormal(FaceNormals);       m_Corners[4].X = m_Corners[2].X;       m_Corners[4].Y = m_Corners[2].Y;       m_Corners[4].Z = m_Corners[2].Z;       m_Corners[4].Tu = m_Corners[2].Tu;       m_Corners[4].Tv = m_Corners[2].Tv;       m_Corners[5].X = m_Corners[1] .X;       m_Corners[5].Y = m_Corners[1].Y;       m_Corners [5].Z = m_Corners [1].Z;       m_Corners[5].Tu = m_Corners[1].Tu;       m_Corners[5] .Tv = m_Corners[1].Tv;       m_bValid = true;  } 
end example
 

Once all of the quads for the terrain have been created, the Terrain class will go back and compute a new normal for each of the vertices. This normal vector will be the average of all of the triangle normal vectors that include that vertex. This will make the terrain look smooth as it is rendered. If the default normal vectors were used for each vertex, the terrain would tend to look faceted and angular. The SetCornerNormal method shown in Listing 4 “12 provides the interface for the Terrain class to alter a vertex normal. Since this function cannot be sure that vector is properly normalized, we will normalize the vector prior to saving it.

Listing 4.12: TerrainQuad SetCornerNormal Method
start example
 public void SetCornerNormal (int Corner, Vector3 Normal)  {     Normal.Normalize();     m_Corners[Corner].SetNormal(Normal);  } 
end example
 

The Dispose method (shown in Listing 4 “13) is an empty method at this time. I am including it for completeness. Future versions of the TerrainQuad class may require that we dispose of allocated objects. One example might be the use of a specific texture per quad. For now, we will be using one texture common to all quads. For this reason, it is defined and maintained by the Terrain class rather than by each quad.

Listing 4.13: TerrainQuad Dispose Method
start example
 public override void Dispose()  {  } 
end example
 

The RenderQuad method (shown in Listing 4 “14) will not actually render anything to the screen. Instead, it will copy the vertices for this quad into a buffer supplied by the calling method. This is part of the batching process that is used to improve rendering efficiency. The video cards can render the triangles quicker if given several thousand vertices at a time whenever possible. To fill the buffer, this method accepts the buffer (an array of vertices) and the current offset within the buffer. If the quad is valid and has not been culled (i.e., it should be visible), each of the six vertices is copied to the buffer. The offset is incremented by six and returned to the calling method.

Listing 4.14: TerrainQuad RenderQuad Method
start example
 public int RenderQuad(int Offset,            CustomVertex.PositionNormalTextured [] vertices)  {      int newOffset = Offset;      if (Valid && !IsCulled)      {          for (int i = 0; i<6; i++)          {              vertices[Offset+i] = m_Corners[i];          }          newOffset += 6;          Culled = true;      }      return newOffset;  } 
end example
 
Note

Notice that the culled flag is reset to the true state after the data has been copied. In this game engine, all objects are set to the culled state after they have been rendered. The culling software assumes that the objects are not visible. If the object is within the viewing frustum, the culled flag is cleared to false to allow the object to be rendered.

The InRect method (shown in Listing 4 “15) is used when an object is being inserted into the Quadtree. A bounding rectangle is passed in as the argument. The method returns a false if the quad is not within the rectangle and a true if it is. The Rectangle class supplied by Microsoft includes a method called Contains that checks a point against the rectangle. We will loop through the four corners of the quad, checking to see if the rectangle contains that point. If it does, we set the return value to true and break out of the loop. If any point is within the rectangle, we do not need to waste time checking any of the other corners.

Listing 4.15: TerrainQuad InRect Method
start example
 public override bool InRect (Rectangle rect)  {      bool inside = false;      // Check to see if the object is within this rectangle by checking each  corner.      for (int i = 0; i<4; i++)      {          if (rect.Contains((int)m_Corners[i].X, (int)m_Corners[i].Z))          {              inside = true;              break;          }      }      return inside;  } 
end example
 

Assembling the Terrain

The TerrainQuad instances will be used in a two-dimensional array to form our terrain. The Terrain class needs to support more than just the visible effect of having some ground below us. It must also supply us with information about that terrain so that we may interact with it as we move along. The ITerrainInfo interface is used to define the various queries that we will want to make against the terrain. It allows us to know what the altitude is below us so that we do not sink through the surface. The interface also returns the slope of the terrain under us so that we are properly oriented on the surface.

Listing 4 “16 shows the definition of the Terrain class. The class contains a two-dimensional array of vectors that holds the raw data points that make up the terrain. There is also a two-dimensional array of the TerrainQuad instances that define the surface mapped over these points. The class will also remember the dimensions of the arrays using the m_xSize and m_ySize integers. The distance between points in the grid is stored in the variable m_Spacing . The other members of the class are the texture and the array of vertices to be rendered in a given pass.

Listing 4.16: Terrain Class Definition
start example
 public class Terrain : IDisposable, ITerrainInfo  {     private Vector3 [,] m_Elevations;      private VertexBuffer m_VB = null; // Vertex buffer      private TerrainQuad[,] m_Quads = null;      private int m_xSize;      private int m_ySize;      private Texture m_Texture; // Image for face      private bool m_bValid = false;      private CustomVertex.PositionNormalTextured [] m_Vertices;      private float m_Spacing; 
end example
 

The class constructor (shown in Listings 4 “17a through 4 “17e in a bit) has the job of building the terrain structure from the supplied data. The first two arguments are the dimensions of the grid of elevations . The third argument is the fully qualified path name of the image file holding the height map. The dimensions of this image must be equal to or greater than the dimensions specified in the first two arguments. If the image is not big enough, an exception will be thrown and the terrain will be considered invalid. The next argument is the fully qualified path to the image that will be used as the texture on each quad. For the sample game engine we are developing, this will be an image of a sandy surface. The final two arguments are used to scale the data from the height map to calculate the elevation posts. The spacing data specifies how far apart each post is in meters. The elevation factor is the vertical scaling to convert the blue color value to an elevation in meters .

Listing 4.17a: Terrain Class Constructor
start example
 public Terrain(int xSize, int ySize, string sName, string sTexture,     float fSpacing, float fElevFactor)  {      int nTemp;      m_Elevations = new Vector3[xSize,ySize];      m_xSize = xSize   1;      m_ySize = ySize   1;      m_Quads = new TerrainQuad[m_xSize,m_ySize];      m_Vertices = new CustomVertex.PositionNormalTextured [3000];      m_Spacing = fSpacing; 
end example
 
Listing 4.17b: Terrain Class Constructor
start example
 try     {          System.Drawing.Bitmap bmp = new System.Drawing.Bitmap(sName);          for (int i=0; i<xSize; i++)          {          for (int j=0; j<ySize; j++)          {              nTemp = bmp.GetPixel(i,j).ToArgb() & 0000000ff;              m_Elevations[i,j].X = i * fSpacing;              m_Elevations[i,j].Z = j * fSpacing;              m_Elevations[i,j].Y = nTemp * fElevFactor;          }    }    bmp.Dispose();  }  catch (DirectXException d3de)  {    Console.AddLine("Unable to load terrain heightmap " + sName);    Console.AddLine(d3de.ErrorString);  }  catch (Exception e)  {    Console.AddLine("Unable to load terrain heightmap " + sName);    Console.AddLine(e.Message);  } 
end example
 
Listing 4.17c: Terrain Class Constructor
start example
 try   {      for (int i=0; i<m_xSize; i++)      {         for (int j=0; j<m_ySize; j++)          {              string sQuadName = "Quad" + i + "   " + j;              m_Quads[i,j] = new TerrainQuad(sQuadName, m_Elevations[i,j],               m_Elevations[i+1,j], m_Elevations[i,j+1], m_Elevations[i+1,j+1]);              CGameEngine.QuadTree.AddObject((Object3D)m_Quads[i,j]);          }      }      Console.AddLine("Done creating quads");   }   catch (DirectXException d3de)   {       Console.AddLine ("Unable to create quads ");       Console.AddLine(d3de.ErrorString);   }   catch (Exception e)   {       Console.AddLine("Unable to create quads ");       Console.AddLine(e.Message);   } 
end example
 
Listing 4.17d: Terrain Class Constructor
start example
 for (int i=1; i<m_xSize   1; i++)  {     for (int j=1; j<m_ySize   1; j++)     {         // Assign normals to each vertex.         Vector3 Normalsw =            m_Quads[i,j].FaceNormals + m_Quads[i   1,j   1].FaceNormals +            m_Quads[i   1,j].FaceNormals + m_Quads[i,j   1].FaceNormals;         m_Quads[i,j].SetCornerNormal(0, Normalsw);         Vector3 Normalse =               m_Quads[i, j].FaceNormals + m_Quads[i,j   1].FaceNormals +               m_Quads[i+1,j].FaceNormals + m_Quads[i+1,j   1].FaceNormals;          m_Quads[i,j].SetCornerNormal (1, Normalse);         Vector3 Normalnw =           m_Quads[i,j].FaceNormals + m_Quads[i   1,j].FaceNormals +            m_Quads[i-1,j+1].FaceNormals + m_Quads[i,j+1].FaceNormals;            m_Quads[i,j].SetCornerNormal (2, Normalnw);         Vector3 Normalne =            m_Quads[i,j].FaceNormals + m_Quads[i,j+1].FaceNormals +            m_Quads[i+1,j+1].FaceNormals + m_Ouads[i+1,j].FaceNormals;         m_Quads[i,j].SetCornerNormal(3, Normalne);     } } 
end example
 
Listing 4.17e: Terrain Class Constructor
start example
 try        {             m_Texture = GraphicsUtility.CreateTexture(CGameEngine.Device3D,                sTexture);        }        catch        {             Console.Add Line ("Unable to create terrain texture using " + sTexture);        }        try        {             // Create a vertex buffer for rendering the terrain.             m_VB = new VertexBuffer(typeof(CustomVertex.PositionNormalTextured),                 3000, CGameEngine.Device3D, Usage.WriteOnly,                CustomVertex.PositionNormalTextured.Format, Pool.Default);             m_bValid = true;       }        catch        {            Console.AddLine("Unable to create terrain vertex buffer");        }        Console.AddLine("terrain loaded");  } 
end example
 

The constructor begins by allocating the arrays for the elevation data and the quads (Listing 4 “17a). Note that the dimensions for the quad array are one smaller in each dimension. This is because the quads lay between the elevation points. A vertex array is allocated to hold three thousand vertices. This is the size of the vertex batch that we will pass to the 3D device with each DrawPrimitive call.

The next step in the constructor is to extract the elevation information from the height map (Listing 4 “17b). The Bitmap class in the System.Drawing namespace is a handy tool for this. It knows how to read many of the common image formats and includes a method to examine individual pixels within the image. To calculate the elevation data, we will loop through the pixels of the height map using the size information passed into the constructor. The X dimension of each elevation point will start at zero and increment east by the spacing value. The Z dimension will operate the same way but in the northern direction. The Y dimension is the elevation at each point. It is calculated by taking the blue component of the pixel s color and multiplying it by the elevation factor. Once we have operated on all of the desired pixels, we call the bitmap s Dispose method. Any exceptions thrown will be sent to the console for display.

The next portion of the constructor (shown in Listing 4 “17c) creates the TerrainQuads for the terrain. This time we loop through the dimensions of the m_Quads array and create the quad instances to populate the array. The data from the elevations array is passed to the TerrainQuad constructor. Each quad is also added to the Quadtree for culling when we are rendering the terrain.

Now that we have created all of the quads, it is time to do a little housekeeping. When we create the quads, we set the vertex normals to be the same as the face normals for that quad. Now we can go back and calculate the correct normals at each vertex. Listing 4 “17d shows the section of the constructor that accomplishes this. In each quad corner (except those along the outside edge of the terrain), four quads come together at that point. To get the normal vector for that point, we need to average the four face normals to get the correct vertex normal. Remember that the easy way to average normalized vectors is to add the vectors and then renormalize. The SetCornerNormal method takes care of the normalizing of the vector.

The final portion of the constructor (shown in Listing 4 “17e) initializes the texture and vertex buffer used to render the terrain. The texture is created using the utility function CreateTexture . We wrap it in a Try/Catch block in case there is not enough video memory to hold the texture or the texture file is not found. The vertex buffer is created to hold 3000 vertices. If we succeed in creating the vertex buffer, it is safe to set the valid flag to true. As a final step, we post a message to the console that the loading of the terrain has been completed.

There are two methods (shown in Listing 4 “18) in the Terrain class for querying the elevation at a given point on the terrain. The first method is HeightOfTerrain , which accepts a position vector and returns the floating-point elevation in feet. It simply passes the X and Z components of the position to the other method, TerrainHeight . The TerrainHeight method accepts an east and a north position and returns the elevation at that point. This is the method that actually calculates the elevation. The method calculates the indices of the four elevation points that surround the position in question. Once we have the four elevation points, it is simply a matter of doing a four- way interpolation between the points to get the elevation.

Listing 4.18: Terrain Class HeightOfTerrain and TerrainHeight Methods
start example
 public float HeightOfTerrain (Vector3 pos)  {     return TerrainHeight (pos.X, pos.Z);  }  public float TerrainHeight (float east, float north)  {     int x1 = (int)(east/m_Spacing   0.5);     int x2 = (int)(east/m_Spacing + 0.5);     int z1= (int)(north/m_Spacing   0.5);     int z2 = (int)(north/m_Spacing + 0.5);     // Interpolation between the corner elevations     float height;     float dx = (east   x1 * m_Spacing) / m_Spacing;     float dy = (north   z1 * m_Spacing) / m_Spacing;     height = m_Elevations[x1,z1].Y + dx * (m_Elevations[x2,z1].Y   m_Elevations[x1,z1].Y) + dy * (m_Elevations[x1,z2].Y   m_Elevations[x1,z1].Y) + dx * dy * (m_Elevations[x1,z1].Y   m_Elevations[x2,z1].Y   m_Elevations[x1,z2].Y +        m_Elevations[x2,z2].Y);     return height;  } 
end example
 

The HeightAboveTerrain method (shown in Listing 4 “19) proves useful in a flight simulation. The height above the terrain is simply the difference between the height of the supplied point and the elevation of the terrain beneath that point. As this method accepts a position vector as an argument, it simply uses that same vector as the argument for the HeightOfTerrain method.

Listing 4.19: Terrain Class HeightAboveTerrain Method
start example
 public float HeightAboveTerrain (Vector3 Position)  {     return HeightOfTerrain (Position)   Position.Y;  } 
end example
 

Another important query that would be made against the terrain is a line of sight (LOS) check. This query traverses a line connecting two points to see if the line passes through the terrain at any point. There are several ways to address the problem. If it were vital that this check be extremely accurate, we would need to do a line polygon intersection test with every polygon along the path from one point to the other. This would be computationally expensive. We will take a slightly lower fidelity approach to the problem. We will step a test point along the line connecting the two points in question. If the terrain height at that point is below the interpolated point s height, we will assume that we still have line of sight. The first tested point that is below the terrain surface will determine that the line of sight has been broken.

Note

There is a small chance that this method will return a false positive if a ridge occurs in the terrain between two of the test points. Decreasing the increment step will decrease the chance of this happening (with a corresponding increase in processing time).

Listing 4 “20 shows the code for the InLineOfSight method. The method begins by assuming that we do have line of sight between the two points. The slope of the line between the two points is then calculated in both the horizontal and vertical axes. The slope will be used to move the test point between the two points being tested. The increment on the east/west axis is calculated at 75 percent of the spacing between elevation data points. This is to ensure that each quad in the terrain grid is checked with at least one test point.

Listing 4.20: Terrain Class InLineOfSight Method
start example
 public bool InLineOfSight(Vector3 Position1, Vector3 Position2)  {     bool los = true;     float north;     float dx = Position2.X   Position1.X;     float dy = Position2.Y   Position1.Y;     float dz = Position2.Z   Position1.Z;     float dp = dz / dx;     float dist = (float)Math.Sqrt(dx*dx + dz*dz);     float de = dy / dist;     float IncX = m_Spacing * 0.75f;     float y = Position1.Y;     float east = Position1.X;     while (east < Position2.X && los)     {        north = Position1.Z + (east - Position1.X) * dp;        los = TerrainHeight(east, north) <= y;        east += IncX;        y += (IncX*dp) * de;     }     return los;  } 
end example
 

The next terrain information query that we will look at is the GetSlope method (shown in Listing 4 “21). This method takes advantage of the fact that the slope of a terrain polygon is equal to the difference between the face normal vector of the polygon and a vector in the vertical direction. The arguments to the method consist of the position in question and a heading in radians. The heading is important since we need to rotate the vector so that the pitch and roll that is calculated corresponds to the heading. The method begins by creating an attitude variable to hold the results and a matrix to rotate the vector. The quad in question is calculated from the X and Z components of the supplied position. Subtracting a vertical vector from the normal vector and transforming it by the rotation matrix calculates the slope vector. The pitch and roll components of the attitude are calculated using standard trigonometric formulas. The X and Z components of the slope vector are checked against zero to prevent divide-by-zero errors.

Listing 4.21: Terrain Class GetSlope Method
start example
 public Attitude GetSlope(Vector3 Position, float Heading)  {     Attitude attitude = new Attitude();     Matrix matrix = Matrix.Identity;     matrix.RotateY(Heading);     int x1 = (int)(Position.X/m_Spacing);     int z1 = (int)(Position.Z/m_Spacing);     Vector3 normal = m_Quads[x1,z1].FaceNormals;     normal.TransformCoordinate(matrix);     if (normal.Z == 0.0f)     {        attitude.Pitch = 0.0f;     }     else     {        attitude.Pitch = -(float)Math.Atan(normal.Y/normal.Z);        if (attitude.Pitch > 0.0)        {           attitude.Pitch = (float)(Math.PI/2.0) - attitude.Pitch;        }        else        {           attitude.Pitch = -((float)(Math.PI/2.0) + attitude.Pitch);        }     }     if (attitude.Pitch > (Math.PI/4.0)  attitude.Pitch <   (Math.PI/4.0))     {        Console.AddLine("Pitch " + attitude.Pitch*180.0/Math.PI + " " +           normal.ToString());     }     if (normal.X == 0.0f)     {        attitude.Roll = 0.0f;     }     else     {        attitude.Roll =   (float)Math.Atan(normal.Y/normal.X);        if (attitude.Roll > 0.0)        {           attitude.Roll = (float)(Math.PI/2.0)   attitude.Roll;        }        else        {           attitude.Roll =   ((float)(Math.PI/2.0) + attitude.Roll);        }     }     if (attitude.Roll > (Math.PI/4.0)   attitude.Roll <   (Math.PI/4.0))     {        Console.AddLine("Roll " + attitude.Roll*180.0/Math.PI+" " +           normal.ToString());     }         attitude.Heading = Heading;     return attitude;  } 
end example
 

The drawing of the terrain is done in the Render method (shown in Listing 4 “22). This is where we will take any of the TerrainQuads that are visible and render them to the screen. The very first thing to do, of course, is to check whether the terrain is valid. If an error occurred while creating the terrain, we might not have anything valid to render. Checking the validity flag allows us to bypass all of the rendering code if there is a problem. If everything is OK, we ensure that the culling mode is correct and let the device know the vertex format that we are using for the terrain triangles that we are about to send. We need to set up a material for inclusion in the rendering device context. If a material is not set, the lighting effects we will look at in Chapter 7 will not work properly. Materials are required for the lighting calculations in the fixed-function pipeline. Since we are using a single texture for all terrain quads, we will set the texture into the first texture position.

Listing 4.22: Terrain Class Render Method
start example
 public void Render(Camera cam)  {      int nQuadsDrawn = 0;      if  (m_bValid)      {           CGameEngine.GetDevice().RenderState.CullMode = Cull.Clockwise;           CGameEngine.Device3D.VertexFormat =               CustomVertex.PositionNormalTextured.Format;           Material mtrl = new Material();           mtrl.Ambient = Color.White;           mtrl.Diffuse = Color.White;           CGameEngine.Device3D.Material = mtrl;           // Set the texture.           CGameEngine.GetDevice().SetTexture(0, m_Texture);           // Set the matrix for normal viewing.           Matrix matWorld = new Matrix();           MatWorld = Matrix.Identity;           CGameEngine.GetDevice().Transform.World = matWorld;           CGameEngine.GetDevice().Transform.View = cam.View;            int Offset = 0;           for (int i=0; i<m_xSize; i++)           {               for (int j=0; j<m_ySize; j++)               {                    try                    {                   Offset = m_Quads[i,j].RenderQuad(Offset, m_Vertices);                   if (Offset >= 2990)                   {                       CGameEngine.Device3D.VertexFormat =                          CustomVertex.PositionNormalTextured.Format;                       m_VB.SetData(m_Vertices, 0, 0);                       CGameEngine.Device3D.SetStreamSource(0, m_VB, 0);                       CGameEngine. Device3D.DrawPrimitives(PrimitiveType.TriangleList, 0, Offset/3);                       nQuadsDrawn += Offset / 6;                       Offset = 0;                   }                   catch                   {                      Console.AddLint("Error rendering quad "+I+","+j);                   }               }             }         }         if (Offset > 0)          {              try              {                     CGameEngine.Device3D.VertexFormat =                         CustomVertex.PositionNormalTextured.Format;                     m_VB.SetData(m_Vertices, 0, 0);                     CGameEngine.GetDevice().SetStreamSource(0, m_VB, 0                     CGameEngine.GetDevice().DrawPrimitives(PrimitiveType.TriangleList, 0, Offset/3);                     nQuadsDrawn += Offset / 6;                     Offset = 0;              }              catch (DirectXException d3de)              {                  Console.AddLine("Unable to render terrain ");                  Console.AddLine(d3de.ErrorString);              }              catch (Exception e)              {                  Console.AddLine("Unable to render terrain");                  Console.AddLine(e.Message);              }          }      }  } 
end example
 
Note

The rendering code appears twice. The second set of lines are required since it is quite unlikely that we will fill the array a fixed number of times with no vertices left over.

The next thing to do is set up the transformation matrices. Since all of the positions are already in world coordinates, we will set the world matrix to the identity state (since no change needs to occur in the coordinates). The view matrix comes from the active camera. For efficiency, we will accumulate the terrain data into a vertex array. The Direct3D device is at its best when given large numbers of vertices at a time. We loop through all of the terrain quads and have their Render methods add data to the vertex array if the quad has not been culled from the scene. When the array is full or we run out of quads, the vertex data is transferred to the vertex buffer and the vertices are drawn with a call to DrawPrimitives . The final method in the Terrain class is Dispose (shown in Listing 4 “23). This method disposes of each of the terrain quads as well as the texture and the vertex buffer.

Listing 4.23: Terrain Class Dispose Method
start example
 public void Dispose()  {      for (int i=0; i<m_xSize; i++)      {          for (int j=0; j<m_ySize; j++)          {              if (m_Quads[i,j] != null) m_Quads[i,j].Dispose();         }      }      if (m_Texture != null) m_Texture.Dispose();      if (m_VB != null) m_VB.Dispose();  } 
end example
 

Now that we have added terrain to the scene, it is starting to look better. Figure 4 “3 shows the terrain and skybox . We currently have a scene of rolling sand dunes with mountains off in the distance. This is better, but still a bit empty. The next section of this chapter will help us dress up the scene a bit by adding more objects to the scene.

click to expand
Figure 4 “3: Skybox and terrain



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