Search Unity

  1. Unity support for visionOS is now available. Learn more in our blog post.
    Dismiss Notice

Question When does a Rigidbody's interpolation occur?

Discussion in 'Physics' started by _eternal, Jun 13, 2022.

  1. _eternal

    _eternal

    Joined:
    Nov 25, 2014
    Posts:
    301
    I'm trying to iron out some finicky issues involving Rigidbody interpolation in 2D.

    Now, I might be incorrect, but here's how I understand interpolation in Unity. Let's assume that physics is running at 60 ticks per second and the game is rendering at 100 fps.
    • Frame 1: FixedUpdate executes, and we use rigidbody.MovePosition to tell our rb where to head towards for the next FixedUpdate
    • Frame 1: Update executes, and Unity automatically picks a location between our previous rb position and our current rb position. Unity moves our transform/collider to that position.
    • Frame 2: Update runs again, because we're rendering faster than our physics tick rate. Unity picks another interpolated position to move our transform to.
    • Frame 3: FixedUpdate executes again, whenever its timer tells it to. Our rb's position is moved to the location we gave it when we previously called MovePosition. And we'll call MovePosition again at the end of the FixedUpdate to determine our target position for the next FixedUpdate.
    And so on.

    If this is the case, where exactly does interpolation occur in the Unity execution order? Is it during Update? Is it after Update but before LateUpdate? Or is it something else entirely?
     
    CodeSmile likes this.
  2. Edy

    Edy

    Joined:
    Jun 3, 2010
    Posts:
    2,482
    As far as I can tell it occurs during Update, where interpolation happens. Here's my own understanding on how it works (might be inaccurate):
    • (previous state: rigidbody at position p0)
    • Frame 1 - FixedUpdate: rigidbody.MovePosition sets a new position p1 to the rigidbody. This is at time Time.fixedtime.
    • (Physics Update): physics executes a simulation step and moves the physic rb to p1. Rigidbody.velocity is computed as (p1-p0) / fixedDeltaTime.
    • Frame 1 - Update: an interpolated position is calculated between p0 and p1 (Lerp) based on the time passed since latest fixed time (Time.time - Time.fixedTime) with respect to the fixed delta time (Time.fixedDeltaTime). The transform is moved to that position.
    • Frame 2 - Update: Same as the above. Here Time.time has advanced but Time.fixedTime didn't, so the Lerp takes a new interpolated position.
    • Frame 3 - FixedUpdate: rigidbody.MovePosition sets a new position p1 to the rigidbody.
    I've successfully implemented interpolation in visual elements that are driven by physics (i.e. car wheels) so they look perfectly smooth at slow motion. The above steps might not be entirely accurate, but they must be quite close. What is sure is that Interpolation uses the latest two known physic positions of the rigidbody, and the interpolations happens somewhere during Unity's internal Update cycle.
     
    Last edited: Jun 13, 2022
  3. MelvMay

    MelvMay

    Unity Technologies

    Joined:
    May 24, 2013
    Posts:
    11,170
    Interpolation/Extrapolation is about writing to the Transform per-frame so it happens per-frame after all the script (MonoBehaviour) Update have been called. The same goes for the physics fixed-update, it happens after the script (MonoBehaviour) FixedUpdate.

    If you have the simulation mode set to Update (per-frame) then interpolation is turned off. Interpolation only happens if the simulation mode is FixedUpdate.
     
  4. _eternal

    _eternal

    Joined:
    Nov 25, 2014
    Posts:
    301
    Thanks for the replies, guys.

    Hang on. Based on my testing just now, I'm not sure if this is true?

    If we try something like this:

    Code (CSharp):
    1. public class DebugPhysicsObjectEarly : MonoBehaviour
    2. {
    3.     [SerializeField] bool logDebug;
    4.  
    5.     Vector2 prevFrameTrPos;
    6.     Vector2 currFrameTrPos;
    7.  
    8.     Vector2 prevFrameRbPos;
    9.     Vector2 currFrameRbPos;
    10.  
    11.     Rigidbody2D rBody;
    12.  
    13.     private void Awake()
    14.     {
    15.         rBody = GetComponent<Rigidbody2D>();
    16.  
    17.         currFrameTrPos = transform.position;
    18.         currFrameRbPos = rBody.position;
    19.     }
    20.  
    21.     void FixedUpdate()
    22.     {
    23.         prevFrameRbPos = currFrameRbPos;
    24.         currFrameRbPos = rBody.position;
    25.         Vector2 delta = currFrameRbPos - prevFrameRbPos;
    26.  
    27.         if (logDebug)
    28.         {
    29.             Debug.Log("Rb position at the beginning of FixedUpdate: " + rBody.position.ToFullString() + "\n" +
    30.                 "Tr position at the beginning of FixedUpdate: " + transform.position.ToFullString() + "\n" +
    31.                 "Rb delta: " + delta.ToFullString() + "\n" +
    32.                 "Frame " + Time.frameCount,
    33.                 gameObject);
    34.         }
    35.     }
    36.  
    37.     private void Update()
    38.     {
    39.         if (logDebug)
    40.         {
    41.             Debug.Log("Rb position at the beginning of Update: " + rBody.position.ToFullString() + "\n" +
    42.                 "Tr position at the beginning of Update: " + transform.position.ToFullString() + "\n" +
    43.                 "Frame " + Time.frameCount,
    44.                 gameObject);
    45.         }
    46.     }
    47.  
    48.     private void LateUpdate()
    49.     {
    50.         prevFrameTrPos = currFrameTrPos;
    51.         currFrameTrPos = transform.position;
    52.         Vector2 delta = currFrameTrPos - prevFrameTrPos;
    53.  
    54.         if (logDebug)
    55.         {
    56.             Debug.Log("Rb position at the beginning of LateUpdate: " + rBody.position.ToFullString() + "\n" +
    57.                 "Tr position at the beginning of LateUpdate: " + transform.position.ToFullString() + "\n" +
    58.                 "Tr Delta: " + delta.ToFullString() + "\n" +
    59.                 "Frame " + Time.frameCount,
    60.                 gameObject);
    61.         }
    62.     }
    63. }
    So, I have one script like that, and then I duplicated it and renamed the new one to DebugPhysicsObjectLate. In Script Execution Order, I moved DebugPhysicsObjectEarly above default time, and I moved DebugPhysicsObjectLate below default time.

    For convenience's sake, I'll just paste in my PhysicsObject testing script.

    Code (CSharp):
    1. public class PhysicsObject : MonoBehaviour
    2. {
    3.     [SerializeField] float moveSpeed = 5f;
    4.  
    5.     [SerializeField] bool logDebug;
    6.  
    7.     Vector2 prevFramePos;
    8.     Vector2 currFramePos;
    9.  
    10.     bool pressingUp;
    11.     bool pressingDown;
    12.     bool pressingLeft;
    13.     bool pressingRight;
    14.  
    15.     Rigidbody2D rBody;
    16.  
    17.     private void Awake()
    18.     {
    19.         rBody = GetComponent<Rigidbody2D>();
    20.  
    21.         currFramePos = transform.position;
    22.     }
    23.  
    24.     void FixedUpdate()
    25.     {
    26.         pressingUp = false;
    27.         pressingDown = false;
    28.         pressingLeft = false;
    29.         pressingRight = false;
    30.  
    31.         if (Input.GetKey(KeyCode.W) || Input.GetKey(KeyCode.UpArrow))
    32.         {
    33.             pressingUp = true;
    34.         }
    35.         if (Input.GetKey(KeyCode.S) || Input.GetKey(KeyCode.DownArrow))
    36.         {
    37.             pressingDown = true;
    38.         }
    39.         if (Input.GetKey(KeyCode.A) || Input.GetKey(KeyCode.LeftArrow))
    40.         {
    41.             pressingLeft = true;
    42.         }
    43.         if (Input.GetKey(KeyCode.D) || Input.GetKey(KeyCode.RightArrow))
    44.         {
    45.             pressingRight = true;
    46.         }
    47.  
    48.         Vector2 move = Vector2.zero;
    49.  
    50.         if (pressingUp)
    51.         {
    52.             move += Vector2.up * (moveSpeed * Time.fixedDeltaTime);
    53.         }
    54.         if (pressingDown)
    55.         {
    56.             move += Vector2.down * (moveSpeed * Time.fixedDeltaTime);
    57.         }
    58.         if (pressingLeft)
    59.         {
    60.             move += Vector2.left * (moveSpeed * Time.fixedDeltaTime);
    61.         }
    62.         if (pressingRight)
    63.         {
    64.             move += Vector2.right * (moveSpeed * Time.fixedDeltaTime);
    65.         }
    66.  
    67.         Vector2 target = rBody.position + move;
    68.         rBody.MovePosition(target);
    69.  
    70.         if (logDebug)
    71.         {
    72.             Debug.Log("Position at the end of FixedUpdate: " + rBody.position.ToFullString() + "\nFrame " + Time.frameCount, gameObject);
    73.         }
    74.     }
    75. }
    My Physics 2D simulation mode is FixedUpdate, and my rigidbody is Kinematic with Interpolate turned on.

    I've done a lot of testing today, so I might have gotten confused somewhere. But from what I'm seeing here, the transform actually moves in between "end of FixedUpdate" and "beginning of Update"? As opposed to moving between "end of Update" and "beginning of LateUpdate".
     
  5. MelvMay

    MelvMay

    Unity Technologies

    Joined:
    May 24, 2013
    Posts:
    11,170
    I wrote it, it's true. ;) If it's not, something has gone horribly wrong.

    A few things to mention for clarity:

    "Position at the end of FixedUpdate" - This won't be "rBody.position + move;", the RB hasn't moved yet so the RB will be at the same position as when the script FixedUpdate started. This is the end of the script FixedUpdate, no internal fixed-update systems have run yet.

    The following per-frame stuff; note that interpolation hasn't run yet because as I've said, the script fixed-update/update happen before the internal Unity system that uses it.

    "Rb position at the beginning of Update: " + rBody.position" - This will show the RB at the "rBody.position + move" position because physics ran in the internal fixed-update (assuming it ran this frame). This is the target of the move.

    "Tr position at the beginning of Update: " + transform.position" - If the FixedUpdate ran this frame, no interpolation has happen yet so transform.position will be at the start of the interpolation movement so the old rBody.position (not the current move target) because interpolation is historic movement from old to new.

    Code (CSharp):
    1.         prevFrameRbPos = currFrameRbPos;
    2.         currFrameRbPos = rBody.position;
    3.         Vector2 delta = currFrameRbPos - prevFrameRbPos;
    I'm not sure what the delta you're using here is for. This is the interpolation from the the previous simulation step, not the interpolation you are about to do. previous will be the current rBody.position and the current will be the position you move to.

    I don't want to get further into back/forth debugging these scripts though, in the end you have the scripting callbacks then the unity internal call for the same thing happens for physics, animation, particles (lots of internal systems) so not just physics.

    Essentially everything edy said above is true.

    NOTE: For sanity, you can read and show the PlayerLoop where you'll see systems call the scripts and other systems immediately after calling things like animation, physics etc.
     
    Edy likes this.
  6. _eternal

    _eternal

    Joined:
    Nov 25, 2014
    Posts:
    301
    Interesting. This makes sense, and I think I was able to solve the problem I was working on anyway.

    Now I must say, my testing still shows that the transform is moving before its Update takes place. The move seems to happen between FixedUpdate and Update: https://imgur.com/a/AtxS2XY

    This is on Unity 2021.3.4, for what it's worth. I realize it would take time to debug this, and perhaps it's not worth it. The general concept of yours and Edy's posts makes sense to me, but I can't seem to reproduce this phenomenon of interpolation occurring after Update rather than after FixedUpdate.
     
    laurentlavigne likes this.
  7. MelvMay

    MelvMay

    Unity Technologies

    Joined:
    May 24, 2013
    Posts:
    11,170
    I've just seen a bug report from (I believe) you but it doesn't make sense TBH.

    For starters, you are issuing a MovePosition per-frame which makes no sense at all because that won't be processed until the simulation runs which in your test project is during the FixedUpdate. You should be doing that prior to the simulation running i.e. in FixedUpdate.

    You can get many frames before FixedUpdate runs and when it does, it can run from 1 to N times. That's obviously not controlled by physics. I also don't follow why you consider your two tests scripts as "early" and "late" as you've only set these identical scripts via the execution-order; they're not delayed in any way.

    When the simulation runs, a MovePosition (or MoveRotation) will set-up an interpolation of the Transform from the current Rigidbody2D position to the new target position. Per-Update, the physics system will interpolate this historic move from the old to current position and write that to the Transform. The Unit-tests prove this works, I don't follow what your project proves TBH.
     
  8. _eternal

    _eternal

    Joined:
    Nov 25, 2014
    Posts:
    301
    Hey, thanks for checking.

    This was a while ago, but I think my main point was that the transform seems to move in between FixedUpdate and Update. If you hold the right arrow and frame step while the object is moving, you should get something like:

    Code (CSharp):
    1. Rb position at the end of FixedUpdate: (3.83, 0.00)
    2. Tr position at the end of FixedUpdate: (3.78, 0.00)
    3. Frame 352
    4. UnityEngine.Debug:Log (object,UnityEngine.Object)
    5. DebugPhysicsObjectLate:FixedUpdate () (at Assets/__Scripts/DebugPhysicsObjectLate.cs:33)
    Code (CSharp):
    1. Rb position at the beginning of Update: (3.92, 0.00)
    2. Tr position at the beginning of Update: (3.87, 0.00)
    3. Frame 352
    4. UnityEngine.Debug:Log (object,UnityEngine.Object)
    5. DebugPhysicsObjectEarly:Update () (at Assets/__Scripts/DebugPhysicsObjectEarly.cs:44)
    Previously, I thought you guys were saying that the transform would be moved after Update or something like that. But it looks like it's being moved after FixedUpdate.

    Looking at your most recent comment, I guess this is probably intended behavior? Something like...
    • FixedUpdate occurs, and we can call MovePosition
    • Simulation occurs, and both the rigidbody and transform are moved
    • Update occurs, and nothing new happens, but we can print the rigidbody/transform's positions to confirm that they have moved
    That's what I'm getting from the demo project. If that's how it's supposed to work, then sure, we're all good.