モーションをスムーズに表示するために重要なことは2つあります。1つ目は、レンダリングするものが、フレームがユーザーに提示されたときの予想状態と一致する必要があること、2つ目は、ユーザーにフレームを提示する必要があることです。比較的一定の間隔で。フレームがT + 10msで表示され、次にT + 30msで表示され、次にT + 40msで表示されると、シミュレーションで実際に表示されているものが正しいとしても、ユーザーは混乱しているように見えます。
メインループには、定期的な間隔でのみレンダリングすることを保証するゲーティングメカニズムがないようです。したがって、レンダーの間に3つの更新を行う場合と、4つの場合を行う場合があります。基本的に、ループは可能な限り頻繁にレンダリングされます。シミュレーションの状態を現在の時間の前にプッシュするのに十分な時間をシミュレーションするとすぐに、次に、その状態をレンダリングします。ただし、更新またはレンダリングにかかる時間の変動や、フレーム間の間隔も変動します。シミュレーションのタイムステップは固定ですが、レンダリングのタイムステップは可変です。
おそらく必要なのは、レンダリングの直前の待機であり、これにより、レンダリング間隔の開始時にのみレンダリングを開始できます。理想的には、それが適応的である必要があります。更新/レンダリングに時間がかかりすぎて、間隔の開始が既に過ぎている場合は、すぐにレンダリングする必要がありますが、間隔の長さを増やして、一貫してレンダリングおよび更新して、間隔が終了する前の次のレンダリング。十分な時間がある場合は、間隔をゆっくり減らして(つまり、フレームレートを上げて)、再び速くレンダリングすることができます。
しかし、これがキッカーです。シミュレーションの状態が「現在」に更新されたことを検出した直後にフレームをレンダリングしない場合は、一時的なエイリアシングを導入します。ユーザーに提示されているフレームがわずかに間違ったタイミングで提示されており、それ自体がスタッターのように感じられます。
これが、読んだ記事で言及されている「部分的なタイムステップ」の理由です。そこには十分な理由があります。それは、物理タイムステップを固定レンダリングタイムステップの固定整数倍に固定しない限り、適切なタイミングでフレームを表示できないためです。あなたはそれらを発表するのが早すぎるか遅すぎるかのどちらかになります。固定されたレンダリングレートを取得し、物理的に正しいものを提示する唯一の方法は、レンダリング間隔が近づいたときに、固定された2つの物理タイムステップの中間になる可能性が高いことを受け入れることです。ただし、レンダリング中にオブジェクトが変更されるわけではありません。レンダリングでは、オブジェクトの場所を一時的に確立して、更新前と更新後の中間のどこかにオブジェクトをレンダリングできるようにする必要があるだけです。それは重要です-レンダリングのためにワールドの状態を変更しないでください。更新のみがワールドの状態を変更する必要があります。
したがって、それを疑似コードループに入れるには、次のようなものが必要だと思います。
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.にループバックし、無期限に続行します。
シミュレーションのタイミングが早すぎるという別のニュアンスがあります。つまり、ユーザーの入力は、フレームが実際にレンダリングされる前に発生したとしても無視される可能性がありますが、ループがスムーズにシミュレーションされていると確信できるまでは心配する必要はありません。