LERP multi points: A to B to C

I’m trying to create two lerps that go sequentially. I’ve done a lot of searching and there weren’t any good answers. I tried using things like dottween and iTween and have ended up with nothing.
I’m trying to move 2000 objects around. Essentially a timeline of data points from period 1 to period 2 to period 3. Using iTween destroyed my performance.

My initial method was create the first coroutine to go A to B. Then at the end of that coroutine, call my second one. The first one gets called in update. But i did debug statements and seems that they don’t get called sequentially.

Any help would be appreciated.

    float Lerp3(float a, float b, float c, float t)
    {
        if (t <= 0.5f)
        {
            return Mathf.Lerp(a, b, t * 2f);
        }
        else
        {
            return Mathf.Lerp(b * 2f, c, t);
        }
    }

Something like this? Untested.

2 Likes

Yes, something like that, but for a vector3 lerp, i was doing this:

        float distCovered = (Time.time - start_time) * lerp_speed;
        float fracJourney = distCovered / journey_length_1;

        transform.position = Vector3.Lerp (start_position, middle_position, fracJourney);

then i had the same thing but with middle_position and end_position and journey_length_2. The journey lengths are simply the vector 3 lengths.

In your example there is the same t values. What would i use for that?

t is a value between 0 and 1, which is common for any lerping library. 0 is A, 0.5 is B and 1 is C. Plug it in changing Mathf.Lerp to Vector3.Lerp and of course float ABC becomes Vector3 ABC.

Just a note, iTween is not the best choice. I would use Leantween or Dotween. Personally I use LeanTween for many things and it works great. There are different calls you can do. There are ways to create paths or at the end of a LeantTween call, you can do a OnComplete that has it do the next step when the tween is finished. Works super well.

        LeanTween.move(targetObject, someVector, .5f).setOnComplete(() =>
        {
           //Tween is complete, do something next.
        });

This is just a basic example of moving something over .5 secs and then when it’s done, it will do something else for example.

@Brathnann thanks, i will check it out.

@hippocoder ,
So if i need to go from point A to B, i can’t just use a discrete value. i need to keep incrementing it. So my first thought is, call the Lerp3 function in Update, and then increment t by .1. Is this the correct approach, so that it smoothly goes from a to b to c?

t += Time.deltaTime;
if (t>=0.5f) Debug.Log(“Reached B”);

Not sure what’s hard to understand, it behaves exactly like Unity’s Mathf.Lerp or Vector3.Lerp, except with 3 values instead of 2?

Okay, so i created my lerp3 function and called it in update. I have 2 problems.

One, it runs very fast. I want it to be slower and have control over the speed. Ideally it should run about 5 seconds.

The second problem is that while the animation is running it runs at about 2 fps. Probably because there’s 2000 objects and this function is being called on update in all of them. So how can i speed this up.

Coroutine’s would be faster than update. But without looking at the code/profiler it would be hard to tell where to optimise

Just slow down your time parameter. A typical way to do this is to multiply time by a speed float.

Don’t call Update on 2000 objects. Use a manager class instead, with an array of 2000 objects. And loop through the objects in an array.

If that doesn’t work, consider switching the whole thing to a particle effect.

2 Likes

To add to what @Kiwasi says about a manager. This great article that Unity put out https://blogs.unity3d.com/2015/12/23/1k-update-calls/ which describes calling updates vs using a manager.

1 Like

A few other general performance tips.

  • Cache the Transforms of each object for looping (the transform getter has some safe-checking that only really needs to be done once during the lerp animation)
  • Set localposition instead of position. (setting position causes unity to internally figure out the local position via traversing the ancestry of that object’s heirarchy)
  • multiply all your floats together before multiplying a vector with the float. A Vector*(floatfloatfloat) gives the same result as Vectorfloatfloat*float. But the first is 6 multiplications while the latter is 9 multiplications.
  • keep the hierarchy of those 2000 objects as simple as possible. if each object has one child then the loop is actually affecting 4000 transforms. if each object has 4 children thats 10000 transforms to update.
  • affecting the rotation will also multiply the number of calculations changing the local position and rotation of each object that has 4 children is actually 20000 updates
  • Unity 5.5 has plans to greatly improve the performance of updating the transforms by using a dirty transform concept.
  • and as BoredMormon said, use a manager. the less work that you don’t have unity do for you automatically, the faster your code is going to run. One Update message is far faster than 2000 Update messages

