https://gafferongames.com/post/fix_your_timestep/

Introduction

Hi, I’m Glenn Fiedler and welcome to Game Physics.

In the previous article we discussed how to integrate the equations of motion using a numerical integrator. Integration sounds complicated, but 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).

이전의 게시글에서 우리는 운동 방정식을 수치 적분기로 적분하는 방법에 대해서 알아보았습니다. 적분은 복잡하게 들리지만 단지 여러분의 물리 시뮬레이션을 작은 시간 단위 “delta time(줄여서 dt)”를 통해 진전시키는 것을 말합니다.

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!

그런데 delta time 값은 어떻게 정해야 할까요? 조금 흔한 주제로 보이지만 이를 다루는 방법은 정말 많고, 각각에는 장점과 단점이 있습니다. 쭉 읽어보세요!



Fixed delta time

The simplest way to step forward is with 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’re lucky enough to have your delta time match the display refresh rate, and you can ensure that your update loop takes less than one frame worth of real time, then you already have the perfect solution for updating your physics simulation and you can stop reading this article.

다양한 면에서 이 코드는 이상적입니다. 만약 dt가 디스플레이의 갱신율과 일치하면 업데이트 루프가 실제 시간의 한 프레임보다 적은 시간을 사용한다는 걸 보장할 수 있으며, 이는 이미 물리 시뮬레이션을 업데이팅하기 위한 완벽한 방법을 가진 것이므로 이 게시글을 그만 읽어도 된다는 뜻입니다.

But in the real world you may not know the display refresh rate ahead of time. VSYNC could be turned off, or you could be running on a slow computer which cannot update and render your frame fast enough to present it at 60fps.

그러나 현실세계에서는 갱신율을 미리 알 수가 없습니다. VSYNC가 꺼진 상태일 수도 있고, 60fps로 업데이트하고 렌더하기에는 너무 느린 컴퓨터로 작동할 수도 있기 때문입니다. 

In these cases your simulation will run faster or slower than you intended.

이러한 경우에는 의도보다 빠르거나 느린 시뮬레이션을 보게될 것입니다.

 


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:

이를 고치는건 단순합니다. 간단히 이전 프레임에 얼마나 소요했는지 잰 다음, 다음 프레임을 위한 delta time에 반영하는 것입니다. 이 방법이 말이 되는 이유는 만약 컴퓨터가 60HZ로 업데이트하기엔 너무 느려서 30FPS로 내려야 할 때, 자동적으로 1/30을 delta time으로 넘겨주기 때문입니다. 60HZ대신 75HZ의 갱신율이라던가 빠른 컴퓨터에서 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 players falling through the floor!

그런데 이런 방식에는 제가 이제부터 설명할 엄청난 문제가 있습니다. 물리 시뮬레이션의 행동이 집어넣은 delta time에 의존하게 된다는 것입니다. 이는 여러분의 게임이 프레임레이트에 따라 약간 다른 “느낌”을 갖는 것에서부터, 스프링 시뮬레이션이 무한대로 폭발하거나, 빠르게 움직이는 객체가 벽을 뚫고 지나가고 플레이어가 바닥에서 꺼지는 현상 등 극단적일수 있습니다.

One thing is for certain though and that is that it’s utterly unrealistic to 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.

한 가지 확실한건 시뮬레이션이 건내진 어떠한 delta time이던지 올바르게 시뮬레이션할거라는 기대가 비현실적이라는 것입니다. 그 이유를 이해하기 위해서 delta time으로 1/10초를 집어넣으면 무슨 일이 일어날 지 생각해 봅시다. 1초, 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. This is usually significantly easier in practice than attempting to make your simulation bulletproof at a wide range of delta time values.

여러분의 시뮬레이션이 delta time이 특정 값 이하일 때만 잘 작동하게끔 하는 것이 좀 더 현실적인 것 같습니다. 이는 여러분의 시뮬레이션이 넓은 범위의 delta time에 대하여 안전하도록 처리하는 것보다 훨씬 쉬운 작업입니다.

With this knowledge at hand, here’s 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:

