一篇通俗易懂的.x文件解析+骨骼动画实现的文章

来源:互联网 发布:c语言的运算程序 编辑:程序博客网 时间:2024/05/19 00:38

 一篇通俗易懂的.x文件解析+骨骼动画实现的文章(需要国外代理看)
网址:http://www.informikon.com/various/the-simplest-skeletal-animation-possible.html
推荐代理:http://www.9i7.cn/(http://www.orzin.com/)

该站内还有一些ManagedCode环境下使用DirectShow的经验文章

The simplest skeletal animation possible    

This document presents, possibly, the simplest skeletal animation sample. A zip file with code can be found here.

Eversince I got my hands on the application Poser, I wanted to create my own utilities to do something similar. At the time, the only information in the DirectX SDK about this stuff could be found in the "SkinnedMesh sample". I recall searching the DirectX mailing list for the string "Craig Peeper" because he was the MS developer who answered the questions about this sample.

Then Jim Adams "Advanced Animation" book helped clear further the subject and Tom Miller's "KickStart" contains a chapter about skeletal animation. Since I had to refresh my mind about this stuff recently, I decided to create the simplest possible code to get "Miss Tiny" up and walking. The sample is strongly influenced by Miller's chapter. Another very good reference about skin meshes is this pdf.

Before starting to look at the code, I believe it is useful to take a look at the content of an x file with bones and animation. Only after being familiar with all the data that need to be processed, we can start to take a look at the functionality provided in the DirectX Extension library. So if you open "tiny.x" in a text editor, you'll find, after some header and template definitions, the following:

  1.  
  2. Frame Scene_Root {
  3. FrameTransformMatrix {
  4. 1.000000,0.000000,0.000000,0.000000,
  5. 0.000000,1.000000,0.000000,0.000000,
  6. 0.000000,0.000000,1.000000,0.000000,
  7. 0.000000,0.000000,0.000000,1.000000;;
  8. }
  9. Frame body {...
 

You have a "Frame" named "Scene_Root" followed by a "FrameTransformMatrix" then the beginning of another frame (named "body"). A little further, you'll find the mesh information:

  1. Mesh {
  2. 4432;
  3. -34.720058;-12.484819;48.088928;,
 

The mesh contains 4432 vertices; the coordinates for these vertices are all listed. Inside the curly braces for the mesh data, you'll find many other template instances. You'll find materials, textures, normals informations like in any other mesh but also what make a skin mesh special the "XSkinMeshHeader" which tell how many bones we can find and the "SkinWeights" template instances with information about one particular bone.

  1. XSkinMeshHeader {
  2. 2;
  3. 4;
  4. 35;
  5. }
  6. SkinWeights {
  7. "Bip01_R_UpperArm";
  8. 156;
  9. 0,
 

Here, we have a mesh with 35 bones and the bone named "Bip01_R_UpperArm" affects 156 vertices. These bones information are contained inside the "curly braces" for the mesh object. After the mesh and its "frame object" ends, you'll find frames with the bone names, for example:

  1. Frame Bip01_Head {
  2. FrameTransformMatrix {
  3. 0.979775,-0.188667,-0.066683,0.000000,
  4. 0.195389,0.973921,0.115333,0.000000,
  5. 0.043184,-0.126029,0.991086,0.000000,
  6. 27.900173,0.000003,0.000000,1.000000;;
  7. }
  8. Frame Dummy21 {
  9. FrameTransformMatrix {
  10. 1.000000,-0.000000,-0.000000,0.000000,
  11. 0.000000,1.000000,0.000000,0.000000,
  12. 0.000000,-0.000000,1.000000,0.000000,
  13. 59.615768,0.000027,0.000002,1.000000;;
  14. }
  15. }
  16. }
 

These are the "TransformMatrix" for each bone. They represent the transformation that has to be apply to the vertices in order to be properly positioned with respect to the parent bone.

Closer to the end of the file, we find the animation data:

  1.  
  2. AnimationSet {
  3. Animation {
  4. AnimationKey {
  5. 4;
  6. 2;
  7. 0;16;
  8. 1.000000,0.000000,0.000000,0.000000,
  9. 0.000000,1.000000,0.000000,0.000000,
  10. 0.000000,0.000000,1.000000,0.000000,
  11. 0.000000,0.000000,0.000000,1.000000;;,
  12. 4960;16;
  13. 1.000000,0.000000,0.000000,0.000000,
  14. 0.000000,1.000000,0.000000,0.000000,
  15. 0.000000,0.000000,1.000000,0.000000,
  16. 0.000000,0.000000,0.000000,1.000000;;;
  17. }
  18. { Scene_Root }
  19. }...
 

We have an "AnimationSet" followed by an "Animation". Then the "AnimationKey" applies to the "Scene_Root" frame. The "AnimationKey" has a keytype 4 (for matrix keys) with 2 keys at "time" 0 and 4960. Our code needs to handle all this data.

Now that we had a peek at the kind of data involved in skeletal animation, let's have a look at the facilities provided by DirectX to deal with such data. One of the first structure we'll encounter is the AnimationRootFrame, if you look up its documentation, you'll see only two properties: the AnimationController and the "FrameHierarchy" which is just a Frame object (a name wrapped around a transformation). The AnimationController will only be used for its AdvanceTime method which takes care of interpolation for times during the animation between keyframes. The "FrameHierarchy" property will point to the "top level" transformation in our x file.

To build this hierachy of frames will call the static method LoadHierachyFromFile of the class Mesh. One of the arguments to this method is an object of type AllocateHierarchy. This is an abstract class with two important methods: CreateFrame and CreateMeshContainer.

The method LoadHierarchyFromFile is responsible for parsing all the data in the x file. The "Create..." methods of the AllocateHierarchy object that we pass to LoadHierarchyFromFile will be called for every "Frame"s and "Mesh"es template instances found in the x file. Both of these "Create..." methods return objects derived from an abstract class; so we'll have to implement these two abstract classes by deriving new classes from Frame and MeshContainer to store the information that we need for our processing. The MeshContainer class contains some important properties: MeshData, SkinInformation The MeshData is just a structure for the mesh data found in the x file (stored in the MeshData property Mesh). The SkinInformation is used to store the information of the "SkinWeights" template instances found in the x file.

After these preliminaries, we can look at the code. We start with some standard using statement and VS-generated code. Then we found some fields:

  1. Timer timer; // use timer to refresh the display
  2. Device device; // the device for DX
  3. float radius; // the radius and center of the mesh
  4. Vector3 center;
  5. AnimationRootFrame rootFrame; // the root frame object
 

We'll use a "Windows Form" timer to be reminded to refresh the display, we need a DirectX Device object. The "radius" and "center" variables will be used in setting the "View" and "Projection" transform. And the "rootFrame" is used to access its FrameHierarchy and AnimationController properties. After some pretty standard code for a Managed DirectX application, we call, in "InitializeGraphics", the method "LoadAnim":

  1. void LoadAnim(string fileName)
  2. {
  3. MyAllocateHierarchy alloc = new MyAllocateHierarchy();
  4. // build the hierarchy of frames from the x file using our alloc object
  5. rootFrame = Mesh.LoadHierarchyFromFile(fileName, MeshFlags.Managed,
  6. device, alloc, null);
  7. radius = Frame.CalculateBoundingSphere(rootFrame.FrameHierarchy,
  8. out center);
  9. // connect the bones the frames in the mesh
  10. AttachBones((MyFrame)rootFrame.FrameHierarchy);
  11. }
 


"LoadAnim" create our derived AllocateHierarchy object and call the static LoadHierarchyFromFile with this object for one of its arguments. This method returns our "rootFrame" object. Then we calculate the values for the variables "radius" and "center" and we call the method "AttachBones".

  1. //
  2. // walk the tree and attach each bone
  3. //
  4. void AttachBones(MyFrame frame)
  5. {
  6. if (frame.MeshContainer != null)
  7. {
  8. AttachBones((MyMeshContainer)frame.MeshContainer);
  9. }
  10. if (frame.FrameSibling != null)
  11. {
  12. AttachBones((MyFrame)frame.FrameSibling);
  13. }
  14. if (frame.FrameFirstChild != null)
  15. {
  16. AttachBones((MyFrame)frame.FrameFirstChild);
  17. }
  18. }
 

This method recursively goes through the "FrameHierarchy" and calls itself or the the method with the same name but using a MeshContainer as argument. The "AttachBones" with a MeshContainer method retrieves the bone transformations and stores it away in a field our MeshContainer derived class. If you look back at "tiny.x", you'll see that the "Frame" template instance contains the "Mesh" template instance and inside the "Mesh" template instance you find all the "SkinWeights" template instances. But after the "Frame" template instance ends, you have a bunch of "Frame" template instances wrapped around a bone name. The "AttachBones" method connect the "SkinWeight" template instance inside the "Mesh" template with the "Frame" transform of the same name.

  1. //
  2. // when we have a mesh container, we set its frameMatrices accordingly
  3. //
  4. void AttachBones(MyMeshContainer mesh)
  5. {
  6. if (mesh.SkinInformation != null)
  7. {
  8. int numBones = mesh.SkinInformation.NumberBones;
  9. // for each bone, find the frame of the same name
  10. MyFrame[] frameMatrices = new MyFrame[numBones];
  11. for (int i = 0; i < numBones; i++)
  12. {
  13. MyFrame frame = (MyFrame)Frame.Find(rootFrame.FrameHierarchy,
  14. mesh.SkinInformation.GetBoneName(i));
  15. if (frame == null)
  16. throw new ArgumentException();
  17. // asssign the found frame into the MeshContaienr frameMatrices
  18. frameMatrices[i] = frame;
  19. }
  20. mesh.FrameMatrices = frameMatrices;
  21. }
  22. }
 

Looking at this code, we notice that our MeshContainer derived class will need a field for an array of transform (named "FrameMatrices"). Now that we have all our matrices setup properly, we can draw our scene:

  1. protected override void OnPaint(System.Windows.Forms.PaintEventArgs e)
  2. {
  3. // move our animation ahead (the constant 0.05 was obtained by trial and error)
  4. if (rootFrame.AnimationController != null)
  5. rootFrame.AnimationController.AdvanceTime(0.05f, null);
  6. // update all the hierarchy of frames with the new transform values
  7. UpdateFrameMatrices((MyFrame)rootFrame.FrameHierarchy, Matrix.Identity);
  8. device.Clear(ClearFlags.Target | ClearFlags.ZBuffer, Color.Blue, 1.0f, 0);
  9. device.BeginScene();
  10. // draw the animation
  11. DrawFrame((MyFrame)rootFrame.FrameHierarchy);
  12. device.EndScene();
  13. device.Present();
  14. }
 

We override OnPaint by first calling AdvanceTime then we have to update all our transforms in the "FrameHierarchy" to reflect the new transforms in the bones. This is done with the call to "UpdateFrameMatrices":

  1. //
  2. // walk the tree and update the hierarchy
  3. //
  4. void UpdateFrameMatrices(MyFrame frame, Matrix parentMatrix)
  5. {
  6. // first transform with the parent
  7. frame.Combined = frame.TransformationMatrix *
  8. parentMatrix;
  9. // if sibling, use transform with parent
  10. if (frame.FrameSibling != null)
  11. {
  12. UpdateFrameMatrices((MyFrame)frame.FrameSibling, parentMatrix);
  13. }
  14. // if child, transform with new combined matrix
  15. if (frame.FrameFirstChild != null)
  16. {
  17. UpdateFrameMatrices((MyFrame)frame.FrameFirstChild, frame.Combined);
  18. }
  19. }
 

"UpdateFrameMatrices update the "Combined" matrix with the "parentMatrix" passed as argument. When we have a sibling transform, we update with this same "parentMatrix" and when we have a child transform, we update with the newly combined matrix. After that, we are ready to call "DrawFrame". The "DrawFrame" method is in two parts. First, we draw each mesh container with the method "DrawMeshContainer" then we just walk the tree recursively.

  1. //
  2. // walk the tree of frames and draw each one
  3. //
  4. void DrawFrame(MyFrame frame)
  5. {
  6. MyMeshContainer mesh = (MyMeshContainer)frame.MeshContainer;
  7. while (mesh != null)
  8. {
  9. DrawMeshContainer(mesh);
  10. mesh = (MyMeshContainer)mesh.NextContainer;
  11. }
  12. if (frame.FrameSibling != null)
  13. {
  14. DrawFrame((MyFrame)frame.FrameSibling);
  15. }
  16. if (frame.FrameFirstChild != null)
  17. {
  18. DrawFrame((MyFrame)frame.FrameFirstChild);
  19. }
  20. }
 

"DrawMeshContainer" is where the "real" work gets done. Before we look at the code for this method, we'll first start to look at its argument: an object of type "MyMeshContainer" which is defined as follow:

  1.  
  2. public class MyMeshContainer : MeshContainer
  3. {
  4. // I'm not a big fan of properties
  5. public Texture[] Textures = null;
  6. public int NumberAttributes = 0;
  7. public int NumberInfluences = 0;
  8. public BoneCombination[] BoneTable;
  9. public MyFrame[] FrameMatrices;
  10. public Matrix[] OffsetMatrices;
  11. }
 

It's just a MeshContainer with some extra data. The objects of type "MyMeshContainer" are created when we call LoadHierarchyFromFile with our AllocateHierarchy-derived object. This last object calls CreateMeshContainer whose code is next:

  1. public override MeshContainer CreateMeshContainer(string name,
  2. MeshData meshData, ExtendedMaterial[] materials,
  3. EffectInstance[] effectInstances, GraphicsStream adjacency,
  4. SkinInformation skinInfo)
  5. {
  6. // create the container object and set some of its properties
  7. MyMeshContainer mesh = new MyMeshContainer();
  8. mesh.Name = name;
  9. int numFaces = meshData.Mesh.NumberFaces;
  10. Device dev = meshData.Mesh.Device;
  11. mesh.SetMaterials(materials);
  12. mesh.SetAdjacency(adjacency);
  13. // fill in the texture info
  14. Texture[] meshTextures = new Texture[materials.Length];
  15. for (int i = 0; i < materials.Length; i++)
  16. {
  17. if (materials[i].TextureFilename != null)
  18. {
  19. meshTextures[i] = TextureLoader.FromFile(dev, materials[i].TextureFilename);
  20. }
  21. }
  22. mesh.Textures = meshTextures;
  23. // if needed, fill in the skin info
  24. if (skinInfo != null)
  25. {
  26. mesh.SkinInformation = skinInfo;
  27. // stores the bone offset matrix away
  28. int numBones = skinInfo.NumberBones;
  29. Matrix[] offsetMatrices = new Matrix[numBones];
  30. for (int i = 0; i < numBones; i++)
  31. offsetMatrices[i] = skinInfo.GetBoneOffsetMatrix(i);
  32. mesh.OffsetMatrices = offsetMatrices;
  33. // Fill the skin info by calling ConvertToBlendedMesh, this generates the BoneTable
  34. meshData.Mesh = skinInfo.ConvertToBlendedMesh(meshData.Mesh, MeshFlags.Managed
  35. | MeshFlags.OptimizeVertexCache, mesh.GetAdjacencyStream(),
  36. out mesh.NumberInfluences, out mesh.BoneTable);
  37. mesh.NumberAttributes = mesh.BoneTable.Length;
  38. }
  39. // use new mesh for our drawing
  40. mesh.MeshData = meshData;
  41. return mesh;
  42. }
 

We'll concentrate on what is new with skin meshes. First, we assign to the property SkinInformation the argument of type "SkinInformation" passed into the method. This method gets called when we load the hierarchy from an x file. If the mesh is a skin mesh, it will contain a "XSkinMeshHeader" template instance; so we store this information away. Then the matrix at the end of each "SkinWeight" template instance is also stored away in the field "OffsetMatrices".

Now, we have to look at the call ConvertToBlendedMesh. When we add "bones" to a mesh (in a modeling application), we have vertices that are affected by more than one bone (so we have a smooth deformation when the elbow bends, for example). The method ConvertToBlendedMesh takes all the vertices and all the bones and creates a new mesh with subsets that can be drawn in a single call. Moreover, it returns a BoneCombination table that we'll use to perform "geometry blending". We define the field NumberAttibutes to be the "BoneTable" length.

We can look at the code for "DrawMeshContainer":

 

  1. //
  2. // when we have a mesh container with skin info, we set the proper
  3. // blending information
  4. //
  5. void DrawMeshContainer(MyMeshContainer mesh)
  6. {
  7. if (mesh.SkinInformation != null)
  8. {
  9. Matrix[] offsetMatrices = mesh.OffsetMatrices;
  10. MyFrame[] frameMatrices = mesh.FrameMatrices;
  11. // for each subset with similar attrib, draw it
  12. for (int iattrib = 0; iattrib < mesh.NumberAttributes; iattrib++)
  13. {
  14. // set the proper world matrix for blending
  15. BoneCombination boneCombo = mesh.BoneTable[iattrib];
  16. for (int i = 0; i < mesh.NumberInfluences; i++)
  17. {
  18. int matrixIndex = boneCombo.BoneId[i];
  19. // it seems when the BoneId is not -1, the matrix index is valid
  20. // where is the doc for this???
  21. if (matrixIndex != -1)
  22. {
  23. Matrix tempMatrix = offsetMatrices[matrixIndex] *
  24. frameMatrices[matrixIndex].Combined;
  25. device.Transform.SetWorldMatrixByIndex(i, tempMatrix);
  26. }
  27. }
  28. // set the render state, material and texture
  29. device.RenderState.VertexBlend = (VertexBlend)boneCombo.BoneId.Length - 1;
  30. device.Material = mesh.GetMaterials()[boneCombo.AttributeId].Material3D;
  31. device.SetTexture(0, mesh.Textures[boneCombo.AttributeId]);
  32. // draw the subset
  33. mesh.MeshData.Mesh.DrawSubset(iattrib);
  34. }
  35. }
  36. }
 

We grab the "FrameMatrices" and the "OffsetMatrices" and for every subsets, we set the "world matrix" to perform "geometry blending" (there is a good discussion of this technique in the DirectX documentation). This "world matrix" is obtained by composing the transformations for the bone offset and the updated transform matrix whose index is given by the property BoneId. The BoneCombination table returned by ConvertToBlendedMesh contains an entry for each subset, and each entry has an AttributeId and a BoneId. The BoneId is an array of indexes to perform "geometry blending".

Then we set the RenderState property VertexBlend to the length of the BoneId array, and after we have set the materials and textures and we are ready to draw the corresponding subset.

This might be the simplest skeletal animation program in MDX. The program is not efficient nor robust. But it serves to illustrate the minimal amount of code needed to perform such animation in Managed DirectX.

原创粉丝点击