In short: I am trying to scrub/lerp an animation, it works but sometimes it lerps the reverse way.

In long: My character has an aim animation, 10 frames long, where he aims from down (-90 degrees) to up (90 degrees). And during the animation he also changes draw order, and some other stuff. The point is, I want to be able to set a value between 0 and 1, to determine where the animation should be. My code works so far, but it has a bug where it sometimes lerps the animation the opposite way. Meaning he aims up when he should aim down. Even though nothing in my debug seems to be the issue: the aim angle is correct, we are on the correct animation, seems to be the correct duration etc. Its's like the animation started reversed, or its trying to mix between the last animations pose (up) and the new animations pose (down), so it becomes up-down, instead of down-up. I don't know. Am I missing some line of code? I've tried skeleton.SetToSetupPose(); but doesn't seem to help.

private void Update()
{
            
    if (someConditions)
    {
        SetAimAnimation(animation01);
    }
    else
    {
        SetAimAnimation(animation02); 
    }
}

// Will lerp an animation from start to finish depending on our current aim angle, ranging from -90 (down) to 90 (up).
private void SetAimAnimation(AnimationReferenceAsset animation, float currentAngle, float downAngle = -90, float upAngle = 90) 
{
     // even though I brute force, e.g. clear the track and set every frame it will sometimes get revered
    //animationState.ClearTrack(aimLayer);
    //var trackEntry = animationState.SetAnimation(aimLayer, animation, false);
 // will only play the animation if its new, otherwise returns the current track entry
     var trackEntry = SetAnimation(aimLayer, animation);
    
    trackEntry.TimeScale = 0;
    // Calculate the animation time based on the progress
    var animationProgress = Mathf.InverseLerp(downAngle, upAngle, currentAngle);
    var animationDuration = trackEntry.Animation.Duration;
    var animationTime = animationDuration * animationProgress;
    // Scrub the animation to the desired time
    trackEntry.TrackTime = animationTime;
    
    Debug.Log($"Animation Progress: {animationProgress}, Animation Duration: {animationDuration}, Animation Time: {animationTime}");
    Debug.Log($"Track Entry: {trackEntry}. Track Time: {trackEntry.TrackTime}, Animation Name: {trackEntry.Animation.Name}, Animation Loop: {trackEntry.Loop}");
    
    // Update the skeleton to reflect changes
    skeletonAnimation.AnimationState.Apply(skeletonAnimation.Skeleton);
}

/// Will only play the animation if it's new, won't play animation if it's the same
private TrackEntry SetAnimation(int trackIndex, AnimationReferenceAsset animation, bool loop = false)
{
    var animationName = animation.Animation.Name;
    var currentTrackEntry = animationState.GetCurrent(trackIndex);
    var currentAnimation = currentTrackEntry?.Animation;
    
    if (currentAnimation == null || currentAnimation.Name != animationName)
        return animationState.SetAnimation(trackIndex, animation, loop);

    return currentTrackEntry;
}
Related Discussions
...

The issue you're experiencing with the animation sometimes lerping in the reverse direction is likely due to the way rotations are being handled during the mix. When mixing between two rotations, there are two possible directions around the circle to compute the halfway point. Spine's AnimationState chooses the shortest direction when the mixing begins, which can sometimes result in unexpected behavior if the initial direction is not what you intended.

To address this, you can use the TrackEntry.resetRotationDirections method to reset the rotation directions when setting the animation. This ensures that the rotation direction is recalculated based on the current state, which can help prevent the reverse lerping issue.

Here's how you can modify your SetAimAnimation method to include the resetRotationDirections call:

