devxlogo

Direct3D, Part 3: Using Meshes to Save and Load Complex Scenes

Direct3D, Part 3: Using Meshes to Save and Load Complex Scenes

n Part 1 of this series, you saw how to get started with Direct3D, Microsoft’s high-performance three-dimensional graphics library. Part 2 explained how to make scenes more realistic by adding lighting and textures to objects. The result of applying those two techniques is often a complex scene containing many objects displayed with different colors, textures, and material characteristics, such as reflectivity.

Because building such complex scenes takes time, it might be nice to be able to save the results so you can load them again later. While it would be simple to save the result as a bitmap file, it would be better if you could save the scene’s geometry and texture information so you could load the scene and manipulate it in three dimensions later.

Direct3D’s Mesh class lets you do just that. A Mesh object saves information about the vertices, faces, normals, and texture data that you need to draw a complex scene. It groups the faces into subsets that you can draw individually; each subset can have its own materials and textures. Finally, the Mesh class provides methods to save and load scenes to and from .x (pronounced “dot X”) files.

This article explains how to use code to build a Mesh object that represents a scene. It also explains how to load and save Mesh objects in .x files.

Introduction to Meshes
A Mesh object contains three buffers that hold key information about the scene that the Mesh represents:

  • The vertex buffer holds information about the scene’s points. This includes the points’ X, Y, and Z coordinates. Depending on how you want to display the scene, it may also include normal and texture coordinate data for each point. In other words, the vertex buffer can accept more than one format. In this article, I’m going to skip to a fairly advanced vertex format and store position, normal, and texture information for each vertex so the program can use lighting and textures discussed in Part 2.
  • The index buffer contains information about the points that make up the scene’s faces. Normally a scene’s faces are triangles, so the index buffer typically contains a list of the vertex indices that make up each triangle. For example, if the first triangle includes the vertices with indexes 10, 39, and 7 in the vertex buffer, then the first three entries in the index buffer are 10, 39, and 7.
  • The attribute buffer contains an integer value for each scene face that indicates that face’s mesh subset. For example, suppose you build a scene that contains a brick courtyard, which, in turn contains a pool of water. In this case, the triangles composing the courtyard would make up one subset, and the triangles composing the pool would make up a second subset. When a program needs to draw the scene, it can call the Mesh object’s DrawSubset method twice, once to draw each subset. Before it calls DrawSubset, the program selects the appropriate material and texture for each subset.

To fill these buffers, the Mesh class provides two methods, SetVertexBufferData and SetIndexBufferData, that define the scene’s vertex and index data. For simple scenes such as a cube, it’s easy enough to build the arrays that these methods use to define the vertex and index data manually; however, you’ll find it’s far simpler to build complex scenes in pieces?it’s not easy to just sit down and write out these arrays. Instead you should use code to build them as much as possible. Rather than building the arrays directly, you save data into two generic List objects and, when you’re finished building the scene, you can copy those values into arrays.

Setting the attribute buffer is just a little trickier. The Mesh class has a SetAttributeTable method but I had trouble getting it to work. To avoid using SetAttributeTable, the sample code that accompanies this article calls the Mesh object’s LockAttributeBuffer method to get an array of attribute values for the scene’s faces. It sets the appropriate subset values and calls UnlockAttributeBuffer to apply the changes to the Mesh.

Building a Mesh
The program example d3dMakeMesh builds and displays a Mesh. The following paragraphs describe the most important parts of the code, but I encourage you to download the example program and refer back to the first and second part of this series to fill in the blanks.