However, the most important thing to do off to bat to figure out this problem is.

Use the Profiler!

It’ll tell you exactly whats killing your fps.

2 Likes

Here is a rethink of what you are doing…

Create methods that are reusable later on…

(vs didnt say there were any errors, but I didnt run it.)

public Vector3 LerpOverDistance(Vector3[] vectors, float time){
	time = Mathf.Clamp01(time);
	if(vectors == null || vectors.Length == 0){
		throw(new Exception("Vectors input must have at least one value"));
	}
	if(vectors.Length == 1){
		return vectors[0];
	}
	
	if(time == 0){
		return vectors[0];
	}
	
	if(time == 1){
		return vectors[vectors.Length - 1];
	}
	
	float[] distances = new float[vectors.Length - 1];
	float total = 0;
	for(int i=0; i<vectors.Length; i++){
		distances[i] = (vectors[i] - vectors[i + 1]).sqrDistance;
		total += distances[i];
	}
	
	float current = total * time;
	int p = 0;
	while(current - distances[p] > 0){
		current -= distances[p++];
	}
	if(distances[p] == 0) return vectors[p];
	
	return Vector3.Lerp(vectors[p], vectors[p + 1], current / distances[p]);
}

public Vector3 LerpOverNumber(Vector3[] vectors, float time){
	time = Mathf.Clamp01(time);
	if(vectors == null || vectors.Length == 0){
		throw(new Exception("Vectors input must have at least one value"));
	}
	if(vectors.Length == 1){
		return vectors[0];
	}
	
	if(time == 0){
		return vectors[0];
	}
	
	if(time == 1){
		return vectors[vectors.Length - 1];
	}
	
	float t = time * vectors.Length;
	int p = (int)Mathf.floor(t);
	t -= p;
	return Vector3.Lerp(vectors[p], vectors[p + 1], t);
}
3 Likes

this worked for me but in the else statement you need to multiply “b” by 2. “return Mathf.Lerp(b*2, c, t)”

2 Likes

I hate to come and be the “Well, actually,” guy, but multiplying b by 2 is also wrong. You need to multiply t by the number of points - 1, and then subtract by the whatever number the leftmost point in the lerp is (so if you are lerping between points a & b, a is the 1st point and b is the 2nd, so you subtract 1).

