Dieser Artikel hat zu viel Wasser.

„Wir fangen an, ein neues Spiel zu entwickeln, und wir brauchen kühles Wasser. Schaffst du das? "


- fragte mich. „Ja, keine Frage! Natürlich kann ich das “, antwortete ich, aber meine Stimme zitterte tückisch. "Und auch über die Einheit?" - und mir wurde klar, dass noch viel Arbeit vor uns liegt.

Also etwas Wasser. Bis zu diesem Moment hatte ich Unity nicht genau wie C # gesehen, also beschloss ich, einen Prototyp für die Tools zu erstellen, die ich kannte: C ++ und DX9. Was ich damals wusste und in der Praxis üben konnte, waren die Bildlauftexturen von Normalen zur Bildung der Oberfläche und die darauf basierende primitive Verschiebungsabbildung. Sofort war es notwendig, absolut alles zu ändern. Realistische animierte Form der Wasseroberfläche. Komplizierte (starke) Schattierung. Schaumbildung. An die Kamera angeschlossenes LOD-System. Ich begann im Internet nach Informationen zu suchen, wie man das alles macht.

Der erste Punkt war natürlich das Verständnis von Jerry Tessendorfs simulierendem Meerwasser .

Akademische Pager mit einer Reihe von abstrusen Formeln wurden mir nie viel gegeben, so dass ich nach ein paar Lesungen wenig verstand. Die allgemeinen Prinzipien waren klar: Jeder Rahmen wird durch eine Höhenkarte unter Verwendung der schnellen Fourier-Transformation erzeugt, die in Abhängigkeit von der Zeit ihre Form sanft ändert, um eine realistische Wasseroberfläche zu bilden. Aber wie und was ich zählen sollte, wusste ich nicht. Ich vertiefte mich langsam in die Weisheit, FFT auf Shadern in D3D9 zu berechnen, und der Quellcode mit einem Artikel irgendwo in der Wildnis des Internets, den ich eine Stunde lang zu finden versuchte, aber ohne Erfolg (leider) wirklich half. Das erste Ergebnis wurde erzielt (beängstigend wie ein Atomkrieg):


Die ersten Erfolge waren erfreulich, und der Transfer von Wasser zu Unity begann mit seiner Fertigstellung.

Im Spiel über Seeschlachten wurden verschiedene Anforderungen an Wasser gestellt:

  • Realistischer Look. Schön wie nahe und entfernte Verkürzungen, dynamischer Schaum, Streuung usw.
  • Unterstützung für verschiedene Wetterbedingungen: Ruhe, Sturm und Zwischenbedingungen. Änderung der Tageszeit.
  • Physik des Auftriebs von Schiffen auf einer simulierten Oberfläche, schwimmende Objekte.
  • Da das Spiel im Mehrspielermodus gespielt wird, sollte das Wasser für alle Teilnehmer des Kampfes gleich sein.
  • Oberflächenzeichnung: Gezeichnete Bereiche des Fluges der Volley-Kerne, Schaum vom Eindringen von Kernen ins Wasser.

Geometrie


Es wurde beschlossen, eine quadtree-ähnliche Struktur mit einem Zentrum um die Kamera herum zu bauen, die diskret wieder aufgebaut wird, wenn sich der Betrachter bewegt. Warum diskret? Wenn Sie das Netz reibungslos mit der Kamera bewegen oder die Neuprojektion des Bildschirmbereichs wie im Artikel Echtzeit-Wasserwiedergabe verwenden - Einführung des projizierten Gitterkonzepts - , springen die Polygone in den Langzeitplänen aufgrund der unzureichenden Auflösung des geometrischen Netzes nach oben und runter. Das ist sehr auffällig. Das Bild kräuselt sich. Um dies zu überwinden, muss entweder die Auflösung des Wassernetzpolygons stark erhöht oder die Geometrie über große Entfernungen „abgeflacht“ werden oder die Polygone so aufgebaut und verschoben werden, dass diese Verschiebungen nicht sichtbar sind. Unser Wasser ist progressiv (hehe) und ich habe den dritten Weg gewählt. Wie bei jeder ähnlichen Technik (die allen bekannt ist, die in Spielen Terrain erstellt haben), müssen Sie T-Übergänge an den Grenzen von Übergängen von Detailebenen entfernen. Um dieses Problem zu lösen, werden zu Beginn 3 Arten von Quads mit vorgegebenen Tessellierungsparametern berechnet:



Der erste Typ ist für Quads gedacht, bei denen es sich nicht um Übergänge zu niedrigeren Details handelt. Keine Seite hat eine zweifach reduzierte Anzahl von Eckpunkten. Der zweite Typ ist für Grenz-, aber nicht für eckige Quads. Der dritte Typ sind eckige Grenzquads. Das endgültige Wassernetz wird durch Drehen und Skalieren dieser drei Arten von Netzen konstruiert.

So sieht ein Render mit einer anderen Farbe des LOD-Wasserstandes aus.


Die ersten Bilder zeigen die Verbindung zweier verschiedener Detailebenen.

Video als Frame ist mit Wasserquads gefüllt:


Ich möchte Sie daran erinnern, dass alles schon lange her ist (und nicht wahr ist). Jetzt können Sie optimaler und flexibler direkt auf der GPU arbeiten (GPU Pro 5. Quadtrees auf der GPU). Und es wird in einem Draw Call gezeichnet, und Tessellation kann das Detail erhöhen.

Später wechselte das Projekt zu D3D11, aber die Hände erreichten nicht die Aufrüstung dieses Teils des Ozeanputzes.

Wellenformgenerierung


Dafür brauchen wir eine schnelle Fourier-Transformation. Für die ausgewählte (notwendige) Auflösung der Wellentextur (nennen wir es zunächst, ich erkläre, welche Daten dort gespeichert sind) bereiten wir die Anfangsdaten anhand der von den Künstlern festgelegten Parameter vor (Kraft, Windrichtung, Wellenabhängigkeit von der Windrichtung und andere). All dies muss in die sogenannten Formeln eingespeist werden. Phillips-Spektrum Wir modifizieren die für jeden Frame erhaltenen Anfangsdaten unter Berücksichtigung der Zeit und führen eine FFT für sie durch. Am Ausgang erhalten wir eine Texturkachelung in alle Richtungen, die den Versatz der Eckpunkte des Flachnetzes enthält. Warum nicht einfach eine Höhenkarte? Wenn Sie nur den Höhenversatz speichern, entsteht eine unrealistische "sprudelnde" Masse, die nur aus der Ferne dem Meer ähnelt:


Wenn wir die Verschiebungen für alle drei Koordinaten berücksichtigen, werden schöne „scharfe“ realistische Wellen erzeugt:


Eine animierte Textur reicht nicht aus. Kacheln sind sichtbar, nicht genügend Details in naher Zukunft. Wir nehmen den beschriebenen Algorithmus und erstellen nicht eine, sondern 3 fft-generierte Texturen. Das erste sind große Wellen. Es legt die Grundwellenform fest und wird für die Physik verwendet. Das zweite sind mittlere Wellen. Und schließlich die kleinste. 3 FFT-Generatoren (4. Option ist die endgültige Mischung):


Die Parameter der Ebenen werden unabhängig voneinander eingestellt und die resultierenden Texturen werden im Wasser-Shader in die endgültige Wellenform gemischt. Parallel zu den Offsets werden auch normale Karten jeder Schicht erzeugt.

Die "Gleichmäßigkeit" des Wassers für alle Schlachtteilnehmer wird durch die Synchronisation der Ozeanparameter zu Beginn des Kampfes sichergestellt. Diese Informationen werden vom Server an jeden Client übertragen.

Modell des physischen Auftriebs


