Search Unity

Question Using constraints to record animations with GameObjectRecorder in the Editor (not runtime)

Discussion in 'Animation Rigging' started by McKnuckle, Jan 6, 2021.

  1. McKnuckle

    McKnuckle

    Joined:
    May 18, 2018
    Posts:
    3
    Hey there,

    I'm currently trying to implement a version of Alexander Bereznyak's IK Rig System (video link) in Unity and I just ran into a problem.

    What I'm trying to do is converting a regular AnimationClip that animates bone transforms into an AnimationClip that animates several float, Vector3 and Quaternion/Vector4 properties in a custom component. I intend to use the GameObjectRecorder with the help of an EvaluationGraph, as Animation Rigging does the same e.g. when transferring motion to constraints. By playing back the animation while evaluating a graph that uses a constraint which calculates my needed property values, I should be able to then write those values into my custom component while recording that component's bindings.
    These animated properties are then used by several custom constraints to correctly arrange their source objects(/targets) in the scene whenever the animation is played back.

    My problem is that I can't use ordinary float/Vector3, etc. variables in the recording process, as they cannot be set during Animation Preview in the Unity Editor if they aren't already being animated by an AnimationClip (I'm assuming because they're not part of the AnimationStream or EvaluationGraph). I'm thinking that the solution to this is to use Animation Rigging's FloatProperty/Vector3Property, etc. instead in order for the AnimationStream to contain those animated properties, but I'm not entirely sure how to bind these values properly in a way that
    1) includes them in the EvaluationGraph
    2) makes them animatable or bindable to the GameObjectRecorder

    I'm currently using Unity version 2020.1.1f1.
    I wanted to ask around for any input you might have, because I'm not even sure whether this is the correct approach for what I'm trying to achieve. I'll be glad to provide any further clarifications if needed.
    Thank you!

    Edit: Added video link
     
    Last edited: Jan 8, 2021
  2. simonbz

    simonbz

    Unity Technologies

    Joined:
    Sep 28, 2015
    Posts:
    295
    Hi,

    There is no limitation to what properties the GameObjectRecorder can write to. If the property is animatable, it doesn't need to be animated necessarily to be written to. Have a look at the Bind function of the GameObjectRecorder if you want to bind properties manually: https://docs.unity3d.com/ScriptReference/Animations.GameObjectRecorder.Bind.html.

    Now, if you're looking at how we implemented the `BakeToConstraint` and `BakeToSkeleton` algorithms in `BakeUtils`, this is not the easiest use case of GameObjectRecorder. In `BakeToConstraint`, we're overriding the constraints to inverse solve back to constraint parameters (e.g. `TwoBoneIKConstraint` becomes `TwoBoneIKInverseConstraint`). Is this something you aim to achieve in your use case?

    Also, we're manually defining `BakeParameters` for all constraints that are used in `BakeUtils`. This is used to specify which bindings are to be considered in the GameObjectRecorder for `BakeToConstraint` and `BakeToSkeleton`. Have a look at the `TwoBoneIKConstraintEditor.cs` for an example of this.
     
  3. McKnuckle

    McKnuckle

    Joined:
    May 18, 2018
    Posts:
    3
    Hi, thank you for replying!

    I was a bit confused about this whole topic when I made my initial post, but I think that some things have cleared up for me now. Thus, I'll try to answer your questions and make this post more comprehensible

    1) Inverse Constraints

    I'm not entirely sure whether InverseRigConstraints are the way to go in this case, but the more I think about it the more I'm convinced that it should fit the purpose of my project.
    I'm guessing that I could use a RigConstraint instead of the Monobehaviour component I'm saving my needed values in right now, setting up an InverseConstraint for the baking process.

    2) The Recording Process

    Yes, I can bind my component's properties to the GameObjectRecorder without them needing to be of the types 'FloatProperty', 'Vector3Property', etc.. The recorded clip contains only empty bindings though, meaning they all have a flat FloatCurve of value 0. Also, the bound properties don't change within the Inspector during Animation Preview in the Editor.
    Keep in mind that the bound values are calculated and set within a custom constraint I made. The following is the ConstraintJob code:

    Code (CSharp):
    1. using Unity.Burst;
    2. using UnityEngine;
    3. using UnityEngine.Animations;
    4. using UnityEngine.Animations.Rigging;
    5.  
    6. // Reads the Transform values of an animated rig during Animation Preview
    7. // in order to calculate the necessary IKAnimValues.
    8. // Outputs those values to a dedicated component whose values are bound to
    9. // a GameObjectRecorder, resulting in the IKAnimClip.
    10.  
    11. [BurstCompile]
    12. public struct IKAnimSetterConstraintJob : IWeightedAnimationJob
    13. {
    14.     public ReadOnlyTransformHandle RigRoot;
    15.     public ReadOnlyTransformHandle LegRootL;
    16.     public ReadOnlyTransformHandle LegTipL;
    17.  
    18.     public IKAnimValues IKAnimComp;
    19.  
    20.     public float LegLMaxReach;
    21.  
    22.     public FloatProperty jobWeight { get; set; }
    23.  
    24.     public void ProcessRootMotion(AnimationStream stream) { }
    25.  
    26.     public void ProcessAnimation(AnimationStream stream)
    27.     {
    28.         // Inputs
    29.         var legRootLPos = LegRootL.GetPosition(stream);
    30.         var legTipLPos = LegTipL.GetPosition(stream);
    31.  
    32.         // Calculate Outputs
    33.         var rigRootPos = RigRoot.GetPosition(stream);
    34.         var rigRootRot = RigRoot.GetRotation(stream);
    35.         var legLDir = (legTipLPos - legRootLPos).normalized;
    36.         float legLScalar = Vector3.Distance(legRootLPos, legTipLPos) / LegLMaxReach;
    37.  
    38.         // Set Outputs
    39.         IKAnimComp.RootPos = rigRootPos;
    40.         IKAnimComp.RootRot = rigRootRot;
    41.  
    42.         IKAnimComp.LegLDir = legLDir;
    43.         IKAnimComp.LegLScalar = legLScalar;
    44.     }
    45. }

    My assumption is that the recorded animation only contains flat 0 curves because I'm not yet using an EvaluationGraph containing my constraint in the recording process (due to me thinking that InverseConstraints aren't fit for this purpose, but they probably are).
    But I'm not sure why the values don't change during Animation Preview when I'm in the Editor. This is what lead me to believe that I needed FloatProperties instead of floats, etc..

    The following code shows the recording process (called by a button press in the Inspector):

    Code (CSharp):
    1. using System;
    2. using UnityEngine;
    3. using UnityEditor;
    4. using UnityEditor.Animations;
    5.  
    6. public class GORecorderTest : MonoBehaviour
    7. {
    8.     // Need this field because I can't get AnimationWindowReflection.activeAnimationClip to work
    9.     public AnimationClip SrcClip;
    10.     public IKAnimValues PropertiesComp;
    11.  
    12.     public void RecordIKAnimClip()
    13.     {
    14.         if (!AnimationMode.InAnimationMode())
    15.             throw new ArgumentException("TEST_AnimationMode must be active during bake operation");
    16.  
    17.         var animator = GetComponent<Animator>();
    18.         var recorder = new GameObjectRecorder(animator.gameObject);
    19.  
    20.         // Get and bind properties
    21.         EditorCurveBinding[] bindings = PropertiesComp.GetBindings();
    22.         foreach (var binding in bindings)
    23.             recorder.Bind(binding);
    24.  
    25.         // Recording Process
    26.         var frameCount = (int)(SrcClip.length * SrcClip.frameRate);
    27.         float dt = 1f / SrcClip.frameRate;
    28.         //float time = 0f;
    29.  
    30.         //graph?.Evaluate(0f);
    31.         recorder.TakeSnapshot(0f);
    32.  
    33.         for (int frame = 1; frame <= frameCount; ++frame)
    34.         {
    35.             //time = frame / clip.frameRate;
    36.             //graph?.Evaluate(time);
    37.             recorder.TakeSnapshot(dt);
    38.         }
    39.  
    40.  
    41.         // Save recorded animation in same folder as source animation
    42.         var ikAnimClip = new AnimationClip();
    43.         recorder.SaveToClip(ikAnimClip, SrcClip.frameRate);
    44.  
    45.         string[] pathFragments = AssetDatabase.GetAssetPath(SrcClip).Split('/');
    46.         string path = "";
    47.         for (int i = 0; i < pathFragments.Length - 1; i++)
    48.             path += pathFragments[i] + "/";
    49.         path += "IKAnimClip.anim";
    50.  
    51.         AssetDatabase.CreateAsset(ikAnimClip, path);
    52.     }
    53. }
    54.  
    and this is the IKAnimValues component:
    Code (CSharp):
    1. using UnityEngine;
    2. using UnityEngine.Animations.Rigging;
    3. using UnityEditor;
    4.  
    5. public class IKAnimValues : MonoBehaviour
    6. {
    7.     public Vector3 RootPos;
    8.     public Quaternion RootRot;
    9.     [Space]
    10.     public Vector3 LegLDir;
    11.     public float LegLScalar;
    12.  
    13.     public EditorCurveBinding[] GetBindings()
    14.     {
    15.         EditorCurveBinding[] bindings = new EditorCurveBinding[11];
    16.  
    17.         bindings[0] = EditorCurveBinding.FloatCurve("", typeof(IKAnimValues), "RootPos.x");
    18.         bindings[1] = EditorCurveBinding.FloatCurve("", typeof(IKAnimValues), "RootPos.y");
    19.         bindings[2] = EditorCurveBinding.FloatCurve("", typeof(IKAnimValues), "RootPos.z");
    20.  
    21.         bindings[3] = EditorCurveBinding.FloatCurve("", typeof(IKAnimValues), "RootRot.x");
    22.         bindings[4] = EditorCurveBinding.FloatCurve("", typeof(IKAnimValues), "RootRot.y");
    23.         bindings[5] = EditorCurveBinding.FloatCurve("", typeof(IKAnimValues), "RootRot.z");
    24.         bindings[6] = EditorCurveBinding.FloatCurve("", typeof(IKAnimValues), "RootRot.w");
    25.  
    26.         bindings[7] = EditorCurveBinding.FloatCurve("", typeof(IKAnimValues), "LegLDir.x");
    27.         bindings[8] = EditorCurveBinding.FloatCurve("", typeof(IKAnimValues), "LegLDir.y");
    28.         bindings[9] = EditorCurveBinding.FloatCurve("", typeof(IKAnimValues), "LegLDir.z");
    29.  
    30.         bindings[10] = EditorCurveBinding.FloatCurve("", typeof(IKAnimValues), "LegLScalar");
    31.  
    32.         return bindings;
    33.     }
    34. }
    Again, thank you!
     
  4. McKnuckle

    McKnuckle

    Joined:
    May 18, 2018
    Posts:
    3
    Hello again,
    I just wanted to post an update on my project.
    Using an InverseConstraint worked out great! It was a bit hard to get everything working though.
    My main problem was that I didn't know how to handle and bind my needed properties correctly (Vector3Property, Vector4Property, FloatProperty), but looking at the example JiggleConstraint from this Animation Rigging talk has helped me a lot. (link to the JiggleConstraint code)
    Thanks again!