Need way to evaluate AnimationCurve in the job

I have interest in having AnimationCurve’s in jobs, so it’s exciting to see @5argon has code available for us to try out. I’ve adjusted the code for it to run in current Entities 0.5.1. I have that adjustment available as a pull request for 5argon’s repo.

It’s pretty unfortunate how unperformant the main thread evaluation is at the moment. Sometime soon, I’m going to dive through unity decompiled to see if I can find the source code for curve evaluation, and get an idea how they optimized it. (EDIT: Nvm about that decompiled idea. The C# code is just a wrapper for the pure native code.)

The new DOTS animation package has curve evaluation that might be worth checking out.

1 Like

It’s worth noting that if you evaluate a lot of curve data, it might be worth it to pre-sample the curve into a lookup blob array. I did just that recently at work and it resulted in a massive performance increase for our use case.

1 Like

Presampling the curve into a lookup array was best take away from this thread.

2 Likes

Just got it working, it just slid straight into my code perfectly and allowed me to burst a lot of stuff that was inside ComponentSystem ForEach before. Thanks for the tips!

It seems like they have a version of this in the DOTS animation already, interval base caching: https://docs.unity3d.com/Packages/com.unity.animation@0.3/api/Unity.Animation.AnimationCurve.html

1 Like

I managed to reproduce the full AnimationCurve (weights and wrap modes) based on the code that 5argon generously provided. It’s useful if you can’t have access to the new animation package. It matches the values produced by AnimationCurve at a precision of 0.0001. Unfortunately, it still runs 10 times slower than the original code when using Burst. It doesn’t use any cache though so the memory impact is minimal and it can run using multiple jobs on the same data. I’ve also added the references I used at the top of the code if you want to dive in the maths^^

5608297–580435–AnimationCurveValueType.cs (7.06 KB)

5 Likes

Any idea on how to get a hold of the Unity.Animation package with the AnimationCurve struct? The api says to install via package manager but it isn’t showing up for me, despite have preview packages enabled.

add

"com.unity.animation": "0.3.0-preview.9",
"com.unity.dataflowgraph": "0.13.0-preview.3",

to your manifest.json file in Packages folder

2 Likes

How are you supposed to use and evaluate the DOTS Animation Curve properly? I think I’m using it right, but evaluating the curve results in some very odd oddness.

public struct CarPhysics : IComponentData
{
  public Unity.Animation.AnimationCurve forceCurve;
}

public class CarPhysicsAuthoring : MonoBehaviour, IConvertGameObjectToEntity
{
  public AnimationCurve forceCurve = AnimationCurve.Linear(0.0f, 1.0f, 1.0f, 0.0f);

  public void Convert(Entity entity, EntityManager dstManager, GameObjectConversionSystem conversionSystem)
  {
    var physComponent = new CarPhysics
    {
      forceCurve = Unity.Animation.Hybrid.CurveConversion.ToDotsAnimationCurve(forceCurve)
    };
    dstManager.AddComponentData(entity, physComponent);
  }
}

public class CarPhysicsSystem : ComponentSystem
{
  protected override void OnUpdate()
  {
    Entities.ForEach((ref CarPhysics car) =>
    {
      // Here it only outputs the value of the first keyframe or some weird value that's on the curve somewhere
      // I have no control on which condition it chooses per update
      float curveValue = Unity.Animation.AnimationCurveEvaluator.Evaluate(0.1f, ref car.forceCurve);
      Debug.Log(curveValue);
    });
  }
}

It only outputs the value of the first keyframe or some weird value that’s on the curve somewhere. I have no control on which condition it chooses per update.

In the Entity Debugger, the field forceCurve has no information associated with it. There isn’t any blobs or anything associated with the DOTS curve showing up for the entity.

I’m using the most recent versions of both the Animation package, and Entities. If this is a bug, I have no idea how to report it to the folks at Unity.

Have you tried using BlobAssetReference instead of Unity.Animation.AnimationCurve ?

BlobAssetReference works perfectly! Thank you!

What even is the difference between the “DOTS” Animation Curve (Unity.Animation.AnimationCurve) and the BlobAssetReference Animation Curve in this package? Is the DOTS version supposed to have better caching abilities or something?

    float EvaluateAnimationCurve(NativeArray<Keyframe> curve, float t)
    {
        float value = 0;

        for (int i = 0; i < curve.Length; i++)
        {
            int next = math.clamp(i + 1, 0, curve.Length - 1);
            Keyframe start = curve[i];
            Keyframe end = curve[next];

            int minCheck = math.select(0, 1, t > start.time);
            int maxCheck = math.select(0, 1, t <= end.time);
            int check = minCheck * maxCheck;

            float distanceTime = end.time - start.time;

            float m0 = start.outTangent * distanceTime;
            float m1 = end.inTangent * distanceTime;

            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;

            value += (a * start.value + b * m0 + c * m1 + d * end.value) * check;
        }

        return value;
    }

I am using this somewhat naive implementation of my own in bursted jobs. Takes in NativeArray of Keyframe that can be obtained directly from AnimationCurve. plain keyframe evaluation with no wrap mode support.

1 Like

From what I can see, the new AnimationCurveBlob ignores weighting? Anyone know if that’s expected in the future? @Djadjouka that class you put together is really impressive. I’m a bit surprised remapping time (or however you want to conceptualise it) for weighted points requires relatively complex recursive methods. Are there any resources (other than those in your file) that you came across about this? I had a bit of success using something like (Math for Game Developers: Parameterised Easing | by Jeremy Rose | Medium) but of course you end up with a discontinuity if points have different weights and I haven’t yet found a decent strategy for lerping between them.
From my understanding a weighted handle is exactly the same as the 2d location of a normal bezier handle - I think the bit that makes this all so tricky is re-parameterising all of this in terms of t.
@Djadjouka would you be willing to let me utilise and distribute part of that source code within a paid Unity store asset?
Also if anyone has any links to further relevant resources that would be much appreciated.

(currently trying to digest Wayback Machine & ‘inversion’)

Hey Timboc, it’s @Djadjouka . Thanks, I agree it could probably be optimized much more. I suggest you try caching some T values if memory is not a problem. That way you just have to do the last Bézier evaluation. It didn’t work really well for me but maybe you will have a better luck depending on your requirements. This reference I included is definitely the most complete one I have found: A Primer on Bézier Curves. I didn’t read everything and I’m not great at mathematics so there’s probably some connections I didn’t make between all the formulas. As far as I know though, there’s probably no magic way of getting rid of the Newton method. And yes you are right that the weighted handles are just the two control points in the cubic Bézier curve.

About performance, my post wasn’t completely right as it seems that starting the job when testing was an order of magnitude higher than my test so quite a large overhead. It also included some compilation for Burst so the job needs to be warmed up to have a proper benchmark. Of course, you probably won’t do just an animation curve evaluation in a job so the overhead would probably be compensated by other calculations as well. When I used it in a jobified animation system, it produced results 4 times faster than the legacy animation system from Unity when using 4 cores. I can’t provide the code but I suggest you try to integrate the code with Burst for your final purpose if you can. I can’t say for sure because I don’t know what your requirements are but I’m pretty sure the current performance should be enough.

And yes, you’re welcome to use the code or anyone else from the community (even for commercial purposes). Thanks for asking though!

1 Like

Many thanks @Soulai . After some more research I think I’m finally on to what I should be doing and the terminology I need. It might be that the Newton iterative method is faster but for now I have a preference for a more constant cost. I think what I need to be doing is solving for the roots of the bezier. For anyone else looking to do this in the future, these are absolutely invaluable:
https://stackoverflow.com/questions/51879836/cubic-bezier-curves-get-y-for-given-x-special-case-where-x-of-control-points

1 Like

The internal Unity method uses a cache of the “time parameter → segment”, to remember where the last time request you made occurred, within the curve.

So if you say:
curve.Evaluate(time);
Then the code searches the whole keyframe array looking for the correct segment.
But if you then call it again:
curve.Evaluate(time+0.001f);
The code will first check the segment from the last query. The idea being that you generally sample curves coherently, i.e. linearly throughout time, etc. So you can take advantage of that. Most of the time, the segment you want will be the same as last time, or adjacent.

Just remember, if you try to support this with your custom solution, that the cache must be per-thread, and not associated directly with the curve data. (unless you have an array of them and use NativeSetTheadIndex to access the correct cache.

This is all you really need to store:
struct Cache
{
int segmentIndex;
}
Just one int. To tell the Evaluate function which segment to check first, instead of checking them all linearly, or using a binary search (I didn’t read your code - hopefully it’s not linear anyway) :wink:

If the time isn’t within the cached segment, you can choose whether to look at an adjacent segment next, (depending on whether time < or > the cached segment, or just revert to a binary search of the segments lower/higher than the cached index, based on which direction you need to search.

(When I say “segment”, I mean the interval between 2 keyframes.)

Hope it helps! (and hope it makes sense!)

11 Likes

Is there now a better solution to use a UnityEngine.AnimationCurve under DOTS?
I saw this one A Fast BlobCurve but not sure how to use it.

Unity.Animation has a converter but as I am under Unity 2021 this package is incompatible and not supported.
So I manually imported Unity.Animation.Curves & Unity.Animation.Curves.Hybrid, which allows me to use AnimationCurveBlob (https://docs.unity3d.com/Packages/com.unity.animation@0.9/api/Unity.Animation.AnimationCurveBlob.html).
Unfortunately, it’s useless since the wrap mode (none, ping-pong, clamp) is not taken into account.

I use AnimationCurves in conjunction with Havok and CalculateVelocityToTarget.
For the 20 or so times I need to do this on a level, I’m close to telling myself that I’m going to keep using ISharedComponentData and do it on the Main Thread. I have some difficulty interpreting the impact of sync point in this case.