Search Unity

Scripting Root Motion

Discussion in 'Editor & General Support' started by Sovogal, Sep 17, 2021.

  1. Sovogal

    Sovogal

    Joined:
    Oct 15, 2016
    Posts:
    100
    Hey. Been banging my head against the keyboard for a week on this issue. I really hope someone here can help me figure out the missing piece to get perfect scripted root motion.

    The issue:
    I need to be able to script root motion at editor time for playback of the motions at runtime without relying on root motion being applied in the animator. I'm running into some position and rotation issues.

    The setup:
    I have a mixamo animation called Sword And Shield Slash using a y-bot model and humanoid avatar rigged from the model. I also have an editor script that bakes root motion from the curve bindings of the animation. The script also allows for running both the baked animation through the animator and an un-baked animation from the AnimationClip.SampleAnimation function.

    Here's an illustration of the issue:

    AnimationClip.SampleAnimation:


    Manually moving transform from baked curves and Animator.applyRootMotion == false:


    Here are the scripts I'm using in case anyone wants to try this for themselves:

    RootMotionBaker.cs
    Code (CSharp):
    1. using System.Collections;
    2. using System.Collections.Generic;
    3. using UnityEngine;
    4.  
    5. public class RootMotionBaker : MonoBehaviour
    6. {
    7.     public AnimationClip Clip;
    8.  
    9.     public Animator Animator;
    10.  
    11.     public AnimationCurve PositionXCurve;
    12.     public AnimationCurve PositionYCurve;
    13.     public AnimationCurve PositionZCurve;
    14.  
    15.     public AnimationCurve RotationXCurve;
    16.     public AnimationCurve RotationYCurve;
    17.     public AnimationCurve RotationZCurve;
    18.     public AnimationCurve RotationWCurve;
    19.  
    20.     public AnimationCurve EulerAnglesXCurve;
    21.     public AnimationCurve EulerAnglesYCurve;
    22.     public AnimationCurve EulerAnglesZCurve;
    23.  
    24.  
    25.     // Start is called before the first frame update
    26.     void Start()
    27.     {
    28.     }
    29.  
    30.     // Update is called once per frame
    31.     void Update()
    32.     {
    33.      
    34.     }
    35. }
    36.  
    RootMotionBakerEditor.cs
    Code (CSharp):
    1. using System;
    2. using System.Collections;
    3. using System.Collections.Generic;
    4. using System.Linq;
    5. using UnityEditor;
    6. using UnityEditor.Animations;
    7. using UnityEngine;
    8.  
    9. [CustomEditor(typeof(RootMotionBaker))]
    10. public class RootMotionBakerEditor : Editor
    11. {
    12.  
    13.     private RootMotionBaker _rootMotionBaker;
    14.     private bool _runningBaked;
    15.     private bool _runningStandard;
    16.     private bool _paused;
    17.     private bool _baking;
    18.     private float _totalTimeRunning;
    19.     private AnimatorController _originalAnimatorController;
    20.     private AnimatorController _replacementAnimatorController;
    21.     private float _timeFactor = .5f;
    22.  
    23.     private void Awake()
    24.     {
    25.         _rootMotionBaker = target as RootMotionBaker;
    26.     }
    27.  
    28.     public override void OnInspectorGUI()
    29.     {
    30.         base.OnInspectorGUI();
    31.  
    32.         //Animator
    33.         var animatorProp = serializedObject.FindProperty("Animator");
    34.         if (animatorProp.objectReferenceValue == null)
    35.         {
    36.             animatorProp.objectReferenceValue = _rootMotionBaker.GetComponent<Animator>();
    37.         }
    38.         else
    39.         {
    40.         }
    41.  
    42.         if (serializedObject.FindProperty("Clip").objectReferenceValue != null)
    43.         {
    44.             if (GUILayout.Button("Bake Root Motion Curves"))
    45.             {
    46.                 var curves = RootMotionFunctions.GetCurves(_rootMotionBaker.Clip);
    47.                 WriteCurves(curves);
    48.             }
    49.  
    50.             GUI.enabled = (_runningBaked || _runningStandard);
    51.  
    52.             if (GUILayout.Button("Pause"))
    53.             {
    54.                 _paused = !_paused;
    55.             }
    56.  
    57.  
    58.             GUI.enabled = true;
    59.  
    60.  
    61.             GUI.enabled = !(_runningBaked || _runningStandard);
    62.  
    63.             if (GUILayout.Button("Run Baked Animation"))
    64.             {
    65.                 _runningBaked = true;
    66.                 _rootMotionBaker.Animator.applyRootMotion = false;
    67.  
    68.                 //Set up animator controller
    69.                 _originalAnimatorController = (AnimatorController)_rootMotionBaker.Animator.runtimeAnimatorController;
    70.  
    71.                 _replacementAnimatorController = new AnimatorController();
    72.                 _replacementAnimatorController.AddLayer("Default");
    73.                 _replacementAnimatorController.layers[0].stateMachine.AddState("Default");
    74.                 _replacementAnimatorController.AddMotion(_rootMotionBaker.Clip, 0);
    75.                 _rootMotionBaker.Animator.runtimeAnimatorController = _replacementAnimatorController;
    76.             }
    77.  
    78.             if (GUILayout.Button("Run Standard Animation"))
    79.             {
    80.                 _runningStandard = true;
    81.             }
    82.  
    83.             GUI.enabled = true;
    84.         }
    85.  
    86.         serializedObject.ApplyModifiedProperties();
    87.     }
    88.  
    89.     private void OnSceneGUI()
    90.     {
    91.         if (!_paused)
    92.         {
    93.             if (_rootMotionBaker.Animator != null && _runningBaked) _rootMotionBaker.Animator.Update(Time.deltaTime);
    94.  
    95.             if (_runningBaked)
    96.             {
    97.                 _rootMotionBaker.Animator.PlayInFixedTime(_rootMotionBaker.Clip.name, 0, _totalTimeRunning);
    98.  
    99.                 Vector3 newPosition = new Vector3(_rootMotionBaker.PositionXCurve.Evaluate(_totalTimeRunning), _rootMotionBaker.PositionYCurve.Evaluate(_totalTimeRunning), _rootMotionBaker.PositionZCurve.Evaluate(_totalTimeRunning));
    100.                 Quaternion newRotation = new Quaternion(_rootMotionBaker.RotationXCurve.Evaluate(_totalTimeRunning), _rootMotionBaker.RotationYCurve.Evaluate(_totalTimeRunning), _rootMotionBaker.RotationZCurve.Evaluate(_totalTimeRunning), _rootMotionBaker.RotationWCurve.Evaluate(_totalTimeRunning));
    101.  
    102.  
    103.                 _rootMotionBaker.Animator.transform.position = newPosition;
    104.                 _rootMotionBaker.Animator.transform.rotation = newRotation;
    105.  
    106.                 _totalTimeRunning += Time.deltaTime * _timeFactor;
    107.                 if (_totalTimeRunning >= _rootMotionBaker.Clip.length)
    108.                 {
    109.                     _runningBaked = false;
    110.                     _totalTimeRunning = 0f;
    111.  
    112.                     _rootMotionBaker.Animator.transform.position -= newPosition;
    113.                     _rootMotionBaker.Animator.transform.rotation *= Quaternion.Inverse(newRotation);
    114.  
    115.                     _rootMotionBaker.Animator.runtimeAnimatorController = _originalAnimatorController;
    116.                 }
    117.  
    118.  
    119.             }
    120.  
    121.             if (_runningStandard)
    122.             {
    123.                 _rootMotionBaker.Clip.SampleAnimation(_rootMotionBaker.Animator.gameObject, _totalTimeRunning);
    124.  
    125.                 _totalTimeRunning += Time.deltaTime * _timeFactor;
    126.                 if (_totalTimeRunning >= _rootMotionBaker.Clip.length)
    127.                 {
    128.                     _runningStandard = false;
    129.                     _totalTimeRunning = 0f;
    130.  
    131.                 }
    132.             }
    133.         }
    134.     }
    135.  
    136.     private void WriteCurves(RootMotionFunctions.RootMotionCurves curves)
    137.     {
    138.         serializedObject.FindProperty("PositionXCurve").animationCurveValue = curves.PositionXCurve;
    139.         serializedObject.FindProperty("PositionYCurve").animationCurveValue = curves.PositionYCurve;
    140.         serializedObject.FindProperty("PositionZCurve").animationCurveValue = curves.PositionZCurve;
    141.         serializedObject.FindProperty("RotationXCurve").animationCurveValue = curves.RotationXCurve;
    142.         serializedObject.FindProperty("RotationYCurve").animationCurveValue = curves.RotationYCurve;
    143.         serializedObject.FindProperty("RotationZCurve").animationCurveValue = curves.RotationZCurve;
    144.         serializedObject.FindProperty("RotationWCurve").animationCurveValue = curves.RotationWCurve;
    145.         serializedObject.FindProperty("EulerAnglesXCurve").animationCurveValue = curves.EulerAnglesXCurve;
    146.         serializedObject.FindProperty("EulerAnglesYCurve").animationCurveValue = curves.EulerAnglesYCurve;
    147.         serializedObject.FindProperty("EulerAnglesZCurve").animationCurveValue = curves.EulerAnglesZCurve;
    148.  
    149.     }
    150. }
    151.  
    RootMotionFunctions.cs (static helper class):
    Code (CSharp):
    1. using System;
    2. using System.Collections;
    3. using System.Collections.Generic;
    4. using System.Linq;
    5. using UnityEditor;
    6. using UnityEditor.Animations;
    7. using UnityEngine;
    8.  
    9. public static class RootMotionFunctions
    10. {
    11.     public struct RootMotionCurves
    12.     {
    13.         public AnimationCurve PositionXCurve;
    14.         public AnimationCurve PositionYCurve;
    15.         public AnimationCurve PositionZCurve;
    16.  
    17.         public AnimationCurve RotationXCurve;
    18.         public AnimationCurve RotationYCurve;
    19.         public AnimationCurve RotationZCurve;
    20.         public AnimationCurve RotationWCurve;
    21.  
    22.         public AnimationCurve EulerAnglesXCurve;
    23.         public AnimationCurve EulerAnglesYCurve;
    24.         public AnimationCurve EulerAnglesZCurve;
    25.     }
    26.  
    27.  
    28.     public static RootMotionCurves GetCurves(AnimationClip clip)
    29.     {
    30.         Action<AnimationCurve, float, float> normalizeKeyframes = (curve, minValue, maxValue) =>
    31.         {
    32.             if (curve.keys.Length > 1)
    33.             {
    34.                 Keyframe[] keyframes = curve.keys.ToArray();
    35.                 float startFrameValue = keyframes.First().value;
    36.                 for (int i = 0; i < keyframes.Length; i++)
    37.                 {
    38.                     keyframes[i].value = Mathf.Clamp(keyframes[i].value - startFrameValue, minValue, maxValue);
    39.                 }
    40.                 curve.keys = keyframes;
    41.             }
    42.         };
    43.  
    44.         var positionXCurve = new AnimationCurve { keys = GetCurveFromAnimationClip(clip, "RootT.x").keys.ToArray() };
    45.         normalizeKeyframes(positionXCurve, float.NegativeInfinity, float.PositiveInfinity);
    46.  
    47.         var positionYCurve = new AnimationCurve { keys = GetCurveFromAnimationClip(clip, "RootT.y").keys.ToArray() };
    48.         normalizeKeyframes(positionYCurve, float.NegativeInfinity, float.PositiveInfinity);
    49.  
    50.         var positionZCurve = new AnimationCurve { keys = GetCurveFromAnimationClip(clip, "RootT.z").keys.ToArray() };
    51.         normalizeKeyframes(positionZCurve, float.NegativeInfinity, float.PositiveInfinity);
    52.  
    53.         var quaternionXCurve = new AnimationCurve { keys = GetCurveFromAnimationClip(clip, "RootQ.x").keys.ToArray() };
    54.         var quaternionYCurve = new AnimationCurve { keys = GetCurveFromAnimationClip(clip, "RootQ.y").keys.ToArray() };
    55.         var quaternionZCurve = new AnimationCurve { keys = GetCurveFromAnimationClip(clip, "RootQ.z").keys.ToArray() };
    56.         var quaternionWCurve = new AnimationCurve { keys = GetCurveFromAnimationClip(clip, "RootQ.w").keys.ToArray() };
    57.  
    58.         var eulerAnglesXCurve = new AnimationCurve();
    59.         var eulerAnglesYCurve = new AnimationCurve();
    60.         var eulerAnglesZCurve = new AnimationCurve();
    61.  
    62.         //Resample and normalize quaternions
    63.         var frames = (int)(clip.length * clip.frameRate);
    64.         var quaternionXKeyframes = new Keyframe[frames];
    65.         var quaternionYKeyframes = new Keyframe[frames];
    66.         var quaternionZKeyframes = new Keyframe[frames];
    67.         var quaternionWKeyframes = new Keyframe[frames];
    68.         var eulerAnglesXKeyframes = new Keyframe[frames];
    69.         var eulerAnglesYKeyframes = new Keyframe[frames];
    70.         var eulerAnglesZKeyframes = new Keyframe[frames];
    71.         Quaternion inverseStartRotation = Quaternion.Inverse(new Quaternion(quaternionXCurve.keys[0].value, quaternionYCurve.keys[0].value, quaternionZCurve.keys[0].value, quaternionWCurve.keys[0].value));
    72.         for (int i = 0; i < frames; i++)
    73.         {
    74.             float time = i * 1 / clip.frameRate;
    75.  
    76.             var xValue = quaternionXCurve.Evaluate(time);
    77.             var yValue = quaternionYCurve.Evaluate(time);
    78.             var zValue = quaternionZCurve.Evaluate(time);
    79.             var wValue = quaternionWCurve.Evaluate(time);
    80.  
    81.             Quaternion sample = new Quaternion(xValue, yValue, zValue, wValue);
    82.             var resample = sample * inverseStartRotation;
    83.  
    84.             quaternionXKeyframes[i].time = time;
    85.             quaternionXKeyframes[i].value = resample.x;
    86.             quaternionYKeyframes[i].time = time;
    87.             quaternionYKeyframes[i].value = resample.y;
    88.             quaternionZKeyframes[i].time = time;
    89.             quaternionZKeyframes[i].value = resample.z;
    90.             quaternionWKeyframes[i].time = time;
    91.             quaternionWKeyframes[i].value = resample.w;
    92.  
    93.  
    94.             Vector3 eulerAnglesSample = new Quaternion(xValue, yValue, zValue, wValue).eulerAngles;
    95.             eulerAnglesXKeyframes[i].time = time;
    96.             eulerAnglesXKeyframes[i].value = eulerAnglesSample.x;
    97.             eulerAnglesYKeyframes[i].time = time;
    98.             eulerAnglesYKeyframes[i].value = eulerAnglesSample.y;
    99.             eulerAnglesZKeyframes[i].time = time;
    100.             eulerAnglesZKeyframes[i].value = eulerAnglesSample.z;
    101.  
    102.         }
    103.         quaternionXCurve.keys = quaternionXKeyframes;
    104.         quaternionYCurve.keys = quaternionYKeyframes;
    105.         quaternionZCurve.keys = quaternionZKeyframes;
    106.         quaternionWCurve.keys = quaternionWKeyframes;
    107.  
    108.         eulerAnglesXCurve.keys = eulerAnglesXKeyframes;
    109.         eulerAnglesYCurve.keys = eulerAnglesYKeyframes;
    110.         eulerAnglesZCurve.keys = eulerAnglesZKeyframes;
    111.  
    112.         return new RootMotionCurves
    113.         {
    114.             PositionXCurve = positionXCurve,
    115.             PositionYCurve = positionYCurve,
    116.             PositionZCurve = positionZCurve,
    117.             RotationXCurve = quaternionXCurve,
    118.             RotationYCurve = quaternionYCurve,
    119.             RotationZCurve = quaternionZCurve,
    120.             RotationWCurve = quaternionWCurve,
    121.             EulerAnglesXCurve = eulerAnglesXCurve,
    122.             EulerAnglesYCurve = eulerAnglesYCurve,
    123.             EulerAnglesZCurve = eulerAnglesZCurve,
    124.         };
    125.     }
    126.  
    127.     private static AnimationCurve GetCurveFromAnimationClip(AnimationClip clip, string curvePropertyName)
    128.     {
    129.         var curveBindings = UnityEditor.AnimationUtility.GetCurveBindings(clip);
    130.  
    131.         //var curve = new AnimationCurve { keys = UnityEditor.AnimationUtility.GetEditorCurve(clip, curveBindings.FirstOrDefault(x => x.propertyName == curvePropertyName)).keys };
    132.         var curve = AnimationUtility.GetEditorCurve(clip, curveBindings.FirstOrDefault(x => x.propertyName == curvePropertyName));
    133.  
    134.         return curve;
    135.     }
    136.  
    137.  
    138.  
    139. }
    140.  
     
    Last edited: Sep 17, 2021