위와 같은 지식을 염두에 둡시다. 다음은 최대 값보다 높은 delta time을 넘겨주지 않으면서 다양한 기계에서 알맞은 속도로 돌아가게 하기 위한 약간의 트릭입니다.

   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 )
        {
            float deltaTime = min( frameTime, dt );
            integrate( state, t, deltaTime );
            frameTime -= deltaTime;
            t += deltaTime;
        }

        render( state );
    }



The benefit of this approach is that we now have an upper bound on delta time. It’s never larger than this value because if it is 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 the so called “spiral of death”.

이런 방법의 장점은 이제 delta time에 대한 상한선이 생겼다는 것입니다. 우리가 timestep을 세분화 하고 있기 때문에 delta time이 상한선보다 커질 일은 절대 없습니다. 단점은 우리가 매 갱신마다 여러 스텝을 밟은 후에 dt에 의해 나누어지지 않는 frame time의 나머지를 소모하면서 추가적인 하나의 스텝을 밟는다는 것입니다. 이는 render bound라면 문제가 되지 않지만, 만약 여러분의 시뮬레이션이 프레임에서 가장 비용이 큰 부분이라면 소위 “죽음의 나선”이라고 불리는 상태가 될 수 있습니다.

What is the spiral of death? It’s what happens when your physics simulation can’t keep up with the steps it’s asked to take. For example, if your simulation is told: “OK, please simulate X seconds worth of physics” and if it takes Y seconds of real time to do so where Y > X, then it doesn’t take Einstein to realize that over time your simulation falls behind. It’s called the spiral of death because being behind causes your update to simulate more steps to catch up, which causes you to fall further behind, which causes you to simulate more steps…

죽음의 나선이 무엇일까요? 이건 여러분의 물리 시뮬레이션이 요구받은 스텝을 따라갈 수 없을 때 생기는 일입니다. 예를 들어 여러분의 시뮬레이션이 “X 초에 해당하는 물리를 시뮬레이팅해주세요”라고 요청을 받았는데 실제시간으로 Y 초가 걸려서 Y > X 가 되었다면,  아인슈타인이 아니더라도 시뮬레이션이 그만큼 뒤쳐졌다는 걸 알 수 있습니다. 죽음의 나선이라고 불리는 이유는 업데이트가 스텝을 따라잡기 위해 더 시뮬레이트를 한다는 것이고, 이건 스텝을 더 뒤쳐지게 만들며, 이를 따라잡기 위해 더 시뮬레이트를...

So how do we avoid this? In order to ensure a stable update I recommend leaving 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, especially if the heavy load is just a temporary spike.

이건 어떻게 해결해야할까요? 저는 안정적인 업데이트를 보장하기 위해 여유를 조금 두는 걸 추천합니다. 여러분은 X 초만큼의 물리 시뮬레이션을 업데이트하는데 드는 실제 시간이 X 초보다 현저하게 적다는 것을 보장해야 합니다. 이를 통해 여러분의 물리 엔진이 여러 프레임을 시뮬레이팅함으로써 일시적인 스파이크로부터 “따라잡기”를 할 수 있게 됩니다. 대신 여러분은 프레임당 최대 스텝 수를 고정(clamp)할 수 있게되며, 무거운 불러오기시에 시뮬레이션이 느려진 것처럼 보일 것입니다.  이는 특히 무거운 불러오기가 단지 일시적인 스파이크일 때 죽음의 나선보다 훨씬 나은 방법입니다.

 


Free the physics

Now let’s 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.

이제 한 발자국 더 나아가봅시다. 만약 여러분이 어떤 실행과 그 다음의 같은 입력에 대해 완벽한 재현을 원한다면 어떨까요?  여러분의 물리 시뮬레이션을 deterministic lockstep을 통해 네트워크하려고 할 때 편리할 뿐만 아니라, 여러분의 시뮬레이션이 한 실행과 그 다음 번 실행에서 다양한 렌더 프레임레이트에 상관 없이 정확히 똑같이 동작한다는 건 좋은 일입니다.

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 due to to the limited precision of floating point arithmetic.

그런데 어쩌면 여러분이 왜 이걸 하는데 완벽하게 고정된 delta time이 필요한지 궁금해 할지 모르겠습니다. 분명 반-고정된 delta time과 약간의 나머지 스텝이 “충분히 좋지” 않을까요? 네 맞습니다. 대부분의 경우에 충분히 좋지만 부동 소수점 연산의 정확성에 대한 한계 때문에 정확하게 같지는 않을 뿐입니다.

