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