Thursday, June 5, 2014

16. Support multiple animations from a single .fbx file for XNA 4.0, XNA 4.0 Refresh & MonoGame!!!



       One of the biggest issues developers had and still have with XNA 4.0 is that it no longer supports a 3D model which contains multiple animations for .fbx files. One approach is to export each animation into a separate FBX file, and then use a custom processor to merge them. You can find a great example of this approach on Shawn Hargreaves Blog here. An open source implementation of this can also be found here. To explain this better, lets say you have 5 characters in your game for example. Each of those characters has 15 animations. So for all five characters, you would have to export each of their animations into separate individual files. This can be a pain and cause hassles for developers; especially if your game has many characters.

       It was because of this flaw in XNA 4.0 that many developers found themselves leaving the framework entirely. Some had no problems with their .fbx files using Blender as their modeling and animation program of choice. Many abandoned .fbx altogether and switched to DirectX files. Although .X rather than .FBX is a decent alternative, its a completely different format that may or may not be supported by your 3D software which posed some problems for developers using XNA 4.0. This flaw in XNA 4.0 hindered my progress for quite some time. I simply didn't want to give up on creating a multi-take animation importer. It took a long time but I was able to fix this and get multiple animations working from a single .fbx file after modifying the XNAnimation Library to work in XNA 4.0. I have posted the source code below for everyone. I hope others will find this useful.


How to extend the XNAnimation library for XNA 4.0 & MonoGame
       In the XNAnimation Pipeline, right-click References and select Add Reference. Under the .NET tab found at the top, locate and add the following reference to the project: Microsoft.XNA.Framework.Content.Pipeline.FBXImporter. In the XNAnimation Pipeline, right-click the project and select Add; then select New Item. Create a new class called SkinnedModelImporter.cs and add the following source code below.

Source Code Below:

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.IO;
using System.Runtime.InteropServices;
using System.Text;
using System.Xml;
using System.Linq;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.Content.Pipeline;
using Microsoft.Xna.Framework.Content.Pipeline.Graphics;
using Microsoft.Xna.Framework.Content.Pipeline.Processors;
using Microsoft.Xna.Framework.Design;


// The type to import.
using TImport = Microsoft.Xna.Framework.Content.Pipeline.Graphics.NodeContent;

// Change the namespace to suit your project
namespace XNAnimationPipeline.Pipeline
{
    [ContentImporter(".fbx", DisplayName = "Multi-take FBX Importer", DefaultProcessor = "")]
    public class SkinnedModelImporter : FbxImporter
    {
        private List<string> _animfiles;
        private List<string> _fbxheader;
        private TImport _master;
        private ContentImporterContext _context;
        
        public override TImport Import(string filename, ContentImporterContext context)
        {
            _context = context;

            // _animfiles will contain list of new temp anim files.
            _animfiles = new List<string>();

            // Decouple header and animation data.
            ExtractAnimations(filename);
            
            // Process master file (this will also process the first animation)
            _master = base.Import(filename, context);

            // Process the remaining animations.
            foreach (string file in _animfiles)
           {
                TImport anim = base.Import(file, context);
                
                // Append animation to master NodeContent.
                AppendAnimation(_master, anim);
           }
            
            // Delete the temporary animation files.
            DeleteTempFiles();
            
            return _master;
        }
        
        private void AppendAnimation(NodeContent masternode, NodeContent animnode)
        {
            foreach (KeyValuePair<string, AnimationContent> anim in animnode.Animations) {
                masternode.Animations.Add(anim.Key, anim.Value);
            }
            
            //foreach (NodeContent child in animnode.Children) {
            //    if (child != null) {
            //        AppendAnimation(child);
            //    }
            //}

            for (int i = 0; i < masternode.Children.Count; i++) {
                if (animnode.Children[i] != null) {
                    AppendAnimation(masternode.Children[i], animnode.Children[i]);
                }
            }
        }
        
        private void ExtractAnimations(string filename)
        {
            List<string> masterFile = File.ReadAllLines(filename).ToList();
            string path = Path.GetDirectoryName(filename);
            int open_idx = 0,
                length,
                num_open = -1,
                filenum = 0;
            bool foundTake = false;

            int idx = masterFile.IndexOf("Takes:  {") + 1;
            _fbxheader = masterFile.Take(idx).ToList();
            List<string> anims = masterFile.Skip(idx).ToList();
            
            // Extract each animation and create a temporary anim file.
            for (int i = 0; i < anims.Count; i++) {
                if (anims[i].Contains("Take: ")) {
                    open_idx = i;
                    num_open = 0;
                    foundTake = true;
                }
                
                if (anims[i].Contains("{") &&
                    foundTake) {
                    num_open++;
                }

                if (anims[i].Contains("}") &&
                    foundTake) {
                    num_open--;
                }
                
                if (num_open == 0 &&
                    foundTake) {
                    // Skip first animation since this is processed in the master
                    // fbx file.
                    if (filenum > 0) {
                        length = i - open_idx + 1;
                        
                        // Create temp file from header + anim data.
                        CreateTempFile(Path.Combine(path, "tmp.anim." + filenum + ".fbx"),
                                       anims.Skip(open_idx).Take(length).ToArray());
                    }
                    filenum++;
                    foundTake = false;
                }
            }
        }
        
