固定你的时间间隔!

原文地址:http://gafferongames.com/game-physics/fix-your-timestep/


Fix Your Timestep!

固定你的时间间隔!

September 2, 2006

Introduction

介绍

Hello, I’m Glenn Fiedler and welcome to the second article in my series onGame Physics.

In the previous article we discussed how to integrate the equations of motion using an RK4 integrator. Integration sounds complicated but really it’s just a way to advance the your physics simulation forward by some small amount of time called “delta time” (or dt for short).

But how to choose this delta time value? This may seem like a trivial subject but in fact there are many different ways to do it, each with their own strengths and weaknesses – so read on!

Fixed delta time

固定时间间隔


The simplest way to step forward is with a fixed delta time, like 1/60th of a second:

最简单的驱动游戏逻辑方式是按照一个固定的时间间隔去计算,比如1/60秒:

double t = 0.0;
double dt = 1.0 / 60.0;

while ( !quit )
{
     integrate( state, t, dt );
     render( state );
     t += dt;
}


In many ways this code is ideal. If you are lucky enough to have your physics delta time match the display rate and you can ensure that your update loop takes less than one frame then you have the perfect solution for updating your physics simulation.

从很多方面来说,这是一段理想的代码。如果你很幸运的能让你的物理间隔时间与显示器刷新率相匹配,并且你能保证你的物理帧时间小于总逻辑每帧需要的时间,那么你就能够完美的更新你的物理逻辑。


But in the real world you may not know the display refresh rate ahead of time, VSYNC could be turned off, or perhaps you could be running on a slow computer which cannot update and render your frame fast enough to present it at 60fps. In these cases your simulation will appear to run faster or slower than you intended.

但是在现实世界你不一定能够预先知道显示器的刷新率,VSYNC标志也许会被关闭掉,亦或者你的程序会在一台比较老旧的计算机上运行,而这破旧的计算机已经无力按照每秒60帧的速度来执行你程序了。如此一来,你的物理逻辑会显得比你的预期要快或者慢一些。


Variable delta time

可变的时间间隔


Fixing this *seems* simple. Just measure how long the previous frame takes, then feed that value back in as the delta time for the next frame. This makes sense because of course, because if the computer is too slow to update at 60HZ and has to drop down to 30fps, you’ll automatically pass in 1/30 as delta time. Same thing for a display refresh rate of 75HZ instead of 60HZ or even the case where VSYNC is turned off on a fast computer:

想解决上面的问题看上去很简单,只需要测算出上一帧的间隔时间,并将其当作下一帧的间隔时间就可以了。这看上去很靠谱,因为如果计算机无法提供每秒60帧的刷新率,而只能按照每秒30帧的速度来渲染,程序会自动将1/30秒作为时间间隔。类似的,如果刷新率提高至了75帧每秒,或者程序是运行在一台关闭了VSYNC标志的超级计算机的情况下,我们也能获得相应的时间间隔。

double t = 0.0;
double currentTime = hires_time_in_seconds();

while ( !quit )
{
     double newTime = hires_time_in_seconds();
     double frameTime = newTime - currentTime;
     currentTime = newTime;
     integrate( state, t, frameTime );
     t += frameTime;
     render( state );
}


But there is a huge problem with this approach which I will now explain. The problem is that the behavior of your physics simulation depends on the delta time you pass in. The effect could be subtle as your game having a slightly different “feel” depending on framerate or it could be as extreme as your spring simulation exploding to infinity, fast moving objects tunneling through walls and the player falling through the floor!

但是这样以来会引入一个严重的问题,我下面会解释。

问题在于,我们的物理逻辑行为起决于我们传入的间隔时间。这样做之后的结果很微妙,我们的游戏体验在不同的帧率下可能会有轻微的不同,或者,物理引擎的一些参数也可能会趋于无穷,从而导致灾难性的后果,比如快速移动的物体会穿过墙体,主角从地面穿越掉落!



One thing is for certain though and that is that it’s utterly unrealistic to just expect your simulation to correctly handle *any* delta time passed into it. To understand why, consider what would happen if you passed in 1/10th of a second as delta time? How about one second? 10 seconds? 100? Eventually you’ll find a breaking point.

有件事是毋庸置疑的,那就是试图让物理引擎正确的处理我们传入的*任意*时间间隔的想法是不现实的。

为什么会这样?试想,当我们传入1/10秒的时候,物理引擎会怎么反应?当我们传入10秒的时候呢?100秒的时候呢?物理引擎总会有垮掉的一刻


