# a better lerp for smooth movement Game developers often use linear interpolation (Lerp) as a very easy way to get smoother movement. However, the traditional `Mathf.Lerp` can lead to framerate-dependent behavior. Enter "Zeno's lerp" - a frame-rate independent alternative. ## The Problem with Traditional usage of Lerp Consider this common Unity code: ```csharp void Update() { transform.position = Vector3.Lerp(transform.position, targetPosition, 0.1f); } ``` This approach has two main issues: 1. **Framerate-dependence**: movement speed changes with framerate. You can help a little by replacing `0.1f` with `0.1f * Time.deltaTime`, but the results are still somewhat framerate-dependent. As a simple proof, compare calling it once with `Time.deltaTime = 10`, which results in `transform.position` going all the way to `targetPosition`, and calling it 10 times with `Time.deltaTime = 1`, which results in `transform.position` going only 65% of the way to `transform.position`. 2. The constant factor (`0.1f` here) is not very intuitive and hard to tune IMO. ## Introducing Zeno's Lerp What I call Zeno's lerp, solves these issues: ```csharp public static class ZenoExtensions { public static Vector3 Zeno(this Vector3 current, Vector3 target, float timeConstant) { float alpha = 1f - Mathf.Exp(-Time.deltaTime / timeConstant); return current + alpha * (target - current); } } // Usage void Update() { transform.position = transform.position.Zeno(targetPosition, 0.1f); } ``` ## Why Zeno's Lerp is Better 1. **Framerate Independence**: Regardless of how frequently it is called, `Zeno` will result in the same value for the same period of time. 2. **Intuitive**: The `timeConstant` parameter represents the time it takes to close about 63% of the distance to the target, making it easier to tune. Just set it to about how long you want it to take for it to cross half the distance. 3. **Built in Time.deltaTime**: I don't like having to write `Time.deltaTime` every time, so I just built it in to this implementation on the assumption that you'll basically always want it. It's not as nice in the sense that it's not built-in to Unity or any other game engine I know of, but it's easy to implement for various types (Vector2, float, etc.) as extension methods. ## Comparison Interactive comparison: [Traditional VS Zeno](https://observablehq.com/d/135d95440bd1b265). Here the solid lines represent the behavior of `Zeno` and the dashed lines represent the traditional behavior of `Lerp`. ![[lerps.png]] You can see the behavior of `Zeno` (solid lines) is totally framerate-independent. Even at 1fps (solid red line), it ends up at the same place after its singular frame as the 5fps and 30fps cases do after 1 second. The behavior of `Lerp` (dashed lines) is not at all framerate-independent. At 5fps (dashed blue line), it shoots out above the the behavior at 30fps (dashed green) and it never goes back. As the framerate gets lower it gets even worse. I didn't even bother graphing Lerp at 1fps, because it's so bad it ruins the whole graph. (Although if you use a clamped Lerp it is not completely disastrous.) However, you can see that at 30fps (dashed green), `Lerp` approximates `Zeno` quite well. In fact, in the limit the two modes of interpolation are identical. So `Zeno` essentially gives you what the traditional `Lerp` would have given you if you had infinite FPS. #### Reproduce this graph ```python import numpy as np import matplotlib.pyplot as plt # Constants time_constant = 0.3 total_time = 1 # Total time to simulate initial_position = 0 target_position = 1 # Time arrays for different frame rates time_1fps = np.linspace(0, total_time, 2) # 1 update per second time_5fps = np.linspace(0, total_time, 6) # 10 updates per second time_30fps = np.linspace(0, total_time, 31) # 30 updates per second def zeno_lerp(current, target, delta_time): alpha = 1 - np.exp(-delta_time / time_constant) return current + alpha * (target - current) def traditional_lerp(current, target, t): return current + t * (target - current) # Simulate movement for different frame rates using Zeno's lerp positions_zeno_1fps = [initial_position] positions_zeno_5fps = [initial_position] positions_zeno_30fps = [initial_position] for dt in np.diff(time_1fps): positions_zeno_1fps.append(zeno_lerp(positions_zeno_1fps[-1], target_position, dt)) for dt in np.diff(time_5fps): positions_zeno_5fps.append(zeno_lerp(positions_zeno_5fps[-1], target_position, dt)) for dt in np.diff(time_30fps): positions_zeno_30fps.append( zeno_lerp(positions_zeno_30fps[-1], target_position, dt) ) # Simulate movement for different frame rates using traditional lerp with t=0.1 positions_traditional_1fps = [initial_position] positions_traditional_5fps = [initial_position] positions_traditional_30fps = [initial_position] positions_traditional_144fps = [initial_position] for _ in range(1, len(time_1fps)): positions_traditional_1fps.append( traditional_lerp( positions_traditional_1fps[-1], target_position, 1 * (1 / time_constant) ) ) for _ in range(1, len(time_5fps)): positions_traditional_5fps.append( traditional_lerp( positions_traditional_5fps[-1], target_position, 1 / 5 * (1 / time_constant), ) ) for _ in range(1, len(time_30fps)): positions_traditional_30fps.append( traditional_lerp( positions_traditional_30fps[-1], target_position, 1 / 30 * (1 / time_constant), ) ) # Plotting plt.figure(figsize=(12, 8)) plt.plot(time_1fps, positions_zeno_1fps, "ro-", label="Zeno Lerp 1 FPS") plt.plot(time_5fps, positions_zeno_5fps, "bo-", label="Zeno Lerp 5 FPS") plt.plot(time_30fps, positions_zeno_30fps, "go-", label="Zeno Lerp 30 FPS") # plt.plot(time_1fps, positions_traditional_1fps, "r:", label="Traditional Lerp 1 FPS") plt.plot(time_5fps, positions_traditional_5fps, "b:", label="Traditional Lerp 5 FPS") plt.plot(time_30fps, positions_traditional_30fps, "g:", label="Traditional Lerp 30 FPS") plt.title("Comparison of Zeno's Lerp vs Traditional Lerp at Different Frame Rates") plt.xlabel("Time (s)") plt.ylabel("Position") plt.legend() plt.grid(True) plt.show() ```