Wie interpoliere ich zwischen zwei Spielzuständen?

24

Was ist das beste Muster, um ein System zu erstellen, bei dem alle Objektpositionen zwischen zwei Aktualisierungszuständen interpoliert werden sollen?

Das Update wird immer mit der gleichen Frequenz ausgeführt, aber ich möchte in der Lage sein, mit jedem FPS zu rendern. Das Rendering wird also so flüssig wie möglich, unabhängig davon, ob die Frames pro Sekunde niedriger oder höher als die Aktualisierungsfrequenz sind.

Ich möchte 1 Frame in die Zukunft aktualisieren und vom aktuellen Frame zum zukünftigen Frame interpolieren. Diese Antwort hat einen Link, der darüber spricht:

Halbfester oder vollständig festgelegter Zeitschritt?

Edit: Wie könnte ich auch die letzte und aktuelle Geschwindigkeit in der Interpolation verwenden? Beispiel: Bei einer reinen linearen Interpolation bewegt sich die Kamera zwischen den Positionen mit derselben Geschwindigkeit. Ich brauche eine Möglichkeit, die Position zwischen den beiden Punkten zu interpolieren, aber berücksichtige die Geschwindigkeit an jedem Punkt für die Interpolation. Dies ist hilfreich für Simulationen mit niedrigen Raten wie Partikeleffekte.

AttackingHobo
quelle
2
Zecken sind logische Zecken? Also dein Update fps <Rendering fps?
Die kommunistische Ente
Ich habe den Begriff geändert. Aber ja, Logik tickt. Und nein, ich möchte das Rendering vollständig von der Aktualisierung befreien, damit das Spiel bei 120 Hz oder 22,8 Hz rendern kann und die Aktualisierung weiterhin mit derselben Geschwindigkeit ausgeführt wird, sofern der Benutzer die Systemanforderungen erfüllt.
AttackingHobo
Dies kann sehr schwierig sein, da beim Rendern alle Objektpositionen still bleiben sollten (Änderungen während des Renderns können zu ungeklärtem Verhalten führen)
Ali1S232
Die Interpolation würde den Zustand zu einem Zeitpunkt zwischen 2 bereits berechneten Aktualisierungsrahmen berechnen. Handelt es sich bei dieser Frage nicht um eine Extrapolation, die den Status für eine Zeit nach dem letzten Aktualisierungsframe berechnet ? Seit dem nächsten Update ist noch nicht mal gekappt.
Maik Semder
Ich denke, wenn er nur einen Thread hat, der aktualisiert / rendert, kann es nicht passieren, dass nur die Renderposition erneut aktualisiert wird. Sie senden nur Positionen an die GPU und aktualisieren sie dann erneut.
Zacharmarz

Antworten:

22

Sie möchten Aktualisierungsraten (logisches Häkchen) und Ziehungsraten (Renderhäkchen) trennen.

Ihre Aktualisierungen erzeugen die Position aller zu zeichnenden Objekte in der Welt.

Ich werde hier zwei verschiedene Möglichkeiten behandeln, die von Ihnen gewünschte, die Extrapolation und auch eine andere Methode, die Interpolation.

1.

Bei der Extrapolation wird die (vorhergesagte) Position des Objekts im nächsten Frame berechnet und dann zwischen der aktuellen Objektposition und der Position des Objekts im nächsten Frame interpoliert.

Dazu muss jedem zu zeichnenden Objekt ein velocityund zugeordnet sein position. Um die Position zu ermitteln, an der sich das Objekt im nächsten Frame befindet, addieren wir einfach velocity * draw_timestepdie aktuelle Position des Objekts, um die vorhergesagte Position des nächsten Frames zu ermitteln. draw_timestepist die Zeit, die seit dem vorherigen Render-Tick (auch bekannt als Draw-Call) vergangen ist.

Wenn Sie es dabei belassen, werden Sie feststellen, dass Objekte "flackern", wenn ihre vorhergesagte Position nicht mit der tatsächlichen Position im nächsten Frame übereinstimmt. Um das Flackern zu beseitigen, können Sie die vorhergesagte Position speichern und bei jedem Zeichenschritt zwischen der zuvor vorhergesagten Position und der neuen vorhergesagten Position hin- und herschalten , wobei die seit der vorherigen Aktualisierung verstrichene Zeit als Lerp-Faktor verwendet wird. Dies führt immer noch zu einem schlechten Verhalten, wenn sich schnell bewegende Objekte plötzlich ändern und Sie diesen Sonderfall möglicherweise behandeln möchten. Alles, was in diesem Absatz gesagt wird, ist der Grund, warum Sie keine Extrapolation verwenden möchten.

2.

Bei der Interpolation wird der Status der letzten beiden Aktualisierungen gespeichert und zwischen diesen basierend auf der aktuellen Zeitspanne interpoliert, die seit der letzten Aktualisierung vergangen ist. In diesem Setup muss jedem Objekt ein positionund zugeordnet sein previous_position. In diesem Fall stellt unsere Zeichnung im schlimmsten Fall einen Update-Tick hinter dem aktuellen Gamestate dar und im besten Fall genau denselben Status wie der aktuelle Update-Tick.