Semi-fixed timestep

半固定时间间隔


It’s much more realistic to say that your simulation is well behaved only if delta time is less than or equal to some maximum value. You can use mathematics to find this exact delta time value given your simulation (there is a whole field on it called numerical analysis, try researching “interval arithmetic” as well if you are keen), or you can arrive at it using a process of experimentation, or by simply tuning your simulation at some ideal framerate that you determine ahead of time. This is usually significantly easier in practice than attempting to make your simulation bulletproof at a wide range of delta time values.

现实一些来说,当传入的时间间隔处于一个合理范围内的时候,我们的物理引擎是能够正常工作的。

我们可以使用数学方法来找出传递给物理系统的一个最佳值(专业一点的术语称为数值分析,如果你热衷此道,可以搜索一下"区间计算"),或者可以通过实验的方式找出这个值,也可以先使用一个比较理想的帧率来计算出这个值,这样要比试图使我们的物理系统能够适应一个宽范围的时间间隔要简单的多。


With this knowledge at hand, here is a simple trick to ensure that you never pass in a delta time greater than the maximum value, while still running at the correct speed on different machines:

有了上述的共识,我们可以使用一个很简单的技巧来保证我们传入的时间间隔不会大于最大的理想值,从而保证在不同的运行环境下能够获得正确的物理行为

double t = 0.0;
double dt = 1 / 60.0;
double currentTime = hires_time_in_seconds();
while ( !quit )
{
     double newTime = hires_time_in_seconds();
     double frameTime = newTime - currentTime;
     currentTime = newTime;

     while ( frameTime > 0.0 )
     {
          const float deltaTime = min( frameTime, dt );
          integrate( state, t, deltaTime );
          frameTime -= deltaTime;
          t += deltaTime;
     }

     render( state );
}


The benefit of this approach is of course that we now have an upper bound on delta time. It’s never larger than this value because we subdivide the timestep. The disadvantage is that we’re now taking multiple steps per-display update including one additional step to consume any the remainder of frame time not divisible by dt. This is no problem if you are render bound, but if your simulation is the most expensive part of your frame you could run into problems including the so called “spiral of death”.

这样的实现方式带来的一个好处当然就是给时间间隔设定了一个上限。我们细分了时间间隔,这样它永远也不会大于这个给定值。这样做的缺点就是在预渲染处理过程中增加了一些计算,其中包括为了消耗掉不能被dt整除的剩余帧间隔时间。如果我们是render bound,这不会造成太大的问题,但如果物理模拟是帧逻辑中最昂贵的部分,那么这可能会带来包括“死亡螺旋”在内的各种问题。


The spiral of death occurs when your physics simulation cannot keep up with the steps it’s being asked to take. If your simulation is told: “OK, please simulate X seconds worth of physics” and if it takes Y seconds to do so where Y > X, then it doesn’t take Einstein to realize that over time your simulation falls behind. In order to ensure a stable update, I recommend having some headroom. You really need to ensure that it takes *significantly less* than X seconds of real time to update X seconds worth of physics simulation. If you can do this then your physics engine can “catch up” from any temporary spike by simulating more frames. Alternatively you can clamp at a maximum # of steps per-frame and the simulation will appear to slow down under heavy load. Arguably this is better than spiraling to death, assuming of course that the heavy load is just a temporary spike.

当物理引擎的模拟过程无法跟上它所被预期进行的步骤时,死亡螺旋便产生了。当物理引擎被告知:“请模拟X秒的物理现象”,然后物理引擎却花了Y秒,而Y>X,那么,无需爱因斯坦那样的智商,我们应该能意识到物理模拟滞后了。为了保证一个稳定的更新过程,我建议增加一些动态余量。我们必须保证物理模拟的一次迭代过程所需要的现实时间“显著”的小于X秒,并且能够达到虚拟世界里X秒同等的效果。如果我们能做到这一点,那么我们的物理引擎就能够通过多模拟几步,从而可以从任何暂时的落后“跟上”来。

或者我们可以将每帧的模拟步数限制在一个最大值,这样一来,在负载比较高的情况下,物理模拟看上去就会有点慢。

可以认为这样的处理要比“死亡螺旋”要好,当然这是在假定高负载只在一个暂时的高峰时刻出现。

Free the physics

解放物理


