CustomAvatarAnimationProcessor.cs 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414
  1. #region File Description
  2. //-----------------------------------------------------------------------------
  3. // CustomAvatarAnimationProcessor.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.Collections.Generic;
  12. using System.ComponentModel;
  13. using System.Linq;
  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 Microsoft.Xna.Framework.GamerServices;
  20. using CustomAvatarAnimation;
  21. using System.IO;
  22. #endregion
  23. namespace CustomAvatarAnimationPipeline
  24. {
  25. [ContentProcessor(DisplayName = "CustomAvatarAnimationProcessor")]
  26. public class CustomAvatarAnimationProcessor :
  27. ContentProcessor<NodeContent, CustomAvatarAnimationData>
  28. {
  29. /// <summary>
  30. /// Stores the model's bind pose
  31. /// </summary>
  32. List<Matrix> bindPose = new List<Matrix>();
  33. /// <summary>
  34. /// The facial expression file to use for the animation
  35. /// </summary>
  36. [DisplayName("Facial expression file")]
  37. [Description("File to use for facial animations")]
  38. public string ExpressionFile
  39. { get; set; }
  40. /// <summary>
  41. /// Processor to convert the model into our CustomAvatarAnimationData type
  42. /// </summary>
  43. public override CustomAvatarAnimationData Process(NodeContent input,
  44. ContentProcessorContext context)
  45. {
  46. // Find the skeleton
  47. NodeContent skeleton = FindSkeleton(input);
  48. // Check for errors
  49. if (skeleton == null)
  50. {
  51. throw new InvalidContentException("Avatar skeleton not found.");
  52. }
  53. else if (skeleton.Animations.Count < 1)
  54. {
  55. throw new InvalidContentException("No animation was found in the file.");
  56. }
  57. else if (skeleton.Animations.Count > 1)
  58. {
  59. throw new InvalidContentException("More than one animation was found.");
  60. }
  61. // The expression animation keyframes
  62. List<AvatarExpressionKeyFrame> expressionAnimationKeyFrames = null;
  63. // Process expression animation
  64. if (!string.IsNullOrEmpty(ExpressionFile))
  65. {
  66. expressionAnimationKeyFrames = ProcessExpressionAnimation();
  67. }
  68. // Remove the extra bones that we will not be using
  69. RemoveEndBonesAndFixBoneNames(skeleton);
  70. // Create a list of the bones from the skeleton hierarchy
  71. IList<NodeContent> bones = FlattenSkeleton(skeleton);
  72. // Check for errors
  73. if (bones.Count != AvatarRenderer.BoneCount)
  74. {
  75. throw new InvalidContentException("Invalid number of bones found.");
  76. }
  77. // Fill the bind pose array with the transforms from the bones
  78. foreach (NodeContent bone in bones)
  79. {
  80. bindPose.Add(bone.Transform);
  81. }
  82. // Build up a table mapping bone names to indices
  83. Dictionary<string, int> boneNameMap = 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. {
  89. boneNameMap.Add(boneName, i);
  90. }
  91. }
  92. // Create the custom animation data
  93. // -- From the error-checking above, we know there will only be one animation
  94. CustomAvatarAnimationData avatarCustomAnimationData = null;
  95. foreach (KeyValuePair<string, AnimationContent> animation
  96. in skeleton.Animations)
  97. {
  98. // Check for an invalid animation
  99. if (animation.Value.Duration <= TimeSpan.Zero)
  100. {
  101. throw new InvalidContentException("Animation has a zero duration.");
  102. }
  103. // Build a list of the avatar keyframes in the animation
  104. List<AvatarKeyFrame> animationKeyFrames =
  105. ProcessAnimation(animation.Value, boneNameMap);
  106. // Check for an invalid keyframes list
  107. if (animationKeyFrames.Count <= 0)
  108. {
  109. throw new InvalidContentException("Animation has no keyframes.");
  110. }
  111. // Create the custom-animation object
  112. avatarCustomAnimationData = new CustomAvatarAnimationData(animation.Key,
  113. animation.Value.Duration, animationKeyFrames, expressionAnimationKeyFrames);
  114. }
  115. return avatarCustomAnimationData;
  116. }
  117. /// <summary>
  118. /// Converts the input expression animation file into expression animation keyframes
  119. /// </summary>
  120. private List<AvatarExpressionKeyFrame> ProcessExpressionAnimation()
  121. {
  122. List<AvatarExpressionKeyFrame> expressionAnimationKeyFrames = new List<AvatarExpressionKeyFrame>();
  123. FileStream fs = File.OpenRead(ExpressionFile);
  124. StreamReader sr = new StreamReader(fs);
  125. while (!sr.EndOfStream)
  126. {
  127. string currentLine = sr.ReadLine();
  128. // Skip comment lines
  129. if (currentLine.StartsWith("#"))
  130. continue;
  131. string[] Components = currentLine.Split(',');
  132. // Check for the correct number of components
  133. if (Components.Length != 6)
  134. throw new InvalidContentException("Error processing facial expression file");
  135. try
  136. {
  137. TimeSpan time = TimeSpan.FromMilliseconds(Convert.ToDouble(Components[0]));
  138. AvatarExpression avatarExpression = new AvatarExpression();
  139. avatarExpression.LeftEye = (AvatarEye)Convert.ToInt32(Components[1]);
  140. avatarExpression.LeftEyebrow = (AvatarEyebrow)Convert.ToInt32(Components[2]);
  141. avatarExpression.Mouth = (AvatarMouth)Convert.ToInt32(Components[3]);
  142. avatarExpression.RightEye = (AvatarEye)Convert.ToInt32(Components[4]);
  143. avatarExpression.RightEyebrow = (AvatarEyebrow)Convert.ToInt32(Components[5]);
  144. AvatarExpressionKeyFrame expressionKeyframe = new AvatarExpressionKeyFrame(time, avatarExpression);
  145. expressionAnimationKeyFrames.Add(expressionKeyframe);
  146. }
  147. catch (Exception)
  148. {
  149. throw new InvalidContentException("Error processing facial expression file");
  150. }
  151. }
  152. // Sort the animation frames
  153. expressionAnimationKeyFrames.Sort((frame1, frame2) => frame1.Time.CompareTo(frame2.Time));
  154. return expressionAnimationKeyFrames;
  155. }
  156. private NodeContent FindSkeleton(NodeContent input)
  157. {
  158. if (input == null)
  159. throw new ArgumentNullException("input");
  160. // Search for the root node of the skeleton
  161. if (input.Name.Contains("BASE__Skeleton"))
  162. {
  163. return input;
  164. }
  165. // Search children nodes
  166. foreach (NodeContent child in input.Children)
  167. {
  168. // Try to find the skeleton in the child nodes
  169. NodeContent skeleton = FindSkeleton(child);
  170. // Return the found skeleton
  171. if (skeleton != null)
  172. return skeleton;
  173. }
  174. // Could not find the skeleton
  175. return null;
  176. }
  177. #region Skeleton Cleaning
  178. /// <summary>
  179. /// Flattens the skeleton into a list. The order in the list is sorted by
  180. /// depth first and then by name
  181. /// </summary>
  182. static IList<NodeContent> FlattenSkeleton(NodeContent skeleton)
  183. {
  184. // safety check on the parameter
  185. if (skeleton == null)
  186. {
  187. throw new ArgumentNullException("skeleton");
  188. }
  189. // Create the destination list of bones
  190. List<NodeContent> bones = new List<NodeContent>();
  191. // Create a list to track current items in the level of tree
  192. List<NodeContent> currentLevel = new List<NodeContent>();
  193. // Add the root node of the skeleton to the list
  194. currentLevel.Add(skeleton);
  195. while (currentLevel.Count > 0)
  196. {
  197. // Create a list of bones to track the next level of the tree
  198. List<NodeContent> nextLevel = new List<NodeContent>();
  199. // Sort the bones in the current level
  200. IEnumerable<NodeContent> sortedBones = from item in currentLevel
  201. orderby item.Name
  202. select item;
  203. // Add the newly sorted items to the output list
  204. foreach (NodeContent bone in sortedBones)
  205. {
  206. bones.Add(bone);
  207. // Add the bone's children to the next-level list
  208. foreach (NodeContent child in bone.Children)
  209. {
  210. nextLevel.Add(child);
  211. }
  212. }
  213. // the next level is now the current level
  214. currentLevel = nextLevel;
  215. }
  216. // return the flattened array of bones
  217. return bones;
  218. }
  219. /// <summary>
  220. /// Removes each bone node that contains "_END" in the name/
  221. /// </summary>
  222. /// <remarks>
  223. /// These bones are not needed by the AvatarRenderer runtime but
  224. /// are part of the Avatar rig used in modeling programs
  225. /// </remarks>
  226. static void RemoveEndBonesAndFixBoneNames(NodeContent bone)
  227. {
  228. // safety-check the parameter
  229. if (bone == null)
  230. {
  231. throw new ArgumentNullException("bone");
  232. }
  233. // Remove unneeded text from the bone name
  234. bone.Name = CleanBoneName(bone.Name);
  235. // Remove each child bone that contains "_END" in the name
  236. for (int i = 0; i < bone.Children.Count; ++i)
  237. {
  238. NodeContent child = bone.Children[i];
  239. if (child.Name.Contains("_END"))
  240. {
  241. bone.Children.Remove(child);
  242. --i;
  243. }
  244. else
  245. {
  246. // Recursively search through the remaining child bones
  247. RemoveEndBonesAndFixBoneNames(child);
  248. }
  249. }
  250. }
  251. /// <summary>
  252. /// Removes extra text from the bone names
  253. /// </summary>
  254. static string CleanBoneName(string boneName)
  255. {
  256. boneName = boneName.Replace("__Skeleton", "");
  257. return boneName;
  258. }
  259. #endregion
  260. #region Animation Processing
  261. /// <summary>
  262. /// Converts an intermediate-format content pipeline AnimationContent object
  263. /// to an avatar-specific AvatarKeyFrame list.
  264. /// </summary>
  265. List<AvatarKeyFrame> ProcessAnimation(AnimationContent animation,
  266. Dictionary<string, int> boneMap)
  267. {
  268. // Create the output list of keyframes
  269. List<AvatarKeyFrame> keyframes = new List<AvatarKeyFrame>();
  270. // Process each channel in the animation
  271. foreach (KeyValuePair<string, AnimationChannel> channel
  272. in animation.Channels)
  273. {
  274. // Don't add animation nodes with "_END" in the name
  275. // -- These bones were removed from the skeleton already
  276. if (channel.Key.Contains("_END"))
  277. {
  278. continue;
  279. }
  280. // Look up what bone this channel is controlling.
  281. int boneIndex;
  282. if (!boneMap.TryGetValue(CleanBoneName(channel.Key), out boneIndex))
  283. {
  284. throw new InvalidContentException(string.Format(
  285. "Found animation for bone '{0}', " +
  286. "which is not part of the skeleton.", channel.Key));
  287. }
  288. // Convert the keyframe data.
  289. foreach (AnimationKeyframe keyframe in channel.Value)
  290. {
  291. keyframes.Add(new AvatarKeyFrame(boneIndex, keyframe.Time,
  292. CreateKeyframeMatrix(keyframe, boneIndex)));
  293. }
  294. }
  295. // Sort the merged keyframes by time.
  296. keyframes.Sort((frame1, frame2) => frame1.Time.CompareTo(frame2.Time));
  297. return keyframes;
  298. }
  299. /// <summary>
  300. /// Create an AvatarRenderer-friendly matrix from an animation keyframe.
  301. /// </summary>
  302. /// <param name="keyframe">The keyframe to be converted.</param>
  303. /// <param name="boneIndex">The index of the bone this keyframe is for.</param>
  304. /// <returns>The converted AvatarRenderer-friendly matrix for this bone
  305. /// and keyframe.</returns>
  306. Matrix CreateKeyframeMatrix(AnimationKeyframe keyframe, int boneIndex)
  307. {
  308. // safety-check the parameter
  309. if (keyframe == null)
  310. {
  311. throw new ArgumentNullException("keyframe");
  312. }
  313. // Retrieve the transform for this keyframe
  314. Matrix keyframeMatrix;
  315. // The root node is transformed by the root of the bind pose
  316. // We need to make the keyframe relative to the root
  317. if (boneIndex == 0)
  318. {
  319. // When the animation is exported the bind pose can have the
  320. // wrong translation of the root node so we hard code it here
  321. Vector3 bindPoseTranslation = new Vector3(0.000f, 75.5199f, -0.8664f);
  322. Matrix keyTransfrom = keyframe.Transform;
  323. Matrix inverseBindPose = bindPose[boneIndex];
  324. inverseBindPose.Translation -= bindPoseTranslation;
  325. inverseBindPose = Matrix.Invert(inverseBindPose);
  326. keyframeMatrix = (keyTransfrom * inverseBindPose);
  327. keyframeMatrix.Translation -= bindPoseTranslation;
  328. // Scale from cm to meters
  329. keyframeMatrix.Translation *= 0.01f;
  330. }
  331. else
  332. {
  333. keyframeMatrix = keyframe.Transform;
  334. // Only the root node can have translation
  335. keyframeMatrix.Translation = Vector3.Zero;
  336. }
  337. return keyframeMatrix;
  338. }
  339. #endregion
  340. }
  341. }