From 3415b510c25fec13480cfbf4774c0a75c3f459d0 Mon Sep 17 00:00:00 2001 From: Noel Stephens Date: Tue, 18 Aug 2026 20:10:01 -0500 Subject: [PATCH 1/2] fix: Lerp smoothing frame rate dependence and freeze at maximum interpolation time The lerp smoothing pass used by the Lerp and SmoothDampening interpolation types applied a fixed factor of 1.0 minus the maximum interpolation time once per frame, with no delta time. The wall clock smoothing rate therefore scaled with the frame rate, so the same setting smoothed by different amounts on different hardware. The factor is now raised to the number of 60fps reference frames elapsed, which makes the rate a function of elapsed time. Results at 60fps are unchanged for every legal setting. Separately, a maximum interpolation time of 1.0 (the upper bound of the inspector range) produced a factor of exactly 0, so the interpolated value never advanced and the transform stopped moving entirely on all three axes. The retained portion is now clamped just below 1.0. This is an independent defect, as raising 1.0 to any power is still 1.0. The LegacyLerp path was already frame rate correct and is unchanged. The documentation on the lerp smoothing fields described the LegacyLerp formula for all interpolation types and has been corrected. --- com.unity.netcode.gameobjects/CHANGELOG.md | 2 + .../BufferedLinearInterpolator.cs | 37 +++++++-- .../Runtime/Components/NetworkTransform.cs | 19 +++-- .../Tests/Editor/InterpolatorTests.cs | 82 +++++++++++++++++++ 4 files changed, 130 insertions(+), 10 deletions(-) diff --git a/com.unity.netcode.gameobjects/CHANGELOG.md b/com.unity.netcode.gameobjects/CHANGELOG.md index 85157fe510..5ecb0cfd48 100644 --- a/com.unity.netcode.gameobjects/CHANGELOG.md +++ b/com.unity.netcode.gameobjects/CHANGELOG.md @@ -29,6 +29,8 @@ Additional documentation and release notes are available at [Multiplayer Documen ### Fixed +- Issue where lerp smoothing was applied per frame instead of over time, which caused the `Lerp` and `SmoothDampening` interpolation types to smooth by different amounts at different frame rates. Results at 60fps are unchanged. (#TBD) +- Issue where setting a maximum interpolation time of 1.0 would stop a `NetworkTransform` from interpolating at all when using the `Lerp` or `SmoothDampening` interpolation types. (#TBD) - Issue with not being able to spawn initially disabled in-scene placed objects. (#4093) - Issue with pre-instantiated network prefab instances being marked as in-scene placed. Now pre-instantiated network prefabs are dynamically spawned. (#4093) - Issue where a user could spawn runtime created `NetworkObject` that has a GlobalObjectIdHash of zero. These are not valid instances and will no longer be allowed to spawn. (#4093) diff --git a/com.unity.netcode.gameobjects/Runtime/Components/Interpolator/BufferedLinearInterpolator.cs b/com.unity.netcode.gameobjects/Runtime/Components/Interpolator/BufferedLinearInterpolator.cs index 8ee288b02f..7e9616dab3 100644 --- a/com.unity.netcode.gameobjects/Runtime/Components/Interpolator/BufferedLinearInterpolator.cs +++ b/com.unity.netcode.gameobjects/Runtime/Components/Interpolator/BufferedLinearInterpolator.cs @@ -209,12 +209,23 @@ public void Reset(T currentValue) internal bool LerpSmoothEnabled; /// - /// Determines how much smoothing will be applied to the 2nd lerp when using the (i.e. lerping and not smooth dampening). + /// The frame rate that is relative to when lerp smoothing. + /// + private const float k_LerpSmoothReferenceFrameRate = 60.0f; + + /// + /// Keeps a of 1.0f from retaining the entire delta each frame, + /// which would stop the value from ever advancing towards the target. + /// + private const float k_MaximumLerpSmoothRetention = 0.99f; + + /// + /// Determines how much smoothing will be applied to the 2nd lerp. /// /// - /// There's two factors affecting interpolation:
- /// - Buffering: Which can be adjusted in set in the .
- /// - Interpolation time: The divisor applied to delta time where the quotient is used as the lerp time. + /// Higher values are smoother, lower values are more precise. The amount of smoothing applied is + /// frame rate independent.
+ /// Buffering also affects interpolation and can be adjusted via . ///
[Range(0.016f, 1.0f)] public float MaximumInterpolationTime = 0.1f; @@ -420,6 +431,22 @@ internal void ResetCurrentState() } } + /// + /// Calculates the frame rate independent lerp smoothing "t" for the current frame. + /// + /// + /// Raising the retained portion to the number of reference frames elapsed makes the smoothing rate + /// a function of elapsed time rather than of how often this is called. + /// + /// The last frame time. + /// The lerp smoothing time to apply for this frame. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private float GetLerpSmoothTime(float deltaTime) + { + var retained = Mathf.Clamp(MaximumInterpolationTime, 0.0f, k_MaximumLerpSmoothRetention); + return 1.0f - Mathf.Pow(retained, deltaTime * k_LerpSmoothReferenceFrameRate); + } + /// /// Interpolation Update to use when smooth dampening is enabled on a . /// @@ -459,7 +486,7 @@ internal T Update(float deltaTime, double tickLatencyAsTime, double minDeltaTime if (LerpSmoothEnabled) { // Apply the smooth lerp to the target to help smooth the final value. - InterpolateState.CurrentValue = Interpolate(InterpolateState.CurrentValue, InterpolateState.NextValue, Mathf.Clamp(1.0f - MaximumInterpolationTime, 0.0f, 1.0f)); + InterpolateState.CurrentValue = Interpolate(InterpolateState.CurrentValue, InterpolateState.NextValue, GetLerpSmoothTime(deltaTime)); } else { diff --git a/com.unity.netcode.gameobjects/Runtime/Components/NetworkTransform.cs b/com.unity.netcode.gameobjects/Runtime/Components/NetworkTransform.cs index de4e86999d..33728ab4ca 100644 --- a/com.unity.netcode.gameobjects/Runtime/Components/NetworkTransform.cs +++ b/com.unity.netcode.gameobjects/Runtime/Components/NetworkTransform.cs @@ -1136,7 +1136,7 @@ public enum InterpolationTypes /// Uses a 1 to 2 phase interpolation approach where:
/// /// The first phase lerps from the previous state update value to the next state update value. - /// The second phase (optional) performs lerp smoothing where the current respective transform value is lerped towards the result of the first phase at a rate of 1.0 minus the respective maximum interpolation time. + /// The second phase (optional) performs lerp smoothing where the current respective transform value is lerped towards the result of the first phase at a frame rate independent rate determined by the respective maximum interpolation time. /// /// /// @@ -1156,7 +1156,7 @@ public enum InterpolationTypes /// Uses a 1 to 2 phase smooth dampening approach where:
/// /// The first phase smooth dampens towards the current tick state update being processed by the accumulated delta time relative to the time to target. - /// The second phase (optional) performs lerp smoothing where the current respective transform value is lerped towards the result of the first phase at a rate of delta time divided by the respective max interpolation time. + /// The second phase (optional) performs lerp smoothing where the current respective transform value is lerped towards the result of the first phase at a frame rate independent rate determined by the respective maximum interpolation time. /// /// /// @@ -1236,7 +1236,10 @@ public enum InterpolationTypes /// Controls position interpolation smoothing. /// /// - /// When enabled, the will apply a final lerping pass where the "t" parameter is calculated by dividing the frame time divided by the . + /// When enabled, the will apply a final lerping pass towards + /// the interpolated result at a rate determined by .
+ /// This is frame rate independent for all , but the same value will not + /// produce the same result under as it does under the others. ///
public bool PositionLerpSmoothing = true; private bool m_PreviousPositionLerpSmoothing; @@ -1257,7 +1260,10 @@ public enum InterpolationTypes /// Controls rotation interpolation smoothing. /// /// - /// When enabled, the will apply a final lerping pass where the "t" parameter is calculated by dividing the frame time divided by the . + /// When enabled, the will apply a final lerping pass towards + /// the interpolated result at a rate determined by .
+ /// This is frame rate independent for all , but the same value will not + /// produce the same result under as it does under the others. ///
public bool RotationLerpSmoothing = true; private bool m_PreviousRotationLerpSmoothing; @@ -1278,7 +1284,10 @@ public enum InterpolationTypes /// Controls scale interpolation smoothing. /// /// - /// When enabled, the will apply a final lerping pass where the "t" parameter is calculated by dividing the frame time divided by the . + /// When enabled, the will apply a final lerping pass towards + /// the interpolated result at a rate determined by .
+ /// This is frame rate independent for all , but the same value will not + /// produce the same result under as it does under the others. ///
public bool ScaleLerpSmoothing = true; private bool m_PreviousScaleLerpSmoothing; diff --git a/com.unity.netcode.gameobjects/Tests/Editor/InterpolatorTests.cs b/com.unity.netcode.gameobjects/Tests/Editor/InterpolatorTests.cs index 520633a961..1552e7ab7a 100644 --- a/com.unity.netcode.gameobjects/Tests/Editor/InterpolatorTests.cs +++ b/com.unity.netcode.gameobjects/Tests/Editor/InterpolatorTests.cs @@ -301,5 +301,87 @@ public void TestDuplicatedValues() Assert.That(interp, Is.EqualTo(2f)); // Since there is no extrapolation, the rest of this test was removed. } + + #region Lerp Smoothing + + // Deliberately not round numbers, so exactly representable values cannot mask a defect. + private const double k_SmoothTickInterval = 1.0d / 30.0d; + private const int k_SmoothTickLatency = 2; + private const float k_SmoothStartValue = 3.17f; + private const float k_SmoothVelocity = 2.3f; + private const double k_SmoothMoveDuration = 1.53d; + private const double k_SmoothTotalDuration = 2.11d; + + /// + /// Drives the lerp and smooth dampening interpolation path with lerp smoothing enabled, where an + /// authority moves at a constant velocity and then holds still while the non-authority renders at + /// . + /// + /// The interpolated value once has elapsed. + private float RunLerpSmoothing(float maximumInterpolationTime, float frameDeltaTime, bool lerp) + { + var interpolator = new BufferedLinearInterpolatorFloat + { + MaximumInterpolationTime = maximumInterpolationTime, + LerpSmoothEnabled = true, + }; + interpolator.ResetTo(k_SmoothStartValue, 0.0d); + + var restValue = k_SmoothStartValue + (float)(k_SmoothVelocity * k_SmoothMoveDuration); + var maxDeltaTime = k_SmoothTickLatency * k_SmoothTickInterval; + var nextTick = 1; + var currentValue = k_SmoothStartValue; + + for (var time = 0.0d; time < k_SmoothTotalDuration; time += frameDeltaTime) + { + // Deliver every state update whose send time has already passed. + while (nextTick * k_SmoothTickInterval <= time) + { + var sentTime = nextTick * k_SmoothTickInterval; + var sentValue = sentTime <= k_SmoothMoveDuration + ? k_SmoothStartValue + (float)(k_SmoothVelocity * sentTime) + : restValue; + interpolator.AddMeasurement(sentValue, sentTime); + nextTick++; + } + + currentValue = interpolator.Update(frameDeltaTime, time - maxDeltaTime, k_SmoothTickInterval, maxDeltaTime, lerp); + } + + return currentValue; + } + + /// + /// Lerp smoothing must still advance the value at 1.0f, the maximum legal value of the + /// family of fields. + /// + [Test] + public void LerpSmoothingDoesNotFreezeAtMaximumInterpolationTime([Values] bool lerp) + { + var result = RunLerpSmoothing(1.0f, 1.0f / 60.0f, lerp); + + Assert.That(result, Is.GreaterThan(k_SmoothStartValue + 1.0f), + $"Interpolated value only advanced {result - k_SmoothStartValue} from {k_SmoothStartValue} over " + + $"{k_SmoothTotalDuration}s of authority motion. The maximum interpolation time froze the transform."); + } + + /// + /// The rate at which lerp smoothing converges must not depend on the frame rate. + /// + [Test] + public void LerpSmoothingIsFrameRateIndependent() + { + // Heavier than the default, where the frame rate dependency is measurable. + const float maximumInterpolationTime = 0.87f; + + var atThirtyFps = RunLerpSmoothing(maximumInterpolationTime, 1.0f / 30.0f, true); + var atTwoFortyFps = RunLerpSmoothing(maximumInterpolationTime, 1.0f / 240.0f, true); + + Assert.That(atThirtyFps, Is.EqualTo(atTwoFortyFps).Within(0.01f), + $"The same elapsed time and interpolation settings produced {atThirtyFps} at 30fps but " + + $"{atTwoFortyFps} at 240fps. The smoothing rate is scaling with the frame rate."); + } + + #endregion } } From 363c94dbbb7fb0e29eaa06737b84ec86e190805c Mon Sep 17 00:00:00 2001 From: Noel Stephens Date: Wed, 19 Aug 2026 08:49:53 -0500 Subject: [PATCH 2/2] update Adding PR number to change log entries. --- com.unity.netcode.gameobjects/CHANGELOG.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/com.unity.netcode.gameobjects/CHANGELOG.md b/com.unity.netcode.gameobjects/CHANGELOG.md index 5ecb0cfd48..7f14295d02 100644 --- a/com.unity.netcode.gameobjects/CHANGELOG.md +++ b/com.unity.netcode.gameobjects/CHANGELOG.md @@ -29,8 +29,8 @@ Additional documentation and release notes are available at [Multiplayer Documen ### Fixed -- Issue where lerp smoothing was applied per frame instead of over time, which caused the `Lerp` and `SmoothDampening` interpolation types to smooth by different amounts at different frame rates. Results at 60fps are unchanged. (#TBD) -- Issue where setting a maximum interpolation time of 1.0 would stop a `NetworkTransform` from interpolating at all when using the `Lerp` or `SmoothDampening` interpolation types. (#TBD) +- Issue where lerp smoothing was applied per frame instead of over time, which caused the `Lerp` and `SmoothDampening` interpolation types to smooth by different amounts at different frame rates. Results at 60fps are unchanged. (#4132) +- Issue where setting a maximum interpolation time of 1.0 would stop a `NetworkTransform` from interpolating at all when using the `Lerp` or `SmoothDampening` interpolation types. (#4132) - Issue with not being able to spawn initially disabled in-scene placed objects. (#4093) - Issue with pre-instantiated network prefab instances being marked as in-scene placed. Now pre-instantiated network prefabs are dynamically spawned. (#4093) - Issue where a user could spawn runtime created `NetworkObject` that has a GlobalObjectIdHash of zero. These are not valid instances and will no longer be allowed to spawn. (#4093)