8

Is there a build-in way how to get a time by value from Animation curve in Unity3d? (The opposite way of Evaluate)

I need to achieve this (instead of getting value from time):

float time = AnimationCurve.Evaluate(float value);

Generically speaking getting X value from Y value.

Nisse Engström
  • 4,738
  • 23
  • 27
  • 42
David Horák
  • 5,535
  • 10
  • 53
  • 77

6 Answers6

15

I know this is 3 years old, but I found via a Google search, and in case someone else lands here:

I simply create an inverse curve, which allows me to look up by time.

public AnimationCurve speedCurve;

private AnimationCurve inverseSpeedCurve;

private void Start()
{
    //create inverse speedcurve
    inverseSpeedCurve = new AnimationCurve();
    for (int i = 0; i < speedCurve.length; i++)
    {
        Keyframe inverseKey = new Keyframe(speedCurve.keys[i].value, speedCurve.keys[i].time);
        inverseSpeedCurve.AddKey(inverseKey);
    }
}
user2330450
  • 151
  • 1
  • 2
  • By far the best answer. Thanks for taking the time to post this on such an old question. – David Foster Dec 03 '19 at 18:48
  • 1
    EDIT on above comment (can't edit after five minutes)... After implementing this, I'll reduce the intensity of my endorsement for this answer. For this to work in all cases, more work will need to be done to invert tangents, else the resulting curve can look very different. I'll be working on this now and will post back here if I have something useful. – David Foster Dec 03 '19 at 19:09
  • 1
    I've found that if you're using tangents in pretty much any useful or interesting way, you're going to have a bad time inverting curves. You'll just (possibly) never end up with the correct result. Please see this image for a demonstration and explanation: https://imgur.com/a/4QdhZJ6 – David Foster Dec 04 '19 at 15:14
4

Just a basic implementation maybe it will give an idea for you. Method loops through all time and if your value is near that value at that time it will yield. It's a coroutine but you can change it to use inside Update maybe?

public AnimationCurve curve;
public float valToTime = .5f;
public float treshold = .005f;
public float yourTime;
IEnumerator valueToTime(float determineTime)
{
    float timeCounter = 0;
    Keyframe[] k = curve.keys;
    float endTime = k[k.Length-1].time;
    Debug.Log("end "+endTime);
    while(timeCounter < endTime)
    {
        float val = curve.Evaluate(timeCounter);
        Debug.Log("val "+ val + "  time "+timeCounter);
        // have to find a better solution for treshold sometimes it misses(use Update?)!
        if(Mathf.Abs(val - determineTime) < treshold)
        {
            //Your time would be this
            yourTime = timeCounter;
            yield break;
        }
        else 
        {
            //If it's -1 than a problem occured, try changing treshold
            yourTime = -1f;
        }
        timeCounter += Time.deltaTime;
        yield return null;
    }
}
nexx
  • 579
  • 1
  • 7
  • 17
  • As far this is the best solution. Performance won't be the best but I my case, I need it only in some specific situation. Thx for the idea. – David Horák Aug 30 '14 at 14:21
4

Putting together the best elements of most of the solutions posted, I've come up with an approach that produces pretty high accuracy. It involves doing the work upfront and so, is also quite efficient.

Note: If the original curve possesses any maximum/minimum point (points on the curve with a gradient of zero) this method will still attempt to invert it but can only do so by introducing several discontinuities to the inverted curve. It is not ideal for such cases.

  1. Evaluate the original curve at several "sample-points" using a "sample-delta" constant.
  2. For each "value" evaluated, compute the tangent at that point as the "sample-delta" / "value-delta".
  3. Create keyframes that use the "value" as the "time" and the "sample-point" as the "value", and set the "inTangent" and "outTangent" to the tangent obtained in Step 3.
  4. Add the keyframe generated at every "sample-point" to a new AnimationCurve().
  5. The new AnimationCurve() is therefore an inverted version of the original.
  6. Smooth the tangents of the new AnimationCurve() (the inverted version) to remove discontinuities caused by sudden and rapid tangent changes. NB: Smoothing the tangents may make the inverted curve lose it's general definition if the original curve had at least one maximum/minimum point.

Image of Normal Curve vs Inverted Curve:

Image of Normal Curve vs Inverted Curve

invertedCurve = new AnimationCurve();

float totalTime = normalCurve.keys[normalCurve.length - 1].time;
float sampleX = 0; //The "sample-point"
float deltaX = 0.01f; //The "sample-delta"
float lastY = normalCurve.Evaluate(sampleX);
while (sampleX < totalTime)
{
    float y = normalCurve.Evaluate(sampleX); //The "value"
    float deltaY = y - lastY; //The "value-delta"
    float tangent = deltaX / deltaY;
    Keyframe invertedKey = new Keyframe(y, sampleX, tangent, tangent);
    invertedCurve.AddKey(invertedKey);

    sampleX += deltaX;
    lastY = y;
}
for(int i = 0; i < invertedCurve.length; i++)
{
    invertedCurve.SmoothTangents(i, 0.1f);
}
Timi Tayo
  • 41
  • 3
3

I needed this very thing just now, so I came up with this. I found it quite accurate and fast (an accuracy value of 10 was enough, and even lower may have done). But it will only work on curves that have ONE definite time for each value (i.e. nothing like waves with multiple times having the same value).

Similar to the other answer, it iterates through possible times - but rather than in a linear fashion the step value starts as the entire time range and halves each time.

Hope it's useful for you.

// NB. Will only work for curves with one definite time for each value
public float GetCurveTimeForValue( AnimationCurve curveToCheck, float value, int accuracy ) {

    float startTime = curveToCheck.keys [0].time;
    float endTime = curveToCheck.keys [curveToCheck.length - 1].time;
    float nearestTime = startTime;
    float step = endTime - startTime;

    for (int i = 0; i < accuracy; i++) {

        float valueAtNearestTime = curveToCheck.Evaluate (nearestTime);
        float distanceToValueAtNearestTime = Mathf.Abs (value - valueAtNearestTime);

        float timeToCompare = nearestTime + step;
        float valueAtTimeToCompare = curveToCheck.Evaluate (timeToCompare);
        float distanceToValueAtTimeToCompare = Mathf.Abs (value - valueAtTimeToCompare);

        if (distanceToValueAtTimeToCompare < distanceToValueAtNearestTime) {
            nearestTime = timeToCompare;
            valueAtNearestTime = valueAtTimeToCompare;
        }
        step = Mathf.Abs(step * 0.5f) * Mathf.Sign(value-valueAtNearestTime);
    }

    return nearestTime;
}
Robin King
  • 367
  • 1
  • 3
  • 10
  • The binary search approach is smart and makes this O(log n). It's a better answer than the accepted answer that is O(n). Thanks for sharing ~two years after the answer was already accepted. – David Foster Dec 04 '19 at 15:31
  • I made a unit test for this and unfortunately it fails to return correct values for a simple curve like the following `AnimationCurve.EaseInOut(-50, 500, 0, -500)`. Though it appears to work for a curve like `AnimationCurve.EaseInOut(0, 0, 1, 1)` – jwinn Mar 01 '23 at 15:58
  • It’s a while since I looked at this but from first glance, it seems your unit test fails as my code assumes the end value is higher than the start value i.e. the curve goes up over time. Should be fixed with one extra int for direction of curve that’s set at -1 or 1, then use that when determining where to step to at the end of the for…next loop. Good catch :) – Robin King Mar 01 '23 at 19:33
