[ 32bits.co.uk ] Coding Direct3D for real people

32Bits - Coding for real people!

.:[ Please always use http://www.32bits.co.uk to access the site. Thanks! ]:.

32Bits main page Introduction to the site The tutorials index Our releases The license for content on this site Public forum for questions and feedback We provide professional services Contact us

 

Last updated: 10:55 AM Monday October 2008

welcome.gif (1805 bytes)

This one is going to be a fairly short tutorial giving you an introduction to D3D meshes. There are 2 aspects to meshes - simple ones, and skinned meshes. Skinned meshes are a huge topic and will take more than one tutorial to cover. So for now we're going to run through a short tutorial on simple meshes, wrap up DirectX Techniques Series 1 and come back to skinned meshes in Techniques Series 2.

With that out of the way, here's a look at what we'll be coding this time:


Fig 4.1: Roar.


Use the arrow keys (left, right, up and down) and home/end to rotate the model. You might have spotted something very similar to this when browsing through the DirectX SDK samples, one of which demonstrates exactly how to render a simple mesh. The difference between the SDK version and the 32Bits version is simplicity - we're going to do in 20 lines of code what takes Microsoft a few hundred!

First let's talk about what meshes are, and why they are important. Up to now, all the previous tutorials have shown you how to render simple objects using manually specified vertex coordinates (cubes, sprites, triangles etc). That's all well and good, but manually specifying vertex coordinates, texture coordinates and material properties for even the simplest object is a real time consuming pain. If you did this for every object in your next 3D FPS blockbuster game, it would take years to write. Major problem, I'm sure you'll agree.

The solution is to use meshes. This allows us (or an artist, if you're as bad at modelling as I am) to create 3D objects in a modelling package such as 3DSMax or Maya, then export them to files. We can then load these files directly into our D3D application and render them on screen. No more specifying of vertex & texture coordinates, we just load the mesh and render away. This is possible because 3D modelling applications also work in object space with triangles & textures to create meshes ("models") - all that needs to happen is for the data to be exported in such a way that D3D can understand it.

As a practical example, here's the tiger from figure 4.1 rendered in wireframe mode:


Fig 4.2: Roar, slightly less.

As you can see it's just a set of triangles with a texture stretched across it, exactly like the cube or terrain we coded in previous tutorials. There is absolutely nothing stopping us from manually specifying the vertices for the tiger in our application, except for the time it would take.

There are two ways to use meshes in our D3D apps. First, we can write an importer for some of the more common mesh types (such as MD3). Second, the far easier option, we can use the built in mesh type supported by D3DX - the .X file. This tutorial will show you how to use .X files, and I'll cover writing a manual importer for MD3 in a later tutorial. There are 2 ways to create a mesh file. If you're not a 3D modeller you can download .3DS mesh files saved from 3DSMax software, and convert them to .X files using a tool called "conv3ds.exe". Conv3DS.exe is part of the DirectX SDK. If you're creating your own models (or getting an artist to do them for you), there are also plugin exporters for 3DSMax that will save the meshes directly to .X files. The exporters are generally the safer option, as conv3ds.exe doesn't always get the conversion right.

There are 2 components to the X format - the .X file itself, and the textures ( jpegs, bmps or any other supported image format) the mesh uses. The .X file is actually just a text file containing directives and information about how to build the mesh. Try opening the tiger.x included in this tutorial in Notepad or UltraEdit. As you can see, it's perfectly possible to manually edit the vertex data in the mesh. One important point to note - the texture filenames used by the mesh are explicitly stated in the .X file itself, therefore if you want to rename the texture files make sure you open the .X file, locate the original texture name and change it to the new texture name:

Material {
0.694118;0.694118;0.694118;1.000000;;
50.000000;
1.000000;1.000000;1.000000;;
0.000000;0.000000;0.000000;;
TextureFilename {
"tiger.bmp";
}
}

If you're interested in learning more about the structure and data of .X files, take a look at the X File Format Reference in MSDN. Understanding the file structure is not a prerequisite of using .X meshes in your apps though, so don't worry about it too much.

There are 2 steps to loading and drawing an .X file using D3DX. First, we load the .X file into memory and retrieve pointers to the material buffer stored in the X file. This buffer contains information on the different textures and materials used in the mesh. We index through the buffer and extract each material & texture element (for every material there is a texture, so number of materials is always equal to the number of textures - this will be important shortly), and store them. At this point we have only retrieved information on the textures and materials not the objects themselves, so we then have to create D3DMATERIAL8 and LPDIRECT3DTEXTURE8 objects as required.

There is one subtle difference between a .X file material and a lighting material. Whereas a lighting material (D3DMATERIAL8) is literally only concerned with lighting, a mesh material (D3DXMATERIAL) also contains the filename of a texture.

Once that step has been completed, the mesh can be rendered. We iterate through the mesh, setting the materials & textures and telling D3D to render the part of the mesh those textures and materials relate to. This is done using pieces of vertex data from the mesh called "subsets". The number of subsets is equal to the number of materials and textures in the mesh. Here's a quick flow chart of the steps to clear up any confusion.


Fig 4.3: This is how we do it.

Not too tough! The last piece of the puzzle is that meshes are created and manipulated in object space, then transformed and rendered via the normal transformation and lighting pipeline. In other words, they are passed through the world, view and projection transforms just like everything else.

Let's take a look at the code for this tutorial.


[ Download complete VC6 workspace as .zip ]

Mesh.cpp
[ View source code in this window ] [ View source code in a new window ] [ Source code in plain ascii .cpp file ]

CD3DMesh.cpp [ View source code in this window ] [ View source code in a new window ] [ Source code in plain ascii .cpp file ]

CD3DMesh.h [ View source code in this window ] [ View source code in a new window ] [ Source code in plain ascii .cpp file ]

CD3DObject.h [ View source code in this window ] [ View source code in a new window ] [ Source code in plain ascii .cpp file ]


To keep in line with previous tutorials the Mesh class inherits from the CD3DObject class, allowing for easier management when we use it in bigger applications. To do this, I've made a couple of minor changes to the CD3DObject class. I'll run through those quickly first. If you need a refresher on the CD3DObject class, take a look at DirectX Basics Series 2 Tutorial 5. First, in the header file I've added a new MESH enum member to the RTTI:

enum D3DRTTI {UNKNOWN, SPRITE, RASTERFONT, CUBE, MESH};

Simple enough. Next I added a pure virtual overloaded Render() function that doesn't require a pointer to a surface, as we don't need one for the Mesh class:

virtual HRESULT Render(LPDIRECT3DDEVICE8& pDevice)=0;

In hindsight I wish I'd passed the LPDIRECT3DDEVICE and SURFACE pointers as pointers rather than references, as then I could have used a default NULL parameter for the surface. That would save adding the overloaded Render() function here. Ah well, hindsight is always 20/20 and changing all the code now will really confuse everyone. Feel free to make any changes you think are appropriate to your own code!


That's the CD3DObject changes out of the way. Here's the CD3DMesh class:

class CD3DMesh : public CD3DObject
{
public:

    CD3DMesh();
    virtual ~CD3DMesh();

    HRESULT Initialise(char* szMeshFile, LPDIRECT3DDEVICE8& pDevice);
    HRESULT Shutdown();

    // Render is a pure virtual in the base class.
    HRESULT Render(LPDIRECT3DDEVICE8& pDevice);

    // These methods are not required but are defined as pure virtual, so must be
    // included.

    HRESULT RestoreVolatile(){return E_FAIL;}
    HRESULT ReleaseVolatile(){return E_FAIL;}
    HRESULT Render(LPDIRECT3DDEVICE8& pDevice, LPDIRECT3DSURFACE8& pRenderSurface){return E_FAIL;}

private:

    LPD3DXMESH m_pMesh;                 // The actual mesh object

    LPD3DXBUFFER m_pMaterialsBuffer;    // Receives the materials when the X file is loaded
    D3DMATERIAL8* m_pMaterials;         // Array of materials extracted from m_pMaterialsBuffer
    LPDIRECT3DTEXTURE8* m_pTextures;    // Array of textures extracted from m_pMaterialsBuffer

    DWORD m_dwNumMaterials;             // The number of materials in this mesh

};


The class definition is pretty simple, and follows the same pattern as before. Take a look at the member variables in this class; here's how they line up to the flow diagram above:


Fig 4.4: Steps and code


The step where the materials buffer is released right after obtaining it is often a confusing one, so just remember which variables do what & when they are used. That's the process, here's the code for the class initialisation.

HRESULT CD3DMesh::Initialise(char* szMeshFile, LPDIRECT3DDEVICE8& pDevice)
{
    HRESULT rslt = S_OK;

    rslt = D3DXLoadMeshFromX(szMeshFile, D3DXMESH_MANAGED, pDevice, NULL,
                             &m_pMaterialsBuffer, NULL, &m_dwNumMaterials, &m_pMesh);
    if(FAILED(rslt)) { return Error(rslt, __LINE__, __FILE__, "Couldn't load mesh from .X file"); }


I've removed the comments from the code snippet to save space. D3DXLoadMeshFromX is the starting point for all the mesh work, it's prototype is as follows:

HRESULT WINAPI D3DXLoadMeshFromX(LPCTSTR pFilename, DWORD Options, LPDIRECT3DDEVICE8 pDevice, LPD3DXBUFFER *ppAdjacency, LPD3DXBUFFER *ppMaterials, DWORD *pNumMaterials, LPD3DXMESH *ppMesh);

Parameter 1 is the filename of the .X mesh file to be loaded. Parameter 2 specifies the options used for mesh creation. This has a very similar purpose to the options used when creating a vertex or index buffer - take a look in the SDK for all the available settings. Parameter 3 is a pointer to the IDirect3DDevice interface for the application. Parameter 4 is a pointer to a buffer that will receive adjacency information for each triangle in the mesh. Adjacency info allows us to quickly see which triangles are (unsurprisingly!) adjacent to any selected triangle in the mesh. There are some advanced uses for this data, but we won't need it in this tutorial so using a NULL for this parameter is fine. Parameter 5 is a pointer to a buffer that will receive all the important material information, so we definately need to specify this. Parameter 6 is a pointer to a DWORD that will receive the all important number of materials (and thus number of textures and subsets) in the mesh. Parameter 7 is a pointer to an actual mesh object that will receive the mesh data, and will be used to render it.

Once the .X file has been loaded successfully, it needs to be processed.

D3DXMATERIAL* pMaterials = (D3DXMATERIAL*)m_pMaterialsBuffer->GetBufferPointer();

// Create 2 arrays, 1 for textures and 1 for materials. D3DXLoadMeshFromX puts the
// number of materials/textures (always the same) into m_dwNumMaterials above.

m_pMaterials = new D3DMATERIAL8[m_dwNumMaterials];
m_pTextures = new LPDIRECT3DTEXTURE8[m_dwNumMaterials];


First, we retrieve a pointer to the start of the materials buffer from the m_pMaterialsBuffer var (which was passed as parameter 5 to the D3DXLoadMeshFromX function). Next we need to create 2 arrays that will contain all of the materials and textures in this mesh. Remember that the number of textures is the same as the number of meshes, so both arrays will have the same number of elements. Next, we need to populate the arrays.

{
for(int iCount = 0; iCount < (int)m_dwNumMaterials; iCount++)
{
    // For each material buffer element, copy the D3DMATERIAL into this class' array.
    m_pMaterials[iCount] = pMaterials[iCount].MatD3D;

    m_pMaterials[iCount].Ambient = m_pMaterials[iCount].Diffuse;

    rslt=D3DXCreateTextureFromFile(pDevice, pMaterials[iCount].pTextureFilename, &m_pTextures[iCount]);
    if(FAILED(rslt)) { return Error(rslt, __LINE__, __FILE__, "D3DXCreateTextureFromFile() failed."); }
}
}


Again I've removed some comments to save space. The purpose of this loop is simple once you understand why we do it. pMaterials is a pointer to an array (the materials buffer) containing all the texture and material information we obtained a few lines of code above. The number of elements in this array is defined by m_dwNumMaterials, which was set by the D3DXLoadMeshFromX function call at the start of the class Initialise method. We need to iterate through this array and extract the material information and texture filenames for the mesh.

First, we copy the material information for the current array element into our m_pMaterials array class member. Because .X files only define the diffuse colour part of a material, we then copy the diffuse material colour into the ambient material colour. If you're unsure why we need to do this, take a quick materials & lighting refresher here. Next we need to load the image defined in the pMaterials array into a texture surface. This is really important - the texture itself is not contained in the pMaterials array, only the filename is. It's up to us to load and store it as necessary.

Once we're done loading all the materials and textures, we're all finished with the buffer.

m_pMaterialsBuffer->Release();
m_pMaterialsBuffer = NULL;


That's the mesh initialised, now we need to render it. Here's the CD3DMesh::Render() method, and it's probably a lot simpler than you would expect!

// Rendering a mesh is very easy. Simply iterate through each material...
{
for(int iCount = 0; iCount < (int)m_dwNumMaterials; iCount++)
{
    //... set the material and texture in the normal way
    rslt = pDevice->SetMaterial(&m_pMaterials[iCount]);
    if(FAILED(rslt)) { return Error(rslt, __LINE__, __FILE__, "Couldn't set the material."); }

    rslt = pDevice->SetTexture(0, m_pTextures[iCount]);
    if(FAILED(rslt)) { return Error(rslt, __LINE__, __FILE__, "Couldn't set the texture."); }

    //... then call ID3DXMesh::DrawSubset to draw the vertices in the mesh
    // that have this material and texture applied to them.

    rslt = m_pMesh->DrawSubset(iCount);
    if(FAILED(rslt)) { return Error(rslt, __LINE__, __FILE__, "DrawSubset() failed."); }
}
}


The comments in the code make this pretty self explanatory. As already discussed, a mesh is divided into subsets of triangles. Each subset has its own texture and material, and the number of subsets equals the number of materials (which in turn equals the number of textures). Therefore to render a mesh we need to loop through all the subsets and render them, setting the materials and textures as we go.

That's precisely what we're doing in the above code. The for() loop iterates for the number of materials in the mesh. At every iteration, we set the material in the current materials array element, we set the texture in the current textures array element and then call ID3DXMesh::DrawSubset() to render the subset of triangles in the mesh that the material and texture relates to. Simple!

Before moving on, it's important to take a look at the CD3DMesh::Shutdown() method.

HRESULT CD3DMesh::Shutdown()
{
    // Free the materials array
    if(m_pMaterials)
        delete [] m_pMaterials;

    // If there are textures...
    if(m_pTextures)
    {
        // ...index through the texture array & release the texture object
        for(int iCount = 0; iCount < (int)m_dwNumMaterials; iCount++)
        {
            m_pTextures[iCount]->Release();
            m_pTextures[iCount] = NULL;
        }

        // Don't forget to delete the array as well
        delete [] m_pTextures;
    }

    // Release the mesh object
    if(m_pMesh)
        m_pMesh->Release();

    m_dwNumMaterials = 0;
    m_pMaterialsBuffer = NULL;
    m_pMaterials = NULL;
    m_pTextures = NULL;
    m_pMesh = NULL;

    return S_OK;
}


I won't go through this code in detail as it's very straight forward. Just remember to properly release all the texture surfaces (COM objects) from the array before deleting the texture array (m_pTextures), or you'll be leaking interfaces. That can get quite serious in complex meshes.

That's covered the mesh class itself, which was probably easier than you expected. Here's how it's implemented into the main code (Mesh.cpp). Mesh.cpp was created with the 32Bits DirectX AppWizard for VC6, so you may notice a slight difference to this framework code compared to previous tutorials. All future tutorials will use the AppWizard as it's cleaner code and more consistent than the older framework. You can get the AppWizard for your own projects from the Products page.

At the top of Mesh.cpp I've declared some globals for the mesh and for some rotations:

CD3DMesh g_Mesh;
float g_XRotation, g_YRotation, g_ZRotation;


To initialise the mesh, the following code was added to the AppInit() function:

// Load the sample mesh into the mesh class object
g_Mesh.Initialise("tiger.x", g_pDevice);

g_XRotation = 0.0f;
g_YRotation = 0.0f;
g_ZRotation = 0.0f;


Finally the following code was added to the Render() function to rotate and render the mesh:

D3DXMATRIX matWorld;
D3DXMatrixRotationYawPitchRoll(&matWorld, g_XRotation, g_YRotation, g_ZRotation);
g_pDevice->SetTransform(D3DTS_WORLD, &matWorld);

// Render the mesh

g_Mesh.Render(g_pDevice);


That's all that's required to render a simple X file mesh! Before wrapping this tutorial up, take a look at the MsgProc() function in Mesh.cpp. You'll notice I've added some extra switch/case statements to the WM_KEYDOWN handler. This code will allow you to rotate the mesh in all 3 dimensions using the left, right, up and down arrow keys, and the home/end keys.

Before wrapping this tutorial up, if you want to take the CD3DMesh class out of the CD3DObject structure it's fairly easy to do - remove the inheritance, remove the virtual functions and remove the calls to the base class methods in the CD3DMesh constructor. It's probably a good idea to keep the Error() method, so just copy that from the CD3DObject class.

You'll probably find that the hardest part about using meshes in DirectX is finding good .X files, especially if you're not a modeller! Have fun with meshes, and see you in the next tutorial where DirectInput mice and Dot3 bumpmapping are covered!

 

Click here to send feedback, bug reports, comments and ideas etc!