Search Unity

Resolved "Vector3.Lerp" might not be transforming smoothly at 30fps (on phone/tablet devices)

Discussion in 'Scripting' started by GuirieSanchez, Nov 30, 2022.

  1. GuirieSanchez

    GuirieSanchez

    Joined:
    Oct 12, 2021
    Posts:
    452
    I have a bezier curve drawn in my game and I'm moving an object (called "blind") around the curve, in the form of an ellipsoid, like in this picture:

    upload_2022-11-30_16-38-31.png
    I have a total of 40 points (0-39) that make up the ellipsoid, and the points are gathered in a list called "fullPathWayPoints". Since I don't know of a way to smoothly interpolate between two points in a path that is not linear (this method will be ideal though), what I'm doing is linearly interpolating between each point, one by one, until the object (blind) reaches the target point. This works fine in the editor, in a PC build, and in a Mac build, although one of the drawbacks of using this method is that I cannot make the object go from point A to point B in a specific time that I can set up. Thus, the way is done is by speed instead: constant speed through all the points until the object reaches its destination. The code is the following:

    Code (CSharp):
    1.  private IEnumerator MoveBlind(int index, int finalPos)
    2.     {
    3.         tParam = 0;
    4.  
    5.         if (index >= fullPathWayPoints.Count - 1) //this one resets the list so it doesn't go beyond the count limits
    6.         {
    7.             while (tParam < 1)
    8.             {
    9.                 tParam += Time.deltaTime * _speed;
    10.  
    11.  
    12.                 _objectPosition = Vector3.Lerp(_fullPathWayPoints[index], fullPathWayPoints[0], tParam);
    13.                 transform.position = _objectPosition;
    14.                 _currentPos = transform.position;
    15.  
    16.                 yield return null;
    17.             }
    18.             index = -1;
    19.         }
    20.         else
    21.         {
    22.             while (tParam < 1)
    23.             {
    24.                 tParam += Time.deltaTime * _speed;
    25.  
    26.  
    27.                 _objectPosition = Vector3.Lerp(fullPathWayPoints[index], fullPathWayPoints[index + 1], tParam);
    28.                 transform.position = _objectPosition;
    29.                 _currentPos = transform.position;
    30.  
    31.                 yield return null;
    32.             }
    33.  
    34.         }
    35.  
    36.         index += 1;
    37.         if (index != finalPos)
    38.         {
    39.             StartCoroutine(MoveBlind(index, finalPos));
    40.         }
    41.         else if (index == finalPos)
    42.         {
    43.             Debug.Log("Finished");
    44.        
    45.         }
    46.  
    47.     }
    Code is not perfect, but at least get the job done (dirtily). As a side question, if someone knows of a way to interpolate between two points in a way that I can set up a time from point A to point B, and also that describes the path marked by the points, it'd be super useful.

    So, the thing is that in the devices I tested (iPad, Xiaomi tablet, and iPhone), the blind (object) stutters while moving across the path. All devices are capped at 30 fps, so at first, I thought that that might be the reason. But everything else moves smoothly even at 30fps, which makes me now think that it might do something to do with the code (it's not the first time that I experienced stutters while using Lerping methods (in other projects, though)). Any help is highly appreciated.

    EDIT: I capped the game on the editor at 30fps and the stutter becomes more obvious.
     
    Last edited: Nov 30, 2022
  2. Kurt-Dekker

    Kurt-Dekker

    Joined:
    Mar 16, 2013
    Posts:
    38,697
    All of the
    .Lerp()
    methods, without exception, know absolutely NOTHING about framerate.

    All .Lerp() methods are simple mathematic constructs, nothing else.

    Obviously they also know absolutely nothing about what platform they are running on.

    Look to your inputs / framerates / Time.deltaTime values.
     
    Last edited: Nov 30, 2022
    GuirieSanchez likes this.
  3. jvo3dc

    jvo3dc

    Joined:
    Oct 11, 2013
    Posts:
    1,520
    Mathematically speaking, nobody knows that in the case of an ellipse or a Bezier curve. Smooth interpolation is easy for both, but since there is no exact formula for their length, there is also no exact way to interpolate in a way that the speed is constant. Of course, there are ways to approximate this better than a linear interpolation, so I'd have a look at that.
     
    GuirieSanchez likes this.
  4. MelvMay

    MelvMay

    Unity Technologies

    Joined:
    May 24, 2013
    Posts:
    11,459
    As Kurt said above, it's just a basic math function for a Vector3. Here's the source. You can look for the platform/render-dependent fault there if you wish. ;) Does sound like one of your previous post TBH.

    I'm not sure if it'll help but I replied to a user a while back about moving between "waypoints" at a constant speed here. It's not exactly what you want but might trigger some insights. I've attached the simple test project too.
     

    Attached Files:

    GuirieSanchez likes this.
  5. GuirieSanchez

    GuirieSanchez

    Joined:
    Oct 12, 2021
    Posts:
    452
    Thank you for the replies. I guess I explained myself wrong. What I should have said is that I noticed the stutters at 30 fps, not that the fps themselves are affecting the Lerp function. I wanted to point to something like this issue:
    upload_2022-12-1_15-40-50.png

    Which I experienced myself in other projects.

    I was thinking there might be a known issue (not by me though) that affects smooth transformations when building or using the editor, therefore I thought I'd ask just to make sure before moving on to investigate other possible causes.

    Since the above mentioned issue happens, afaik, only on the editor and I'm experiencing stutters on both build and editor, I'll assume the cause is something different: I'll have a look then at
    My first hunch is that it might have something to do with the fact that I'm lerping small lines that make up a curve, and the lower the framerate, the more noticeable the steps (each linear path) that make up the ellipsoid. Although I should double-check and see if there are noticeable stutters on each step that define the curve.

    Regarding the side question:
    Thank you, this pointed me in the right direction.
    I calculated the desired speed (the one that travels from point A to the nearest point in the desired time), and then multiplied that speed by the total waypoints the object should travel along, and it works nicely: It takes the object the same time to travel to any waypoint of the ellipsoid. Something like:

    Code (CSharp):
    1. //calculate speed
    2.         int speed = 2; //this is the desired speed in my case
    3.         int multiplier;
    4.         if(targetRoutePos > _currentRoutePos)
    5.         {
    6.             multiplier = targetRoutePos - _currentRoutePos;
    7.         }
    8.         else
    9.         {
    10.             multiplier = (40 - _currentRoutePos) + (targetRoutePos - 1);
    11.         }
    12.         _speed = speed * multiplier;
     
    Last edited: Dec 1, 2022
  6. digipaul

    digipaul

    Joined:
    Nov 24, 2018
    Posts:
    43
    One small issue notice in your code is that tParam in your loop will go past one. This could lead to jitters at low framerate as the overshoot will increase with Time.deltaTime. You could replace the while(tParam<1) statement with a for loop. Something like for(float tParam=0;tParam<1;tParam+=Time.deltaTime * _speed)
     
    halley likes this.
  7. GuirieSanchez

    GuirieSanchez

    Joined:
    Oct 12, 2021
    Posts:
    452
    Like this?

    Code (CSharp):
    1. for (tParam = 0; tParam < 1; tParam += Time.deltaTime * _speed)
    2.             {
    3.                 //tParam += Time.deltaTime * _speed;
    4.  
    5.                 _objectPosition = Vector3.Lerp(fullPathWayPoints[index], fullPathWayPoints[0], tParam);
    6.                 transform.position = _objectPosition;
    7.                 _currentPos = transform.position;
    8.  
    9.                 yield return null;
    10.             }
     
  8. digipaul

    digipaul

    Joined:
    Nov 24, 2018
    Posts:
    43
    Yes, that should do it.
     
    GuirieSanchez likes this.
  9. GuirieSanchez

    GuirieSanchez

    Joined:
    Oct 12, 2021
    Posts:
    452
    Thank you. Would you mind telling me why the While-Loop leads to jitters whereas the For-Loop does not?
     
  10. GuirieSanchez

    GuirieSanchez

    Joined:
    Oct 12, 2021
    Posts:
    452
    Hi guys. Sorry for necroing, but I noticed something wrong with this method.

    The thing is that the lower the framerate, the longer it takes the object to travel along the curve to the target point.

    I don't understand why since I'm calculating it with deltaTime, as mentioned here by @digipaul
     
    Last edited: Jan 13, 2023
  11. chemicalcrux

    chemicalcrux

    Joined:
    Mar 16, 2017
    Posts:
    720
    Each update, you look at a variable. If the variable is less than 1, you add to it and yield. Otherwise, you start a new coroutine to move to the next point.

    Let's say that it takes 1 second to move from one point to the next. So, after 3 seconds, we should be at the end of the third coroutine.

    Let's also say that our framerate is...1. So, at each update, the following sequence happens:

    Frame 0: t=0, so we run the loop. Add 1 to t.
    Frame 1: t=1, so we don't loop. Start a new coroutine.
    Frame 2: t=0, so we run the loop. Add 1 to t.
    Frame 3: t=1, so we don't loop. Start a new coroutine.

    It takes us two updates to start a new coroutine, even though we should be starting a new one every single frame.

    One solution would be to check if t >= 1 at the end of the loop, and to immediately break out of it if that's true. That way, we proceed immediately after we reach the end.

    However, that's not the only issue!

    Suppose our framerate is 2.5, and that we've fixed the previous issue. Here's a new sequence:

    Frame 0: t=0, so we run the loop. Add 0.4 to t.
    Frame 1: t=0.4, so we run the loop. Add 0.4 to t.
    Frame 2: t=0.8, so we run the loop. Add 0.4 to t. t >= 1, so we break the loop. Start a new coroutine.
    Frame 3: t=0, so we run the loop. Add 0.4 to t.
    Frame 4: t=0.4, so we run the loop. Add 0.4 to t.

    Two seconds have passed, but we're only at t=0.8.

    Each time we start the coroutine, we "lose" any extra time we accumulated. As an extreme example, imagine if we had a framerate of 0.1: we'd go WAY past t=1, but only move forward one point!

    My suggestion: Don't start a new coroutine. Instead, do something a bit like this:

    Code (CSharp):
    1. index = 0;
    2. t = 0;
    3.  
    4. while (true) {
    5.   t += Time.deltaTime;
    6.  
    7.   while (t >= 1) {
    8.     index += 1;
    9.     t -= 1;
    10.   }
    11.  
    12.   if (index >= points.Count - 1)
    13.     break;
    14.  
    15.   target.position = Vector3.Lerp(points[index], points[index+1], t);
    16.   yield return null;
    17. }
    18.  
    19. target.position = points[^1];
    20.  
    21.  
    This prevents each step from "keeping the change": if you get to t=1.1, then after switching indices, you'll be at t=0.1.

    Once the index gets too high, it sets the target's position to the final point (since, otherwise, you'd be left a bit short), and then the coroutine exits.

    foo[^1]
    is shorthand for
    foo[foo.Count-1]
    , btw.
     
    GuirieSanchez likes this.
  12. GuirieSanchez

    GuirieSanchez

    Joined:
    Oct 12, 2021
    Posts:
    452
    This makes complete sense. The fact that you noticed it is amazing. Thank you!
    I think I could fiddle your solution into the previous code and fix it, but I was thinking of giving a try to this method you suggested:

    Although I don't understand it perfectly. I haven't used the while(true) method yet, so I'm not sure about how it works. How am I supposed to tell it if it's true or false, where do I establish its condition?
    Edit: I think I figured it out myself while writing the post, but I'll keep it so I can get a confirmation. So, what I think is that the condition will be true forever until that "if statement" that breaks the while(true). I assume that will be equivalent to false, then get out of the "while(true)" function and continue to perform the code inside the coroutine till the end of it. Please let me know if I didn't get it right.

    Another thing is, the starting index can be whatever point (from 0 to 39), and when it reaches 39, it should reset itself back to 0, to continue its way along the curve. However, I noticed that this
    Code (CSharp):
    1. if (index >= points.Count - 1)
    2.     break;
    will get out of the while (true), and position the object to the end of the points (39), but it should continue if the target position is other than 39.

    So, I think I'm almost done, but there's only one thing I can't figure out right now:

    Code (CSharp):
    1. index = X /*whatever number is passed in*/;
    2.         t = 0;
    3.  
    4.         while (true)
    5.         {
    6.             t += Time.deltaTime;
    7.             while (t >= 1)
    8.             {
    9.                 index += 1;
    10.                 t -= 1;
    11.             }
    12.             if (index >= points.Count - 1)
    13.             {
    14.                 index = 0; // I think I should reset the index here, so it can go from 0 to the target Position
    15.                 break; // ****** Here I should NOT break, because I will get out of the while(true),
    16.                        // so an alternative is needed *****
    17.             }
    18.  
    19.             if (index == finalPos)
    20.             {
    21.                 //Run whatever code: e.g., trigger events
    22.                 break;
    23.             }
    24.  
    25.             target.position = Vector3.Lerp(points[index], points[index + 1], t);
    26.             yield return null;
    27.         }
    28.         target.position = points[finalPos];
    29.  
    30.     }
    So, in order to prevent the index to go out of bounds, I need to do a check and reset the index if it reaches the
    points.Count - 1
    . As you can see in the posted code, I have no idea how to stop the code from continuing while keep staying inside the "while(true)"
     
  13. chemicalcrux

    chemicalcrux

    Joined:
    Mar 16, 2017
    Posts:
    720
    You can use the
    continue
    keyword to skip the rest of the loop without breaking out.

    Also, if you want to smoothly move from point 39 to point 0, then you can do something like this:

    Code (CSharp):
    1. int next = (index + 1) % points.Count;
    % is the modulo operator. It computes the remainder of integer division. 39 / 40 has a remainder of 39; 40 / 40 has a remainder of 0.

    If index is 39 and points.Count is 40, then next will be 0, and you'll be able to lerp between those two points properly.
     
  14. GuirieSanchez

    GuirieSanchez

    Joined:
    Oct 12, 2021
    Posts:
    452
    Ok, maybe this will do it:


    Code (CSharp):
    1. index = X /*whatever number is passed in*/;
    2.         t = 0;
    3.  
    4.         while (true)
    5.         {
    6.             t += Time.deltaTime;
    7.             while (t >= 1)
    8.             {
    9.                 index += 1;
    10.                 t -= 1;
    11.             }
    12.  
    13.             if (index == finalPos)
    14.             {
    15.                 //Run whatever code: e.g., trigger events
    16.                 break;
    17.             }
    18.  
    19.             target.position = Vector3.Lerp(points[index], points[index + 1], t);
    20.  
    21.             if (index >= points.Count - 1)
    22.             {
    23.                 index = 0;
    24.                 target.position = points[^1];
    25. // not breaking, but instead, positioning the obj to the end of the points and resetting the index
    26.                
    27.             }
    28.             yield return null;
    29.         }
    30.         target.position = points[finalPos];
     
  15. GuirieSanchez

    GuirieSanchez

    Joined:
    Oct 12, 2021
    Posts:
    452
    I forgot about it! thanks

    Which one would you recommend, using the

    Code (CSharp):
    1. if (index >= points.Count - 1)
    2.             {
    3.                 index = 0;
    4.                 target.position = points[^1];
    5.              
    6.             }
    at the end of the while(true) after the lerp function or before (with the
    continue;
    )

    PS: I'm trying to figure out how to add the % for smoothing inside the coroutine.

    EDIT: I've done some testing and it works perfectly! (and frame independently). Thanks again. I noticed the jump from point 39 to 0, as you mentioned. So adding that % is a must for me now. I'll reach out afterI figure it out.
     
    Last edited: Jan 13, 2023
  16. GuirieSanchez

    GuirieSanchez

    Joined:
    Oct 12, 2021
    Posts:
    452
    I figured it out. So, since the % for the
    points.Count
    is 0 (40/40), then I just simply change
    Code (CSharp):
    1. if (index >= points.Count - 1)
    for
    Code (CSharp):
    1. if (index > points.Count - 1)
    Because there's no index+1 going out of bounds. Now it's working nicely.

    However, there seems to be one last problem. When the speed for the obj is high and the framerate is low, the object will go past the target point and go for another round. It was going in circles in a loop, never reaching the target point, unless it hits it by chance (for example, at 10 fps sometimes it hits after 20 rounds, sometimes after 50, sometimes after 2).

    I believe this issue is caused because the value of:
    Code (CSharp):
    1. t += Time.deltaTime * _speed;
    is greater than 1 when the delta time is high enough. So the index is added before running the subsequent code.

    For instance, you can have an index of 0, so you would hopefully start interpolating from point 0 to point 1 in that frame; then you get the t value for that frame and check if it's greater than 1 (normally it'd be less). In this case, since the delta and speed are high, the t value surpasses 1 directly and adds 1 to the index. So, for that round, it's going as index 1 instead of index 0. If this is the case for any frame, then the index always skips a step: (1-3-5-7...etc.).
     
    Last edited: Jan 13, 2023
  17. chemicalcrux

    chemicalcrux

    Joined:
    Mar 16, 2017
    Posts:
    720
    I'd just check if the index is at least as high as the target point. >= is your friend when you're worried about skipping something!
     
    GuirieSanchez likes this.
  18. GuirieSanchez

    GuirieSanchez

    Joined:
    Oct 12, 2021
    Posts:
    452
    But this doesn't help for my use case. Sometimes you start at the end of the curve, let's say point 33. And let's say that the target point is 7. So the path goes: 33-34-35-36-37-38-39-0-1-2-3-4-5-6-7.

    If I use the
    >=
    operator then it would stop immediately without traveling a single point
     
  19. chemicalcrux

    chemicalcrux

    Joined:
    Mar 16, 2017
    Posts:
    720
    Ooh, I see. I was only imagining the basic "start at 0, stop at the last one" use-case.

    I'd suggest abstracting a little. Compute a starting point, plus the distance you need to travel:

    Code (CSharp):
    1. // start and end are integer indices
    2.  
    3. int count = points.Count;
    4.  
    5. // how far start and end are from each other
    6. int delta = end - start; // 7 - 33 = -26
    7.  
    8. // If start > end, add count to get a positive value!
    9. if (delta < 0)
    10.   delta += count; // -26 + 40 = 14
    11.  
    12. int index = 0;
    13. float t = 0;
    14.  
    15. // proceed as usual
    To compute the two indices you're lerping between, you'll just do:

    Code (CSharp):
    1. int i1 = (start + index) % count;
    2. int i2 = (start + index + 1) % count;
    Stop when
    index >= delta
    .
     
    GuirieSanchez likes this.
  20. GuirieSanchez

    GuirieSanchez

    Joined:
    Oct 12, 2021
    Posts:
    452
    I came up with a dirty solution that involved calculating the total points to travel and subtracting one point for each index added (checked on each loop). But your solution is so elegant, to be honest, and it seems much more robust. I've just tested and it works pretty neatly.

    I really appreciate your help. I can finally mark the thread resolved.
     
    chemicalcrux likes this.
  21. GuirieSanchez

    GuirieSanchez

    Joined:
    Oct 12, 2021
    Posts:
    452
    Hi @chemicalcrux

    Upon testing it I noticed that the object sometimes glitches while moving along the curve. By glitching, I mean that the object advances steadily and correctly but suddenly and instantly teleports way back for a frame and then teleports to the previous/correct position. And it does it multiple times over the course of the coroutine. I tracked the object's position to rule out any possible visual glitch, but indeed its position changes:

    upload_2023-1-18_17-6-57.png

    As you can see, if we only have a look at the X position, it goes down from 842.48 little by little, around 6 units per frame, but always down to its destination. But then it jumps back up for one frame, and then again jumps back down to the correct position, resulting in an unpleasant visual experience.

    It only happens sometimes, and I have no clue why.

    Any ideas are highly appreciated
     
  22. chemicalcrux

    chemicalcrux

    Joined:
    Mar 16, 2017
    Posts:
    720
    You should inspect the values of index and t. I'm guessing there's a messed-up edge case somewhere -- maybe a < instead of a <=?
     
    GuirieSanchez likes this.
  23. GuirieSanchez

    GuirieSanchez

    Joined:
    Oct 12, 2021
    Posts:
    452
    Here's the coroutine I'm using:

    Code (CSharp):
    1. private void CoroutineTest()
    2.     {
    3.  
    4.         t = 0;
    5.         speed = 0.2f;
    6.  
    7.         int count = points.Count;
    8.         int start = index;
    9.         int delta = finalPos - start;
    10.         if (delta < 0)
    11.             delta += count;
    12.         int i1;
    13.         int i2;
    14.  
    15.         index = 0;
    16.  
    17.  
    18.         while (true)
    19.         {
    20.  
    21.             i1 = (start + index) % count;
    22.             i2 = (start + index + 1) % count;
    23.             t += Time.deltaTime * speed;
    24.  
    25.             while (t >= 1)
    26.             {
    27.                 index += 1;
    28.                 t -= 1;
    29.             }
    30.  
    31.  
    32.             if (index >= delta)
    33.             {
    34.                 Debug.Log("Finished!");
    35.                 break;
    36.             }
    37.  
    38.             if (index > points.Count - 1)
    39.             {
    40.                 index = 0;
    41.                 continue;
    42.             }
    43.  
    44.             _objectPosition = Vector3.Lerp(points[i1], points[i2], t);
    45.             transform.position = _objectPosition;
    46.  
    47.             Debug.Log($"i1: {i1}");
    48.             Debug.Log($"i2: {i2}");
    49.             Debug.Log($"_objectPosition: {_objectPosition}");
    50.  
    51.  
    52.             yield return null;
    53.         }
    54.         _objectPosition = points[finalPos];
    55.         transform.position = _objectPosition;
    56.  
    57.  
    58.     }
    I noticed that the glitches/jumps do not happen arbitrarily. They happen when the object reaches each of the 40 points it can travel. For instance, if it goes from 0 to 20, it glitches 20 times:
    when the obj reaches point 1, it teleports back to point 0 for a frame and continues steadily from point 1 to point 2. Then, when it reaches point 2, it teleports back to point 1 for a frame. When reaching point 3, back to point 2, and so on and so forth.
     
    Last edited: Jan 18, 2023
  24. chemicalcrux

    chemicalcrux

    Joined:
    Mar 16, 2017
    Posts:
    720
    When you see something like that, you should check your order of operations!
    • Calculate i1 and i2 based on your index
    • Modify t
    • If t is high enough, modify the index
    • Use i1, i2, and t to compute a position
    i1 and i2 get computed before you decide what t should be! You then use them together with t.

    I would say that i1 and i2 are "stale" -- they used old data. So, just compute them afterwards :)
     
    GuirieSanchez likes this.
  25. GuirieSanchez

    GuirieSanchez

    Joined:
    Oct 12, 2021
    Posts:
    452
    It was exactly that. Now that you pointed it out it makes total sense. I checked the code several times but I don't know why I kept overlooking that. I guess I was faulting something else in the code. Thanks for sharing your expertise, and sorry for reopening the post with a rookie mistake.
    I guess I'll continue brushing up on the basics, thanks for the tip!
     
    Last edited: Jan 18, 2023
    chemicalcrux likes this.