[Editor Script] Mirror AnimationClip generator

Discussion in 'Scripting' started by n0mad, Dec 8, 2009.

  1. n0mad

    n0mad

    Member

    Joined:
    Jan 27, 2009
    Messages:
    3,731
    Hello,

    After days of researches and hard work, I came to find out how to generate mirror clips.

    In short, it takes an animation component from an FBX, and automatically generates mirrored clips of the model without using negative scaling.
    (reminder : negative scaling a skinnedMesh can lead to cutting the overall FPS by an half).

    it is useful for every type of game where you typically have to make 2 same models face each other, like a fighting game.

    Unfortunately, I'm afraid that due to a lack of indeep mathematical engineering (and of time), I can't come to a generic script that would accept any character rig and magically translate it.

    I know there is a way to do it generically (hint : detecting each bone's relative orientation to its child), but I just don't have enough time, as it would take days to make it clean.

    All I can do is to provide an guideline tutorial. Better than nothing, heh :roll:

    Here we go :

    1) Make a class that extends AssetPostprocessor
    2) In this class, put a function called OnPreprocessModel(). Here you will put anything you need to be initialized (in this case I'm using a lot of hashtables to store transactional AnimationCurves)

    2a) Here, you can use a method to automatically split your animationclips. Here is one I cooked :

    It will use a simple .TXT file, in which each line would be a new animationClip.
    But it has to follow that precise format :

    firstFrameNumber-lastFrameNumber : clipName : LoopBoolean :

    like this :

    0-4 : bindpose : false :
    5-30 : stand : true :
    35-48 : s_LP : false :
    etc..

    Its name would be the same as the .FBX file it's refering to.

    Code (csharp):
    1. void SplitClips ()
    2.     {
    3. ModelImporter MI = (ModelImporter)assetImporter;
    4.        
    5.         MI.splitAnimations = true;
    6.         MI.generateAnimations = ModelImporterGenerateAnimations.InRoot;
    7.         MI.reduceKeyframes = true;
    8.        
    9.         _text = (TextAsset)Resources.Load ("YourAnimMapFolder/"+MI.name);
    10.         //Relative to Resources folder
    11.  
    12.         string[] _strings;
    13.         string _clipName;
    14.         int _start;
    15.         int _end;
    16.         bool _loop;
    17.        
    18.         string[] _array = _text.text.Split (new char[] { '\n' });
    19.        
    20.         _clipAnimations = new ModelImporterClipAnimation[_array.Length];
    21.        
    22.         for (int i = 0; i < _array.Length; i++) {
    23.             _strings = _array[i].Split (new char[] { ':' });
    24.             _start = System.Convert.ToInt16 (_strings[0].Split (new char[] {
    25.                 '-',
    26.                 ' '
    27.             })[0].Trim ());
    28.             _end = System.Convert.ToInt16 (_strings[0].Split (new char[] {
    29.                 '-',
    30.                 ' '
    31.             })[1].Trim ());
    32.             _clipName = _strings[1].Trim ();
    33.             _loop = System.Convert.ToBoolean (_strings[2].Trim ());
    34.            
    35.             _clipAnimations[i] = NewClip (_clipName, _start, _end, _loop);
    36.            
    37.         }
    38.        
    39.         MI.clipAnimations = _clipAnimations;
    40.        
    41.        
    42.        
    43.     }
    44.  
    45. ModelImporterClipAnimation NewClip (string _clipName, int _start, int _end, bool _loop)
    46.     {
    47.        
    48.         _newClip.name = _clipName;
    49.         _newClip.firstFrame = _start;
    50.         _newClip.lastFrame = _end;
    51.         _newClip.loop = _loop;
    52.        
    53.         return _newClip;
    54.        
    55.     }
    56.  
    So you won't have to re-enter your Import Settings each time you mess something with the FBX file ;)

    3) Then, create OnPostprocessModel() (which means after the model has been imported). All the manipulation stuff goes in there. Everything below is placed in this function.
    4) Make sure you're in a safe folder with :
    Code (csharp):
    1. if (assetPath.IndexOf ("/YourCustomDirectory/") == -1)
    2.             return;
    This folder has to be in Resources directory.
    5) You can put a nice prompt like :
    Code (csharp):
    1. if (EditorUtility.DisplayDialog ("Conversion", "Create a mirror animation from the original ?", "Yes", "No"))
    2.             Apply (g);
    If no is pressed, nothing happens.
    6) The Apply(GameObject g) function is where all the core manipulations will be made, which is why we're asking a reference of the original asset's GameObject.
    First, you will have to create the .ANIM asset we will be writing :
    Code (csharp):
    1. //Asset GameObject creation
    2.         _newAsset = new GameObject (g.name);
    3.         _newPath = "Assets/Resources/YourCustomDirectory/anim_" + g.name + ".anim";
    4.                
    5.         _newAnim = (Animation)_newAsset.AddComponent (typeof(Animation));      
    6.         _newAnim.hideFlags = HideFlags.NotEditable;    
    7.         AssetDatabase.CreateAsset (_newAnim, _newPath);
    8. //Just link the Animation to the asset
    Important thing : don't forget to set the hideFlags to something else than DontSave for each object your adding to this asset, or it won't be saved.

    Ok, here comes our animation generator.
    Here, we only consider creating mirror clips for a biped animation, but it can be extended to every type of model, as soon as you understand "how your rig works".
    Basically, you "just" have to understand how your rig works.
    Each bone is a rotation, except for the root bone, which is also a position (scale not recommended at all).
    So mirroring is just wanting to make the right side act as the left side, and vice-versa.

    The trick is to catch every rotation dependancy one bone of a side has with its mirror one. This might sound painful, but it only takes up to 30 minutes :

    1) just put your rig in a editable bindpose state (= static, without any animated transform). Check off Animation component, or you won't be able to change values.
    2) for each editor rotation field of each bone, increment it by 30-50°, and see if the mirror side field has to be incremented by a negative or positive factor to create a mirror effect.
    For example, for the most bones of my rig, if I add 40° to leftBone's X-axis Local Rotation (Euler Angles speaking), I have to add 40° to rightBone's X-axis Local Rotation too.
    But for Y and Z, I have to add the opposite. Adding 40° to leftBone's Y-axis Local Rotation would mean adding -40° to rightBone's Y-axis Local Rotation, same for Z.

    Each bone is different, but if you managed to make a clean rig, you wouldn't have to change this setup too many times, because each bone alignment would be natively unified by Unity.

    3) So the trick is to parse every AnimationCurve of every bone in every AnimationClip, and then create a new AnimationClip() from its conversion (if you don't create new clip, your changes won't be saved as you would be working on the FBX read-only file).
    AnimationUtility is very good for parsing and setting this stuff, GetAllCurves() to get curves arrays, SetEditorCurve() to set your animationClip's optimized curve.

    The conversion itself consists of determining by how much the side you're copying is being rotated from its bindpose, and then apply this rotation to your converted bone, without forgetting to apply its negative/positive factor.

    For each curve's key, the common formula is :
    _newKeyVal = DestinationBodyMember_bindposeKeyVal + ((_curve.keys[currentKeyIndex].value - ReferenceBodyMember_bindposeKeyVal) * Sign_factor);

    For rotations, you will have to process an Euler conversion prior to this formula, do the calculation for X, Y and Z, and then convert it back to Quaternion for it to be written in the curve.

    When your AnimationClip creation is finished, you can add it to the asset :

    Code (csharp):
    1. AssetDatabase.AddObjectToAsset (_newClip, _newAssetPath);
    And reimport it, to make changes visible before hitting button "Save Project" :

    Code (csharp):
    1. AssetDatabase.ImportAsset (_newAssetPath);
    4)As we're creating a new asset from scratch, it will be more convenient to copy the non-mirrored clips too.

    Here is a function I made, inspired by a post from Rune, which can also inspire your mirror clip generation routine.
    Code (csharp):
    1.  
    2. //AnimationClipCurveData[] is transmitted by AnimationUtlity.GetAllCurves()
    3. void AddCloneClip (string _newName, AnimationClipCurveData[] _curvesData, string _oldName)
    4.     {  
    5.         _newClip = new AnimationClip ();
    6.         _newClip.name = _newName;
    7.            
    8.         for (int i = 0; i < _curvesData.Length; i++) {
    9.             AnimationUtility.SetEditorCurve (_newClip, _curvesData[i].path, _curvesData[i].type, _curvesData[i].propertyName, _curvesData[i].curve);
    10.         }
    11.        
    12. //_newAnim is the animation container placed in our new asset, indeed  
    13. //_anim is the Animation component of the FBX
    14.         if (_newAnim[_newName] == null) {
    15.             _newAnim.AddClip (_newClip, _newName);
    16.             _newAnim[_newName].speed = _anim[_oldName].speed;
    17.             _newAnim[_newName].wrapMode = _anim[_oldName].wrapMode;
    18.         }
    19.         _newClip.hideFlags = HideFlags.NotEditable;
    20.         AssetDatabase.AddObjectToAsset (_newClip, _newAssetPath);
    21.        
    22.         _newClip = null;
    23.         Object.DestroyImmediate (_newClip);    
    24.     }
    5) Finally, you just have to save the asset by performing some functions :

    Code (csharp):
    6) and kill the gameObject which hosted the Animation container :

    Code (csharp):
    1. foreach (Transform child in _newAsset.transform)
    2.             Object.DestroyImmediate (child.gameObject);
    3.        
    4.         Object.DestroyImmediate (_newAsset);
    Weird thing is that we have to destroy its child first, because if not, Unity would throw an exception telling that we can't destroy instances to avoid its dependancies data loss. But here we never created children, though.

    7) Et voilà.


    Sorry, it's a bit messy, as we have to make the script perfetly fit our rig's setup, which is why I can't write a generic script.

    I hope to have help folks, though, as it works like a charm for me. My clips are perfectly mirrored now, without any performance loss.

    Good luck
  2. n0mad

    n0mad

    Member

    Joined:
    Jan 27, 2009
    Messages:
    3,731
    One thing I forgot to mention was that this method doesn't involve real mirroring, but a realistic side switching.

    Which means that it does save texture native orientation.

    For example, with classic negative scaling, the regular chocolate bar would look like this :

    [​IMG]

    With this thread's method, all the textures would keep their readability.
  3. gritche

    gritche

    New Member

    Joined:
    Aug 27, 2009
    Messages:
    44
    The very first part (creating/saving clips) helped me cut my loading time by 5x. I may come back to this post if i need some mirroring, but many thanks allready.
  4. n0mad

    n0mad

    Member

    Joined:
    Jan 27, 2009
    Messages:
    3,731
    Glad to have helped you, you're welcome ;)
  5. the_gnoblin

    the_gnoblin

    Member

    Joined:
    Jan 10, 2009
    Messages:
    721
    :D very helpful, thanks.
  6. goodhustle

    goodhustle

    New Member

    Joined:
    Jun 4, 2009
    Messages:
    310
    Hi n0mad, thanks for posting this! I'm partway through following your roadmap on how to do this, and I'm wondering if you recall much about your approach.

    I am finding that since curves come out of my data one quaternion component at a time, I need to do two passes through the data in order to collect keyframes from multiple components, then go to eulerangles and back to quaternions, and then finally loop through the curve data again and assign the reversed data. Does that sound right?
  7. goodhustle

    goodhustle

    New Member

    Joined:
    Jun 4, 2009
    Messages:
    310
    I have this completely working now in Unity iPhone 1.7, but unfortunately the process breaks in my Unity 3.0b4. I haven't tried the script I wrote in Unity 2.6, so I'm not sure what happened, but my guess is that the rotation interpolation settings changed somewhere along the line. It's too bad we don't have access to that curve interpolation setting via script. Did you ever try this trick on recent Unity (non-iPhone) builds?
  8. goodhustle

    goodhustle

    New Member

    Joined:
    Jun 4, 2009
    Messages:
    310
    For future reference, I filed case 368352 about problems using this approach in Unity 3.0b4.
  9. goodhustle

    goodhustle

    New Member

    Joined:
    Jun 4, 2009
    Messages:
    310
    Great news! Rune responded to my issue and was able to suggest a fix. When creating new quaternions from the euler angle representation that we work within, there are actually two quaternions that correspond to the same euler angle set. So when you convert back, you can apply something similar to the following code pattern to ensure that the "correct" rotation is applied:

    Code (csharp):
    1.  
    2. if (Quaternion.Dot(_newRotationKeyframes[j], _newRotationKeyframes[j-1]) < 0) {
    3.    _newRotationKeyframes[j] = new Quaternion(
    4.       -_newRotationKeyframes[j].x,
    5.       -_newRotationKeyframes[j].y,
    6.       -_newRotationKeyframes[j].z,
    7.       -_newRotationKeyframes[j].w);
    8. }
    9.  
    This operates on an array of rotation keyframes in Quaternion representation named _newRotationKeyframes.
  10. n0mad

    n0mad

    Member

    Joined:
    Jan 27, 2009
    Messages:
    3,731
    Oh ... I'm totally sorry not to have noticed your question back ago Getluky .......

    Well let's get to the core :
    Yes, yes, you have to do the back and forth, because of the unserializable component (if I recall correctly).


    Thank you very much for this contribution, to both of you !

    I'll be diving into serious code revision for U3 next week, so this will be my top priority to adapt the mirroring script. Be sure to have full explanations from me on how to make it work on U3, very soon !

    Cheers
  11. n0mad

    n0mad

    Member

    Joined:
    Jan 27, 2009
    Messages:
    3,731
  12. dogzerx2

    dogzerx2

    Member

    Joined:
    Dec 27, 2009
    Messages:
    2,528
    Damn... is negative scaling really that bad? I'm doing the negative scale thing with a character and it doesn't seem to affect my PC, but will affect the iphone? (I'm yet to purchase an iphone for testing)
  13. n0mad

    n0mad

    Member

    Joined:
    Jan 27, 2009
    Messages:
    3,731
    Sorry, didn't see the update post ^^
    Yep negative scaling is slaughtering the CPU. I tested on iPhone 3GS, which is still pretty fast, and fps just dropped by an half :/
    I don't know how it would act on today's 1.5Ghz cpus, but I guess it would put an unnecessary fps drop as the fbx mirroring can be achieved. I'm still using it, and have to assume loading twice the base number of animations, but I guess it's better than 15 fps :)