Es gibt zwei Dinge, die entscheidend dafür sind, dass die Bewegung reibungslos erscheint. Das erste ist offensichtlich, dass das, was Sie rendern, dem erwarteten Status zum Zeitpunkt der Präsentation des Rahmens für den Benutzer entsprechen muss. Das zweite ist, dass Sie dem Benutzer Bilder präsentieren müssen in einem relativ festen Intervall. Das Präsentieren eines Rahmens bei T + 10 ms, dann eines weiteren bei T + 30 ms und eines weiteren bei T + 40 ms erscheint dem Benutzer als ruckelnd, selbst wenn das, was für diese Zeiten tatsächlich angezeigt wird, gemäß der Simulation korrekt ist.
In Ihrer Hauptschleife scheint es keinen Gating-Mechanismus zu geben, um sicherzustellen, dass Sie nur in regelmäßigen Abständen rendern. Manchmal führen Sie also 3 Aktualisierungen zwischen den Renderings durch, manchmal 4. Grundsätzlich wird Ihre Schleife so oft wie möglich gerendert, sobald Sie genug Zeit simuliert haben, um den Simulationsstatus vor die aktuelle Zeit zu verschieben dann rendern Sie diesen Zustand. Aber jede Variabilität, wie lange das Aktualisieren oder Rendern dauert, und das Intervall zwischen den Frames variieren ebenfalls. Sie haben einen festen Zeitschritt für Ihre Simulation, aber einen variablen Zeitschritt für Ihr Rendering.
Was Sie wahrscheinlich brauchen, ist eine Wartezeit kurz vor dem Rendern, um sicherzustellen, dass Sie das Rendern immer erst zu Beginn eines Renderintervalls starten. Im Idealfall sollte dies anpassungsfähig sein: Wenn das Aktualisieren / Rendern zu lange gedauert hat und der Beginn des Intervalls bereits verstrichen ist, sollten Sie sofort rendern, aber auch die Intervalllänge erhöhen, bis Sie konsistent rendern und aktualisieren können und trotzdem darauf zugreifen können das nächste Rendern vor Ablauf des Intervalls. Wenn Sie genügend Zeit haben, können Sie das Intervall langsam verkürzen (dh die Bildrate erhöhen), um wieder schneller zu rendern.
Aber, und hier ist der Kicker: Wenn Sie den Frame nicht sofort rendern, nachdem Sie festgestellt haben, dass der Simulationsstatus auf "jetzt" aktualisiert wurde, führen Sie zeitliches Aliasing ein. Der Rahmen, der dem Benutzer präsentiert wird, wird etwas zur falschen Zeit präsentiert, und das an sich wird sich wie ein Stottern anfühlen.
Dies ist der Grund für den "Teilzeitschritt", der in den von Ihnen gelesenen Artikeln erwähnt wird. Es gibt es aus einem guten Grund, und das liegt daran, dass Sie die Frames einfach nicht zum richtigen Zeitpunkt präsentieren können, wenn Sie Ihren Physik-Zeitschritt nicht auf ein festes ganzzahliges Vielfaches Ihres festen Rendering-Zeitschritts festlegen. Sie präsentieren sie entweder zu früh oder zu spät. Die einzige Möglichkeit, eine feste Renderrate zu erhalten und dennoch etwas physikalisch Korrektes zu präsentieren, besteht darin, zu akzeptieren, dass Sie sich zum Zeitpunkt des Rendering-Intervalls höchstwahrscheinlich auf halbem Weg zwischen zwei Ihrer festen Physik-Zeitschritte befinden. Dies bedeutet jedoch nicht, dass die Objekte während des Renderns geändert werden. Nur, dass das Rendering vorübergehend festlegen muss, wo sich die Objekte befinden, damit sie irgendwo zwischen dem Ort, an dem sie sich vor und dem Ort nach dem Update befanden, gerendert werden können. Das ist wichtig - ändern Sie niemals den Weltzustand für das Rendern, nur Aktualisierungen sollten den Weltzustand ändern.
Um es in eine Pseudocode-Schleife zu bringen, brauche ich etwas mehr wie:
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);
}
Damit dies funktioniert, müssen alle zu aktualisierenden Objekte wissen, wo sie sich zuvor befanden und wo sie sich jetzt befinden, damit das Rendering das Wissen darüber verwenden kann, wo sich das Objekt befindet.
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);
}
}
Lassen Sie uns eine Zeitleiste in Millisekunden erstellen, die besagt, dass das Rendern 3 ms dauert, das Aktualisieren 1 ms dauert, Ihr Aktualisierungszeitschritt auf 5 ms festgelegt ist und Ihr Renderzeitschritt bei 16 ms [60 Hz] beginnt (und bleibt).
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
- Zuerst initialisieren wir zum Zeitpunkt 0 (also currentTime = 0)
- Wir rendern mit einem Anteil von 1,0 (100% currentTime), der die Welt zum Zeitpunkt 0 zeichnet
- Wenn dies abgeschlossen ist, beträgt die tatsächliche Zeit 3, und wir erwarten nicht, dass der Frame bis 16 endet. Daher müssen wir einige Updates ausführen
- T + 3: Wir aktualisieren von 0 auf 5 (also danach currentTime = 5, previousTime = 0)
- T + 4: noch vor dem Frame-Ende, also aktualisieren wir von 5 auf 10
- T + 5: noch vor dem Frame-Ende, also aktualisieren wir von 10 auf 15
- T + 6: noch vor dem Frame-Ende, also aktualisieren wir von 15 auf 20
- T + 7: noch vor dem Frame-Ende, aber currentTime liegt direkt hinter dem Frame-Ende. Wir möchten nicht weiter simulieren, da dies uns über die Zeit hinaus treiben würde, die wir als nächstes rendern möchten. Stattdessen warten wir leise auf das nächste Renderintervall (16)
- T + 16: Es ist Zeit, erneut zu rendern. previousTime ist 15, currentTime ist 20. Wenn wir also bei T + 16 rendern möchten, sind wir 1 ms durch den 5 ms langen Zeitschritt. Wir sind also 20% des Weges durch den Rahmen (Anteil = 0,2). Beim Rendern zeichnen wir Objekte 20% des Weges zwischen ihrer vorherigen Position und ihrer aktuellen Position.
- Kehren Sie zu 3. zurück und fahren Sie auf unbestimmte Zeit fort.
Es gibt noch eine weitere Nuance, zu weit im Voraus zu simulieren, was bedeutet, dass die Eingaben des Benutzers möglicherweise ignoriert werden, obwohl sie vor dem tatsächlichen Rendern des Frames aufgetreten sind. Machen Sie sich darüber jedoch keine Sorgen, bis Sie sicher sind, dass die Schleife reibungslos simuliert.