#region File Description //----------------------------------------------------------------------------- // SkinnedModelProcessor.cs // // Microsoft XNA Community Game Platform // Copyright (C) Microsoft Corporation. All rights reserved. //----------------------------------------------------------------------------- #endregion using System; using System.Collections.Generic; using CustomModelAnimation; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Content.Pipeline; using Microsoft.Xna.Framework.Content.Pipeline.Graphics; using Microsoft.Xna.Framework.Content.Pipeline.Processors; namespace CustomModelAnimationPipeline { /// /// Custom processor extends the builtin framework ModelProcessor class, /// adding animation support. /// [ContentProcessor(DisplayName = "Skinned Model Processor")] public class SkinnedModelProcessor : ModelProcessor { // Maximum number of bone matrices we can render in a single pass const int MaxBones = 59; /// /// The main Process method converts an intermediate format content pipeline /// NodeContent tree to a ModelContent object with embedded animation data. /// public override ModelContent Process(NodeContent input, ContentProcessorContext context) { ValidateMesh(input, context, null); // Find the skeleton. BoneContent skeleton = MeshHelper.FindSkeleton(input); if (skeleton == null) throw new InvalidContentException("Input skeleton not found."); // We don't want to have to worry about different parts of the model being // in different local coordinate systems, so let's just bake everything. FlattenTransforms(input, skeleton); // Read the bind pose and skeleton hierarchy data. IList bones = MeshHelper.FlattenSkeleton(skeleton); if (bones.Count > MaxBones) { throw new InvalidContentException(string.Format( "Skeleton has {0} bones, but the maximum supported is {1}.", bones.Count, MaxBones)); } List bindPose = new List(); List inverseBindPose = new List(); List skeletonHierarchy = new List(); foreach (BoneContent bone in bones) { bindPose.Add(bone.Transform); inverseBindPose.Add(Matrix.Invert(bone.AbsoluteTransform)); skeletonHierarchy.Add(bones.IndexOf(bone.Parent as BoneContent)); } // Convert animation data to our runtime format. Dictionary animationClips = ProcessAnimations(skeleton.Animations, bones); Dictionary rootClips = new Dictionary(); // Chain to the base ModelProcessor class so it can convert the model data. ModelContent model = base.Process(input, context); // Convert each animation in the root of the object foreach (KeyValuePair animation in input.Animations) { ModelAnimationClip processed = AnimatedModelProcessor.ProcessRootAnimation(animation.Value, model.Bones[0].Name); rootClips.Add(animation.Key, processed); } // Store our custom animation data in the Tag property of the model. model.Tag = new ModelData(animationClips, null, bindPose, inverseBindPose, skeletonHierarchy); return model; } protected override void ProcessVertexChannel( GeometryContent geometry, int vertexChannelIndex, ContentProcessorContext context) { bool isWeights = geometry.Vertices.Channels[vertexChannelIndex].Name == VertexChannelNames.Weights(); base.ProcessVertexChannel(geometry, vertexChannelIndex, context); if (isWeights) { geometry.Vertices.Channels.ConvertChannelContent("BlendIndices0"); geometry.Vertices.Channels.ConvertChannelContent("BlendWeight0"); } } /// /// Converts an intermediate format content pipeline AnimationContentDictionary /// object to our runtime AnimationClip format. /// static Dictionary ProcessAnimations( AnimationContentDictionary animations, IList bones) { // Build up a table mapping bone names to indices. Dictionary boneMap = new Dictionary(); for (int i = 0; i < bones.Count; i++) { string boneName = bones[i].Name; if (!string.IsNullOrEmpty(boneName)) boneMap.Add(boneName, i); } // Convert each animation in turn. Dictionary animationClips; animationClips = new Dictionary(); foreach (KeyValuePair animation in animations) { ModelAnimationClip processed = ProcessAnimation(animation.Value, boneMap); animationClips.Add(animation.Key, processed); } if (animationClips.Count == 0) { throw new InvalidContentException("Input file does not contain any animations."); } return animationClips; } /// /// Converts an intermediate format content pipeline AnimationContent /// object to our runtime AnimationClip format. /// static ModelAnimationClip ProcessAnimation(AnimationContent animation, Dictionary boneMap) { List keyframes = new List(); // For each input animation channel. foreach (KeyValuePair channel in animation.Channels) { // Look up what bone this channel is controlling. int boneIndex; if (!boneMap.TryGetValue(channel.Key, out boneIndex)) { throw new InvalidContentException(string.Format( "Found animation for bone '{0}', which is not part of the skeleton.", channel.Key)); } // Convert the keyframe data. foreach (AnimationKeyframe keyframe in channel.Value) { keyframes.Add(new ModelKeyframe(boneIndex, keyframe.Time, keyframe.Transform)); } } // Sort the merged keyframes by time. keyframes.Sort(CompareKeyframeTimes); if (keyframes.Count == 0) throw new InvalidContentException("Animation has no keyframes."); if (animation.Duration <= TimeSpan.Zero) throw new InvalidContentException("Animation has a zero duration."); return new ModelAnimationClip(animation.Duration, keyframes); } /// /// Comparison function for sorting keyframes into ascending time order. /// static int CompareKeyframeTimes(ModelKeyframe a, ModelKeyframe b) { return a.Time.CompareTo(b.Time); } /// /// Makes sure this mesh contains the kind of data we know how to animate. /// static void ValidateMesh(NodeContent node, ContentProcessorContext context, string parentBoneName) { MeshContent mesh = node as MeshContent; if (mesh != null) { // Validate the mesh. if (parentBoneName != null) { context.Logger.LogWarning(null, null, "Mesh {0} is a child of bone {1}. SkinnedModelProcessor " + "does not correctly handle meshes that are children of bones.", mesh.Name, parentBoneName); } if (!MeshHasSkinning(mesh)) { context.Logger.LogWarning(null, null, "Mesh {0} has no skinning information, so it has been deleted.", mesh.Name); mesh.Parent.Children.Remove(mesh); return; } } else if (node is BoneContent) { // If this is a bone, remember that we are now looking inside it. parentBoneName = node.Name; } // Recurse (iterating over a copy of the child collection, // because validating children may delete some of them). foreach (NodeContent child in new List(node.Children)) ValidateMesh(child, context, parentBoneName); } /// /// Checks whether a mesh contains skininng information. /// static bool MeshHasSkinning(MeshContent mesh) { foreach (GeometryContent geometry in mesh.Geometry) { if (!geometry.Vertices.Channels.Contains(VertexChannelNames.Weights())) return false; } return true; } /// /// Bakes unwanted transforms into the model geometry, /// so everything ends up in the same coordinate system. /// static void FlattenTransforms(NodeContent node, BoneContent skeleton) { foreach (NodeContent child in node.Children) { // Don't process the skeleton, because that is special. if (child == skeleton) continue; // Bake the local transform into the actual geometry. MeshHelper.TransformScene(child, child.Transform); // Having baked it, we can now set the local // coordinate system back to identity. child.Transform = Matrix.Identity; // Recurse. FlattenTransforms(child, skeleton); } } } }