A bad formula for this is Lerp(point1, point2, t * (numberOfPoints - 1) - leftPointNumber

So for 3 points and the 2nd lerp, it’s Mathf.Lerp(b, c, (t * 2f) - 1f);

Here’s the code for this too. Hopefully, this saves someone in the future from 3 hours of figuring it out.

    float Lerp3(float a, float b, float c, float t)
    {
        if (t <= 0.5f)
        {
            return Mathf.Lerp(a, b, t * 2f);
        }
        else
        {
            return Mathf.Lerp(b, c, (t * 2f) - 1f);
        }
    }
2 Likes

Hey ! Here is a version less visible but without condition (indispensable for shader) :

// t between -1 and 1
float Lerp3(float a, float b, float c, float t)
{
    return lerp(lerp(a, b, min(t, 0.0f) + 1.0f), c, max(t, 0.0f));
}

With t include in [-1, 1]. To convert t from [0, 1] to [-1, 1] apply:

// t between 0 and 1
float Lerp3(float a, float b, float c, float t)
{
    t = t * 2.0f - 1.0f;
    return lerp(lerp(a, b, min(t, 0.0f) + 1.0f), c, max(t, 0.0f));
}

I would also like to state a limitation of all the proposed formulas. If we consider the area under the curve that occupies a, b and c. So, a will occupy 25%, b 50% and c 25%. If anyone has a mathematical solution to this problem, I would be curious to know it.

2 Likes

-1 to 1 is super handy for dot product compares ^

It misses a -1 line 53, otherwise this is a great solution that work with any amount of points.

Corrected version :

public Vector3 LerpOverDistance(Vector3[] vectors, float time){
    time = Mathf.Clamp01(time);
    if(vectors == null || vectors.Length == 0){
        throw(new Exception("Vectors input must have at least one value"));
    }
    if(vectors.Length == 1){
        return vectors[0];
    }
  
    if(time == 0){
        return vectors[0];
    }
  
    if(time == 1){
        return vectors[vectors.Length - 1];
    }
  
    float[] distances = new float[vectors.Length - 1];
    float total = 0;
    for(int i=0; i<vectors.Length; i++){
        distances[i] = (vectors[i] - vectors[i + 1]).sqrDistance;
        total += distances[i];
    }
  
    float current = total * time;
    int p = 0;
    while(current - distances[p] > 0){
        current -= distances[p++];
    }
    if(distances[p] == 0) return vectors[p];
  
    return Vector3.Lerp(vectors[p], vectors[p + 1], current / distances[p]);
}

public Vector3 LerpOverNumber(Vector3[] vectors, float time){
    time = Mathf.Clamp01(time);
    if(vectors == null || vectors.Length == 0){
        throw(new Exception("Vectors input must have at least one value"));
    }
    if(vectors.Length == 1){
        return vectors[0];
    }
  
    if(time == 0){
        return vectors[0];
    }
  
    if(time == 1){
        return vectors[vectors.Length - 1];
    }
  
    float t = time * (vectors.Length - 1);
    int p = (int)Mathf.floor(t);
    t -= p;
    return Vector3.Lerp(vectors[p], vectors[p + 1], t);
}
1 Like

This isn’t terribly optimal. Not your fault per se, just wanted to add that if someone wants to call this in a loop, it’s better to turn this into a dedicated class and internally cache the distances. Also it should’ve been magnitude (instead of sqrDistance) on line 22.

Here’s an example

using System;
using UnityEngine;

public class MultiPointLerp {

  static readonly Vector3 _NaN => new(float.NaN, float.NaN, float.NaN);

  Vector3[] _pts; // points
  float[] _dist; // cached distances
  float? _total; // cached total distance

  public Vector3 this[int index] => _pts[index];
  public int Count => _pts.Length;

  public MultiPointLerp(params Vector3[] points) {
    if(points is null) throw new ArgumentException();
    _pts = new Vector3[points.Length];
    Array.Copy(points, 0, _pts, 0, points.Length);
  }

  public MultiPointLerp(Vector3[] points, int index, int count) {
    if(points is null) throw new ArgumentException();
    _pts = new Vector3[count];
    Array.Copy(points, index, _pts, 0, count);
  }

  public float TotalDistance { get {
    if(!_total.HasValue) _total = getTotalDistance();
    return _total.Value;
  } }

  public float GetDistance(int index) {
    _dist??= getDistances();
    return _dist[index];
  }

  public Vector3 GetDirection(int index) {
    if(index >= Count - 1) throw new ArgumentOutOfRangeException();
    var seg = GetSegment(index);
    return (seg.b - seg.a).normalized;
  }

  public (Vector3 a, Vector3 b) GetSegment(int index) {
    if(index >= Count - 1) throw new ArgumentOutOfRangeException();
    return ( this[index], this[index+1] );
  }

  public float GetInterpolant(float time, out int index) {
    index = 0;
    if(Count == 0) { index = -1; return 1f; }
    if(Count == 1 || time <= 0f) return this[0];
    if(time >= 1f) return this[Count - 1];
   
    var c = TotalDistance * time;
    while(c - _dist[index] > 0f) c -= _dist[index++];
    return c / _dist[index];
  }

  public Vector3 GetSegmentPoint(int index, float t)
    => (1f - t) * this[index] + t * this[index+1];

  public Vector3 GetPoint(float time) {
    var t = GetInterpolant(time, out int index);
    if(index < 0) return _NaN;
    return GetSegmentPoint(index, t);
  }

  float getTotalDistance() {
    _dist??= getDistances();
    var total = 0f;
    for(int i = 0; i < _dist.Length; i++)
      total += _dist[i];
    return total;
  }

  float[] getDistances() {
    var d = new float[Count - 1];
    for(int i = 0; i < Count - 1; i++)
      d[i] = (this[i+1] - this[i]).magnitude;
    return d;
  }

}

(haven’t tested it; edit: fixed some problems; edit: added some stuff etc)

1 Like

:slight_smile: I almost wanted to point this out as well. When doing a distance / length normalized lerp you almost always want to calculate and store that normalization data only once and reuse it. Once the lerp as started the positions in the array should not change anyways, otherwise you would get strange effects while lerping.

1 Like