Here’s some of the code that the program uses to set its vertex and index information for the “brick courtyard with pool” scene:

   ' Make a scene.   Dim vertices As New List(Of CustomVertex.PositionNormalTextured)   Dim indices As New List(Of Short)   Dim num_faces As Integer = 0      ' Make the ground.   Dim ground_face_start As Integer = num_faces   For x As Integer = -10 To 5 Step 5      AddHorzRectangle(vertices, indices, _        x, x + 5, -10, -5, 0, 1, 0, 1)      AddHorzRectangle(vertices, indices, _        x, x + 5, 5, 10, 0, 1, 0, 1)   Next x   AddHorzRectangle(vertices, indices, -10, -5, -5, 0, 0, 1, 0, 1)   AddHorzRectangle(vertices, indices, -10, -5, 0, 5, 0, 1, 0, 1)   AddHorzRectangle(vertices, indices, 5, 10, -5, 0, 0, 1, 0, 1)   AddHorzRectangle(vertices, indices, 5, 10, 0, 5, 0, 1, 0, 1)      ' Inside of pool.   AddRectangle(vertices, indices, _      New Vector3(-5, Y_WATER, 5), New Vector3(-5, Y_GROUND, 5), _      New Vector3(0, Y_GROUND, 5), New Vector3(0, Y_WATER, 5), _      0, 0, 0, 0.2, 1, 0.2, 1, 0)   ...   num_faces = indices.Count  3   Dim ground_face_stop As Integer = num_faces - 1      ' Water.   Dim water_face_start As Integer = num_faces   AddRectangle(vertices, indices, _      New Vector3(-5, Y_WATER, -5), New Vector3(-5, Y_WATER, 5), _      New Vector3(5, Y_WATER, 5), New Vector3(5, Y_WATER, -5), _      0, 0, 0, 1, 1, 1, 1, 0)   num_faces = indices.Count  3   Dim water_face_stop As Integer = num_faces - 1   ...

The program first creates two generic Lists named vertices and indices and then defines a variable called num_faces to keep track of the number of faces created so far.

Next, the code creates faces to define the scene’s brick-covered ground. It sets the variable ground_face_start to the index of the first face that makes up the ground. It then calls the helper method AddHorzRectangle to create data representing horizontal rectangles. This method adds PositionNormalTextured objects to the vertices collection to represent points with normal and texture information. It also adds index information to the indices collection.

The code then calls the AddRectangle method to add non-horizontal rectangles to the vertex and index data. After adding all the faces needed to draw the ground, it saves the index of the last face in the variable ground_face_stop.

The code then repeats these steps to create data representing a pool of water and two blocks of wood.

The next step is to create a Mesh so you can save the scene. To do that, you first create a new Mesh object, copy the values in the vertices and indices collections into arrays, and then call the Mesh’s SetVertexBuffer and SetIndexBuffer methods:

   ' Build the mesh.   m_Mesh = New Mesh(num_faces, vertices.Count, 0, _      CustomVertex.PositionNormalTextured.Format, m_Device)      ' Convert the lists into arrays.   Dim vertices_arr() As CustomVertex.PositionNormalTextured = _      vertices.ToArray()   Dim indices_arr() As Short = indices.ToArray()      ' Set the vertex and face data.   m_Mesh.SetVertexBufferData(vertices_arr, LockFlags.None)   m_Mesh.SetIndexBufferData(indices_arr, LockFlags.None)

You need to assign the correct subset for each face. The sample application calls LockAttributeBufferArray to get an array holding the faces’ subset numbers. Then it uses the ground_face_start, ground_face_stop, water_face_start, and other variables to determine which faces belong to each subset. This example uses four subsets for faces, corresponding to the brick, water, wood side grain, and wood end grain faces in the scene. After setting the faces’ subset numbers, the code calls UnlockAttributeBuffer to save the changes. Here’s the code:

   ' Assign each face's subset.   Dim subset() As Integer = _      m_Mesh.LockAttributeBufferArray(LockFlags.Discard)   For i As Integer = ground_face_start To ground_face_stop      subset(i) = 0   Next i   For i As Integer = water_face_start To water_face_stop      subset(i) = 1   Next i   For i As Integer = wood_face_start To wood_face_stop      subset(i) = 2   Next i   For i As Integer = woodend_face_start To woodend_face_stop      subset(i) = 3   Next i   m_Mesh.UnlockAttributeBuffer(subset)

Your code can specify the vertices and faces in the Mesh in any order you like; however, not all orders are equally efficient. For example, it would be best to draw all of the faces for a particular subset at one time, so you don’t need to load their material and texture repeatedly. Meshes also often represent large areas tiled with a collection of triangles; so drawing them in large strips or fans may be much more efficient than drawing the faces individually.

