Fade out a PlayableDirector so an Animator can take over.

I have a timeline that has some animation tracks that are driving objects in the scene. My animator also has an idle animation that takes over whenever the timeline is not playing, which works great! However, this is my problem. My timeline is in a looping state, and I want to 'fade out' the timeline and let the idle animation of the animator take over.

It seems that that the timeline is perfectly happy to blend with the animator in other cases, since it blends just fine when the clips themselves have eases in/out. But I can't figure out how to blend the timeline as a whole. I've tried modifying the playableGraph itself, setting various weights to zero, but so far nothing has had any effect. Is there some specific recipe I am missing?

1 Like

This is difficult to do because timeline is aggressively setting the output weight of it's tracks. One, less than ideal solution is to have a non-looping duplicate timeline that fades out. When you want to fade out stop the original and start the faded one.

2 Likes

Hmm but that wouldn't work because the timeline that is looping is fairly long. When I would switch to the duplicate timeline, it could happen at any time during the loop. So at what time would I place the fade out? If I place it at the end and the transition happens right at the start, I have to wait for the entire duration of the timeline before the fade out occurs.

1 Like

Here's a sample I think does what you are looking for. If you don't use override tracks, or have any holes without extrapolation it should work just fine. It redirects the track to a new playable output that won't have it's weight overwritten.

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Animations;
using UnityEngine.Playables;

[RequireComponent(typeof(PlayableDirector))]
public class FadeOutTrack : MonoBehaviour
{
    public bool startFade = false;
    public float fadeTime = 2;

    private PlayableDirector playableDirector;
    private AnimationPlayableOutput output;

    // Assume playOnAwake on the playabledirector
    void OnEnable ()
    {
        playableDirector = GetComponent<PlayableDirector>();

        if (playableDirector.playableGraph.IsValid())
        {
            // assumes the first output is the one we want to fade
            var oldOutput = (AnimationPlayableOutput) playableDirector.playableGraph.GetOutputByType<AnimationPlayableOutput>(0);
            if (oldOutput.IsOutputValid() && oldOutput.GetTarget() != null)
            {
                // create a new output to replace the existing
                output = AnimationPlayableOutput.Create(playableDirector.playableGraph, "fake", oldOutput.GetTarget());
                output.SetSourcePlayable(oldOutput.GetSourcePlayable());
                output.SetSourceInputPort(oldOutput.GetSourceInputPort());
                output.SetWeight(1.0f);
                oldOutput.SetTarget(null);
            }
        }

    }

    // Update is called once per frame
    void Update () {
        if (startFade)
        {
            startFade = false;
            if (output.IsOutputValid())
                StartCoroutine(FadeOut());
        }
    }

    IEnumerator FadeOut()
    {
        float t = 0;
        while (t < fadeTime)
        {
            float weight = 1 - Mathf.Clamp01(t / fadeTime);
            output.SetWeight(weight);
            yield return null;
            t += Time.deltaTime;
        }
        playableDirector.Stop();
    }
}
3 Likes

Ah very interesting, thanks for the code sample! Will play around with this....

1 Like

Wait, so.. The Animator creates its own playable graph with an AnimatorControllerPlayable somewhere in there, outputing animation data. The PlayableDirector also creates its own playable graph (say, given a TimelineAsset). Even though they're 2 separate graphs, the animation data is able to blend between them both?

Or how does that work? Cause I thought 2 separate AnimationPlayableOutputs wouldn't be able to blend with each other (especially being part of 2 separate graphs)

1 Like

The animator keeps a stack of AnimationPlayableOutputs and blends them based on the weight of the output. The animator controller (state machine) also generates an AnimationPlayableOutput, and it's typically treated as the base. That's how timeline blends to / from a state machine.

The code block above redirects the track's subgraphs to a different output. Timeline updates the weight on the old one (which no longer has a target), and the new one uses the same subgraph but with whatever weight you provide.

4 Likes

duplicated this comment from another relevant post in case people here need the answer:

Thanks a lot. I have been making some really good advancements with my use of timeline and playables. Manged to crack custom timeline assets + tracks for all kinds of useful things. Just started to understand the base playable system (for playable graphs, not just timeline stuff). I now am at the point where I believe i know enough to make a giant playable graph system that can act as a big animator controller style system with blending (for example to blend between clips for idle and running).

The last bit of knowledge is how to get these "states" to blend to other states. I do believe with what you posted this will be more doable now :)

So some questions on that "workaround" (solution imo):

  • So it essentially is reading two timeline assets, getting the track that is similar and then syncing up (blending) their weights from the weight at output of one, to weight of start of another?
  • Could this be used to fade a multi track timeline to another multi track timeline (same tracks in same order but potentially different bindings)?
1 Like

@seant_unity can you explain how you would blend from one timeline being in control to another, rather than from timeline to animator in control?

1 Like

The concept would be the same as the script above, and would only work for Animation Tracks. Audio and script tracks wouldn't cross fade, at least not by using a weight on their outputs.

