Using the ID3DXMesh Class

Home | Up | Search | X-Zone News | Services | Book Support | Links | Feedback | Smalltalk MT | The Scrapyard | FAQ | Technical Articles

 

Written by Robert Dunlop
Microsoft DirectX MVP

Table of Contents

Introduction
Creation of Mesh Objects
Retrieving Mesh Information
Mesh Storage
Using Attributes to Define Subsets
Optimizing Meshes
Rendering with DrawSubset
Using Cloning
Generating Normals

Related Articles of Interest:

Creating Subsets in ID3DXMesh

Introduction

DirectX 8 Graphics introduced a series of helper interfaces and functions for handling various aspects of mesh creation, manipulation, and rendering. 

The interfaces are derived from a base class, ID3DXBaseMesh, which is the basis for several different interfaces used to encapsulate mesh objects:

ID3DXMesh A basic mesh object.
ID3DXPMesh Provides progressive simplification of a mesh for variable LOD implementation.
ID3DXSPMesh Used to perform mesh simplification.
ID3DXSkinMesh Provides support of surface skinning of meshes.

In this article, we will focus on the ID3DXMesh interface.  This function provides basic mesh support, through the encapsulation of the following components:

bulletA vertex buffer, containing the mesh vertices
bulletAn index buffer, containing 3 indices per face.  All drawing is performed using indexed triangle lists.
bulletAn attribute ID for each face, which is a 32 bit value allowing you to specify faces as belonging to separate groups.  These groups can then be rendered separately, with different materials, textures, and render states.

Creation of Mesh Objects

There are a number of functions that can be used to create an ID3DXMesh mesh object.  We'll group them into four categories:

Basic mesh creation functions Create a mesh object of specified format and storage capacity.
Shape creation functions Create a mesh object containing one of several basic primitives.
Mesh file functions Allow the loading of mesh objects from X files.
Mesh operations Performs operations on an existing mesh object (such as optimization or format conversion), creating a new mesh.  These will be covered in later sections of this article, as appropriate.

Basic Mesh Creation Functions

There are two basic functions used for creation of an empty mesh container, D3DXCreateMesh() and D3DXCreateMeshFVF().  These functions vary in the definition of the vertex format - the first accepts a declarator, while the later accepts a combination of flexible vertex format flags.  The syntax of these functions is:

HRESULT D3DXCreateMesh(
    DWORD NumFaces,
    DWORD NumVertices,
    DWORD Options,
    CONST DWORD* pDeclaration,
    LPDIRECT3DDEVICE8 pDevice,
    LPD3DXMESH* ppMesh
);

HRESULT D3DXCreateMeshFVF(
    DWORD NumFaces,
    DWORD NumVertices,
    DWORD Options,
    DWORD FVF,
    LPDIRECT3DDEVICE8 pDevice,
    LPD3DXMESH* ppMesh
);

These functions take the following parameters:

DWORD NumFaces Number of faces in the mesh.  This will determine the size of the index buffer, which will have 3 * NumFaces indices.
DWORD NumVertices Number of vertices in the mesh.  This will determine the size of the vertex buffer.
DWORD Options Option flags for mesh creation.  See SDK documentation.
CONST DWORD* pDeclaration Used in D3DXCreateMesh().  Pointer to a declarator which defines the vertex format of the mesh.
DWORD FVF Combination of flexible vertex format flags that specify the vertex format of the mesh.
LPDIRECT3DDEVICE8 pDevice Pointer to a valid IDirect3DDevice8 interface.
LPD3DXMESH* ppMesh Pointer to a variable of type ID3DXMESH * that will recieve a pointer to the mesh.

Shape Creation Functions

The following shape creation functions create mesh objects containing various shapes:

