Procedural AnimationClip loop broken when using EnsureQuaternionContinuity

I’m building an animation using AnimationCurves, and I must use EnsureQuaternionContinuity otherwise I get some undesired rotation. The issue is that this method breaks my first and last frame tangents, and since this method is on the AnimationClip, I cannot fix the tangents after the fact. Visually, this results in a hard direction change rather than a smooth transition between the first and last frame.

How can I get a procedurally generated animation that loops?

Here’s a full repro, just drop that script on a cube gameobject. When you press play, it’ll loop smoothly. If you check the Ensure Quaternion Continuity property in the inspector and play again, you’ll see the loop isn’t smooth anymore. You’ll also need an Animation on the gameobject.

I’d like to either:

  • Find a magic method that “fixes” the loop using scripting, or;
  • Find a method to replace EnsureQuaternionContinuity (e.g. implement my own version of it that modifies the AnimationCurve values directly)

Here’s the script:

using UnityEngine;

public class EnsureQuaternionContinuityLoop : MonoBehaviour
{
    public bool ensureQuaternionContinuity;
    public bool longPath;

    public void Start()
    {
        var rotX = CreateCurve(0, 0);
        var rotY = CreateCurve(0, -0.5f);
        var rotZ = CreateCurve(0, 0);
        var rotW = CreateCurve(1, longPath ? -1 : 1);

        var clip = new AnimationClip
        {
            legacy = true,
            wrapMode = WrapMode.Loop
        };
        clip.SetCurve("", typeof(Transform), "localRotation.x", rotX);
        clip.SetCurve("", typeof(Transform), "localRotation.y", rotY);
        clip.SetCurve("", typeof(Transform), "localRotation.z", rotZ);
        clip.SetCurve("", typeof(Transform), "localRotation.w", rotW);

        if (ensureQuaternionContinuity)
            clip.EnsureQuaternionContinuity();

        var animation = GetComponent<Animation>();
        animation.AddClip(clip, "Anim 1");
        animation.Play("Anim 1");
    }

    private static AnimationCurve CreateCurve(float frame1, float frame2)
    {
        var curve = new AnimationCurve();
        curve.AddKey(0, frame1);
        curve.AddKey(1, frame2);
        curve.AddKey(2, frame1);
        curve.MoveKey(0, Flat(curve[0]));
        curve.MoveKey(1, Flat(curve[1]));
        curve.MoveKey(2, Flat(curve[2]));
        return curve;
    }

    private static Keyframe Flat(Keyframe keyframe)
    {
        keyframe.inTangent = 0f;
        keyframe.outTangent = 0f;
        return keyframe;
    }
}

Thanks a lot, I did try very hard to find a workaround to no avail.

My solution was to re-write the quaternion continuity code directly on the source animation curves. This is too extensive for this forum, but in short the idea is to loop through each quaternion and if Quaternion.Dot(cur, last) < 0.0f then reverse all components of the quaternion (e.g. new Quaternion(-cur.x, -cur.y, -cur.z, -cur.w)) and recalculate tangents.