Sploosh! In diesem Tutorial zeige ich Ihnen, wie Sie mit einfachen Mathematik-, Physik- und Teilcheneffekten großartig aussehende 2D-Wasserwellen und -tröpfchen simulieren können.
Hinweis: Obwohl dieses Tutorial mit C # und XNA geschrieben wurde, sollten Sie in der Lage sein, in fast jeder Spieleentwicklungsumgebung dieselben Techniken und Konzepte anzuwenden.
Wenn Sie XNA haben, können Sie die Quelldateien herunterladen und die Demo selbst kompilieren. Ansonsten schauen Sie sich das Demo-Video unten an:
Die Wassersimulation besteht aus zwei weitgehend unabhängigen Teilen. Zuerst machen wir die Wellen mit einem Federmodell. Zweitens verwenden wir Partikeleffekte, um Spritzer hinzuzufügen.
Um die Wellen zu erzeugen, modellieren wir die Wasseroberfläche als eine Reihe vertikaler Quellen, wie in diesem Diagramm dargestellt:
Dadurch können die Wellen auf und ab rollen. Wir werden dann Wasserpartikel an ihren benachbarten Partikeln ziehen lassen, damit sich die Wellen ausbreiten können.
Eine tolle Sache an Federn ist, dass sie einfach zu simulieren sind. Federn haben eine gewisse natürliche Länge; Wenn Sie eine Feder dehnen oder komprimieren, versucht sie, auf diese natürliche Länge zurückzukehren.
Die Kraft, die eine Feder liefert, wird durch das Hooke'sche Gesetz gegeben:
\ [
F = -kx
\]
F
ist die von der Feder erzeugte Kraft, k
ist die Federkonstante und x
ist die Verschiebung der Feder von ihrer natürlichen Länge. Das negative Vorzeichen gibt an, dass die Kraft in der entgegengesetzten Richtung liegt, in die die Feder verschoben wird. Wenn Sie die Feder nach unten drücken, wird sie nach oben gedrückt und umgekehrt.
Die Federkonstante, k
, bestimmt die Steifheit der Feder.
Um Federn zu simulieren, müssen wir anhand von Hookes Gesetz herausfinden, wie Partikel bewegt werden. Dafür brauchen wir ein paar weitere Formeln aus der Physik. Erstens, Newtons zweiter Bewegungssatz:
\ [
F = ma
\]
Hier, F
ist Kraft, m
ist Masse und ein
ist Beschleunigung. Dies bedeutet, je stärker eine Kraft auf ein Objekt drückt und je leichter das Objekt ist, desto mehr beschleunigt es.
Die Kombination dieser beiden Formeln und das Umordnen ergibt:
\ [
a = - \ frac k m x
\]
Dies gibt uns die Beschleunigung für unsere Teilchen. Wir gehen davon aus, dass alle unsere Partikel die gleiche Masse haben, sodass wir kombinieren können k / m
in eine einzige Konstante.
Um die Position aus der Beschleunigung zu bestimmen, müssen wir die numerische Integration durchführen. Wir werden die einfachste Form der numerischen Integration verwenden - für jeden Frame machen wir einfach Folgendes:
Position + = Geschwindigkeit; Geschwindigkeit + = Beschleunigung;
Dies wird als Euler-Methode bezeichnet. Es ist nicht die genaueste Art der numerischen Integration, aber sie ist schnell, einfach und für unsere Zwecke angemessen.
Alles in allem tun unsere Wasseroberflächenpartikel für jeden Frame folgendes:
Position des öffentlichen Schwimmers, Geschwindigkeit; public void Update () const float k = 0.025f; // Passen Sie diesen Wert nach Ihren Wünschen an. float x = Height - TargetHeight; Schwimmbeschleunigung = -k * x; Position + = Geschwindigkeit; Geschwindigkeit + = Beschleunigung;
Hier, TargetHeight
ist die natürliche Position der Oberseite der Feder, wenn sie weder gedehnt noch zusammengedrückt wird. Sie sollten diesen Wert auf die Stelle einstellen, an der die Wasseroberfläche sein soll. Für die Demo habe ich sie auf 240 Pixel in der Mitte des Bildschirms eingestellt.
Ich habe bereits erwähnt, dass die Federkonstante, k
, steuert die Steifheit der Feder. Sie können diesen Wert anpassen, um die Eigenschaften des Wassers zu ändern. Eine niedrige Federkonstante lockert die Federn. Dies bedeutet, dass eine Kraft große Wellen verursacht, die langsam schwingen. Umgekehrt erhöht eine hohe Federkonstante die Spannung in der Feder. Kräfte erzeugen kleine Wellen, die schnell schwingen. Eine hohe Federkonstante lässt das Wasser eher wie Wackeln mit Jello aussehen.
Eine Warnung: Setzen Sie die Federkonstante nicht zu hoch. Sehr steife Federn bringen sehr starke Kräfte auf, die sich innerhalb kürzester Zeit stark verändern. Dies funktioniert bei der numerischen Integration, die die Federn als Reihe diskreter Sprünge in regelmäßigen Zeitabständen simuliert, nicht gut. Eine sehr steife Feder kann sogar eine Schwingungsperiode haben, die kürzer ist als Ihr Zeitschritt. Schlimmer noch, die Euler-Integrationsmethode neigt dazu, Energie zu gewinnen, wenn die Simulation weniger genau wird, wodurch steife Federn explodieren.
Es gibt bisher ein Problem mit unserem Federmodell. Sobald eine Feder zu schwingen beginnt, hört sie nie auf. Um dieses Problem zu lösen, müssen wir einige anwenden Dämpfung. Die Idee ist, eine Kraft in die entgegengesetzte Richtung aufzubringen, in die sich unsere Feder bewegt, um sie zu verlangsamen. Dies erfordert eine kleine Anpassung unserer Federformel:
\ [
a = - \ frac k m x - dv
\]
Hier, v
ist Geschwindigkeit und d
ist der Dämpfungsfaktor - Eine weitere Konstante können Sie anpassen, um das Gefühl des Wassers anzupassen. Es sollte ziemlich klein sein, wenn Ihre Wellen oszillieren sollen. Die Demo verwendet einen Dämpfungsfaktor von 0,025. Ein hoher Dämpfungsfaktor lässt das Wasser dick aussehen wie Melasse, während ein niedriger Wert die Wellen für lange Zeit schwingen lässt.
Jetzt, da wir eine Quelle machen können, wollen wir sie zum Modellieren von Wasser verwenden. Wie im ersten Diagramm gezeigt, modellieren wir das Wasser mit einer Reihe paralleler, vertikaler Federn. Wenn die Quellen alle unabhängig sind, breiten sich die Wellen natürlich niemals aus wie echte Wellen.
Ich zeige zuerst den Code und gehe ihn dann durch:
für (int i = 0; i < springs.Length; i++) springs[i].Update(Dampening, Tension); float[] leftDeltas = new float[springs.Length]; float[] rightDeltas = new float[springs.Length]; // do some passes where springs pull on their neighbours for (int j = 0; j < 8; j++) for (int i = 0; i < springs.Length; i++) if (i > 0) leftDeltas [i] = Spread * (Federn [i] .Höhe - Federn [i - 1] .Höhe); Federn [i - 1] .Speed + = leftDeltas [i]; wenn ich < springs.Length - 1) rightDeltas[i] = Spread * (springs[i].Height - springs [i + 1].Height); springs[i + 1].Speed += rightDeltas[i]; for (int i = 0; i < springs.Length; i++) if (i > 0) Federn [i - 1] .High + = leftDeltas [i]; wenn ich < springs.Length - 1) springs[i + 1].Height += rightDeltas[i];
Dieser Code wird bei jedem Frame von Ihrem aufgerufen Aktualisieren()
Methode. Hier, Federn
ist eine Reihe von Federn, die von links nach rechts angeordnet sind. leftDeltas
ist ein Array von Schwimmern, das den Höhenunterschied zwischen jeder Feder und ihrem linken Nachbarn speichert. RightDeltas
ist das Äquivalent für die richtigen Nachbarn. Wir speichern alle diese Höhenunterschiede in Arrays, weil die letzten beiden ob
Anweisungen ändern die Höhe der Federn. Wir müssen die Höhenunterschiede messen, bevor eine der Höhen geändert wird.
Der Code beginnt mit der Ausführung des Hookeschen Gesetzes für jede Feder, wie zuvor beschrieben. Anschließend wird der Höhenunterschied zwischen den einzelnen Federn und ihren Nachbarn betrachtet, und jede Feder zieht ihre benachbarten Federn zu sich hin, indem sie die Positionen und Geschwindigkeiten der Nachbarn ändert. Der Nachbarziehschritt wird achtmal wiederholt, damit sich die Wellen schneller ausbreiten können.
Es gibt noch einen weiteren einstellbaren Wert Verbreitung
. Sie steuert, wie schnell sich die Wellen ausbreiten. Es kann Werte zwischen 0 und 0,5 annehmen, wobei größere Wellen die Wellen schneller ausbreiten.
Um die Wellen in Bewegung zu setzen, fügen wir eine einfache Methode hinzu Spritzen()
.
public void Splash (int index, float speed) if (index> = 0 && index.) < springs.Length) springs[i].Speed = speed;
Wenn Sie Wellen schlagen wollen, rufen Sie an Spritzen()
. Das Index
Der Parameter bestimmt, an welcher Quelle das Spritzen entstehen soll, und die Geschwindigkeit
Der Parameter bestimmt, wie groß die Wellen sein werden.
Wir werden die XNA verwenden PrimitiveBatch
Klasse aus dem XNA PrimitivesSample. Das PrimitiveBatch
Klasse hilft uns, Linien und Dreiecke direkt mit der GPU zu zeichnen. Du verwendest es gerne so:
// in LoadContent () primitiveBatch = new PrimitiveBatch (GraphicsDevice); // in Draw () primitiveBatch.Begin (PrimitiveType.TriangleList); foreach (Dreieck in trianglesToDraw) primitiveBatch.AddVertex (triangle.Point1, Color.Red); primitiveBatch.AddVertex (triangle.Point2, Color.Red); primitiveBatch.AddVertex (triangle.Point3, Color.Red); primitiveBatch.End ();
Zu beachten ist, dass Sie standardmäßig die Dreieckscheitelpunkte im Uhrzeigersinn angeben müssen. Wenn Sie sie im Gegenuhrzeigersinn hinzufügen, wird das Dreieck gelöscht und Sie werden es nicht sehen.
Es ist nicht notwendig, für jedes Pixel eine Feder zu haben. In der Demo habe ich 201 Federn verwendet, die über ein 800 Pixel breites Fenster verteilt sind. Das ergibt genau 4 Pixel zwischen jeder Feder, wobei die erste Feder bei 0 und die letzte bei 800 Pixel steht. Sie könnten wahrscheinlich noch weniger Quellen verwenden und trotzdem das Wasser glatt aussehen lassen.
Was wir tun wollen, ist, dünne, hohe Trapezoide zu zeichnen, die sich vom unteren Rand des Bildschirms bis zur Wasseroberfläche erstrecken und die Federn verbinden, wie in diesem Diagramm dargestellt:
Da Grafikkarten keine Trapezoide direkt zeichnen, müssen wir jedes Trapez als zwei Dreiecke zeichnen. Damit es ein bisschen schöner aussieht, wird das Wasser dunkler, je tiefer es wird, wenn die unteren Ecken dunkelblau gefärbt werden. Die GPU interpoliert Farben automatisch zwischen den Scheitelpunkten.
primitiveBatch.Begin (PrimitiveType.TriangleList); Farbe midnightBlue = neue Farbe (0, 15, 40) * 0,9 f; Color lightBlue = neue Farbe (0,2f, 0,5f, 1f) * 0,8f; var viewport = GraphicsDevice.Viewport; Float Bottom = Viewport.Hohe; // Dehne die x-Positionen der Federn, um die gesamte Fensterspeicherskala aufzunehmen = viewport.Width / (springs.Length - 1f); // Verwenden Sie unbedingt die Float-Division für (int i = 1; i < springs.Length; i++) // create the four corners of our triangle. Vector2 p1 = new Vector2((i - 1) * scale, springs[i - 1].Height); Vector2 p2 = new Vector2(i * scale, springs[i].Height); Vector2 p3 = new Vector2(p2.X, bottom); Vector2 p4 = new Vector2(p1.X, bottom); primitiveBatch.AddVertex(p1, lightBlue); primitiveBatch.AddVertex(p2, lightBlue); primitiveBatch.AddVertex(p3, midnightBlue); primitiveBatch.AddVertex(p1, lightBlue); primitiveBatch.AddVertex(p3, midnightBlue); primitiveBatch.AddVertex(p4, midnightBlue); primitiveBatch.End();
Hier ist das Ergebnis:
Die Wellen sehen ziemlich gut aus, aber ich würde gerne ein Plätschern sehen, wenn der Stein das Wasser berührt. Partikeleffekte sind dafür perfekt.
Bei einem Partikeleffekt werden viele kleine Partikel verwendet, um einen visuellen Effekt zu erzielen. Sie werden manchmal für Dinge wie Rauch oder Funken verwendet. Wir werden Partikel für die Wassertröpfchen in den Spritzern verwenden.
Das erste, was wir brauchen, ist unsere Partikelklasse:
Klasse Partikel public Vector2 Position; öffentliche Vector2-Geschwindigkeit; public float Orientierung; public Particle (Vektor2-Position, Vektor2-Geschwindigkeit, Float-Orientierung) Position = Position; Geschwindigkeit = Geschwindigkeit; Orientierung = Orientierung;
Diese Klasse enthält nur die Eigenschaften, die ein Partikel haben kann. Als Nächstes erstellen wir eine Liste von Partikeln.
ListePartikeln = neue Liste ();
Bei jedem Frame müssen wir die Partikel aktualisieren und zeichnen.
void UpdateParticle (Partikelpartikel) const float Schwerkraft = 0,3f; Partikel.Geschwindigkeit.Y + = Schwerkraft; Partikel.Position + = Partikel.Geschwindigkeit; Particle.Orientation = GetAngle (Particle.Velocity); private float GetAngle (Vector2 vector) return (float) Math.Atan2 (vector.Y, vector.X); public void Update () foreach (var-Partikel in Partikeln) UpdateParticle (Partikel); // lösche Partikel, die sich außerhalb des Bildschirms befinden, oder unter Wasserpartikeln = Partikel. Wo (x => x.Position.X> = 0 && x.Position.X <= 800 && x.Position.Y <= GetHeight(x.Position.X)).ToList();
Wir aktualisieren die Partikel so, dass sie unter die Schwerkraft fallen, und stellen die Ausrichtung der Partikel auf die Richtung ein, in der sie sich bewegen. Wir entfernen dann alle Partikel, die sich außerhalb des Bildschirms oder unter Wasser befinden, indem wir alle Partikel kopieren, die wir in eine neue Liste aufnehmen möchten Zuordnung zu Partikeln. Als nächstes zeichnen wir die Partikel.
void DrawParticle (Partikelpartikel) Vektor2-Ursprung = neuer Vector2 (ParticleImage.Width, ParticleImage.Height) / 2f; spriteBatch.Draw (ParticleImage, Partikel.Position, Null, Color.White, Partikel.Orientation, Ursprung, 0.6f, 0, 0); public void Draw () foreach (var-Partikel in Partikeln) DrawParticle (Partikel);
Unten ist die Textur, die ich für die Partikel verwendet habe.
Wann immer wir einen Spritzer erzeugen, machen wir einen Haufen Partikel.
private void CreateSplashParticles (float xPosition, Float-Geschwindigkeit) float y = GetHeight (xPosition); if (Geschwindigkeit> 60) für (int i = 0; i < speed / 8; i++) Vector2 pos = new Vector2(xPosition, y) + GetRandomVector2(40); Vector2 vel = FromPolar(MathHelper.ToRadians(GetRandomFloat(-150, -30)), GetRandomFloat(0, 0.5f * (float)Math.Sqrt(speed))); particles.Add(new Particle(pos, velocity, 0));
Sie können diese Methode aus dem aufrufen Spritzen()
Methode, mit der wir Wellen machen. Der Parameter Geschwindigkeit ist, wie schnell der Stein das Wasser trifft. Wir machen größere Spritzer, wenn sich der Stein schneller bewegt.
GetRandomVector2 (40)
gibt einen Vektor mit einer zufälligen Richtung und einer zufälligen Länge zwischen 0 und 40 zurück. Wir möchten den Positionen etwas Zufälliges hinzufügen, damit die Partikel nicht alle an einem einzigen Punkt erscheinen. FromPolar ()
kehrt zurück Vector2
mit einer bestimmten Richtung und Länge.
Hier ist das Ergebnis:
Unsere Spritzer sehen recht anständig aus und einige großartige Spiele wie World of Goo haben Spritzer mit Partikeleffekten, die denen unseres ähneln. Ich werde Ihnen jedoch eine Technik zeigen, mit der die Spritzer flüssiger wirken. Die Technik verwendet Metabälle, organisch wirkende Blobs, über die ich zuvor ein Tutorial geschrieben habe. Wenn Sie an den Details zu Metaballs und ihrer Funktionsweise interessiert sind, lesen Sie dieses Tutorial. Wenn Sie nur wissen möchten, wie Sie sie auf unsere Spritzer auftragen, lesen Sie weiter.
Metaballs sehen in der Art, wie sie miteinander verschmelzen, flüssigkeitsähnlich aus, sodass sie gut zu unseren Flüssigkeitsspritzern passen. Um die Metaballs zu erstellen, müssen wir neue Klassenvariablen hinzufügen:
RenderTarget2D metaballTarget; AlphaTestEffect alphaTest;
Was wir so initialisieren:
var view = GraphicsDevice.Viewport; metaballTarget = new RenderTarget2D (GraphicsDevice, view.Width, view.Height); alphaTest = new AlphaTestEffect (GraphicsDevice); alphaTest.ReferenceAlpha = 175; alphaTest.Projection = Matrix.CreateTranslation (-0.5f, -0.5f, 0) * Matrix.CreateOrthographicOffCenter (0, view.Width, view.Height, 0, 0, 1);
Dann zeichnen wir die Metabälle:
GraphicsDevice.SetRenderTarget (metaballTarget); GraphicsDevice.Clear (Color.Transparent); Color lightBlue = neue Farbe (0,2f, 0,5f, 1f); spriteBatch.Begin (0, BlendState.Additive); foreach (var-Partikel in Teilchen) Vektor2-Ursprung = neuer Vektor2 (ParticleImage.Width, ParticleImage.Height) / 2f; spriteBatch.Draw (ParticleImage, Partikel.Position, null, lightBlue, Partikel.Orientation, Ursprung, 2f, 0, 0); spriteBatch.End (); GraphicsDevice.SetRenderTarget (null); device.Clear (Color.CornflowerBlue); spriteBatch.Begin (0, null, null, null, null, alphaTest); spriteBatch.Draw (metaballTarget, Vector2.Zero, Color.White); spriteBatch.End (); // Zeichne Wellen und andere Dinge
Der Metaball-Effekt hängt von einer Partikelstruktur ab, die mit zunehmender Entfernung vom Zentrum ausgeblendet wird. Ich habe es auf einen schwarzen Hintergrund gesetzt, um ihn sichtbar zu machen:
So sieht es aus:
Die Wassertröpfchen verschmelzen jetzt, wenn sie nahe sind. Sie verschmelzen jedoch nicht mit der Wasseroberfläche. Wir können dies beheben, indem wir der Wasseroberfläche einen Farbverlauf hinzufügen, der das Wasser allmählich ausblendet, und es unserem Metaball-Renderziel zur Verfügung stellen.
Fügen Sie der obigen Methode vor der Zeile den folgenden Code hinzu GraphicsDevice.SetRendertarget (null)
:
primitiveBatch.Begin (PrimitiveType.TriangleList); const Schwimmerdicke = 20; Float Scale = GraphicsDevice.Viewport.Width / (Federn.Länge - 1f); für (int i = 1; i < springs.Length; i++) Vector2 p1 = new Vector2((i - 1) * scale, springs[i - 1].Height); Vector2 p2 = new Vector2(i * scale, springs[i].Height); Vector2 p3 = new Vector2(p1.X, p1.Y - thickness); Vector2 p4 = new Vector2(p2.X, p2.Y - thickness); primitiveBatch.AddVertex(p2, lightBlue); primitiveBatch.AddVertex(p1, lightBlue); primitiveBatch.AddVertex(p3, Color.Transparent); primitiveBatch.AddVertex(p3, Color.Transparent); primitiveBatch.AddVertex(p4, Color.Transparent); primitiveBatch.AddVertex(p2, lightBlue); primitiveBatch.End();
Jetzt verschmelzen die Partikel mit der Wasseroberfläche.
Die Wasserpartikel sehen ein bisschen flach aus und es wäre schön, ihnen etwas Schatten zu geben. Im Idealfall würden Sie dies in einem Shader tun. Um dieses Tutorial jedoch einfach zu halten, verwenden wir einen schnellen und einfachen Trick: Wir zeichnen die Partikel einfach dreimal mit unterschiedlichen Tönungen und Versätzen, wie in der folgenden Abbildung dargestellt.
Dazu möchten wir die Metaball-Partikel in einem neuen Renderziel erfassen. Dieses Renderziel wird dann für jeden Farbton einmalig gezeichnet.
Zuerst deklarieren Sie ein neues RenderTarget2D
genau wie bei den Metabällen:
iclesTarget = new RenderTarget2D (GraphicsDevice, view.Width, view.Height);
Dann statt zu zeichnen metaballsTarget
direkt auf den Backbuffer wollen wir ihn zeichnen PartikelnZiel
. Gehen Sie dazu zu der Methode, in der wir die Metabälle zeichnen, und ändern Sie einfach diese Zeilen:
GraphicsDevice.SetRenderTarget (null); device.Clear (Color.CornflowerBlue);
… An:
GraphicsDevice.SetRenderTarget (PartikelTarget); device.Clear (Color.Transparent);
Verwenden Sie dann den folgenden Code, um die Partikel dreimal mit unterschiedlichen Farbtönen und Versätzen zu zeichnen:
Color lightBlue = neue Farbe (0,2f, 0,5f, 1f); GraphicsDevice.SetRenderTarget (null); device.Clear (Color.CornflowerBlue); spriteBatch.Begin (); spriteBatch.Draw (PartikelTarget, -Vector2.One, neue Farbe (0.8f, 0.8f, 1f)); spriteBatch.Draw (particleTarget, Vector2.One, neue Farbe (0f, 0f, 0.2f)); spriteBatch.Draw (PartikelTarget, Vector2.Zero, lightBlue); spriteBatch.End (); // Zeichne Wellen und anderes Zeug
Das war es für einfaches 2D-Wasser. Für die Demo habe ich einen Stein hinzugefügt, den Sie ins Wasser werfen können. Ich zeichne das Wasser mit etwas Transparenz auf dem Felsen, damit es wie unter Wasser aussieht, und verlangsamt es, wenn es unter Wasser ist, da es widerstandsfähig gegen Wasser ist.
Um die Demo ein bisschen schöner aussehen zu lassen, ging ich zu opengameart.org und fand ein Bild für den Felsen und einen Himmelshintergrund. Sie finden den Felsen und den Himmel unter http://opengameart.org/content/rocks und opengameart.org/content/sky-backdrop.