D3DXCreateBox() Creates an axis-aligned box of specified dimensions.
D3DXCreateCylinder() Creates a cylinder of specified length, radius (per end, allowing taper), and axial and radial resolution.
D3DXCreatePolygon() Create a polygon with the specified number of sides and length per side.
D3DXCreateSphere() Create a sphere with the specified radius and both latitudinal and longitudinal resolution.
D3DXCreateTorus() Create a torus with specified inner and outer radii, number of rings, and sides per ring.
D3DXCreateTeapot() Create a teapot.
D3DXCreateText() Create a mesh containing 3D text, using a specified string, extrusion depth, text quality, and a device context with a selected TrueType font.

See the SDK documentation for the syntax of these functions.

Mesh File Functions

Functions are provided that allow you to load objects from a mesh file into one or more ID3DXMesh objects, as well as attaining the material and texture information needed to render the objects.

There are two functions that can be used to create ID3DXMesh objects from X files.  The first, D3DXLoadMeshFromX(), accepts a file name and loads the requested X file into a new mesh.  If there are multiple objects in the file, they are collapsed into a single mesh.  The second loading function is D3DXLoadMeshFromXof(), which can be used to load an object from an open X file through a pointer to an IDirectXFileData interface passed to this function.

In addition to returning a new mesh object, these functions may return a buffer containing an array of one or more D3DXMATERIAL structures, which contain the materials and the file names of the textures to be used when rendering the mesh.  If more than one material is returned, as indicated by the material count returned by the mesh loading function, then there are multiple subsets in the mesh.  The loader will already have tagged each face with the appropriate attribute ID (subset number), ranging from 0 to the one less than the number of materials.  For more information on this, see Using Attributes to Define Subsets and  Rendering with DrawSubset() later in this article

Retrieving Mesh Information

Several methods are provided by the base class that allow you to retrieve information about the mesh:

ID3DXBaseMesh::GetNumFaces Returns the number of faces in the mesh.  Equal to the number of indices in the index buffer divided by three.
ID3DXBaseMesh::GetNumVertices Returns the number of vertices in the vertex buffer.
ID3DXBaseMesh::GetFVF Returns a DWORD containing one or more flexible vertex format flags defining the vertex format.
ID3DXBaseMesh::GetDeclaration Returns an array containing a declarator of the vertex format.
ID3DXBaseMesh::GetOptions Returns option flags used in mesh creation.

Mesh Storage

An ID3DXMesh object contains a vertex buffer and an index buffer.  You can access these using methods provided by the interface, either by getting a pointer to the buffer object that you can then lock and access, or by having the mesh object lock the buffer itself and return a pointer into the buffer for direct access.

Accessing the Vertex Buffer

The vertex buffer can be accessed through one of two methods: ID3DXMesh::GetVertexBuffer(), or ID3DXMesh::LockVertexBuffer().  The GetVertexBuffer() method returns a pointer to an IDirect3DVertexBuffer8 object, which you can then lock for access.  Be sure to release the vertex buffer after you are done with it, as the reference counter will be incremented by this call:

