diff --git a/com.unity.netcode.gameobjects/CHANGELOG.md b/com.unity.netcode.gameobjects/CHANGELOG.md index 931ae821f3..2cff19c009 100644 --- a/com.unity.netcode.gameobjects/CHANGELOG.md +++ b/com.unity.netcode.gameobjects/CHANGELOG.md @@ -22,6 +22,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. (#4130) +- 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. (#4130) ### Security 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 152cea793b..5228005f50 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 666183abe6..e946c4db86 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 } }