You would need to use the workaround once the timeline is created (either with Play(), or RebuildGraph()) to replace the AnimationPlayableOutputs with outputs that can have the weight set.

When you want to transition from one timeline to another, start fading out the old timeline by changing the weights of the newly created AnimationPlayableOutputs, while fading in the outputs on the new timeline.

The animator will stack and blend the outputs so when the new timeline starts, any tracks that are cross fading should blend from one timeline to another.

Is that what you were looking for?

2 Likes

[quote=“seant_unity”, post:10, topic: 689294]
The concept would be the same as the script above, and would only work for Animation Tracks. Audio and script tracks wouldn’t cross fade, at least not by using a weight on their outputs.

You would need to use the workaround once the timeline is created (either with Play(), or RebuildGraph()) to replace the AnimationPlayableOutputs with outputs that can have the weight set.

When you want to transition from one timeline to another, start fading out the old timeline by changing the weights of the newly created AnimationPlayableOutputs, while fading in the outputs on the new timeline.

The animator will stack and blend the outputs so when the new timeline starts, any tracks that are cross fading should blend from one timeline to another.

Is that what you were looking for?
[/quote]
Thats great thanks! :slight_smile:

1 Like

I ended up solving a similar situation with the SimpleAnimation component and adding in a custom output weight for that. While that works, I would really like to understand why it does and a bit more about the underlying concept -
from this thread, my assumption is:

  • the Animator in my case has three different AnimationPlayableOutputs on its "stack"

  • its own AnimationController

  • the Timeline

  • the SimpleAnimation component (which also creates a graph and outputs directly to it)

  • for some magical reason, the above order is always guaranteed - the SimpleAnimation seems to be always "on top". Why is that? How would I change it?

  • another magical thing is that AnimationPlayableOutput stack. While I understand it's there, I don't see a way to access / view it - the PlayableGraph Visualizer doesn't show it at all, it just shows the separate Graphs. How can I access that stack, and modify it if needed?

  • Conceptually, would it be possible to have two graphs generated from the same Animator component - one "below" the Timeline, and one "above" (on the AnimationPlayableOutput stack)?

@seant_unity let me know if this should be a separate thread - it does feel very much related though.

EDIT: So the order is not guaranteed. Seems it's based on which one was added last - leading to weird situations when a Timeline loops, because that seems to cause the Graph to be recreated and thus changes the stack order. Even more important to get an answer now, as I can't control it reliably!

1 Like

From my digging, I don't think there's a way to access or control how an Animator blends the outputs targeting it.

Our case isn't identical, but what we are doing instead is iterating over the AnimationPlayableOutputs of a graph and finding all the ones targeting the same animator.

if ( boundAnimator != null ) {
    int animOutputCount = playableGraph.GetOutputCountByType<AnimationPlayableOutput>();
    for ( int i = 0; i < animOutputCount; i++ ) {
        AnimationPlayableOutput animOutput = (AnimationPlayableOutput)graph.GetOutputByType<AnimationPlayableOutput>(i);
        if ( animOutput.GetTarget() == boundAnimator ) {
            // Store a reference to animOutput here
        }
    }
}

