Reset animation track scene offset, without rebuilding graph

So I have a timeline, using an animation track with “Apply Scene Offsets” mode.

Because I don’t want to destroy and rebuild the graph every time I run the timeline (for performance reasons), I set the director wrap mode to “Hold” and pause it when it reaches the end of the sequence.
And when I want to re-run the timeline, I just set the director time to zero and play.

First time I run the timeline, it works as expected, with the animated object starting the animation at its current position in the scene.
The problem happens when I re-run the timeline. Instead of using the object’s current position, it uses the position of the object at the time of the first run.

I’m aware that if I stop the director and then play, it will not cause the issue. But as I mentioned I don’t want to destroy and rebuild the graph.

So is there any API I can use to reset the scene offset in this scenario?. And if not, is there any workaround through reflection or modification to the timeline package source code?

1 Like

There is no way to do it with the public API, but it can be done via reflection. The offset is applied using the animation playable AnimationOffsetPlayable which is an internal class in UnityEngine.Animations. It contains SetPosition & SetRotation methods calls that can be called without rebuilding the graph.

Here’s a quick example to get you started. It uses a private static versions of the SetPosition method to make the reflection a bit easier.

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

[RequireComponent(typeof(PlayableDirector))]
public class UpdateSceneOffset : MonoBehaviour
{
    public Vector3 positionOffset;
    public Animator animator;

    void Start()
    {
        var director = GetComponent<PlayableDirector>();
        if (!director.playableGraph.IsValid())
            director.RebuildGraph();

        var offsetType = typeof(AnimationLayerMixerPlayable).Assembly.GetType("UnityEngine.Animations.AnimationOffsetPlayable");

        for (int i = 0; i < director.playableGraph.GetOutputCountByType<AnimationPlayableOutput>(); i++)
        {
            var playableOutput = (AnimationPlayableOutput) director.playableGraph.GetOutputByType<AnimationPlayableOutput>(i);
            if (animator == playableOutput.GetTarget())
            {
                // get the root of the sub-graph
                var subGraphRoot = playableOutput.GetSourcePlayable().GetInput(playableOutput.GetSourceOutputPort());
                var queue = new Queue<Playable>();
                queue.Enqueue(subGraphRoot);

                // find the first node using breadth first
                var node = Playable.Null;
                while (queue.Count > 0)
                {
                    node = queue.Dequeue();
                    if (node.GetPlayableType() == offsetType)
                        break;
                    for (int j = 0; j < node.GetInputCount(); j++)
                    {
                        queue.Enqueue(node.GetInput(j));
                    }

                    node = Playable.Null;
                }

                if (node.IsValid())
                {
                    var args = new object[] {node.GetHandle(), positionOffset};
                    offsetType.GetMethod("SetPositionInternal", BindingFlags.NonPublic | BindingFlags.Static).Invoke(null, args);
                };

                break;
            }
        }
    }
}
1 Like

Thanks a lot @seant_unity for the quick and comprehensive reply

I tried your example with the “Scene Offsets” mode, but it wasn’t very clear what value I should set for the ‘positionOffset’.
I tried the animated object position and local position, but it was giving me some weird results.

But when I changed the offset mode to “Apply Transform Offsets” and set the ‘positionOffset’ to the local position, it worked perfectly (same thing for rotation also).

But then I didn’t like that the reflection solution would generate garbage because of the boxing, so I modified the “AnimationTrack” class, and added a method to do that without reflection.

For whoever is interested, here’s how I did it:-

  • In the timeline package folder, go to the file “Runtime\Animation\AnimationTrack.cs”

  • Add an AnimationOffsetPlayable field to the class “AnimationTrack”

AnimationOffsetPlayable cachedOffsetPlayable;
  • Find the method “ApplyTrackOffset”, and modify it as follows
//find this line in the method
var offsetPlayable = AnimationOffsetPlayable.Create(graph, pos, rot, 1);
//add this line after it
cachedOffsetPlayable = offsetPlayable;
  • Add the following method
public void ResetTransformOffset(PlayableDirector director) {
    if (cachedOffsetPlayable.IsValid()) {
        //get the bound animator object transform
        var animatorTransform = GetBinding(director).transform;
        cachedOffsetPlayable.SetPosition(animatorTransform.localPosition);
        cachedOffsetPlayable.SetRotation(animatorTransform.localRotation);
    }
}
  • Now in you code, whenever you want to play the timeline you can write the following (Note the track should be in “Apply Transform Offsets” mode)
AnimationTrack animationTrack = (director.playableAsset as TimelineAsset)
            .GetOutputTracks()
            .OfType<AnimationTrack>()
            .FirstOrDefault(track => track.name == "track name");//based on track name, or whatever filter you prefer
animationTrack.ResetTransformOffset(director);

Thanks again @seant_unity for pointing me in the right direction, please feel free to correct me if anything I mentioned is incorrect

Side question / request
any chance of adding such API, so I don’t have to apply my modification every time I want to update the timeline package?

1 Like

That’s a good solution! Much easier to edit the package and avoid the reflection nonsense. That’s pretty much how we are doing it in editor when modifying offsets as well.

As for the request, we’ve been investigating this issue - Unity Issue Tracker - Apply Scene Offsets is not applied when looping a Timeline and it’s similar to the issue you’ve run into. Because we are using the offsets to simulate how state machines apply root motion, looping the timeline doesn’t ‘continue’, so one solution is to update the offsets on loop.

We haven’t decided on a solution yet, but hopefully what we come up with is applicable to your case as well.

We probably wouldn’t have an AnimationTrack method, as we can’t guarantee a 1:1 relationship between tracks and instances (i.e. two playable directors can be running off the same timeline with different bindings), but an API to do this would definitely be useful.

1 Like

Still accurate now and does works with Apply Scene offset on Timeline 1.8.7 and 2021 LTS