Da musste nicht nur ein schönes Bild gemacht werden, sondern auch das realistische Verhalten der Schiffe. Unter Berücksichtigung der Tatsache, dass ein stürmisches Meer (große Wellen) im Spiel vorhanden sein sollte, bestand eine weitere Aufgabe, die gelöst werden musste, darin, den Auftrieb von Objekten auf der Oberfläche des erzeugten Meeres sicherzustellen. Zuerst habe ich versucht, die GPU auf die Textur der Welle zurückzulesen. Da jedoch schnell klar wurde, dass die gesamte Physik des Seekampfes auf dem Server ausgeführt werden muss, muss das Meer oder vielmehr die erste Schicht, die die Wellenform festlegt, auch auf dem Server gelesen werden (und höchstwahrscheinlich gibt es kein Fasten und / oder kompatible GPU) wurde beschlossen, eine voll funktionsfähige Kopie des GPU-FFT-Generators in Form eines nativen C ++ - Plug-Ins für Unity auf die CPU zu schreiben. Ich habe den FFT-Algorithmus nicht selbst implementiert und ihn in der Intel Performance Primitives (IPP) -Bibliothek verwendet. Die gesamte Bindung und Nachbearbeitung der Ergebnisse wurde jedoch von mir durchgeführt, gefolgt von der Optimierung der SSE und der Parallelisierung durch Threads. Dies beinhaltete die Vorbereitung des Datenarrays für die FFT für jeden Frame und die endgültige Konvertierung der berechneten Werte in eine Wellenversatzkarte.

Es gab ein weiteres interessantes Merkmal des Algorithmus, das auf den Anforderungen der Wasserphysik basierte. Was benötigt wurde, war eine Funktion, um an einem bestimmten Punkt der Welt schnell Wellenhöhen zu erhalten. Dies ist logisch, da dies die Grundlage für den Auftrieb eines Objekts ist. Da wir jedoch am Ausgang des FFT-Prozessors eine Offset-Karte und keine Höhenkarte erhalten, hat uns die übliche Auswahl aus der Textur bei Bedarf nicht die Wellenhöhe gegeben. Betrachten Sie der Einfachheit halber die 2D-Option:



Um eine Welle zu bilden, enthalten Texel (durch vertikale Linien dargestellte Texturelemente) einen Vektor (Pfeile), der den Versatz des Scheitelpunkts des flachen Netzes (blaue Punkte) in Richtung seiner Endposition (Pfeilspitze) festlegt. Angenommen, wir nehmen diese Daten und versuchen, die Höhe des Wassers an der für uns interessanten Stelle daraus zu extrahieren. Zum Beispiel müssen wir die Höhe bei hB kennen. Wenn wir den Texelvektor tB nehmen, erhalten wir einen Versatz zu einem Punkt in der Nähe von hC, der sich sehr von dem unterscheiden kann, was wir brauchen. Es gibt zwei Möglichkeiten, um dieses Problem zu lösen: Überprüfen Sie bei jeder Höhenanforderung den Satz benachbarter Texel, bis wir eines finden, das einen Versatz zu der für uns interessanten Position aufweist. In unserem Beispiel finden wir, dass Texel tA den nächsten Versatz enthält. Dieser Ansatz kann jedoch nicht schnell genannt werden. Das Scannen des Texelradius ist unklar, welche Größe (und ob das stürmische Meer oder die ruhigen Verschiebungen stark variieren können) lange dauern kann.

Die zweite Option: Konvertieren Sie die versetzte Karte nach der Berechnung mithilfe des Streuungsansatzes in eine Höhenkarte. Dies bedeutet, dass wir für jeden Versatzvektor die Höhe der Welle schreiben, die er auf den Punkt setzt, an dem er verschoben wird. Dies ist ein separates Datenarray, mit dem die Höhe am interessierenden Punkt ermittelt wird. Unter Verwendung unserer Darstellung enthält die Zelle tB die Höhe hB, die aus dem Vektor tA → hB erhalten wird. Es gibt noch eine weitere Funktion. Die Zelle tA enthält keinen gültigen Wert, da sich kein Vektor darin bewegt. Um solche "Löcher" zu füllen, wird ein Durchgang gemacht, um sie mit benachbarten Werten zu füllen.

So sieht es aus, wenn Sie die Verschiebungen mithilfe von Vektoren (rot - großer Versatz, grün - klein) visualisieren:


Der Rest ist einfach. Die Ebene der bedingten Wasserlinie ist für das Schiff festgelegt. Darauf wird ein rechteckiges Gitter von Probenpunkten bestimmt, das die Orte der Anwendung von Kräften definiert, die für das Schiff aus dem Wasser drücken. Dann prüfen wir für jeden Punkt anhand der oben beschriebenen Wasserhöhenkarte, ob er sich unter Wasser befindet oder nicht. Wenn sich der Punkt unter Wasser befindet, üben Sie an diesem Punkt die vertikale Kraft bis zum physischen Rumpf des Körpers aus, skaliert durch den Abstand vom Punkt zur Wasseroberfläche. Wenn wir über Wasser nichts tun, wird die Schwerkraft alles für uns tun. Tatsächlich sind die Formeln dort etwas komplizierter (alles zur Feinabstimmung des Schiffsverhaltens), aber das Grundprinzip ist dies. In dem Video zur Auftriebsvisualisierung unten sind die blauen Würfel die Positionen der Proben, und die Linien von ihnen nach unten sind die Größe der Kraft, die aus dem Wasser herausgedrückt wird.


Bei der Implementierung des Servers gibt es einen weiteren interessanten Optimierungspunkt. Es ist nicht erforderlich, unterschiedliches Wasser für unterschiedliche Kampfinstanzen zu simulieren, wenn diese unter denselben Wetterbedingungen (denselben FFT-Simulatorparametern) passieren. Die logische Entscheidung war daher, einen Pool von Simulatoren zu erstellen, für die Kampfeinheiten Anforderungen zur Gewinnung von simuliertem Wasser mit den angegebenen Parametern erfüllen. Wenn die Parameter in mehreren Fällen gleich sind, kehrt dasselbe Wasser zu ihnen zurück. Dies wird mithilfe der Memor Mapped File API implementiert. Wenn der FFT-Simulator erstellt wird, erhält er Zugriff auf seine Daten, indem Deskriptoren der erforderlichen Blöcke exportiert werden. Anstatt einen echten Simulator zu starten, startet die Serverinstanz einen „Dummy“, der einfach Daten preisgibt, die von diesen Deskriptoren geöffnet wurden. Es gab einige lustige Fehler im Zusammenhang mit dieser Funktionalität. Aufgrund von Referenzzählfehlern wurde der Simulator zerstört, aber die Speicherzuordnungsdatei ist aktiv, während mindestens ein Handle dazu geöffnet ist. Die Daten wurden nicht mehr aktualisiert (es gibt keinen Simulator) und das Wasser wurde gestoppt.

Auf der Client-Seite benötigen wir Informationen über die Wellenform, um das Eindringen von Kernen in die Welle zu berechnen und Partikel- und Schaumsysteme zu spielen. Der Schaden wird auf dem Server berechnet und dort muss auch korrekt festgestellt werden, ob der Kern ins Wasser gelangt ist (die Welle kann das Schiff schließen, insbesondere bei Stürmen). Hier ist es bereits erforderlich, die Höhenkartenverfolgung analog durchzuführen, wie dies bei Parallaxen-Mapping- oder SSAO-Effekten der Fall ist.

Schattierung


Im Prinzip wie anderswo. Reflexionen, Brechungen und Streuung unter der Oberfläche werden geschickt geknetet, wobei die Tiefe des Bodens berücksichtigt wird, der Fresneleffekt berücksichtigt wird und der Spiegel betrachtet wird. Wir betrachten die Streuung von Graten in Abhängigkeit vom Sonnenstand. Schaum wird wie folgt erzeugt: Erstellen Sie einen „Schaumfleck“ auf den Wellenbergen (verwenden Sie die Höhe als Metrik), und wenden Sie dann neu erstellte Flecken auf die Flecken aus vorherigen Bildern an, während Sie deren Intensität verringern. So erhalten wir ein Verschmieren von Schaumflecken in Form eines Schwanzes von einem Laufwellenkamm.


Wir verwenden die erhaltene "Flecken" -Textur als Maske, mit der wir die Texturen von Blasen, Flecken usw. mischen. Wir erhalten ein ziemlich realistisches dynamisches Schaummuster auf der Oberfläche der Wellen. Diese Maske wird für jede FFT-Ebene erstellt (ich erinnere Sie, wir haben 3 davon), und im endgültigen Mix mischen sich alle.

Das obige Video zeigt eine Schaummaske. Die erste und zweite Schicht. Ich ändere die Parameter des Generators und das Ergebnis ist auf der Textur sichtbar.

