SkinnedMesh DPlay Maze - Re-using The Revised D3D SkinnedMesh Sample Code In An Updated DPlay Maze Sample by (17 January 2003) | Return to The Archives

Introduction

With a revised SkinnedMesh DX 9.0 SDK sample in hand providing an encapsulation of the skin mesh code, one could now consider re-using this code. One such use would be to enable the DPlay Maze sample to render skinned characters for the players in the maze.
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.
Downloads for this article: article_mazeclient_skinned.zip (570k), d3dsaver-mixed.cpp (122k)
The DPlay Maze sample system is structured as follows:

> - A console-based server app.
> - A console-based client app.
> - A D3D-based client app.

The implementations are provided in the following folders:

> - MazeServer, containing the console-based maze-server specific source
> - MazeConsoleClient, containing the console-based maze-client specific source
> - MazeClient, containing the D3D-based maze-client specific source
> - MazeCommon, containing source shared among the 3 apps.

We are primarily concerned with the MazeClient D3D-based implementation here, I wont cover any of the other application components of the DPlay Maze system.
MazeClient can be broken into two parts, the common files and the MazeClient specific files. I am going to concentrate on the MazeClient specific files. Figure 1 below shows the project window for MazeClient, focusing on the D3D graphics rendering area.
Figure 1. MazeClient project view of D3D-based app specific files
Looking at this, one would hope that to change the rendering of characters to use the D3DX skinned mesh functionality that changing D3DGraphics.cpp would be sufficient. And one would be correct in this hope except for one detail. That detail is that the way the CD3DSkinMesh class is implemented, it depends on having a mixed-vertex-processing device, and thus changes need to occur in d3dsaver.cpp to re-use this class as-is. This led to the creation of a new d3dsaver-mixed module, as shown below in Figure 2 which shows the entire project, including the new d3dsaver-mixed.cpp and the skinnedmesh-mesh.cpp files.
Figure 2. MazeClient-Skinned project view of D3D-based app specific files
Note it is open to debate on whether this is the proper thing to do, to change the CreateDevice invocation used by the D3D screensaver framework, or whether the method CD3DSkinMesh->GenerateSkinnedMesh should be changed to generate the mesh differently instead of assuming the vertex processing type of the device. For this purpose, I chose to stay with the implementation of CD3DSkinMesh from the previous article, but this could easily be revisited and changed.
In the new module, d3dsaver-mixed.cpp, in both:

    HRESULT CD3DScreensaver::CreateFullscreenRenderUnit( RenderUnit* pRenderUnit )

and

    HRESULT CD3DScreensaver::CreateWindowedRenderUnit( RenderUnit* pRenderUnit )

The behavior flag used to control device vertex processing type:

    pRenderUnit->dwBehavior =  D3DWindowedModeInfo.dwBehavior;

is changed to:

    pRenderUnit->dwBehavior = D3DCREATE_MIXED_VERTEXPROCESSING;

With that change, we can now use the CD3DSkinMesh class to generate, animate, and render skinned mesh characters with changes to the d3dgraphics.cpp file only. Add skinnedmesh-mesh.cpp to the project first. Then, in d3dgraphics.cpp replace:

    class CD3DGraphics : public IMazeGraphics, public CD3DScreensaver
    {
    …
    CD3DFile*               m_pPlayerMesh;
    }

With:

    class CD3DGraphics : public IMazeGraphics, public CD3DScreensaver
    {
    …
    CD3DSkinMesh*           m_pPlayerMesh;
    LPDIRECT3DSTATEBLOCK9   m_pPixelState;
    LPDIRECT3DSTATEBLOCK9   m_pVertexState;
    D3DCAPS9                m_d3dCaps;
    D3DXVECTOR3             m_vObjectCenter;    // Center of bounding sphere of object
    FLOAT                   m_fObjectRadius;    // Radius of bounding sphere of object
    }

That replaces the CD3DFile mesh instance variable used to load tank.x from the resource fork, with the CD3DSkinMesh instance variable and a couple of control variables. The justification for the state blocks will come in a bit. The position, radius, and caps variables are all used by the app to control the skinned mesh character.
If you remember from the previous article the m_d3dCaps D3DCAPS9 variable is used to provide caps information to the skinned mesh implementation and is re-used in quite a few places. It might be desirable to factor out the absolute dependence on having access to the caps bits and the dependence on mixed vertex processing by developing separate skinned mesh methods for the case where the hardware can and cant handle enough bones to be done completely in hardware or needing software assist and thus requiring the mixed vertex processing device where required; but that would be a greater change to the SkinnedMesh sample than I deemed required to make the “encapsulation is good” point of this 2-part article series.
Since the DPlay MazeClient application uses the SDK screensaver framework, it’s a simple matter to go thru each overloaded method and change the m_pPlayerMesh code from CD3DFile handling to CD3DSkinMesh handling.
First though, we need some additional control variables, as in:

    //character orientation
    D3DXMATRIX  matScale,matTrans,matRot, matRender;

    //remote camera
    #define FACTOR      10

    D3DXVECTOR3 vSkyCamEyePt;
    D3DXVECTOR3 vLookatPt = D3DXVECTOR3( 0.0f, -0.5f, 0.0f );
    D3DXVECTOR3 vUpVec = D3DXVECTOR3( 0, 1.0f, 0 );

    FLOAT       fSkyCamTime = -360.0f;
    FLOAT       fSkyCamAngle;

    //additional keyboard UI
    bool bSkyCamera = FALSE;
    bool bPaused    = FALSE;

The character orientation control variables allow us to store the transformations necessary to place the character in the maze world and move it around correctly. The remote camera control variables enable a camera to give an “outside the maze” view, and the two bools let us add to the window procedure the ability to switch between the remote camera and to pause the simulation by keystrokes, ‘C’ for the camera, and space-bar or ‘ ‘ for pausing. Simply add to the window procedure the following:

    LRESULT CD3DGraphics::SaverProc( HWND hWnd , UINT message , WPARAM wParam , LPARAM lParam )
    {
    …
            if( wParam == ' ' )
                bPaused = !bPaused;
            if( wParam == 'C' )
                bSkyCamera = !bSkyCamera;
    }

to allow us to go “outside” the maze and look in as well as pause the simulation.
One note on pausing, I have added code to disable the “client side motion prediction” logic that “guesses” where the player will be, since when the player is paused it isn’t moving forward. Before I did this, the players would walk right out of the maze and off into the distance which was quite disconcerting. There is another pause issue like that, in that if the client doesn’t talk to the server to get updated position information for the player, after a certain point the client “decides” the player information is no good and stops rendering other players. This could be fixed, but I decided that was too large a change for the purposes of this discussion, however it is worth noting.
Now, in the constructor, replace:

    CD3DGraphics::CD3DGraphics()
    {
    ..
    //    m_pPlayerMesh  = new CD3DFile();
    }

with

    CD3DGraphics::CD3DGraphics()
    {
    ..
        m_pPlayerMesh  = new CD3DSkinMesh();
        D3DXMatrixScaling(&matScale, 0.00125f, 0.00125f, 0.00125f);
    }

The destructor needs no changes.
In CD3DGraphics::InitDeviceObjects() we need to capture pixel state, and in CD3DGraphics::RestoreDeviceObjects() we need to capture vertex state since the original rendering implementation does not handle the state changes needed to render the skinned characters and revert to the state needed to render the maze. This is accomplished as per below:

    HRESULT CD3DGraphics::InitDeviceObjects()
    {
    //pixel state for skinned characters
        m_pd3dDevice->BeginStateBlock();

        m_pd3dDevice->SetTextureStageState( 0, D3DTSS_COLOROP, D3DTOP_SELECTARG2 );
        m_pd3dDevice->SetTextureStageState( 0, D3DTSS_COLORARG1, D3DTA_TEXTURE );
        m_pd3dDevice->SetTextureStageState( 0, D3DTSS_ALPHAOP, D3DTOP_SELECTARG2 );
        m_pd3dDevice->SetTextureStageState( 0, D3DTSS_ALPHAARG1, D3DTA_TEXTURE );

        m_pd3dDevice->SetTextureStageState( 0, D3DTSS_COLORARG2, D3DTA_DIFFUSE );
        m_pd3dDevice->SetTextureStageState( 1, D3DTSS_COLOROP, D3DTOP_DISABLE );
        m_pd3dDevice->SetTextureStageState( 0, D3DTSS_ALPHAARG2, D3DTA_DIFFUSE );
        m_pd3dDevice->SetTextureStageState( 1, D3DTSS_ALPHAOP, D3DTOP_DISABLE );

        m_pd3dDevice->EndStateBlock(&m_pPixelState);
    }

