Search Unity

  1. If you have experience with import & exporting custom (.unitypackage) packages, please help complete a survey (open until May 15, 2024).
    Dismiss Notice
  2. Unity 6 Preview is now available. To find out what's new, have a look at our Unity 6 Preview blog post.
    Dismiss Notice

[SOLVED] How to best animate (change) an AnimationCurve in code

Discussion in 'General Discussion' started by _watcher_, Apr 5, 2020.

  1. _watcher_

    _watcher_

    Joined:
    Nov 7, 2014
    Posts:
    261
    Let me explain my issue in detail, for easier viisualization. It is assumed you are familiar with the notion of reference types vs value types and their (im)mutability.

    The situation is this:
    There is a TrailRenderer
    This TrailRenderer references an AnimationCurve called widthCurve
    This AnimationCurve then references a built-in array, called keys (Keyframe[])
    Each key (type Keyframe) references a property of type float called time

    What i intend to do, is 'animate' the AnimationCurve referenced by the TrailRenderer, by changing times of all its keyFrames (lets say i need to shift them by -0.01f each frame - for simplicity), to achieve a sort-of 'phased effect'. I hope that statement made sense, because that's where id like your help - how to adjust the (let's call it) 'phase' of the existing AnimationCurve.

    NOTE that:
    AnimationCurve is a class type
    AnimationCurve->keys is built-in array of Keyframe[]
    Keyframe is a struct
    Keyframe->time is a public float (autoproperty)

    TESTS
    When i try to change AnimationCurve->keys->time
    > the value is not saved in the Keyframe of the AnimationCurve's keys.
    > this makes sense to me, since Keyframe is a struct and i am changing newly copied value, not referenced value.

    When i try to create new Keyframe, change its time to a new value, and set it back into the keys array, the time value (keyFrame value) is not saved, because aparently, built-in array referencing value types (struct Keyframe in this case) returns them as values, not references. So again, same issue. All right, one way up we go..

    When i create the whole Keyframes[] array, fill it with proper data (changed time for each Keyframe), and set it back into the original AnimationCurve->keys, the time values are not saved. Now this is a question mark for me (?) for me. Why would a referenced type like Keyframe[] stored inside a reference type like AnimationCurve (class instance) not persist the changes i am setting?

    QUESTIONmark
    It seems that the only way to 'animate' AnimationCurve' over time, is to (hold it) --- create a whole new AnimationCurve, each frame (this could be cached instance i presume) and set it to the target animationCurve (in my case that is trailRend.widthCurve). Is this expected? This seems like a huge deal to create animation curves each frame just to set the time properties of their keyframes!

    The bottom line is, i am simply trying to create a 'phased animation', where the animation curve 'moves' a little bit from left to right (right to left), each frame. And for that i need to create a whole new AnimationCurve with the whole array of keyFrames each frame? Any other cleaner option to achieve the same effect?

    Thank you (and sorry for the wall of text)!
     
    Last edited: Apr 6, 2020
  2. neginfinity

    neginfinity

    Joined:
    Jan 27, 2013
    Posts:
    13,604
    You could try to create two curves and cycle through them, while animating. I.e. alter curve #0, then assign. Next frame alter curve #1, assign. The frame after that alter curve #0, assign.
     
  3. _watcher_

    _watcher_

    Joined:
    Nov 7, 2014
    Posts:
    261
    Well i find it exciting how your mind works similar to mine, as i did just that prior. You see, i have 10 custom curves made just for this, available through an inspector array, and i replace them in the original which works 'ok' (it really is not user-friendly to have to 'adjust' 10 curves) - well it works well for higher animation speeds, smooth-like. But there is also an animation speed parameter, that needs to change.. which has effect on how fast the widthCurve appears to animate (it really is frame-by-frame animation at this point). Now when the animation speed gets slower, eg 1 'animationCurve change per 10 frames' slower - its noticeably choppy, thats why i need this to be programmatic (i really dont want to have 100 animation curves to modify via inspector). Programmatically i can then also go really wild, like morphing the amplitude, phase, tangents in selective keyFrames of all AnimationCurves, which im really looking forward to.

    I found there is this function in AnimationCurve, which should simplify what im trying to do?
    AnimationCurve.MoveKey(keyIndex, newKey)
    which, it appears combines functionality of AnimationCurve.RemoveKey and AnimationCurve.AddKey
    but it has no effect, when i set a key (that i manufacture, with phased time value) in that way. The widthCurve.keys[keyIndex] does not change to the newly supplied key. How is it supposed to work is a mystery to me.

    I apprecciate the help, at the end this is more of a 'best practice' question. Creating new AnimationCurve each frame, then working on it, then assigning it works fine, just a bit of a performance hit, nothing major though.

    EDIT: it appears i misunderstood what you ment by having 2 cached curves and adjusting them intermittently (so much for our minds working similarly XD). How is that better than having just one? But honestly i'm not sure i could event adjust the keys of my own created curve, for the same reason i cant change keys of TrailRenderer->widthCurve (the whole structure seems to be made of immutable types).
     
    Last edited: Apr 5, 2020
  4. neginfinity

    neginfinity

    Joined:
    Jan 27, 2013
    Posts:
    13,604
    It appears based on your description that Unity employs some sort of blackbox mumbojumbo to apply the curve. And the effect only triggers when you make a NEW curve from scratch, and does not trigger when you reuse existing curve.

    It is unknown what that "hidden mumbo-jumbo" is, but since it needs different instance each time to work, a reasonable idea is to use two instances and cycle them. This way, each time you'll update the curve the mechanism will receive a "new" curve (meaning a different instance compared to previous one), and that, in theory, could satisfy "hidden mumbo-jumbo" component that fails to work when you reuse the curve. Cycling few existing curves also will not produce additional garbage, which is a good thing.
     
    _watcher_ and angrypenguin like this.
  5. angrypenguin

    angrypenguin

    Joined:
    Dec 29, 2011
    Posts:
    15,632
    The idea isn't to pre-make all of your potential animated states in advance. It's more like "double buffering", if you care to search for that concept. Show one item while you modify the other, then swap them. You can solve the smooth animation issue by mathematically figuring out the state of the back-buffer, rather than just using pre-made frames.

    All of that said... are you sure this is the best approach? If you're doing other stuff with your trail in your script then it does make sense to keep everything together. Otherwise, I would look at doing something with the vertex shader, personally.
     
  6. _watcher_

    _watcher_

    Joined:
    Nov 7, 2014
    Posts:
    261
    I thought this could be the case, but was hesitant to assume, so i asked. Thanks for clarifying.

    Actually i can have a cached AnimationCurve that i've created (just one is enough), and set its .keys each frame, then set it to TrailRenderer.widthCurve, this works fine. What i can not do is set .keys (or anything below that) to the TrailRenderer's widthCurve - that does not update the widthCurve with the new keys i just set (its not like its read-only, it allows me to set it, just doesn't actually update the values). Anything i try to set directly to Trailrenderer.widthCurve (curve itself, keys, specific Keyframe, keyframe's time) does not update the widthCurve, only overwriting the whole widthCurve does. It is strange to me, because '.keys' is a built-in array type, which is a reference type not a value type, so why cant i directly set it? Anyways its a minute detail when i think of it, so its fine.

    I'm not, that's why i asked for suggestions. I am actually doing lots of other stuff with the curve at the same time. Also just doing it all with vertices sounds like the better approach, but my skills are limited there, but i also considered that option in the beginning. In fact there is a pretty awesome example in paper.js that does just that, all vector-based, and the code is pretty sizzling-mouth-watering-sexy. I just wanted to do something a bit different in Unity WebGL (haha nice joke! no, really).

    The 'create curve from scratch and set it' works well, i might test with having 2 and the 'double-buffering' approach as you guys suggested, thank you very much. Im marking the thread [SOLVED], i think we touched upon a few alt solutions already.
     
    Last edited: Apr 6, 2020
  7. neginfinity

    neginfinity

    Joined:
    Jan 27, 2013
    Posts:
    13,604
    Looking into documentation, it seems that widthCurve is a property and not a public variable. Meaning if you receive a curve via getter method, it can be a temporary class and not an internal reference to the curve ... OR that unless you assign the curve via setter the state of the renderer won't refresh. Either is a possibility.

    Trying to mess with fields of a class returned as a property used to trigger a compile error.

    Try this:
    Code (csharp):
    1.  
    2. var tmpCurve = trailRenderer.widthCurve;
    3. //Alter curve here.
    4. ....
    5. trailRenderer.widthCurve = tmpCurve;
    6.  
     
  8. _watcher_

    _watcher_

    Joined:
    Nov 7, 2014
    Posts:
    261
    It is what i'm doing, actually. Well not exactly, this is what i'm using:
    Code (csharp):
    1.  
    2. if (tmpKeys.Length != trailRend.widthCurve.keys.Length) {
    3.     tmpKeys = new Keyframe[trailRend.widthCurve.keys.Length];
    4. }
    5. for (int i=0; i<tmpKeys.Length; i++) {
    6.     // GetTimePhasedKey: rebuilds key with values from widthCurve.keys[i] and adjusts '.time'
    7.     // NOTE: must rebuild, cant just change .time, we'd be only changing a copy of the value.
    8.     Keyframe newKey = GetTimePhasedKey(trailRend.widthCurve.keys[i], animationSpeed * animationSpeedMult);
    9.     tmpKeys[i] = newKey;
    10. }
    11.  
    12. // set keys to the cached curve
    13. myAnimationCurve.keys = tmpKeys;
    14. // set the cached curve to live curve
    15. trailRend.widthCurve = myAnimationCurve;
    16.  
    I don't think i can simply copy widthCurve to myAnimationCurve, then adjust 'time's in AnimationCurve.keys, because time and Keyframe are not referenced and i only get their copies to modify. EDIT: Just tested this. Here are the results:
    Code (csharp):
    1. myAnimationCurve = trailRend.widthCurve;
    2. Debug.Log("[pre] "+myAnimationCurve.keys[1].time); // 0.1f
    3. myAnimationCurve.keys[1].time = 10f;
    4. Debug.Log("[post] "+myAnimationCurve.keys[1].time); // 0.1f
    5.  
    So as you can see, all the value-types (Keyframes with their values, and keys array) need to be completely reconstructed.

    Here is code examples of the TESTS i mentioned by word in the original post, in case someone might find them useful (also explaining whats happening step by step):

    Example1:
    Code (csharp):
    1. myAnimationCurve = trailRend.widthCurve;
    2.  
    3. Debug.Log("[pre] "+myAnimationCurve.keys[1].time); // 0.1f
    4. for (int i=0; i<myAnimationCurve.keys.Length; i++) {
    5.     myAnimationCurve.keys[i].time = 10f;
    6. }
    7. Debug.Log("[post] "+myAnimationCurve.keys[1].time); // 0.1f
    8.  
    9. trailRend.widthCurve = myAnimationCurve;
    10.  
    Result:
    Since keys.time is returned as value, and not reference to the value, no change is made to the original.

    Example2:
    Code (csharp):
    1. myAnimationCurve = trailRend.widthCurve;
    2.  
    3. // float time,float value,float inTangent,float outTangent,float inWeight,float outWeight
    4. Keyframe myKey = new Keyframe(10f, 1f, 0, 0, 0, 0); // time of Framekey is set to 10f
    5.  
    6. Debug.Log("[pre] "+myAnimationCurve.keys[1].time); // 0.1f
    7. myAnimationCurve.keys[1] = myKey;
    8. Debug.Log("[post] "+myAnimationCurve.keys[1].time); // 0.1f
    9.  
    10. trailRend.widthCurve = myAnimationCurve;
    11.  
    Result:
    Built-in array referencing value types (struct Keyframe in this case) returns them as values, not references.

    Example3:
    Code (csharp):
    1. myAnimationCurve = trailRend.widthCurve;
    2.  
    3. Keyframe[] myKeys = new Keyframe[myAnimationCurve.keys.Length];
    4.  
    5. Debug.Log("[pre] "+myAnimationCurve.keys[1].time); // 0.1f
    6.  
    7. myKeys[1] = new Keyframe(10f, 1f, 0, 0, 0, 0); // time of Framekey is set to 10f
    8.  
    9. myAnimationCurve.keys = myKeys;
    10.  
    11. Debug.Log("[post] "+myAnimationCurve.keys[1].time); // 10f (Hurray!)
    12.  
    13. trailRend.widthCurve.keys = myKeys;
    14.  
    15. Debug.Log("[post2] "+trailRend.widthCurve.keys[1].time); // 0.1f (Too bad!)
    16.  
    17. trailRend.widthCurve = myAnimationCurve;
    18.  
    19. Debug.Log("[post3] "+trailRend.widthCurve.keys[1].time); // 10f (Hurray!)
    20.  
    Result:
    So as mentioned, i was surprised that i cant just set the .keys array of the original widthCurve (seems some 'mumbojumbo' not a reference type). but as you mentioned also, if i modify the working curve (myAnimationCurve) first and then set that, it works.
     
    Last edited: Apr 6, 2020
  9. neginfinity

    neginfinity

    Joined:
    Jan 27, 2013
    Posts:
    13,604
    Found the reason.
    It is in the docs:
    https://docs.unity3d.com/ScriptReference/AnimationCurve-keys.html
    You need to assign entire array at once for curve to update. When you access it directly you get a copy of internal array, and even if you update it, it will "poof" unless assigned to something.

    Basically watch out for public properties that pretend to be fields. This is one of them.

    Try:
    Code (csharp):
    1.  
    2. var tmpKeys = animCurve.keys;
    3. keys[1] = ....//
    4. animCurve.keys = tmpKeys;
    5.  
     
  10. _watcher_

    _watcher_

    Joined:
    Nov 7, 2014
    Posts:
    261
    Thanks i missed that. The first part of the sentence i can agree with. It is not only by getting value though, but also by setting value?
    See my code above:
    Code (csharp):
    1. trailRend.widthCurve.keys = myKeys;
    Which doesn't update the curve's value.
    Only setting the whole animCurve does.

    As shown above this does not work (?)
    Anyways unless i missed something from what you ment.. and you are welcome to paraphrase so i can understand (God knows im sometimes slow to understand).. but otherwise i think we've mentioned all the important permutations here already, and bless you let's close the topic!
     
    Last edited: Apr 7, 2020
  11. Deleted User

    Deleted User

    Guest

    I figured it out thanks to you mate, thanks.
    var keys = _lineRenderer.widthCurve.keys;
    keys[1].time = 1f;
    AnimationCurve curve = new AnimationCurve();
    curve.keys = keys;
    _lineRenderer.widthCurve = curve;