https://gafferongames.com/post/snapshot_interpolation/?utm_source=chatgpt.com
介绍
大家好,我是Glenn Fiedler,欢迎来到网络物理学。
在上一篇文章中,我们使用确定性帧同步技术实现了物理模拟的网络化。现在,在本文中,我们将使用一种完全不同的技术——快照插值——来实现同样的模拟网络化。
背景
虽然确定性帧同步在带宽方面非常高效,但并不总是能够确保模拟具有确定性。跨平台浮点确定性很难。
此外,随着玩家数量的增加,确定性帧同步会变得棘手:你无法模拟第 n 帧,除非你收到所有玩家在该帧的输入,否则玩家最终只能等待滞后最严重的玩家。因此,我建议最多 2-4 名玩家使用确定性帧同步。
因此,如果您的模拟并非确定性,或者您希望更高的玩家数量,那么您需要一种不同的技术。快照插值非常适合。它在很多方面与确定性帧同步截然相反:快照插值不是运行两个模拟,一个在左侧,一个在右侧,并使用完美的确定性和同步输入来保持同步,而是根本不会在右侧运行任何模拟!
快照
相反,我们从左侧的模拟中捕获所有相关状态的快照并将其传输到右侧,然后在右侧我们使用这些快照重建模拟的视觉近似值,所有这些都无需运行模拟本身。
作为第一步,让我们发送渲染每个立方体所需的状态:
struct CubeState{bool interacting;vec3f position;quat4f orientation;};
我相信你现在已经知道这项技术的代价是增加了带宽使用量。带宽使用量大幅增加。别胡思乱想,因为快照包含了整个模拟的视觉状态。稍微计算一下,我们就能发现每个立方体序列化后的大小只有 225 位,也就是 28.1 字节。由于我们的模拟中有 900 个立方体,这意味着每个快照大约有 25 KB。这可真是个大问题!
现在,我希望大家放松一下,深吸一口气,想象一下我们生活在这样一个世界:我实际上可以通过互联网以每秒 60 次的速度发送这么大的数据包,而不会导致网络崩溃。想象一下,我拥有 FIOS (我有),或者我正通过主干链路连接到另一台同样位于主干网络上的计算机。想象一下,我住在韩国。您可以采取任何措施来消除疑虑,但最重要的是,别担心,因为我将在下一篇文章中用整篇文章向您展示如何优化快照带宽。
当我们以数据包形式发送快照数据时,我们会在数据包顶部包含一个 16 位序列号。此序列号从零开始,并随着每个数据包的发送而增加。接收时,我们会使用此序列号来确定数据包中的快照是否比最近收到的快照更新或更旧。如果更旧,则将其丢弃。
每一帧我们只在右侧渲染收到的最新快照:
仔细观察,即使我们尽可能快地发送数据(每帧一个数据包),你仍然会在右侧看到卡顿。这是因为互联网无法保证每秒发送 60 个数据包的间隔恰好是 1/60 秒。数据包存在抖动。在某些帧中,你会收到两个快照数据包。而在其他帧中,你一个数据包也收不到。
抖动和故障
这其实在你刚开始联网的时候很常见。你一开始在局域网上玩游戏,发现你可以非常快速地发送数据包(60pps),而且大多数时候你的游戏看起来都很棒,因为在局域网上,这些数据包的到达速率实际上和发送速率是一样的……然后你开始尝试通过无线网络或互联网玩游戏,你开始遇到卡顿。别担心。有办法解决这个问题!
首先,我们来看看这种简单的方法会占用多少带宽。每个数据包大小为 25312.5 字节,加上 28 字节的 IP 和 UDP 报头以及 2 字节的序列号。也就是说,每个数据包 25342.5 字节,以每秒 60 个数据包的速度计算,总共每秒 1520550 字节,也就是 11.6 兆比特/秒。现在肯定有互联网连接可以支持这种流量……但说实话,考虑到抖动问题,每秒发送 60 次数据包并不会给我们带来太多好处,所以我们稍微降低一下,每秒只发送 10 个快照:
您可以在上方看到效果。右侧的效果不太好,但至少我们将带宽减少了六倍,降至约 2 兆比特/秒。我们肯定在朝着正确的方向前进。
线性插值
现在来看看快照的技巧。我们的做法是,不是立即渲染接收到的快照数据,而是将快照缓存在插值缓冲区中一小段时间。这个插值缓冲区会将快照保留一段时间,这样您不仅能获得想要渲染的快照,而且从统计上讲,您也很有可能获得下一个快照。然后,随着右侧时间的推进,我们会在两个略微延迟的快照的位置和方向之间进行插值,从而营造出平滑运动的视觉效果。实际上,我们用少量的额外延迟换取了流畅性。
您可能会惊讶于使用 10pps 的线性插值效果有多么好:
仔细观察,你会发现右侧有一些瑕疵。首先是玩家方块悬浮在空中时出现的细微位置抖动。这是你的大脑在位置插值采样点处检测到的一阶不连续性造成的。另一个瑕疵发生在一堆方块被放入一个块魂球中时,你可以看到随着旋转速度的增加和减少,出现一种“脉动”。这是因为附着的方块在围绕玩家方块旋转的两个采样点之间进行线性插值,有效地通过玩家方块进行插值,因为它们在圆上两点之间采用最短的线性路径。
埃尔米特插值
我觉得这些瑕疵令人无法接受,但我不想通过提高数据包发送速率来修复它们。让我们看看在相同发送速率下,我们能做些什么来让它看起来更好。我们可以尝试的一件事是升级到更精确的位置插值方案,该方案在位置样本之间进行插值,同时考虑每个采样点的线速度。
这可以用Hermite 样条(发音为“air-mitt”)来实现
与其他控制点间接影响曲线的样条曲线不同,Hermite 样条曲线保证通过起点和终点,同时匹配起点和终点的速度。这意味着速度在采样点之间平滑,并且块魂球中的立方体倾向于围绕立方体旋转,而不是以高速穿过立方体进行插值。
上图展示了 10pps 时位置的 Hermite 插值。由于我们需要在快照中包含每个立方体的线速度,因此带宽略有增加,但我们能够在相同的发送速率下显著提高质量。我再也看不到任何瑕疵了。回过头来将其与原始的、未插值的 10pps 版本进行比较。我们能够以如此低的发送速率重建如此高质量的模拟,真是令人惊叹。
顺便说一句,我发现无需对方向四元数进行高阶插值即可获得平滑插值。这很棒,因为我对在采样点处以指定角速度在方向四元数之间进行精确插值进行了大量研究,但这似乎很困难。为了获得可接受的结果,只需将线性插值 + 归一化 (nlerp) 切换为球面线性插值 (slerp),以确保方向插值的角速度恒定。
我认为这是因为模拟中的立方体在空中时往往具有恒定的角速度,而较大的角速度变化仅在发生碰撞时不连续地发生。也可能是因为方向在空中时变化缓慢,而位置相对于屏幕上受影响的像素数量变化迅速。无论如何,球面线性插值似乎已经足够好了,这很棒,因为这意味着我们不需要在快照中发送角速度。
处理现实世界的情况
现在我们必须处理数据包丢失问题。在上一篇文章中讨论了 UDP 与 TCP 之后,我相信您已经明白为什么我们永远不会考虑通过 TCP 发送快照了。
快照对时间至关重要,但与确定性锁步快照的输入不同,快照无需可靠。如果某个快照丢失,我们可以跳过它,并在插值缓冲区中插入更新的快照。我们永远不想停下来等待丢失的快照数据包重新发送。这就是为什么您应该始终使用 UDP 发送快照的原因。
告诉你一个秘密。上面的线性插值和 Hermite 插值视频不仅以每秒 10 个数据包的发送速率录制,而且还以 5% 的丢包率录制,抖动为 60fps 时有 +/- 2 帧。我处理这些视频丢包和抖动的方法是,确保在插值之前,快照在插值缓冲区中保留适当的时间。
我的经验法则是,插值缓冲区应该具有足够的延迟,这样即使连续丢失两个数据包,仍然有内容可以进行插值。通过实验,我发现在 2-5% 的丢包率下,最佳延迟量是数据包发送速率的 3 倍。以每秒 10 个数据包的速度计算,这个延迟就是 300 毫秒。我还需要一些额外的延迟来处理抖动,根据我的经验,抖动通常只有一两帧 @ 60fps,因此上面的插值视频是以 350 毫秒的延迟录制的。
增加 350 毫秒的延迟似乎很多,事实也确实如此。但是,如果刻意减少延迟,每次丢失一个数据包最终都会造成十分之一秒的卡顿。在其他领域(例如 FPS、飞行模拟器、赛车游戏等),人们经常使用一种技术来隐藏插值缓冲区造成的延迟,那就是使用外推法。但根据我的经验,外推法对刚体效果不佳,因为它们的运动是非线性且不可预测的。这里你可以看到 200 毫秒的外推,将总延迟从 350 毫秒减少到 150 毫秒:
问题是它不是很好。原因是外推对物理模拟一无所知。外推不知道与地板的碰撞,所以立方体会向下推断穿过地板,然后弹回以进行纠正。预测不知道将玩家立方体悬在空中的弹簧力,所以立方体最初向上移动的速度比应有的速度慢,必须迅速赶上。它也不知道碰撞以及碰撞响应如何工作,所以在地板上滚动的立方体和其他立方体也会被错误预测。最后,如果你观察块魂球,你会发现外推预测附着的立方体继续沿其切线速度移动,而它们应该与玩家立方体一起旋转。
结论
可以想象,你可以花费大量时间来提升这种外推的质量,并使其能够感知立方体的各种运动模式。你可以对每个立方体进行调整,并确保它们至少不会穿透地板。你可以在立方体之间使用边界球来添加一些近似碰撞检测或响应。你甚至可以让块魂球中的立方体预测围绕玩家立方体旋转的运动。
但即使你做到了这一切,仍然会存在预测错误,因为你根本无法准确地将物理模拟与近似值匹配。如果你的模拟主要是线性运动,例如快速移动的飞机、船只、宇宙飞船——你可能会发现,简单的外推法在短时间内(50-250毫秒左右)效果很好,但根据我的经验,一旦物体开始与其他非静止物体碰撞,外推法就会开始失效。
如何减少插值带来的延迟?350毫秒似乎仍然不可接受,而且我们无法使用外推法来减少延迟,否则会大大增加误差。解决方案很简单:提高发送速率!如果我们每秒发送30个快照,我们可以在150毫秒的延迟下获得相同数量的数据包丢失保护。每秒发送60个数据包只需要85毫秒。
为了提高发送速率,我们需要进行一些相当不错的带宽优化。不过别担心,我们可以做很多事情来优化带宽。内容太多了,以至于这篇文章容不下,我不得不额外添加一篇计划外的文章来涵盖所有内容!
下一篇文章:快照压缩