Meiner Meinung nach möchten Sie wahrscheinlich eine Interpolation, wie ich sie beschrieben habe, da sie einfacher zu implementieren ist und es in Ordnung ist, einen winzigen Sekundenbruchteil (z. B. 1/60 Sekunde) hinter Ihrem aktuellen aktualisierten Status zu zeichnen.


Bearbeiten:

Falls das oben Genannte nicht ausreicht, um eine Implementierung durchzuführen, finden Sie hier ein Beispiel für die von mir beschriebene Interpolationsmethode. Ich werde nicht auf die Hochrechnung eingehen, da mir kein reales Szenario einfällt, in dem Sie es vorziehen sollten.

Wenn Sie ein ziehbar Objekt erstellen, wird es um die Eigenschaften speichern erforderlich gezogen werden (dh der Zustand benötigten Informationen zu ziehen).

In diesem Beispiel werden Position und Drehung gespeichert. Möglicherweise möchten Sie auch andere Eigenschaften wie Farbe oder Texturkoordinatenposition speichern (z. B. wenn eine Textur gescrollt wird).

Um zu verhindern, dass Daten geändert werden, während der Render-Thread sie zeichnet (dh die Position eines Objekts wird geändert, während der Render-Thread zeichnet, aber alle anderen wurden noch nicht aktualisiert), müssen wir eine Art Doppelpuffer implementieren.

Ein Objekt speichert zwei Kopien davon previous_state. Ich werde sie in ein Array stellen und sie als previous_state[0]und bezeichnen previous_state[1]. Es werden ebenfalls zwei Kopien benötigt current_state.

Um zu verfolgen, welche Kopie des Doppelpuffers verwendet wird, speichern wir eine Variable state_index, die sowohl dem Update- als auch dem Draw-Thread zur Verfügung steht.

Der Update-Thread berechnet zunächst alle Eigenschaften eines Objekts unter Verwendung seiner eigenen Daten (beliebige Datenstrukturen). Dann kopiert er current_state[state_index]zu previous_state[state_index]und kopiert die neuen Daten relevant für das Zeichnen, positionund rotationin current_state[state_index]. Dann wird state_index = 1 - state_indexdie aktuell verwendete Kopie des Doppelpuffers umgedreht.

Alles im obigen Absatz muss mit herausgenommenem Schloss gemacht werden current_state. Das Update und Draw Threads entfernen beide diese Sperre. Die Sperre wird nur für die Dauer des schnellen Kopierens von Statusinformationen aufgehoben.

Im Render-Thread führen Sie dann eine lineare Interpolation von Position und Drehung wie folgt durch:

current_position = Lerp(previous_state[state_index].position, current_state[state_index].position, elapsed/update_tick_length)

Wo elapsedist die Zeit, die im Render-Thread seit dem letzten Update-Tick vergangen ist, und update_tick_lengthwie lange dauert Ihre feste Update-Rate pro Tick (z. B. bei 20FPS-Updates update_tick_length = 0.05) ?

Wenn Sie die Lerpobige Funktion nicht kennen , lesen Sie den Artikel von Wikipedia zum Thema: Lineare Interpolation . Wenn Sie jedoch nicht wissen, was Lerping ist, sind Sie wahrscheinlich nicht bereit, entkoppelte Aktualisierungen / Zeichnungen mit interpolierten Zeichnungen zu implementieren.

Olhovsky
quelle
1
+1 Gleiches gilt für Orientierungen / Rotationen und alle anderen Zustände, die sich im Laufe der Zeit ändern, z. B. Materialanimationen in Partikelsystemen usw.
Maik Semder
1
Guter Punkt Maik, ich habe nur die Position als Beispiel genommen. Sie müssen die "Geschwindigkeit" jeder Eigenschaft speichern, die Sie extrapolieren möchten (dh die Änderungsrate über die Zeit dieser Eigenschaft), wenn Sie die Extrapolation verwenden möchten. Am Ende kann ich mir wirklich keine Situation vorstellen, in der Extrapolation besser ist als Interpolation. Ich habe sie nur einbezogen, weil die Frage des Fragestellers danach gefragt hat. Ich benutze Interpolation. Bei der Interpolation müssen wir die aktuellen und vorherigen Aktualisierungsergebnisse aller zu interpolierenden Eigenschaften speichern, wie Sie sagten.
Olhovsky
Dies ist eine Wiederholung des Problems und des Unterschieds zwischen Interpolation und Extrapolation. Es ist keine Antwort.
1
In meinem Beispiel habe ich Position und Rotation im Zustand gespeichert. Sie können auch nur die Geschwindigkeit (oder Geschwindigkeit) im Status speichern. Dann lernst du genau so zwischen den Geschwindigkeiten ( Lerp(previous_speed, current_speed, elapsed/update_tick_length)). Sie können dies mit einer beliebigen Nummer tun, die Sie im Bundesstaat speichern möchten. Beim Lerping erhalten Sie nur einen Wert zwischen zwei Werten, wenn ein Lerp-Faktor angegeben wird.
Olhovsky
1
Für die Interpolation von Winkelbewegungen wird empfohlen, Slerp anstelle von Lerp zu verwenden. Am einfachsten wäre es, die Quaternionen beider Staaten und Slerp zwischen ihnen aufzubewahren. Ansonsten gelten für Winkelgeschwindigkeit und Winkelbeschleunigung die gleichen Regeln. Haben Sie einen Testfall für die Skelettanimation?
Maik Semder
-2

