A Fast BlobCurve

This BlobCurve runs about 12 times faster than UnityEngine.AnimationCurve. And about 30% faster than Unity.Animation.AnimationCurve.

Basically, it uses only one dot(float4,float4) for one sample.

       public float Sample(float time)
        {
            TimeCheck();
            float t = (time - StartTime) * InvDuration;
            float tSq = t * t;
            float4 timeSerial = float4(tSq * t, tSq, t, 1);
            return dot(Factors, timeSerial);
        }

Because Matrix multiplication is done in blob baking.

       /// <summary>
        /// Convert From Cubic Hermite spline parameter
        /// </summary>
        public NativeCurveSegment(
            float value0, float tangent0, float time0,
            float value1, float tangent1, float time1)
        {
            float duration = time1 - time0;
            float4 factors = mul(S_BezierMat, float4(value0, value0 + (tangent0 * duration * OneThird), value1 - (tangent1 * duration * OneThird), value1));
            this = new NativeCurveSegment(factors, time0, duration);
        }

So it should run much faster than Unity.Animation.Curves.

keyframe index search is done by a cached local first binary search.

Source attached.

This curve has been used in my game for some time. It does reference several functions from my Toolkit, but that’s trivial. Will upload to GitHub when the whole lib is ready. Let me know if there are optimization suggestions.

Test Code

    [NaughtyAttributes.Button(nameof(PerformanceTest))]
    unsafe private void PerformanceTest()
    {
        if (curve == null || !blobCurveRef.IsCreated) return;
        var keys = curve.keys;
        var start = curve.keys[0].time;
        var time = start;
        var watch = System.Diagnostics.Stopwatch.StartNew();
        //-------------------------------------------------------------------------------------------------
        var pFun = NativeDelegate<EvaluateCall>.Compile(BurstEvaluateBlobCurve).AsFunctionPointer().Invoke;
        var copy = blobCurveRef;
        var value = 0f;
        watch.Restart();
        pFun(start, performanceTestStepLen, sampleCount, UnsafeUtility.AddressOf(ref copy), &value);
        watch.Stop();
        var tick = watch.ElapsedTicks;
        //-------------------------------------------------------------------------------------------------
        var copy2 = unityCurve;
        var pFunUnity = NativeDelegate<EvaluateCall>.Compile(BurstEvaluateUnityCurveBlob).AsFunctionPointer().Invoke;
        var value1 = 0f;
        watch.Restart();
        pFunUnity(start, performanceTestStepLen, sampleCount, UnsafeUtility.AddressOf(ref copy2), &value1);
        watch.Stop();
        var tick1 = watch.ElapsedTicks;

        //-------------------------------------------------------------------------------------------------
        watch.Restart();
        var value2 = 0f;
        for (int i = 0; i < sampleCount; i++)
        {
            value2 += curve.Evaluate(time);
            time += performanceTestStepLen;
        }
        watch.Stop();
        var tick2 = watch.ElapsedTicks;

        float f = System.Diagnostics.Stopwatch.Frequency * 1.0e-3f;
        float ms = tick / f;
        float ms1 = tick1 / f;
        float ms2 = tick2 / f;

        Debug.Log($"Simpled {sampleCount} times, BlobCurve : {tick} ticks {ms} ms, Unity.Animation.AnimationCurve : {tick1} ticks {ms1} ms, UnityEngine.AnimationCurve : {tick2} ticks {ms2} ms");
        //Debug.Log($"None-Bursted BlobCurve used{tick2} ticks {ms2} ms");
    }
    unsafe delegate void EvaluateCall(float time, float step, int count, void* ptr, float* valueOut);
    [BurstCompile]
    unsafe public static void BurstEvaluateBlobCurve(float time, float step, int count, void* pBlobCurveRef, float* valueOut)
    {
        var assetReference = UnsafeUtility.AsRef<BlobAssetReference<BlobCurve>>(pBlobCurveRef);
        ref var blobCurve = ref assetReference.Value;
        var value = 0f;
        var cache = BlobCurveCache.Empty;
        for (int i = 0; i < count; i++)
        {
            value += blobCurve.EvaluateIgnoreWrapMode(time, ref cache);
            time += step;
        }
        *valueOut = value;
    }

    [BurstCompile]
    unsafe public static void BurstEvaluateUnityCurveBlob(float time, float step, int count, void* pAnimCurve, float* valueOut)
    {
        ref var curve = ref UnsafeUtility.AsRef<UnityCurve>(pAnimCurve);
        var value = 0f;
        for (int i = 0; i < count; i++)
        {
            value += AnimationCurveEvaluator.Evaluate(time, ref curve);
            time += step;
        }
        *valueOut = value;
    }

