GPU gebunden. Zweiter Teil Endloser Wald



In fast jedem Spiel ist es notwendig, die Spielebenen mit Objekten zu füllen, die visuellen Reichtum, Schönheit und Variabilität der virtuellen Welt schaffen. Nimm ein Spiel der offenen Welt. Dort sind Bäume, Gras, Erde und Wasser die wichtigsten „Platzhalter“ des Bildes. Heute wird es nur sehr wenige GPGPUs geben, aber ich werde versuchen, Ihnen zu erklären, wie Sie viele Bäume und Steine ​​in den Rahmen zeichnen können, wenn Sie es nicht können, aber wirklich wollen.

Es sollte sofort bemerkt werden, dass wir ein kleines Indie-Studio haben und wir oft nicht die Ressourcen haben, um jede Kleinigkeit zu zeichnen und zu modellieren. Daher muss für verschiedene Subsysteme ein „Überbau“ der vorgefertigten Funktionalität des Motors geschaffen werden. So war es im ersten Artikel des Zyklus über Animationen (dort haben wir das fertige Unity-Animationssystem verwendet und beschleunigt), also wird es hier sein. Dies vereinfacht die Einführung neuer Funktionen in das Spiel erheblich (weniger zu erlernen, weniger Fehler usw.).

Die Aufgabe: Sie müssen viel Wald zeichnen. Im Spiel haben wir eine Echtzeitstrategie (RTS) mit großen Ebenen (30x30 km), und diese legt die grundlegenden Anforderungen an das Rendering-System fest:

  • Mit Hilfe der Minikarte können wir sofort zu jedem Punkt auf dem Level wechseln. Und die Daten über die Objekte für die neue Position sollten bereit sein. In FPS- oder TPS-Spielen können wir uns nach einiger Zeit nicht mehr auf das Laden von Ressourcen verlassen.
  • Objekte auf so großen Ebenen benötigen eine wirklich große Anzahl. Hunderttausende, wenn nicht Millionen.
  • Auch hier machen es große Ebenen sehr lang und schwierig, "Wälder" manuell einzurichten. Die prozedurale Erzeugung von Wald, Steinen und Büschen ist notwendig, jedoch mit der Möglichkeit der manuellen Anpassung und Anordnung an wichtigen Stellen der Spielebene.

Wie kann dieses Problem gelöst werden? Solch eine Anzahl von gewöhnlich angeordneten Objekten der Einheit zieht immer noch nicht. Wir werden beim Keulen und Dosieren sterben. Das Rendern ist über die Instanzierung möglich. Es ist notwendig, ein Steuerungssystem zu schreiben. Bäume müssen modelliert werden. Es muss ein Baumanimationssystem erstellt werden. Ooh Ich will es schön und sofort. Es gibt SpeedTree, aber es gibt keine API für Animationen, die Draufsicht auf die Werbetafeln ist schrecklich, da es keine "horizontale Werbetafel" gibt und die Dokumentation schlecht ist. Aber wann hat uns das aufgehalten? Wir werden das SpeedTree-Rendering optimieren.

Rendern


Lassen Sie uns zunächst sehen, ob bei normalen Speedtree-Objekten alles so schlecht ist:



Hier stehen ca. 2.000 Bäume auf der Bühne. Beim Rendern ist alles in Ordnung, da instanziiert man die Bäume zu Batches, aber bei der CPU ist alles schlecht. Die Hälfte der Zeit, die die Kamera rendert, kühlt ab. Und wir brauchen Hunderttausende. Wir lehnen GameObjects definitiv ab, aber jetzt müssen wir die Struktur des SpeedTree-Modells aufdecken, den Mechanismus zum Wechseln von LODs und alles mit Griffen tun.

Der SpeedTree-Baum besteht aus mehreren LODs (in der Regel 4), von denen das letzte eine Werbetafel ist, und alle anderen sind Geometrien mit unterschiedlichem Detailgrad. Jedes von ihnen besteht aus mehreren Sabmeshs mit eigenem Material:


Dies ist nicht die Besonderheit von SpeedTree. Jede Struktur kann eine solche Struktur haben. Die LOD-Umschaltung wird in zwei verfügbaren Modi implementiert:

  1. Überblenden:

  2. Geschwindigkeitsbaum:


CrossFade (in Bezug auf Unity-Shader definiert durch das Präprozessor-Define LOD_FADE_CROSSFADE) ist die wichtigste LOD-Umschaltmethode für Szenenobjekte mit mehreren Detailebenen. Es besteht darin, dass bei einem LOD-Wechsel das Netz, das verschwinden soll, nicht nur verschwindet (der Qualitätssprung des Modells wird deutlich sichtbar), sondern sich auf dem Bildschirm durch Dithering „auflöst“. Ein einfacher Effekt, der die Verwendung echter Transparenz (Alpha-Blending) vermeidet. Das Modell, das genauso aussehen soll, wird auf dem Bildschirm "angezeigt".

SpeedTree (LOD_FADE_PERCENTAGE) wurde speziell für Bäume entwickelt. Zusätzlich zu den Hauptkoordinaten werden in der Geometrie der Blätter, Zweige und des Stammes die zusätzlichen Koordinaten der Position der Junior-Vertices in Bezug auf die aktuelle LOD-Ebene aufgezeichnet. Der Grad des Übergangs von einer Ebene zur anderen ist der Gewichtswert für die lineare Interpolation dieser beiden Positionen. Das Verschieben zur / von der Plakatwand erfolgt mit der CrossFade-Methode.

Im Prinzip ist dies alles, was Sie wissen müssen, um Ihr eigenes LOD-Vermittlungssystem zu implementieren. Das Rendern selbst ist einfach. Wir durchlaufen alle Arten von Bäumen, alle LODs und alle Sabmeshs jedes LODs. Wir installieren das entsprechende Material und zeichnen alle Instanzen dieses Objekts mithilfe von Instanzen auf einen Schlag. Somit entspricht die Anzahl der DrawCalls der Anzahl der eindeutigen Objekte in der Szene. Woher wissen wir, was wir zeichnen sollen? Das wird uns helfen

Forest Generator


Die Landung selbst ist einfach und unprätentiös. Für jede Baumart teilen wir die Welt in Quadrate, so dass jeder Baum auf einen Baum passt. Wir gehen alle Quads durch und überprüfen die Maske des Formulars:



Kann man hier an einem bestimmten Punkt im Level einen Baum pflanzen? Die Maske mit "bewaldeten" Stellen wird vom Leveldesigner gezeichnet. Zuerst war das Ganze auf der CPU und C #. Der Generator arbeitete langsam und die Größe der Ebenen wuchs, so dass das Warten auf die Regeneration für einige zehn Minuten anstrengend wurde. Es wurde beschlossen, den Generator auf die GPU und den Compute-Shader zu übertragen. Auch hier ist alles einfach. Wir brauchen die Höhenkarte des Landes, die Baumpflanzmaske und AppendStructuredBuffer, wo wir die generierten Bäume hinzufügen (Position und ID, das sind alle Daten).

Ein spezielles Skript, das von Hand an wichtigen Stellen mit Bäumen angeordnet wird, fügt sich in übliche Arrays ein und entfernt das ursprüngliche Objekt aus der Szene.

Culling & LOD-Umschaltung


Die Position und Art des Baums zu kennen, reicht nicht aus, um ein effektives Rendering zu erzielen. Für jeden Frame muss festgelegt werden, welche Objekte sichtbar sind und welche LOD (unter Berücksichtigung der Übergangslogik) an das Rendering gesendet werden sollen.

Ein spezieller Compute-Shader erledigt dies ebenfalls. Für jedes Objekt wird zuerst ein Frustum-Culling durchgeführt:


Ist das Objekt sichtbar, wird die LOD-Schaltlogik ausgeführt. Je nach Bildschirmgröße bestimmen wir den gewünschten LOD-Level. Wenn der CrossFade-Modus für die LOD der Gruppe eingestellt ist, erhöhen wir die Übergangszeit für das Dithering. Bei SpeedTree Percentage wird der normalisierte Übergangswert zwischen LODs berücksichtigt.

Moderne grafische APIs verfügen über hervorragende Funktionen, mit denen Zeichnungsübermittlungsinformationen an den Zeichnungsaufruf im Rechenpuffer übergeben werden können (z. B. ID3D11DeviceContext :: DrawIndexedInstancedIndirect für D3D11). Dies bedeutet, dass Sie diesen Rechenpuffer auch auf der GPU füllen können. Somit stellt sich heraus, dass es ein vollständig CPU-unabhängiges System ist (rufen Sie einfach Graphics.DrawMeshInstancedIndirect auf). In unserem Fall ist es nur notwendig, die Anzahl der Instanzen jedes Submeshs aufzuzeichnen. Die restlichen Informationen (die Anzahl der Indizes im Netz und die Offsets) sind statisch.

Der Rechenpuffer mit Argumenten für den Aufruf von draw ist in Abschnitte unterteilt, von denen jeder für den Aufruf des Renderings seines Submeshs verantwortlich ist. Erhöhen Sie im Compute-Shader für das im aktuellen Frame zu zeichnende Netz den entsprechenden InstanceCount-Wert.

So sieht es im Render aus:


GPU-Okklusions-Culling ist ein offensichtlicher nächster Schritt, aber für RTS mit einer solchen Kamera und nicht sehr großen Hügeln sind die Gewinne nicht so offensichtlich (und hier ist es für die Interessierten). Ich habe es noch nicht getan.

Damit alles korrekt gezeichnet wird, müssen Sie die SpeedTree-Shader etwas anpassen, um die Position und die Werte für die Übergänge zwischen den LODs aus den entsprechenden Rechenpuffern zu übernehmen.

Jetzt zeichnen wir schöne, aber statische Bäume. Und die SpeedTree-Bäume werden vom Wind realistisch beeinflusst und animiert. Die gesamte Logik solcher Animationen befindet sich in der Datei SpeedTreeWind.cginc, es gibt jedoch keine Dokumentation oder keinen Zugriff auf interne Parameter von Unity.

CBUFFER_START(SpeedTreeWind) float4 _ST_WindVector; float4 _ST_WindGlobal; float4 _ST_WindBranch; float4 _ST_WindBranchTwitch; float4 _ST_WindBranchWhip; float4 _ST_WindBranchAnchor; float4 _ST_WindBranchAdherences; float4 _ST_WindTurbulences; float4 _ST_WindLeaf1Ripple; float4 _ST_WindLeaf1Tumble; float4 _ST_WindLeaf1Twitch; float4 _ST_WindLeaf2Ripple; float4 _ST_WindLeaf2Tumble; float4 _ST_WindLeaf2Twitch; float4 _ST_WindFrondRipple; float4 _ST_WindAnimation; CBUFFER_END 

Wie würden wir sie auswählen? Zu diesem Zweck rendern wir für jeden Baumtyp das ursprüngliche SpeedTree-Objekt an einer unsichtbaren Stelle (bzw. in Unity sichtbar, in der Kamera jedoch nicht sichtbar, da sonst die Parameter nicht aktualisiert werden). Dies kann erreicht werden, indem der Begrenzungsrahmen stark vergrößert wird und das Objekt hinter der Kamera platziert wird. Jeder Frame wird mit material.GetVector (...) vom gewünschten Wertesatz befreit.

Die Bäume flattern also im Wind, aber die Draufsicht auf die Werbetafeln ist bedrückend:


Mit der Shader-Option BILLBOARD_FACE_CAMERA_POS noch schlimmer:


