SkinnedModelProcessor.cs 9.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258
  1. #region File Description
  2. //-----------------------------------------------------------------------------
  3. // SkinnedModelProcessor.cs
  4. //
  5. // Microsoft XNA Community Game Platform
  6. // Copyright (C) Microsoft Corporation. All rights reserved.
  7. //-----------------------------------------------------------------------------
  8. #endregion
  9. #region Using Statements
  10. using System;
  11. using System.IO;
  12. using System.Collections.Generic;
  13. using Microsoft.Xna.Framework;
  14. using Microsoft.Xna.Framework.Content.Pipeline;
  15. using Microsoft.Xna.Framework.Content.Pipeline.Graphics;
  16. using Microsoft.Xna.Framework.Content.Pipeline.Processors;
  17. using SkinnedModel;
  18. using Microsoft.Xna.Framework.Graphics;
  19. #endregion
  20. namespace SkinnedModelPipeline
  21. {
  22. /// <summary>
  23. /// Custom processor extends the builtin framework ModelProcessor class,
  24. /// adding animation support.
  25. /// </summary>
  26. [ContentProcessor]
  27. public class SkinnedModelProcessor : ModelProcessor
  28. {
  29. /// <summary>
  30. /// The main Process method converts an intermediate format content pipeline
  31. /// NodeContent tree to a ModelContent object with embedded animation data.
  32. /// </summary>
  33. public override ModelContent Process(NodeContent input,
  34. ContentProcessorContext context)
  35. {
  36. ValidateMesh(input, context, null);
  37. // Find the skeleton.
  38. BoneContent skeleton = MeshHelper.FindSkeleton(input);
  39. if (skeleton == null)
  40. throw new InvalidContentException("Input skeleton not found.");
  41. // We don't want to have to worry about different parts of the model being
  42. // in different local coordinate systems, so let's just bake everything.
  43. FlattenTransforms(input, skeleton);
  44. // Read the bind pose and skeleton hierarchy data.
  45. IList<BoneContent> bones = MeshHelper.FlattenSkeleton(skeleton);
  46. if (bones.Count > SkinnedEffect.MaxBones)
  47. {
  48. throw new InvalidContentException(string.Format(
  49. "Skeleton has {0} bones, but the maximum supported is {1}.",
  50. bones.Count, SkinnedEffect.MaxBones));
  51. }
  52. List<Matrix> bindPose = new List<Matrix>();
  53. List<Matrix> inverseBindPose = new List<Matrix>();
  54. List<int> skeletonHierarchy = new List<int>();
  55. foreach (BoneContent bone in bones)
  56. {
  57. bindPose.Add(bone.Transform);
  58. inverseBindPose.Add(Matrix.Invert(bone.AbsoluteTransform));
  59. skeletonHierarchy.Add(bones.IndexOf(bone.Parent as BoneContent));
  60. }
  61. // Convert animation data to our runtime format.
  62. Dictionary<string, AnimationClip> animationClips;
  63. animationClips = ProcessAnimations(skeleton.Animations, bones);
  64. // Chain to the base ModelProcessor class so it can convert the model data.
  65. ModelContent model = base.Process(input, context);
  66. // Store our custom animation data in the Tag property of the model.
  67. model.Tag = new SkinningData(animationClips, bindPose,
  68. inverseBindPose, skeletonHierarchy);
  69. return model;
  70. }
  71. /// <summary>
  72. /// Converts an intermediate format content pipeline AnimationContentDictionary
  73. /// object to our runtime AnimationClip format.
  74. /// </summary>
  75. static Dictionary<string, AnimationClip> ProcessAnimations(
  76. AnimationContentDictionary animations, IList<BoneContent> bones)
  77. {
  78. // Build up a table mapping bone names to indices.
  79. Dictionary<string, int> boneMap = new Dictionary<string, int>();
  80. for (int i = 0; i < bones.Count; i++)
  81. {
  82. string boneName = bones[i].Name;
  83. if (!string.IsNullOrEmpty(boneName))
  84. boneMap.Add(boneName, i);
  85. }
  86. // Convert each animation in turn.
  87. Dictionary<string, AnimationClip> animationClips;
  88. animationClips = new Dictionary<string, AnimationClip>();
  89. foreach (KeyValuePair<string, AnimationContent> animation in animations)
  90. {
  91. AnimationClip processed = ProcessAnimation(animation.Value, boneMap);
  92. animationClips.Add(animation.Key, processed);
  93. }
  94. if (animationClips.Count == 0)
  95. {
  96. throw new InvalidContentException(
  97. "Input file does not contain any animations.");
  98. }
  99. return animationClips;
  100. }
  101. /// <summary>
  102. /// Converts an intermediate format content pipeline AnimationContent
  103. /// object to our runtime AnimationClip format.
  104. /// </summary>
  105. static AnimationClip ProcessAnimation(AnimationContent animation,
  106. Dictionary<string, int> boneMap)
  107. {
  108. List<Keyframe> keyframes = new List<Keyframe>();
  109. // For each input animation channel.
  110. foreach (KeyValuePair<string, AnimationChannel> channel in
  111. animation.Channels)
  112. {
  113. // Look up what bone this channel is controlling.
  114. int boneIndex;
  115. if (!boneMap.TryGetValue(channel.Key, out boneIndex))
  116. {
  117. throw new InvalidContentException(string.Format(
  118. "Found animation for bone '{0}', " +
  119. "which is not part of the skeleton.", channel.Key));
  120. }
  121. // Convert the keyframe data.
  122. foreach (AnimationKeyframe keyframe in channel.Value)
  123. {
  124. keyframes.Add(new Keyframe(boneIndex, keyframe.Time,
  125. keyframe.Transform));
  126. }
  127. }
  128. // Sort the merged keyframes by time.
  129. keyframes.Sort(CompareKeyframeTimes);
  130. if (keyframes.Count == 0)
  131. throw new InvalidContentException("Animation has no keyframes.");
  132. if (animation.Duration <= TimeSpan.Zero)
  133. throw new InvalidContentException("Animation has a zero duration.");
  134. return new AnimationClip(animation.Duration, keyframes);
  135. }
  136. /// <summary>
  137. /// Comparison function for sorting keyframes into ascending time order.
  138. /// </summary>
  139. static int CompareKeyframeTimes(Keyframe a, Keyframe b)
  140. {
  141. return a.Time.CompareTo(b.Time);
  142. }
  143. /// <summary>
  144. /// Makes sure this mesh contains the kind of data we know how to animate.
  145. /// </summary>
  146. static void ValidateMesh(NodeContent node, ContentProcessorContext context,
  147. string parentBoneName)
  148. {
  149. MeshContent mesh = node as MeshContent;
  150. if (mesh != null)
  151. {
  152. // Validate the mesh.
  153. if (parentBoneName != null)
  154. {
  155. context.Logger.LogWarning(null, null,
  156. "Mesh {0} is a child of bone {1}. SkinnedModelProcessor " +
  157. "does not correctly handle meshes that are children of bones.",
  158. mesh.Name, parentBoneName);
  159. }
  160. if (!MeshHasSkinning(mesh))
  161. {
  162. context.Logger.LogWarning(null, null,
  163. "Mesh {0} has no skinning information, so it has been deleted.",
  164. mesh.Name);
  165. mesh.Parent.Children.Remove(mesh);
  166. return;
  167. }
  168. }
  169. else if (node is BoneContent)
  170. {
  171. // If this is a bone, remember that we are now looking inside it.
  172. parentBoneName = node.Name;
  173. }
  174. // Recurse (iterating over a copy of the child collection,
  175. // because validating children may delete some of them).
  176. foreach (NodeContent child in new List<NodeContent>(node.Children))
  177. ValidateMesh(child, context, parentBoneName);
  178. }
  179. /// <summary>
  180. /// Checks whether a mesh contains skininng information.
  181. /// </summary>
  182. static bool MeshHasSkinning(MeshContent mesh)
  183. {
  184. foreach (GeometryContent geometry in mesh.Geometry)
  185. {
  186. if (!geometry.Vertices.Channels.Contains(VertexChannelNames.Weights()))
  187. return false;
  188. }
  189. return true;
  190. }
  191. /// <summary>
  192. /// Bakes unwanted transforms into the model geometry,
  193. /// so everything ends up in the same coordinate system.
  194. /// </summary>
  195. static void FlattenTransforms(NodeContent node, BoneContent skeleton)
  196. {
  197. foreach (NodeContent child in node.Children)
  198. {
  199. // Don't process the skeleton, because that is special.
  200. if (child == skeleton)
  201. continue;
  202. // Bake the local transform into the actual geometry.
  203. MeshHelper.TransformScene(child, child.Transform);
  204. // Having baked it, we can now set the local
  205. // coordinate system back to identity.
  206. child.Transform = Matrix.Identity;
  207. // Recurse.
  208. FlattenTransforms(child, skeleton);
  209. }
  210. }
  211. }
  212. }