Search Unity

  1. Welcome to the Unity Forums! Please take the time to read our Code of Conduct to familiarize yourself with the forum rules and how to post constructively.
  2. Dismiss Notice

Lerp, Slerp, and the quest for PERFECT framerate independence

Discussion in 'Editor & General Support' started by bigpants, Jul 11, 2014.

  1. bigpants

    bigpants

    Joined:
    Dec 9, 2009
    Posts:
    48
    For various reasons in my current game, I do this quite often:
    void Update () {
    myPosition = Vector3.Lerp(myPosition, Vector3.zero, Time.deltaTime);
    }
    I've seen examples of this everywhere, and it appears to provide decent framerate protection.


    ------------------
    However, as the following code illustrates, that approach is not perfect.

    20 frames @ 60fps
    hold = 100f;
    for (int count = 0; count < 20; count++) {
    hold = Mathf.Lerp(hold, 0f, (1f / 60f));
    }
    // outputs 0-98.33 1-96.69 2-95.08 3-93.50 4-91.94 5-90.41 6-88.90 ... 19-71.45


    10 frames @ 30fps
    hold = 100f;
    for (int count = 0; count < 10; count++) {
    hold = Mathf.Lerp(hold, 0f, (1f / 30f));
    }
    // outputs 0-96.67 1-93.44 2-90.33 3-87.32 ... 9-71.25

    After 1/3 of a second:
    60fps changes 100 into 71.45
    30fps changes 100 into 71.25
    although i'm more concerned about the difference at the start (98.33 vs. 96.67)


    ------------------
    My game appears to play fine at 30fps, 60fps, and 200fps so I think this approach is good enough.
    Is there a situation where this becomes a major problem?

    Note: I have LOTS of camera code that will experience this same problem:
    cameraPosition = Vector3.Lerp(cameraPosition, playerPosition, Time.deltaTime);
    I imagine everyone here has similar camera code... because how else could you do it?

    I'm hoping the error is so small it's considered insignificant. But is that true?

    I realize the perfect fix is to do this:
    myPosition = Vector3.Lerp(myStartPoint, Vector3.zero, Time.deltaTime);
    BUT that would require significant effort at this point,
    determining myStartPoint is tricky in some cases,
    and I have no idea how you'd apply this to a camera.
     
    neonblitzer and Gamba04 like this.
  2. bigpants

    bigpants

    Joined:
    Dec 9, 2009
    Posts:
    48
    I did find one instance where the error adds up: SCREENSHAKE!
    When you smash into things, I shake the screen a lot then settle down.

    At 30fps, the screenshake lasts for 1/2 a second. At 60fps, the screenshake lasts slightly less - AND IT'S VERY noticeable since shaking the screen is REALLY noticeable. As a result, at higher framerates my game lacked the "smash impact". I originally attributed this to the feeling of 60fps vs. 30fps, when it was actually a bug. Once I fixed the screenshake code, 60fps had the same "smash impact" of 30fps, but the game was much smoother. I hate to think about what other feelings are getting subtle messed up :(


    Original Buggy Screenshake Code

    Update()
    {
    screenShake = Mathf.Lerp(screenShake, 0f, 7.5f * Time.deltaTime); // the bug
    angle = Random.value * Mathf.PI * 2f;
    Camera.position = playerPosition + Camera.rotation * new Vector3(Mathf.Cos(angle) * screenShake, Mathf.Sin(angle) * screenShake, 0f);
    }


    New Fixed Screenshake Code

    Update()
    {
    screenShake -= 1.5f * deltaTime; // consistent at ALL framerates
    length = (Mathf.Pow(1.3f, screenShake) - 1f);
    angle = Random.value * Mathf.PI * 2f;
    Camera.position = playerPosition + Camera.rotation * new Vector3(Mathf.Cos(angle) * length, Mathf.Sin(angle) * length, 0f);
    }


    Fun Fact
    A game developer friend informed me this problem is referred to framerate independent damping.
    There's muchos google about it.
     
    neonblitzer likes this.
  3. AlkisFortuneFish

    AlkisFortuneFish

    Joined:
    Apr 26, 2013
    Posts:
    959
    Actually, that wouldn't work at all, it would jitter around (myStartPoint-Vector3.zero)*Time.deltaTime. To do that, you would need to actually reset a counter and increment it by deltaTime appropriately scaled to give you the desired lerp time. However, that is not the same behaviour that you would get with the usual pos = lerp(pos, target, dt) style of lerp, it will be a linear interpolation, it won't dampen anything.

    If I were you, I would have a look at smoothdamp. I haven't actually looked it performs when it comes to framerate independence accuracy but it's worth a try.
     
  4. _met44

    _met44

    Joined:
    Jun 1, 2013
    Posts:
    633
    Don't use deltaTime if you need precision, there is some error every frame and it adds up.

    The most accurate is to use "real time" varibles and base your calculations on that.

    So what you do is not only you store the start position, but also the start "real time" then use the difference as your lerp's alpha.

    This way any floating imprecision error you would have on a frame wouldn't not add up with the next. The current error would be limited to the error of the last frame instead of the sum of all errors.

    It would look like this:

    Code (csharp):
    1.  
    2. float _startTime;
    3. Vector3 _startPosition;
    4.  
    5. void Start()
    6. {
    7.     this._startTime = Time.realtimeSinceStartup;
    8.     this._startPosition = this.transform.position;
    9. }
    10.  
    11. void Update()
    12. {
    13.     this.transform.positon = Vector3.Lerp(this._startPosition, Vector3.zero, Time.realtimeSinceStartup - this._startTime);
    14. }
    15.  
    Another option is to use FixedUpdate() and Vector3.MoveToward() so you don't have to worry about time but only about speed.
     
  5. AlkisFortuneFish

    AlkisFortuneFish

    Joined:
    Apr 26, 2013
    Posts:
    959
    Using realtime is not a very good idea, apart from the fact that it completely unscaled and doesn't actually stop when paused, the precision is hardware-specific. The docs state that you can end up with the same value being returned on multiple frames if your framerate is higher than the precision of the timer.

    Also, it is still a 32-bit float, it only has a limited number of significant figures to work with and is a constantly incremented number. As the game runs for longer and longer, the actual precision of the lower bits will get worse and worse until the result is choppy. This is very easy to see if you use a shader that uses a plain _Time to do something and run the game for a while.

    FixedUpdate would be more predictable but there can be visible jitter there as well, if the fixed timestep is not much faster than the framerate and goes in and out of phase with the frame time.
     
  6. _met44

    _met44

    Joined:
    Jun 1, 2013
    Posts:
    633
    I didn't want to go into too much detail but for the time scaling and pausing issues you mention there are works arround. For example I wrote myself a nice little timer class that uses the realtime to count time spend in game, with start/pause/resume/reset features. It could easily have a scale factor as well.

    At first when making this class I used deltaTime but the time was clearly not right in matters of seconds. I tried with the timeSinceLevelLoad and after experimenting over a few minutes it confirmed it was right.

    Maybe I should setup a test scene that would run for a longer period. For my game which was running for arround one minute per scene it worked perfectly fine.
     
  7. jammingames

    jammingames

    Joined:
    Nov 17, 2013
    Posts:
    8
    The main reason you are seeing differences here is not because deltaTime is innacurate, but rather because it is variable and you are using it in a Lerp.

    The third value of the lerp is a percentage (0-1), 0 being from value, 1 being to value.

    Time.deltaTime varies to dampen the affects of framerate changes, so it will have the exact opposite effect when used within a Lerp function.

    If you would like to use Lerp I recommend doing so over a duration like so:


    Code (CSharp):
    1.  
    2. float currentLerpValue = 0;
    3. float duration = 3; //total time of lerp
    4. Vector3 startPosition;
    5. Vector3 targetPosition;
    6.  
    7. void OnEnable()
    8. {
    9. startPosition = transform.position;
    10. }
    11.  
    12. void Update()
    13. {
    14.       if (transform.position != targetPosition)
    15.       {
    16.          //move towards duration by increments of Time.deltaTime
    17.          currentLerpTime = Mathf.MoveTowards (currentLerpTime, duration, Time.deltaTime);
    18.          transform.position = Vector3.Lerp(startPosition, targetPosition, currentLerpTime);
    19.       }
    20. }
    21.  
    22.  
    This also works great in a coroutine