What we want then 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.

우리가 원하는건 두 세계의 장점 모두 입니다. 시뮬레이션을 위한 고정된 delta time 값과 다양한 프레임레이트에서 렌더할 수 있는 기능입니다. 이 두 가지는 완전히 달라보입니다. 우리가 시뮬레이션과 렌더링 프레임레이트를 디커플링하는 방법을 찾지 않는 이상은요.

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. Easy.

이제 그 방법을 알아봅시다. 물리 시뮬레이션을 고정된 dt time step에 따라 진행하면서 한 편으로 렌더러로부터 오는 타이머 값을 따라올 수 있게 해야 시뮬레이션이 올바른 레이트로 진행될 수 있습니다. 예를 들어 만약 디스플레이의 프레임레이트가 50fps인데 시뮬레이션이 100fps로 진행된다면 한 번의 디스플레이 업데이트마다 두 번의 물리 스텝을 밟아야 한다는 것입니다. 쉽죠?

What if the display framerate is 200fps? Well in this case it we need to take half a physics step each display update, but we can’t do that, we must advance with constant dt. So we take one physics step every two display updates.

만약 디스플레이의 프레임레이트가 200fps 면 어떨까요? 이런 경우에 각각의 디스플레이 업데이트마다 반 보의 물리 스텝이 필요하지만, 우리는 상수 dt를 사용해야 하기에 그럴 수 없습니다. 따라서 두 번의 프레임 업데이트마다 한 번의 물리 스텝을 밟으면 될 것입니다.

Even trickier, what if the display framerate is 60fps, but we want our simulation to run at 100fps? There is no easy multiple. What if VSYNC is disabled and the display frame rate fluctuates from frame to frame?

좀 더 까다로운 문제로, 디스플레이의 프레임레이트가 60fps인데 시뮬레이션이 100fps로 동작하게 하고 싶다면 어떨까요? 이 때는 쉬운 곱셈이 없습니다. 만약 VSYNC가 꺼져있고 디스플레이 프레임 레이트가 프레임마다 변동한다면 어떨까요?

If you head just exploded don’t worry, all that is needed to solve this is to change your point of view. Instead of thinking that you have a certain amount of frame time you must simulate before rendering, flip your viewpoint upside down and think of it like this: the renderer produces time and the simulation consumes itin discrete dt sized steps.

여러분의 머리가 폭발할 것 같아도 걱정하지 마세요. 이 걸 해결하기 위해 필요한 건 관점을 바꾸는 것 뿐이니까요. 여러분이 렌더링하기 전에 시뮬레이트해야하는 특정한 양의 프레임 타임이 있다고 생각하는걸 그만두고, 관점을 뒤집어서 이렇게 생각해보기 바랍니다: 렌더러가 시간을 생성하고 시뮬레이션이 이를 별도의  dt만큼의 스텝으로 소모한다고요. 

For example:

   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 in the common case we 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를 통해서만 적분을 하기 때문에 각각의 프레임의 끝마다 시뮬레이트되지 않은 약간의 시간을 남겨둔다는 걸 주의합시다. 이 여분의 시간은 다음 프레임에 축적기를 통해 넘겨지므로 버려지지 않습니다. 

 


The final touch

But what do to with this remaining time? It seems incorrect 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.

Now consider that the majority of render frames will have some small remainder of frame time left in the accumulator that cannot be simulated because it is less than dt. This means we’re displaying the state of the physics simulation at a time slightly different from the render time, causing a subtle but visually unpleasant stuttering of the physics simulation on the screen.

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

    double t = 0.0;
    double dt = 0.01;

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

    State previous;
    State current;

    while ( !quit )
    {
        double newTime = time();
        double frameTime = newTime - currentTime;
        if ( frameTime > 0.25 )
            frameTime = 0.25;
        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 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.

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.

'프로그래밍 > 물리, 수학, 3D' 카테고리의 다른 글

벡터 내적의 직관적 의미  (0) 2021.03.11
integration basics 번역  (0) 2018.06.17