Search Unity

Need way to evaluate AnimationCurve in the job

Discussion in 'Data Oriented Technology Stack' started by 5argon, May 19, 2018.

  1. 5argon

    5argon

    Joined:
    Jun 10, 2013
    Posts:
    1,261
    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 :

    Code (CSharp):
    1. Evaluate can only be called from the main thread.
    2. Constructors and field initializers will be executed from the loading thread when loading a scene.
    3. Don't use this function in the constructor or field initializers, instead move initialization code to the Awake or Start function.
    4. UnityEngine.AnimationCurve:Evaluate(AnimationCurve, Single)
    5. FormationSt:LerpFormation(FormationSt, FormationSt, Single, Boolean) (at Assets/Scripts/Gameplay/Gameplay/FormationSt.cs:139)
    6. Job:Execute() (at Assets/Scripts/Gameplay/Gameplay/System/GameplayLayoutSystem.cs:207)
    7. Unity.Jobs.JobStruct`1:Execute(Job&, IntPtr, IntPtr, JobRanges&, Int32) (at /Users/builduser/buildslave/unity/build/Runtime/Jobs/Managed/IJob.cs:30)
    8. Unity.Jobs.JobHandle:ScheduleBatchedJobsAndComplete(JobHandle&)
    9. 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;

    Screenshot 2018-05-19 17.51.35.png

    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)
     
    Last edited: May 19, 2018
  2. LennartJohansen

    LennartJohansen

    Joined:
    Dec 1, 2014
    Posts:
    2,274
    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.

    Code (csharp):
    1.  
    2. public static class AnimationCurveExtention
    3.     {
    4.         public static float[] GenerateCurveArray(this AnimationCurve self)
    5.         {
    6.             float[] returnArray = new float[256];
    7.             for (int j = 0; j <= 255; j++)
    8.             {
    9.                 returnArray[j] = self.Evaluate(j / 256f);            
    10.             }              
    11.             return returnArray;
    12.         }
    13.     }
    14.  
     
  3. SubPixelPerfect

    SubPixelPerfect

    Joined:
    Oct 14, 2015
    Posts:
    180
    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
     
    rigidbuddy and Enrico-Monese like this.
  4. 5argon

    5argon

    Joined:
    Jun 10, 2013
    Posts:
    1,261
    That's a nice approach! Thank you!
     
  5. snacktime

    snacktime

    Joined:
    Apr 15, 2013
    Posts:
    2,316
    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.

    Code (csharp):
    1.  
    2. public float Evaluate(float t, Keyframe keyframe0, Keyframe keyframe1)
    3.         {
    4.             float dt = keyframe1.time - keyframe0.time;
    5.  
    6.             float m0 = keyframe0.outTangent * dt;
    7.             float m1 = keyframe1.inTangent * dt;
    8.  
    9.             float t2 = t * t;
    10.             float t3 = t2 * t;
    11.  
    12.             float a = 2 * t3 - 3 * t2 + 1;
    13.             float b = t3 - 2 * t2 + t;
    14.             float c = t3 - t2;
    15.             float d = -2 * t3 + 3 * t2;
    16.  
    17.             return a * keyframe0.value + b * m0 + c * m1 + d * keyframe1.value;
    18.         }
    19.  
     
  6. 5argon

    5argon

    Joined:
    Jun 10, 2013
    Posts:
    1,261
    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.

    Code (CSharp):
    1. using UnityEngine;
    2. using Unity.Mathematics;
    3. using Unity.Collections;
    4.  
    5. public struct SampledAnimationCurve : System.IDisposable
    6. {
    7.     NativeArray<float> sampledFloat;
    8.     /// <param name="samples">Must be 2 or higher</param>
    9.     public SampledAnimationCurve(AnimationCurve ac, int samples)
    10.     {
    11.         sampledFloat = new NativeArray<float>(samples, Allocator.Persistent);
    12.         float timeFrom = ac.keys[0].time;
    13.         float timeTo = ac.keys[ac.keys.Length - 1].time;
    14.         float timeStep = (timeTo - timeFrom) / (samples - 1);
    15.  
    16.         for (int i = 0; i < samples; i++)
    17.         {
    18.             sampledFloat[i] = ac.Evaluate(timeFrom + (i * timeStep));
    19.         }
    20.     }
    21.  
    22.     public void Dispose()
    23.     {
    24.         sampledFloat.Dispose();
    25.     }
    26.  
    27.     /// <param name="time">Must be from 0 to 1</param>
    28.     public float EvaluateLerp(float time)
    29.     {
    30.         int len = sampledFloat.Length - 1;
    31.         float clamp01 = time < 0 ? 0 : (time > 1 ? 1 : time);
    32.         float floatIndex = (clamp01 * len);
    33.         int floorIndex = (int)math.floor(floatIndex);
    34.         if (floorIndex == len)
    35.         {
    36.             return sampledFloat[len];
    37.         }
    38.  
    39.         float lowerValue = sampledFloat[floorIndex];
    40.         float higherValue = sampledFloat[floorIndex + 1];
    41.         return math.lerp(lowerValue, higherValue, math.frac(floatIndex));
    42.     }
    43. }
     
    Last edited: May 21, 2018
  7. 5argon

    5argon

    Joined:
    Jun 10, 2013
    Posts:
    1,261
    In case someone stumbled upon this in the future, I have found that Unity's `Evaluate` is exactly Cubic Hermite Spline function described in https://en.wikipedia.org/wiki/Cubic_Hermite_spline, 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)

    Screenshot 2019-05-01 23.17.40.png

    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

    Screenshot 2019-05-01 23.14.39.png

    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.

    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".
     
    Last edited: May 2, 2019
  8. 5argon

    5argon

    Joined:
    Jun 10, 2013
    Posts:
    1,261
    Update : I tried improving on the naive implementation (left) with matrix based using some methods from Mathematics (right).

    Screenshot 2019-05-02 18.05.50.png

    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)

    Screenshot 2019-05-02 17.35.15.png

    (Matrix)

    Screenshot 2019-05-02 18.04.40.png
     
    Last edited: May 2, 2019
  9. Joachim_Ante

    Joachim_Ante

    Unity Technologies

    Joined:
    Mar 16, 2005
    Posts:
    4,646
    I'd recommend using BlobData for this instead of NativeArray. It makes it so you can easily reference it from an IComponentData.
     
  10. illinar

    illinar

    Joined:
    Apr 6, 2011
    Posts:
    557
    Are there any examples of BlobData usage?
     
  11. 5argon

    5argon

    Joined:
    Jun 10, 2013
    Posts:
    1,261
    You could copy what BlobificationTests.cs is doing to get started.
     
    illinar likes this.
  12. snusfen

    snusfen

    Joined:
    May 20, 2015
    Posts:
    42
    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<KeyFrameBufferElement>. That way we could simply convert the Animationcurve to a dynamic buffer on the entity that needed it.

    Code (CSharp):
    1. public struct AnimationCurveKeyframe : IBufferElementData
    2. {
    3.     public Keyframe keyFrame;
    4. }
    Populate a "keyframe buffer"
    Code (CSharp):
    1.  
    2. DynamicBuffer<AnimationCurveKeyframe> curveBuffer = dstManager.AddBuffer<AnimationCurveKeyframe>(entity);
    3. Keyframe[] keys = animationCurve.keys;
    4. for(int k = 0; k < keys.Length; ++k)
    5. {
    6.     Keyframe key = keys[k];
    7.     curveBuffer.Add(new AnimationCurveKeyframe
    8.     {
    9.         keyFrame = key
    10.     });
    11. }
    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

    Code (CSharp):
    1. public struct KeyframeBlobArray
    2. {
    3.     public BlobArray<Keyframe> keys;
    4. }
    5.  
    6. public struct FooComponent : IComponentData
    7. {
    8.     public BlobAssetReference<KeyframeBlobArray> animationCurve;
    9. }
    10.  
    11. public static unsafe BlobAssetReference<KeyframeBlobArray> ConstructKeyframeBlob(Keyframe[] keyframes)
    12.     {
    13.         BlobAllocator allocator = new BlobAllocator(-1);
    14.         ref var root = ref allocator.ConstructRoot<KeyframeBlobArray>();
    15.  
    16.         allocator.Allocate(keyframes.Length, ref root.keys);
    17.  
    18.         for(int i = 0; i < keyframes.Length; ++i)
    19.         {
    20.             Keyframe k = keyframes[i];
    21.             root.keys[i] = k;
    22.         }
    23.  
    24.         BlobAssetReference<KeyframeBlobArray> keyframeBlob = allocator.CreateBlobAssetReference<KeyframeBlobArray>(Allocator.Persistent);
    25.         allocator.Dispose();
    26.  
    27.         return keyframeBlob;
    28.     }
    29.  

    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?
     
    Last edited: May 13, 2019
    Creepgin likes this.
  13. Joachim_Ante

    Joachim_Ante

    Unity Technologies

    Joined:
    Mar 16, 2005
    Posts:
    4,646
    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.
     
    psuong, snusfen, eizenhorn and 2 others like this.
  14. snusfen

    snusfen

    Joined:
    May 20, 2015
    Posts:
    42
    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
     
    psuong likes this.
  15. Joachim_Ante

    Joachim_Ante

    Unity Technologies

    Joined:
    Mar 16, 2005
    Posts:
    4,646
    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.
     
    Kender, siggigg, psuong and 2 others like this.
  16. FROS7

    FROS7

    Joined:
    Apr 8, 2015
    Posts:
    26
    Is the name unfortunate enough for Shared Component Data get a rename?
     
    Kender, illinar and siggigg like this.
  17. ReadyPlayGames

    ReadyPlayGames

    Joined:
    Jan 24, 2015
    Posts:
    41
    I've spent a long time trying to get Shared Component Data to "work" because I was using it incorrectly based on the name.
     
    Kender, Shinyclef and FROS7 like this.
  18. illinar

    illinar

    Joined:
    Apr 6, 2011
    Posts:
    557
    Maybe ChunkComponentData?

    @5argon Could you please share your Blob based curve implementation if you've done it?
     
    Kender and siggigg like this.
  19. 5argon

    5argon

    Joined:
    Jun 10, 2013
    Posts:
    1,261
    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.
     
    illinar and recursive like this.
  20. snacktime

    snacktime

    Joined:
    Apr 15, 2013
    Posts:
    2,316
    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.