SkinnedModelProcessor.cs 10 KB

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