LPDIRECT3DVERTEXBUFFER pVBuf;
if (SUCCEEDED(pMesh->GetVertexBuffer(&pVBuf))) {
    VERTEX *pVert;
    if (SUCCEEDED(pVBuf->Lock(0,0,(BYTE **) &pVert,D3DLOCK_DISCARD) {
        DWORD numVerts=pMesh->GetNumVertices();
        for (int i=0;i<numVerts;i++) {
            *pVert=...
            pVert++;
        }
        pVBuf->Unlock();
    }
    pVBuf->Release();
}

A more convenient alternative is offered by the LockVertexBuffer() method, which handles the lock/unlock functionality, and returns a pointer directly to the vertex data:

VERTEX *pVert;
if (SUCCEEDED(pMesh->LockVertexBuffer(D3DLOCK_DISCARD,(BYTE **) &pVert) {
    ...
    pMesh->UnlockVertexBuffer();
}

Accessing the Index Buffer

Like the vertex buffer, the index buffer can be accessed through one of two methods: ID3DXMesh::GetIndexBuffer(), or ID3DXMesh::LockIndexBuffer().  The GetIndexBuffer() method returns a pointer to an IDirect3DIndexBuffer8 object, which you can then lock for access.  Be sure to release the index buffer after you are done with it, as the reference counter will be incremented by this call:

LPDIRECT3DINDEXBUFFER pIndBuf;
if (SUCCEEDED(pMesh->GetIndexBuffer(&pIndBuf))) {
    WORD pInd;
    DWORD numFaces=pMesh->GetFaces();
    if (SUCCEEDED(pIndBuf->Lock(0,numFaces*3*sizeof(WORD),(BYTE **) &pInd,D3DLOCK_DISCARD) {
        for (int i=0;i<numFaces;i++) {
            for (int j=0;j<3;j++) {
                *pInd=...
                pInd++;
            }
        }
        pIndBuf->Unlock();
    }
    pIndBuf->Release();
}

A more convenient alternative is offered by the LockIndexBuffer() method, which handles the lock/unlock functionality, and returns a pointer directly to the index data:

    WORD pInd;
if (SUCCEEDED(pMesh->LockIndexBuffer(D3DLOCK_DISCARD,(BYTE **) &pInd) {
    ...
    pMesh->UnlockIndexBuffer();
}

Using Attributes to Define Subsets

The ID3DXMesh interface allows the mesh to be divided into separate subsets, which can be rendered separately.  This is achieved through the use of attribute IDs, which are stored as a DWORD value for each face in the mesh.  These values specify an ordinal defining which group each face belongs to.  While it is common convention to use sequential numbers starting at zero, you may use any arbitrary values within the range of a DWORD to define your groups.

With the exception of the X file loading functions, upon initial creation of a mesh through one of the D3DX mesh functions all of the faces in the mesh will have an attribute ID of zero.  This facilitates rendering the entire mesh without having to set attribute IDs, if division of the mesh is not required.

The X file loading functions (D3DXLoadMeshFromX() and D3DXLoadMeshFromXof()) automatically set the attribute IDs, generating a group for each material / texture combination present.  These will be numbered sequentially, starting at group zero.

If you need to manually define subsets, you can use the ID3DXMesh::LockAttributeBuffer() and ID3DXMesh::UnlockAttributeBuffer() methods to access an array of DWORDs corresponding to the attribute IDs for each face.  For example, if we had a mesh with 150 faces, and we wanted to divide it into two groups comprised on the first 100 faces as group 0 and the remaining faces as group 1, we could do the following:

DWORD *id;
if (SUCCEEDED(pMesh->LockAttributeBuffer(D3DLOCK_DISCARD,&id))) {
    for (int i=0;i<100;i++) {
        *id=0;
        id++;
    }
    for (i=0;i<50;i++) {
        *id=1;
        id++;
    }
    pMesh->UnlockAttributeBuffer();
}

Note that while the example uses consecutive groups of faces, this does not have to be the case - you can assign IDs in any manner you wish.  However, this raises a performance issue, because the vertices and indices in a group may not be sequential.  Left like this, if we were to call on the mesh object to render a group, it would perform a linear search of the attribute IDs, causing a significant performance hit.

Fortunately, there is a ready solution to this.  By calling the Optimize() or OptimizeInPlace() methods of the mesh (further discussed in Optimizing Meshes) with the D3DXMESHOPT_ATTRSORT flag, the mesh is optimized for rendering of the groups defined by the attribute IDs.  In addition to sorting the vertices and indices, it defines D3DXATTRIBUTERANGE structures specifying the ranges of vertices and indices used by each subset.

For a more generalized function to defining mesh subsets, see Creating Subsets in ID3DXMesh.

Rendering with DrawSubset

The DrawSubset() method is used to render a specified subset of the mesh using the current renderstates.  A common usage is to iterate through the subsets and render them with different materials and textures, as specified in an X file.  For example, we can load an X file and its textures something like this:

LPD3DXBUFFER m_pbufMaterials;
D3DXMATERIAL **m_pMaterials;
LPDIRECT3DTEXTURE8 *m_ppTextures;
DWORD m_dwNumMaterials;
LPD3DXMESH m_pMesh;
...
hr = D3DXLoadMeshFromX( strMeshPath, D3DXMESH_MANAGED, m_pd3dDevice,
                        &m_pbufAdjacency, &m_pbufMaterials, &m_dwNumMaterials,
                        &m_pMesh );
if( FAILED(hr) )
    return hr;
   
m_pMaterials = (D3DXMATERIAL*)m_pbufMaterials->GetBufferPointer();
m_ppTextures = new LPDIRECT3DTEXTURE8[m_dwNumMaterials];

for( UINT i=0; i<m_dwNumMaterials; i++ )
{
    if( FAILED( D3DXCreateTextureFromFile( m_pd3dDevice,
                                           m_pMaterials[i].pTextureFilename,
                                           &m_ppTextures[i] ) ) )
        m_ppTextures[i] = NULL;
}

Then, we can iterate through the subsets and draw them with the appropriate materials and textures:

for( UINT i = 0; i < m_dwNumMaterials; i++ )
{
    m_pd3dDevice->SetMaterial( &m_pMaterials[i].MatD3D );
    m_pd3dDevice->SetTexture( 0, m_ppTextures[i] );
    m_pMesh->DrawSubset( i );
}

Optimizing Meshes

Meshes can be optimized for improved performance using the ID3DXBaseMesh::Optimize() and ID3DXBaseMesh::OptimizeInPlace() methods.  Optimize() creates a new mesh, while OptimizeInPlace() optimizes an existing mesh.

These functions take one or more of the following flags:

D3DXMESHOPT_ATTRSORT Reorders faces to group by attribute.
D3DXMESHOPT_COMPACT Removes unused vertices.
D3DXMESHOPT_IGNOREVERTS Optimize indices without changing vertices.
D3DXMESHOPT_SHAREVB Forces the cloned meshes to share vertex buffers. These meshes will have separate index buffers that index into the shared vertex buffer.
D3DXMESHOPT_STRIPREORDER Reorders faces to maximize length of adjacent triangles.
D3DXMESHOPT_VERTEXCACHE Reorders faces to increase the cache hit rate of vertex caches.

Note that the D3DXMESHOPT_STRIPREORDER and D3DXMESHOPT_VERTEXCACHE optimization flags are mutually exclusive.

Using Cloning

The ID3DXBaseMesh::Clone() and ID3DXBaseMesh::CloneFVF() functions allow you to make copies of an existing mesh object, that contain a copy of the mesh but may have a different vertex format.  This is useful when vertex components must be added to a mesh, as shown in the section Generating Normals, below.  Clone() and CloneFVF() take a declarator or a combination of FVF flags, respectively.

Generating Normals

Calculating normals for meshes stored in an ID3DMesh object is a snap, using the D3DX helper functions.  The D3DXComputeNormals() function takes a pointer to a mesh object, and computes the vertex normals by averaging the face normals for all the faces that share a vertex.

If the mesh vertex format does not currently include normals, then we can use the CloneMeshFVF() method to generate a new mesh interface with a vertex format that includes normals:

DWORD fvf=pMesh->GetFVF();
if (!(fvf&D3DFVF_NORMAL)) {
    fvf|=D3DFVF_NORMAL;
    ID3DXMesh *newMesh;
    if (FAILED(pMesh->CloneMeshFVF(0,fvf,lpDevice,&newMesh))) {
        // handle error and exit
        ...
    }
    pMesh->Release();
    pMesh=newMesh;
}
D3DXComputeNormals(pMesh);
 

Related Articles of Interest:

Creating Subsets in ID3DXMesh

This site, created by DirectX MVP Robert Dunlop and aided by the work of other volunteers, provides a free on-line resource for DirectX programmers.

Special thanks to WWW.MVPS.ORG, for providing a permanent home for this site.

Visitors Since 1/1/2000: Hit Counter
Last updated: 07/26/05.