XNA Skinned Model Animations

The Skinned Model sample from the App Hub education catalogue is great for getting animated characters into your game, but there’s a bit of a flaw with the export process. The problem is, when you export your character from 3DS Max (and possibly other modelling programs), all you get is one animation, named ‘Take 001’. Wouldn’t it be nice if we could define different animations for different parts of the animation timeline? Well, we’re going to do just that :). As an added bonus, we’ll also be adding in events, so you can be notified when certain parts of your animation are hit.

This tutorial is based on the Skinned Model sample, so grab it from the App Hub if you want to follow along, or skip to the end if you want the final version (which is released under the same license as the original).

What we’ll be doing is creating an XML file to go with our exported model, which will define our animation clips and events. The animations defined in this file will replace animations defined in the source model file. So, first thing is to define the class that will be represented by the XML file. Create a class in the SkinnedModelPipeline project named AnimationDefinition, like so:

usingSystem;usingSystem.Collections.Generic;usingSystem.Text;usingMicrosoft.Xna.Framework;usingMicrosoft.Xna.Framework.Content;usingMicrosoft.Xna.Framework.Content.Pipeline;usingMicrosoft.Xna.Framework.Content.Pipeline.Serialization;usingMicrosoft.Xna.Framework.Content.Pipeline.Serialization.Intermediate;namespace SkinnedModelPipeline
{/// <summary>/// A class for storing our animation definitions/// </summary>publicclass AnimationDefinition
{/// <summary>/// The original clip name that was exported by the modelling package/// Usually this will be Take 001/// </summary>publicstring OriginalClipName
{get;set;}/// <summary>/// The number of frames in the original animation/// </summary>publicint OriginalFrameCount
{get;set;}/// <summary>/// A class for storing information about individual clips that we want to create/// </summary>publicclass ClipPart
{/// <summary>/// The name we have given the clip/// </summary>publicstring ClipName
{get;set;}/// <summary>/// The starting frame of the clip/// </summary>publicint StartFrame
{get;set;}/// <summary>/// The ending frame of the clip/// </summary>publicint EndFrame
{get;set;}/// <summary>/// A class for defining events in an animation/// </summary>publicclassEvent{/// <summary>/// The name of the event/// </summary>publicstring Name
{get;set;}/// <summary>/// The frame that the event fires on/// </summary>publicint Keyframe
{get;set;}};/// <summary>/// Our list of events in this animation clip/// Animation clips do not require events, so this is marked as optional/// </summary>[Microsoft.Xna.Framework.Content.ContentSerializer(Optional =true)]public List<Event> Events
{get;set;}};/// <summary>/// The list of clip parts that we are breaking the original clip into/// </summary>public List<ClipPart> ClipParts
{get;set;}}}

using System;
using System.Collections.Generic;
using System.Text;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.Content.Pipeline;
using Microsoft.Xna.Framework.Content.Pipeline.Serialization;
using Microsoft.Xna.Framework.Content.Pipeline.Serialization.Intermediate;
namespace SkinnedModelPipeline
{
/// <summary>
/// A class for storing our animation definitions
/// </summary>
public class AnimationDefinition
{
/// <summary>
/// The original clip name that was exported by the modelling package
/// Usually this will be Take 001
/// </summary>
public string OriginalClipName
{
get;
set;
}
/// <summary>
/// The number of frames in the original animation
/// </summary>
public int OriginalFrameCount
{
get;
set;
}
/// <summary>
/// A class for storing information about individual clips that we want to create
/// </summary>
public class ClipPart
{
/// <summary>
/// The name we have given the clip
/// </summary>
public string ClipName
{
get;
set;
}
/// <summary>
/// The starting frame of the clip
/// </summary>
public int StartFrame
{
get;
set;
}
/// <summary>
/// The ending frame of the clip
/// </summary>
public int EndFrame
{
get;
set;
}
/// <summary>
/// A class for defining events in an animation
/// </summary>
public class Event
{
/// <summary>
/// The name of the event
/// </summary>
public string Name
{
get;
set;
}
/// <summary>
/// The frame that the event fires on
/// </summary>
public int Keyframe
{
get;
set;
}
};
/// <summary>
/// Our list of events in this animation clip
/// Animation clips do not require events, so this is marked as optional
/// </summary>
[Microsoft.Xna.Framework.Content.ContentSerializer(Optional = true)]
public List<Event> Events
{
get;
set;
}
};
/// <summary>
/// The list of clip parts that we are breaking the original clip into
/// </summary>
public List<ClipPart> ClipParts
{
get;
set;
}
}
}

