Modular D3D SkinnedMesh - Towards A Better Modularity Within The D3D SkinnedMesh Sample by (16 January 2003) | Return to The Archives

Introduction

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

About The Author

Phil Taylor started programming for Windows in 1987. His history with Windows multimedia starts with the Windows 3.0 MME conference, in November 1990, which launched multimedia for Windows. He worked in multimedia while in Silicon Valley during 1991-1995, and wrote one of the first Windows 3D books for Windows 3.1 in 1994 for Addison-Wesley. He worked with early copies of the GameSDK, which became DirectX, and left Silicon Valley in 1995 to work on and ship DirectX 2.0 games while at Dynamix including EarthSiege II and A-1O Tank-Killer II. He was hired by Alex St John and Microsoft in 1996 to evangelize Direct3D, helping its rise from DirectX 3 to DirectX 7. He then joined the DirectX team as SDK PM for DirectX 8.0, 8.1, and 9.0. He was also PM for Managed DirectX, released in DirectX 9.0 to provide DirectX support to .NET programmers. While in those roles, he authored the Driving DirectX column for MSDN, and single-handedly accounted for 50% of the MSDN DX web site content during 2000-2002. Phil recently left Microsoft to work at ATI. He continues to track Windows multimedia technology at a deep technical level, and writes the occasional DirectX article in his spare time.

Article Series:

> - Modular D3D SkinnedMesh - Towards A Better Modularity Within The D3D SkinnedMesh Sample

Copyright 1999-2008 (C) FLIPCODE.COM and/or the original content author(s). All rights reserved.
Please read our Terms, Conditions, and Privacy information.