Wir brauchen horizontale Werbetafeln (von oben nach unten). Dies ist ein Standardfeature von SpeedTree seit King Pea, aber nach den Foren ist es immer noch nicht in Unity implementiert. Beitrag aus dem offiziellen SpeedTree- Forum : "Die Unity-Integration hat nie die horizontale Werbetafel verwendet." Wir werden unsere Hände befestigen. Die Geometrie selbst ist einfach herzustellen. Wie finde ich die UV-Koordinaten eines Sprites in einem Atlas für sie heraus?


Wir bekommen das alte SpeedTreeRT SDK und finden die Struktur in der Dokumentation:

 struct SBillboard { bool m_bIsActive; const float* m_pTexCoords; const float* m_pCoords; float m_fAlphaTestValue; }; 

"M_pTexCoords zeigt auf einen Satz von 4 (s, t) Texturkoordinaten, die die auf der Werbetafel verwendeten Bilder definieren. m_pTexCoords enthält 8 Einträge. “, heißt es in einer Fremdsprache. Nun, wir werden nach einer Folge von 4 Gleitkommawerten in einer binären SPM-Datei suchen, von denen jeder im Bereich [0..1] liegt. Durch die Methode des wissenschaftlichen Stocherns stellen wir fest, dass sich die gewünschte Sequenz vor einem Block von 12 Float-Zeichen befindet, die dem Muster entsprechen:

 float signs[] = { -1, 1, -1, 1, 1, -1, 1, 1, 1, -1, 1, 1 }; 

Wir schreiben ein kleines Konsolendienstprogramm für die Profis, das alle SPM-Dateien durchläuft und nach UV-Koordinaten für horizontale Werbetafeln sucht. Die Ausgabe ist ein solches CSV-Label:

 Azalea_Desktop.spm: 0, 1, 0.333333, 1, 0.333333, 0.666667, 0, 0.666667, Azalea_Desktop_Flowers_1.spm: 0, 1, 0.333333, 1, 0.333333, 0.666667, 0, 0.666667, Azalea_Desktop_Flowers_2.spm: 0, 1, 0.333333, 1, 0.333333, 0.666667, 0, 0.666667, Leaf_Map_Maker_Desktop_1_Modeler_Use_Only.spm: Pattern not found! Leaf_Map_Maker_Desktop_2_Modeler_Use_Only.spm: Pattern not found! BarrelCactus_Cluster_Desktop_1.spm: 0, 0.592376, 0.407624, 0.592376, 0.407624, 0.184752, 0, 0.184752, BarrelCactus_Cluster_Desktop_2.spm: 0, 1, 0.499988, 1, 0.499988, 0.500012, 0, 0.500012, BarrelCactus_Desktop_1.spm: 0, 0.2208, 0.220748, 0.2208, 0.220748, 5.29885e-05, 0, 5.29885e-05, BarrelCactus_Desktop_2.spm: 0, 1, 0.301392, 1, 0.301392, 0.698608, 0, 0.698608, 

Um der Geometrie der horizontalen Werbetafel Texturkoordinaten zuzuweisen, suchen wir den gewünschten Datensatz und analysieren ihn.

Nun ist es so:


Immer noch nicht sehr. Unter Verwendung der Alpha-Testschwelle wird die vertikale Werbetafel in den Aufnahmen vom Winkel zur Kamera ausgeblendet:



Zusammenfassung

Profiler mit dynamischer (wie viele Dinge werden gerendert) und statischer (wie viele Objekte und ihre Parameter befinden sich in der Szene) Statistik:


Nun, das letzte schöne Video (die zweite Hälfte zeigt das Umschalten der Qualitätsstufen):


Was wir am Ende haben:

  • Das System ist vollständig CPU-unabhängig.
  • Das geht schnell.
  • Es werden fertige SpeedTree-Assets verwendet, die Sie im Internet kaufen können.
  • Natürlich habe ich sie mit jeder LODGroup befreundet, nicht nur mit SpeedTree. So viele Kieselsteine ​​sind jetzt auch möglich.

Zu den Unzulänglichkeiten zählt das Fehlen von Okklusions-Culling und die immer noch wenig aussagekräftigen Werbetafeln.

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


All Articles