Currently the skinning code in the DX 9.0 SDK SkinMesh sample is tightly coupled to the application, such that adding a skinned character to another sample is quite a bit of work. In this updated sample I show one take at providing such modularity, and follow that up by adding a skinned character to the DPlay Maze sample, as an illustration of how this modularity helps to allow re-use of SDK components.
In certain cases, I have elided parts of the code unrelated to the skinned character with elipses, as in ...
which is consistent with the style I used in the MSDN articles I wrote while at MS.
The SkinnedMesh sample contains one source file with all code contained in it.
At the top of the file, all types are defined, including some helper types:
// enum for various skinning modes possible
enum METHOD
{
D3DNONINDEXED,
D3DINDEXED,
SOFTWARE,
D3DINDEXEDVS,
D3DINDEXEDHLSLVS,
NONE
};
//-----------------------------------------------------------------------------
// Name: struct D3DXFRAME_DERIVED
// Desc: Structure derived from D3DXFRAME so we can add some app-specific
// info that will be stored with each frame
//-----------------------------------------------------------------------------
struct D3DXFRAME_DERIVED: public D3DXFRAME
{
D3DXMATRIXA16 CombinedTransformationMatrix;
};
//-----------------------------------------------------------------------------
// Name: struct D3DXMESHCONTAINER_DERIVED
// Desc: Structure derived from D3DXMESHCONTAINER so we can add some app-specific
// info that will be stored with each mesh
//-----------------------------------------------------------------------------
struct D3DXMESHCONTAINER_DERIVED: public D3DXMESHCONTAINER
{
LPDIRECT3DTEXTURE9* ppTextures; // array of textures, entries are NULL if no texture specified
// SkinMesh info
LPD3DXMESH pOrigMesh;
LPD3DXATTRIBUTERANGE pAttributeTable;
DWORD NumAttributeGroups;
DWORD NumInfl;
LPD3DXBUFFER pBoneCombinationBuf;
D3DXMATRIX** ppBoneMatrixPtrs;
D3DXMATRIX* pBoneOffsetMatrices;
DWORD NumPaletteEntries;
bool UseSoftwareVP;
DWORD iAttributeSW; // used to denote the split between SW and HW if necessary for non-indexed skinning
};
|
It really should not be necessary for the application to have any visibility into what should be internal types to the skinned character implementation, so the D3DXFRAME_DERIVED and D3DXMESHCONTAINER_DERIVED types only serve to make initial understanding more difficult.
Similarly, the major types are defined as follows:
class CMyD3DApplication;
//-----------------------------------------------------------------------------
// Name: class CAllocateHierarchy
// Desc: Custom version of ID3DXAllocateHierarchy with custom methods to create
// frames and meshcontainers.
//-----------------------------------------------------------------------------
class CAllocateHierarchy: public ID3DXAllocateHierarchy
{
public:
STDMETHOD(CreateFrame)(THIS_ LPCTSTR Name, LPD3DXFRAME *ppNewFrame);
STDMETHOD(CreateMeshContainer)(THIS_ LPCTSTR Name, LPD3DXMESHDATA pMeshData,
LPD3DXMATERIAL pMaterials, LPD3DXEFFECTINSTANCE pEffectInstances, DWORD NumMaterials,
DWORD *pAdjacency, LPD3DXSKININFO pSkinInfo,
LPD3DXMESHCONTAINER *ppNewMeshContainer);
STDMETHOD(DestroyFrame)(THIS_ LPD3DXFRAME pFrameToFree);
STDMETHOD(DestroyMeshContainer)(THIS_ LPD3DXMESHCONTAINER pMeshContainerBase);
CAllocateHierarchy(CMyD3DApplication *pApp) :m_pApp(pApp) {}
public:
CMyD3DApplication* m_pApp;
};
//-----------------------------------------------------------------------------
// Name: class CMyD3DApplication
// Desc: Application class. The base class (CD3DApplication) provides the
// generic functionality needed in all Direct3D samples. CMyD3DApplication
// adds functionality specific to this sample program.
//-----------------------------------------------------------------------------
class CMyD3DApplication : public CD3DApplication
{
TCHAR m_strMeshFilename[MAX_PATH];
TCHAR m_strInitialDir[MAX_PATH];
LPD3DXFRAME m_pFrameRoot;
LPD3DXANIMATIONCONTROLLER m_pAnimController;
CD3DFont* m_pFont;
CD3DFont* m_pFontSmall;
CD3DArcBall m_ArcBall; // mouse rotation utility
D3DXVECTOR3 m_vObjectCenter; // Center of bounding sphere of object
FLOAT m_fObjectRadius; // Radius of bounding sphere of object
METHOD m_SkinningMethod; // Current skinning method
D3DXMATRIXA16* m_pBoneMatrices;
UINT m_NumBoneMatricesMax;
LPDIRECT3DVERTEXSHADER9 m_pIndexedVertexShader[4];
LPD3DXEFFECT m_pEffect;
D3DXMATRIXA16 m_matView;
protected:
HRESULT OneTimeSceneInit();
HRESULT InitDeviceObjects();
HRESULT ConfirmDevice( D3DCAPS9* pCaps, DWORD dwBehavior,
D3DFORMAT adapterFormat, D3DFORMAT backBufferFormat );
HRESULT RestoreDeviceObjects();
HRESULT InvalidateDeviceObjects();
HRESULT DeleteDeviceObjects();
HRESULT Render();
HRESULT FrameMove();
HRESULT FinalCleanup();
LRESULT MsgProc( HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam );
void DrawMeshContainer( LPD3DXMESHCONTAINER pMeshContainerBase, LPD3DXFRAME pFrameBase );
void DrawFrame( LPD3DXFRAME pFrame );
HRESULT SetupBoneMatrixPointersOnMesh( LPD3DXMESHCONTAINER pMeshContainer );
HRESULT SetupBoneMatrixPointers( LPD3DXFRAME pFrame );
void UpdateFrameMatrices( LPD3DXFRAME pFrameBase, LPD3DXMATRIX pParentMatrix );
void UpdateSkinningMethod( LPD3DXFRAME pFrameBase );
public:
CMyD3DApplication();
HRESULT GenerateSkinnedMesh( D3DXMESHCONTAINER_DERIVED *pMeshContainer );
};
|
Note the coupling of the application class to the CAllocateHierarchy allocation class. There is no reason the skinned character allocators should be coupled to the application.
Next, look at the definition of the CMyD3DApplication class. Member variables and methods to manipulate the skinned character are embedded into the application class. As in:
LPD3DXFRAME m_pFrameRoot;
LPD3DXANIMATIONCONTROLLER m_pAnimController;
METHOD m_SkinningMethod; // Current skinning method
D3DXMATRIXA16* m_pBoneMatrices;
UINT m_NumBoneMatricesMax;
LPDIRECT3DVERTEXSHADER9 m_pIndexedVertexShader[4];
void DrawMeshContainer( LPD3DXMESHCONTAINER pMeshContainerBase, LPD3DXFRAME pFrameBase );
void DrawFrame( LPD3DXFRAME pFrame );
HRESULT SetupBoneMatrixPointersOnMesh( LPD3DXMESHCONTAINER pMeshContainer );
HRESULT SetupBoneMatrixPointers( LPD3DXFRAME pFrame );
void UpdateFrameMatrices( LPD3DXFRAME pFrameBase, LPD3DXMATRIX pParentMatrix );
void UpdateSkinningMethod( LPD3DXFRAME pFrameBase );
HRESULT GenerateSkinnedMesh( D3DXMESHCONTAINER_DERIVED *pMeshContainer );
|
And in the CD3DApplication methods code as shown below.
The CD3DApplication constructor initializes what could properly be internal variables of an encapsulating class:
CMyD3DApplication::CMyD3DApplication()
{
…
m_pAnimController = NULL;
m_pFrameRoot = NULL;
…
m_SkinningMethod = D3DNONINDEXED;
m_pBoneMatrices = NULL;
m_NumBoneMatricesMax = 0;
}
|
The OneTimeSceneInit method does nothing so I skip it.
The FrameMove method exposes the internals of the animation controller, which could be encapsulated:
HRESULT CMyD3DApplication::FrameMove()
{
…
if (m_pAnimController != NULL)
m_pAnimController->SetTime(m_pAnimController->GetTime() + m_fElapsedTime);
UpdateFrameMatrices(m_pFrameRoot, &matWorld);
return S_OK;
}
|
The Render method is a bit cleaner, with a method invocation, as all the CD3DApplication methods should look like, but the effect calls could be further hidden:
HRESULT CMyD3DApplication::Render()
{
// Clear the backbuffer
m_pd3dDevice->Clear( 0L, NULL, D3DCLEAR_TARGET|D3DCLEAR_ZBUFFER,
0x000000ff, 1.0f, 0L );
m_pEffect->SetMatrix( "mViewProj", &matProj);
m_pEffect->SetVector( "lhtDir", &vLightDir);
…
// Begin the scene
if( SUCCEEDED( m_pd3dDevice->BeginScene() ) )
{
DrawFrame(m_pFrameRoot);
…
}
return S_OK;
}
|
The InitDeviceObjects method again invokes a series of methods to set up parts of the skinned mesh character which makes it hard to extend this app to do multiple characters. Also, its hard to see that the effect code and the mesh code are coupled and should be both encapsulated.
HRESULT CMyD3DApplication::InitDeviceObjects()
{
TCHAR strMeshPath[MAX_PATH];
TCHAR strSkinnedMeshFXPath[MAX_PATH];
HRESULT hr;
CAllocateHierarchy Alloc(this);
…
return hr;
hr = D3DXLoadMeshHierarchyFromX(strMeshPath, D3DXMESH_MANAGED, m_pd3dDevice, &Alloc, NULL, &m_pFrameRoot, &m_pAnimController);
if (FAILED(hr))
return hr;
hr = SetupBoneMatrixPointers(m_pFrameRoot);
if (FAILED(hr))
return hr;
hr = D3DXFrameCalculateBoundingSphere(m_pFrameRoot, &m_vObjectCenter, &m_fObjectRadius);
if (FAILED(hr))
return hr;
// Create Effect for HLSL skinning
// Find the vertex shader file
if( FAILED( hr = DXUtil_FindMediaFileCb( strSkinnedMeshFXPath,
sizeof(strSkinnedMeshFXPath), _T("SkinnedMesh.fx") ) ) )
return hr;
hr = D3DXCreateEffectFromFile( m_pd3dDevice, strSkinnedMeshFXPath, NULL,
NULL, D3DXSHADER_DEBUG, NULL, &m_pEffect, NULL);
if (FAILED(hr))
return hr;
return S_OK;
}
|
The RestoreDeviceObjects method exposes setting up the vertex shaders for one particular method of skinning, which should be internals of the skinned mesh character. The app should initialize a skinned mesh character, and ask for a rendering of a particular method, but the app should not have intimate knowledge of the internals of a skinned character. Note the blocks for the effect and the shaders are separated, making it harder to see they are coupled.
HRESULT CMyD3DApplication::RestoreDeviceObjects()
{
HRESULT hr;
..
// Restore Effect
if ( m_pEffect )
m_pEffect->OnResetDevice();
…
// load the indexed vertex shaders
for (DWORD iInfl = 0; iInfl < 4; ++iInfl)
{
LPD3DXBUFFER pCode;
// Assemble the vertex shader file
if( FAILED( hr = D3DXAssembleShaderFromResource(NULL, MAKEINTRESOURCE(IDD_SHADER1 + iInfl), NULL, NULL, 0, &pCode, NULL ) ) )
return hr;
// Create the vertex shader
if( FAILED( hr = m_pd3dDevice->CreateVertexShader( (DWORD*)pCode->GetBufferPointer(),
&m_pIndexedVertexShader[iInfl] ) ) )
{
return hr;
}
pCode->Release();
}
return S_OK;
}
|
The InvalidateDeviceObjects method performs the reverse, freeing the vertex shaders, and again exposes setting up the vertex shaders for one particular method of skinning, which should be internals of the skinned mesh character.
HRESULT CMyD3DApplication::InvalidateDeviceObjects()
{
..
// release the vertex shaders
for (DWORD iInfl = 0; iInfl < 4; ++iInfl)
{
SAFE_RELEASE(m_pIndexedVertexShader[iInfl]);
}
// Invalidate Effect
if ( m_pEffect )
m_pEffect->OnLostDevice();
return S_OK;
}
|
The DeleteDeviceObjects method uses the CAllocateHierarchy helper class and deletes core skinned mesh variables.
HRESULT CMyD3DApplication::DeleteDeviceObjects()
{
CAllocateHierarchy Alloc(this);
..
D3DXFrameDestroy(m_pFrameRoot, &Alloc);
SAFE_RELEASE(m_pAnimController);
SAFE_RELEASE(m_pEffect);
return S_OK;
}
|
Similarly, the usage in the window procedure can be simplified a bit, but I don’t show it here due to space considerations.
In addition, I have skipped the implementation of the CAllocateHierarchy class which is embedded in the application. As are various skinned mesh specific methods like GenerateSkinnedMesh. That drastically complicates developers learning how to use this functionality, as they must wade thru a raft of code to get to the "good bits".
Contrast that with a modular implementation.
First, we break up the single app source file, skinnedmesh.cpp, into:
Skinnedmesh-mesh,cpp, the CD3DSkinMesh class implementation
Skinnedmesh-mesh.h, the CD3DSkinMesh class definition
Skinnedmesh-app.h, the CD3DApplication class definitions
Skinnedmesh-app.cpp, the CD3DApplication class implementation
Skinnedmesh-mesh.h defines the CD3DSkinMesh class as follows:
class CD3DSkinMesh
{
public:
CD3DSkinMesh();
//CD3DApp mini-API
HRESULT OneTimeSceneInit();
HRESULT InitDeviceObjects(TCHAR* strMeshPath,
TCHAR* strSkinnedMeshPath,
D3DCAPS9 m_d3dCaps,
LPDIRECT3DDEVICE9 m_pd3dDevice,
D3DXVECTOR3* m_vObjectCenter,
FLOAT* m_fObjectRadius);
HRESULT RestoreDeviceObjects(LPDIRECT3DDEVICE9 m_pd3dDevice);
HRESULT FrameMove(D3DXMATRIXA16 matWorld, float m_fElapsedTime);
HRESULT Render(D3DCAPS9 m_d3dCaps,LPDIRECT3DDEVICE9 m_pd3dDevice,
D3DXMATRIXA16 m_matView, D3DXMATRIXA16 m_matProj,
D3DXVECTOR4 vLightDir);
HRESULT InvalidateDeviceObjects();
HRESULT DeleteDeviceObjects(D3DCAPS9 m_d3dCaps);
HRESULT FinalCleanup();
//public helpers
METHOD getSkinningMethod() { return m_SkinningMethod; };
LPD3DXFRAME getFrameRoot() { return m_pFrameRoot; };
HRESULT GenerateSkinnedMesh( D3DXMESHCONTAINER_DERIVED *pMeshContainer,
D3DCAPS9 m_d3dCaps,
LPDIRECT3DDEVICE9 m_pd3dDevice,
METHOD m_SkinningMethod );
void UpdateSkinningMethod( LPD3DXFRAME pFrameBase,
D3DCAPS9 m_d3dCaps,
LPDIRECT3DDEVICE9 m_pd3dDevice,
METHOD m_SkinningMethod );
protected:
LPD3DXFRAME m_pFrameRoot;
LPD3DXANIMATIONCONTROLLER m_pAnimController;
METHOD m_SkinningMethod; // Current skinning method
D3DXMATRIXA16* m_pBoneMatrices;
UINT m_NumBoneMatricesMax;
LPDIRECT3DVERTEXSHADER9 m_pIndexedVertexShader[4];
LPD3DXEFFECT m_pEffect;
void DrawMeshContainer( LPD3DXMESHCONTAINER pMeshContainerBase,
LPD3DXFRAME pFrameBase,
D3DCAPS9 m_d3dCaps,LPDIRECT3DDEVICE9 m_pd3dDevice,D3DXMATRIXA16 m_matView );
void DrawFrame( LPD3DXFRAME pFrame,
D3DCAPS9 m_d3dCaps,
LPDIRECT3DDEVICE9 m_pd3dDevice,
D3DXMATRIXA16 m_matView );
HRESULT SetupBoneMatrixPointersOnMesh( LPD3DXMESHCONTAINER pMeshContainer );
HRESULT SetupBoneMatrixPointers( LPD3DXFRAME pFrame );
void UpdateFrameMatrices( LPD3DXFRAME pFrameBase, LPD3DXMATRIX pParentMatrix );
};
|
Note this class defines both the internal methods to generate, animate, and render the skinned mesh character as well as the CD3DApplication “mini-API” methods for the CD3DApplication to call, as discussed in my previous MSDN articles on using the DX SDK sample framework.
This makes it very easy to use a CD3DSkinMesh skinned character. Define an instance variable in the CD3DApplication as follows:
CD3DSkinMesh* m_pSkinMesh;
|
Add some control variables for the app to control the character:
METHOD m_AppSkinningMethod;
D3DXVECTOR3 m_vObjectCenter;
FLOAT m_fObjectRadius;
|
And the CD3DApplication code then looks like below.
The CD3DApplication constructor
CMyD3DApplication::CMyD3DApplication()
{
..
m_pSkinMesh = new CD3DSkinMesh();
m_AppSkinningMethod = D3DNONINDEXED;
}
|
The OneTimeSceneInit method does nothing so I again skip it.
The FrameMove method now simply calls an instance method on the member variable.
HRESULT CMyD3DApplication::FrameMove()
{
…
m_pSkinMesh->FrameMove(matWorld, m_fElapsedTime);
}
|
The Render method also now simply calls an instance method on the member variable
HRESULT CMyD3DApplication::Render()
{
…
m_pSkinMesh->Render(m_d3dCaps,m_pd3dDevice,m_matView, matProj, vLightDir);
}
|
The InitDeviceObjects method still locates the paths, since that’s an attribute of the app, eg where to load media from, but then uses the skinned mesh instance variable to do all the heavy lifting.
HRESULT CMyD3DApplication::InitDeviceObjects()
{
TCHAR strMeshPath[MAX_PATH];
TCHAR strSkinnedMeshFXPath[MAX_PATH];
HRESULT hr;;
// Initialize the font's internal textures
m_pFont->InitDeviceObjects( m_pd3dDevice );
m_pFontSmall->InitDeviceObjects( m_pd3dDevice );
// Load the mesh from the specified file
hr = DXUtil_FindMediaFileCb( strMeshPath, sizeof(strMeshPath), m_strMeshFilename );
if (FAILED(hr))
return hr;
// Find the vertex shader file
if( FAILED( hr = DXUtil_FindMediaFileCb( strSkinnedMeshFXPath,
sizeof(strSkinnedMeshFXPath), _T("SkinnedMesh.fx") ) ) )
return hr;
//Generate Skinned mesh
m_pSkinMesh->InitDeviceObjects( strMeshPath,strSkinnedMeshFXPath,
m_d3dCaps, m_pd3dDevice,
&m_vObjectCenter, &m_fObjectRadius );
return S_OK;
}
|
The RestoreDeviceObjects method now simply calls the instance method to restore the effect file and the shaders.
HRESULT CMyD3DApplication::RestoreDeviceObjects()
{
…
// restore effect, load the indexed vertex shaders
hr = m_pSkinMesh->RestoreDeviceObjects(m_pd3dDevice);
}
|
The InvalidateDeviceObjects method presents a slightly simpler view, although in this case the size of the method doesn’t make this as critical
HRESULT CMyD3DApplication::InvalidateDeviceObjects()
{
…
// release the vertex shaders, invalidate the effec
m_pSkinMesh->InvalidateDeviceObjects();
return S_OK;
}
|
The DeleteDeviceObjects method again presents a slightly simpler view, although in this case the size of the method doesn’t make this as critical
HRESULT CMyD3DApplication::DeleteDeviceObjects()
{
..
m_pSkinMesh->DeleteDeviceObjects(m_d3dCaps);
return S_OK;
}
|
Using a skinned mesh character is now very clean, and very easy to see how a 2nd skinned character could be added by simply defining a 2nd instance variable and invoking 2 sets of calls in each CD3DApplication method.
This is much better in terms of:
teaching folk how to use the skinned mesh functionality without having to understand all the details, including the underdocumented ones.
enabling re-using SDK functionality in other SDK apps
enabling re-using SDK functionality in external developers apps
It would be illustrative to attempt to add a 2nd character in the app in each form, and contrast those 2 attempts. That effort and the result would highlight the superiority of the modular version.
When additional documentation is forthcoming on the intricacies of the new mesh hierarchy, the new allocation scheme, and the new controller scheme then the CD3DSkinMesh class is a self-contained unit for further articles, instead of the previous scheme where the extreme coupling to the application would entail talking about application details instead of focusing on the skinned mesh details.
Some further cleanup could be done, for instance the setting of the vertex shader constants for the D3DINDEXEDVS skinning method. These are view matrix values that properly have to come from the app, but perhaps another method for the CD3DSkinMesh class could clean that up and properly encapsulate this.
I hope this helps make the case for providing a modular version of SkinnedMesh. In the next article, I will show how to take this new source file and the CD3DSkinMesh class and add skinned mesh characters to the DPlay Maze sample. That should further make the case for how more effective the modular version is.
Download: article_skinnedmesh_modular.zip (85k)
Updated Download (02/17/2003): article_skinnedmesh_modular_normalmaps.zip (350k)
The updated download above was made available by David Jurado Gonz, a reader who kindly fixed the VS.NET errors.
His fixes include:
Corrected decorated names of some function arguments.
No more aligned args in function arguments.
It now compiles under VC++.NET.
It can be switched the skinning method (like the original sample).
The update can be found on David's site as well, at:
http://atc1.aut.uah.es/~infind/Programming/Programming.htm |