Later in our custom mixer (though it doesn't have to be) we were able to override the weights of the saved PlayableOutputs to have more manual control over which outputs were actually influencing the targeted Animator. It seems like as long as these weights are changed after PrepareFrame step of any Animator Tracks (potentially the Prepare Frame of a custom track lower down in a Timeline) and before Unity's Internal Animation Update, the manually set weights will be used by the Animator. You could also disconnect some of these outputs if that makes more sense in your case.

The above code was iterating the playableGraph of a Timeline in a custom track, but a very similar approach can be used to iterate the PlayableOutputs of the Animator.playbleGraph, the difference being that you already know the GetTarget() Animator in this case.

As far as I know, (correct me if I'm wrong!) the legacy animation stuff should always be "on top" and there's no PlayableOutput or weighting to it.

1 Like

So, the order the outputs are processed is based on the last one run (as noted in the edit above), except that the animator controller always is first.

And yes - the animation tracks modify the weight of the outputs each frame (it promotes < 1 weights so tracks will blend), so if you are going to do that manually, it needs to be before the internal animation update.

1 Like

Just running into a weird situation here (a bug?). Based on the above (order of processing is the last one run) I was able to make a custom RuntimeAnimatorController graph always the top one (above Animator and Timeline). However, when modifying the Timeline time from script, the order seems to be different! Whenever PlayableDirector.time is accessed, the output seems to be unpredictable.... @seant_unity any idea? Is that a known behaviour?

To the best of my knowledge, the way an Animator blends the AnimationPlayableOutputs targeting it has some quirks. Some things to consider and check when trying to do more complicated Timeline/Animator stuff:

  1. As mentioned, order matters. Given two outputs weighted at 1, the last evaluated output will overwrite. By default these outputs are evaluated in the order they are bound, (Timeline track outputs top to bottom) with the Animator assigned AnimationController.playableGraph being first.

  2. An AnimationOutput belonging to a Timeline that's Paused OR is set to Manual update method will not be evaluated (or blended) by the Animator. Evaluate has to be manually called after the internal animation update (aka. in Late Update) if you want it to overwrite stuff. (I strongly suspect this is not a performant thing to do)

  3. The weights on the AnimationOutputs also matter. The AnimationOutput of an Animation Track on a Timeline will be automatically weighted to zero when no animations are playing. This allows a timeline track to not overwrite the default Animation Controller (and even blend between them). Clip extrapolation of any type counts as having an animation that is playing.
    3b. This automatic output weight adjustment behaves differently outside play mode. In edit/preview mode, the wight of the track's AnimationOutput will always stay at 1.0. Blank spaces on the track are filled with an injected "HumanoidDefault" t-pose and blending is completely internal to the Timeline graph. This prevents previewing blends between multiple animation tracks (potentially on multiple timelines) targeting the same animator.

  4. An AnimationOutput containing a clip that only animates the Transform and/or Rotation of an object will have it's root motion output additively blended (won't overwrite) previous animation outputs not containing root motion. This blending behavior is dependent on the contents of the animation clips linked to the output. As soon as a clip containing non-motion keys is added to the graph, the blending behavior will change and the outputs will go back to "normal" blending.
    4b. This additive blending can impact the way script-driven transformations are applied and blended with animation track output.

2 Likes

[quote=“fherbst”, post:15, topic: 689294]
Just running into a weird situation here (a bug?). Based on the above (order of processing is the last one run) I was able to make a custom RuntimeAnimatorController graph always the top one (above Animator and Timeline). However, when modifying the Timeline time from script, the order seems to be different! Whenever PlayableDirector.time is accessed, the output seems to be unpredictable… @seant_unity any idea? Is that a known behaviour?
[/quote]

@zander_m 's list seems pretty spot on. As for accessing .time from script, that shouldn’t affect anything unless you are calling Evaluate() manually, which does an immediate evaluation. Changing anything that causes the underlying graph to rebuilt (the Timeline editor does this somewhat frequently) can cause the order to change, since it will remove an old output an put on a new one.

1 Like

I guess I need more information regarding point (4). I'm seeing a ton of weird behaviours regarding Root Motion going crazy (oscillating / offset / scaled up 100x times) if time is controlled through anything but "Game Time" or even then when PlayableDirector.time is touched. I'll try to reproduce it in a simpler example and report a bug.

Does the Timeline Graph rebuilding have a proper event to attach to? There seems to only be paused, played, and stopped.

1 Like

No it doesn't. Clips and track behaviours will receive it indirectly through OnPlayableDestroy/OnPlayableCreate and OnGraphStop/OnGraphStart callbacks being triggered though.

1 Like

[quote=“seant_unity”, post:4, topic: 689294]
Here’s a sample I think does what you are looking for. If you don’t use override tracks, or have any holes without extrapolation it should work just fine. It redirects the track to a new playable output that won’t have it’s weight overwritten.

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Animations;
using UnityEngine.Playables;

[RequireComponent(typeof(PlayableDirector))]
public class FadeOutTrack : MonoBehaviour
{
    public bool startFade = false;
    public float fadeTime = 2;

    private PlayableDirector playableDirector;
    private AnimationPlayableOutput output;

    // Assume playOnAwake on the playabledirector
    void OnEnable ()
    {
        playableDirector = GetComponent<PlayableDirector>();

        if (playableDirector.playableGraph.IsValid())
        {
            // assumes the first output is the one we want to fade
            var oldOutput = (AnimationPlayableOutput) playableDirector.playableGraph.GetOutputByType<AnimationPlayableOutput>(0);
            if (oldOutput.IsOutputValid() && oldOutput.GetTarget() != null)
            {
                // create a new output to replace the existing
                output = AnimationPlayableOutput.Create(playableDirector.playableGraph, "fake", oldOutput.GetTarget());
                output.SetSourcePlayable(oldOutput.GetSourcePlayable());
                output.SetSourceInputPort(oldOutput.GetSourceInputPort());
                output.SetWeight(1.0f);
                oldOutput.SetTarget(null);
            }
        }

    }

    // Update is called once per frame
    void Update () {
        if (startFade)
        {
            startFade = false;
            if (output.IsOutputValid())
                StartCoroutine(FadeOut());
        }
    }

    IEnumerator FadeOut()
    {
        float t = 0;
        while (t < fadeTime)
        {
            float weight = 1 - Mathf.Clamp01(t / fadeTime);
            output.SetWeight(weight);
            yield return null;
            t += Time.deltaTime;
        }
        playableDirector.Stop();
    }
}

[/quote]

In 2019.3, It seems to get error at line 29 and 30.
Error is
“Cannot set multiple PlayableOutputs to the same source playable and output port”

However, If using this way for dynamic blending animation with same Animator,
I needs some tracks that have same bindings. It’s so redundancy.
If possible, I want something more smart way for blending weights of tracks.

Is there a way to change weights of override tracks in runtime?