From 930fe4252a9040e2e0b612a86a038917481693e1 Mon Sep 17 00:00:00 2001 From: jverral Date: Thu, 26 Mar 2026 16:40:20 -0700 Subject: [PATCH] Optimize AnimationUtils --- .../Runtime/Scripts/AnimationUtils.cs | 269 +++++++++++++++++- 1 file changed, 263 insertions(+), 6 deletions(-) diff --git a/Packages/com.unity.cloud.gltfast/Runtime/Scripts/AnimationUtils.cs b/Packages/com.unity.cloud.gltfast/Runtime/Scripts/AnimationUtils.cs index 67d99a68d..f1f63bb9a 100644 --- a/Packages/com.unity.cloud.gltfast/Runtime/Scripts/AnimationUtils.cs +++ b/Packages/com.unity.cloud.gltfast/Runtime/Scripts/AnimationUtils.cs @@ -5,6 +5,7 @@ #if UNITY_ANIMATION using System; +using System.Buffers; using System.Text; using GLTFast.Schema; using Unity.Collections; @@ -19,15 +20,153 @@ static class AnimationUtils { const float k_TimeEpsilon = 0.00001f; + const int k_TranslationPropertyIndex = 0; + const int k_ScalePropertyIndex = 1; + + static readonly string[][] k_Vec3PropertyNames = { + new[] { "localPosition.x", "localPosition.y", "localPosition.z" }, + new[] { "localScale.x", "localScale.y", "localScale.z" }, + }; + public static void AddTranslationCurves(AnimationClip clip, string animationPath, NativeArray times, NativeArray values, InterpolationType interpolationType) { - AddVec3Curves(clip, animationPath, "localPosition.", times, values, interpolationType); + AddVec3Curves(clip, animationPath, k_TranslationPropertyIndex, times, values, interpolationType); } public static void AddScaleCurves(AnimationClip clip, string animationPath, NativeArray times, NativeArray values, InterpolationType interpolationType) { - AddVec3Curves(clip, animationPath, "localScale.", times, values, interpolationType); + AddVec3Curves(clip, animationPath, k_ScalePropertyIndex, times, values, interpolationType); } public static void AddRotationCurves(AnimationClip clip, string animationPath, NativeArray times, NativeArray values, InterpolationType interpolationType) { +#if UNITY_6000_2_OR_NEWER + Profiler.BeginSample("AnimationUtils.AddRotationCurves"); + var rentedX = ArrayPool.Shared.Rent(times.Length); + var rentedY = ArrayPool.Shared.Rent(times.Length); + var rentedZ = ArrayPool.Shared.Rent(times.Length); + var rentedW = ArrayPool.Shared.Rent(times.Length); + var count = 0; + +#if DEBUG + uint duplicates = 0; +#endif + + try { + switch (interpolationType) { + case InterpolationType.Step: { + for (var i = 0; i < times.Length; i++) { + var time = times[i]; + var value = values[i]; + rentedX[i] = new Keyframe(time, value.value.x, float.PositiveInfinity, 0); + rentedY[i] = new Keyframe(time, value.value.y, float.PositiveInfinity, 0); + rentedZ[i] = new Keyframe(time, value.value.z, float.PositiveInfinity, 0); + rentedW[i] = new Keyframe(time, value.value.w, float.PositiveInfinity, 0); + } + count = times.Length; + break; + } + case InterpolationType.CubicSpline: { + for (var i = 0; i < times.Length; i++) { + var time = times[i]; + var inTangent = values[i * 3]; + var value = values[i * 3 + 1]; + var outTangent = values[i * 3 + 2]; + rentedX[i] = new Keyframe(time, value.value.x, inTangent.value.x, outTangent.value.x, .5f, .5f); + rentedY[i] = new Keyframe(time, value.value.y, inTangent.value.y, outTangent.value.y, .5f, .5f); + rentedZ[i] = new Keyframe(time, value.value.z, inTangent.value.z, outTangent.value.z, .5f, .5f); + rentedW[i] = new Keyframe(time, value.value.w, inTangent.value.w, outTangent.value.w, .5f, .5f); + } + count = times.Length; + break; + } + default: { // LINEAR + var prevTime = times[0]; + var prevValue = values[0]; + var inTangent = new quaternion(new float4(0f)); + + Assert.AreEqual(times.Length, values.Length); + for (var i = 1; i < times.Length; i++) { + var time = times[i]; + var value = values[i]; + + if (prevTime >= time) { + // Time value is not increasing, so we ignore this keyframe + // This happened on some Sketchfab files (see #298) +#if DEBUG + duplicates++; +#endif + continue; + } + + // Ensure shortest path rotation ( see https://www.khronos.org/registry/glTF/specs/2.0/glTF-2.0.html#interpolation-slerp ) + if (math.dot(prevValue, value) < 0) { + value.value = -value.value; + } + + var dT = time - prevTime; + var dV = value.value - prevValue.value; + quaternion outTangent; + if (dT < k_TimeEpsilon) { + outTangent.value.x = (dV.x < 0f) ^ (dT < 0f) ? float.NegativeInfinity : float.PositiveInfinity; + outTangent.value.y = (dV.y < 0f) ^ (dT < 0f) ? float.NegativeInfinity : float.PositiveInfinity; + outTangent.value.z = (dV.z < 0f) ^ (dT < 0f) ? float.NegativeInfinity : float.PositiveInfinity; + outTangent.value.w = (dV.w < 0f) ^ (dT < 0f) ? float.NegativeInfinity : float.PositiveInfinity; + } else { + outTangent = dV / dT; + } + + rentedX[count] = new Keyframe(prevTime, prevValue.value.x, inTangent.value.x, outTangent.value.x); + rentedY[count] = new Keyframe(prevTime, prevValue.value.y, inTangent.value.y, outTangent.value.y); + rentedZ[count] = new Keyframe(prevTime, prevValue.value.z, inTangent.value.z, outTangent.value.z); + rentedW[count] = new Keyframe(prevTime, prevValue.value.w, inTangent.value.w, outTangent.value.w); + count++; + + inTangent = outTangent; + prevTime = time; + prevValue = value; + } + + rentedX[count] = new Keyframe(prevTime, prevValue.value.x, inTangent.value.x, 0); + rentedY[count] = new Keyframe(prevTime, prevValue.value.y, inTangent.value.y, 0); + rentedZ[count] = new Keyframe(prevTime, prevValue.value.z, inTangent.value.z, 0); + rentedW[count] = new Keyframe(prevTime, prevValue.value.w, inTangent.value.w, 0); + count++; + break; + } + } + + var rotX = new AnimationCurve(); + var rotY = new AnimationCurve(); + var rotZ = new AnimationCurve(); + var rotW = new AnimationCurve(); + rotX.SetKeys(new ReadOnlySpan(rentedX, 0, count)); + rotY.SetKeys(new ReadOnlySpan(rentedY, 0, count)); + rotZ.SetKeys(new ReadOnlySpan(rentedZ, 0, count)); + rotW.SetKeys(new ReadOnlySpan(rentedW, 0, count)); + + clip.SetCurve(animationPath, typeof(Transform), "localRotation.x", rotX); + clip.SetCurve(animationPath, typeof(Transform), "localRotation.y", rotY); + clip.SetCurve(animationPath, typeof(Transform), "localRotation.z", rotZ); + clip.SetCurve(animationPath, typeof(Transform), "localRotation.w", rotW); + } + finally { + ArrayPool.Shared.Return(rentedX); + ArrayPool.Shared.Return(rentedY); + ArrayPool.Shared.Return(rentedZ); + ArrayPool.Shared.Return(rentedW); + } + + Profiler.EndSample(); +#if DEBUG + if (duplicates > 0) { + ReportDuplicateKeyframes(); + } +#endif +#else + AddRotationCurvesLegacy(clip, animationPath, times, values, interpolationType); +#endif + } + +#if !UNITY_6000_2_OR_NEWER + static void AddRotationCurvesLegacy(AnimationClip clip, string animationPath, NativeArray times, NativeArray values, InterpolationType interpolationType) { Profiler.BeginSample("AnimationUtils.AddRotationCurves"); var rotX = new AnimationCurve(); var rotY = new AnimationCurve(); @@ -130,6 +269,7 @@ public static void AddRotationCurves(AnimationClip clip, string animationPath, N } #endif } +#endif public static string CreateAnimationPath(int nodeIndex, string[] nodeNames, int[] parentIndex) { Profiler.BeginSample("AnimationUtils.CreateAnimationPath"); @@ -183,7 +323,122 @@ public static void AddMorphTargetWeightCurves( Profiler.EndSample(); } - static void AddVec3Curves(AnimationClip clip, string animationPath, string propertyPrefix, NativeArray times, NativeArray values, InterpolationType interpolationType) { + static void AddVec3Curves(AnimationClip clip, string animationPath, int propertyIndex, NativeArray times, NativeArray values, InterpolationType interpolationType) { +#if UNITY_6000_2_OR_NEWER + Profiler.BeginSample("AnimationUtils.AddVec3Curves"); + var rentedX = ArrayPool.Shared.Rent(times.Length); + var rentedY = ArrayPool.Shared.Rent(times.Length); + var rentedZ = ArrayPool.Shared.Rent(times.Length); + var count = 0; + +#if DEBUG + uint duplicates = 0; +#endif + + try { + switch (interpolationType) { + case InterpolationType.Step: { + for (var i = 0; i < times.Length; i++) { + var time = times[i]; + var value = values[i]; + rentedX[i] = new Keyframe(time, value.x, float.PositiveInfinity, 0); + rentedY[i] = new Keyframe(time, value.y, float.PositiveInfinity, 0); + rentedZ[i] = new Keyframe(time, value.z, float.PositiveInfinity, 0); + } + count = times.Length; + break; + } + case InterpolationType.CubicSpline: { + for (var i = 0; i < times.Length; i++) { + var time = times[i]; + var inTangent = values[i * 3]; + var value = values[i * 3 + 1]; + var outTangent = values[i * 3 + 2]; + rentedX[i] = new Keyframe(time, value.x, inTangent.x, outTangent.x, .5f, .5f); + rentedY[i] = new Keyframe(time, value.y, inTangent.y, outTangent.y, .5f, .5f); + rentedZ[i] = new Keyframe(time, value.z, inTangent.z, outTangent.z, .5f, .5f); + } + count = times.Length; + break; + } + default: { // LINEAR + var prevTime = times[0]; + var prevValue = values[0]; + var inTangent = new float3(0f); + + for (var i = 1; i < times.Length; i++) { + var time = times[i]; + var value = values[i]; + + if (prevTime >= time) { + // Time value is not increasing, so we ignore this keyframe + // This happened on some Sketchfab files (see #298) +#if DEBUG + duplicates++; +#endif + continue; + } + + var dT = time - prevTime; + var dV = value - prevValue; + float3 outTangent; + if (dT < k_TimeEpsilon) { + outTangent.x = (dV.x < 0f) ^ (dT < 0f) ? float.NegativeInfinity : float.PositiveInfinity; + outTangent.y = (dV.y < 0f) ^ (dT < 0f) ? float.NegativeInfinity : float.PositiveInfinity; + outTangent.z = (dV.z < 0f) ^ (dT < 0f) ? float.NegativeInfinity : float.PositiveInfinity; + } else { + outTangent = dV / dT; + } + + rentedX[count] = new Keyframe(prevTime, prevValue.x, inTangent.x, outTangent.x); + rentedY[count] = new Keyframe(prevTime, prevValue.y, inTangent.y, outTangent.y); + rentedZ[count] = new Keyframe(prevTime, prevValue.z, inTangent.z, outTangent.z); + count++; + + inTangent = outTangent; + prevTime = time; + prevValue = value; + } + + rentedX[count] = new Keyframe(prevTime, prevValue.x, inTangent.x, 0); + rentedY[count] = new Keyframe(prevTime, prevValue.y, inTangent.y, 0); + rentedZ[count] = new Keyframe(prevTime, prevValue.z, inTangent.z, 0); + count++; + break; + } + } + + var curveX = new AnimationCurve(); + var curveY = new AnimationCurve(); + var curveZ = new AnimationCurve(); + curveX.SetKeys(new ReadOnlySpan(rentedX, 0, count)); + curveY.SetKeys(new ReadOnlySpan(rentedY, 0, count)); + curveZ.SetKeys(new ReadOnlySpan(rentedZ, 0, count)); + + var propNames = k_Vec3PropertyNames[propertyIndex]; + clip.SetCurve(animationPath, typeof(Transform), propNames[0], curveX); + clip.SetCurve(animationPath, typeof(Transform), propNames[1], curveY); + clip.SetCurve(animationPath, typeof(Transform), propNames[2], curveZ); + } + finally { + ArrayPool.Shared.Return(rentedX); + ArrayPool.Shared.Return(rentedY); + ArrayPool.Shared.Return(rentedZ); + } + + Profiler.EndSample(); +#if DEBUG + if (duplicates > 0) { + ReportDuplicateKeyframes(); + } +#endif +#else + AddVec3CurvesLegacy(clip, animationPath, propertyIndex, times, values, interpolationType); +#endif + } + +#if !UNITY_6000_2_OR_NEWER + static void AddVec3CurvesLegacy(AnimationClip clip, string animationPath, int propertyIndex, NativeArray times, NativeArray values, InterpolationType interpolationType) { Profiler.BeginSample("AnimationUtils.AddVec3Curves"); var curveX = new AnimationCurve(); var curveY = new AnimationCurve(); @@ -262,9 +517,10 @@ static void AddVec3Curves(AnimationClip clip, string animationPath, string prope } } - clip.SetCurve(animationPath, typeof(Transform), $"{propertyPrefix}x", curveX); - clip.SetCurve(animationPath, typeof(Transform), $"{propertyPrefix}y", curveY); - clip.SetCurve(animationPath, typeof(Transform), $"{propertyPrefix}z", curveZ); + var propNames = k_Vec3PropertyNames[propertyIndex]; + clip.SetCurve(animationPath, typeof(Transform), propNames[0], curveX); + clip.SetCurve(animationPath, typeof(Transform), propNames[1], curveY); + clip.SetCurve(animationPath, typeof(Transform), propNames[2], curveZ); Profiler.EndSample(); #if DEBUG if (duplicates > 0) { @@ -272,6 +528,7 @@ static void AddVec3Curves(AnimationClip clip, string animationPath, string prope } #endif } +#endif static void AddScalarCurve(AnimationClip clip, string animationPath, string propertyPrefix, int curveIndex, int valueStride, NativeArray times, NativeArray values, InterpolationType interpolationType) { Profiler.BeginSample("AnimationUtils.AddScalarCurve");