In CD3DGraphics::RestoreDeviceObjects() we need to capture vertex state to allow the character rendering to restore the state required by the maze rendering

    HRESULT CD3DGraphics::RestoreDeviceObjects()
    {
    …
        //vertex state for skinned character
        m_pd3dDevice->BeginStateBlock();

        // Set projection matrix
        const float fov = 1.8f;
        const float znear = 0.1f;
        const float zfar = 30.0f;
        const float viewwidth = float(tan(fov*0.5f) ) * znear;
        const float viewheight = viewwidth * float(m_d3dsdBackBuffer.Height) /
                                             float(m_d3dsdBackBuffer.Width);
        D3DXMatrixPerspectiveLH( &m_Projection, viewwidth, viewheight, znear, zfar );
        m_pd3dDevice->SetTransform( D3DTS_PROJECTION, &m_Projection );

        // Set renderstates and lighting
        D3DXMATRIX matIden; D3DXMatrixIdentity( &matIden );
        m_pd3dDevice->SetTransform( D3DTS_WORLD, &matIden );
        m_pd3dDevice->SetRenderState( D3DRS_LIGHTING, TRUE );
        m_pd3dDevice->SetRenderState( D3DRS_AMBIENT, 0x333333 );
        m_pd3dDevice->SetSamplerState( 0, D3DSAMP_MINFILTER, D3DTEXF_LINEAR );
        m_pd3dDevice->SetSamplerState( 0, D3DSAMP_MAGFILTER, D3DTEXF_LINEAR );
        m_pd3dDevice->SetSamplerState( 0, D3DSAMP_MIPFILTER, D3DTEXF_LINEAR );
        m_pd3dDevice->SetRenderState( D3DRS_ZENABLE, TRUE );
        m_pd3dDevice->SetRenderState( D3DRS_ZWRITEENABLE, TRUE );
        m_pd3dDevice->SetRenderState( D3DRS_ZFUNC, D3DCMP_LESSEQUAL );
        m_pd3dDevice->SetRenderState( D3DRS_SPECULARENABLE, FALSE );
        m_pd3dDevice->SetRenderState( D3DRS_FOGENABLE, FALSE );

        D3DMATERIAL9 mtrl;
        D3DUtil_InitMaterial( mtrl, 1.0f, 1.0f, 1.0f );
        m_pd3dDevice->SetMaterial( &mtrl );

        // Init light
        D3DUtil_InitLight( m_Light, D3DLIGHT_POINT,
                           m_vCameraPos.x, m_vCameraPos.y, m_vCameraPos.z );
        m_Light.Direction = D3DXVECTOR3(0,0,0);
        m_Light.Range = 80.0f;
        m_Light.Attenuation0 = 1.0f;
        m_Light.Attenuation1 = 0.0f;
        m_Light.Attenuation2 = 1.0f;
        m_pd3dDevice->SetLight( 0, &m_Light );
        m_pd3dDevice->LightEnable( 0, TRUE );

        m_pd3dDevice->EndStateBlock(&m_pVertexState);

    …
    }

In CD3DGraphics::FrameMove() the remote camera used for debugging needs to be updated. That code is pretty simple, and isn’t relevant here so I skip it. Note the skinned character animation is not updated here since its running on a different clock from the core simulation, similar to how a real game application treats character animation time versus core simulation time. Also notice the adjustment to the maze simulation, where if the simulation isn’t paused the simulation time is advanced. This allows us to pause the game and switch to the remote camera with the player position remaining constant in the simulation.

    HRESULT CD3DGraphics::FrameMove()
    {
        if ( !bPaused )
            m_pMazeApp->FrameMove( m_fElapsedTime );

        //move sky cam
        UpdateRemoteCamera();

        return S_OK;
    }

