Developing the Model Class


The Mesh class provides a fine, low-level means of rendering a complex object in a three-dimensional scene. A game engine needs to look at the problem from a somewhat higher level. Regarding the game engine, we want a simple interface to position and render the desired mesh. In order to avoid a naming collision with the Microsoft classes, we will call our class Model .

Defining the Model Attributes

Our Model class has a number of requirements. The first requirement is that a model must inherit from our Object3D class. The model is just a more complicated three-dimensional object. This allows the game engine to hold a collection of objects without caring whether a given object is a simple billboard with two triangles or a model with hundreds or even thousands of triangles . The Object3D class is our common base class. The next requirement is that the class must support the IDisposable interface. Since the underlying mesh will be allocating memory on the video card, it is mandatory that we implement the Dispose method so that this memory can be freed as soon as it is no longer needed. The final requirement is that the class must implement the IDynamic interface. The models within our game will not be static objects like the billboards are. It is likely that some or all of the models will be capable of moving through the scene. The player s vehicle (typically referred to as ownship ) is a primary example. The IDynamic interface defines the Update method that is called by the game engine in order to update the state of the object. Listing 5 “2 defines the attributes that make up the Model class.

Listing 5.2: Model Class Declaration
start example
 public class Model : Object3D, IDisposable, IDynamic  {     #region Attributes     private Mesh m_mesh = null; // Our mesh object in sysmem     private Material[] m_meshMaterials; // Materials for our mesh     private Texture[] m_meshTextures; // Textures for our mesh     private Vector3 m_vOffset = new Vector3(0.0f, 0.0f, 0.0f);     private Attitude m_AttitudeOffset = new Attitude();     private ProgressiveMesh[] m_pMeshes = null;     private int m_currentPmesh = 0;     private int m_nNumLOD = 1;     private float[] m_LODRanges = null;     private float m_fMaxLODRange = 1.0f;     private GraphicsStream m_adj = null;     private Vector3 m_PositiveExtents = new Vector3(   1.0f,   1.0f,   1.0f);     private Vector3 m_NegativeExtents = new Vector3(1.0f,1.0f,1.0f);     private Vector3[] m_Corners = new Vector3[8];     public Vector3 Offset { get { return m_vOffset; } }    #endregion 
end example
 

The first four attributes of the class build the basic mesh for the model. These represent the mesh and the textures and materials needed to render the mesh. The m_adj attribute is the adjacency information. This information is held in a GraphicsStream and is used by some of the utility methods of the Mesh class. The next five attributes will be used in the construction of the progressive meshes that form the level-of-detail portion of the model. There is an array of progressive mesh instances and a corresponding array of ranges. The ranges help in determining which of the progressive meshes should be rendered based on the current range between the object and the camera.

The next two attributes adjust the position of the mesh compared with that of the model. Ideally, a mesh would be built such that the reference position would be centered within the object at ground level. The person creating the mesh may decide to place the reference position elsewhere. The solution is to include an offset in both position and attitude that can be applied to the model. With these two attributes, it does not matter where the reference position is or what the basic orientation of the model was when built. When we create a model, we can specify these offsets to get the positioning and orientation we need.

The final three attributes are used in collision detection for the model. In order to be as efficient as possible, collision checking is done in an increasing amount of accuracy. The Object3D class already has a bounding radius for the first and quickest type of collision testing. For the Model class, we will include the next most accurate form of collision checking. This is the object-aligned bounding box described early in the chapter. To create this box, we will need to know the size of the smallest box that completely encloses the object. From these extents we can later calculate the corners of the box based on the position and attitude of the object.

Constructing a Model

We have established the attributes that make up the class. Now it is time to look at how we build an instance of the class in the constructor. The code for the constructor is shown in Listings 5 “3a through 5 “3e throughout this section. The beginning of the constructor is shown in Listing 5 “3a. The constructor requires four arguments. The first argument is the name of the model. This string is passed down into the Object3D base class. The second argument is the path to the mesh data file that will be loaded into the model. The last two arguments are the positional and attitude adjustments that are to be applied to the model.

Listing 5.3a: Model Class Constructor
start example
 public Model(string name, string meshFile, Vector3 offset, Attitude adjust)   : base(name)  {     Mesh pTempMesh = null;     WeldEpsilons Epsilons = new WeldEpsilons();     Vector3 objectCenter;         // Center of bounding sphere of object     m_vOffset = offset;     m_AttitudeOffset = adjust;     m_vPosition.X = 100.0f;     m_vPosition.Z = 100.0f;     ExtendedMaterial[] materials = null; 
end example
 
Listing 5.3b: Model Class Constructor
start example
 try  {     // Load the m_mesh from the specified file.     m_mesh = Mesh.FromFile(meshFile, MeshFlags.SystemMemory,                        CGameEngine.Device3D, out m_adj, out materials);     // Lock the vertex buffer to generate a simple bounding sphere.     VertexBuffer vb = m_mesh.VertexBuffer;     GraphicsStream vertexData = vb.Lock(0, 0, LockFlags.NoSystemLock);     m_fRadius = Geometry.ComputeBoundingSphere(vertexData,                         m_mesh.NumberVertices, m_mesh.VertexFormat,                         out objectCenter);     Geometry.ComputeBoundingBox(vertexData, m_mesh.NumberVertices,                         m_mesh.VertexFormat, out m_NegativeExtents,                         out m_PositiveExtents);      vb.Unlock();      vb.Dispose(); 
end example
 
Listing 5.3c: Model Class Constructor
start example
 m_vOffset.Y =   m_NegativeExtents.Y;  m_Corners[0].X = m_NegativeExtents.X;  m_Corners[0].Y = m_NegativeExtents.Y + m_vOffset.Y;  m_Corners[0].Z = m_NegativeExtents.Z;  m_Corners[1].X = m_PositiveExtents.X;  m_Corners[1].Y = m_NegativeExtents.Y + m_vOffset.Y;  m_Corners[1].Z = m_NegativeExtents.Z;  m_Corners[2].X = m_NegativeExtents.X;  m_Corners[2].Y = m_PositiveExtents.Y + m_vOffset.Y;  m_Corners[2].Z = m_NegativeExtents.Z;  m_Corners[3].X = m_PositiveExtents.X;  m_Corners[3].Y = m_PositiveExtents.Y + m_vOffset.Y;  m_Corners[3].Z = m_NegativeExtents.Z;  m_Corners[4].X = m_NegativeExtents.X;  m_Corners[4].Y = m_NegativeExtents.Y + m_vOffset.Y;  m_Corners[4].Z = m_PositiveExtents.Z;  m_Corners[5].X = m_PositiveExtents.X;  m_Corners[5].Y = m_NegativeExtents.Y + m_vOffset.Y;  m_Corners[5].Z = m_PositiveExtents.Z;  m_Corners[6].X = m_PositiveExtents.X;  m_Corners[6].Y = m_PositiveExtents.Y + m_vOffset.Y;  m_Corners[6].Z = m_PositiveExtents.Z;  m_Corners[7].X = m_PositiveExtents.X;  m_Corners[7].Y = m_PositiveExtents.Y + m_vOffset.Y;  m_Corners[7].Z = m_PositiveExtents.Z; 
end example
 
Listing 5.3d: Model Class Constructor (Conclusion)
start example
 // Perform simple cleansing operations on m_mesh.     pTempMesh = Mesh. Clean (m_mesh, m_adj, m_adj);     m_mesh.Dispose();     m_mesh = pTempMesh;     // Perform a weld to try and remove excess vertices.     // Weld the mesh using all epsilons of 0.0f.     // A small epsilon like 1e-6 works well too.     m_mesh.WeldVertices(0, Epsilons, m_adj, m_adj);     // Verify validity of mesh for simplification.     m_mesh.Validate(m_adj);     CreateLod();  }  catch (DirectXException d3de)  {     Console.AddLine("Unable to load mesh " + meshFile);     Console.Add Line(d3de.ErrorString);  }  catch (Exception e)  {     Console.AddLine("Unable to load mesh " + meshFile);     Console.AddLine(e.Message);  } 
end example
 
Listing 5.3e: Model Class Constructor
start example
 if (m_meshTextures == null && materials != null)  {     // We need to extract the material properties and texture names.     m_meshTextures  = new Texture[materials.Length];     m_meshMaterials = new Material[materials.Length];     for(int i=0; i<materials.Length; i++)     {        m_meshMaterials[i] = materials[i].Material3D;        // Set the ambient color for the material (D3DX does not do this).        m_meshMaterials[i].Ambient = m_meshMaterials[i].Diffuse;        // Create the texture.        try        {            if (materials[i].TextureFilename != null)            {               m_meshTextures[i] =                         TextureLoader.FromFile(CGameEngine.Device3D,                         materials[i].TextureFilename);            }        }        catch (DirectXException d3de)        {           Console.AddLine("Unable to load texture " +                                           materials[i].TextureFilename);           Console.AddLine(d3de.ErrorString);        }        catch (Exception e)        {            Console.AddLine("Unable to load texture " +                                            materials[i].TextureFilename);            Console.AddLine(e.Message);        }      }    }  } 
end example
 

The first section of the constructor defines some internal variables for later use in the constructor and saves the offset data into the attribute variables . The epsilon values are used in the vertex welding operation. They define the error tolerance for determining if two vertices are in the same position.

The position of the model is set to one hundred units in both the X and Z dimensions. This ensures that the model will default to a position within the terrain. The game application would set the model to its proper initial position after the model has been created. The ExtendedMaterial array is passed back from the method that loads the mesh from the data file.

The next section of the constructor (shown in Listing 5 “3b) covers the loading of the mesh and the computation of the bounding radius around the object. The Mesh class provided by Microsoft has a FromFile method for loading meshes stored in the DirectX format. This method also takes a fully qualified path and filename as a parameter. We will use the version of the method that returns both the adjacency information as well as the material definitions for the mesh. A more advanced version of this class would also include support for effects. We will load the mesh into system memory. If we were going to render the model using this mesh, we would use the Managed flag rather than the SystemMemory flag so that the mesh would be placed in video memory if possible. Since we wish to support levels of detail in our rendering, we will use the basic mesh in system memory to create the progressive meshes in managed (usually video) memory.

Once the mesh has been loaded, our next step is to compute the bounding radius for the model. To do this, we need to access the vertex data that has been loaded in the model s vertex buffer. We obtain a reference to the vertex buffer from the mesh. Once we have a reference to the vertex buffer, we need to lock the buffer to gain access to its data. This form of the Lock method returns a GraphicsStream that we will use next. The arguments of the Lock method specify that we want the entire vertex buffer and we will allow system background processing to continue while we have the buffer locked.

With the buffer locked we can use the ComputeBoundingSphere method of the Geometry class to calculate the radius from the vertex data. It takes as arguments the GraphicsStream from the vertex buffer as well as the number of vertices and the vertex format. The latter two can be obtained from the Mesh class. The final argument is a vector that is also returned from the method that will be filled with the position of the center of the object. We will be assuming for the purposes of this simple game engine that the position of the model with the specified offsets is the center of the object. A second method of the Geometry class provides us with our bounding box data. ComputeBoundingBox accepts the same first three arguments as the previous method. The last two arguments are the minimum and maximum extents of the mesh. Notice in both of these methods that the arguments that pass information back to us are prefixed with the out keyword. This signals to the compiler that these arguments must be passed as references so that they may be modified by the method and return information back to the calling method. Without the out prefix, the argument would not be modified by the method. Once we are done with the vertex buffer, we must remember to call the Unlock and the Dispose methods to release the vertex buffer.

The next section of the constructor (shown in Listing 5 “3c) calculates the corners of the bounding box based on the extents obtained from the mesh. The model offset in the Y-axis vertical axis) is set to the inverse of the negative Y extent. This ensures that the bottom of the model will rest against the ground when the model has an altitude that corresponds with the height of the ground at its location.

The operations to this point in the constructor have been based on the mesh as it was loaded from the data file. The next section of the constructor (shown in Listing 5 “3d) will prepare the mesh for our model so that we can render it with maximum efficiency. The first step is to create a clean version of the mesh. The Clean method of the Mesh class prepares the mesh for simplification. It does this by adding a vertex wherever two fans of triangles originally shared a vertex. The progressive mesh simplification routines are not as effective if we do not perform this operation first. The first argument of the Clean method is the instance of a mesh that we wish to clean. The second and third arguments are GraphicsStream instances. One is the adjacency stream for the original mesh, and the second is the new stream for the cleaned mesh. The method returns a new mesh created by the method.

Now that we have a new, improved, clean mesh, we no longer need the original mesh. We can dispose of the original mesh now and replace its reference with that of the cleaned mesh. The cleaning process adds vertices that will be required when we create simplified versions of the mesh. The WeldVertices method will remove vertices that are exact duplicates of another vertex. When verifying whether a vertex is duplicated , this method checks more than just the position of the vertex. It also checks the normal, color, and texture coordinate values associated with the vertex. The Epsilons structure defines the tolerances used in comparing each of these values. Since we do not specify any initial values for the structure when it is created, all of its values will be zero. This states that all values must match exactly for a weld to occur.

The first argument for the WeldVertices method is a set of flags that can tailor the behavior of the operation. For a normal operation, a zero works just fine. The second argument is the epsilon structure holding the comparison tolerances. The last two arguments are the original and resulting adjacency streams, just like in the Clean method. Once any duplicated vertices are welded, we are about ready to create the progressive meshes. The last thing required prior to creating the progressive meshes is to validate the mesh. The Validate method verifies that the mesh is properly configured and returns a GraphicsStream that may be used to perform the creation of the progressive meshes. The actual creation of the new meshes is accomplished in the CreateLod method of the Model class. By placing this functionality in a separate method, we allow the user of the game engine to specify how the levels of detail are configured. The default setting for this operation in the constructor is the creation of a single level of detail that would be used for all ranges.

The last portion of the constructor (shown in Listing 5 “3e) manages the materials for the model. The material and texture information for the model is returned from the FromFile method in the ExtendedMaterial array called materials . We will separate this information into two arrays, one array for the material information and one array for the textures. The first step is to allocate the arrays based on the number of entries in the material array. The material data is copied from the Material3D member of the ExtendedMaterial array. One thing that Microsoft does not do is set the ambient color as part of the data. We will set the ambient color to be the same as the diffuse color. Loading the textures for the model is a bit more involved.

The ExtendedMaterial structure does not hold the texture. It holds only the name of the file that holds the texture. Before trying to load the texture, we need to ensure that the filename is set. It is quite possible for part of a model to have material values without having a texture. If the filename is null, we will skip the loading of the texture. This won t cause any problems when rendering, since DirectX understands that a null texture means that there is no texture to apply to the triangles. The TextureLoader class supplies the FromFile method needed to create a texture from the specified file.

When an instance of the Model class is created, the constructor builds only one level of detail. The game engine should permit the user to specify how many levels to be used for each model. It should also be able to specify the maximum range for the levels of detail. Each level will switch as its range is reached. The individual ranges will be an equal fraction of the maximum range. For example, if the maximum range is 500 feet and ten levels are specified, the levels will switch every 50 feet. The public interface that allows the user to specify the level-of-detail settings is called SetLOD (shown in Listing 5 “4).

Listing 5.4: Model Class SetLOD Method
start example
 public void SetLOD(int numLOD, float MaxRange)  {     if (numLOD < 1) numLOD = 1;     m_nNumLOD = numLOD;     m_fMaxLODRange = MaxRange;     m_LODRanges = new float [numLOD];     float rangeDelta = MaxRange / numLOD;     for (int i=0; i < numLOD; i++)     {        m_LODRanges[i] = rangeDelta * (i+1);     }     CreateLod();  } 
end example
 

The SetLOD method will require two arguments: the number of levels and the maximum range. We must ensure that we do not accept bad inputs. No matter what the user of the game engine specifies, we need to have at least one level of detail. The first line of the method checks to see if the number of levels is at least one. If not, it sets it to a value of one. The number of levels and the maximum range is saved within the class for later reference. A range delta is calculated by dividing the maximum range by the number of levels, which gives us our change of levels at a regular interval. The range array is created large enough to hold range values corresponding with each detail level. The array is then filled with these range values. The final step is to call the CreateLod method. This method was originally called by the constructor to create a single level of detail. Calling the method now will delete the original level-of-detail model and create the requested number of levels.

I have referred to the CreateLod method a couple of times now. The method is shown in Listing 5 “5. It begins by creating a temporary ProgressiveMesh from the basic mesh that will be used to create the lower resolution versions for the other levels of detail. The SimplifyVertex flag is specified to instruct the ProgressiveMesh constructor to simplify the mesh s vertex structure if possible. We want to start this process with the simplest mesh structure we can get. The temporary mesh provides us with the range of vertices that can be used with the mesh. The maximum value is, of course, the original number of vertices. An enhanced mesh allows us to increase the number of vertices in a mesh, but a progressive mesh only allows us to reduce the number. The constructor also determines the minimum number of vertices that may be kept in the mesh. This is largely based on the triangulation of the mesh and the number of objects within the mesh.

Listing 5.5: Model Class CreateLod Method
start example
 private void CreateLod()  {     ProgressiveMesh pPMesh = null;     int cVerticesMin = 0;     int cVerticesMax = 0;     int cVerticesPerMesh = 0;     pPMesh = new ProgressiveMesh(m_mesh, m_adj, null, 1,                       MeshFlags.SimplifyVertex);     cVerticesMin = pPMesh.MinVertices;     cVerticesMax = pPMesh.MaxVertices;     if (m_pMeshes != null)     {        for (int iPMesh = 0; iPMesh < m_pMeshes.Length; iPMesh++)        {             m_pMeshes[iPMesh].Dispose();        }     }     cVerticesPerMesh = (cVerticesMax   cVerticesMin) / m_nNumLOD;     m_pMeshes = new ProgressiveMesh[m_nNumLOD];     // Clone all the separate m_pMeshes.     for (int iPMesh = 0; iPMesh < m_pMeshes. Length; iPMesh++)     {        m_pMeshes[m_pMeshes.Length   1   iPMesh] =                  pPMesh.Clone(MeshFlags.Managed  MeshFlags.VbShare,                  pPMesh.VertexFormat, CGameEngine.Device3D);        // Trim to appropriate space.        if (m_nNumLOD > 1)        {           m_pMeshes[m_pMeshes.Length   1   iPMesh].TrimByVertices (cVerticesMin +                 cVerticesPerMesh * iPMesh, cVerticesMin +                cVerticesPerMesh * (iPMesh+1));        }        m_pMeshes[m_pMeshes.Length   1   iPMesh]                .OptimizeBaseLevelOfDetail(MeshFlags.OptimizeVertexCache);     }     m_currentPmesh = 0;     m_pMeshes[m_currentPmesh].NumberVertices = cVerticesMax;     pPMesh.Dispose();  } 
end example
 

If the array of level-of-detail meshes currently exists, we must dispose of those meshes before we replace them. We have created meshes that may likely be stored within video memory. We lose efficiency if we leave them there after we replace them.

Creating the level-of-detail meshes is a three-step process. We begin by cloning the temporary mesh into the level-of-detail mesh array slot we are currently building. The two flags included in the cloning process ( Managed and VbShare ) specify that the mesh will be in managed memory (in video memory if possible) and that the vertex buffer will be shared with the original. All of our level-of-detail meshes will share the same vertex buffer. In this way, the vertex buffer only needs to exist in video memory once. What will differ between meshes will be the index buffers that specify which vertices are used.

At this point, we have an exact duplicate of the original mesh. If this is not the first (highest) level of detail, there are two more steps to take. The first step is to reduce the number of vertices in the mesh appropriately for the current level that we are building. The TrimByVertices method does this for us. The number of vertices is based on the current level and the difference between the minimum and maximum number of vertices divided by the number of levels. The final step is to call OptimizeBaseLevelOfDetail . This method optimizes the index buffer so that the faces and vertices are processed in the most efficient order. Once we have created all of our level-of-detail meshes, we can dispose of the temporary mesh. We don t need to worry about losing that shared vertex buffer, since the other meshes are still referencing it.

Implementing Required Model Methods

Now that we have established how to create a model, it is time to add methods to the class in order to manipulate the model. The following methods provide the means to control the model.

Checking If a Model Is Within an Area

Remember that every Object3D -based class has an InRect method, which determines whether an object is within the supplied rectangle. The InRect method for the Model class is shown in Listing 5 “6. This method will use a simple algorithm to make this determination. We will check to see if the distance between the center of the rectangle and the center of the model is less than the sum of the bounding radii of the rectangle and the model. One assumption that this method makes is that the rectangle will always be a square. The normal calculations for distance use the Pythagorean theorem of the square root of the sum of the squares. Unfortunately, the square root operation is one of the most computationally expensive. Luckily, we do not actually need to know the distance. We only need to know if one distance is less than another. Because of this, we can compare the squares of the distances and achieve the same result ”just one more way in which we can buy a bit more speed and efficiency to keep our rendering rate as high as possible.

Listing 5.6: Model Class InRect Method
start example
 public override bool InRect(Rectangle rect)  {     // Check to see if the bounding circle around the model     // intersects this rectangle.     int center_x = (rect.Left + rect.Right)/2;     int center_z = (rect.Top + rect.Bottom)/2;     int delta_x = center_x   (int)m_vPosition.X;     int delta_z = center_z   (int)m_vPosition.Z;     int distance_squared = delta_x * delta_x + delta_z * delta_z;     int combined_radius = (int)(m_fRadius * m_fRadius) +                                                     (rect.Width*rect.Width);     bool bInside = distance_squared < combined_radius;     return bInside;  } 
end example
 

Testing for Collisions

The next couple of methods concern collision checking for the models. Collision tests for models is a bit more involved. Up until now, our collision tests have been strictly based on bounding radius intersections. Now we will be working with object-oriented bounding boxes to get a higher quality collision test. In order to do this, we will need to be able to query the bounding box corners of one model from the Collide method of another model. The method used to expose the corner is the GetCorner method (shown in Listing 5 “7).

Listing 5.7: Model Class GetCorner Method
start example
 Vector3 GetCorner(int index)  {     Vector3 WorldCorner =               Vector3.TransformCoordinate(m_Corners[index],m_Matrix);     return WorldCorner;  } 
end example
 

For the corner to be of value to us, it needs to be in the world coordinate system. The class can do this quickly and easily using the TransformCoordinate method of the Vector3 class. By passing the object-relative position of the corner and the model s matrix to this method, we get back the world-relative position of the corner. You will see this method put to use in the Collide method.

The Collide method (shown in Listings 5 “8a through 5 “8c in this section) is another of the methods required of an Object3D -based class. It returns true if this model is in a collision situation with the supplied object. This method is more involved than previous Collide methods mentioned earlier. We will start out the same as the other simpler methods by beginning with a bounding radius check (see Listing 5 “8a). If the bounding radii do not intersect, then it is not possible that there would be any other type of collision.

Listing 5.8a: Model Class Collide Method
start example
 public override bool Collide(Object3D Other)  {     Plane[] planeCollide;    // Planes of the collide box     Vector3[] WorldCorners = new Vector3[8];     // Perform bounding sphere collision test.     float delta_north = Other.North   North;     float delta_east = Other.East   East;     float distance_squared = delta_north * delta_north +        delta_east * delta_east;     float combined_radius = (Radius * Radius)+(Other.Radius * Other.Radius);     bool bCollide = distance_squared < combined_radius; 
end example
 
Listing 5.8b: Model Class Collide Method
start example
 // If the bounding spheres are in contact, perform a more  // precise collision test,  if (bCollide)  {     planeCollide = new Plane[6];     for(int i = 0; i < 8; i++)        WorldCorners[i] =                Vector3.TransformCoordinate(m_Corners[i],m_Matrix);     planeCollide[0] = Plane.FromPoints(WorldCorners[7],WorldCorners[3],                                     WorldCorners[5]); // Right     planeCollide[1] = Plane.FromPoints(WorldCorners[2],WorldCorners[6],                                   WorldCorners[4]); // Left     planeCollide[2] = Plane.FromPoints(WorldCorners[6],WorldCorners[7],                                     WorldCorners[5]); // Far     planeCollide[3] = Plane.FromPoints(WorldCorners[0],WorldCorners[1],                                   WorldCorners[2]); // Near     planeCollide[4] = Plane.FromPoints(WorldCorners[2],WorldCorners[3],                                   WorldCorners[6]); // Top     planeCollide[5] = Plane.FromPoints(WorldCorners[1],WorldCorners[0],                                   WorldCorners[4]); // Bottom 
end example
 
Listing 5.8c: Model Class Collide Method (Conclusion)
start example
 if (Other.GetType() == typeof(Model))  {     for(int i = 0; i < 8; i++)     {        float distance;        Vector3 testPoint = ((Model)Other).GetCorner(i);        for(int iPlane = 0; iPlane < 6; iPlane++)        {           distance = planeCollide[iPlane].Dot(testPoint);           if (distance > 0.0f) bCollide = true;        }     }  }  else  {     float distance;     Vector3 testPoint = Other.Position;     testPoint.Y += 0.1f;     for(int iPlane = 0; iPlane < 6; iPlane++)     {        distance = planeCollide[iPlane].Dot(testPoint);        if (distance > 0.0f)        {           bCollide = true;        }    }    for(int i = 0; i < 8; i++)     {        testPoint = Other.Position;        float angle = ((float)Math.PI / 4) * i;        testPoint.X += (float)Math.Cos(angle) * Other.Radius;        testPoint.Y += 0.2f;        testPoint.Z += (float)Math.Sin(angle) * Other.Radius;        for(int iPlane = 0; iPlane < 6; iPlane++)        {           distance = planeCollide[iPlane].Dot(testPoint);           if (distance > 0.0f)           {                bCollide = true;           }          }        }      }    }    return bCollide;  } 
end example
 

If the bounding radii check indicates that there might be a collision, it is time to perform a more detailed collision test. We will do this by testing points from the other object to see if they are within the object-aligned bounding box. The box is formed by the six planes that make up the faces of the box. The Plane class has the FromPoints method that creates the plane based on three points on that plane. These points come from the corners of the bounding box transformed into world space. The code for building the collision planes appears in Listing 5 “8b.

The final step in the process depends on what type of object we are testing against (shown in Listing 5 “8c). If the other object is a Model object, we can use its bounding box information in the test. Here is where we will use the GetCorner method we examined earlier. We loop through the eight corners of the other model and see if any of them are within this model s box. The dot product of the plane and the point returns the distance from that point and the surface of the plane. If this distance is positive, it indicates that the point is within the box, and we do have a collision.

If the other object is not a Model , we will need to perform the test a bit differently. We will start with the simple test to see if the center position of the other model is within the bounding box. We also check eight points around the other object s bounding circle to see if any of those points are within the box. This combination ensures that we can positively determine if we have collided with a billboard, since billboards are the most common objects other than models that we might collide with.

Rendering the Model

The Render method (shown in Listing 5 “9) handles the drawing of the model on the screen. Even though models are the most complex objects that we have encountered thus far, they are not that difficult to render. The Mesh class supplied by Microsoft does most of the difficult work for us. We begin by setting the culling mode to counterclockwise. This is the standard way in which models are built. You may, if you wish, make this something that the user of the game engine can specify for each model to make the engine even more flexible. We will stick with supporting just the one mode for simplicity s sake. If the culling mode is set incorrectly, the models will not appear in the display. After setting the culling mode, we transfer the model s matrix into the device s world transform matrix so that all of the vertices in the model are properly transformed.

Listing 5.9: Model Class Render Method
start example
 public override void Render(Camera cam)  {      // Meshes are divided into subsets, one for each material. Render      // them in a loop.      CGameEngine.Device3D.RenderState.CullMode =            Microsoft.DirectX.Direct3D.Cull.CounterClockwise;          if (m_Parent != null)         {            world_matrix = Matrix.Multiply(m_Matrix, m_Parent.WorldMatrix);         }         else         {            world_matrix = m_Matrix;         }     CGameEngine.Device3D.Transform.World = world_matrix;      for(int i=0; i<m_meshMaterials.Length; i++)      {         // Set the material and texture for this subset.         CGameEngine.Device3D.Material = m_meshMaterials[i];         CGameEngine.Device3D.SetTexture(0, m_meshTextures[i]);         // Draw the m_mesh subset.         m_pMeshes[m_currentPmesh].DrawSubset(i);      }         if (m_Children.Count > 0)         {            Object3D obj;            for (int i=0; i<m_Children.Count; i++)            {                obj = (Object3D)m_Children.GetByIndex(i);                obj.Render(cam);            }        }     Culled = true;  } 
end example
 

The actual rendering of the model is performed by looping through the list of materials that make up the model. For each material, we set the material and texture information in the device. The DrawSubset method of the current mesh renders all of the faces of the model that use that material and texture. If the model has any children, we also have each of them render themselves . After rendering the entire model and any children, the Culled flag is set. Remember that the Render method is called next pass only if the culling routine has cleared this flag.

Updating the Model

Since the Model class also supports the IDynamic interface, we need to have an Update method (shown in Listing 5 “10). The Update method takes care of any changes that happen to the model as time progresses. The user of the game engine can expand upon the dynamics of a model by specifying an additional Update method that follows the ObjectUpdate delegate definition.

Listing 5.10: Model Class Update Method
start example
 public override void Update(float DeltaT)  {     if (Visible)     {     try     {        if (m_UpdateMethod != null)        {           m_UpdateMethod((Object3D)this, DeltaT);        }        m_Matrix = Matrix.Identity;        m_Matrix =          Matrix.RotationYawPitchRoll(Heading+m_AttitudeOffset.Heading,           Pitch+m_AttitudeOffset.Pitch,Roll+m_AttitudeOffset.Roll);        Matrix temp = Matrix.Translation(m_vPosition);        m_Matrix.Multiply(temp);        // Determine the proper LOD index based on range from the camera.        int index = m_nNumLOD;        for (int i = 0; i < m_nNumLOD; i++)        {            if (Range < m_LODRanges[i])            {               index = i;               break;            }        }        if (index >= m_nNumLOD) index = m_nNumLOD   1;        m_currentPmesh = index;        if (m_bHasMoved && m_Quads.Count > 0)        {           Quad q = (Quad)m_Quads[0];           q.Update(this);        }    }     catch (DirectXException d3de)     {        Console.AddLine("Unable to update a Model " + Name);        Console.AddLine(d3de.ErrorString);     }     catch (Exception e)     {        Console.AddLine("Unable to update a Model " + Name);        Console.AddLine(e.Message);     }        if (m_Children.Count > 0)        {           Object3D obj;           for (int i=0; i<m_Children.Count; i++)           {              obj = (Object3D)m_Children.GetByIndex(i);              obj.Update(DeltaT);           }        }     }  } 
end example
 

If the user has specified an Update method (i.e., the method reference is not null), it is called and supplied a reference to this model and the update time delta. This external Update method is likely to change the position and attitude of the object. This is, of course, what makes the objects dynamic. Since the object is likely to have moved in some way, we need to recalculate the world transform matrix for the model. This matrix is formed by multiplying a matrix based on the attitude of the model with another matrix based on the position of the model.

The Update method must also determine which of the level-of-detail models is now appropriate. The distance between the current camera and the model is determined during the culling process and saved in the object s Range attribute. We check this distance against each of the level-of-detail ranges. The lowest level of detail that is still within the proper range is used as the current level of detail. Care is taken that if the model is beyond the farthest range we stop at the lowest level of detail and do not try to index past the end of the array.

If the object has moved, it may no longer be in the same portion of the Quadtree used for culling. The position and attitude attributes of the Object3D class Set methods all set the has moved flag whenever they are called. If the object has moved and knows that it exists in at least one quad, we will request that the Quadtree update itself. This is accomplished by calling the Update method of the first quad in the object s list. This will always be the highest-level quad of the set and it will ensure that all of the quads beneath it also update properly. If the model has any children, they should be updated as well.

Disposing of the Model

The last method in the class is the Dispose method (shown in Listing 5 “11). We have a number of objects allocated by this class that need to be disposed of ”the basic mesh for the model and any children of the model. Also, we need to consider each of the level-of-detail meshes that must be freed as well as any textures required by the model.

Listing 5.11: Model Class Dispose Method
start example
 public override void Dispose()      {         m_mesh.Dispose();         if (m_Children.Count > 0)         {            Object3D obj;            for (int i=0; i<m_Children.Count; i++)            {               obj = (Object3D)m_Children.GetByIndex(i);               obj.Dispose();            }         }         if (m_pMeshes != null)         {            for (int iPMesh = 0; iPMesh < m_pMeshes.Length; iPMesh++)            {               m_pMeshes[iPMesh].Dispose();            }        }        if (m_meshTextures != null)         {            for(int i=0; i<m_meshMaterials.Length; i++)            {               // Create the texture.               m_meshTextures[i].Dispose();            }        }     }   }  } 
end example
 

Using a Model in a Game

This chapter has been dedicated so far to the design of the Model class. Now let s take a look at how a user of the game engine might interact with models within a game. To use a model within the game, we need to do several things. We need to create the model and give it to the game engine so that it may be managed and rendered. We must also perform any initial setup of the model. This all comes under the heading of instantiating the model, and will typically be done in the background task started while the developer splash screen is being displayed. This is the same routine that loads the terrain and billboards. Figure 5 “1 shows our sandy terrain with billboard trees and cacti. It also shows two cars that we have added using the Model class.

click to expand
Figure 5 “1: Models in our scene

The portion of the background task code dealing with the instantiation of the models is shown in Listing 5 “12. The process begins with creating the model and adding to the game engine with the AddObject method. Notice that we can do both operations in one step for each model by placing the call to the constructor as the argument to the AddObject call. Each model must have a unique name associated with it. This allows us to retrieve a reference to the model back from the game engine so that we can manipulate the model.

Listing 5.12: Instantiating Models
start example
 m_Engine.AddObject(new Model("car1", "SprintRacer.x",     new Vector3(0.0f, 0.8f, 0.0f), new Attitude(0.0f, (float)Math.PI, 0.0f)));  m_Engine.AddObject(new Model("car2", "SprintRacer.x",     new Vector3(0.0f, 0.8f, 0.0f), new Attitude(0.0f, (float)Math.PI, 0.0f)));  Model ownship = (Model)m_Engine.GetObject("car1");  ownship.North = 60.0f;  ownship.East = 60.0f;  m_Engine.Cam.Attach(m_ownship, new Vector3(0.0f, 0.85f,   4.5f));  m_Engine.Cam.LookAt(m_ownship);  ownship.Heading = (float)Math.PI * 0.25f;  ownship.SetLOD(10, 3000. 0f);  ownship.SetUpdateMethod(new ObjectUpdate(OwnshipUpdate));  Model car2 = (Model)m_Engine.GetObject("car2");  car2.North = 100.0f;  car2.East = 100.0f;  car2.SetLOD(10, 300.0f);  car2.SetUpdateMethod(new ObjectUpdate(OpponentUpdate)); 
end example
 

We use the game engine s GetObject method to get this reference. Once we have the reference, we can set the model s position and heading. We can also do things like attaching a camera to the model and changing the number of levels of detail that are used by the model. Most importantly, we can set the Update method that the model will use. In this example, we have two different Update methods: one method for the car the player will drive and one for a computer-managed vehicle.

The OwnshipUpdate method (shown in Listing 5 “13) is the method passed to the model representing the player vehicle. When we get to Chapter 10, we will have physics-based dynamics for the vehicles. At this point, we will just move the model along the terrain surface at a user-controlled speed. The position on the terrain is a simple integration based on the current speed and heading. The height of the model and its pitch and roll are based on information on the terrain surface that the model is resting upon.

Listing 5.13: Sample Update Method 1
start example
 public void OwnshipUpdate(Object3D Obj, float DeltaT)  {    Obj.North = Obj.North +        ownship_speed * (float)Math.Cos(Obj.Heading) * DeltaT;     Obj.East = Obj.East +        ownship_speed * (float)Math.Sin(Obj.Heading) * DeltaT;     Obj.Height = CGameEngine.Ground.HeightOfTerrain(Obj.Position) +        ((Model)Obj).Offset.Y;     Obj.Attitude = CGameEngine.Ground.GetSlope(Obj.Position, Obj.Heading);      if (m_bUsingJoystick)     {         Obj.Heading = Obj.Heading +            (CGameEngine.Inputs.GetJoystickNormalX()   1.0f)*DeltaT;         ownship_speed +=            (1.0f   (float)CGameEngine.Inputs.GetJoystickNormalY()) * 0.5f;     }     else if (m_bUsingMouse)     {        try        {           ownship_speed += (float)CGameEngine.Inputs.GetMouseZ() * 0.1f;           float x = (float)CGameEngine.Inputs.GetMouseX();           Obj.Heading = Obj.Heading + (x * .10f)*DeltaT;           if (CGameEngine.Inputs.IsKeyPressed(Key.DownArrow))           {              ownship_speed   = 0.1f;           }           else if (CGameEngine.Inputs.IsKeyPressed(Key.UpArrow))           {              ownship_speed += 0.1f;           }        }        catch (Exception e)        {           GameEngine.Console.AddLine("Exception");           GameEngine.Console.AddLine(e.Message);        }     }     else if (m_bUsingKeyboard)     {        if (CGameEngine.Inputs.IsKeyPressed(Key.LeftArrow))        {           Obj.Heading = Obj.Heading   .50f * DeltaT;        }        else if (CGameEngine.Inputs.IsKeyPressed(Key.RightArrow))        {           Obj.Heading = Obj.Heading + .50f * DeltaT;        }       if (CGameEngine.Inputs.IsKeyPressed(Key.DownArrow))        {          ownship_speed   = 0.1f;        }        else if (CGameEngine.Inputs.IsKeyPressed(Key.UpArrow))        {           ownship_speed += 0.1f;        }     }     foreach (Object3D test_obj in m_Engine.Objects)     {        if (test_obj != Obj)        {           if (Obj.Collide(test_obj))           {              GameEngine.Console.AddLine(Obj.Name + " collided with " +                 test_obj.Name);           }        }     }     foreach (Object3D test_obj in GameEngine.BillBoard.Objects)     {        if (test_obj != Obj)        {           if (Obj.Collide(test_obj))           {              GameEngine.Console.AddLine(Obj.Name + " collided with " +                 test_obj.Name);           }        }     }  } 
end example
 

A joystick, the mouse, or the keyboard may be chosen by the user to control speed and heading. If the user selects a joystick control, the X-axis of the joystick controls the heading and the Y-axis controls the speed. If the mouse was chosen, the side-to-side movement of the mouse (X-axis) controls the vehicle heading. If the mouse has a scroll wheel, it can be used to control the speed. Just in case the mouse doesn t have a scroll wheel, we include keyboard control of the speed using the up and down arrows. Finally, if the keyboard was chosen , we use the four arrow keys to control the speed and heading.

The last thing that we will do in the OwnshipUpdate method is check to see if the vehicle has collided with anything. This is just a matter of iterating through the game engine s list of objects and billboards and checking for a collision. For now, we will just post a message to the console whenever a collision is detected . Once we add physics to the game engine, we can add more realistic reactions .

The Update method for the other vehicle (shown in Listing 5 “14) is much simpler. At this point, the vehicle will be stationary. The only thing we need the routine to do is ensure that the vehicle is resting on the terrain. We set the vehicle s height to the terrain height at its position plus any offset specified for the model.

Listing 5.14: Sample Update Method 2
start example
 public void OpponentUpdate(Object3D Obj, float DeltaT)  {     Obj.Height = CGameEngine.Ground.HeightOfTerrain(Obj.Position) +        ((Model)Obj).Offset.Y;  } 
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