Feb 4, 2012

Animation Sharing between Different Characters with Same Topology in Unity


Image Source: http://answers.unity3d.com/questions/191282/sharing-animations-between-models.html


So let's say you have multiple characters with different bone lengths, but you just don't have manpower to author different animations for all of them. Can we share animations, instead?  Sure, why not, if you can live without transition and scale animations on joints: in other words, you are only gonna use rotations. Think it this way. When you rotate your right elbow inward by 90 degrees, your pose will look same as your shorter friend applying the exactly same rotation on his right elbow. Got it?

Okay, so I was searching Unity forum to see if rotation-only animation is officially supported.  Didn't look like it.  Then next up.  Can we somehow trim out non-rotational info from an anim file in the pipeline? I was not able to find the answer again, or even anyone asking. (Sorry, after finishing this post, I actually found someone asking, but noone really answered.)

And the good news is....... -drum rolls- .... I figured it out! :D

So this is what I came up with and it works great: (code shown below)

  1. Make a script called ConvertToRotationOnlyAnim.cs inside of Assets/Editor folder.
  2. Add a menu item invoking this script.
  3. Import your animation into Unity.  (doesn't matter where it's from as long as Unity sees it as animation)
  4. Right-click on the imported animation asset and select the menu item we just added at step #2.
  5. In the Script, copy over only the curves which have "m_LocalRotation" as propertyName field.
  6. Now set the new _rot animation clip to your game object's animation component.
  7. Hit play and enjoy... :)
And here is the full source code I wrote for this.  Hopefully the comment is self-explanatory:

using UnityEditor;
using UnityEngine;

using System.IO;

public class ConvertToRotationOnlyAnim
{
    [MenuItem("Assets/Convert To Rotation Animation")]
    static void ConvertToRotationAnimation()
    {
        // Get Selected Animation Clip
        AnimationClip sourceClip = Selection.activeObject as AnimationClip;
        if (sourceClip == null)
        {
            Debug.Log("Please select an animation clip");
            return;
        }

        // Rotation only anim clip will have "_rot" post fix at the end
        const string destPostfix = "_rot";

        string sourcePath = AssetDatabase.GetAssetPath(sourceClip);
        string destPath = Path.Combine(Path.GetDirectoryName(sourcePath), sourceClip.name) + destPostfix + ".anim";

        // first try to open existing clip to avoid losing reference to this animation from other meshes that are already using it.
        AnimationClip destClip = AssetDatabase.LoadAssetAtPath(destPath, typeof(AnimationClip)) as AnimationClip;
        if (destClip == null)
        {
            // existing clip not found.  Let's create a new one
            Debug.Log("creating a new rotation only animation at " + destPath);
            
            destClip = new AnimationClip();
            destClip.name = sourceClip.name + destPostfix;

            AssetDatabase.CreateAsset(destClip, destPath);
            AssetDatabase.Refresh();

            // and let's load it back, just to make sure it's created?
            destClip = AssetDatabase.LoadAssetAtPath(destPath, typeof(AnimationClip)) as AnimationClip;
        }

        if (destClip == null)
        {
            Debug.Log("cannot create/open the rotation only anim at " + destPath);
            return;
        }

        // clear all the existing curves from destination.
        destClip.ClearCurves();

        // Now copy only rotation curves
        AnimationClipCurveData[] curveDatas = AnimationUtility.GetAllCurves(sourceClip, true);
        foreach (AnimationClipCurveData curveData in curveDatas)
        {
            if (curveData.propertyName.Contains("m_LocalRotation"))
            {
                AnimationUtility.SetEditorCurve(
                    destClip,
                    curveData.path,
                    curveData.type,
                    curveData.propertyName,
                    curveData.curve
                );
            }
        }

        Debug.Log("Hooray! Coverting to rotation-only anim to " + destClip.name + " is done");
    }
}




Update (Mar 21, 2011): One anonymous commenter enhanced this by allowing local root bone translation.  Without this you won't be able to translate your model :) 


Change this line to:
if (curveData.propertyName.Contains("m_LocalRotation"))


this:
if (curveData.propertyName.Contains("m_LocalRotation")
  || (curveData.path.Equals("Bip01 Pelvis")
  && curveData.propertyName.Contains("m_LocalPosition")))


Thanks anonymous! ( not the hacker group :P )

8 comments:

  1. Very impressive and generous of you to provide the source code. Programmers can do anything!

    ReplyDelete
    Replies
    1. lol that sounds nicer than what i did.. :P thanks!

      On my free time, I play with different things for a while until i get bored and move onto something else.... and I figured whatever knowledge I found out while i was playing might be beneficial to other people, and didn't want them to waste time figuring out what I did already..

      So that's why I'm doing it.. cuz I enjoy freely available knowledge out there too...

      Delete
  2. Thanks a lot! I was just scrubbing my head with this yesterday.. using same animation on multiple characters I mean. The problem was the models got stretched since the animation was not rotation only. Might even better idea to do the conversion in animation software, just haven't figured out yet how to do it.

    ReplyDelete
    Replies
    1. glad it helped... Stripping out those info from 3DS Max is technically possible, but I found it's much easier to do it in the script. i.e., you will have to strip it out again and again whenever you change something in 3DS Max.

      Delete
    2. Made a small enhancement. By not removing the m_localPosition from the skeleton's root object you can still have movement in your animations.. not just in place animation :)

      Small change (in MY skeleton bip01 pelvis is the root):

      if (curveData.propertyName.Contains("m_LocalRotation") || (curveData.path.Equals("Bip01 Pelvis") && curveData.propertyName.Contains("m_LocalPosition")))

      Works great.. BUT if you really have big differences on skeleton heights you must take the difference into account.. add the diff to the roots position in the lateupdate for example..

      Delete
    3. Oh nice.. :) that makes sense I'll update my main post too! thanks!

      Delete
  3. Would it be possible to add an offset to the Bip01 bone so that should the two characters be different proportions, the Y position of the root bone would adjust accordingly?

    It works great if the two characters sharing animations' Bip01 bone are lined up, but if I have say a child character and I want his animations on an adult, the adult would be halfway through the floor since the child's size is so different. If not, then there's no problem in scaling!

    ReplyDelete
    Replies
    1. I guess you put the root bone at pelvis? I usually put the root bone at the ground and pelvis being the only child of the root bone. This way, the problem you are describing shouldn't happen.

      also adding offset can be done in code too. simply apply a different world position for each character? this world transformation will be multiplied to the transformations from the anim system.

      also new anim system in unity 4 seems to support copying animation between different skeletons it. I don't touch Unity nowadays, so had no chance to look at personally though

      Delete