Need way to evaluate AnimationCurve in the job

I am using AnimationCurve not for animation but for calculations. It is a class so it could not go inside a job, moreover even if it can, this message would show up :

Evaluate can only be called from the main thread.
Constructors and field initializers will be executed from the loading thread when loading a scene.
Don't use this function in the constructor or field initializers, instead move initialization code to the Awake or Start function.
UnityEngine.AnimationCurve:Evaluate(AnimationCurve, Single)
FormationSt:LerpFormation(FormationSt, FormationSt, Single, Boolean) (at Assets/Scripts/Gameplay/Gameplay/FormationSt.cs:139)
Job:Execute() (at Assets/Scripts/Gameplay/Gameplay/System/GameplayLayoutSystem.cs:207)
Unity.Jobs.JobStruct`1:Execute(Job&, IntPtr, IntPtr, JobRanges&, Int32) (at /Users/builduser/buildslave/unity/build/Runtime/Jobs/Managed/IJob.cs:30)
Unity.Jobs.JobHandle:ScheduleBatchedJobsAndComplete(JobHandle&)
Unity.Jobs.JobHandle:Complete() (at /Users/builduser/buildslave/unity/build/Runtime/Jobs/ScriptBindings/JobHandle.bindings.cs:20)

However animationCurve.keys inside are already pure struct that would do well in a class/ECS. Is there any way that I can evaluate those keys in the job? Any algorithm that I have to rewrite in a job is fine too but I don’t know the terms to search for. A keyframe contains :

float m_Time;
float m_Value;
float m_InTangent;
float m_OutTangent;

int m_TangentMode;

int m_WeightedMode;

float m_InWeight;
float m_OutWeight;

Looking into CsReference the code already says thread safe so actually there might be a chance that Unity can already do this?

Also just an idea, in the docs I understand why a job could not spawn an another job well. But what if a job could ask for the main thread to perform a task for it? Then we can access all of main thread-only methods in the middle of the job. (Not an expert in concurrency so I don’t know what kind of problem would arise if we allow this)

2 Likes

As a workaround for sampling curves in jobs I made a small extention to the AnimationCurve that pre samples 256 samples in an array. Then pass this in a native array to the job. You loose resolution, but at least you can work with curves ourside of the jobs.

Not what you are looking for, but could work in some cases.

public static class AnimationCurveExtention
    {
        public static float[] GenerateCurveArray(this AnimationCurve self)
        {
            float[] returnArray = new float[256];
            for (int j = 0; j <= 255; j++)
            {
                returnArray[j] = self.Evaluate(j / 256f);            
            }              
            return returnArray;
        }
    }
10 Likes

i was using similar approach to pass an animation curve tho a shader

in a job/shader you can lerp nearest available samples to get smooth result

2 Likes

That’s a nice approach! Thank you!

This is what I do. This only handles 2 keys. Took this off an internet post not my idea. I actually like the caching approach above better though.

public float Evaluate(float t, Keyframe keyframe0, Keyframe keyframe1)
        {
            float dt = keyframe1.time - keyframe0.time;

            float m0 = keyframe0.outTangent * dt;
            float m1 = keyframe1.inTangent * dt;

            float t2 = t * t;
            float t3 = t2 * t;

            float a = 2 * t3 - 3 * t2 + 1;
            float b = t3 - 2 * t2 + t;
            float c = t3 - t2;
            float d = -2 * t3 + 3 * t2;

            return a * keyframe0.value + b * m0 + c * m1 + d * keyframe1.value;
        }

Hey I ended up making a solution as well based on suggestions from this thread. This struct can go in C# Jobs but be sure to construct it outside because the constructor will evaluate AnimationCurve on the main thread. Use it if you want.

using UnityEngine;
using Unity.Mathematics;
using Unity.Collections;

public struct SampledAnimationCurve : System.IDisposable
{
    NativeArray<float> sampledFloat;
    /// <param name="samples">Must be 2 or higher</param>
    public SampledAnimationCurve(AnimationCurve ac, int samples)
    {
        sampledFloat = new NativeArray<float>(samples, Allocator.Persistent);
        float timeFrom = ac.keys[0].time;
        float timeTo = ac.keys[ac.keys.Length - 1].time;
        float timeStep = (timeTo - timeFrom) / (samples - 1);

        for (int i = 0; i < samples; i++)
        {
            sampledFloat[i] = ac.Evaluate(timeFrom + (i * timeStep));
        }
    }

    public void Dispose()
    {
        sampledFloat.Dispose();
    }

    /// <param name="time">Must be from 0 to 1</param>
    public float EvaluateLerp(float time)
    {
        int len = sampledFloat.Length - 1;
        float clamp01 = time < 0 ? 0 : (time > 1 ? 1 : time);
        float floatIndex = (clamp01 * len);
        int floorIndex = (int)math.floor(floatIndex);
        if (floorIndex == len)
        {
            return sampledFloat[len];
        }

        float lowerValue = sampledFloat[floorIndex];
        float higherValue = sampledFloat[floorIndex + 1];
        return math.lerp(lowerValue, higherValue, math.frac(floatIndex));
    }
}
14 Likes

In case someone stumbled upon this in the future, I have found that Unity’s Evaluate is exactly Cubic Hermite Spline function described in Cubic Hermite spline - Wikipedia, by naively copy that I could get equal result as AnimationCurve.Evaluate down to within 0.0001f accuracy. Tested with a unit test .Equals(__).Within(0.0001f), in a test that randomize a curve with 100+ points of random value and tangents, then iterate linearly over them from 0~1 in a small increment (like 0.001f) each time.

(Also it is what @snacktime said above, a b c d are each of the hermite function)

So, it is possible to extract out Keyframe (already a struct, could all be in the job) then use Cubic Hermite Spline interpolation on a pair of keyframes on thread and get the same result, without the AnimationCurve class instance. Here I just move them out to NativeArray of Keyframe and they work fine in IJobParallelFor

The next problem is the weight, it seems like not a classical parameter seen in any wikis so I don’t know where it should go? I debugged that weight went from 0 to 1 on dragging to the right side like this, but still haven’t figured out where to plug that in.

4490071--413707--licecap.gif

Here’s my empirical observation of Unity’s weight. If someone could figure this out…

The weight ranges from 0 to 1. In the gif, dragging the handle to the right increase the weight. Dragging stretch up do not affect the weight (however you are changing the tangent)

Both tangents are 0, with weight applied and both weight are 0.3333333, it results in the same shape. I think this is the biggest hint, 0.3333333 may has to do something with the “cubic” function.

Both tangents are 0, with weight applied and both weight are at maximum 1, the curve skewed further in X axis to meet at the center between 2 points. Indicating that, weight 1 doesn’t mean unweighted like a weight function would have behave but rather really maximum possible weight.

Both tangents are 0, with weight applied and both weight are 0, results in a linear graph as if their tangents are 1.

Both tangents are 1, weight do not affect their shape at all no matter the value. It stays linear. Suggesting that weight do something to the components which became tangent, but nullified when both components are equal. I guess it did something to the cos component? Since with maximum weight the graph skewed further in X axis.

However if the other tangent is not 1, changing the weight of the side that has tangent 1 do affect the shape. Indicating that the weight is not simply “weighting that side’s tangent”.

11 Likes

Update : I tried improving on the naive implementation (left) with matrix based using some methods from Mathematics (right).

However the generated assembly looks about the same (?), maybe I will profile later how much better the matrix version could perform. (Or equivalent? The matrix multiply part ended up looking like when I was multiplying individually, maybe there is no more shortcut)

(Naive)

(Matrix)

2 Likes

I’d recommend using BlobData for this instead of NativeArray. It makes it so you can easily reference it from an IComponentData.

5 Likes

Are there any examples of BlobData usage?

You could copy what BlobificationTests.cs is doing to get started.

1 Like

We use AnimationCurves quite a bit and are also trying to figure out the best approach for them, so this thread has been a great help.

My main question right now is whether to use Dynamic buffers or BlobArrays

We started out by using DynamicBuffer. That way we could simply convert the Animationcurve to a dynamic buffer on the entity that needed it.

public struct AnimationCurveKeyframe : IBufferElementData
{
    public Keyframe keyFrame;
}

Populate a “keyframe buffer”

DynamicBuffer<AnimationCurveKeyframe> curveBuffer = dstManager.AddBuffer<AnimationCurveKeyframe>(entity);
Keyframe[] keys = animationCurve.keys;
for(int k = 0; k < keys.Length; ++k)
{
    Keyframe key = keys[k];
    curveBuffer.Add(new AnimationCurveKeyframe
    {
        keyFrame = key
    });
}

I then came upon this thread and saw the advice of using blobs. I fiddled around with it a bit and got that to work, and ended up with something not much more complicated than

public struct KeyframeBlobArray
{
    public BlobArray<Keyframe> keys;
}

public struct FooComponent : IComponentData
{
    public BlobAssetReference<KeyframeBlobArray> animationCurve;
}

public static unsafe BlobAssetReference<KeyframeBlobArray> ConstructKeyframeBlob(Keyframe[] keyframes)
    {
        BlobAllocator allocator = new BlobAllocator(-1);
        ref var root = ref allocator.ConstructRoot<KeyframeBlobArray>();

        allocator.Allocate(keyframes.Length, ref root.keys);

        for(int i = 0; i < keyframes.Length; ++i)
        {
            Keyframe k = keyframes[i];
            root.keys[i] = k;
        }

        BlobAssetReference<KeyframeBlobArray> keyframeBlob = allocator.CreateBlobAssetReference<KeyframeBlobArray>(Allocator.Persistent);
        allocator.Dispose();

        return keyframeBlob;
    }

Which feels better, since I can now include it in my components instead of having an attached buffer.

What I’m trying to figure out currently is which is the better approach, technically. What are the potential pitfalls of DynamicBuffer vs BlobArray in this context, what would be the drawback with each approach? It would be nice with some typical use cases for BlobAssetReferences.

Are there only special cases where you should use them, or can they be freely used in places where you need array-like structures in your components?

1 Like

BlobArray is better for curve data.

  1. It can be shared. Commonly animation curves, clips etc are shared data. DynamicBuffer is made for having per entity uniuq arrays. BlobArrays are for immutable shared assets.
  2. BlobData is easier to consume because you can simply reference one or multiple on a single icomponentdata and safely read all of it from a job.
7 Likes

Don’t mean to side-track this thread too much, just want to make sure I understand

If I understand you correctly, would the general rule be to use BlobArrays for shared data, and DynamicBuffer for entity specific data? In other words, of having unique data in BlobArrays in IComponentData, adding DynamicBuffer is preferred?

Side-note, huge thanks to you and your team (and this whole community), for spending so much time here in the forums

1 Like

Shared component data is really for segmenting your entities into forced chunk grouping. The name is unfortunate I think. Because really if you use it as data sharing mechanism, you will mostly shoot yourself in the foot because often you just end up with too small chunks.

BlobData is just a reference to shared immutable data. BlobData is also easily accessable from jobs and can contain complex data.

7 Likes

Is the name unfortunate enough for Shared Component Data get a rename?

5 Likes

I’ve spent a long time trying to get Shared Component Data to “work” because I was using it incorrectly based on the name.

3 Likes

Maybe ChunkComponentData?

@5argon Could you please share your Blob based curve implementation if you’ve done it?

2 Likes

I have benchmarked that it is currently so slower than AnimationCurve.Evaluate (like by about 20 times) on main thread evaluation, so I was afraid to share it until I have time to get it good enough… (could barely win on multithread evaluation while AnimationCurve do it all on main thread, with tons of evaluations)

Anyways I have opened that code’s repo : https://github.com/5argon/JobAnimationCurve. There are failing tests about performance and you can see how many ticks is the target in the test log. Also there are ignored tests about weights that will fail if not ignored. All other tests verify that the answer is equal to regular AnimationCurve.

4 Likes

I would think an optimized version would be using caching heavily. Off the top of my head…

Start with a set precision, the eval input is normalized to the closest point at the set precision. So for any length animation curve you have a known number of points up front and you can cache all the evaluations values in a NativeArray. You could calculate the entire curve once on the first access, or do it lazily.

I can’t imagine that you need so much precision as to make it not viable memory wise.