Bei der Soft Body Dynamics werden realistisch verformbare Objekte simuliert. Wir werden es hier verwenden, um einen zerreißbaren Stoffvorhang und eine Reihe von Ragdolls zu simulieren, mit denen Sie interagieren und mit dem Bildschirm umgehen können. Es wird schnell, stabil und einfach genug sein, um mit Mathematik auf Highschool-Niveau zu tun.
Hinweis: Obwohl dieses Tutorial in Processing geschrieben und mit Java kompiliert wurde, sollten Sie in der Lage sein, in fast jeder Spieleentwicklungsumgebung dieselben Techniken und Konzepte anzuwenden.
In dieser Demo sehen Sie einen großen Vorhang (der die Stoffsimulation vorführt) und eine Reihe kleiner Stickmen (die die Ragdoll-Simulation zeigen):
Sie können die Demo auch selbst ausprobieren. Klicken und ziehen Sie, um zu interagieren, drücken Sie 'R' zum Zurücksetzen und drücken Sie 'G', um die Schwerkraft umzuschalten.
Die Bausteine unseres Spiels sind der Punkt. Um Mehrdeutigkeiten zu vermeiden, nennen wir es das PointMass
. Die Details sind im Namen: es ist ein Punkt im Raum und repräsentiert eine Masse.
Die grundlegendste Art und Weise, Physik für diesen Punkt zu implementieren, besteht darin, die Geschwindigkeit auf irgendeine Weise "vorwärts" zu bringen.
x = x + velX y = y + velY
Wir können nicht davon ausgehen, dass unser Spiel die ganze Zeit mit der gleichen Geschwindigkeit läuft. Für einige Benutzer kann es mit 15 Frames pro Sekunde laufen, für andere jedoch mit 60 Bildern. Es ist am besten, die Frameraten aller Bereiche zu berücksichtigen, was mit einem Zeitschritt erfolgen kann.
x = x + velX * timeElapsed y = y + velY * timeElapsed
Wenn auf diese Weise ein Frame länger dauern würde als für eine andere Person, würde das Spiel immer noch mit der gleichen Geschwindigkeit laufen. Für eine Physik-Engine ist dies jedoch unglaublich instabil.
Stellen Sie sich vor, Ihr Spiel bleibt ein oder zwei Sekunden stehen. Der Motor würde das überkompensieren und den Motor bewegen PointMass
an mehreren Wänden und Objekten vorbei, mit denen er sonst eine Kollision hätte feststellen können. Somit wäre nicht nur die Kollisionserkennung betroffen, sondern auch die Methode der Beschränkungslösung, die wir verwenden werden.
Wie können wir die Stabilität der ersten Gleichung haben?, x = x + velX
, mit der Konsistenz der zweiten Gleichung, x = x + velX * timeElapsed
? Was wäre, wenn wir vielleicht die beiden kombinieren könnten??
Genau das machen wir. Stell dir unsere vor verstrichene Zeit
war 30
. Wir könnten genau dasselbe wie die letztere Gleichung tun, aber mit einer höheren Genauigkeit und Auflösung, indem wir aufrufen x = x + (velX * 5)
sechsmal.
elapsedTime = lastTime - currentTime lastTime = currentTime // setze lastTime // füge Zeit hinzu, die nicht verwendet werden konnte. Frame = verstrichene Zeit + = leftOverTime // Unterteilung in 16-ms-Zeitblöcke = Zeit (= abgelaufene Zeit / 16) // Speicherzeit Wir konnten nicht für den nächsten Frame verwenden. leftOverTime = abgelaufene Zeit - Zeitschritte * 16 für (i = 0; i < timesteps; i++) x = x + velX * 16 y = y + velY * 16 // solve constraints, look for collisions, etc.
Der Algorithmus verwendet hier einen festen Zeitschritt, der größer als eins ist. Er findet die verstrichene Zeit, zerlegt sie in "Brocken" fester Größe und schiebt die verbleibende Zeit auf den nächsten Frame. Nach und nach führen wir die Simulation für jeden Abschnitt durch, in den unsere verstrichene Zeit aufgeteilt ist.
Ich wählte 16 für die Zeitschrittgröße, um die Physik so zu simulieren, als würde sie mit ungefähr 60 Frames pro Sekunde laufen. Umwandlung von verstrichene Zeit
Frames pro Sekunde können mit ein paar Berechnungen gemacht werden: 1 Sekunde / abgelaufeneTimeInSeconds
.
1 s / (16 ms / 1000 s) = 62,5 fps
, ein 16ms-Zeitschritt entspricht also 62,5 Bildern pro Sekunde.
Einschränkungen sind Einschränkungen und Regeln, die der Simulation hinzugefügt werden, um zu bestimmen, wohin PointMasses gehen kann und wo nicht.
Sie können als Randbedingung einfach sein, um zu verhindern, dass sich PointMasses vom linken Rand des Bildschirms bewegt:
wenn (x < 0) x = 0 if (velX < 0) velX = velX * -1
Das Hinzufügen der Einschränkung für den rechten Bildschirmrand erfolgt auf ähnliche Weise:
if (x> width) x = width wenn (velX> 0) velX = velX * -1
Wenn Sie dies für die y-Achse tun, müssen Sie jedes x in ein y ändern.
Wenn Sie die richtigen Einschränkungen haben, kann dies zu sehr schönen und faszinierenden Interaktionen führen. Einschränkungen können auch äußerst komplex werden. Stellen Sie sich vor, Sie simulieren einen vibrierenden Körbchenkorn, bei dem sich keines der Körner kreuzt, oder einen 100-Gelenk-Roboterarm oder etwas ganz einfaches als Stapel von Kisten. Ein typischer Prozess besteht darin, Kollisionspunkte zu finden, den genauen Zeitpunkt der Kollision zu ermitteln und dann die richtige Kraft oder den richtigen Impuls für jeden Körper zu finden, um diese Kollision zu verhindern.
Das Verständnis der Komplexität, die ein Satz von Einschränkungen haben kann, kann schwierig sein und diese Einschränkungen dann lösen, in Echtzeit ist noch schwieriger. Was wir tun, ist die Vereinfachung der Einschränkung erheblich zu vereinfachen.
Ein Mathematiker und Programmierer namens Thomas Jakobsen erforschte einige Möglichkeiten, die Physik von Charakteren für Spiele zu simulieren. Er schlug vor, dass Genauigkeit bei weitem nicht so wichtig ist wie Glaubwürdigkeit und Leistung. Das Herzstück seines gesamten Algorithmus war eine Methode, die seit den 60er Jahren zur Modellierung der molekularen Dynamik verwendet wird Verlet-Integration. Sie sind möglicherweise mit dem Spiel Hitman: Codename 47 vertraut. Es war eines der ersten Spiele, in denen die Ragdoll-Physik verwendet wurde, und es werden die von Jakobsen entwickelten Algorithmen verwendet.
Verlet Integration ist die Methode, mit der wir die Position unseres PointMass weiterleiten. Was wir vorher gemacht haben, x = x + velX
, ist eine Methode mit dem Namen Euler-Integration (die ich auch beim Codieren von destruktivem Pixel-Gelände verwendet habe)..
Der Hauptunterschied zwischen der Integration von Euler und Verlet besteht darin, wie Geschwindigkeit implementiert wird. Mit Euler wird eine Geschwindigkeit mit dem Objekt gespeichert und mit jedem Frame an die Position des Objekts angehängt. Bei Verwendung von Verlet wird jedoch Trägheit angewendet, indem die vorherige und die aktuelle Position verwendet werden. Nehmen Sie die Differenz in den beiden Positionen und fügen Sie sie der letzten Position hinzu, um die Trägheit anzuwenden.
// Trägheit: bewegte Objekte bleiben in Bewegung. velX = x - lastX velY = y - lastY nextX = x + velX + accX * timestepSq nextY = y + velY + accY * timestepSq lastX = x lastY = y x = nextX y = nextY
Wir haben die Schwerkraft beschleunigt. Anders als das, accX
und accY
Für das Lösen von Kollisionen ist es nicht erforderlich. Mit der Verlet-Integration müssen wir keine Kollisionen mehr durchführen. Das Ändern der Position allein reicht aus, um eine stabile, realistische und schnelle Simulation zu erhalten. Was Jakobsen entwickelte, ist ein linearer Ersatz für etwas, das sonst nichtlinear ist.
Die Vorteile von Verlet Integration lassen sich am besten am Beispiel zeigen. In einer Fabric-Engine haben wir nicht nur PointMasses, sondern auch Links zwischen ihnen. Unsere "Links" sind eine Distanzbeschränkung zwischen zwei PointMasses. Idealerweise möchten wir, dass zwei PointMasses mit dieser Einschränkung immer einen bestimmten Abstand voneinander haben.
Wenn wir diese Einschränkung lösen, sollte Verlet Integration diese Punktmassen in Bewegung halten. Wenn zum Beispiel ein Ende schnell nach unten bewegt werden soll, muss das andere Ende wie eine Peitsche durch Trägheit folgen.
Für jedes Paar von verbundenen PointMasses wird nur ein Link benötigt. Alle Daten, die Sie für den Link benötigen, sind die PointMasses und die Reststrecken. Optional können Sie Steifheit haben, um die Frühlingsbeschränkung zu erhöhen. In unserer Demo haben wir auch eine "Tränenempfindlichkeit", dh die Entfernung, bei der die Verbindung entfernt wird.
Ich werde es nur erklären restingDistance
hier, aber der Reißabstand und die Steifigkeit sind sowohl in der Demo als auch im Quellcode implementiert.
Link restingDistance tearDistance Steifheit PointMass A PointMass B lösen () math zum Lösen der Entfernung
Sie können lineare Algebra verwenden, um nach der Einschränkung zu suchen. Ermitteln Sie die Abstände zwischen den beiden und bestimmen Sie, wie weit Sie entlang der Entfernung fahren restingDistance
Sie sind es, dann übersetzen Sie sie basierend auf dem und ihren Unterschieden.
// Berechne den Abstand diffX = p1.x - p2.x diffY = p1.y - p2.yd = sqrt (diffX * diffX + diffY * diffY) // Differenz-Skalardifferenz = (RestingDistance - d) / d // Translation für jeden PointMass. Sie werden um die Hälfte des erforderlichen Abstandes verschoben, um sich dem Ruheabstand anzupassen. translateX = diffX * 0,5 * Differenz translateY = diffY * 0,5 * Differenz p1.x + = translateX p1.y + = translateY p2.x - = translateX p2.y - = translateY
In der Demo berücksichtigen wir auch Masse und Steifheit. Bei der Lösung dieser Einschränkung gibt es einige Probleme. Wenn mehr als zwei oder drei PointMasses miteinander verbunden sind, kann das Lösen einiger dieser Einschränkungen die zuvor gelösten Einschränkungen verletzen.
Thomas Jakobsen ist auch auf dieses Problem gestoßen. Zuerst könnte man ein Gleichungssystem erstellen und alle Beschränkungen auf einmal lösen. Dies würde jedoch rasch an Komplexität zunehmen und es wäre schwierig, mehr als nur wenige Links in das System einzufügen.
Jakobsen entwickelte eine Methode, die auf den ersten Blick dumm und naiv wirkt. Er hat eine Methode namens "Entspannung" entwickelt, bei der wir die Beschränkung nicht einmal lösen, sondern mehrmals lösen. Jedes Mal, wenn wir nach den Links wiederholen und sie analysieren, werden die Links immer näher an alle zu lösenden Objekte.
Um es zusammenzufassen, hier ist, wie unsere Engine im Pseudocode arbeitet. Ein genaueres Beispiel finden Sie im Quellcode der Demo.
animationLoop numPhysicsUpdates = Viele können jedoch in die verstrichene Zeit für (jedes numPhysicsUpdates) // passen (wobei constraintSolve eine Zahl von 1 oder höher ist. Ich verwende normalerweise 3 für (jedes constraintSolve) für (jedes Link-Constraint) lösungs-Constraint) // Beenden der Link-Einschränkung // Beenden der Constraint-Aktualisierung Physik // (Verlet verwenden!) // Ende der Physikaktualisierung Zeichnungspunkte und Links
Jetzt können wir den Stoff selbst bauen. Das Erstellen der Links sollte ziemlich einfach sein: Linke Links, wenn PointMass nicht der erste in der Zeile ist, und Link, wenn es nicht der Erste in seiner Spalte ist.
Die Demo verwendet eine eindimensionale Liste, um PointMasses zu speichern, und findet Punkte, auf die mithilfe von Links verwiesen werden kann x + y * Breite
.
// Wir möchten, dass sich die Y-Schleife außerhalb befindet, sodass sie Zeile für Zeile anstelle von Spalte für Spalte nach (jedes Y von 0 bis Höhe) nach (jedes X von 0 bis Breite) neues PointMass durchsucht wird at x, y // links anhängen, wenn (x! = 0) PM an letzte PM in Liste anfügen // rechts anhängen, wenn (y! = 0) PM an PM @ ((y - 1) * anhängen ( width + 1) + x) in der Liste, wenn (y == 0) Pin PM PM zur Liste hinzufügen
Möglicherweise stellen Sie im Code fest, dass wir auch "Pin PM" haben. Wenn wir nicht wollen, dass unser Vorhang fällt, können wir die oberste Reihe der PointMasses an ihren Startpositionen fixieren. Um eine Pin-Einschränkung zu programmieren, fügen Sie einige Variablen hinzu, um die Position der Pins zu verfolgen, und verschieben Sie den PointMass nach jeder Auflösung der Einschränkung an diese Position.
Ragdolls waren Jakobsens ursprüngliche Absichten, Verlet Integration einzusetzen. Zuerst beginnen wir mit den Köpfen. Wir erstellen eine Circle-Einschränkung, die nur mit der Grenze interagiert.
Kreis PointMass-Radius lösen () wenn (y < radius) y = 2*(radius) - y; if (y > Höhenradius) y = 2 * (Höhe - Radius) - y; if (x> Breitenradius) x = 2 * (Breite - Radius) - x; wenn (x < radius) x = 2*radius - x;
Als Nächstes können wir den Körper erstellen. Ich habe jeden Körperteil hinzugefügt, um die Masse- und Längenanteile eines normalen menschlichen Körpers einigermaßen genau anzupassen. Auschecken Body.pde
in den Quelldateien für vollständige Details. Dies führt uns zu einem anderen Problem: Der Körper wird sich leicht in unbeholfene Formen verwinden und wirkt sehr unrealistisch.
Es gibt verschiedene Möglichkeiten, dies zu beheben. In der Demo verwenden wir unsichtbare und sehr unbewegliche Verbindungen von den Füßen zur Schulter und vom Becken zum Kopf, um den Körper auf natürliche Weise in eine weniger unbequeme Ruheposition zu bringen.
Sie können auch Scheinwinkelbeschränkungen erstellen, indem Sie Verknüpfungen verwenden. Nehmen wir an, wir haben drei PointMasses, von denen zwei mit einem in der Mitte verbunden sind. Sie können eine Länge zwischen den Enden finden, um jeden gewählten Winkel zu erfüllen. Um diese Länge zu finden, können Sie das Cosinus-Gesetz verwenden.
A = Ruhedistanz vom EndpunktMass zum Mittelpunkt PointMass B = Ruhedistanz vom anderen PointMass zum Mittelpunkt PointMass Länge = sqrt (A * A + B * B * 2 * A * B * cos (Winkel)) stellt eine Verbindung zwischen dem EndpunktMass mit Längen her als Ruhedistanz
Ändern Sie den Link so, dass diese Einschränkung nur gilt, wenn der Abstand geringer als der Ruheabstand ist oder wenn der Abstand größer ist. Dadurch wird verhindert, dass der Winkel im Mittelpunkt je nach Bedarf zu nah oder zu weit ist.
Mit einer vollständig linearen Physik-Engine ist die Tatsache, dass es sich um eine beliebige Dimension handeln kann, die Sie möchten. Alles, was mit x zu tun war, wurde auch mit einem y-Wert erledigt, und daher kann es auf drei oder sogar vier Dimensionen ausgedehnt werden (ich bin nicht sicher, wie Sie das rendern würden!)
Zum Beispiel eine Link-Einschränkung für die Simulation in 3D:
// Berechne den Abstand diffX = p1.x - p2.x diffY = p1.y - p2.y diffZ = p1.z - p2.zd = sqrt (diffX * diffX + diffY * diffY + diffZ * diffZ) // Unterschied skalare Differenz = (restingDistance - d) / d // Übersetzung für jeden PointMass. Sie werden um die Hälfte des erforderlichen Abstandes verschoben, um sich dem Ruheabstand anzupassen. translateX = diffX * 0,5 * Differenz translateY = diffY * 0,5 * Differenz translateZ = diffZ * 0,5 * Differenz p1.x + = translateX p1.y + = translateY p1.z + = translateZ p2.x - = translateX p2.y - = translateY p2.z - = translateZ
Danke fürs Lesen! Ein Großteil der Simulation basiert stark auf Thomas Jakobsens Artikel über Advanced Character Physics aus der GDC 2001. Ich bemühte mich, das meiste der komplizierten Dinge zu beseitigen und vereinfachte den Punkt, den die meisten Programmierer verstehen werden. Wenn Sie Hilfe benötigen oder Kommentare haben, können Sie gerne unten posten.