        private void CreateTempFile(string filename, string[] data)
        {
            List<string> completefile = new List<string>();
            completefile.AddRange(_fbxheader);
            completefile.AddRange(data);

            try {
                // Write data to new temp file.
                File.WriteAllLines(filename, completefile.ToArray());

                // Store temp file name for processing.
                _animfiles.Add(filename);
            }
            catch {
                // Error while creating temp file.
                _context.Logger.LogWarning(nullnull"Error creating temp file: {0}", filename);
            }
        }
        
        private void DeleteTempFiles()
        {
            foreach (string file in _animfiles) {
                File.Delete(file);
            }
        }
    }
}




How to extend the XNAnimation library for XNA 4.0 Refresh 
       In the XNAnimation Pipeline, open up SkinnedModelImporter.cs and replace it with the following source code below.

Source Code Below:

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.IO;
using System.Runtime.InteropServices;
using System.Text;
using System.Xml;
using System.Linq;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.Content.Pipeline;
using Microsoft.Xna.Framework.Content.Pipeline.Graphics;
using Microsoft.Xna.Framework.Content.Pipeline.Processors;
using Microsoft.Xna.Framework.Design;


// The type to import.
using TImport = Microsoft.Xna.Framework.Content.Pipeline.Graphics.NodeContent;

// Change the namespace to suit your project
namespace SkinnedModelPipeline
{
    [ContentImporter(".fbx", DisplayName = "Multi-take FBX Importer", DefaultProcessor = "")]
    public class SkinnedModelImporter : FbxImporter
    {
        private List<string> _animfiles;
        private List<string> _fbxheader;
        private TImport _master;
        private ContentImporterContext _context;

        public override TImport Import(string filename, ContentImporterContext context)
        {
            _context = context;

            // _animfiles will contain list of new temp anim files.
            _animfiles = new List<string>();

            // Decouple header and animation data.
            ExtractAnimations(filename);

            // Process master file (this will also process the first animation)
            _master = base.Import(filename, context);

            // Process the remaining animations.
            foreach (string file in _animfiles)
            {
                TImport anim = base.Import(file, context);

                // Append animation to master NodeContent.
                AppendAnimation(_master, anim);
            }

            // Delete the temporary animation files.
            DeleteTempFiles();

            return _master;
        }

        private void AppendAnimation(NodeContent masternode, NodeContent animnode)
        {
            foreach (KeyValuePair<string, AnimationContent> anim in animnode.Animations)
            {
                if (!masternode.Animations.ContainsKey(anim.Key))
                {
                    masternode.Animations.Add(anim.Key, anim.Value);
                }
                else
                {
                    //overwrite the animation that was stored inside the
                    //master file because it is of the wrong length (except the first animation).
                    masternode.Animations[anim.Key] = anim.Value;
                }
            }

            //foreach (NodeContent child in animnode.Children) {
            //    if (child != null) {
            //        AppendAnimation(child);
            //    }
            //}

            for (int i = 0; i < masternode.Children.Count; i++)
            {
                if (animnode.Children[i] != null)
                {
                    AppendAnimation(masternode.Children[i], animnode.Children[i]);
                }
            }
        }

        private void ExtractAnimations(string filename)
        {
            List<string> masterFile = File.ReadAllLines(filename).ToList();
            string path = Path.GetDirectoryName(filename);
            int open_idx = 0,
                length,
                num_open = -1,
                filenum = 0;
            bool foundTake = false;

            int idx = masterFile.IndexOf("Takes:  {") + 1;
            _fbxheader = masterFile.Take(idx).ToList();
            List<string> anims = masterFile.Skip(idx).ToList();

            // Extract each animation and create a temporary anim file.
            for (int i = 0; i < anims.Count; i++)
            {
                if (anims[i].Contains("Take: "))
                {
                    open_idx = i;
                    num_open = 0;
                    foundTake = true;
                }

                if (anims[i].Contains("{") &&
                    foundTake)
                {
                    num_open++;
                }

                if (anims[i].Contains("}") &&
                    foundTake)
                {
                    num_open--;
                }

                if (num_open == 0 &&
                    foundTake)
                {
                    // Skip first animation since this is processed in the master
                    // fbx file.
                    if (filenum > 0)
                    {
                        length = i - open_idx + 1;

                        // Create temp file from header + anim data.
                        CreateTempFile(Path.Combine(path, "tmp.anim." + filenum + ".fbx"),
                                       anims.Skip(open_idx).Take(length).ToArray());
                    }
                    filenum++;
                    foundTake = false;
                }
            }
        }

        private void CreateTempFile(string filename, string[] data)
        {
            List<string> completefile = new List<string>();
            completefile.AddRange(_fbxheader);
            completefile.AddRange(data);

            try
            {
                // Write data to new temp file.
                File.WriteAllLines(filename, completefile.ToArray());

                // Store temp file name for processing.
                _animfiles.Add(filename);
            }
            catch
            {
                // Error while creating temp file.
                _context.Logger.LogWarning(null, null, "Error creating temp file: {0}", filename);
            }
        }

        private void DeleteTempFiles()
        {
            foreach (string file in _animfiles)
            {
                File.Delete(file);
            }
        }
    }
}