In CD3DGraphics::Render() I have added code to render the local player or “self” at the current viewpoint, as shown below. Note I skip the remote camera code, if that is interesting to you check the source file.

    HRESULT CD3DGraphics::Render()
    {
    …
        DrawSkinnedCharacter(m_pPlayerMesh,
                             m_fElapsedTime, m_fObjectRadius,
                             m_vCameraPos, m_fCameraYaw,
                             m_d3dCaps, m_pd3dDevice);
        m_pVertexState->Apply();
        m_pPixelState->Apply();
    …
    }

Global Function DrawSkinnedCharacter does the heavy lifting, in performing the necessary translations and rotations to place the D3DX skinned character at the correct location in the maze with the correct orientation and then invoking CD3DSkinMesh methods FrameMove and Render to animate and render the skinned mesh character as shown below.

    void DrawSkinnedCharacter(CD3DSkinMesh* m_pMesh,
                              FLOAT m_fElapsedTime, FLOAT m_fObjectRadius,
                              D3DXVECTOR3 m_vPos, FLOAT m_fYaw,
                              D3DCAPS9 m_d3dCaps, LPDIRECT3DDEVICE9 m_pd3dDevice)
    {
        D3DXMatrixTranslation( &matTrans,
                m_vPos.x,
                m_vPos.y - 0.15f,
                m_vPos.z);
        D3DXMatrixRotationYawPitchRoll( &matRot,
                D3DX_PI-m_fYaw,
                0.0f,
                0.0f);
        D3DXMatrixMultiply( &matTrans, &matScale, &matTrans );
        D3DXMatrixMultiply( &matRender, &matRot, &matTrans );

        m_pMesh->FrameMove(matRender, m_fElapsedTime, m_fObjectRadius);
        m_pMesh->Render(m_d3dCaps, m_pd3dDevice, matRender);
    }

A parallel invocation of DrawSkinnedCharacter in CD3DGraphics::DrawPlayers() renders the skinned mesh character for all the remote players in the maze as shown below

    void CD3DGraphics::DrawPlayers()
    {
    …
        DrawSkinnedCharacter(m_pPlayerMesh,
                             m_fElapsedTime, m_fObjectRadius,
                             vPosGuess,AngleToFloat(aYawGuess),
                             m_d3dCaps,m_pd3dDevice);
        m_pVertexState->Apply();
        m_pPixelState->Apply();
    …
    }

In CD3DGraphics::InvalidateDeviceObjects() and CD3DGraphics::DeleteDeviceObjects() the code is identical, invoking the parallel methods on the m_pPlayerMesh instance variable so I don’t duplicate that code here.
With the code out of the way, its time for some pretty pictures. Figure 3 below shows a remote camera view of the local player before a remote player is within the visible cells. Figure 4 below shows a remote camera view as the remote player wanders into the view of the local player. Figure 5 shows a normal camera view as the remote player walks up to the local player. Figure 6 shows a remote camera view as the local and remote player get very close to each other. Figure 7 shows the corresponding normal camera view, which shows a nice frontal view of the skinned mesh character.
Figure 3.
Figure 4.
Figure 5.
Figure 6.
Figure 7.
This is much cooler looking than the current rendering of a basic tank .x file, leverages other SDK components by re-using the modular Direct3D SkinMesh sample, and helps to showcase how cool D3DX skinned mesh support is. By adding UI to enable the user to switch between the 4 D3DX skinning methods, the DPlay Maze sample could be updated to be the basis for a useful D3DX testbed application, especially relevant to ISVs with its multiple DirectX SDK component usage ( DPlay, D3D, D3DX ) and its D3D rendering consisting of the 3D world with characters and the 2D UI making it consistant with rendering patterns many real-world applications embody.
I hope this helps make the case for using the modular version of SkinnedMesh. And provides a useful update to the DPlay Maze sample.
Downloads: article_mazeclient_skinned.zip (570k), d3dsaver-mixed.cpp (122k)
Previous Article: Modular DX9 SkinnedMesh Article (01/16/2003)

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:

> - SkinnedMesh DPlay Maze - Re-using The Revised D3D SkinnedMesh Sample Code In An Updated DPlay Maze 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.