SkinnedModelProcessor.cs 9.8 KB

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