如图,横坐标 t 是时间,纵坐标 v 是速度,图中图像表示为 v = 2 ,意义为以恒定速度 2 (这里省略了单位)前进 2 秒。那么这 2 秒内,移动的距离是多少呢?
很简单,我们可以直接得到 2 x 2 = 4,稍作观察我们就会发现下图:
路程的值就是 v = 2 与 t 轴组成的矩形的面积。
有了前面的例子,这个图像理解起来就更简单了:路程的值为图像中红色阴影部分的面积,即 2 x 2 / 2 = 2。
上述两个例子中,我们所要求得的路程的值,均为规整图形的面积,那么如果 v 值随着 t 而做非匀速变化呢?如下图:
这次并不能简单的求得阴影部分面积了。但是我们可以用一种取巧的方法:
我们将阴影部分分割为一个一个的矩形,从图像上可以看出,我们只要把所有矩形的面积求出并相加,就能得到一个近似值,也就是我们要求得的路程值。此求值过程,就是所谓的积分过程。
上述三个例子均是极其简单的运动问题,同时也是一个简单的积分过程。在游戏引擎物理相关的模拟中,大多也是如此。
比如,给某物体施加一个恒定的力,随着时间变化,这个力会推动物体行进多少距离?
再比如,给某物体施加一个扭矩,随着时间变化,这个物体会旋转多少度?
这个值的精确度取决于矩形的宽,即 t 值的间隔大小:
结合图5、6 来看我们不难发现,如果矩形的宽度越小,分割出的矩形越多,我们得到的值就越精确。带入具体的坐标含义来说就是,如果我们计算的时间间隔(矩形的宽)越小,那么我们得到的最终路程值就越精确。
同理我们不难得出一个重要结论: 在游戏引擎的物理计算中,如果我们能保证每次计算的时间间隔足够小,那么物理计算的精确性也就越高
物理引擎的计算结果决定了最终的渲染结果。例如一个在某个力的作用下运动的球,在经过两秒的运动后,此时它在游戏场景的位置,就决定了渲染结果中这个球所在画面中的位置。
游戏引擎会以某种帧率来渲染画面,这貌似可以用来驱动物理引擎的运作。但是问题的关键在于,我们无法保证渲染帧率的稳定性。这一秒渲染帧率可能还是 60fps,下一秒可能就因为场景的复杂度提高而降低至30fps。如果我们的项目需要物理引擎保持 50fps 的计算频率,那 30fps 的渲染帧率就无法驱动物理引擎做出正确的模拟了,自然渲染结果也会变得不正常。
那么如何保证物理引擎能够不受渲染帧率影响,同时能以一个足够精确的时间间隔持续运行呢?
通常有我们有两种解决方案可供选择,其一为 ue 所采用的半固定物理帧(semi-fixed),其二为 unity 采用的固定物理帧(fixed)。
其中,Max Substeop Delta Time 最好理解,它代表着我们希望物理引擎以哪种帧率运行,如图上数值 0.016667 即代表着为我们希望物理引擎能以每秒 60 帧的速度运行(1 / 0.016 = 60),从而保证在游戏中的物理计算能够符合我们对物理模拟精度的要求。当然,这个值也可以是 0.033 ,即 30FPS,这个值的选取就是看我们对游戏中物理计算的精度有何种要求。Max substeps 我们放到最后解释。
接下我们进行一次演绎推理,来看看半固定物理帧到底是怎么运行的。
1. 我们先设定两个关键属性的值,其中 max substep delta time = 0.016 (后简称 MDST),max substeps = 6
2. 我们假设 frame 0 耗时为 0.016 ,这与我们设定的 MDST 值相等,所以 frame 1 我们的物理计算只需一次
3. 要理解第三步推理,我们还是要强调上文中提到过的一个最基本的原则,渲染帧率有可能随着画面变动而变动,但是,不论是半固定还是固定物理帧,都是为了保证物理引擎的运算频率不受渲染帧率的影响。在此基础上我们可以从图中观察第三步的推理。假设 frame 1 耗时 0.033,而我们期望的物理帧运算间隔为 MDST = 0.016,即每秒60帧的物理计算频率(1 / 0.016 = 60),也就是说,为了满足这个要求,我们需要弥补第 1 帧所耽误的时间,第 2 帧的物理帧就要运行 0.033 / MDST = 2 次。并且物理帧运算时将得到 0.033 / 2 = 0.0165 的时间间隔值,作为物理计算的必要参数。
4. 跳出每帧计算的细节,我们可以从更高的角度来观察。如果我们设定 MDST = 0.016,也就是说我们要求物理计算频率为 60fps。观察图中第四步,我们不难发现,前一帧多消耗的时间,会在后一帧补以多次物理计算的形式补回来,这个算法的确能保证物理计算的评率符合我们的设定频率。
下面是这个算法的具体代码,可以与上图中的推理过程相互印证:
从代码中可以看到,在计算 substeps 时,采取了向上取整的方法,这是为了保证我们的物理帧尽可能的多,而不是少。同时我们也可以从代码中看出 Max Substeps 参数的含义,即如果系统运行的速度低过一定的阈值,并且我们无限制的增加物理帧运算,就会导致整个系统的恶性循环。所以一旦系统运行的速度降低到一定程度,半固定物理帧策略就会做出适当的取舍,放弃弥补物理计算的不足。
下图所示是 unity 中关于固定物理帧的参数设置:
此策略的关键参数为 Fixed Timestep,如图所示 Fixed Timestep 值为 0.02,即我们要求物理计算以 50 fps (1 / 0.02 = 50) 的频率进行。
1. 假设在第 1 帧时我们消耗的时间为 0.024,这比我们设置的 Fixed Timestep 的值大了 0.008
2. 第 2 帧时我们消耗的时间同样为 0.024, 与 Fixed Timestep 的值相比还是大了 0.008
3. 这两帧耗多消耗的时间总和为 0.016,正好与我们的 Fixed Timestep 值相等,即第 2 帧我们可以多做一次物理计算,以弥补我们多消耗的时间带来的物理模拟的延迟
下面是这个算法的具体代码,可以与上图中的所示过程相互印证:
最后还要多补充的几点是,当我们在引擎中做物理相关的方法调用时,通常都被要求放在关于物理的帧回掉中,例如 unity 物理帧回调位 fixedupdate,ue 则为 substep 回调并需要写特定的代码进行绑定。这个要求其实过于笼统。准确的说,只有我们需要进行某种“恒定的物理作用”时,才需要遵守这里条原则,例如以一个恒定的力持续推进一个物体。
原因是为了保证我们前文一直强调的物理计算的准确性,物理引擎在模拟计算时,一直在物理帧中进行。如果我们在代码中进行的物理相关操作,没有放到物理帧中,这就会导致游戏中有两种物理计算,一种是精确地在物理帧中的,一种是不精确的在非物理帧中的。如果这两个物理操作均在非物理帧中,那么最起码也还保证了一致性,不会因为处在不同类型帧中从而导致的调用次数不一致(例如 unity 的updata 方法回调频率可能是 200fps,fixedupdate 仅为 50fps,物理引擎内部模拟一个力只在 50fps 下,而用户在 update 中手动调用施加力的方法则 1 秒钟会调用 200次,这显然会出现不协调的物理结果)。但是现实状况是引擎内部的物理模拟本就在物理帧,用户没有其他选择,如果我们的调用不在物理帧,那连一致性都无法保证了,就更别提准确性了。
对于那些一次性的物理操作,并且不涉及的时间间隔相关的物理值计算,例如在某帧施加一个冲击力,则可以再非物理帧中调用。
非固定物理帧的参数设置不当,可能会导致游戏帧率陷入恶性循环,永远无法跳出低帧率状态。
而固定物理帧则会由于前一帧的耗时和 Fixed Timestep 相差不多,导致累积结果漫长,从而导致画面延迟。
关于 ue 为和决定选择使用非固定物理帧,官方论坛上有如下回答:
We actually had a debate about these two techniques [fixed and semi-fixed timestep] and eventually decided on semi-fixed, and here’s why:
If you use [fixed timestep] you have to use a timebank. If you want to tick physics at 60fps and you have a frame that took a little bit more than 1/60 you will need to have some left over time. Then you will tick the physics engine with the perfect 1/60 delta time, leaving the remainder for the next frame. The problem is that the rest of the engine still uses the original delta time. This means that things like blueprint will be given a delta time of 1/60 + a bit.
评论区
共 6 条评论热门最新