Merging animation files

Originally posted to Shawn Hargreaves Blog on MSDN, Friday, June 18, 2010

Our skinned animation sample supports any number of animations, which are imported from the input BoneContent.Animations by the SkinnedModelProcessor, stored in the output SkinningData.AnimationClips dictionary, and can be switched or blended between in whatever way your runtime game code sees fit.

But if you try to create a model which contains multiple animations, you will discover that current Autodesk FBX exporters only support a single animation take per FBX file!

One solution is to concatenate all your animations into a single long one, then choose the appropriate time region when you play back the data. For instance, "run" might be located between 2 sec and 3.1 sec, while 3.1 sec to 5.4 sec is "jump". Kinda like the animation version of sprite sheets, this works ok, but it can be a pain keeping track of which data means what.

Another approach is to export each animation into a separate FBX file, then use a custom processor to merge them.

 

using System.Linq;
using Microsoft.Xna.Framework.Content.Pipeline;
using Microsoft.Xna.Framework.Content.Pipeline.Graphics;
using Microsoft.Xna.Framework.Content.Pipeline.Processors;

namespace MergeAnimationsPipeline
{
    [ContentProcessor]
    public class MergeAnimationsProcessor : ModelProcessor
    {
        public string MergeAnimations { get; set; }


        public override ModelContent Process(NodeContent input, ContentProcessorContext context)
        {
            if (!string.IsNullOrEmpty(MergeAnimations))
            {
                foreach (string mergeFile in MergeAnimations.Split(';')
                                                            .Select(s => s.Trim())
                                                            .Where(s => !string.IsNullOrEmpty(s)))
                {
                    MergeAnimation(input, context, mergeFile);
                }
            }

            // TODO: whatever other processing you need.
            // eg. if you use SkinnedModelProcessor from our sample, that code would go here.

            return base.Process(input, context);
        }


        void MergeAnimation(NodeContent input, ContentProcessorContext context, string mergeFile)
        {
            NodeContent mergeModel = context.BuildAndLoadAsset<NodeContent, NodeContent>(
                                                new ExternalReference<NodeContent>(mergeFile), null);

            BoneContent rootBone = MeshHelper.FindSkeleton(input);

            if (rootBone == null)
            {
                context.Logger.LogWarning(null, input.Identity, "Source model has no root bone.");
                return;
            }

            BoneContent mergeRoot = MeshHelper.FindSkeleton(mergeModel);

            if (mergeRoot == null)
            {
                context.Logger.LogWarning(null, input.Identity, "Merge model '{0}' has no root bone.", mergeFile);
                return;
            }

            foreach (string animationName in mergeRoot.Animations.Keys)
            {
                if (rootBone.Animations.ContainsKey(animationName))
                {
                    context.Logger.LogWarning(null, input.Identity,
                        "Cannot merge animation '{0}' from '{1}', because this animation already exists.",
                        animationName, mergeFile);
                        
                    continue;
                }

                context.Logger.LogImportantMessage("Merging animation '{0}' from '{1}'.", animationName, mergeFile);

                rootBone.Animations.Add(animationName, mergeRoot.Animations[animationName]);
            }
        }
    }
}
Blog index   -   Back to my homepage