SkinnedModelProcessor.cs 11 KB

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