DirectX 9 is good! It allows you to import .x files into your game projects unlike newer versions of DirectX. Why not ask yourself, "What do I want in my game?" well, the answer for me is 3D game characters and a nice 3D scene. I am writing this tutorial so that you may follow the steps required to create such a scene. Well at least the characters. I’ll leave the scene up to you. The scene will serve as an area for our characters to walk around. Unfortunately 3D studio max is not free for many people. I have no answer for this because I was unsuccessful in using Blender for animations; maybe now that I have experience I'd manage however if you are able to get hold of max, then great! There's nothing stopping you. There are plugin exporters available that do the job. And the aim of this tutorial is purely for teaching you how to create 3D DirectX games using any of the exporters available and as a refresher course for myself. DirectX is not easy but it is a lot of fun. So let's get started!
Let's start from the beginning: Creating a window. Setup a new empty Win32 project in Visual Studio; you will need a version of the DirectX SDK and to link in the include and library directories of the SDK into your project. This is done via project properties. In the linker->Input Additional dependencies add d3d9.lib and d3dx9.lib.
First we will include <windows.h> and create a WinMain() function and a Windows procedure WinProc(). WinMain is the entry point of the program that is executed first in a standard Windows application. Our WinMain function is outlined as:
And the outline of our WndProc is:
Place these declarations at the top of the cpp file after includes so that they can be referenced throughout other functions.
Now we will want an object to handle various application events such as game initialization, updating the game and cleaning of resources. We will create a simple Game object for this. Within the Init() function we will create a window and hold a handle to it, m_mainWindow:
Don't forget the semi-colon after class declaration.
We create the window class that describes our window and register it with the operating system in the Game::Init() function. We will use a basic window class for this:
Register the window class with the OS:
And finally, create the window; We use WS_OVERLAPPEDWINDOW for a standard minimize, maximize and close options:
In the WinMain function we must create an instance of Game and call the Init function to initialize the window:
Every Windows application has a message pump. The message pump is a loop that forwards Windows messages to the window procedure by "looking" or peeking at the internal message queue. We "look" at a message within our message pump, remove it from the queue with PM_REMOVE and send it to the window procedure typically for a game. PeekMessage is better than GetMessage in the case where we want to execute game code because it returns immediately. GetMessage on the other hand would force us to wait for a message.
Now for our message pump; place this in the WinMain function:
At the end of WinMain, call the application cleanup function and return with a message code:
Now for our window procedure implementation; We will use a basic implementation to handle messages:
Check that the program works so far. You should be able to build the project and run it and display the window. If all goes well, we can start with Direct3D.
Now that we have created a window, let's setup Direct3D and render a background colour to the screen. We will add to our Game object so we can easily work with different parts of our application in a clear and secure manner such as creation, updating the game and application destruction. With our game object we can intialize Direct3D in an Init() function and update our game with Game::Update() and clean up DirectX resources with Game::Cleanup(). Name this class to suit your preference. I will call it Game. The m_ prefix is Hungarian notation and was developed by someone called Charles Simonyi, a Microsoft programmer. It is short for member variable. Other prefixes might be nCount, n for number, s_ for static variables. Anyway here is the Game object with added functions and variables specific to Direct3D:
Further reading recommended: Character Animation with Direct3D - Carl Granberg
We could do with a helper function to release, free up memory used by DirectX COM objects in a safe manner. We use a generic template class for this or you could create a macro for the same purpose:
Don't forget to include it in your main cpp file. #include "dxhelper.h"
In our Init function we fill out the present parameters and setup Direct3D;
Let's go over the parameters quickly; BackBufferWidth specifies the width of the offscreen buffer, the back buffer. The offscreen buffer is a memory segment that we render a scene to and it becomes the on-screen buffer, what we actually see on the screen when it is flipped with the front buffer, the on-screen buffer. Similarly with the BackBufferHeight. We specify 8 bits for alpha, 8 bits for red, 8 bits for green and 8 bits for blue so this is 32-bit true colour allocated for our buffer. We specify that there will only be one back buffer; the reason we might have two back buffers is to speed up the rendering to the screen e.g. while the onscreen buffer is diplayed you could prepare two offscreen buffers so you could flip one and then the next is ready to be displayed so you could flip the other immediately. Multisampling is a technique that improves the quality of an image but takes up more processing time. So we specify D3DMULTISAMPLE_NONE. We specify SWAPEFFECT_DISCARD to remove the oncreen buffer when it is swapped with a backbuffer; so the backbuffer becomes the front and the old front is deleted. m_pp.hDeviceWindow is the window to render to. Windowed can be true, displaying the scene in a window, or false to display the scene fullscreen. We set m_pp.EnableAutoDepthStencil to true to enable depth bufferring; where a depth buffer is used effectively causes 3D objects in the world to overlap correctly; a z value will be specified for each pixel of the depth buffer and this will effectively enable depth testing which is basically a test comparison of the z value of each pixel; If a pixel's z value is less, nearer to the screen, than another pixels z value, it is closer to the screen so will be written to the offscreen buffer. We use a default refresh rate and immediate buffer swapping. The other type of buffer swapping could be D3DPRESENT_INTERVAL_DEFAULT, default interval, which might be the screen refresh rate.
Next we create the device with a call to d3d9->CreateDevice().
We need to specify a global Direct3D device object so that we can use the device anywhere in the program.
Then the CreateDevice() function will create the device object. We use the default display adapter and then D3DDEVTYPE_HAL to use the hardware abstraction layer, hardware graphics acceleration for our rendering of the scene. This is much faster as opposed to software rendering. Hardware means the graphics card in this case. We specify to use hardware vertex processing too. And then we pass the present parameters structure that describes properties of the device to create. And lastly we pass the g_pDevice variable to retrieve a handle to the newly created device.
Now, before we continue with animation we must do a bit of device handling. For example if the user does ALT+TAB our device might be lost and we need to reset it so that our resources are maintained. You'll notice we have onDeviceLost and onDeviceGained functions in our game object. These will work hand-in-hand with the deviceStatus variable to handle a device lost situation. After a device has been lost we must reconfigure it with onDeviceGained().
To check for a lost device we check the device cooperative level for D3D_OK. If the cooperative level is not D3D_OK then our device can be in a lost state or in a lost and not reset state. This should be regularly checked so we will put the code in our Game::Update() function.
Our OnDeviceLost function and OnDeviceGained look like:
When the program starts we have to set the m_deviceStatus variable so that we can use it. So in our Game::Game() constructor set the variable:
Now we need to implement the Render and Cleanup functions. We will just clear the screen and free up memory in these functions.
Finally, we want to render our scene and update it. Remember the Update() method handles device lost events.
We want our game to run with a consistent frame rate so that Update() updates the game at the same frame rate on all PCs. If we just updated the game inconsistently without frame rate, our game would be faster on faster computers and slower on slower computers and also might speed up or slow down if we do not specify a frame rate.
Therefore we use GetTickCount() and pass a change in time to our Update() function. GetTickCount() returns the number of milliseconds that have elapsed since the system was started. We record the start time, and subtract this from the current time of each iteration of the loop. We then set the new start time; repeat this calculation and get the change in time and pass this value to Update(deltaTime).
Our message loop now is defined as:
Now we have a complete Direct3D framework we can begin with loading an animated model. I will supply you the code so far and then talk about how we can load an animation hierarchy:
Hopefully you have found the tutorial useful up until now. In this part I will start by setting the perspective projection and then cover the steps needed to load and animate a mesh. We will be animating our mesh object with hardware acceleration. This involves creating an HLSL .fx file.
Requirements
You will need a method of producing an .x file with animation info. For this I will be using 3d studio max 2014 and an exporter plugin. There are a number of plugins available. If you can't get hold of this then you may have to use Blender, however I don't know if the .x files produced with Blender are sufficient for this tutorial. And I don't have the incentive to find out. So I leave this to the reader. Hopefully you will find a way to produce an .x file with an animation hierarchy in a suitable format for loading into a DirectX application.
Setting up a perspective projection
We have three matrices to create. World, View and Projection. The World matrix represents the world transformation for a set of objects such as meshes. This is the position of these meshes in the game world. The view matrix represents the camera. It has a lookAt component, an Up component that specifies the up direction of the world, y or z. And also an eye component that is the "look from" component; the position of the camera.
Our perspective matrix defines the "fish eye" factor or field-of-view, which is the angle range we can see from our eye. This is typically 45 degrees. We must also specify the aspect ratio, which is the number of width pixels in correspondence with the number of height pixels; the ratio of width to height. Typically set this to WINDOW_WIDTH / WINDOW_HEIGHT. And lastly, the z near plane and z far plane must be specified, these are the cut-off points of our view. Typically the z near plane of a projection frustum is z=1.0f. Z here is the point of cut-off; anything closer to the eye than 1.0 is cut-off, not rendered. Similarly anything further away than zfar is cut-off, not rendered too.
We add the describing transform states with the D3DTS_ constants in the device SetTranform function passing each tranform matrix as parameters. This is done in our Render() function.
Now that we have the scene set up, the basic camera and projection ready - it's time to load a model with animation.
Loading an Animation Hierarchy
In real life humans and animals have bones. Likewise, our game characters have bones. A bone in DirectX is represented with the D3DXFRAME structure. You may just think of this structure as a bone. A bone may have a parent and a sibling. For example a parent bone might be the upper arm and a child bone might be the lower arm. When you move your upper arm, your lower arm, forearm, moves with it. That is why a forearm is the child. If you move your lower arm however, the upper arm is not affected. And hence that is why the upper arm is a parent. Each bone may have a transformation, a rotation and position for example. A sibling is just a bone that shares the same parent as another bone(D3DXFRAME). Let's look at the D3DXFRAME structure to see how we can represent a bone in code.
A bone has a name, a transformation, and optionally a mesh associated with it; and optionally a sibling and a child. With this structure we can represent a whole hierarchy or skeleton in other words. By associating sibling bones and child bones we can link all the D3DXFRAME bones together, which in turn will be a representation of a skeleton such as a human or an animal.
The names of the bones could be "right leg", "left leg", "right forearm" for example to give you an idea. The transformations defined in a basic D3DXFRAME are the local bone transformations. These are local to the bones in contrast with the world transformations, which are the actual transformations in the world; the final transforms. In our game we require the world transformations of the bones to render them at the exact positions in the world; so we will extend this structure to include them. For easy reference we will call the new structure Bone.
The WorldMatrix is the combination of a bone's local matrix with its parent's WorldMatrix. This is simply a multiplication of the two so that the child bone inherits the transformation of its parent.
We must traverse the bone hierarchy to calculate each of the new matrices. This is done with the following recursive function:
Then to calculate all of the bones matrices that make up the skeleton we call CalculateWorldMatrices on the root node, the parent bone of the hierarchy with the identity matrix as the parent. This will traverse all children and siblings of those children and build each of the world matrices.
To load a bone hierarchy from an .x file we must implement the ID3DXAllocateHierarchy interface. This interface defines 4 functions that we must implement ourselves. Then we pass the implemented object in a call to D3DXLoadMeshHierarchyFromX(). That will create a skeleton of a character. And after we have the skeleton we can apply skinning by using an HLSL effect file to make our character effectively have skin.
To implement the functions declared in ID3DXAllocateHierarchy we provide a new class definition that inherits from it. We will call this class AllocateHierarchyImp. The new class and inherited functions effectively looks like this:
In these functions we handle the allocation of memory for bones and the deallocation of memory we allocated ourselves for bones and associated bone meshes. CreateFrame is fairly simple; we just allocate memory for a bone with new Bone; and allocate memory for the name of the bone with new char[strlen(Name)+1];.
CreateMeshContainer on the other hand is more complicated. Bear in mind these functions are called by the D3DXLoadMeshHierarchyFromX() function. Information about the mesh we are loading is passed to these functions.
Before I jump into the code for these functions we should consider a class that will provide the mechanism of loading a skinned mesh separately for each of our animated characters. Thus we will create a class called SkinnedMesh that caters for each individual character. This class is outlined as:
SkinnedMesh.h
We need to define a mesh container structure so that we can hold a mesh associated with each bone and prepare for skinning the mesh. Like with the Bone when we extended the D3DXFRAME, we extend the D3DXMESHCONTAINER to represent a mesh associated with a bone. The D3DXMESHCONTAINER looks like this:
Time for a cup of coffee.
MeshData holds the actual mesh. pMaterials holds material and texture info. pEffects may hold effects associated with the mesh. pAdjacency holds adjacency info, which is indices of faces, triangles, adjacent to each other. And pSkinInfo holds skinning info such as vertex weights and bone offset matrices that are used to add a skin effect to our animations.
And our extended version to cater for skinning looks like this:
SkinnedMesh.h
The attribute table is an array of D3DXATTRIBUTERANGE objects. The AttribId of this object corresponds to the material or texture to use for rendering a subset of a mesh. Because we are working with a COM object for the mesh, the memory for it will be deallocated after the function completes unless we add a reference using AddRef(). So we call AddRef on the pMesh of MeshData: pMeshData->pMesh->AddRef().
Notice boneMatrixPtrs; these are pointers to world bone transformation matrices in the D3DXFRAME structures. This means if we change the world transform of a bone these will be affected or in other words these will point to the changed matrices. We need the world transformations to perform rendering. When we animate the model, we call CalculateWorldMatrices to calculate these new world matrices that represent the pose of the bones, the bone transformations. A boneMesh may be affected by a number of bones so we use pointers to these bones matrices instead of saving them twice or multiple times for bone meshes and also so we only have to update one bone matrix with our animation controller for that bone to take effect. Using the modified local transformations of the model from the animation controller, we get these matrices from the bones that influence a mesh. We then use the boneoffset matrices to calclate the local transformations as these are not stored and offset matrices are stored in pSkinInfo. When we multiply a bone offset matrix with a bone's world matrix we get the bone's local transformation without it's affecting parent's world transformation. We want to do this when we pass the transformation to the skinning shader. The mesh in this case is the mesh associated with one of the bones, found in the D3DXFRAME structure pMeshContainer.
Now that you understand the Bone and BoneMesh structures somewhat we can begin to implement the ID3DXAllocateHierachy. We'll start with CreateFrame. In this function we allocate memory for the name of the bone as well as memory for the bone itself:
SkinnedMesh.cpp
And the DestroyFrame function should deallocate memory allocated in CreateFrame:
A single mesh can have a number of bones influencing it. Each bone has a set of vertex weights associated with it corresponding to each vertex of the mesh. The weights determine how much a vertex is affected by each bone. The greater the weight of a bone, the more a vertex will be affected by the transformation of that bone. This is how the skin works. The weights of each vertex of the model that correspond to affecting bones are passed to the HLSL effect file that performs the skinning on the GPU. And the bones that influence the vertex are passed to the HLSL file as well through the BLENDINDICES0 semantic. These weights and bones are stored in the .x file and therefore are loaded in the MeshData of the D3DXMESHCONTAINER BoneMesh. By rendering a subset of the BoneMesh, we pass these weight and bone parameters to the effect file, which in turn performs the skinning based on these values. We pass the values to the HLSL file during the SkinnedMesh rendering function.
Look back at the BoneMesh structure. Bone offset matrices are inverse matrices that tranform a bone from world space to local space; that is to the transformation uneffected by its parent. These are stored in the .x file and can be retrieved with pSkinInfo of D3DXSKININFO.
To skin a mesh with hardware skinning we need to put the vertex data of each mesh to render in a format that has vertex weight and influencing bone indices. The bone indices are indices of bones that affect the vertex. Each bone in the .x file has a set of vertices that are "attached" to that bone and a weight for each vertex that determines how much the bone affects that vertex. To include this information in our mesh, we must convert the mesh to an "indexed blended" mesh. When we convert the mesh to an indexed blended mesh, the additional bone indices and vertex weights are added to our vertex information within the mesh.
Now is a good time to show you how to load a mesh container since you know about the elements that make up one. Here it is again:
localBoneMatrices are calculated when we render the mesh by using boneMatrixPtrs and boneOffsetMatrices so we can pass the local bone matrix array to the shader to perform skinning. In our CreateMeshContainer function we must allocate memory for these matrix arrays and obtain pointers to the world transformations of our bones.
Hopefully you understood that code to create a mesh ready for animating.
But before we animate it we have to provide the mesh deallocation implementation. This is simply a case of deallocating the memory we allocated ourselves and releasing the COM objects used:
Now that the AllocateHierarchy functions are implemented we can go ahead and call D3DXLoadMeshHierarchyFromX passing the AllocateHierarchy object to it. This is done in the SkinnedMesh::Load function. When we call this function we retrieve a pointer to the root bone of the hierarchy that allows us to traverse the whole hierarchy with this one bone to calculate new matrices for animation for example. With just the root node we can add matrix pointers to each of our meshes that correspond to the world transformations of each bone and effectively will point to matrices that make up the animation when we animate the model.
In our SkinnedMesh::Load function is where we call D3DXLoadMeshHierarchyFromX. First we need to create an instance of the AllocateHierarchy; Our SkinnedMesh implementation then becomes:
Now in our game object we can test whether the hierarchy of one of our .x files can be loaded. Place SkinnedMesh model1; in the private members of your Game object. And call model1.Load("Your file.x") in the Init() function of Game. You will need to put an .x file containing a bone hierarchy into the directory that the game runs from. You can test whether a hierarchy was loaded using a break point. Hopefully all is good. You loaded a bone hierarchy.
We still have a few things to setup before model animation can occur. With the HLSL shader that we create, we define an interpolation of vertices from a start pose to a final pose. Each start and end pose of an animation is known as an animation set and these should typically be stored in the .x file. Whenever we pass a vertex to the HLSL effect it will be updated to a new position and then that new position will be passed the HLSL effect file and be updated and this is how we create a skinned animation but we need to setup the matrices that make this animation work. We must pass the bone tranformations to this effect file uneffected by their parents in order to calculate new vertex positions with the vertex shader. For these tranformations we calculate them in our Render function using bone pointer matrices and bone offset matrices. So we need to add bone matrix pointers to each bone mesh of the hierarchy. Then when we render the mesh we can easily access these from the array of matrix pointers to the world matrices of the bones that affect the mesh skin.
These bone matrix pointers are added to each mesh that is affected by bones; we use pointers so that when a bone's transformation changes the pointers will be affected and resultingly the mesh skin too. To add pointers we must traverse the whole hierarchy and check for a bone mesh; if one exists and has skinning information, we add the matrix pointers to affecting bones. The affecting bones are the bones contained in pSkinInfo:
We call this function passing the root node to it to setup all the pointers to influencing mesh bones world matrices. This is done after loading the hierarchy. Also add a call to CalculateWorldMatrices in the Load function to add world matrices to each of the bones. If you don't add these the model will be displayed as a jumble of meshes.
Now we need to free the memory of the matrix pointers when the skinned mesh is destroyed. This again involves traversing the hierarchy and freeing memory used by pointers. Add the following function to the SkinnedMesh class:
And call it on SkinnedMesh destruction.
Finally everything is set up for us to add the skinning effect and animate the model with an animation controller. First we create the effect; we will make this global so we can use it in the Render function of our SkinnedMesh.
This effect will be our interface to the HLSL effect file. We can upload variables to the file through this interface. Create a new effect file called skinning.fx; this is simply an ASCII text file. This will be our shader that performs skinning. We create the effect with D3DXCreateEffectFromFile(). Call this from the Init function of your Game object. Just set flags to D3DXSHADER_DEBUG for now because the shader is not known to work yet.
Modify OnDeviceLost and OnDeviceGained to cater for the effect file:
Now we need to implement the Render function of the SkinnedMesh and the HLSL file. In this file we calculate both skinning and lighting of the model. We first define our vertex structure that corresponds to the vertex structure of the index blended mesh; this will be the input vertex data to the shader:
Here we get the position of the vertex that we will modify in the shader; and we get the normal, which is the direction vector of the vertex and used in lighting. The weights are the weights of the affecting bones, which can be found by the bone indices. We use the weights and the bone matrices to determine the new position of the vertex for each vertex and normals. Therefore we store the matrices in a matrix array as follows:
To calculate the new vertex positions we apply each bone weight to a multiplication of the original vertex position and the bone transformation matrix and sum up the results. However there is one more thing - the combination of weights must add up to 1, which is equivelent to 100%. See, each weight applies a percentage of effect on a vertex so they must add up to 1. Therefore we calculate the last weight as 1-totalWeights; one minus the sum total so that they definitely add up to one.
Here is the complete shader for performing skin and lighting:
The technique tells the system to use vertex and pixel shader version 2 and to pass the output of vs_Skinning to the input of ps_Lighting. The pixel shader ps_Lighting essentially "Lights" the pixels of the texture.
Now that we have the vertex shader, we can use it. And render the model. In this Render function we get a handle to the technique with g_pEffect->GetTechniqueByName("Skinning") and pass the mesh to it; and glory be the shader will perform its work. The Render function will be called on the root bone and traverse the bone hierarchy and render each of the mesh containers associated with the bones of the hierarchy.
Here is the Render function:
Now that we have the shader in place and the render function we can aquire an animation controller and control the animations of the model.
We get an animation controller from the D3DXLoadHierarchyFromX function. We will add this controller to the SkinnedMesh class in the private members:
Then in our SkinnedMesh Load function add this as a parameter to D3DXLoadMeshHierarchyFromX:
The way animation works is there are a number of animation sets stored in the model or .x file that correspond to different animation cycles such as a walk animation or jump. Depending on the character, each animation has a name and we can set the current animation using its name. If we have different characters we will want to access or set different animations per character using name strings, e.g. SetAnimation("Walk"), SetAnimation("Sit") like that. For this we can use a map of strings to animation IDs. With this map we can get the ID of each animation set by using the name along with the map. A map has an array of keys and values associated with those keys. The key here is the name of the animation and the value is its animation set ID.
Lastly once we have the animation sets we can play an animation by calling m_pAnimControl->AdvanceTime(time, NULL);
First let's get the animation sets and store their names and IDs in a map. For this we will create a function in our SkinnedMesh class void GetAnimationSets(). Create a map by including <map> and make sure using namespace std; is at the top of the SkinnedMesh cpp file. Create the map called map<string, dword="DWORD">animationSets; For this you will also need to include <string>. using namespace std means we can use map and string without std::map or std::string for example if you didn't know already. We will also add another two functions void SetAnimation(string name) and void PlayAnimation(float time).
We get and save the animation sets to our map with the following function:
We set the active animation set with SetAnimation:
When we update the game we call the following function to play the active animation:
Usage:
In the SkinnedMesh::Load function add the following line on hierarchy load success:
In the Game::Init function, set the active animation set - for example if we have a walk cycle animation:
And then play the animation in Game::Update():
There is one more thing we often want to do. That is render static non-moving meshes that are part of the hierarchy. We might for example have a mesh that doesn't have skin info; so we need to save this; Notice that we create an indexed blended mesh only if there is skin info. Well now we need to save the mesh if there is no skin info:
In our CreateMeshContainer function:
Now we have the static meshes saved in BoneMesh structures we can render them. But we need first to light the mesh with a lighting shader; Add this to the shader file:
Now we can render the static meshes of our model; We have a static mesh if there is no pSkinInfo. Here we set the lighting technique to active and render the mesh with texturing:
That's us done! We have an animated model that we can work with. We can set the active animation and render it with skinning!
In this part we will export an animation hierarchy from 3d studio max.
You may have to adjust the camera. E.g.
12 April 2014: Initial release
17 April 2014: Updated included download
22 April 2014: Updated
24 April 2014: Updated
Setting up Direct3D
Let's start from the beginning: Creating a window. Setup a new empty Win32 project in Visual Studio; you will need a version of the DirectX SDK and to link in the include and library directories of the SDK into your project. This is done via project properties. In the linker->Input Additional dependencies add d3d9.lib and d3dx9.lib.
First we will include <windows.h> and create a WinMain() function and a Windows procedure WinProc(). WinMain is the entry point of the program that is executed first in a standard Windows application. Our WinMain function is outlined as:
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR cmdLine, int showCmd);
And the outline of our WndProc is:
LRESULT CALLBACK WndProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam);
Place these declarations at the top of the cpp file after includes so that they can be referenced throughout other functions.
Now we will want an object to handle various application events such as game initialization, updating the game and cleaning of resources. We will create a simple Game object for this. Within the Init() function we will create a window and hold a handle to it, m_mainWindow:
class Game{ public: Game(); ~Game(); HRESULT Init(HINSTANCE hInstance); void Update(float deltaTime); void Cleanup(); private: HWND m_mainWindow; };
Don't forget the semi-colon after class declaration.
We create the window class that describes our window and register it with the operating system in the Game::Init() function. We will use a basic window class for this:
WNDCLASS wc; //Prepare class for use memset(&wc, 0, sizeof(WNDCLASS)); wc.style=CS_HREDRAW | CS_VREDRAW; //Redraws the window if width or height changes. //Associate the window procedure with the window wc.lpfnWndProc=(WNDPROC)WndProc; wc.hInstance=hInstance; wc.lpszClassName="Direct3D App";
Register the window class with the OS:
if(FAILED(RegisterClass(&wc))){ return E_FAIL; }
And finally, create the window; We use WS_OVERLAPPEDWINDOW for a standard minimize, maximize and close options:
m_mainWindow = CreateWindow("Direct3D App", //The window class to use "Direct3D App", //window title WS_OVERLAPPEDWINDOW, //window style 200, //x 200, //y CW_USEDEFAULT, //Default width CW_USEDEFAULT, //Default height NULL, //Parent Window NULL, //Menu hInstance, //Application instance 0); //Pointer to value parameter, lParam of WndProc if(!m_mainWindow){return E_FAIL;} And after CreateWindow, add: ShowWindow(m_mainWindow, SW_SHOW); UpdateWindow(m_mainWindow); //Function completed successfully return S_OK;
In the WinMain function we must create an instance of Game and call the Init function to initialize the window:
Game mygame; if(FAILED(mygame.Init(hInstance))){ MessageBox(NULL, "Failed to initialize game", NULL, MB_OK); return 1; }
Every Windows application has a message pump. The message pump is a loop that forwards Windows messages to the window procedure by "looking" or peeking at the internal message queue. We "look" at a message within our message pump, remove it from the queue with PM_REMOVE and send it to the window procedure typically for a game. PeekMessage is better than GetMessage in the case where we want to execute game code because it returns immediately. GetMessage on the other hand would force us to wait for a message.
Now for our message pump; place this in the WinMain function:
MSG msg; memset(&msg, 0, sizeof(MSG)); while(WM_QUIT != msg.message){ while(PeekMessage(&msg, 0, 0, 0, PM_REMOVE) != 0){ //Put the message in a recognizable form TranslateMessage(&msg); //Send the message to the window procedure DispatchMessage(&msg); } //Update the game //Don't worry about this just now }
At the end of WinMain, call the application cleanup function and return with a message code:
mygame.Cleanup(); return (int)msg.wParam;
Now for our window procedure implementation; We will use a basic implementation to handle messages:
LRESULT CALLBACK WndProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam){ //Handle window creation and destruction events switch(msg){ case WM_CREATE: break; case WM_DESTROY: PostQuitMessage(0); break; } //Handle all other events with default procedure return DefWindowProc(hWnd, msg, wParam, lParam); }
Check that the program works so far. You should be able to build the project and run it and display the window. If all goes well, we can start with Direct3D.
Now that we have created a window, let's setup Direct3D and render a background colour to the screen. We will add to our Game object so we can easily work with different parts of our application in a clear and secure manner such as creation, updating the game and application destruction. With our game object we can intialize Direct3D in an Init() function and update our game with Game::Update() and clean up DirectX resources with Game::Cleanup(). Name this class to suit your preference. I will call it Game. The m_ prefix is Hungarian notation and was developed by someone called Charles Simonyi, a Microsoft programmer. It is short for member variable. Other prefixes might be nCount, n for number, s_ for static variables. Anyway here is the Game object with added functions and variables specific to Direct3D:
#include "d3d9.h" #include "d3dx9.h" class Game{ public: Game(); ~Game(); HRESULT Init(HINSTANCE hInstance); void Update(float deltaTime); void Render(); void Cleanup(); void OnDeviceLost(); void OnDeviceGained(); private: HWND m_mainWindow; D3DPRESENT_PARAMETERS m_pp; bool m_deviceStatus; };
Further reading recommended: Character Animation with Direct3D - Carl Granberg
We could do with a helper function to release, free up memory used by DirectX COM objects in a safe manner. We use a generic template class for this or you could create a macro for the same purpose:
dxhelper.h template<class T> inline void SAFE_RELEASE(T t) { if(t)t->Release(); t = NULL; }
Don't forget to include it in your main cpp file. #include "dxhelper.h"
In our Init function we fill out the present parameters and setup Direct3D;
HRESULT Game::Init(){ //... Window Creation Code Here //... //Direct3D Initialization //Create the Direct3D object IDirect3D9* d3d9 = Direct3DCreate9(D3D_SDK_VERSION); if(d3d9 == NULL) return E_FAIL; memset(&m_pp, 0, sizeof(D3DPRESENT_PARAMETERS)); m_pp.BackBufferWidth = 800; m_pp.BackBufferHeight = 600; m_pp.BackBufferFormat = D3DFMT_A8R8G8B8; m_pp.BackBufferCount = 1; m_pp.MultiSampleType = D3DMULTISAMPLE_NONE; m_pp.MultiSampleQuality = 0; m_pp.SwapEffect = D3DSWAPEFFECT_DISCARD; m_pp.hDeviceWindow = m_mainWindow; m_pp.Windowed = true; m_pp.EnableAutoDepthStencil = true; m_pp.AutoDepthStencilFormat = D3DFMT_D24S8; m_pp.Flags = 0; m_pp.FullScreen_RefreshRateInHz = D3DPRESENT_RATE_DEFAULT; m_pp.PresentationInterval = D3DPRESENT_INTERVAL_IMMEDIATE; if(FAILED(d3d9->CreateDevice(D3DADAPTER_DEFAULT, D3DDEVTYPE_HAL, m_mainWindow, D3DCREATE_HARDWARE_VERTEXPROCESSING, &m_pp, &g_pDevice))) return E_FAIL; //We no longer need the Direct3D object SAFE_RELEASE(d3d9); //Success the Direct3D device was created return S_OK; }
Let's go over the parameters quickly; BackBufferWidth specifies the width of the offscreen buffer, the back buffer. The offscreen buffer is a memory segment that we render a scene to and it becomes the on-screen buffer, what we actually see on the screen when it is flipped with the front buffer, the on-screen buffer. Similarly with the BackBufferHeight. We specify 8 bits for alpha, 8 bits for red, 8 bits for green and 8 bits for blue so this is 32-bit true colour allocated for our buffer. We specify that there will only be one back buffer; the reason we might have two back buffers is to speed up the rendering to the screen e.g. while the onscreen buffer is diplayed you could prepare two offscreen buffers so you could flip one and then the next is ready to be displayed so you could flip the other immediately. Multisampling is a technique that improves the quality of an image but takes up more processing time. So we specify D3DMULTISAMPLE_NONE. We specify SWAPEFFECT_DISCARD to remove the oncreen buffer when it is swapped with a backbuffer; so the backbuffer becomes the front and the old front is deleted. m_pp.hDeviceWindow is the window to render to. Windowed can be true, displaying the scene in a window, or false to display the scene fullscreen. We set m_pp.EnableAutoDepthStencil to true to enable depth bufferring; where a depth buffer is used effectively causes 3D objects in the world to overlap correctly; a z value will be specified for each pixel of the depth buffer and this will effectively enable depth testing which is basically a test comparison of the z value of each pixel; If a pixel's z value is less, nearer to the screen, than another pixels z value, it is closer to the screen so will be written to the offscreen buffer. We use a default refresh rate and immediate buffer swapping. The other type of buffer swapping could be D3DPRESENT_INTERVAL_DEFAULT, default interval, which might be the screen refresh rate.
Next we create the device with a call to d3d9->CreateDevice().
We need to specify a global Direct3D device object so that we can use the device anywhere in the program.
IDirect3DDevice9* g_pDevice = NULL;
Then the CreateDevice() function will create the device object. We use the default display adapter and then D3DDEVTYPE_HAL to use the hardware abstraction layer, hardware graphics acceleration for our rendering of the scene. This is much faster as opposed to software rendering. Hardware means the graphics card in this case. We specify to use hardware vertex processing too. And then we pass the present parameters structure that describes properties of the device to create. And lastly we pass the g_pDevice variable to retrieve a handle to the newly created device.
Now, before we continue with animation we must do a bit of device handling. For example if the user does ALT+TAB our device might be lost and we need to reset it so that our resources are maintained. You'll notice we have onDeviceLost and onDeviceGained functions in our game object. These will work hand-in-hand with the deviceStatus variable to handle a device lost situation. After a device has been lost we must reconfigure it with onDeviceGained().
To check for a lost device we check the device cooperative level for D3D_OK. If the cooperative level is not D3D_OK then our device can be in a lost state or in a lost and not reset state. This should be regularly checked so we will put the code in our Game::Update() function.
#define DEVICE_LOSTREADY 0 #define DEVICE_NOTRESET 1 HRESULT coop = g_pDevice->TestCooperativeLevel(); if(coop != D3D_OK) { if(coop == D3DERR_DEVICELOST) { if(m_deviceStatus == DEVICE_LOSTREADY) OnDeviceLost(); } else if(coop == D3DERR_DEVICENOTRESET) { if(m_deviceStatus == DEVICE_NOTRESET) OnDeviceGained(); } }
Our OnDeviceLost function and OnDeviceGained look like:
void Game::OnDeviceLost() { try { //Add OnDeviceLost() calls for DirectX COM objects m_deviceStatus = DEVICE_NOTRESET; } catch(...) { //Error handling code } } void Game::OnDeviceGained() { try { g_pDevice->Reset(&m_pp); //Add OnResetDevice() calls for DirectX COM objects m_deviceStatus = DEVICE_LOSTREADY; } catch(...) { //Error handling code } }
When the program starts we have to set the m_deviceStatus variable so that we can use it. So in our Game::Game() constructor set the variable:
Game::Game(){ m_deviceStatus = DEVICE_LOSTREADY; }
Now we need to implement the Render and Cleanup functions. We will just clear the screen and free up memory in these functions.
void Game::Render() { g_pDevice->Clear(0, NULL, D3DCLEAR_TARGET | D3DCLEAR_ZBUFFER, 0xff00ff00, 1.0f, 0); if(SUCCEEDED(g_pDevice->BeginScene())) { // Perform some rendering to back buffer. g_pDevice->EndScene(); } // Swap buffers. g_pDevice->Present(NULL, NULL, NULL, NULL); } void Game::Cleanup() { SAFE_RELEASE(g_pDevice); }
Finally, we want to render our scene and update it. Remember the Update() method handles device lost events.
We want our game to run with a consistent frame rate so that Update() updates the game at the same frame rate on all PCs. If we just updated the game inconsistently without frame rate, our game would be faster on faster computers and slower on slower computers and also might speed up or slow down if we do not specify a frame rate.
Therefore we use GetTickCount() and pass a change in time to our Update() function. GetTickCount() returns the number of milliseconds that have elapsed since the system was started. We record the start time, and subtract this from the current time of each iteration of the loop. We then set the new start time; repeat this calculation and get the change in time and pass this value to Update(deltaTime).
Our message loop now is defined as:
//Get the time in milliseconds DWORD startTime = GetTickCount(); float deltaTime = 0; MSG msg; memset(&msg, 0, sizeof(MSG)); while(msg.message != WM_QUIT){ if(PeekMessage(&msg, 0, 0, 0, PM_REMOVE)){ //Put the message in a recognizable form TranslateMessage(&msg); //Send the message to the window procedure DispatchMessage(&msg); } else{ //Update the game DWORD t=GetTickCount(); deltaTime=float(t-startTime)*0.001f; //Pass time in seconds mygame.Update(deltaTime); //Render the world mygame.Render(); startTime = t; } }
Now we have a complete Direct3D framework we can begin with loading an animated model. I will supply you the code so far and then talk about how we can load an animation hierarchy:
#include <windows.h> #include "d3d9.h" #include "d3dx9.h" #include "dxhelper.h" #define DEVICE_LOSTREADY 0 #define DEVICE_NOTRESET 1 IDirect3DDevice9* g_pDevice = NULL; int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR cmdLine, int showCmd); LRESULT CALLBACK WndProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam); class Game{ public: Game(); ~Game(); HRESULT Init(HINSTANCE hInstance); void Update(float deltaTime); void Render(); void Cleanup(); void OnDeviceLost(); void OnDeviceGained(); private: HWND m_mainWindow; D3DPRESENT_PARAMETERS m_pp; bool m_deviceStatus; }; Game::Game(){ m_deviceStatus = DEVICE_LOSTREADY; } Game::~Game(){ } HRESULT Game::Init(HINSTANCE hInstance){ WNDCLASS wc; //Prepare class for use memset(&wc, 0, sizeof(WNDCLASS)); wc.style=CS_HREDRAW | CS_VREDRAW; //Redraws the window if width or height changes. //Associate the window procedure with the window wc.lpfnWndProc=(WNDPROC)WndProc; wc.hInstance=hInstance; wc.lpszClassName=L"Direct3D App"; if(FAILED(RegisterClass(&wc))){ return E_FAIL; } m_mainWindow = CreateWindow(L"Direct3D App", //The window class to use L"Direct3D App", //window title WS_OVERLAPPEDWINDOW, //window style 200, //x 200, //y CW_USEDEFAULT, //Default width CW_USEDEFAULT, //Default height NULL, NULL, hInstance, //Application instance 0); //Pointer to value parameter, lParam of WndProc if(!m_mainWindow){return E_FAIL;} ShowWindow(m_mainWindow, SW_SHOW); UpdateWindow(m_mainWindow); //Direct3D Initialization //Create the Direct3D object IDirect3D9* d3d9 = Direct3DCreate9(D3D_SDK_VERSION); if(d3d9 == NULL) return E_FAIL; memset(&m_pp, 0, sizeof(D3DPRESENT_PARAMETERS)); m_pp.BackBufferWidth = 800; m_pp.BackBufferHeight = 600; m_pp.BackBufferFormat = D3DFMT_A8R8G8B8; m_pp.BackBufferCount = 1; m_pp.MultiSampleType = D3DMULTISAMPLE_NONE; m_pp.MultiSampleQuality = 0; m_pp.SwapEffect = D3DSWAPEFFECT_DISCARD; m_pp.hDeviceWindow = m_mainWindow; m_pp.Windowed = true; m_pp.EnableAutoDepthStencil = true; m_pp.AutoDepthStencilFormat = D3DFMT_D24S8; m_pp.Flags = 0; m_pp.FullScreen_RefreshRateInHz = D3DPRESENT_RATE_DEFAULT; m_pp.PresentationInterval = D3DPRESENT_INTERVAL_IMMEDIATE; if(FAILED(d3d9->CreateDevice(D3DADAPTER_DEFAULT, D3DDEVTYPE_HAL, m_mainWindow, D3DCREATE_HARDWARE_VERTEXPROCESSING, &m_pp, &g_pDevice))) return E_FAIL; //We no longer need the Direct3D object SAFE_RELEASE(d3d9); //Success the Direct3D device was created return S_OK; } void Game::Update(float deltaTime){ HRESULT coop = g_pDevice->TestCooperativeLevel(); if(coop != D3D_OK) { if(coop == D3DERR_DEVICELOST) { if(m_deviceStatus == DEVICE_LOSTREADY) OnDeviceLost(); } else if(coop == D3DERR_DEVICENOTRESET) { if(m_deviceStatus == DEVICE_NOTRESET) OnDeviceGained(); } } } void Game::Cleanup() { SAFE_RELEASE(g_pDevice); } void Game::OnDeviceLost() { try { //Add OnDeviceLost() calls for DirectX COM objects m_deviceStatus = DEVICE_NOTRESET; } catch(...) { //Error handling code } } void Game::OnDeviceGained() { try { g_pDevice->Reset(&m_pp); //Add OnResetDevice() calls for DirectX COM objects m_deviceStatus = DEVICE_LOSTREADY; } catch(...) { //Error handling code } } void Game::Render() { if(m_deviceStatus==DEVICE_LOSTREADY){ g_pDevice->Clear(0, NULL, D3DCLEAR_TARGET | D3DCLEAR_ZBUFFER, 0xff00ff00, 1.0f, 0); if(SUCCEEDED(g_pDevice->BeginScene())) { // Perform some rendering to back buffer. g_pDevice->EndScene(); } // Swap buffers. g_pDevice->Present(NULL, NULL, NULL, NULL); } } int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR cmdLine, int showCmd){ Game mygame; if(FAILED(mygame.Init(hInstance))){ MessageBox(NULL, L"Failed to initialize game", NULL, MB_OK); return 1; } //Get the time in milliseconds DWORD startTime = GetTickCount(); float deltaTime = 0; MSG msg; memset(&msg, 0, sizeof(MSG)); while(WM_QUIT != msg.message){ while(PeekMessage(&msg, 0, 0, 0, PM_REMOVE) != 0){ //Put the message in a recognizable form TranslateMessage(&msg); //Send the message to the window procedure DispatchMessage(&msg); } //Update the game DWORD t=GetTickCount(); deltaTime=float(t-startTime)*0.001f; //Pass time in seconds mygame.Update(deltaTime); //Render the world mygame.Render(); startTime = t; } mygame.Cleanup(); return (int)msg.wParam; } LRESULT CALLBACK WndProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam){ //Handle window creation and destruction events switch(msg){ case WM_CREATE: break; case WM_DESTROY: PostQuitMessage(0); break; } //Handle all other events with default procedure return DefWindowProc(hWnd, msg, wParam, lParam); }
Loading an Animation Hierarchy
Hopefully you have found the tutorial useful up until now. In this part I will start by setting the perspective projection and then cover the steps needed to load and animate a mesh. We will be animating our mesh object with hardware acceleration. This involves creating an HLSL .fx file.
Requirements
You will need a method of producing an .x file with animation info. For this I will be using 3d studio max 2014 and an exporter plugin. There are a number of plugins available. If you can't get hold of this then you may have to use Blender, however I don't know if the .x files produced with Blender are sufficient for this tutorial. And I don't have the incentive to find out. So I leave this to the reader. Hopefully you will find a way to produce an .x file with an animation hierarchy in a suitable format for loading into a DirectX application.
Setting up a perspective projection
We have three matrices to create. World, View and Projection. The World matrix represents the world transformation for a set of objects such as meshes. This is the position of these meshes in the game world. The view matrix represents the camera. It has a lookAt component, an Up component that specifies the up direction of the world, y or z. And also an eye component that is the "look from" component; the position of the camera.
Our perspective matrix defines the "fish eye" factor or field-of-view, which is the angle range we can see from our eye. This is typically 45 degrees. We must also specify the aspect ratio, which is the number of width pixels in correspondence with the number of height pixels; the ratio of width to height. Typically set this to WINDOW_WIDTH / WINDOW_HEIGHT. And lastly, the z near plane and z far plane must be specified, these are the cut-off points of our view. Typically the z near plane of a projection frustum is z=1.0f. Z here is the point of cut-off; anything closer to the eye than 1.0 is cut-off, not rendered. Similarly anything further away than zfar is cut-off, not rendered too.
We add the describing transform states with the D3DTS_ constants in the device SetTranform function passing each tranform matrix as parameters. This is done in our Render() function.
D3DXMATRIX view, proj, world; D3DXMatrixIdentity(&world); //Set to no transform //Position the camera behind the z axis(z=-10) //Make Y the up direction of the world //And look at the centre of the world origin(0,0,0) D3DXMatrixLookAtLH(&view, &D3DXVECTOR3(0,0,-10.0f), &D3DXVECTOR3(0.0f, 1.0f, 0.0f), &D3DXVECTOR3(0.0f, 0.0f, 0.0f)); //Make the field of view 45 degrees //Use the window dimensions for aspect ratio so rendered image is not //stretched when window is resized. //Set znear to 1.0f and zfar to 10000.0f RECT rc; GetClientRect(m_mainWindow, &rc); D3DXMatrixPerspectiveFovLH(&proj, D3DXToRadian(45.0), (float)rc.width / (float)rc.height, 1.0f, 10000.0f); g_pDevice->SetTransform(D3DTS_WORLD, &world); g_pDevice->SetTransform(D3DTS_VIEW, &view); g_pDevice->SetTransform(D3DTS_PROJECTION, &proj);
Now that we have the scene set up, the basic camera and projection ready - it's time to load a model with animation.
Loading an Animation Hierarchy
In real life humans and animals have bones. Likewise, our game characters have bones. A bone in DirectX is represented with the D3DXFRAME structure. You may just think of this structure as a bone. A bone may have a parent and a sibling. For example a parent bone might be the upper arm and a child bone might be the lower arm. When you move your upper arm, your lower arm, forearm, moves with it. That is why a forearm is the child. If you move your lower arm however, the upper arm is not affected. And hence that is why the upper arm is a parent. Each bone may have a transformation, a rotation and position for example. A sibling is just a bone that shares the same parent as another bone(D3DXFRAME). Let's look at the D3DXFRAME structure to see how we can represent a bone in code.
struct D3DXFRAME{ LPSTR Name; D3DXMATRIX TransformationMatrix; LPD3DXMESHCONTAINER pMeshContainer; D3DXFRAME* pFrameSibling; D3DXFRAME* pFrameFirstChild; };
A bone has a name, a transformation, and optionally a mesh associated with it; and optionally a sibling and a child. With this structure we can represent a whole hierarchy or skeleton in other words. By associating sibling bones and child bones we can link all the D3DXFRAME bones together, which in turn will be a representation of a skeleton such as a human or an animal.
The names of the bones could be "right leg", "left leg", "right forearm" for example to give you an idea. The transformations defined in a basic D3DXFRAME are the local bone transformations. These are local to the bones in contrast with the world transformations, which are the actual transformations in the world; the final transforms. In our game we require the world transformations of the bones to render them at the exact positions in the world; so we will extend this structure to include them. For easy reference we will call the new structure Bone.
struct Bone: public D3DXFRAME { D3DXMATRIX WorldMatrix; };
The WorldMatrix is the combination of a bone's local matrix with its parent's WorldMatrix. This is simply a multiplication of the two so that the child bone inherits the transformation of its parent.
We must traverse the bone hierarchy to calculate each of the new matrices. This is done with the following recursive function:
void CalculateWorldMatrices(Bone* child, D3DXMATRIX* parentMatrix){ D3DXMatrixMultiply(&child->WorldMatrix, &child->TransformationMatrix, parentMatrix); //Each sibling has same parent as child //so pass the parent matrix of this child if(child->pFrameSibling){ CalculateWorldMatrices((Bone*)child->pFrameSibling, parentMatrix); } //Pass child matrix as parent to children if(child->pFrameFirstChild){ CalculateWorldMatrices((Bone*)child->pFrameFirstChild, &child->WorldMatrix); } }
Then to calculate all of the bones matrices that make up the skeleton we call CalculateWorldMatrices on the root node, the parent bone of the hierarchy with the identity matrix as the parent. This will traverse all children and siblings of those children and build each of the world matrices.
To load a bone hierarchy from an .x file we must implement the ID3DXAllocateHierarchy interface. This interface defines 4 functions that we must implement ourselves. Then we pass the implemented object in a call to D3DXLoadMeshHierarchyFromX(). That will create a skeleton of a character. And after we have the skeleton we can apply skinning by using an HLSL effect file to make our character effectively have skin.
To implement the functions declared in ID3DXAllocateHierarchy we provide a new class definition that inherits from it. We will call this class AllocateHierarchyImp. The new class and inherited functions effectively looks like this:
class AllocateHierarchyImp : public ID3DXAllocateHierarchy{ public: STDMETHOD(CreateFrame)(LPCSTR Name, LPD3DXFRAME* ppNewFrame); STDMETHOD(CreateMeshContainer)(LPCSTR Name, CONST D3DXMESHDATA* pMeshData, CONST D3DXMATERIAL* pMaterials, CONST D3DXEFFECTINSTANCE* pEffectInstances, DWORD NumMaterials, CONST DWORD* pAdjacency, LPD3DXSKININFO pSkinInfo, LPD3DXMESHCONTAINER* ppNewMeshContainer); STDMETHOD(DestroyFrame)(LPD3DXFRAME pFrameToFree); STDMETHOD(DestroyMeshContainer)(LPD3DXMESHCONTAINER pMeshContainerBase); };
In these functions we handle the allocation of memory for bones and the deallocation of memory we allocated ourselves for bones and associated bone meshes. CreateFrame is fairly simple; we just allocate memory for a bone with new Bone; and allocate memory for the name of the bone with new char[strlen(Name)+1];.
CreateMeshContainer on the other hand is more complicated. Bear in mind these functions are called by the D3DXLoadMeshHierarchyFromX() function. Information about the mesh we are loading is passed to these functions.
Before I jump into the code for these functions we should consider a class that will provide the mechanism of loading a skinned mesh separately for each of our animated characters. Thus we will create a class called SkinnedMesh that caters for each individual character. This class is outlined as:
SkinnedMesh.h
class SkinnedMesh{ public: SkinnedMesh(); ~SkinnedMesh(); void Load(WCHAR filename[]); void Render(Bone* bone); private: void CalculateWorldMatrices(Bone* child, D3DXMATRIX* parentMatrix); void AddBoneMatrixPointers(Bone* bone); D3DXFRAME* m_pRootNode; };
We need to define a mesh container structure so that we can hold a mesh associated with each bone and prepare for skinning the mesh. Like with the Bone when we extended the D3DXFRAME, we extend the D3DXMESHCONTAINER to represent a mesh associated with a bone. The D3DXMESHCONTAINER looks like this:
struct D3DXMESHCONTAINER{ LPSTR Name; D3DXMESHDATA MeshData; LPD3DXMATERIAL pMaterials; LPD3DXEFFECTINSTANCE pEffects; DWORD NumMaterials; DWORD* pAdjacency; LPD3DXSKININFO pSkinInfo; D3DXMESHCONTAINER* pNextMeshContainer; };
Time for a cup of coffee.
MeshData holds the actual mesh. pMaterials holds material and texture info. pEffects may hold effects associated with the mesh. pAdjacency holds adjacency info, which is indices of faces, triangles, adjacent to each other. And pSkinInfo holds skinning info such as vertex weights and bone offset matrices that are used to add a skin effect to our animations.
And our extended version to cater for skinning looks like this:
SkinnedMesh.h
struct BoneMesh: public D3DXMESHCONTAINER { vector<D3DMATERIAL9> Materials; vector<IDirect3DTexture9*> Textures; DWORD NumAttributeGroups; D3DXATTRIBUTERANGE* attributeTable; D3DXMATRIX** boneMatrixPtrs; D3DXMATRIX* boneOffsetMatrices; D3DXMATRIX* localBoneMatrices; };
The attribute table is an array of D3DXATTRIBUTERANGE objects. The AttribId of this object corresponds to the material or texture to use for rendering a subset of a mesh. Because we are working with a COM object for the mesh, the memory for it will be deallocated after the function completes unless we add a reference using AddRef(). So we call AddRef on the pMesh of MeshData: pMeshData->pMesh->AddRef().
Notice boneMatrixPtrs; these are pointers to world bone transformation matrices in the D3DXFRAME structures. This means if we change the world transform of a bone these will be affected or in other words these will point to the changed matrices. We need the world transformations to perform rendering. When we animate the model, we call CalculateWorldMatrices to calculate these new world matrices that represent the pose of the bones, the bone transformations. A boneMesh may be affected by a number of bones so we use pointers to these bones matrices instead of saving them twice or multiple times for bone meshes and also so we only have to update one bone matrix with our animation controller for that bone to take effect. Using the modified local transformations of the model from the animation controller, we get these matrices from the bones that influence a mesh. We then use the boneoffset matrices to calclate the local transformations as these are not stored and offset matrices are stored in pSkinInfo. When we multiply a bone offset matrix with a bone's world matrix we get the bone's local transformation without it's affecting parent's world transformation. We want to do this when we pass the transformation to the skinning shader. The mesh in this case is the mesh associated with one of the bones, found in the D3DXFRAME structure pMeshContainer.
Now that you understand the Bone and BoneMesh structures somewhat we can begin to implement the ID3DXAllocateHierachy. We'll start with CreateFrame. In this function we allocate memory for the name of the bone as well as memory for the bone itself:
SkinnedMesh.cpp
HRESULT AllocateHierarchyImp::CreateFrame(LPCSTR Name, LPD3DXFRAME *ppNewFrame) { Bone *bone = new Bone; memset(bone, 0, sizeof(Bone)); if(Name != NULL) { //Allocate memory for name bone->Name = new char[strlen(Name)]; strcpy(bone->Name, Name); } //Prepare Matrices D3DXMatrixIdentity(&bone->TransformationMatrix); D3DXMatrixIdentity(&bone->WorldMatrix); //Return the new bone *ppNewFrame = (D3DXFRAME*)bone; return S_OK; }
And the DestroyFrame function should deallocate memory allocated in CreateFrame:
HRESULT AllocateHierarchyImp::DestroyFrame(LPD3DXFRAME pFrameToFree) { if(pFrameToFree) { //Free up memory if(pFrameToFree->Name != NULL) delete [] pFrameToFree->Name; delete pFrameToFree; } pFrameToFree = NULL; return S_OK; }
A single mesh can have a number of bones influencing it. Each bone has a set of vertex weights associated with it corresponding to each vertex of the mesh. The weights determine how much a vertex is affected by each bone. The greater the weight of a bone, the more a vertex will be affected by the transformation of that bone. This is how the skin works. The weights of each vertex of the model that correspond to affecting bones are passed to the HLSL effect file that performs the skinning on the GPU. And the bones that influence the vertex are passed to the HLSL file as well through the BLENDINDICES0 semantic. These weights and bones are stored in the .x file and therefore are loaded in the MeshData of the D3DXMESHCONTAINER BoneMesh. By rendering a subset of the BoneMesh, we pass these weight and bone parameters to the effect file, which in turn performs the skinning based on these values. We pass the values to the HLSL file during the SkinnedMesh rendering function.
Look back at the BoneMesh structure. Bone offset matrices are inverse matrices that tranform a bone from world space to local space; that is to the transformation uneffected by its parent. These are stored in the .x file and can be retrieved with pSkinInfo of D3DXSKININFO.
To skin a mesh with hardware skinning we need to put the vertex data of each mesh to render in a format that has vertex weight and influencing bone indices. The bone indices are indices of bones that affect the vertex. Each bone in the .x file has a set of vertices that are "attached" to that bone and a weight for each vertex that determines how much the bone affects that vertex. To include this information in our mesh, we must convert the mesh to an "indexed blended" mesh. When we convert the mesh to an indexed blended mesh, the additional bone indices and vertex weights are added to our vertex information within the mesh.
Now is a good time to show you how to load a mesh container since you know about the elements that make up one. Here it is again:
struct BoneMesh: public D3DXMESHCONTAINER { vector<D3DMATERIAL9> Materials; vector<IDirect3DTexture9*> Textures; DWORD NumAttributeGroups; D3DXATTRIBUTERANGE* attributeTable; D3DXMATRIX** boneMatrixPtrs; D3DXMATRIX* boneOffsetMatrices; D3DXMATRIX* localBoneMatrices; };
localBoneMatrices are calculated when we render the mesh by using boneMatrixPtrs and boneOffsetMatrices so we can pass the local bone matrix array to the shader to perform skinning. In our CreateMeshContainer function we must allocate memory for these matrix arrays and obtain pointers to the world transformations of our bones.
HRESULT AllocateHierarchyImp::CreateMeshContainer(LPCSTR Name, CONST D3DXMESHDATA *pMeshData, CONST D3DXMATERIAL *pMaterials, CONST D3DXEFFECTINSTANCE *pEffectInstances, DWORD NumMaterials, CONST DWORD *pAdjacency, LPD3DXSKININFO pSkinInfo, LPD3DXMESHCONTAINER *ppNewMeshContainer) { //Allocate memory for the new bone mesh //and initialize it to zero BoneMesh *boneMesh = new BoneMesh; memset(boneMesh, 0, sizeof(BoneMesh)); //Add a reference to the mesh so the load function doesn't get rid of //it pMeshData->pMesh->AddRef(); //Get the device IDirect3DDevice9 *pDevice = NULL; pMeshData->pMesh->GetDevice(&pDevice); //Get the mesh materials and create related textures D3DXMATERIAL mtrl; for(int i=0;i<NumMaterials;i++){ memcpy(&mtrl, &pMaterials[i], sizeof(D3DXMATERIAL)); boneMesh->Materials.push_back(mtrl.MatD3D); IDirect3DTexture9* pTexture = NULL; //If there is a texture associated with this material, load it into //the program if(mtrl.pTextureFilename != NULL){ wchar_t fname[MAX_PATH]; memset(fname, 0, sizeof(wchar_t)*MAX_PATH); mbstowcs(fname, mtrl.pTextureFilename, MAX_PATH); D3DXCreateTextureFromFile(pDevice, fname, &pTexture); boneMesh->Textures.push_back(pTexture); } else{ //Make sure we have the same number of elements in //Textures as we do Materials boneMesh->Textures.push_back(NULL); } } //Now we need to prepare the mesh for hardware skinning; as //mentioned earlier we need the bone offset matrices, and these are //stored in pSkinInfo. Here we get the bone offset matrices and //allocate memory for the local bone matrices that influence the //mesh. But of course this is only if skinning info is available. if(pSkinInfo != NULL){ boneMesh->pSkinInfo = pSkinInfo; pSkinInfo->AddRef(); DWORD maxVertInfluences = 0; DWORD numBoneComboEntries = 0; ID3DXBuffer* boneComboTable = 0; //Convert mesh to indexed blended mesh to add additional //vertex components; weights and influencing bone indices. //Store the new mesh in the bone mesh. pSkinInfo->ConvertToIndexedBlendedMesh(pMeshData->pMesh, D3DXMESH_MANAGED | D3DXMESH_WRITEONLY, 30, 0, //Not used 0, //Not used 0, //Not used 0, //Not used &maxVertInfluences, &numBoneComboEntries, &boneComboTable, &boneMesh->MeshData.pMesh); if(boneComboTable != NULL) //Not used boneComboTable->Release(); //As mentioned, the attribute table is used for selecting //materials and textures to render on the mesh. So we aquire //it here. boneMesh->MeshData.pMesh->GetAttributeTable(NULL, &boneMesh->NumAttributeGroups); boneMesh->attributeTable = new D3DXATTRIBUTERANGE[boneMesh->NumAttributeGroups]; boneMesh->MeshData.pMesh->GetAttributeTable(boneMesh->attributeTable, NULL); //Next we load the offset matrices and allocate memory for //the local bone matrices. skin info holds the number of bones //that influence this mesh in terms of the bones used to create //the skin. int NumBones = pSkinInfo->GetNumBones(); boneMesh->boneOffsetMatrices = new D3DXMATRIX[NumBones]; boneMesh->localBoneMatrices = new D3DXMATRIX[NumBones]; for(int i=0;i < NumBones;i++){ boneMesh->boneOffsetMatrices[i] = *(boneMesh->pSkinInfo->GetBoneOffsetMatrix(i)); } } //Return new mesh *ppNewMeshContainer = boneMesh; return S_OK; }
Hopefully you understood that code to create a mesh ready for animating.
But before we animate it we have to provide the mesh deallocation implementation. This is simply a case of deallocating the memory we allocated ourselves and releasing the COM objects used:
HRESULT AllocateHierarchyImp::DestroyMeshContainer(LPD3DXMESHCONTAINER pMeshContainerBase) { BoneMesh* boneMesh = (BoneMesh*)pMeshContainerBase; //Release textures int nElements = boneMesh->Textures.size(); for(int i=0;i<nElements;i++){ if(boneMesh->Textures[i] != NULL) boneMesh->Textures[i]->Release(); } //Delete local bone matrices and offset if we have skin info if(boneMesh->pSkinInfo != NULL){ delete[] boneMesh->localBoneMatrices; delete[] boneMesh->boneOffsetMatrices; delete[] boneMesh->attributeTable; } //Release mesh and skin info if(boneMesh->pSkinInfo){boneMesh->pSkinInfo->Release();} if(boneMesh->MeshData.pMesh){boneMesh->MeshData.pMesh->Release();} return S_OK; }
Now that the AllocateHierarchy functions are implemented we can go ahead and call D3DXLoadMeshHierarchyFromX passing the AllocateHierarchy object to it. This is done in the SkinnedMesh::Load function. When we call this function we retrieve a pointer to the root bone of the hierarchy that allows us to traverse the whole hierarchy with this one bone to calculate new matrices for animation for example. With just the root node we can add matrix pointers to each of our meshes that correspond to the world transformations of each bone and effectively will point to matrices that make up the animation when we animate the model.
In our SkinnedMesh::Load function is where we call D3DXLoadMeshHierarchyFromX. First we need to create an instance of the AllocateHierarchy; Our SkinnedMesh implementation then becomes:
SkinnedMesh::SkinnedMesh(){ } SkinnedMesh::~SkinnedMesh(){ } void SkinnedMesh::Load(WCHAR filename[]){ AllocateHierarchyImp boneHierarchy; D3DXLoadMeshHierarchyFromX(filename, D3DXMESH_MANAGED, g_pDevice, &boneHierarchy, NULL, &m_pRootNode, NULL); } void SkinnedMesh::Render(Bone *bone){ } void SkinnedMesh::CalculateWorldMatrices(Bone *child, D3DXMATRIX *parentMatrix){ D3DXMatrixMultiply(&child->WorldMatrix, &child->TransformationMatrix, parentMatrix); //Each sibling has same parent as child //so pass the parent matrix of this child if(child->pFrameSibling){ CalculateWorldMatrices((Bone*)child->pFrameSibling, parentMatrix); } //Pass child matrix as parent to children if(child->pFrameFirstChild){ CalculateWorldMatrices((Bone*)child->pFrameFirstChild, &child->WorldMatrix); } } void SkinnedMesh::AddBoneMatrixPointers(Bone *bone){ }
Now in our game object we can test whether the hierarchy of one of our .x files can be loaded. Place SkinnedMesh model1; in the private members of your Game object. And call model1.Load("Your file.x") in the Init() function of Game. You will need to put an .x file containing a bone hierarchy into the directory that the game runs from. You can test whether a hierarchy was loaded using a break point. Hopefully all is good. You loaded a bone hierarchy.
We still have a few things to setup before model animation can occur. With the HLSL shader that we create, we define an interpolation of vertices from a start pose to a final pose. Each start and end pose of an animation is known as an animation set and these should typically be stored in the .x file. Whenever we pass a vertex to the HLSL effect it will be updated to a new position and then that new position will be passed the HLSL effect file and be updated and this is how we create a skinned animation but we need to setup the matrices that make this animation work. We must pass the bone tranformations to this effect file uneffected by their parents in order to calculate new vertex positions with the vertex shader. For these tranformations we calculate them in our Render function using bone pointer matrices and bone offset matrices. So we need to add bone matrix pointers to each bone mesh of the hierarchy. Then when we render the mesh we can easily access these from the array of matrix pointers to the world matrices of the bones that affect the mesh skin.
These bone matrix pointers are added to each mesh that is affected by bones; we use pointers so that when a bone's transformation changes the pointers will be affected and resultingly the mesh skin too. To add pointers we must traverse the whole hierarchy and check for a bone mesh; if one exists and has skinning information, we add the matrix pointers to affecting bones. The affecting bones are the bones contained in pSkinInfo:
void SkinnedMesh::AddBoneMatrixPointers(Bone *bone){ if(bone->pMeshContainer != NULL){ BoneMesh* boneMesh=(BoneMesh*)bone->pMeshContainer; if(boneMesh->pSkinInfo != NULL){ //Get the bones affecting this mesh' skin. int nBones=boneMesh->pSkinInfo->GetNumBones(); //Allocate memory for the pointer array boneMesh->boneMatrixPtrs = new D3DXMATRIX*[nBones]; for(int i=0;i<nBones;i++){ Bone* bone=(Bone*)D3DXFrameFind(m_pRootNode, boneMesh->pSkinInfo->GetBoneName(i)); if(bone != NULL){ boneMesh->boneMatrixPtrs[i]=&bone->WorldMatrix; } else{ boneMesh->boneMatrixPtrs[i]=NULL; } } } } //Traverse Hierarchy if(bone->pFrameSibling){AddBoneMatrixPointers((Bone*)bone->pFrameSibling);} if(bone->pFrameFirstChild){AddBoneMatrixPointers((Bone*)bone->pFrameFirstChild);} }
We call this function passing the root node to it to setup all the pointers to influencing mesh bones world matrices. This is done after loading the hierarchy. Also add a call to CalculateWorldMatrices in the Load function to add world matrices to each of the bones. If you don't add these the model will be displayed as a jumble of meshes.
void SkinnedMesh::Load(WCHAR filename[]){ AllocateHierarchyImp boneHierarchy; if(SUCCEEDED(D3DXLoadMeshHierarchyFromX(filename, D3DXMESH_MANAGED, g_pDevice, &boneHierarchy, NULL, &m_pRootNode, NULL))){ D3DXMATRIX identity; D3DXMatrixIdentity(&identity); CalculateWorldMatrices((Bone*)m_pRootNode, &identity); AddBoneMatrixPointers((Bone*)m_pRootNode); } }
Now we need to free the memory of the matrix pointers when the skinned mesh is destroyed. This again involves traversing the hierarchy and freeing memory used by pointers. Add the following function to the SkinnedMesh class:
void SkinnedMesh::FreeBoneMatrixPointers(Bone *bone){ if(bone->pMeshContainer != NULL){ BoneMesh* boneMesh=(BoneMesh*)bone->pMeshContainer; if(boneMesh->boneMatrixPtrs != NULL){ delete[] boneMesh->boneMatrixPtrs; } } //Traverse Hierarchy if(bone->pFrameSibling){FreeBoneMatrixPointers((Bone*)bone->pFrameSibling);} if(bone->pFrameFirstChild){FreeBoneMatrixPointers((Bone*)bone->pFrameFirstChild);} }
And call it on SkinnedMesh destruction.
SkinnedMesh::~SkinnedMesh(){ FreeBoneMatrixPointers((Bone*)m_pRootNode); }
Animating a Hierarchy
Finally everything is set up for us to add the skinning effect and animate the model with an animation controller. First we create the effect; we will make this global so we can use it in the Render function of our SkinnedMesh.
ID3DXEffect* g_pEffect=NULL;
This effect will be our interface to the HLSL effect file. We can upload variables to the file through this interface. Create a new effect file called skinning.fx; this is simply an ASCII text file. This will be our shader that performs skinning. We create the effect with D3DXCreateEffectFromFile(). Call this from the Init function of your Game object. Just set flags to D3DXSHADER_DEBUG for now because the shader is not known to work yet.
//Create effect //Only continue application if effect compiled successfully if(FAILED(D3DXCreateEffectFromFile(g_pDevice, L"skinning.fx", NULL, NULL, D3DXSHADER_DEBUG, NULL, &g_pEffect, NULL))){ return E_FAIL; }
Modify OnDeviceLost and OnDeviceGained to cater for the effect file:
void Game::OnDeviceLost() { try { //Add OnDeviceLost() calls for DirectX COM objects g_pEffect->OnLostDevice(); m_deviceStatus = DEVICE_NOTRESET; } catch(...) { //Error handling code } } void Game::OnDeviceGained() { try { g_pDevice->Reset(&m_pp); //Add OnResetDevice() calls for DirectX COM objects g_pEffect->OnResetDevice(); m_deviceStatus = DEVICE_LOSTREADY; } catch(...) { //Error handling code } }
Now we need to implement the Render function of the SkinnedMesh and the HLSL file. In this file we calculate both skinning and lighting of the model. We first define our vertex structure that corresponds to the vertex structure of the index blended mesh; this will be the input vertex data to the shader:
struct VS_INPUT_SKIN { float4 position: POSITION0; float3 normal: NORMAL; float2 tex0: TEXCOORD0; float4 weights: BLENDWEIGHT0; int4 boneIndices: BLENDINDICES0; };
Here we get the position of the vertex that we will modify in the shader; and we get the normal, which is the direction vector of the vertex and used in lighting. The weights are the weights of the affecting bones, which can be found by the bone indices. We use the weights and the bone matrices to determine the new position of the vertex for each vertex and normals. Therefore we store the matrices in a matrix array as follows:
extern float4x4 BoneMatrices[40];
To calculate the new vertex positions we apply each bone weight to a multiplication of the original vertex position and the bone transformation matrix and sum up the results. However there is one more thing - the combination of weights must add up to 1, which is equivelent to 100%. See, each weight applies a percentage of effect on a vertex so they must add up to 1. Therefore we calculate the last weight as 1-totalWeights; one minus the sum total so that they definitely add up to one.
Here is the complete shader for performing skin and lighting:
//World and View*Proj Matrices matrix matWorld; matrix matVP; //Light Position float3 lightPos; //Texture texture texDiffuse; //Skinning variables extern float4x4 BoneMatrices[40]; extern int MaxNumAffectingBones = 2; //Sampler sampler DiffuseSampler = sampler_state { Texture = (texDiffuse); MinFilter = Linear; MagFilter = Linear; MipFilter = Linear; AddressU = Wrap; AddressV = Wrap; AddressW = Wrap; MaxAnisotropy = 16; }; //Vertex Output / Pixel Shader Input struct VS_OUTPUT { float4 position : POSITION0; float2 tex0 : TEXCOORD0; float shade : TEXCOORD1; }; //Vertex Input struct VS_INPUT_SKIN { float4 position : POSITION0; float3 normal : NORMAL; float2 tex0 : TEXCOORD0; float4 weights : BLENDWEIGHT0; int4 boneIndices : BLENDINDICES0; }; VS_OUTPUT vs_Skinning(VS_INPUT_SKIN IN) { VS_OUTPUT OUT = (VS_OUTPUT)0; float4 v = float4(0.0f, 0.0f, 0.0f, 1.0f); float3 norm = float3(0.0f, 0.0f, 0.0f); float lastWeight = 0.0f; IN.normal = normalize(IN.normal); for(int i = 0; i < MaxNumAffectingBones-1; i++) { //Multiply position by bone matrix v += IN.weights[i] * mul(IN.position, BoneMatrices[IN.boneIndices[i]]); norm += IN.weights[i] * mul(IN.normal, BoneMatrices[IN.boneIndices[i]]); //Sum up the weights lastWeight += IN.weights[i]; } //Make sure weights add up to 1 lastWeight = 1.0f - lastWeight; //Apply last bone v += lastWeight * mul(IN.position, BoneMatrices[IN.boneIndices[MaxNumAffectingBones-1]]); norm += lastWeight * mul(IN.normal, BoneMatrices[IN.boneIndices[MaxNumAffectingBones-1]]); //Get the world position of the vertex v.w = 1.0f; float4 posWorld = mul(v, matWorld); OUT.position = mul(posWorld, matVP); //Output texture coordinate is same as input OUT.tex0 = IN.tex0; //Calculate Lighting norm = normalize(norm); norm = mul(norm, matWorld); OUT.shade = max(dot(norm, normalize(lightPos - posWorld)), 0.2f); return OUT; } //Pixel Shader float4 ps_Lighting(VS_OUTPUT IN) : COLOR0 { float4 color = tex2D(DiffuseSampler, IN.tex0); return color * IN.shade; } technique Skinning { pass P0 { VertexShader = compile vs_2_0 vs_Skinning(); PixelShader = compile ps_2_0 ps_Lighting(); } }
The technique tells the system to use vertex and pixel shader version 2 and to pass the output of vs_Skinning to the input of ps_Lighting. The pixel shader ps_Lighting essentially "Lights" the pixels of the texture.
Now that we have the vertex shader, we can use it. And render the model. In this Render function we get a handle to the technique with g_pEffect->GetTechniqueByName("Skinning") and pass the mesh to it; and glory be the shader will perform its work. The Render function will be called on the root bone and traverse the bone hierarchy and render each of the mesh containers associated with the bones of the hierarchy.
Here is the Render function:
void SkinnedMesh::Render(Bone *bone){ //Call the function with NULL parameter to use root node if(bone==NULL){ bone=(Bone*)m_pRootNode; } //Check if a bone has a mesh associated with it if(bone->pMeshContainer != NULL) { BoneMesh *boneMesh = (BoneMesh*)bone->pMeshContainer; //Is there skin info? if (boneMesh->pSkinInfo != NULL) { //Get the number of bones influencing the skin //from pSkinInfo. int numInflBones = boneMesh->pSkinInfo->GetNumBones(); for(int i=0;i < numInflBones;i++) { //Get the local bone matrices, uneffected by parents D3DXMatrixMultiply(&boneMesh->localBoneMatrices[i], &boneMesh->boneOffsetMatrices[i], boneMesh->boneMatrixPtrs[i]); } //Upload bone matrices to shader. g_pEffect->SetMatrixArray("BoneMatrices", boneMesh->localBoneMatrices, boneMesh->pSkinInfo->GetNumBones()); //Set world transform to identity; no transform. D3DXMATRIX identity; D3DXMatrixIdentity(&identity); //Render the mesh for(int i=0;i < (int)boneMesh->NumAttributeGroups;i++) { //Use the attribute table to select material and texture attributes int mtrlIndex = boneMesh->attributeTable[i].AttribId; g_pDevice->SetMaterial(&(boneMesh->Materials[mtrlIndex])); g_pDevice->SetTexture(0, boneMesh->Textures[mtrlIndex]); g_pEffect->SetMatrix("matWorld", &identity); //Upload the texture to the shader g_pEffect->SetTexture("texDiffuse", boneMesh->Textures[mtrlIndex]); D3DXHANDLE hTech = g_pEffect->GetTechniqueByName("Skinning"); g_pEffect->SetTechnique(hTech); g_pEffect->Begin(NULL, NULL); g_pEffect->BeginPass(0); //Pass the index blended mesh to the technique boneMesh->MeshData.pMesh->DrawSubset(mtrlIndex); g_pEffect->EndPass(); g_pEffect->End(); } } } //Traverse the hierarchy; Rendering each mesh as we go if(bone->pFrameSibling!=NULL){Render((Bone*)bone->pFrameSibling);} if(bone->pFrameFirstChild!=NULL){Render((Bone*)bone->pFrameFirstChild);} }
Now that we have the shader in place and the render function we can aquire an animation controller and control the animations of the model.
We get an animation controller from the D3DXLoadHierarchyFromX function. We will add this controller to the SkinnedMesh class in the private members:
ID3DXAnimationController* m_pAnimControl;
Then in our SkinnedMesh Load function add this as a parameter to D3DXLoadMeshHierarchyFromX:
D3DXLoadMeshHierarchyFromX(filename, D3DXMESH_MANAGED, g_pDevice, &boneHierarchy, NULL, &m_pRootNode, &m_pAnimControl)
The way animation works is there are a number of animation sets stored in the model or .x file that correspond to different animation cycles such as a walk animation or jump. Depending on the character, each animation has a name and we can set the current animation using its name. If we have different characters we will want to access or set different animations per character using name strings, e.g. SetAnimation("Walk"), SetAnimation("Sit") like that. For this we can use a map of strings to animation IDs. With this map we can get the ID of each animation set by using the name along with the map. A map has an array of keys and values associated with those keys. The key here is the name of the animation and the value is its animation set ID.
Lastly once we have the animation sets we can play an animation by calling m_pAnimControl->AdvanceTime(time, NULL);
First let's get the animation sets and store their names and IDs in a map. For this we will create a function in our SkinnedMesh class void GetAnimationSets(). Create a map by including <map> and make sure using namespace std; is at the top of the SkinnedMesh cpp file. Create the map called map<string, dword="DWORD">animationSets; For this you will also need to include <string>. using namespace std means we can use map and string without std::map or std::string for example if you didn't know already. We will also add another two functions void SetAnimation(string name) and void PlayAnimation(float time).
The skinned mesh now looks like: class SkinnedMesh{ public: SkinnedMesh(); ~SkinnedMesh(); void Load(WCHAR filename[]); void Render(Bone* bone); private: void CalculateWorldMatrices(Bone* child, D3DXMATRIX* parentMatrix); void AddBoneMatrixPointers(Bone* bone); void FreeBoneMatrixPointers(Bone* bone); //Animation functions void GetAnimationSets(); D3DXFRAME* m_pRootNode; ID3DXAnimationController* m_pAnimControl; map<string, int>animationSets; public: void SetAnimation(string name); void PlayAnimation(D3DXMATRIX world, float time); };
We get and save the animation sets to our map with the following function:
void SkinnedMesh::GetAnimationSets(){ ID3DXAnimationSet* pAnim=NULL; for(int i=0;i<(int)m_pAnimControl->GetMaxNumAnimationSets();i++) { pAnim=NULL; m_pAnimControl->GetAnimationSet(i, &pAnim); //If we found an animation set, add it to the map if(pAnim != NULL) { string name = pAnim->GetName(); animationSets[name]=i;//Creates an entry pAnim->Release(); } } }
We set the active animation set with SetAnimation:
void SkinnedMesh::SetAnimation(string name){ ID3DXAnimationSet* pAnim = NULL; //Get the animation set from the name. m_pAnimControl->GetAnimationSet(animationSets[name], &pAnim); if(pAnim != NULL) { //Set the current animation set m_pAnimControl->SetTrackAnimationSet(0, pAnim); pAnim->Release(); } }
When we update the game we call the following function to play the active animation:
void SkinnedMesh::PlayAnimation(D3DXMATRIX world, float time){ //The world matrix here allows us to position the model in the scene. m_pAnimControl->AdvanceTime(time, NULL);//Second parameter not used. //Update the matrices that represent the pose of animation. CalculateWorldMatrices((Bone*)m_pRootNode, &world); }
Usage:
In the SkinnedMesh::Load function add the following line on hierarchy load success:
//Save names of sets to map GetAnimationSets();
In the Game::Init function, set the active animation set - for example if we have a walk cycle animation:
string set="Walk"; model1.SetAnimation(set);
And then play the animation in Game::Update():
D3DXMATRIX identity; D3DXMatrixIdentity(&identity); model1.PlayAnimation(identity, deltaTime*0.5f);
There is one more thing we often want to do. That is render static non-moving meshes that are part of the hierarchy. We might for example have a mesh that doesn't have skin info; so we need to save this; Notice that we create an indexed blended mesh only if there is skin info. Well now we need to save the mesh if there is no skin info:
In our CreateMeshContainer function:
if(pSkinInfo != NULL){ //... } else{ //We have a static mesh boneMesh->MeshData.pMesh = pMeshData->pMesh; boneMesh->MeshData.Type = pMeshData->Type; }
Now we have the static meshes saved in BoneMesh structures we can render them. But we need first to light the mesh with a lighting shader; Add this to the shader file:
//Vertex Input struct VS_INPUT { float4 position : POSITION0; float3 normal : NORMAL; float2 tex0 : TEXCOORD0; }; VS_OUTPUT vs_Lighting(VS_INPUT IN) { VS_OUTPUT OUT = (VS_OUTPUT)0; float4 posWorld = mul(IN.position, matWorld); float4 normal = normalize(mul(IN.normal, matWorld)); OUT.position = mul(posWorld, matVP); //Calculate Lighting OUT.shade = max(dot(normal, normalize(lightPos - posWorld)), 0.2f); //Output texture coordinate is same as input OUT.tex0=IN.tex0; return OUT; } technique Lighting { pass P0 { VertexShader = compile vs_2_0 vs_Lighting(); PixelShader = compile ps_2_0 ps_Lighting(); } }
Now we can render the static meshes of our model; We have a static mesh if there is no pSkinInfo. Here we set the lighting technique to active and render the mesh with texturing:
if (boneMesh->pSkinInfo != NULL) { //... } else{ //We have a static mesh; not animated. g_pEffect->SetMatrix("matWorld", &bone->WorldMatrix); D3DXHANDLE hTech = g_pEffect->GetTechniqueByName("Lighting"); g_pEffect->SetTechnique(hTech); //Render the subsets of this mesh with Lighting for(int mtrlIndex=0;mtrlIndex<(int)boneMesh->Materials.size();mtrlIndex++){ g_pEffect->SetTexture("texDiffuse", boneMesh->Textures[mtrlIndex]); g_pEffect->Begin(NULL, NULL); g_pEffect->BeginPass(0); //Pass the index blended mesh to the technique boneMesh->MeshData.pMesh->DrawSubset(mtrlIndex); g_pEffect->EndPass(); g_pEffect->End(); } }
That's us done! We have an animated model that we can work with. We can set the active animation and render it with skinning!
Saving an .x File
In this part we will export an animation hierarchy from 3d studio max.
- Place the exporter plugin in the plugins directory of max. Fire up 3d Studio Max. New Empty Scene.
- Go to Helpers in the right-hand menu.
- Select CATParent.
- Click and drag in the perspective viewport to create the CATParent triangle object.
- Double click Base Human in the CATRig Load Save list.
- Model a rough human shaped character around the bones. E.g. create new box edit mesh.
- Click on the CATRig triangle. Go to motion in the menu tabs.
- Click and hold Abs and select the little man at the bottom.
- Press the stop sign to activate the walk cycle.
- Go to modifiers and select skin modifier. On the properties sheet you will see Bones:; click Add. Select all the bones. And click Select. Now if you play the animation the skin should follow the bones.
- Lastly, you need to add a material to the mesh, click the material editor icon and drag a material to the mesh.
- Now export the scene or selected model using the exporter plugin. You will need to export vertex normals animation and select Y up as the worlds up direction to suit the game. Select Animation in the export dialog. And select skinning! can't forget that. Set a frame range e.g. 0 to 50 and call the set "walk". Click Add Animation Set. And Save the model.
- Now you can check that the model exported successfully using DXViewer utility that comes with the DirectX SDK.
- Now you can try loading the model into the program.
You may have to adjust the camera. E.g.
D3DXMatrixLookAtLH(&view, &D3DXVECTOR3(0,0.0f,-20.0f), &D3DXVECTOR3(0.0f, 10.0f, 0.0f), &D3DXVECTOR3(0.0f, 1.0f, 0.0f));
Article Update Log
12 April 2014: Initial release
17 April 2014: Updated included download
22 April 2014: Updated
24 April 2014: Updated