Für dieses Problem müssen Sie Ihre Definitionen von Start und Ende etwas anders überlegen. Anfänger denken oft an Positionsänderungen pro Frame und das ist ein guter Anfang. Betrachten wir für meine Antwort eine eindimensionale Antwort.

Angenommen, Sie haben einen Affen an Position x. Jetzt haben Sie auch ein "addX", zu dem Sie die Position des Affen pro Frame basierend auf der Tastatur oder einem anderen Steuerelement hinzufügen. Dies funktioniert, solange Sie eine garantierte Bildrate haben. Nehmen wir an, Ihr x ist 100 und Ihr addX ist 10. Nach 10 Frames sollte sich Ihr x + = addX auf 200 summieren.

Anstelle von addX sollten Sie jetzt bei variabler Bildrate in Bezug auf Geschwindigkeit und Beschleunigung denken. Ich werde Sie durch all diese Arithmetik führen, aber es ist super einfach. Wir möchten wissen, wie weit Sie pro Millisekunde (1/1000 Sekunde) fahren möchten.

Wenn Sie mit 30 Bildern pro Sekunde aufnehmen, sollte Ihre Geschwindigkeit 1/3-Sekunde betragen (10 Bilder aus dem letzten Beispiel bei 30 Bildern pro Sekunde), und Sie wissen, dass Sie in dieser Zeit 100 'x' zurücklegen möchten. Stellen Sie daher Ihre Geschwindigkeit auf 100 Entfernung / 10 FPS oder 10 Entfernung pro Frame. In Millisekunden entspricht dies 1 Abstand x pro 3,3 Millisekunden oder 0,3 'x' pro Millisekunde.

Jetzt müssen Sie bei jedem Update nur noch die verstrichene Zeit herausfinden. Unabhängig davon, ob 33 ms verstrichen sind (1/30 Sekunde) oder was auch immer, multiplizieren Sie einfach den Abstand 0,3 mit der Anzahl der verstrichenen Millisekunden. Dies bedeutet, dass Sie einen Zeitgeber benötigen, der Ihnen ms-Genauigkeit (Millisekunden) gibt, aber die meisten Zeitgeber geben Ihnen dies. Mach einfach so etwas:

var beginTime = getTimeInMillisecond ()

... später ...

var time = getTimeInMillisecond ()

var elapsedTime = time-beginTime

beginTime = Zeit

... Verwenden Sie jetzt diese verstrichene Zeit, um alle Ihre Entfernungen zu berechnen.

Mickey
quelle
1
Er hat keine variable Aktualisierungsrate. Er hat eine feste Aktualisierungsrate. Um ehrlich zu sein, ich weiß wirklich nicht, welchen Punkt Sie hier
ansprechen wollen
1
??? -1. Das ist der springende Punkt, ich habe eine garantierte Aktualisierungsrate, aber eine variable Renderrate, und ich möchte, dass es glatt und ohne Ruckeln ist.
AttackingHobo
Variable Aktualisierungsraten funktionieren nicht gut mit vernetzten Spielen, Konkurrenzspielen, Wiedergabesystemen oder anderen Dingen, die darauf beruhen, dass das Spiel deterministisch ist.
AttackingHobo
1
Ein festes Update ermöglicht auch die einfache Integration von Pseudo-Friktion. Wenn Sie beispielsweise Ihre Geschwindigkeit mit 0,9 pro Frame multiplizieren möchten, wie können Sie dann herausfinden, mit wie viel multipliziert werden soll, wenn Sie einen schnellen oder langsamen Frame haben? Ein festes Update wird manchmal stark bevorzugt - praktisch alle Physiksimulationen verwenden eine feste Aktualisierungsrate.
Olhovsky
2
Wenn ich eine variable Framerate verwendet habe und einen komplexen Anfangszustand mit vielen voneinander abprallenden Objekten eingerichtet habe, gibt es keine Garantie dafür, dass es genau dasselbe simuliert. Tatsächlich wird es höchstwahrscheinlich jedes Mal etwas anders simulieren, mit kleinen Unterschieden zu Beginn, die sich über einen kurzen Zeitraum in völlig unterschiedliche Zustände zwischen den einzelnen Simulationsläufen zusammensetzen.
AttackingHobo