Search Unity

Question Which interpolation to use for predicted physics

Discussion in 'NetCode for ECS' started by Occuros, Mar 28, 2022.

  1. Occuros

    Occuros

    Joined:
    Sep 4, 2018
    Posts:
    300
    When running the physics simulation in the prediction loop, in nonideal networking conditions I can observe still jitter and micro teleporting for predicted entities.

    I assume it has to do with not choosing the correct way to interpolate the predicted entities:

    - using PhysicsBody Smoothing (None, Interpolated, Extrapolated)
    - using Translation and Rotation Variants with SmoothingAction.Clamp, SmoothingAction.Interpolate,
    SmoothingAction.InterpolateAndExtrapolate
    - using GhostPredictionSmoothingSystem and Registering `DefaultTranslateSmoothingAction` (and maybe also adding one for rotation?)

    As there are many different combinations and possibilities, I'm not sure which combination should be used in which circumstance.

    Another question related to this is how to set the number of ticks the server will run at (apart from setting physics Ticks Per Sim Tick).

    I'm currently testing with a Quest 2 (at 72fps) headset, and the physics predictions work, but not well enough (in terms of smoothness) to be useable in VR, under normal conditions (around 50-70ms of latency).

    Any advice would be appreciated.
     
  2. timjohansson

    timjohansson

    Unity Technologies

    Joined:
    Jul 13, 2016
    Posts:
    473
    In your case you want to setup PhysicsBody smoothing and maybe GhostPredictionSmoohting if you see prediction errors.

    The predicted physics updates at a fixed frame-rate while the client prediction normally performs partial ticks to update at a variable frame-rate. If you want your physics to be at the exact same latency and update frequency as other objects PhysicsBody Extrapolated is closest to that. You could also try Interpolated and see if you like that better, but it means objects have one additional physics tick worth of latency before they move visually. None means the physics positions update at 60Hz no matter what your rendering frequency is - it is usually not what you want. This setting is only for predicted ghosts, it has no effect on interpolated ghosts.

    `GhostPredictionSmoothingSystem.RegisterSmoothingAction` is used to smooth out prediction errors. If the client miss-predicts this action will smoothly interpolate between the incorrectly predicted and corrected state over time. In the case of physics this would apply to the starting position of the prediction. Prediction errors should normally be relatively infrequent if the prediction is setup correctly and network conditions are not terrible. This setting is also only for predicted ghosts and has no effect on interpolated ghosts.

    `SmoothingAction.Clamp/Interpolate/InterpolateAndExtrapolate` control how values in snapshots are interpolated when applied. Predicted ghosts apply the most recent snapshot and never perform any interpolation as it would give bad prediction errors. This only works for interpolated ghosts and has no effect on predicted ghosts.

    The tick-rate of the server is specified in a `ClientServerTickRate` ( https://docs.unity3d.com/Packages/com.unity.netcode@0.50/api/Unity.NetCode.ClientServerTickRate.html ) singleton, specifically the `SimulationTickRate`.
    Create an entity with that component on the server before the clients connect. The client will receive the settings from the server so you do not need to set them there manually.
     
    Samsle and Occuros like this.
  3. Occuros

    Occuros

    Joined:
    Sep 4, 2018
    Posts:
    300
    Thank you @timjohansson,

    This cleared up a lot of questions I had.

    Now the local prediction and physics interaction work already relatively well (still see some 1 frame jumps that aren't smoothed out from time to time.). But the main issue is for remote players.

    If I locally grab a physics-based object (by pinning it to the hand with a joint or by adjusting the physics velocities every frame) it works as expected, but a remote player using the same behaves very differently. It's extremely jittery, in a way that can't be really explained by wrong predictions.

    I tried both, with a Remote Predicted Player, and without adding the cost fields, but in either case, it behaved the same.

    I am currently running the Simulation at the same tick rate as the framerate.

    Might I be missing something that the remote player interactions behave so differently compared to the owner-predicted one?

    Also is there a way to smooth the motion of the predicted entities, as it seems the interpolation or extrapolation of the physics parts isn't enough to smooth out small jitters even for owner predicted entities?
     
  4. timjohansson

    timjohansson

    Unity Technologies

    Joined:
    Jul 13, 2016
    Posts:
    473
    For occasional 1 frame jumps - that sounds like either prediction errors which you can smooth out a bit with GhostPredictionSmoothingSystem.RegisterSmoothingAction or artifacts from using physics extrapolation if you went with that. You could try switching the physics interpolation to interpolated and see if it is better, and separately try out a smoothing action to see which one it is.

    For remote player prediction, fast paced twitchy movement it will always have lots of prediction errors. The client is predicting into the future compared to the server state, and the latest state it has from the server is in the past compared to current server state since transfer takes some time. That means you never have the latest inputs from remote players, the inputs you have are probably at least 200ms or so old. The way the prediction works it will assume the inputs didn't change from the last received when something is missing - you need to think about how you encode the input data to make that a better approximation (for example it might make more sense to send delta for motion instead of absolute position).
    You will probably also need to setup a GhostPredictionSmoothingSystem.RegisterSmoothingAction and tweak the implementation to make it better for your case - but you can never get it perfect due to the pesky laws of physics in the real world.

    If you instead go with interpolated players you will create a join between something that is in the past compared to the server while the physics object is in the future compared to the server. On the server itself they will both be at exactly the same time, on the client they will diff by a large margin - which means you will get large prediction errors in this case too.

    If I understand your use-case correct I would recommend looking at the new prediction switching feature in 0.50, see https://github.com/Unity-Technologi...pleproject/Assets/Samples/PredictionSwitching for a sample. With that you can dynamically switch objects between being interpolated and predicted. So you could run predicted physics for objects you are interacting with locally - based on proximity or something else if that makes more sense in your setup - while other players and objects they are interacting with are all interpolated. It will still break if two users grab the same object or are close and interact directly with each other - but that's not really possible to fix until quantum computers become more mainstream.
     
    Krooq likes this.
  5. Occuros

    Occuros

    Joined:
    Sep 4, 2018
    Posts:
    300
    Thank you @timjohansson that helped a lot.

    Indeed switching it to be predicted (and my better understanding of the prediction loop and not making some wrong assumptions), made it work incredibly well.

    A lot better than I was expecting, especially with joints, etc. it is indeed looking very promising.
     
  6. PhilSA

    PhilSA

    Joined:
    Jul 11, 2013
    Posts:
    1,926
    @timjohansson

    I'm currently looking at an issue where I'm seeing the occasional "1-frame jump" mentioned above on an interpolated+predicted kinematic moving body.

    I made a little test for debugging. My interpolated+predicted kinematic body moves with a constant velocity along the z axis, and I've tried to log the following values on every variable frame right after the TransformSystemGroup where interpolation is applied:
    • LocalToWorld.Position.z (interpPos)
    • PhysicsGraphicalInterpolationBuffer.PreviousTransform.pos.z (prevPos)
    • Translation.Value.z (currPos)
    here's what I'm getting:




    Here's what my test rigidbody ghost looks like (the "initial linear velocity" is what makes it move):


    And here's my debug.Log code:
    Code (CSharp):
    1.  
    2.     [UpdateInWorld(TargetWorld.Client)]
    3.     [UpdateInGroup(typeof(FixedStepSimulationSystemGroup))]
    4.     [UpdateAfter(typeof(BufferInterpolatedRigidBodiesMotion))]
    5.     [UpdateAfter(typeof(ExportPhysicsWorld))]
    6.     public partial class InterpolationDebugPredictionSystem : SystemBase
    7.     {
    8.         protected override void OnUpdate()
    9.         {
    10.             GhostPredictionSystemGroup ghostPredictionSystemGroup = World.GetOrCreateSystem<GhostPredictionSystemGroup>();
    11.             uint tick = ghostPredictionSystemGroup.PredictingTick;
    12.  
    13.             Entities.ForEach((in Translation translation, in InterpolationDebug interpDebug, in PhysicsGraphicalInterpolationBuffer interpBuffer) =>
    14.             {
    15.                 UnityEngine.Debug.Log("PHYSICS PREDICTION UPDATE - tick:" + tick + " - prevPos:" + interpBuffer.PreviousTransform.pos.z + " - currPos:" + translation.Value.z);
    16.             }).Schedule();
    17.         }
    18.     }
    19.  
    20.     [UpdateInWorld(TargetWorld.Client)]
    21.     [UpdateInGroup(typeof(SimulationSystemGroup))]
    22.     [UpdateAfter(typeof(TransformSystemGroup))]
    23.     public partial class InterpolationDebugVariableSystem : SystemBase
    24.     {
    25.         protected override void OnUpdate()
    26.         {
    27.             float mostRecentFixedTime = (float)World.GetOrCreateSystem<RecordMostRecentFixedTime>().MostRecentElapsedTime;
    28.             float timeAhead = (float)Time.ElapsedTime - mostRecentFixedTime;
    29.  
    30.             Entities.ForEach((in LocalToWorld ltw, in Translation translation, in InterpolationDebug interpDebug, in PhysicsGraphicalInterpolationBuffer interpBuffer) =>
    31.             {
    32.                 UnityEngine.Debug.Log("<color=#00FF00> VARIABLE UPDATE - timeAhead:" + timeAhead + " - interpPos:" + ltw.Position.z + " - prevPos:" + interpBuffer.PreviousTransform.pos.z + " - currPos:" + translation.Value.z + "</color>");
    33.             }).Schedule();
    34.         }
    35.     }
    36.  

    There is something fishy going on in the two highlighted lines here: On one frame the "interpPos" is exactly the same value as "currPos", and then on the next frame, the "interpPos" goes back a bit. I think this is where the jitter or "1-frame jump" is happening. The "timeAhead" value is the same as the one calculated by the
    SmoothRigidBodiesGraphicalMotion system; it's "Time.ElapsedTime -
    m_RecordMostRecentFixedTime.MostRecentElapsedTime". It kinda looks like something triggers a variable update at a "Time.ElapsedTime" that is exactly the same as the elapsedTime of the latest FixedUpdate, so the "timeAhead" value is 0, and the interpolation code decides to skip interpolation when elapsedTime is 0. This problem doesn't happen in offline mode, because the "elapsedTime" of a variableUpdate will pretty much never perfectly coincide with the elapsedTime of a fixedUpdate in practice.

    In short: I believe this interpolation jitter happens because once in a while, something in NetCode sets a "Time.ElapsedTime" for the variable update that is exactly the same as the elapsed time of the last physics prediction update (or of the latest tick). After a bit more digging, I've discovered that this happens in "GhostPredictionSystemGroup" when "ClientSimulationSystemGroup.serverTickFraction == 1". Now we'd have to find out if there is a good reason for this, and if it can be changed. Looks like the serverTickFraction is set to 1 in "ClientSimulationSystemGroup", with the comment: "// If the tick is within +/- 5% of a frame from matching a tick - just use the actual tick instead". What was the original intention for this bit of code?

    But aside from all this, I have some vague doubts about the way we save the "previous position" for interpolation during the physics prediction group. Shouldn't it be saved only once before all of the several physics prediction updates on any given frame are processed? Basically it would make sure that interpolation works from whatever transform was last rendered to whatever transform is up-to-date in simulation, regardless of if there have been prediction corrections or not. I could be wrong though
     
    Last edited: May 30, 2022
  7. PhilSA

    PhilSA

    Joined:
    Jul 11, 2013
    Posts:
    1,926
    @timjohansson
    Too much info in my last post for it to be easily digestible, so I'll sum things up:

    I've solved the interpolation jitter issue by removing all the lines of code in ClientSimulationSystemGroup.cs under the "// If the tick is within +/- 5% of a frame from matching a tick - just use the actual tick instead" comment. This is what was happening;
    • This +/- 5% rounding code was setting the ClientSimulationSystemGroup.serverTickFraction to 1
    • When serverTickFraction is 1, the GhostPredictionSystemGroup sets a Time.ElapsedTime that is exactly the same as the elapsedTime of the latest full tick (or of the latest physics prediction update)
    • When the elapsedTime is the same as the one of the latest physics update, the SmoothRigidBodiesGraphicalMotion calculates a timeAhead of 0, and when this value is 0, it skips interpolation and directly applies the transform of bodies in simulation
    This is what causes a 1-frame jitter once in a while. Just to be clear; I don't think making SmoothRigidBodiesGraphicalMotion use the "previous transform" when timeAhead is 0 would fully fix the issue. The fix would be about making sure Time.ElapsedTime is always 100% correct, because the interpolation code needs that accuracy to behave properly in any case. Any rounding of the time will also make all movement feel less fluid
     
    Last edited: May 30, 2022
    Krooq, TRS6123 and hippocoder like this.
  8. timjohansson

    timjohansson

    Unity Technologies

    Joined:
    Jul 13, 2016
    Posts:
    473
    To me it sounds like there is a bug where SmoothRigidBodiesGraphicalMotion cannot handle timeAhead == 0 and fixing that would fix the 1 frame jitter.

    Only changing the 5% cutoff for tick fraction sounds like it will just make the bug more rare and mostly hide the incorrect smoothing action, it doesn't seem like the root cause of the bug you are seeing.
    The rounding to full ticks is there to avoid running simulation steps with tiny delta time, since we constantly adjust the time scale to stay in sync with the server - and the time from running the simulation until image being on screen has some variance - applying some rounding doesn't affect smoothness all that much. 5% seems like it could be a bit high though - should probably be lower by default and configurable.

    Are you using interpolate or extrapolate for physics smoothing? It looks to me like the smoothing calculations might be incorrect when using interpolate - seems like they should use previous transform when time ahead is 0 in that case - but they seem fine when using extrapolate, so I assume you are using interpolate?
     
  9. PhilSA

    PhilSA

    Joined:
    Jul 11, 2013
    Posts:
    1,926
    I'm using interpolate, yes.

    My impression is that both parts can cause issues:
    • The interpolation skip when timeahead is 0 causes the most blatant jitter
    • The 5% rounding causes all movement (not just interpolation) to be slightly less fluid than it should be, due to accelerating/decelerating time-based movement only on certain frames. Visually, it kinda would have a similar effect to a framerate that has spikes once in a while (although they would be spikes of only 5%, so I'm probably exaggerating the impact this actually has in practice)
    However, since you're saying the timescale is constantly adjusted to keep up with server, then I suppose the tiny accelerations/decelerations of movement might be inescapable even if we removed the rounding, and so I'd agree that only the interpolation system would need a fix

    I'd find it useful to have the rounding % configurable though
     
    Last edited: May 31, 2022