private void SetAimAnimation(AnimationReferenceAsset animation, float currentAngle, float downAngle = -90, float upAngle = 90) 
{
    var trackEntry = SetAnimation(aimLayer, animation);
    
    trackEntry.TimeScale = 0;
    var animationProgress = Mathf.InverseLerp(downAngle, upAngle, currentAngle);
    var animationDuration = trackEntry.Animation.Duration;
    var animationTime = animationDuration * animationProgress;
    trackEntry.TrackTime = animationTime;
    
    // Reset rotation directions to ensure correct lerping
    trackEntry.ResetRotationDirections();
    
    Debug.Log($"Animation Progress: {animationProgress}, Animation Duration: {animationDuration}, Animation Time: {animationTime}");
    Debug.Log($"Track Entry: {trackEntry}. Track Time: {trackEntry.TrackTime}, Animation Name: {trackEntry.Animation.Name}, Animation Loop: {trackEntry.Loop}");
    
    skeletonAnimation.AnimationState.Apply(skeletonAnimation.Skeleton);
}

Additionally, ensure that you are not inadvertently setting the animation multiple times within the same frame, as this can also cause unexpected behavior. You can add debug logs to verify that SetAnimation is not being called more than once per frame.

If the issue persists, consider using an intermediary animation to ensure the bones rotate in the desired direction, or avoid mixing between the problematic animations altogether.

    Spinebot
    I tried this and it doesn't work.
    Just thoughts: It shouldn't matter because we are scrubbing an already set animation, and not lerping bones or anything like that? The problem is sometimes it looks like the animation was started reversed, (e.g. character is looking up when he should look down and vice versa), even though the angle and all other values are correct and nothing seems to be different from when it actually works.

    What seems to help, is to call animationState.ClearTrack(aimLayer); whenever an animation is changed on that track. But it doesn't feel like a real solution? Works for now

    @PeterBacall Sorry to hear you're having troubles.

    I assume that you have verified that the if-branch below is really entered only once after the animation is already set, and not multiple times:

    if (currentAnimation == null || currentAnimation.Name != animationName)
            return animationState.SetAnimation(trackIndex, animation, loop);

    What seems to be missing in your code is since you're calling AnimationState.Apply manually, I see no call to either SkeletonAnimation.AfterAnimationApplied() or manually to Skeleton.UpdateWorldTransform(Skeleton.Physics.Update);

      Harald

      That explains a lot, I actually Frankensteined together parts of the "scrubbing system" from some old forum post and AI (yeah I know...), I removed skeletonAnimation.AnimationState.Apply(skeletonAnimation.Skeleton); because it just made things worse (firing multiple events when only one should be fired etc). Should I add it back with mentioned methods (UpdateWorldTransform etc). Or just leave it be? Is it actually needed here? No longer having issues since I removed it. But I am calling a lot of "ClearTrack(aimLayer)" at the end of some states to make stuff work expectedly.

      Sorry if it's obvious questions, just started with SkeletonAnimation, been using SkeletonMecanim for years and figured its time to move away from that toxic relationship (Unitys Animator is pretty bad).
      Edit:

      Harald SkeletonAnimation.AfterAnimationApplied()

      Where can I find more documentation on this and similar stuff

      • Harald ha risposto a questo messaggio

        PeterBacall Should I add it back with mentioned methods (UpdateWorldTransform etc). Or just leave it be? Is it actually needed here? No longer having issues since I removed it. But I am calling a lot of "ClearTrack(aimLayer)" at the end of some states to make stuff work expectedly.

        Calling ClearTrack every frame and adding the animation at the desired time is a rather harsh workaround. Personally I would recommend using AnimationState.Apply() and Skeleton.UpdateWorldTransform(Skeleton.Physics.Update), or even more recommended simply call SkeletonAnimation.ApplyAnimation() which does this for you.

        PeterBacall Where can I find more documentation on this and similar stuff

        Currently you can find it here:
        http://esotericsoftware.com/spine-runtime-skeletons#Runtime-Skeletons
        You are right that this should be mentioned in some of the spine-unity documentation sections. We will later update the documentation accordingly, sorry for the troubles!

        12 giorni dopo

        @PeterBacall We have now updated the documentation pages accordingly, you can find two new sections here on the topic:
        https://esotericsoftware.com/spine-unity-main-components#Script-Execution-Order
        https://esotericsoftware.com/spine-unity-main-components#Manual-Updates
        We hope this helps, please let us know in case you're missing anything important or anything remains unclear.