모션이 부드럽게 나타나게하는 데 중요한 두 가지 사항이 있습니다. 첫 번째는 렌더링하는 것이 프레임이 사용자에게 제시 될 때 예상 상태와 일치해야한다는 것이고, 두 번째는 사용자에게 프레임을 제시해야한다는 것입니다. 비교적 고정 된 간격으로. T + 10ms에서 프레임을 제시 한 다음 T + 30ms에서 다른 프레임을 제시 한 다음 T + 40ms에서 다른 프레임을 제시하면 시뮬레이션에 따라 해당 시간 동안 실제로 표시된 내용이 정확하더라도 사용자가 판단하는 것처럼 보입니다.
메인 루프에는 일정한 간격으로 만 렌더링 할 수있는 게이팅 메커니즘이없는 것 같습니다. 때로는 렌더링간에 3 번의 업데이트를 수행하고 때로는 4 번을 수행 할 수도 있습니다. 기본적으로 현재 시간 앞에서 시뮬레이션 상태를 푸시하기에 충분한 시간을 시뮬레이션하면 루프가 최대한 자주 렌더링됩니다. 그 상태를 렌더링합니다. 그러나 업데이트 또는 렌더링에 걸리는 시간과 프레임 간 간격도 다양합니다. 시뮬레이션에는 정해진 시간 간격이 있지만 렌더링에는 다양한 시간 간격이 있습니다.
렌더링 직전의 대기 시간이 필요할 수 있습니다. 이렇게하면 렌더링 간격이 시작될 때만 렌더링을 시작할 수 있습니다. 이상적으로는 적응해야합니다. 업데이트 / 렌더링에 너무 오래 걸리고 간격의 시작이 이미 지났다면 즉시 렌더링해야하지만 일관성있게 렌더링 및 업데이트 할 수있을 때까지 인터벌 길이를 늘려야합니다. 간격이 끝나기 전에 다음 렌더 여유 시간이 충분하면 간격을 천천히 줄이고 (즉, 프레임 속도를 높이면) 다시 빠르게 렌더링 할 수 있습니다.
그러나 여기에 키커가 있습니다. 시뮬레이션 상태가 "지금"으로 업데이트 된 것을 감지 한 직후에 프레임을 렌더링하지 않으면 임시 앨리어싱이 도입됩니다. 사용자에게 제공되는 프레임이 약간 잘못된 시간에 표시되고 있으며, 그 자체가 더듬 거리는 느낌이들 것입니다.
이것이 당신이 읽은 기사에서 언급 된 "부분 시간 단계"의 이유입니다. 거기에는 합당한 이유가 있습니다. 물리적 타임 스텝을 고정 렌더링 타임 스텝의 고정 적분 배수로 고정하지 않으면 적시에 프레임을 표시 할 수 없기 때문입니다. 너무 일찍 또는 너무 늦게 발표 할 수 있습니다. 고정 렌더링 속도를 얻고 물리적으로 올바른 것을 제시하는 유일한 방법은 렌더링 간격이 다가올 때 고정 물리 시간 단계 중 두 단계 사이에있을 가능성이 있다는 점을 받아들이는 것입니다. 그렇다고 렌더링 중에 객체가 수정되는 것은 아닙니다. 렌더링은 오브젝트가있는 위치를 임시로 설정하여 오브젝트가 업데이트 이전과 이전의 위치 사이에 렌더링되도록 할 수 있습니다. 중요합니다. 렌더링을 위해 월드 상태를 변경하지 말고 업데이트 만 월드 상태를 변경해야합니다.
의사 코드 루프에 넣으려면 다음과 같은 것이 더 필요하다고 생각합니다.
InitialiseWorldState();
previousTime = currentTime = 0.0;
renderInterval = 1.0 / 60.0; //A nice high starting interval
subFrameProportion = 1.0; //100% currentFrame, 0% previousFrame
while (true)
{
frameStart = ActualTime();
//Render the world state as if it was some proportion
// between previousTime and currentTime
// E.g. if subFrameProportion is 0.5, previousTime is 0.1 and
// currentTime is 0.2, then we actually want to render the state
// as it would be at time 0.15. We'd do that by interpolating
// between movingObject.previousPosition and movingObject.currentPosition
// with a lerp parameter of 0.5
Render(subFrameProportion);
//Check we've not taken too long and missed our render interval
frameTime = ActualTime() - frameStart;
if (frameTime > renderInterval)
{
renderInterval = frameTime * 1.2f; //Give us a more reasonable render interval that we actually have a chance of hitting
}
expectedFrameEnd = frameStart + renderInterval;
//Loop until it's time to render the next frame
while (ActualTime() < expectedFrameEnd)
{
//step the simulation forward until it has moved just beyond the frame end
if (previousTime < expectedFrameEnd) &&
currentTime >= expectedFrameEnd)
{
previousTime = currentTime;
Update();
currentTime += fixedTimeStep;
//After the update, all objects will be in the position they should be for
// currentTime, **but** they also need to remember where they were before,
// so that the rendering can draw them somewhere between previousTime and
// currentTime
//Check again we've not taken too long and missed our render interval
frameTime = ActualTime() - frameStart;
if (frameTime > renderInterval)
{
renderInterval = frameTime * 1.2f; //Give us a more reasonable render interval that we actually have a chance of hitting
expectedFrameEnd = frameStart + renderInterval
}
}
else
{
//We've brought the simulation to just after the next time
// we expect to render, so we just want to wait.
// Ideally sleep or spin in a tight loop while waiting.
timeTillFrameEnd = expectedFrameEnd - ActualTime();
sleep(timeTillFrameEnd);
}
}
//How far between update timesteps (i.e. previousTime and currentTime)
// will we be at the end of the frame when we start the next render?
subFrameProportion = (expectedFrameEnd - previousTime) / (currentTime - previousTime);
}
이를 위해 업데이트되는 모든 객체가 작동하려면 렌더링이 객체의 위치에 대한 지식을 사용할 수 있도록 이전 및 현재 위치에 대한 지식을 보존해야합니다.
class MovingObject
{
Vector velocity;
Vector previousPosition;
Vector currentPosition;
Initialise(startPosition, startVelocity)
{
currentPosition = startPosition; // position at time 0
velocity = startVelocity;
//ignore previousPosition because we should never render before time 0
}
Update()
{
previousPosition = currentPosition;
currentPosition += velocity * fixedTimeStep;
}
Render(subFrameProportion)
{
Vector actualPosition =
Lerp(previousPosition, currentPosition, subFrameProportion);
RenderAt(actualPosition);
}
}
렌더링 시간은 3ms, 업데이트 시간은 1ms, 업데이트 시간 단계는 5ms로, 렌더링 시간 단계는 16ms [60Hz]에서 시작 (및 유지)된다는 타임 라인을 밀리 초 단위로 표시해 보겠습니다.
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
R0 U5 U10 U15 U20 W16 R16 U25 U30 U35 W32 R32
- 먼저 우리는 시간 0에서 초기화합니다 (따라서 currentTime = 0)
- 1.0 (100 % currentTime)의 비율로 렌더링하여 시간 0에 세계를 그립니다.
- 완료되면 실제 시간은 3이며 프레임이 16까지 끝날 것으로 예상하지 않으므로 업데이트를 실행해야합니다.
- T + 3 : 0에서 5로 업데이트합니다 (따라서 currentTime = 5, previousTime = 0)
- T + 4 : 프레임이 끝나기 전에 5에서 10으로 업데이트
- T + 5 : 여전히 프레임이 끝나기 전에 10에서 15로 업데이트
- T + 6 : 프레임이 끝나기 전에 15에서 20으로 업데이트
- T + 7 : 여전히 프레임 끝 이전이지만 currentTime은 프레임 끝을 초월합니다. 더 이상 시뮬레이션하고 싶지 않다면 다음 렌더링 시간을 넘어서게됩니다. 대신 다음 렌더링 간격을 조용히 기다립니다 (16).
- T + 16 : 다시 렌더링해야합니다. previousTime은 15, currentTime은 20입니다. 따라서 T + 16에서 렌더링하려면 5ms의 긴 시간 간격으로 1ms가됩니다. 따라서 우리는 프레임을 통과하는 길이의 20 %입니다 (비율 = 0.2). 렌더링 할 때 이전 위치와 현재 위치 사이의 20 %의 오브젝트를 그립니다.
- 다시 3으로 루프하고 무한정 계속하십시오.
너무 미리 시뮬레이션하는 것에 대한 또 다른 뉘앙스가 있습니다. 즉, 프레임이 실제로 렌더링되기 전에 발생하더라도 사용자의 입력이 무시 될 수 있지만 루프가 원활하게 시뮬레이션된다고 확신 할 때까지 걱정하지 마십시오.