Search Unity

Animation Curve Glitches

Discussion in 'Scripting' started by ADeveloper, Nov 11, 2011.

  1. ADeveloper

    ADeveloper

    Joined:
    Nov 11, 2011
    Posts:
    4
    While using Unity's animation curves we are experiencing undesirable glitches which seem to be caused by AnimationCurve's key frame interpolation method. Does anybody else see this or have a workaround for this?

    Here is a project containing 4 test scenes which highlight the issue:
    View attachment $QuaternionTests00.zip

    Test00 – Manual Rotation (Euler)
    In this test, we rotate a mesh 0 to +359.999 degrees about the vertical Y-axis over a fixed period of time by manipulating the game object's transform Euler angles. At the end of the period, the mesh is rotated back to 0 degrees and the cycle is repeated.

    This is a standard animation technique for giving continuous smooth rotations. Even though it appears that rotations beyond 0 to +360 degrees are occurring, the requested angles are always within the 0 to +360 degree bounds required by specifying Euler angles.

    The chart below illustrates how the Y-axis angle is changed with time. Note that even though the curve is discontinuous, the animation motion appears to be continuous to the observer:

    $AnimationCurveEuler.png

    No glitches are seen in this test and the behavior of Unity meets the expectation of the observer. The animation is good.


    Test01 – Manual Rotation (Quaternion)
    In this test, we rotate a mesh 0 to +359.999 degrees about the vertical Y-axis over a fixed period of time by manipulating the game object's transform rotation quaternion. This is achieved by assigning a value of -1.0 to +0.999 to the Y component of the quaternion. At the end of the period, the mesh is rotated back to 0 degrees and the cycle is repeated.

    This is a standard animation technique for giving continuous smooth rotations. Even though it appears that rotations beyond -180 to +180 degrees are occurring, the requested angles are always within the -180 to +180 bounds required by specifying quaternion rotation angles.

    The chart below illustrates how the quaternion Y component is changed with time. Note that even though the curve is discontinuous, the animation motion appears to be continuous to the observer:

    $AnimationCurveQuaternion.png

    No glitches are seen in this test and the behavior of Unity meets the expectation of the observer. The animation is good.


    Test02 – Animation Rotation (Quaternion)
    In this test, we rotate a mesh 0 to +359.999 degrees about the vertical Y-axis over a fixed period of time by using an AnimationCurve to manipulate the game object's transform rotation quaternion. This is achieved by assigning a value of -1.0 to +0.999 to the Y component of the quaternion via keyFrames. At the end of the period, the mesh is rotated back to 0 degrees and the cycle is repeated.

    Depending on your machine, you might be able to notice glitches in this test and the behavior of Unity does not meet the expectation of the observer. The glitches occur as the mesh passes through the 0 degree point. The glitches may be too fast to comprehend what is happening. The animation is not acceptable.


    Test03 – Multiple Animation Rotations (Quaternion)
    In this test, we rotate a mesh 0 to 359.999 degrees about the vertical Y-axis over a fixed period of time by using an AnimationCurve to manipulate the game object's transform rotation quaternion. This is achieved by assigning a value of -1.0 to +0.999 to the Y component of the quaternion via keyFrames. At the end of the period, the mesh is rotated back to 0 degrees and the cycle is repeated. In order to increase the machine load and slow down the animation, multiple animation clips are assigned to the single mesh.

    On most machines, you should now be able to clearly notice glitches in this test and the behavior of Unity does not meet the expectation of the observer. The glitches occur as the mesh passes through the 0 degree point. The glitch behavior should now be clear – the mesh rotates in the reverse direction a full 360 degrees before resuming normal behavior. The animation is not acceptable.

    The chart below illustrates how the quaternion Y component is changed with time. Note that even though the value 'trace' has now been made continuous by the Animation curve, the animation motion appears to be discontinuous to the observer, as the mesh is rotated in the wrong direction in between the key frames which straddle the 0 degree point:

    $AnimationCurveInterpolation.png

    Note that we believe that this is not an issue of attempting to rotate outside of a Quaternion's (+/-180) degree bounds, nor is it an issue of path ambiguity at the 180 degree point. It seems to be an issue created by the AnimationCurve (or Animation) class as the manual rotation methods do not suffer from glitches.

    We believe the issue lies in the interpolation method used by the AnimationCurve (or Animation) class. It appears to always try to create a continuous curve, even though this is often NOT what is needed for smooth continuous animation. In other words, it seems to be optimized for creating a nice mathematical curve, rather than a nice animation and these are quite different goals.

    These other threads seem to relate to the same issue:
    http://forum.unity3d.com/threads/18318-Rotation-direction-for-AnimationCurves
    http://forum.unity3d.com/threads/33125-Broken-AnimationCurve-for-rotations


    We have considered reproducing AnimationCurve's functionality (minus the glitches) using a custom method but this is sub-optimal as this would be slower and not allow us to use animation blending.

    It would be good if AnimationCurve would have a correct rotational interpolation mode for animations in which no interpolation is done whenever a transition greater than some amount occurs and/or it would be good to be able to disable interpolation completely for AnimationCurves. Either of these would actually provide a performance boost too as unnecessary interpolations would no longer have to be performed.

    How have other people got around this issue?
    [note: Bug case 429206 filed, please file too if this is an issue for you]
     
    Last edited: Nov 21, 2011
  2. UnityRamzes

    UnityRamzes

    Joined:
    Jul 12, 2022
    Posts:
    1
    11 years later, it'd still be great
     
  3. halley

    halley

    Joined:
    Aug 26, 2013
    Posts:
    2,447
    Please don't necro post. This is your first post on the forum, and you're not adding anything useful.

    An animation curve cannot replicate a sawtooth with a zero width on the downstroke like it's presented. "Depending on the machine" means "if you happen interpolate between the top of the sawtooth and the bottom of the sawtooth." This is going to have a chance to show a value that is not continous, which is pretty evident from the animation curve not being continuous. Unless you specifically create a new feature for AnimationCurve where a handle is understood to be a non-continuous zero-width sawtooth, this will continue to occur.

    Also, a Quaternion mathematically has to remain a unit vector in 4space. Any other magnitude in 4space is going to be wildly misinterpreted by the engine when it tries to form rotation matrices. Animating a single element of a quaternion without regard to this requirement on the other elements is going to have artifacts, because the magnitude in 4space will be varying with the single element that you're driving.
     
  4. Rathmansbach

    Rathmansbach

    Joined:
    Oct 26, 2023
    Posts:
    5
    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:

    Code (CSharp):
    1. using UnityEditor;
    2. using UnityEngine;
    3.  
    4. public class AnimationRecorderRotationData
    5. {
    6.     // Set things
    7.     private Animator animator;
    8.     private Transform animationTransform;
    9.     private AnimationClip animationClip;
    10.     private float recordingStartTime;
    11.  
    12.     // Data during recording
    13.     private int lastKeyFrameNumber = -1;
    14.     private float lastKeyFrameTime = 0;
    15.     private Quaternion lastRotation;
    16.  
    17.     // Derived
    18.     private AnimationCurve rotationCurveX;
    19.     private AnimationCurve rotationCurveY;
    20.     private AnimationCurve rotationCurveZ;
    21.  
    22.     public AnimationRecorderRotationData(Animator animator, Transform animationTransform, AnimationClip animationClip)
    23.     {
    24.         this.animator = animator;
    25.         this.animationTransform = animationTransform;
    26.         this.animationClip = animationClip;
    27.  
    28.         // Instanciate
    29.         rotationCurveX = new AnimationCurve();
    30.         rotationCurveY = new AnimationCurve();
    31.         rotationCurveZ = new AnimationCurve();
    32.  
    33.     }
    34.  
    35.     public void SaveKeyFrame()
    36.     {
    37.         // First keyframe
    38.         if (lastKeyFrameNumber == -1)
    39.         {
    40.             // Set recording start time
    41.             recordingStartTime = Time.time;
    42.  
    43.             Vector3 firstEulerAngles = NormalizeEulerAngles(animationTransform.localRotation.eulerAngles);
    44.  
    45.             // Set initial values
    46.             rotationCurveX.AddKey(0f, firstEulerAngles.x);
    47.             rotationCurveY.AddKey(0f, firstEulerAngles.y);
    48.             rotationCurveZ.AddKey(0f, firstEulerAngles.z);
    49.  
    50.             // Update last Keyframe values
    51.             lastKeyFrameNumber = 0;
    52.             lastKeyFrameTime = 0f;
    53.             lastRotation = Quaternion.Euler(firstEulerAngles);
    54.             return;
    55.         }
    56.  
    57.         // Current keyframe blendshape values
    58.         int currKeyFrameNumber = Mathf.Max(Mathf.FloorToInt((Time.time - recordingStartTime) * animationClip.frameRate), 0);
    59.  
    60.         // Animation keyframe has not yet updated
    61.         if (currKeyFrameNumber <= lastKeyFrameNumber)
    62.         {
    63.             return;
    64.         }
    65.  
    66.         // Calculations only when Update keyframe
    67.         float currKeyFrameTime = currKeyFrameNumber / animationClip.frameRate;
    68.  
    69.         // When skipped multiple keyframes, catch up to current one by interpolation
    70.         while (currKeyFrameNumber > lastKeyFrameNumber + 1)
    71.         {
    72.             int nextKeyFrameNumber = lastKeyFrameNumber + 1;
    73.             float nextKeyFrameTime = nextKeyFrameNumber / animationClip.frameRate;
    74.  
    75.             // Interpolate
    76.             float t = Mathf.InverseLerp(lastKeyFrameTime, currKeyFrameTime, nextKeyFrameTime);
    77.             Vector3 nextEulerAngles = NormalizeEulerAngles(Quaternion.Lerp(lastRotation, animationTransform.localRotation, t).eulerAngles);
    78.  
    79.             // Set Keys
    80.             rotationCurveX.AddKey(nextKeyFrameTime, nextEulerAngles.x);
    81.             rotationCurveY.AddKey(nextKeyFrameTime, nextEulerAngles.y);
    82.             rotationCurveZ.AddKey(nextKeyFrameTime, nextEulerAngles.z);
    83.  
    84.             // Update last values
    85.             lastKeyFrameNumber = nextKeyFrameNumber;
    86.             lastKeyFrameTime = nextKeyFrameTime;
    87.             lastRotation = Quaternion.Euler(nextEulerAngles);
    88.         }
    89.  
    90.         // Update current Keyframe
    91.         Vector3 currEulerAngles = NormalizeEulerAngles(animationTransform.localRotation.eulerAngles);
    92.         rotationCurveX.AddKey(currKeyFrameTime, currEulerAngles.x);
    93.         rotationCurveY.AddKey(currKeyFrameTime, currEulerAngles.y);
    94.         rotationCurveZ.AddKey(currKeyFrameTime, currEulerAngles.z);
    95.  
    96.         // Update last values
    97.         lastKeyFrameNumber = currKeyFrameNumber;
    98.         lastKeyFrameTime = currKeyFrameTime;
    99.         lastRotation = Quaternion.Euler(currEulerAngles);
    100.     }
    101.  
    102.     private static Vector3 NormalizeEulerAngles(Vector3 eulers)
    103.     {
    104.         eulers.x = NormalizeAngle(eulers.x);
    105.         eulers.y = NormalizeAngle(eulers.y);
    106.         eulers.z = NormalizeAngle(eulers.z);
    107.         return eulers;
    108.     }
    109.  
    110.     private static float NormalizeAngle(float angle)
    111.     {
    112.         if (angle > 270f)
    113.         {
    114.             angle -= 360f;
    115.         } else if (angle < -270f)
    116.         {
    117.             angle += 360f;
    118.         }
    119.  
    120.         return angle;
    121.     }
    122.  
    123.     public void WriteDataToAnimationClip()
    124.     {
    125.         // Relative Path
    126.         string relativePath = AnimationUtility.CalculateTransformPath(animationTransform, animator.transform);
    127.  
    128.         // Set animation clips
    129.         animationClip.SetCurve(relativePath, typeof(Transform), "localEulerAngles.x", rotationCurveX);
    130.         animationClip.SetCurve(relativePath, typeof(Transform), "localEulerAngles.y", rotationCurveY);
    131.         animationClip.SetCurve(relativePath, typeof(Transform), "localEulerAngles.z", rotationCurveZ);
    132.     }
    133. }
    134.  
    This is how to use it for example:

    Code (CSharp):
    1. // Initialize, for example in Start()
    2. rotationData = new AnimationRecorderRotationData(animator, transformsForRotation, animationClip);
    3.  
    4. // Update, for example in Update()
    5. rotationData.SaveKeyFrame();
    6.  
    7. // Save, for example in OnDestroy()
    8. rotationData.WriteDataToAnimationClip();
    9.  
    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://forum.unity.com/threads/converting-animation-curve-from-quaternion-to-euler-via-script.256272/
    and https://github.com/Unity-Technologi...AnimationWindow/RotationCurveInterpolation.cs
     
    Last edited: Nov 23, 2023