Next, we’ll need to modify the runtime project to add information about our events to the animations. Create a class in the SkinnedModelWindows project named AnimationEvent, with this in it:

#region Using StatementsusingSystem;usingSystem.Collections.Generic;usingSystem.Linq;usingSystem.Text;#endregionnamespace SkinnedModel
{/// <summary>/// Information about an event in our animation/// </summary>publicclass AnimationEvent
{/// <summary>/// The name of the event/// </summary>publicString EventName
{get;set;}/// <summary>/// The time of the event/// </summary>public TimeSpan EventTime
{get;set;}}}

Now, we need to add our events store to the animation clip, as well as the clip name. So open up AnimationClip.cs, and add this to the end of the class:

/// <summary>/// Callback events for the animation clips/// </summary>[ContentSerializer]public List<AnimationEvent> Events {get;privateset;}/// <summary>/// The name of the clip/// </summary>[ContentSerializer]publicstring Name {get;privateset;}

Almost there. We need to modify the SkinnedModelProcessor class so that it reads in our XML files describing our animations and stores them in the model file that it generates, replacing the original animation (the Take 001). So, in SkinnedModelProcessor.cs, in the ProcessAnimations function, we need to first check if an animation definition file exists that we will use to override the ones in the model. By having this check, it means that we don’t need to create an animation definition for every skinned model, just the ones that we want custom animations on. You’ll also need to modify the ProcessAnimations function to take two extra parameters, which are the ContentProcessorContext and ContentIdentity. We use these to get information about the current file we are processing, so we can look for an animation definition with the same name. The below code is the updated ProcessAnimations function:

/// <summary>/// Converts an intermediate format content pipeline AnimationContentDictionary/// object to our runtime AnimationClip format./// </summary>static Dictionary<string, AnimationClip> ProcessAnimations(
AnimationContentDictionary animations, IList<BoneContent> bones,
ContentProcessorContext context, ContentIdentity sourceIdentity){// Build up a table mapping bone names to indices.
Dictionary<string, int> boneMap =new Dictionary<string, int>();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<string, AnimationClip> animationClips;
animationClips =new Dictionary<string, AnimationClip>();// We process the original animation first, so we can use their keyframesforeach(KeyValuePair<string, AnimationContent> animation in animations){
AnimationClip processed = ProcessAnimation(animation.Value, boneMap, animation.Key);
animationClips.Add(animation.Key, processed);}// Check to see if there's an animation clip definition// Here, we're checking for a file with the _Anims suffix.// So, if your model is named dude.fbx, we'd add dude_Anims.xml in the same folder// and the pipeline will see the file and use it to override the animations in the// original model file.string SourceModelFile = sourceIdentity.SourceFilename;string SourcePath = Path.GetDirectoryName(SourceModelFile);string AnimFilename = Path.GetFileNameWithoutExtension(SourceModelFile);
AnimFilename +="_Anims.xml";string AnimPath = Path.Combine(SourcePath, AnimFilename);if(File.Exists(AnimPath)){// Add the filename as a dependency, so if it changes, the model is rebuilt
context.AddDependency(AnimPath);// Load the animation definition from the XML file
AnimationDefinition AnimDef = context.BuildAndLoadAsset<XmlImporter, AnimationDefinition>(new ExternalReference<XmlImporter>(AnimPath), null);// Break up the original animation clips into our new clips// First, we check if the clips contains our clip to break upif(animationClips.ContainsKey(AnimDef.OriginalClipName)){// Grab the main clip that we are using
AnimationClip MainClip = animationClips[AnimDef.OriginalClipName];// Now remove the original clip from our animations
animationClips.Remove(AnimDef.OriginalClipName);// Process each of our new animation clip partsforeach(AnimationDefinition.ClipPart Part in AnimDef.ClipParts){// Calculate the frame times
TimeSpan StartTime = GetTimeSpanForFrame(Part.StartFrame, AnimDef.OriginalFrameCount, MainClip.Duration.Ticks);
TimeSpan EndTime = GetTimeSpanForFrame(Part.EndFrame, AnimDef.OriginalFrameCount, MainClip.Duration.Ticks);// Get all the keyframes for the animation clip// that fall within the start and end time
List<Keyframe> Keyframes =new List<Keyframe>();foreach(Keyframe AnimFrame in MainClip.Keyframes){if((AnimFrame.Time>= StartTime)&&(AnimFrame.Time<= EndTime)){
Keyframe NewFrame =new Keyframe(AnimFrame.Bone, AnimFrame.Time- StartTime, AnimFrame.Transform);
Keyframes.Add(NewFrame);}}// Process the events
List<AnimationEvent> Events =new List<AnimationEvent>();if(Part.Events!=null){// Process each eventforeach(AnimationDefinition.ClipPart.EventEventin Part.Events){// Get the event time within the animation
TimeSpan EventTime = GetTimeSpanForFrame(Event.Keyframe, AnimDef.OriginalFrameCount, MainClip.Duration.Ticks);// Offset the event time so it is relative to the start of the animation
EventTime -= StartTime;// Create the event
AnimationEvent NewEvent =new AnimationEvent();
NewEvent.EventTime= EventTime;
NewEvent.EventName=Event.Name;
Events.Add(NewEvent);}}// Create the clip
AnimationClip NewClip =new AnimationClip(EndTime - StartTime, Keyframes, Events, Part.ClipName);
animationClips[Part.ClipName]= NewClip;}}}if(animationClips.Count==0){thrownew InvalidContentException("Input file does not contain any animations.");}return animationClips;}/// <summary>/// Gets a TimeSpan value for a frame index in an animation/// </summary>privatestatic TimeSpan GetTimeSpanForFrame(int FrameIndex, int TotalFrameCount, long TotalTicks){float MaxFrameIndex =(float)TotalFrameCount -1;float AmountOfAnimation =(float)FrameIndex / MaxFrameIndex;float NumTicks = AmountOfAnimation *(float)TotalTicks;returnnew TimeSpan((long)NumTicks);}