Now to take it one step further. What if you want exact reproducibility from one run to the next given the same inputs? This comes in handy when trying to network your physics simulation using deterministic lockstep, but it’s also generally a nice thing to know that your simulation behaves exactly the same from one run to the next without any potential for different behavior depending on the render framerate.

让我们更进一步。当给定相同的输入,我们想要保证每一步物理模拟的可重复性时,该怎么做?

在联机模式下,使用一致的步调来进行物理模拟是相当有用的,要知道,在渲染帧率可能不同的情况下,物理模拟能够保证每一步都是确定的、一致的,是一件非常美妙的事情


But you ask why is it necessary to have fully fixed delta time to do this? Surely the semi-fixed delta time with the small remainder step is “good enough”? And yes, you are right. It is good enough in most cases but it is not *exactly the same*. It takes only a basic understanding of floating point numbers to realize that (v*dt) + (v*dt) is not necessarily equal to v*2*dt due to to the limited precision of floating point arithmetic, so it follows that in order to get exactly the same result (and I mean exact down to the floating point bits) it is necessary to use a fixed delta time value.

但是我们不禁要问,为了达到上述的要求,是否必须使用固定的帧间隔?当然,半固定的帧间隔,加上把剩余的小片时间用于物理模拟是否已经足够好了?答案是肯定的,在大多数的情况下,半固定时间间隔的表现已经够好了,但这是“完全不同”的。我们只需要很少的浮点数知识就能够意识到 (v*dt) + (v*dt) 是不一定等于 v*2*dt 的,这是因为浮点数运算的精度是有限的。

因此,为了得到一个完全一致的结果(我的意思是精确到浮点数的bit位),使用固定的帧间隔是完全必要的。


What we want is the best of both worlds: a fixed delta time value for the simulation plus the ability to render at different framerates. These two things seem completely at odds, and they are – unless we can find a way to decouple the simulation and rendering framerates.

我们想要两全其美:一个固定的时间间隔供物理模拟,加上能够适应不同帧率的渲染机制。这两件事情看上去是矛盾的,除非我们能够找到分离模拟和渲染的方法。


Here’s how to do it. Advance the physics simulation ahead in fixed dt time steps while also making sure that it keeps up with the timer values coming from the renderer so that the simulation advances at the correct rate. For example, if the display framerate is 50fps and the simulation runs at 100fps then we need to take two physics steps every display update.

下面将展示如何做到这一点。使用固定时间间隔推进物理模拟的同时将保证它能够跟上从渲染引擎传递过来的定时器值,这样物理引擎就能够以正确的频率推进。例如,当渲染帧率为50fps,模拟帧率为100fps的时候,我们需要在每次刷新屏幕的时候让物理引擎进行两帧模拟。


What if the display framerate is 200fps? Well in this case it would seem that we need to take half a physics step each display update, but we can’t do that, so instead we must take physics step every two display updates. Even trickier, what if the display framerate is 60fps, but we want our simulation to run at 100fps? There is no easy multiple here. Now what if VSYNC is disabled and the display frame rate fluctuates from frame to frame?

当渲染帧率为200fps的时候如何?在这种情况下,我们似乎需要每次刷新屏幕的时候进行半帧物理模拟,但是我们不能那么做,所以我们必须每刷新两次屏幕,再进行一帧物理模拟。更棘手的是,如果渲染帧率为60fps,而我们的模拟帧率为100fps的时候该怎么办?


To solve this it is necessary to flip your view of the problem somewhat. Instead of considering that we have a certain amount of frame time that we must simulate before rendering, think of it like this: The renderer produces time and the physics simulation consumes it in discrete dt sized chunks.

为了解决这个问题,我们需要将看问题的视角逆转过来。与其考虑我们要在渲染前要模拟一定的帧数,不如这样思考:渲染器产生时间,然后物理模拟以离散大小的块状dt来消耗掉这个时间。

double t = 0.0;
const double dt = 0.01;
double currentTime = hires_time_in_seconds();
double accumulator = 0.0;

while ( !quit )
{  
     double newTime = hires_time_in_seconds();
     double frameTime = newTime - currentTime;
     currentTime = newTime;
     accumulator += frameTime;
     
     while ( accumulator >= dt )
     {  
          integrate( state, t, dt );
          accumulator -= dt;
          t += dt;
     }

     render(state);
}

Notice that unlike the semi-fixed timestep we only ever integrate with steps sized dt so it follows that it’s possible to have some unsimulated time left over at the end of each frame. This left over time is passed on to the next frame via the accumulator variable and is not thrown away.

