- Home /
Constant total time over multiple Lerp functions
Hey there, I could use some math help.
I want to move a transform over path using Lerp and an array of Vector3, like a waypoint system. I have access to the distances between the waypoints and the total distance of the path.
How would I implement the Lerp function per waypoint so that the total time for the transform to move from the beginning to the end of the path would a fixed number? For example: I have 12 waypoints with various distances and I want the total time to be 3 seconds.
Before suggesting using MoveTowards or anything else than Lerp, I'm using the Lerp values for some other data too, so I want to keep using it.
Thanks in advance,
Daan
Answer by Bunny83 · Aug 06, 2018 at 11:18 AM
You just want to "normalize" the time for the whole path. Since you know the distances of the individual path segments you can calculate a weigthed fraction depending on the whole path.
In relation to the whole path (0 - 1) each segment only has a length of "segment length" / total length. Of course if you add up all those numbers in order you will get "1". You can either store the absolute normalized starting time of each segment as well as the normalized length, or just store the normalized length and calculate the accumulated length as you're searching for the current segment.
Imagine something like this:
public class Segment
{
public float nLength;
public Vector3 pos;
}
Here nLength is the normalized length as described above (so the segment length divided by the totel length). "pos" is just the target position. Usually when lerping along a path you could optimise the process be remembering the current used segment and the accumulated start time. If you want "random access" to the whole lerp range you would need to determine the right segment each time.
public static Vector3 PathLerp(List<Segment> aSegments, float aTime)
{
if (aSegments == null || aSegments.Count == 0)
throw new System.ArgumentException("segments not set","aSegments");
if (aSegments.Count == 1 || aTime <= 0f)
return aSegments[0].pos;
float acc = 0f;
float start = 0f;
int index = 0;
while(aTime > acc)
{
start = acc;
index++;
if (index >= aSegments.Count)
return aSegments[aSegments.Count - 1].pos;
acc += aSegments[index].nLength;
}
var seg1 = aSegments[index - 1];
var seg2 = aSegments[index];
return Vector3.Lerp(seg1.pos, seg2.pos, (aTime - start) / seg2.nLength);
}
Not that this is not tested. However the idea of the while loop is to determine the current target segment. Note that the first segment would just be the start position of your path and shouldn't have any "length". Since a path with n points only has "n-1" segments between those points. "aTime" is just the normalized lerp time for the whole path. "start" is the calculated starting time of the current segment. So we just subtract the starting time from the current time and divide by the normalized length of the current segment to get a value between 0 and 1. As long as you increase "aTime" at a constant speed you should move along the path with a constant speed.
As i said in most cases you just want to linearly move along the path. In this case you can get rid of the while loop as long as you remember the last index.
List<Segment> segments;
int current = 1;
float t = 0f;
void Update()
{
if (current > 0 && current < segments.Count)
{
t += speed * Time.deltaTime / totalLength / segments[current].nLength;
if (t>1f)
{
t -= 1f;
t *= segments[current].nLength;
current++;
if (current >= segments.Count)
return;
t /= segments[current].nLength;
}
pos = Vector3.Lerp(segments[current-1].pos, segments[current].pos, t);
}
}
This, again, is untested but should work. To actually initialize the segments you have to calculate the normalized length.
float totalLength = 0f;
for(int i = 1; i < segments.Count; i++)
{
segments[i].nLength = Vector3.Distance(segments[i-1].pos, segments[i].pos);
totalLength += segments[i].nLength ;
}
for(int i = 1; i < segments.Count; i++)
{
segments[i].nLength /= totalLength;
}
Note that "speed" is in world units per second. So if you want to go though the whole path in 3 sec you need a speed of
speed = totalLength / 3f;
It's common to base such thing on the actual speed. However if you prefer just to define a time ins$$anonymous$$d of a speed you can do;
t += Time.deltaTime / segments[current].nLength / timeDuration;
I think this is what I was looking for. I currently deter$$anonymous$$e speed based on length per "segment" (I calculate distance between waypoints everytime it reaches the next one, but storing them should be more efficient since the waypoints don't move at all), and divide them by the total time, which I thought worked but apparently only in the setup I was using.
I'll try this out tomorrow and let you know how it works out, thanks =D
I'm having a bit of trouble with understanding the math behind how you calculate t in your second example. Arguably t could just be reset to 0 after getting the next segment?
I don't see why you would subtract 1 and divide and multiply it afterward.
Also doesn't speed need to be calculated every time you target the next segment?
Ok actually there was an error in my code ^^. I've fixed my answer ^^. We need to divide by nLength, not multiply.
Now just break down the parts of this code:
t += speed * Time.deltaTime / totalLength / segments[current].nLength;
if (t>1f)
{
t -= 1f;
t *= segments[current].nLength;
current++;
t /= segments[current].nLength;
}
"speed" is just your desired speed in world units per second. "speed / totalLength" gives us the overall speed in "percentage" per second ("programmer percentage" so a value between 0 and 1 ^^). This affects just the overall unifed lerp speed. Remember the "aTime" in "PathLerp" was a linear lerp value for the whole path.
So speed / totalLength is the required "t" increase per second. then we multiply by Time.deltaTime so actually get the required percentage change per frame. Finally we divide by our normalized "nLength" of the current segment which accounts for the different speeds of the different segments.
$$anonymous$$eep in $$anonymous$$d that t is a floating point value and the time per frame is not constant. So as we reach the end of a section we will most likely overshoot our final t value "1". Just for example "1.12". Though at 1 a new section starts. So we are already partially into the next section. To fix this we subtract "1" (for the completed segment) and "unscale" our time so it's no longer "fixed" for our last segment. Then we switch to the next segment and scale the time again with the new section specific factor. This should ensure to get a linear motion.
Imagine you have a very short segment followed by a long segment. nLength of the current will be way smaller than nLength of the next (long) segment. That means t has to increase faster for the short segment, but slower for the long segment. Since we divide by nLength we actually get a larger increase for the short segment. Since we overshoot 1 by 0.12 for example we first "normalize" the overshoot time by multiplying back in nLength. Say the length of the short segment is "0.1" so the normalized time would become "0.012". Say the next long segment has a nLength of 0.5 (so half of the entire path). We divide 0.012 by 0.5 and get "0.024" which is the correct point on the new segment. $$anonymous$$eep in $$anonymous$$d the new segment is longer so t has to increase slower
This is quite hilarious. I actually had the code fixed before you replied (one of my roommates is a math $$anonymous$$cher), but encountered a different bug that I think your t calculations actually fix.
I reset t to 0 everytime I get to the next point and can only "get" the new point after a check made in update, basically hard limiting getting a single point per frame. Now what would happen was that the more points with tiny length segments I had, the slower the animation would go. Effectively getting approximate good results with long straight segments, but diverge more for every small curve.
With your t calculations, you allow for overshoot and could entirely skip, even multiple points if the deltaTime was larger then the time to take to reach the next point, if that makes any sense.
Thanks so much! I believe we can call this questions closed!
I just had a bit of time and just tested both ways, PathLerp as well as the manual update solution and both work now as expected. I've measured the distance traveled independently from the movement by comparing the position from the last frame with the current and divide by deltaTime to get the current speed of the object. I set the speed to 4 and i get a perfect 4 all along the path:
Answer by $$anonymous$$ · Aug 06, 2018 at 11:08 AM
You can add a timer variable and divide it by 3:
float counter = 0;
void Update ()
{
Vector3 start = new Vector3(0,0,2);
Vector3 end = new Vector3(0,0,10);
counter += Time.deltaTime;
transform.position = Vector3.Lerp(start , end , counter / 3);
}
You can replace the start
and end
variable with your waypoint positions.
This would only work for a single Lerp, what I'm trying to achieve is a constant speed over multiple Lerps with various distances.
Yeah i know that, as i said in the answer:
You'll have to replace them by something else, for example if you have a List/Array of transforms, then make an "Index" variable of type (Int), and whenYou can replace the
start
andend
variable with your waypoint positions.
waypoints[index].position == nextWaypoint.position
then
index++;
and
counter = 0;
.
And the lerp will look like: Yeah i know that, as i said in the answer:
> You can replace the
start
and
end
variable with your waypoint positions. You'll have to replace them by something else, for example if you have a List/Array of transforms, then make an "Index" variable of type (Int), and when
waypoints[index].position == nextWaypoint.position
then
index++;
and
counter = 0;
.
And the lerp will look like: float counter = 0; void Update () { Vector3 start = new Vector3(0,0,2); Vector3 end = new Vector3(0,0,10); counter += Time.deltaTime; transform.position = Vector3.Lerp(waypoints[index].position , nextWaypoint.position , counter / 3); }
The code doesn't look like a code in the site which i don't know why, but.. you only have to replace
start
by
waypoints[index].position
, and
end
by
nextWaypoint.position
.
I don't think I've made myself clear. I know how to implement the code.
Your example however, would make every single individual Lerp last exactly 3 seconds. What I'm trying to achieve is make all the Lerps in total last a total amount of 3 seconds and make sure that the object moves at a constant speed (so NOT 3 seconds per Lerp) to do so.
Answer by madks13 · Aug 06, 2018 at 11:22 AM
You will have to calculate the total distance of the path. Then you have your speed units/second. Then multiply by deltaTime. You got your distance to move by. Then move along the path.
Answer by DawidNorasDev · Aug 07, 2018 at 09:39 AM
Easy (but not exactly what you asked) solution is to use SmoothDamp instead. or even better Vector3.MoveTowards.