Search Unity

PlayableGraph / AnimationScriptPlayable questions

Discussion in 'Animation' started by 3DI70R, Nov 1, 2018.

  1. 3DI70R

    3DI70R

    Joined:
    Nov 12, 2014
    Posts:
    2
    Hello guys.

    I have a few questions about PlayableGraph and other things.

    1) Is there any good example, how to correctly implement custom MixerPlayable, that can mix other mixers?
    Implementation in current sample mixers (Default Playables) is very primitive, and incorrect. By that, i mean, that Mixer evaluates inputs and applies result to object by himself. This leads to inability to chain multiple mixers together, for more complex mixing behaviors.
    In my case, i want to create playable that controls Inverse Kinematics data. I have such hierarchy:
    • AnimationIkPlayable - Holds information about IK configuration (weights, positions, rotations, etc)
    • AnimationIkMixerPlayable - Extends AnimationIkPlayable, mixes all inputs and saves result in own instance (since it is derived from AnmationIkPlayable), so other mixers can read data from it
    • AnimationIkOutputPlayable - Gets IK information from attached input, and applies it to animation stream
    And it almost works, but graph update order is not what i expecting. For example, if i attach them like this: Mixer1 -> Mixer2 -> Node they will be updated in this exact order, which induces X frames delay for each action (Mixer1 reads data from Mixer2, but Mixer2 is not yet updated and contains data from previous frame). I expect that each input for each mixer is updated before mixer update (thus creating reverse update order Node -> Mixer1 -> Mixer2). AnimationJob seems to have such behavior, so i wondering, if normal playables can be configured for it too
    And that leaves me thinking - am i doing this all correctly but just missing a few flags here and there, or my implementation is completely wrong.

    2) Can AnimationScriptPlayable additionaly have custom PlayableBehavior?
    Currently, you can modify animation stream in AnimationJob, but you cannot react to Playable events (like PrepareFrame) and should update AnimationJob somewhere in Update() method. In my case, i'd wanted to evaluate inputs, and then update AnimationJob with new parameters. My current setup is awful, but it works. Basically, AnimationIkOutputPlayable references AnimationJob and when output updates, it sets new parameters for AnimationJob. Its very unnatural, since graph output changes one of the graph nodes. Is there are better solution for this?

    Here is my messy and experimental code, if someone interested.
    https://github.com/3DI70R/CharacterAnimator
     
    Last edited: Nov 1, 2018
    ModLunar likes this.
  2. 3DI70R

    3DI70R

    Joined:
    Nov 12, 2014
    Posts:
    2
    Bump
    Here is test code that demonstrates issue that i have in first question.

    Code (CSharp):
    1.  
    2. using UnityEngine;
    3. using UnityEngine.Playables;
    4.  
    5. public class GraphTest : MonoBehaviour
    6. {
    7.     public class TestBehavior : PlayableBehaviour
    8.     {
    9.         public string name;
    10.         public int value;
    11.  
    12.         public override void ProcessFrame(Playable playable, FrameData info,
    13.             object playerData)
    14.         {
    15.             base.ProcessFrame(playable, info, playerData);
    16.  
    17.             if (playable.GetInputCount() > 0)
    18.             {
    19.                 // just copy value from input for demonstration purposes
    20.                 value = ((ScriptPlayable<TestBehavior>) playable.GetInput(0))
    21.                     .GetBehaviour().value;
    22.             }
    23.         }
    24.     }
    25.    
    26.     private PlayableGraph graph;
    27.     private ScriptPlayable<TestBehavior> firstPlayable;
    28.     private ScriptPlayable<TestBehavior> lastPlayable;
    29.     private int counter;
    30.    
    31.     private void Start()
    32.     {
    33.         graph = PlayableGraph.Create("Test Graph");
    34.         var o = ScriptPlayableOutput.Create(graph, "output");
    35.        
    36.         firstPlayable = ScriptPlayable<TestBehavior>.Create(graph,
    37.             new TestBehavior { name = "Node 1" });
    38.         var p2 = ScriptPlayable<TestBehavior>.Create(graph,
    39.             new TestBehavior { name = "Node 2" });
    40.         var p3 = ScriptPlayable<TestBehavior>.Create(graph,
    41.             new TestBehavior { name = "Node 3" });
    42.         var p4 = ScriptPlayable<TestBehavior>.Create(graph,
    43.             new TestBehavior { name = "Node 4" });
    44.         var p5 = ScriptPlayable<TestBehavior>.Create(graph,
    45.             new TestBehavior { name = "Node 5" });
    46.         var p6 = ScriptPlayable<TestBehavior>.Create(graph,
    47.             new TestBehavior { name = "Node 6" });
    48.         var p7 = ScriptPlayable<TestBehavior>.Create(graph,
    49.             new TestBehavior { name = "Node 7" });
    50.         lastPlayable = ScriptPlayable<TestBehavior>.Create(graph,
    51.             new TestBehavior { name = "Source" });
    52.  
    53.         firstPlayable.AddInput(p2, 0, 1f);
    54.         p2.AddInput(p3, 0, 1f);
    55.         p3.AddInput(p4, 0, 1f);
    56.         p4.AddInput(p5, 0, 1f);
    57.         p5.AddInput(p6, 0, 1f);
    58.         p6.AddInput(p7, 0, 1f);
    59.         p7.AddInput(lastPlayable, 0, 1f);
    60.         o.SetSourcePlayable(firstPlayable);
    61.        
    62.         graph.Play();
    63.     }
    64.  
    65.     private void Update()
    66.     {
    67.         lastPlayable.GetBehaviour().value = counter;
    68.         counter++;
    69.     }
    70.  
    71.     private void LateUpdate()
    72.     {
    73.         Debug.Log("Last playable: " + lastPlayable.GetBehaviour().value + ", " +
    74.                   "First Playable: " + firstPlayable.GetBehaviour().value);
    75.     }
    76.  
    77.     private void OnDestroy()
    78.     {
    79.         graph.Destroy();
    80.     }
    81. }
    82.  
    83.  
    I've also modified Playable Graph Visualizer to display actual values as node titles, and here is gif with code above running.

    NodeDelay.gif
     
    ModLunar likes this.
  3. ModLunar

    ModLunar

    Joined:
    Oct 16, 2016
    Posts:
    374
    Interesting.. not a full answer to all your questions, but I haven't encountered issues with AnimationMixerPlayables so far under my usage.

    However, I HAVE confirmed that you can connect a custom ScriptPlayable<T> between an AnimationMixerPlayable and an AnimationScriptPlayable, and everything will work.
    You can also make your ScriptPlayable<T> an input to your AnimationScriptPlayable, and everything will work as well.

    By "everything will work", I've tested that it:
    1. Still writes out the same animation data
    2. Still mixes properly with other animation playables (tested with an AnimatorControllerPlayable as input 0 into the mixer, and AnimationScriptPlayable as input 1 into the mixer)
    3. Your custom PlayableBehaviour class (T above) will have all its callbacks get called.

    This does NOT mean that you should start trying to write data from your ScriptPlayable<T> over your animation data, as it probably won't mix, and won't be handled in Unity's internal multi-threaded animation update,
    BUT it is helpful for getting the playable callbacks AND writing animation data at the same time!

    This is a screenshot of the PlayableGraph I was testing with. I put Debug.Log(...) calls in my ExamplePlayable (ScriptPlayable<ExamplePlayable>):

    upload_2023-3-9_0-29-55.png
     
    mandisaw and SolarianZ like this.
  4. SolarianZ

    SolarianZ

    Joined:
    Jun 13, 2017
    Posts:
    237
    Here is some of my experience using Playable to develop new animation controllers. I hope it can be helpful to you!

    All Playables in the PlayableGraph have two methods:
    • PrepareFrame
    • ProcessFrame
    On each frame of the game, the PlayableGraph will be traversed twice:
    • The first traversal starts with the root node (the node connected to PlayableOutput) and traverses in a pre-order manner, calling the
      PrepareFrame
      method.
    • The second traversal traverses in a post-order manner and calls the
      ProcessFrame
      method.
    It is okay to connect ScriptPlayable (as well as any other Playable) to the animation Playable tree. Therefore, you can input AnimationScriptPlayable into a custom ScriptPlayable node and complete the state update logic (such as updating input weights and replacing inputs) in the
    PrepareFrame
    method. PlayableGraph ensures that on each frame, the
    PrepareFrame
    method of the parent node is executed first, and the
    ProcessFrame
    method of the child node is executed first. However, one thing to note is that in this case, the
    PrepareFrame
    method of ScriptPlayable can be executed normally, but its
    ProcessFrame
    method will not be executed. Only the
    ProcessFrame
    method of ScriptPlayable connected to ScriptPlayableOutput will be executed.

    In short, only handle pose and motion in AnimationScriptPlayable and put all other logic in the corresponding ScriptPlayable.

    The
    ProcessRootMotion
    and
    ProcessAnimation
    methods of AnimationScriptPlayable are executed during the
    ProcessFrame
    period, and its
    PrepareFrame
    method is not open to us. If I remember correctly, setting the input weight of AnimationScriptPlayable does not affect data from the subtree. We need to manually make the weight effective through
    AnimationStream
    .

    There are two issues that can easily cause animation abnormalities:
    • The
      Playable.SetTime
      method can jump the animation progress, but it also affects the RootMotion of the animation, causing the character to teleport. For example, if you set a Walk animation that has been played for 10 seconds to 3 seconds, the character's motion from 3 to 10 seconds will also be rewound, and your character will be suddenly pulled back.
    • In the
      AnimationScriptPlayable.ProcessRootMotion
      method, we can modify
      AnimationStream.velocity
      to achieve position correction, but the final
      Animator.velocity
      (which can be seen in the
      OnAnimatorMove
      method) seems not entirely from
      AnimationStream.velocity
      .
    These two issues seriously hindered my work, but I have not found a suitable solution yet.:(

    (In this graph, I didn't place the ScriptPlayable before the AnimationPlayable, but instead created two trees with the same structure, which have the same effect.)
    upload_2023-3-13_15-38-7.png
     
    mandisaw and ModLunar like this.
  5. ModLunar

    ModLunar

    Joined:
    Oct 16, 2016
    Posts:
    374
    Wow!

    Thanks for all the insight @SolarianZ !
    This makes a lot of sense, and unfortunately I don't have much direct experience with controlling the root motion through AnimationScriptPlayables currently, but if I find anything out sometime, I'll certainly share my findings too :)

    Also your PlayableGraphMonitor editor window looks amazing, great work!
     
    SolarianZ likes this.
  6. SolarianZ

    SolarianZ

    Joined:
    Jun 13, 2017
    Posts:
    237