注意到不像半固定时间间隔的做法,我们只用一个固定的dt步进来驱动物理模拟,所以在每一帧结束的时候可能会留下一些未被模拟的时间,而这些时间会被通过accumulator留到下一帧,所以剩余时间不会被丢掉


The final touch

最后的改变


But what do to with this remaining time? It seems incorrect somehow doesn’t it?

但是我们应该怎么处理这个剩余时间?这看上去似乎就是不正确,不是么?


To understand what is going on consider a situation where the display framerate is 60fps and the physics is running at 50fps. There is no nice multiple so the accumulator causes the simulation to alternate between mostly taking one and occasionally two physics steps per-frame when the remainders “accumulate” above dt.

要了解到发生了什么,不妨思考下面的情况,渲染帧率为60fps,物理模拟为50fps。

这个时候没有一个很好的倍数,因此采用accumulator后,物理模拟就会每帧进行一次,偶尔每帧进行两次,这起决于累积的剩余dt时间。


Now consider that in general all render frames will have some small remainder of frame time left in the accumulator that cannot be simulated because it is less than dt. What this means is that we’re displaying the state of the physics simulation at a time value slightly different from the render time. This causes a subtle but visually unpleasant stuttering of the physics simulation on the screen known as temporal aliasing.

现在考虑一下,一般来说,所有的渲染帧都将遗留下一些不能被物理模拟消耗掉的帧时间,因为它们小于dt,这些时间会被accumulator累积起来。这意味着,我们渲染的时候,物理模拟的状态和渲染的状态是有区别的。这会导致物理系统的一个轻微但从视觉上来说不愉快的停顿,这被称为走样。


The solution is to interpolate between the previous and current physics state based on how much time is left in the accumulator:

解决的办法是以accumulator里剩余的时间大小来在先前和当前物理状态之间进行插值处理:

double t = 0.0;
const double dt = 0.01;
double currentTime = hires_time_in_seconds();
double accumulator = dt;
State previous;
State current;

while ( !quit )
{
     double newTime = time();
     double frameTime = newTime - currentTime;

     if ( frameTime > 0.25 )
          frameTime = 0.25; // note: max frame time to avoid spiral of death

     currentTime = newTime;
     accumulator += frameTime;

     while ( accumulator >= dt )
     {
          previousState = currentState;
          integrate( currentState, t, dt );
          t += dt;
          accumulator -= dt;
     }

     const double alpha = accumulator / dt;
     State state = currentState*alpha + previousState * ( 1.0 - alpha );
     render( state );
}


This looks pretty complicated but here is a simple way to think about it. Any remainder in the accumulator is effectively a measure of just how much more time is required before another whole physics step can be taken. For example, a remainder of dt/2 means that we are currently halfway between the current physics step and the next. A remainder of dt*0.1 means that the update is 1/10th of the way between the current and the next state.

这看上去很复杂,但是有一个简单的思考方式。accumulator里的所有剩余时间都是可以用来判断是否可以进行另一帧物理模拟。

举个例子,一个dt/2大小的剩余时间意味着我们距离下一帧物理模拟更新还有一半的路程。一个dt*0.1大小的剩余时间意味着我们距离下一帧物理模拟更新已经进行了1/10。


We can use this remainder value to get a blending factor between the previous and current physics state simply by dividing by dt. This gives an alpha value in the range [0,1] which is used to perform a linear interpolation between the two physics states to get the current state to render. This interpolation is easy to do for single values and for vector state values. You can even use it with full 3D rigid body dynamics if you store your orientation as a quaternion and use a spherical linear interpolation (slerp) to blend between the previous and current orientations.

我们可以利用这个剩余时间值来获得一个先前和当前物理状态的混合因子,只需要简单的将这个值除以dt。

这个操作会给出一个范围在[0,1]之间的alpha值,我们可以使用这个alpha值来在上述两个物理状态之间进行线性插值运算,从而获得一个可以交付渲染的当前状态。这样的插值运算对于单值和向量来说都是简单的。

我们甚至可以在3D刚体力学中使用这种运算,只要我们将当前方向储存为一个四元数,并使用球面线性插值来在先前和当前方向进行混合。


欢迎转载,转载请注明出处


Creative Commons License
This work is licensed under a Creative Commons Attribution-NonCommercial-ShareAlike 3.0 China Mainland License.
  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值