Also, I got float2 float3 curve working.
the reason for them is that keyframe searching is the slowest part of curve evaluation. keep xyz synced in one curve is much faster than using 3 curves.
Quaternion curve and Transform curve is under test. will upload later.

6404436–716880–BlobCurve.cs (26.2 KB)
6404436–716883–BlobCurve2.cs (17 KB)
6404436–716886–BlobCurve3.cs (18.2 KB)

31 Likes


Shape match: UnityEngine.AnimationCurve is drawn as a red line, which is completely covered by NativeCurve (green line).
The Number is KeyFrame segment index. Red means they are out ranging curve time and extended via Loop/PingPong/Clamp.

And the original curve:

8 Likes

Manually vectorized BinarySearch could further improve performance.
Basically, in each iteration divide the current range into 4 segments, and search them all with int4 index, float4 time range, and bool4 result. and this could be generalized to all BinarySearch.

Continuous sampling will not benefit from this, but random access will.

Could extend local search to 4 Neighbors while it’s still one SIMD, one more step ahead will make fast forward playing animation hit Neighbors better.

Wow, this is very cool - thanks for sharing! I can’t comment on potential optimizations, just wanted to express my appreciation! This is very useful!

EDIT: Ah, just one question … is there a reason you call the method to evaluate the value of the curve at a given point in time Sample instead of Evaluate? Sample is a great name, but it’s inconsistent with the naming of AnimationCurve. I haven’t found documentation for Unity.Animation.Curves to check how they called the method there.

No specific reason for that. you can use Evaluate if you like it :wink:

1 Like

I updated the source code. renamed to BlobCrrve.
And I found that Unity.Animation.AnimationCurve supports only clamp mode as for wrap mode.
The original post and source code is updated.

1 Like

The new test, after some optimization:
About 12 times faster than UnityEngine.AnimationCurve. And about 30% faster than Unity.Animation.AnimationCurve.
Detail profile and source update in the original post.

It looks like math operation complexity is not that important in this case. Because they are as trivial as several assembly instructions. BlobArray memory access time takes way much longer than that. So I used some cache to reduce the chance of memory access (like Unity.Animation.AnimationCurve does).
And I got a result that’s 30% faster than Unity.Animation.AnimationCurve.
Also, the comparison is done in a Clamp only warp mode(no Loop/PingPong). as Unity.Animation.AnimationCurve support only clamp mode for now.
My Curve supports Loop/PingPong. but when any none Clamp mode is used in the curve. It’s about 5 times slower.
due to the use of modular operator (%) and two if branch.

2 Likes

New version uses Evaluate as you suggested :wink:

2 Likes

Fixed BlobCurve2/3 Conversion bug. Source updated.

2 Likes

Can you upload missing files? Currently missing BlobCurveCache and extensions it looks like.

Oh. This is a pretty sweet idea, I’m rolling my own tweening system in DOTS and I have a “Curve” and a “CurveAndKeyframe” BlobAsset, and those would benefit greatly from this.

Is there any news about this library?

I also don’t know if there is an official solution for this (one that takes advantage of all the features of UnityEngine.AnimationCurve, unlike Unity.Animation which only supports clamp mode).

1 Like

It still works just fine in 2022.

The proposed example is not very clear.
Are we supposed to use AnimationCurve from com.unity.animation in order to use BlobAssetReference>?

Because I use Unity 2021 I am not able to import the Animation package (it breaks and is unsupported) which is also why I am looking for an alternative.

AnimationCurve used for initial conversion is not from Animation package. Its from UnityEngine namespace.

Use BlobCurve.Create to convert AnimationCurve to BlobAssetReference, then you can attach BlobCurveSampler to the entity.

After that you can use Evaluate extension methods either on BlobCurveSampler, or
BlobAssetReference. Or you can call it directly on the BlobCurve.

Thank you for explaining how it works.
Everything works fine.
Thanks also to the OP, this is exactly what I was looking for.

Do the new features of Entities 1.0.0 allow a better approach to animation curves or is it still relevant to try to update these classes?
These are the only external systems I use in my project and some of the techniques they cover are still unknown to me.

There’s nothing new in Entities 1.0 in this regard. I am working on updating my solution, which is a very different technique from what is discussed in this thread, but very fast.

5 Likes