Optimizing Meshes
To allow the Mesh to work as efficiently as possible, the code should let the Mesh optimize its data. The following code shows how the example program does this. The optimization method takes as a parameter an array listing the faces adjacent to each of the Mesh’s faces (fortunately, the Mesh’s GenerateAdjacency method can build this array for you so you don’t need to do it yourself). The parameter 0.1 tells this method how precise the vertex coordinates are. In this example, if two vertices lie with 0.1 units of each other, the method considers them to be the same point:

   ' Optimize.   Dim adjacency( _      m_Mesh.NumberFaces * 3 - 1) _      As Integer   m_Mesh.GenerateAdjacency( _      CSng(0.1), adjacency)   m_Mesh.OptimizeInPlace _      (MeshFlags.OptimizeVertexCache, _      adjacency)      ' Remember the number of subsets.   m_NumSubSets = m_Mesh. _      GetAttributeTable().Length

The preceding code creates an array big enough to hold the adjacency information and then calls the GenerateAdjacency method to fill in the array’s entries. It next calls the Mesh’s OptimizeInPlace method to optimize the Mesh, and finally, saves the number of subsets in the Mesh for later use.

The rest of the setup code creates materials and textures for each of the Mesh’s subsets (that process is explained in Part 2, so I won’t repeat that discussion here). For now, all you need to know is that the program loads one material and texture for each subset into the arrays m_Materials and m_Textures.

The code that draws the Mesh is very similar to the code used in Part 2 of this series. Because both approaches involve drawing scenes, you set up matrices, and then select a material and texture before calling a drawing method to produce a picture. The difference is in how the two draw objects in the scene. The previous approach used the Direct3D device’s DrawPrimitives method to generate results. In contrast, the d3dMakeMesh program example calls the Mesh’s DrawSubset method to draw each of its subsets. The Mesh then handles the details of drawing triangle strips, fans, or whatever.

The following fragment shows the core of the d3dMakeMesh program’s drawing code. For each subset, the program selects that subset’s material and texture, and then calls the Mesh’s DrawSubset method:

Figure 1. Pool Parlor: The sample program d3dMakeMesh builds a Mesh representing a brick courtyard that contains a pool of water and two blocks of wood.
   ' Draw the mesh's subsets.   For i As Integer = 0 To m_NumSubSets - 1      m_Device.Material = m_Materials(i)      m_Device.SetTexture(0, m_Textures(i))         m_Mesh.DrawSubset(i)   Next i

Listing 1 shows the complete CreateMesh method. Figure 1 shows the sample d3dMakeMesh program in action. As discussed, above, the program makes four subsets that represent the groups of faces that use four different textures. The different subsets define the brick ground, the bricks inside the pool area, the water, the sides of the wooden blocks, and the tops of the wooden blocks.

The result is an impressively complicated scene drawn with a few simple lines of code. However, a major part of the motivation behind using a Mesh is not to draw a complex scene, but to do so in a standardized way so you can load and save Mesh data. The next sections explain how to save a Mesh into a .x file and load a Mesh from such a file.

Saving .x Files
Many game developers build complex scenes in advanced 3-D modelers such as Studio Max, Blender 3D, and Lightwave 3D. They can then export the results into .x files and load them into Visual Studio applications.

If you’re a do-it-yourselfer, however, it’s not too hard to write Visual Basic or C# code to save a Mesh into a .x file. There are really only a couple of steps after you build the Mesh. The sample program d3dSaveXFile, which you can find in the downloadable code in both VB.NET and C# versions, uses the following code to save a .x file:

   ' Save the mesh into a .x file.   ' Make the extended materials.   Dim exmaterials(m_Materials.Length - 1) _      As ExtendedMaterial   Dim files() As String = _      {"brick.jpg", "water.jpg", _      "wood.jpg", "woodend.jpg"}      For i As Integer = 0 To m_Materials.Length - 1      exmaterials(i).Material3D = m_Materials(i)      exmaterials(i).TextureFilename = files(i)   Next i      ' Prepare the mesh and save it.   m_Mesh.GenerateAdjacency(CSng(0.1), adjacency)   m_Mesh.Save("test.x", adjacency, exmaterials, XFileFormat.Text)

First, the code makes an array of ExtendedMaterial structures to store information about the Mesh’s materials and textures. Each ExtendedMaterial holds material information plus the name of the file that contains its texture. Later, you must assume that the material files are located in the some directory where the program loading the .x file can find them (usually in the same directory as the .x file).

Next, the program creates a new adjacency array. Like the program d3dMakeMesh, this example uses an adjacency array when it optimizes the Mesh data. Optimizing the data may rearrange the scene’s faces and vertices, however, so the original adjacency array may now be incorrect. The code shown here calls the Mesh’s GenerateAdjacency method to recreate that data.

Finally, the program calls the Mesh object’s Save method, passing it the name of the file where it should save the data, the adjacency array, the extended materials array, and the format it wants the file to use (Binary, Text, or Compressed).

That’s all there is to it.

Loading .x Files
Loading a .x file is a little more involved than saving one, because you have to read the individual texture files. The following code shows how the sample program d3dLoadXFile loads .x files:

   ' Load a mesh from a .x file.   Public Sub LoadMesh(ByVal file_path As String, _   ByVal file_name As String)      ' Load the mesh.      If Not file_path.EndsWith("") Then file_path &= ""      Dim exmaterials() As ExtendedMaterial = Nothing      m_Mesh = Mesh.FromFile(file_name, MeshFlags.Managed, _         m_Device, exmaterials)         ' Load the textures and materials.      ReDim m_Textures(exmaterials.Length - 1)      ReDim m_Materials(exmaterials.Length - 1)      For i As Integer = 0 To exmaterials.Length - 1         Dim texture_file As String = _            exmaterials(i).TextureFilename         If texture_file IsNot Nothing Then            Debug.WriteLine("Texture " & i & ": " & texture_file)            If texture_file.Length > 0 Then               Try                  m_Textures(i) = TextureLoader.FromFile( _                     m_Device, file_path & texture_file)               Catch ex As Exception                  Debug.WriteLine("*********************")                  Debug.WriteLine("Error loading texture " & _                     texture_file)               End Try            End If         Else            Debug.WriteLine("Texture " & i & ": " & "")         End If            m_Materials(i) = exmaterials(i).Material3D         m_Materials(i).Ambient = m_Materials(i).Diffuse      Next i         ' Save the number of subsets.      m_NumSubSets = m_Materials.Length   End Sub

The preceding code makes sure the path where it should look for texture files ends with a backslash. It defines an ExtendedMaterial array and calls the Mesh class’s FromFile method to load the file. The code then allocates arrays to hold one material and one texture for each of the entries in the loaded ExtendedMaterial array. It loops through the array, saving the materials and trying to open the texture files. Finally, the code saves the number of subsets so the drawing code can loop through the subsets calling DrawSubset for each.

When it starts, example program d3dLoadXFile looks through a Meshes directory (included in the download) and lists any .x files it finds in its combo box. When you select one of the files, the program loads and displays it.

Figure 2 shows program d3dLoadXFile displaying a .x file built by Microsoft and included in the SDK samples. This scene uses a single texture file (tiger.bmp) to draw each of the tiger’s sides.

?
Figure 2. Terrific Tiger: The program d3dLoadXFile loaded this Mesh from a .x file.
?
Figure 3. Pretty Plane: This scene uses two texture files.

Figure 3 shows another .x file included in the SDK samples that uses two texture files to draw the plane’s body and wings.

Because different .x files may place their data in different places at different scales, the d3dLoadXFile program provides Zoom In (+) and Zoom Out (?) buttons that you can use to zoom in or out on the data.

With a little experimentation, you’ll become comfortable with the power of the Mesh class. A Mesh can represent multiple subsets of faces, each of which uses its own materials and textures. By using a Mesh, you can draw relatively complex scenes with remarkably little code, as well as save scenes and reload them by reading Mesh data from .x files without too much trouble.

But that’s not all! Meshes can do still more, such as providing a basis for three-dimensional animation. A Mesh can contain a hierarchy of separate objects that are spatially related to each other. For example, if you wanted to model a humanoid robot, the robot’s head, arms, and legs are all attached to the body. As the body moves, so do these appendages. Similarly, if you bend an elbow joint, the attached forearm and hand move as well. Connecting meshes in this manner is called mesh animation, which I hope to cover in a future article. Until then, send any .x file Mesh masterpieces you create to [email protected]. I’ll be happy to post the best of your creations so you can share them with others.

devxblackblue

About Our Editorial Process

At DevX, we’re dedicated to tech entrepreneurship. Our team closely follows industry shifts, new products, AI breakthroughs, technology trends, and funding announcements. Articles undergo thorough editing to ensure accuracy and clarity, reflecting DevX’s style and supporting entrepreneurs in the tech sphere.

See our full editorial policy.

About Our Journalist