What we do is process the original animations, so that all the keyframe data is there, then we check for an animation definition file and, if it exists, use it to replace the animation clips from the original model. We also need to modify ProcessAnimation to handle the new AnimationClip constructor:

OK, so now we can override animations in the models using our XML file. Before I show you an example file, there’s one last thing to do, which is to add in the event callback system into the runtime. So, we need to add in a place to register our event callbacks into the AnimationPlayer class. At the end of the ‘Fields’ region, we need to add:

So now we can define custom animations with events. Lets see it in action…

Add a new file to your content project, giving it the same name as the model, but with _Anims.xml. In our case, our model file is dude.fbx, so we want dude_Anims.xml. We don’t actually want the content pipeline to build this directly, so set the Build Action property to None, the Content Processor to No Processing Required, and Copy to Output Directory to Do not copy. Our XML looks like this:

<?xmlversion="1.0"encoding="utf-8"?><XnaContent><AssetType="SkinnedModelPipeline.AnimationDefinition"><!-- The original name of the clip we are breaking up --><OriginalClipName>Take 001</OriginalClipName><!-- The total number of frames in the original clip --><OriginalFrameCount>100</OriginalFrameCount><!-- The new parts we want --><ClipParts><!-- Each item is one of our new clips --><Item><ClipName>Idle</ClipName><StartFrame>0</StartFrame><EndFrame>50</EndFrame></Item><Item><ClipName>Fire</ClipName><StartFrame>51</StartFrame><EndFrame>99</EndFrame><!-- We can register events in this clip, so we can know when certain frames are hit --><Events><Item><Name>FireFrame</Name><Keyframe>70</Keyframe></Item></Events></Item></ClipParts></Asset></XnaContent>

<?xml version="1.0" encoding="utf-8" ?>
<XnaContent>
<Asset Type="SkinnedModelPipeline.AnimationDefinition">
<!-- The original name of the clip we are breaking up -->
<OriginalClipName>Take 001</OriginalClipName>
<!-- The total number of frames in the original clip -->
<OriginalFrameCount>100</OriginalFrameCount>
<!-- The new parts we want -->
<ClipParts>
<!-- Each item is one of our new clips -->
<Item>
<ClipName>Idle</ClipName>
<StartFrame>0</StartFrame>
<EndFrame>50</EndFrame>
</Item>
<Item>
<ClipName>Fire</ClipName>
<StartFrame>51</StartFrame>
<EndFrame>99</EndFrame>
<!-- We can register events in this clip, so we can know when certain frames are hit -->
<Events>
<Item>
<Name>FireFrame</Name>
<Keyframe>70</Keyframe>
</Item>
</Events>
</Item>
</ClipParts>
</Asset>
</XnaContent>

Now we can play our new clips like normal. We can also add callbacks for events. For example, the FireFrame event, we add like this:

So there you have it. You can download the updated sample here. You can use the ‘1’ and ‘2’ keys to switch between animation clips. The second one has a callback registered to it. One thing to note is that you’ll have to make sure you do a Rebuild Solution if you’re adding an animation XML to an existing model, because the content pipeline doesn’t know that the model depends on the animation definition until after it has built it once with the animation file there. Enjoy!