3

just stumbled upon this problem myself and didn't like the solutions mentioned here, so i wanted to share my own. It's rather an adaption to the answer which inverts the keyframes. I improved it by also inverting the tangents and the weight of the points. I'm sure there is an easier way, but i found this working nicely for reversing the animationcurve.

Edit: Forgot to mention, for me it only worked when the tangents are set to weighted, i don't know what weight calculation unity does when you set it to auto or similar, so weighted was predicatable and easy to inverse.

            inverseCurve = new AnimationCurve();
        for (int i = 0; i < initialCurve.length; i++)
        {
            float inWeight = (initialCurve.keys[i].inTangent * initialCurve.keys[i].inWeight) / 1;
            
            float outWeight = (initialCurve.keys[i].outTangent * initialCurve.keys[i].outWeight) / 1;
            
            Keyframe inverseKey = new Keyframe(initialCurve.keys[i].value, initialCurve.keys[i].time, 1/initialCurve.keys[i].inTangent, 1/initialCurve.keys[i].outTangent, inWeight, outWeight);
            
            inverseCurve.AddKey(inverseKey);
        }
  • Why do you divide by `/ 1` at the end of the computation for `inWeight` rsp. `outWeight`? Both products are floats, the type is a float - doesn't that division do nothing at all? – IARI Oct 16 '21 at 09:01
  • 1
    Yeah they do nothing, i think they where just a mental thing I added as I was looking at the internal docs. Can't say for sure though what I thought there, been quite a while. – Lukas Leder Oct 17 '21 at 15:54
1

Thought I'd share my own version, as suggested in other forums too I tried looping over Evaluate() instead of reversing the whole curve which I think is overkill and not always feasible.

This checks for a value approximation down to the indicated decimals, it also assumes that the curve has "normalized" time (if it wasn't the case this could be expanded by looking for the smallest and the biggest time keys.

/// <summary>
/// Inverse of Evaluate()
/// </summary>
/// <param name="curve">normalized AnimationCurve (time goes from 0 to 1)</param>
/// <param name="value">value to search</param>
/// <returns>time at which we have the closest value not exceeding it</returns>
public static float EvaluateTime(this AnimationCurve curve, float value, int decimals = 6) {
    // Retrieve the closest decimal and then go down
    float time = 0.1f;
    float step = 0.1f;
    float evaluate = curve.Evaluate(time);
    while(decimals > 0) {
        // Loop until we pass our value
        while(evaluate < value) {
            time += step;
            evaluate = curve.Evaluate(time);
        }

        // Go one step back and increase precision of the step by one decimal
        time -= step;
        evaluate = curve.Evaluate(time);
        step /= 10f;
        decimals--;
    }

    return time;
}
Xriuk
  • 429
  • 5
  • 16