技术博客

物理插值:从 3.x 插件到 4.x 内置,平滑是怎么来的

第 71 / 149 篇 · 2025-04-15 · godot · 物理引擎渲染同步性能优化

60Hz 的物理帧率配 144Hz 的显示器,是最常见的性能与流畅度矛盾:物理跑 60 步够用,但渲染 144 帧只显示 60 个不同位置,肉眼可见一顿一顿。老办法是把物理帧率拉到 144,但物理成本翻 2.4 倍。物理插值(physics interpolation)给了另一条路:物理照常 60Hz 跑,渲染帧显示的位置是两个物理状态之间的插值。Godot 3.x 时代这功能要靠社区插件(Physics Interpolation Addon),4.3 起内置进引擎,一个开关解决。这篇讲它背后的数学和实现细节。

插值公式与边界处理

设最近一次完成的物理步把状态从 $x_{t-1}$ 推进到 $x_t$,下一次物理步在 $t + \Delta t$ 到来。当前渲染时刻距 $t$ 过去了 $\alpha$($0 \le \alpha < 1$ 个物理步长)。插值显示位置:

$$ x_{\text{display}} = (1 - \alpha)\, x_{t-1} + \alpha\, x_t $$

$\alpha = 0$ 时显示上一物理帧的位置,$\alpha \to 1$ 时逼近当前物理帧。视觉上物体连续移动,因为相邻渲染帧之间 $\alpha$ 均匀增长,$x_{\text{display}}$ 均匀变化。

一个容易漏掉的细节:刚体有朝向,四元数插值不能分量线性插(会过冲、会翻转到错误方向),要用 nlerp(归一化线性插值)或 slerp。内置实现里朝向走 Transform3D 的 interpolate_with,四元数做球面插值。自己写插件时这是第一坑——物体直线运动正常、旋转时抽搐,八成是四元数直接 lerp 分量没归一化。

第二个细节是外推(extrapolation)的选择:公式只用到 $x_{t-1}$ 和 $x_t$,显示位置永远不超前于物理模拟——最坏情况延迟一个物理步的显示(16.6ms),好处是永远不会"看到未来"导致碰撞穿帮。有些引擎的插值会外推到 $x_{t+1}$,Godot 内置不做,延迟换稳妥。

引擎内置版做了什么

3.x 的社区插件靠脚本层在每帧 _process 里手动记录/恢复 Transform,既慢又和引擎的变换脏标记系统打架(CanvasItem 的全局变换缓存见第 16 篇)。4.3 内置后,插值下沉到 SceneTree 层级:项目设置 physics/common/physics_interpolation 打开后,每帧渲染前树会算出一个帧级插值系数,需要插值的节点(刚体、物理骨骼、以及打了标记的节点)在全局变换计算时套用上面的公式,和脏标记传播结合,一次遍历搞定。

这比脚本插件省在两点:

  1. 零分配、零脚本调用:插值发生在变换求值内部,不经过 GDScript;
  2. 只插值"动了的"东西:静止物体(StaticBody、未激活刚体)直接显示当前物理位置,不存历史状态,内存开销恒定。

实测与坑

实测数据(简单场景,60Hz 物理 / 144Hz 渲染,刚体自由落体):不开插值,位置更新呈台阶状,每 2.4 个渲染帧重复一次位置;开启后轨迹严格线性。CPU 开销基本可以忽略——每帧多做一次线性组合,真正成本是每节点多存一份上一物理变换(16 个 float 的 Transform3D)。

但有几个坑实打实存在:

  • 摄像机要不要插:跟随物理物体的相机(Camera3D 直接 look_at 刚体)插值后画面平滑了,但输入延迟感增加——显示位置滞后物理位置最多一个物理步。手感敏感的游戏(平台跳跃)建议相机用脚本平滑而不是直接绑刚体,或者在插值开启后重新调输入响应参数。
  • ** _physics_process 里的视觉操作会打架**:你如果在物理帧里改某个 Sprite 的位置(比如代码驱动的飘带),插值系统不知道这是"视觉操作"还是"物理状态",会拿上一帧物理位置来插,出现诡异的回拉。解决办法是把视觉逻辑挪到 _process,或者对该节点关闭插值。
  • Teleports 需要清历史:瞬间移动物体(重生、传送)后,插值会从旧位置平滑"滑"到新位置——拖一道残影。引擎为此提供通知:物理传送后要重置插值状态(NOTIFICATION_RESET_PHYSICS_INTERPOLATION 或直接 reset_physics_interpolation()),把 $x_{t-1}$ 设成 $x_t$。
  • 和物理同步事件的关系:body_entered 这类物理信号在物理步内发出(第 14 篇),插值不改变物理时机,只改变显示位置。所以"碰撞特效贴显示位置还是物理位置"要自己选:贴显示位置(视觉正确)、触发判定用物理位置(逻辑正确),两者差一个插值偏移。

什么时候不用它

渲染帧率被 V-Sync 锁在 60、物理也是 60 的场景,插值系数恒为 0 或 1,白存历史状态。竞技类对输入延迟极度敏感的项目,16.6ms 的显示延迟可能是减分项——这时反而是物理帧率提到 120+ 的正解。以及需要"渲染帧精确对应物理状态"做录制回放的场合(帧同步见第 19 篇),插值会让回放画面和逻辑帧错位。

60Hz 物理 × 144Hz 渲染:插值系数 α 的滚动 物理步 t−1 物理步 t 物理步 t+1 t+2 渲染帧 α: 0.25 → 0.5 → 0.75 → 0(下一步) x(t−1)→x(t):真实运动 黄点:渲染帧显示的插值位置,折线被抹平

本文基于 Godot 4.3+ 内置物理插值(physics/common/physics_interpolation、SceneTree 帧级插值系数、Transform3D::interpolate_with),机制对照 3.x 社区插件 Physics Interpolation Addon。