Skip to content

AnimationUtils Curve generation methods are slow #815

@jverral

Description

@jverral

Describe the bug
When loading models with animation at runtime, I'm seeing rather large spikes in time

Image

The issue is that AnimationCurve.AddKey does a sort every time you call it. I get a 2x speedup by allocating arrays and then doing new AnimationCurve(keys); Unfortunately, that is a lot of extra allocations. However, on Unity 6, one can use ArrayPool.Shared.Rent and then use AnimationCurve.SetKeys and use AsSpan on the rented arrays.

Example Fix:

This is a non-unity 6 fix with the tradeoff being an increase in memory:

Image
    static void AddVec3Curves(AnimationClip clip, string animationPath, string propertyPrefix, NativeArray<float> times, NativeArray<float3> values, InterpolationType interpolationType) {
        Profiler.BeginSample("AnimationUtils.AddVec3Curves");

        var keysX = ArrayPool<Keyframe>.Shared.Rent(times.Length);
        var keysY = ArrayPool<Keyframe>.Shared.Rent(times.Length);
        var keysZ = ArrayPool<Keyframe>.Shared.Rent(times.Length);
        int actualCount;

#if DEBUG
uint duplicates = 0;
#endif

        switch (interpolationType) {
            case InterpolationType.Step: {
                for (var i = 0; i < times.Length; i++) {
                    var time = times[i];
                    var value = values[i];
                    keysX[i] = new Keyframe(time, value.x, float.PositiveInfinity, 0);
                    keysY[i] = new Keyframe(time, value.y, float.PositiveInfinity, 0);
                    keysZ[i] = new Keyframe(time, value.z, float.PositiveInfinity, 0);
                }
                actualCount = 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];
                    keysX[i] = new Keyframe(time, value.x, inTangent.x, outTangent.x, .5f, .5f);
                    keysY[i] = new Keyframe(time, value.y, inTangent.y, outTangent.y, .5f, .5f);
                    keysZ[i] = new Keyframe(time, value.z, inTangent.z, outTangent.z, .5f, .5f);
                }
                actualCount = times.Length;
                break;
            }
            default: { // LINEAR
                var prevTime = times[0];
                var prevValue = values[0];
                var inTangent = new float3(0f);
                actualCount = 0;

                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) {
                        bool dTNeg = dT < 0f;
                        outTangent.x = (dV.x < 0f) ^ dTNeg ? float.NegativeInfinity : float.PositiveInfinity;
                        outTangent.y = (dV.y < 0f) ^ dTNeg ? float.NegativeInfinity : float.PositiveInfinity;
                        outTangent.z = (dV.z < 0f) ^ dTNeg ? float.NegativeInfinity : float.PositiveInfinity;
                    } else {
                        outTangent = dV / dT;
                    }

                    keysX[actualCount] = new Keyframe(prevTime, prevValue.x, inTangent.x, outTangent.x);
                    keysY[actualCount] = new Keyframe(prevTime, prevValue.y, inTangent.y, outTangent.y);
                    keysZ[actualCount] = new Keyframe(prevTime, prevValue.z, inTangent.z, outTangent.z);
                    actualCount++;

                    inTangent = outTangent;
                    prevTime = time;
                    prevValue = value;
                }

                keysX[actualCount] = new Keyframe(prevTime, prevValue.x, inTangent.x, 0);
                keysY[actualCount] = new Keyframe(prevTime, prevValue.y, inTangent.y, 0);
                keysZ[actualCount] = new Keyframe(prevTime, prevValue.z, inTangent.z, 0);
                actualCount++;

                break;
            }
        }

		AnimationCurve curveX;
		AnimationCurve curveY;
		AnimationCurve curveZ;
		if (actualCount != keysX.Length || actualCount != keysY.Length || actualCount != keysZ.Length)
		{
			// AnimationCurve requires an exact-length array; one allocation shared across all three copies.
			Keyframe[] tempKeys = new Keyframe[actualCount];
			Array.Copy(keysX, tempKeys, actualCount);
			curveX = new AnimationCurve(tempKeys);
			Array.Copy(keysY, tempKeys, actualCount);
			curveY = new AnimationCurve(tempKeys);
			Array.Copy(keysZ, tempKeys, actualCount);
			curveZ = new AnimationCurve(tempKeys);
		}
		else // exact match, just straight up use the rented arrays without copying
		{
			curveX = new AnimationCurve(keysX);
			curveY = new AnimationCurve(keysY);
			curveZ = new AnimationCurve(keysZ);
		}

		ArrayPool<Keyframe>.Shared.Return(keysX);
        ArrayPool<Keyframe>.Shared.Return(keysY);
        ArrayPool<Keyframe>.Shared.Return(keysZ);

        clip.SetCurve(animationPath, typeof(Transform), $"{propertyPrefix}x", curveX);
        clip.SetCurve(animationPath, typeof(Transform), $"{propertyPrefix}y", curveY);
        clip.SetCurve(animationPath, typeof(Transform), $"{propertyPrefix}z", curveZ);

Files

Attach or link to .gltf/.glb files that trigger the bug.

In addition, make sure to run those files through the glTF Validator first. If you encounter errors or warnings, try to make sure they are not responsible for the issue and file a bug report with the software that generated the glTF file as well.

Tip

You have to ZIP archive them first in order for GitHub to accept the upload.

If your files are confidential:

  • Try to create a similar, but intellectual-property-free glTF that reproduces the bug in the same way (so any community member can have a look)
  • Otherwise, still create this issue and send the files (or a link to them) discretely via email

To Reproduce
Steps to reproduce the behavior:

  1. Go to '...'
  2. Click on '....'
  3. Scroll down to '....'
  4. See error

Expected behavior
A clear and concise description of what you expected to happen.

Screenshots
If applicable, add screenshots to help explain your problem.

Desktop (please complete the following information):

  • glTFast version
  • Unity Editor version [e.g. 2021.2.1f1]
  • Render Pipeline and version [e.g. Universal Render Pipeline 12.0]
  • Platform: [e.g. Editor, Windows Player, iOS]

additionally (if significant for the bug):

  • Device: [e.g. iPhone6]
  • OS: [e.g. iOS8.1]
  • For WebGL: Browser [e.g. stock browser, safari]

Additional context
Add any other context about the problem here.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions