diff --git a/com.unity.netcode.gameobjects/CHANGELOG.md b/com.unity.netcode.gameobjects/CHANGELOG.md
index 85157fe510..7f14295d02 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. (#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)
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
}
}