SkinnedModelProcessor.cs 10 KB

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