Und ein Video von einem etwas ungeschickten stürmischen Meer. Hier können Sie die Wellenform, die Generatorfähigkeiten und den Schaum deutlich sehen:


Wasserentnahme


Verwendungsbild:



Verwendet für:

  • Marker, Visualisierung der Expansionszone der Kerne.
  • Schaum an der Stelle ziehen, an der die Kerne auf das Wasser treffen.
  • Schaumiges Kielwasser des Schiffes
  • Drücken Sie Wasser unter dem Schiff heraus, um die Auswirkungen der Wellen, die das Deck und den überfluteten Laderaum überfluten, zu beseitigen.

Der offensichtliche Basisfall ist die projektive Texturierung. Es wurde umgesetzt. Es gibt jedoch zusätzliche Anforderungen. In der Nähe befindliche Arten - Seife aufgrund unzureichender Auflösung (Sie können sie erhöhen, aber nicht unendlich), und ich möchte, dass diese projektiven Zeichnungen auf dem Wasser weit sichtbar sind. Wo ist das gleiche Problem gelöst? Das ist richtig, in Schatten (Schattenkarte). Wie ist sie dort gelöst? Richtig, kaskadierte (parallele geteilte) Schattenkarten. Wir werden diese Technologie auch in Betrieb nehmen und auf unsere Aufgabe anwenden. Wir zerlegen den Kamerastumpf in N (normalerweise 3-4) Teilstümpfe. Für jedes konstruieren wir ein beschreibendes Rechteck in der horizontalen Ebene. Für jedes solche Rechteck erstellen wir eine orthographische Projektionsmatrix und zeichnen alle interessierenden Objekte für jede von N solchen ortho-Kameras. Jede dieser Kameras zeichnet in eine separate Textur und kombiniert sie dann im Ocean Shader zu einem soliden projektiven Bild.


Also habe ich ein riesiges Flugzeug mit einer Fahnenstruktur auf das Meer gesetzt:



Folgendes enthalten Splits:



Zusätzlich zu den üblichen Bildern ist es notwendig, auf genau die gleiche Weise eine zusätzliche Schaummaske (für Spuren von Schiffen und Trefferstellen von Kernen) sowie eine Maske zum Auspressen von Wasser unter den Schiffen zu zeichnen. Das sind viele Kameras und viele Gänge. Zuerst funktionierte es so schnell, aber nach dem Wechsel zu D3D11, der Verwendung der „Ausbreitung“ der Geometrie im geometrischen Shader und dem Zeichnen jeder Kopie in ein separates Renderziel über SV_RenderTergetArrayIndex, konnte dieser Effekt erheblich beschleunigt werden.

Verbesserungen und Upgrades


D3D11 ist in vielen Momenten sehr freihändig. Nachdem ich zu Unity 5 gewechselt war, habe ich einen FFT-Generator für die Compute-Shader erstellt. Optisch hat sich nichts geändert, aber es ist etwas schneller geworden. Die Übersetzung der Fehlberechnung der Textur von Reflexionen von einem separaten vollwertigen Kamera-Rendering in die Screen Space Planar Reflections- Technologie führte zu einer guten Leistungssteigerung. Ich habe oben über die Optimierung von Wasseroberflächenobjekten geschrieben, aber meine Hände haben die Übertragung des Netzes auf die Quadtree-GPU nicht erreicht.

Vieles könnte möglicherweise optimaler und einfacher gemacht werden. Umzäunen Sie beispielsweise keine Gärten mit einem CPU-Simulator, sondern führen Sie einfach die GPU-Option auf einem Server mit einem WARP (Software) -D3D-Gerät aus. Die Datenfelder dort sind nicht sehr groß.

Nun, im Allgemeinen irgendwie. Zu der Zeit, als die Entwicklung begann, war alles modern und cool. Jetzt ist es an einigen Stellen bereits fehl am Platz. Es gibt mehr verfügbare Materialien, auch wenn es ein ähnliches Analogon zu Github gibt: Crest . Die meisten Spiele mit Meeren verfolgen einen ähnlichen Ansatz.

Source: https://habr.com/ru/post/de451604/


All Articles