Broken AnimationCurve for rotations?

Referring to problem with rotations in AnimationCurves in http://forum.unity3d.com/viewtopic.php?t=32213&highlight= we can now actually see the glitches with the new Animation Editor. (We were hoping that these has been taken care by 2.6 update, but it seems we were optimistic.)

The first picture shows how rotation is jumping from 359->1 (as of course it should as rotation added by 2 degrees in 359 goes to 1) and thus when interpolating can give anything from 359 to 1. This leads to a one frame jump/incorrect angle in animation.

In the second picture it is clearly visible that between two keys the orientation behaves very strangely. The tangents are not causing this as in both keys they are “in line” with the correct values. BUT, as soon as I click to the tangent and make a small adjustment, all the shown glitches are suddenly gone?! (And Unity crashes almos every time. Yes, I sent a bug report!)

Is there any known solutions available to this rotation problem or should we really throw AnimationCurves away and start writing our own animation handling?

BR, Juha


Yeah this is a real problem still. Did you create your own animation system in the end?

Perhaps this issue (at the link below) with Animation Curves is the reason for the effects you are observing?..

http://forum.unity3d.com/threads/111...Curve-Glitches

The issue occurs with Eulers and Quaternions, seemingly due to the Animation Curve interpolator insisting on creating a smooth curve, rather than a smooth animation. We too hope that Unity fixes this glaring error, or at least provides an alternative such as no interpolation.

This is my working example of a component that records the rotation of a transform when created and updated and then finally saves the recorded data to an animation clip:

using UnityEditor;
using UnityEngine;

public class AnimationRecorderRotationData
{
    // Set things
    private Animator animator;
    private Transform animationTransform;
    private AnimationClip animationClip;
    private float recordingStartTime;

    // Data during recording
    private int lastKeyFrameNumber = -1;
    private float lastKeyFrameTime = 0;
    private Quaternion lastRotation;

    // Derived
    private AnimationCurve rotationCurveX;
    private AnimationCurve rotationCurveY;
    private AnimationCurve rotationCurveZ;

    public AnimationRecorderRotationData(Animator animator, Transform animationTransform, AnimationClip animationClip)
    {
        this.animator = animator;
        this.animationTransform = animationTransform;
        this.animationClip = animationClip;

        // Instanciate
        rotationCurveX = new AnimationCurve();
        rotationCurveY = new AnimationCurve();
        rotationCurveZ = new AnimationCurve();

    }

    public void SaveKeyFrame()
    {
        // First keyframe
        if (lastKeyFrameNumber == -1)
        {
            // Set recording start time
            recordingStartTime = Time.time;

            Vector3 firstEulerAngles = NormalizeEulerAngles(animationTransform.localRotation.eulerAngles);

            // Set initial values
            rotationCurveX.AddKey(0f, firstEulerAngles.x);
            rotationCurveY.AddKey(0f, firstEulerAngles.y);
            rotationCurveZ.AddKey(0f, firstEulerAngles.z);

            // Update last Keyframe values
            lastKeyFrameNumber = 0;
            lastKeyFrameTime = 0f;
            lastRotation = Quaternion.Euler(firstEulerAngles);
            return;
        }

        // Current keyframe blendshape values
        int currKeyFrameNumber = Mathf.Max(Mathf.FloorToInt((Time.time - recordingStartTime) * animationClip.frameRate), 0);

        // Animation keyframe has not yet updated
        if (currKeyFrameNumber <= lastKeyFrameNumber)
        {
            return;
        }

        // Calculations only when Update keyframe
        float currKeyFrameTime = currKeyFrameNumber / animationClip.frameRate;

        // When skipped multiple keyframes, catch up to current one by interpolation
        while (currKeyFrameNumber > lastKeyFrameNumber + 1)
        {
            int nextKeyFrameNumber = lastKeyFrameNumber + 1;
            float nextKeyFrameTime = nextKeyFrameNumber / animationClip.frameRate;

            // Interpolate
            float t = Mathf.InverseLerp(lastKeyFrameTime, currKeyFrameTime, nextKeyFrameTime);
            Vector3 nextEulerAngles = NormalizeEulerAngles(Quaternion.Lerp(lastRotation, animationTransform.localRotation, t).eulerAngles);

            // Set Keys
            rotationCurveX.AddKey(nextKeyFrameTime, nextEulerAngles.x);
            rotationCurveY.AddKey(nextKeyFrameTime, nextEulerAngles.y);
            rotationCurveZ.AddKey(nextKeyFrameTime, nextEulerAngles.z);

            // Update last values
            lastKeyFrameNumber = nextKeyFrameNumber;
            lastKeyFrameTime = nextKeyFrameTime;
            lastRotation = Quaternion.Euler(nextEulerAngles);
        }

        // Update current Keyframe
        Vector3 currEulerAngles = NormalizeEulerAngles(animationTransform.localRotation.eulerAngles);
        rotationCurveX.AddKey(currKeyFrameTime, currEulerAngles.x);
        rotationCurveY.AddKey(currKeyFrameTime, currEulerAngles.y);
        rotationCurveZ.AddKey(currKeyFrameTime, currEulerAngles.z);

        // Update last values
        lastKeyFrameNumber = currKeyFrameNumber;
        lastKeyFrameTime = currKeyFrameTime;
        lastRotation = Quaternion.Euler(currEulerAngles);
    }

    private static Vector3 NormalizeEulerAngles(Vector3 eulers)
    {
        eulers.x = NormalizeAngle(eulers.x);
        eulers.y = NormalizeAngle(eulers.y);
        eulers.z = NormalizeAngle(eulers.z);
        return eulers;
    }

    private static float NormalizeAngle(float angle)
    {
        if (angle > 270f)
        {
            angle -= 360f;
        } else if (angle < -270f)
        {
            angle += 360f;
        }

        return angle;
    }

    public void WriteDataToAnimationClip()
    {
        // Relative Path
        string relativePath = AnimationUtility.CalculateTransformPath(animationTransform, animator.transform);

        // Set animation clips
        animationClip.SetCurve(relativePath, typeof(Transform), "localEulerAngles.x", rotationCurveX);
        animationClip.SetCurve(relativePath, typeof(Transform), "localEulerAngles.y", rotationCurveY);
        animationClip.SetCurve(relativePath, typeof(Transform), "localEulerAngles.z", rotationCurveZ);
    }
}

This is how to use it for example:

// Initialize, for example in Start()
rotationData = new AnimationRecorderRotationData(animator, transformsForRotation, animationClip);

// Update, for example in Update()
rotationData.SaveKeyFrame();

// Save, for example in OnDestroy()
rotationData.WriteDataToAnimationClip();

The solution was to use the euler angles, normalize them between -180 and 180 and then use localEulerAngles when writing the curve to the animation clip.

Sources: https://discussions.unity.com/t/543434
and UnityCsReference/Editor/Mono/Animation/AnimationWindow/RotationCurveInterpolation.cs at 2019.4 · Unity-Technologies/UnityCsReference · GitHub