Wie Nanite zu Fortnite Battle Royale, Kapitel 4, kommt

26. Januar 2023
Hallo, ich bin Graham Wihlidal, Engineering Fellow (Graphics) bei Epic Games. Ich will heute einige der aufregenden Funktionen und Verbesserungen präsentieren, die wir dieses Jahr entwickelt haben, um Nanite im 4. Kapitel von Fortnite Battle Royale zu veröffentlichen. Diese Funktionen können Sie in der Unreal Engine 5.1 (als Beta) ausprobieren.

Unreal Engine 5.0 wurde mit Nanite in produktionsbereitem Zustand veröffentlicht. Man konnte damit schon eine Menge toller Sachen anstellen, aber die erste Version unterstützte noch nicht all die großartigen Funktionen, die für Nicht-Nanite-Meshes verfügbar sind. Stattdessen hatten wir uns darauf konzentriert, die Kernfunktionen von Nanite zu perfektionieren.

In Zukunft möchten wir die Unterstützung in viele weitere Bereiche ausweiten, die Nanite bis jetzt noch nicht abdeckte. Nutzer haben unser Augenmerk verstärkt auf Funktionen wie World Position Offset, Pixel Depth Offset, benutzerdefinierte UVs, doppelseitige Materialien und maskierte Materialien gelenkt. Zu Beginn des Jahres stellte sich das Team der Herausforderung, Unterstützung für einige dieser Funktionen umzusetzen, wobei wir eine Reihe nicht ganz einfacher Probleme lösen mussten.

GPUs fingen mit einer sogenannten festen Funktions-Pipeline an, bei der die Art und Weise, auf die Geometrie transformiert wurde – und wie Tiefe und Farbe geschrieben wurden –, direkt in die Hardware eingebaut war und nur mit einem begrenzten Satz vorgegebener Funktionen konfiguriert werden konnte. Später wurde Hardware durch Shader-Code „programmierbar“, was in Sachen Grafik neue Möglichkeiten eröffnete und Funktionen ermöglichte, die mit einer festen Funktions-Pipeline schwierig oder gar nicht umzusetzen waren.

Von Anfang an unterstützte Nanite immer „programmierbare“ Material-Diagramm-Shader, die die Ausgabefarben kontrollierten, aber der Rasterizer selbst, der festlegt, wie die Scheitelpunkte auf dem Bildschirm angeordnet sind und welche Pixel von den Dreiecken bedeckt werden, war von der Funktionsweise eine „feste Funktion“: Zwar war er als Shader-Code vorhanden, aber Inhaltsersteller hatten keinen Einfluss auf diese Logik.

Um die zuvor erwähnten Funktionen in Nanite unterstützen zu können, mussten wir den Rasterizer selbst programmierbar machen.

Erster Prototyp

Wir begannen mit einem Architekturprototypen, der für die Unterstützung von Material-Diagramm-Logik im Rasterizer nötig war und „Programmierbarer Rasterizer für Nanite“ genannt wurde.

Die folgenden Bilder zeigen den ersten Prototypen eines maskierten Materials, das über dem Mesh eines Nanite-Müllautos animiert wurde.
 
Der Prototyp hat in jedem Fall bewiesen, dass ein programmierbarer Rasterizer überhaupt umsetzbar ist, aber es wird noch einiges an Arbeit nötig sein, bis er produktionsbereit und effizient ist.

Wir konnte einige klare Ziele festlegen, die die Entwicklung dieser Arbeit vorantreiben:
  • Das Performance-Profil des bestehenden, schnellen „Feste Funktion“-Pfad behalten – die Einführung des programmierbaren Rasterizers sollte bestehende Inhalte nicht verlangsamen.
  • Sicherstellen, dass der programmierbare und der feste Rasterizer-Pfad weitgehend den gleichen Code-Pfad teilen, aus Gründen der Wartungsfreundlichkeit.
  • Es ist zu bedenken, dass der programmierbare Rasterizer von vielen Inhalten stark genutzt werden wird, also sollten einfache Berechnungen eine Performance liefern, die nur wenig langsamer als der Rasterizer mit fester Funktion ist.
  • Instanz- und Cluster-Culling sollten nur einmalig durchgeführt werden.
  • Die zusätzlichen Speicherkosten für GPU und Nanite sollten minimiert werden.
  • Alles sollte als Produktionsfunktion in UE 5.1 verfügbar sein.

Der erste Prototyp war darauf ausgelegt, nur ein einziges programmierbares Material in der Szene zu unterstützen, also war der nächste Schritt ein ordentlicher „Rasterizer-Behälter“-Vorgang, durch den wir echte Spielszenen aus Hunderten von Materialien unterstützen konnten, was uns dann ermöglichte, echte Inhalte zu testen.
Mittelalterspiel-Aufnahme fast komplett in Nanite umgewandelt!
Nanite-Dreieck-Visualisierung
Einzigartige „Rasterizer-Behälter“ (Materialien) in den Szenen
Auch wenn die Mittelalterspiel-Testszene funktionierte, war einiges an Optimierung und Arbeit an den Funktionen nötig, bevor der programmierbare Rasterizer für Nanite in einem Spiel lieferbereit war.

Es wurde sehr schnell klar, dass Nanite im 4. Kapitel von Fortnite Battle Royale Nanite gut gebraucht werden könnte, aber wir konnten eine vollumfängliche Unterstützung der nötigen Funktionen noch nicht gewährleisten. Sogar die scheinbar einfachen Meshes wie opake Gebäudeteile benötigten World Position Offset, um den beliebten „Wackel“-Effekt zu animieren, wenn sie Schaden erlitten. Fortnite verwendete den programmierbaren Rasterizer als Erstes.

Anwendungen in Fortnite

Animierte Props

Die erste Anwendung, die wir unterstützen wollten, waren einfache, opake Props mit statischem Mesh, die World Position Offset für sekundäre Animationen nutzten. Diese Props benötigen zwar Nanite nicht, um effizient gerendert werden zu können, aber Virtual Shadow Maps zeigten mit Nanite-Meshes wesentlich bessere Performance. Daher war es wichtig, so viele Teile der Szene wie möglich mit Nanite zu rendern.


 

 

Gebäude

Ein wichtiger Aspekt von Fortnite sind die verschiedenen Gebäude-Sets, und Nanite hat bereits bewiesen, dass große Städte kein Problem sind. Es sollte also niemanden überraschen, dass wir uns bei allen Gebäude-Meshes für Nanite entschieden haben – um visuelle Wiedergabetreue zu erhöhen, plötzliche Erhöhungen der Detailstufe zu verhindern und die Leistung zu verbessern.
Konstruktion
Es gibt in Fortnite bei Weitem zu viele Gebäude-Meshes, um sie auf allen Plattformen von Grund auf neu zu generieren, also erstellten wir einen Offline-Prozess, der mit Hilfe handgemachter Displacement-Texturen und -Regeln qualitativ hochwertige Nanite-Meshes mit sehr viel höherer Dreieckszahl als die traditionellen Versionen generieren kann.

Hinweis: Dies ist kein „Laufzeit“-Ersetzen, sondern ein eigens für Fortnite entwickelter Offline-Arbeitsablauf zum Erstellen von Displacement-Meshes.

Zusätzlich zu visuellen Verbesserungen beim ersten Rendering verbessern die verschobenen Nanite-Meshes auch enorm die Qualität der Virtual Shadow Maps, da geometrische Details wie Ziegel – zuvor als zweidimensionale gemalte Bereiche der Textur dargestellt – nun in den tatsächlichen dreidimensionalen Raum projiziert werden. Dies verleiht den Oberflächen mehr Tiefe und Detail, was ordentliche Eigenschatten und Silhouetten ermöglicht.
 
Die Gebäude in Fortnite sind durchweg opake statische Meshes und könnten komplett mit den Nanite-Funktionen in Unreal Engine 5.0 gerendert werden, ausgenommen dem „Wackel“-Effekt, der kurzzeitig auftritt, wenn ein Gebäudeteil Schaden erleidet (zum Beispiel wenn ein Spieler mit einer Spitzhacke darauf einschlägt.)

Dieser ist eine einfache Animationsspur, die durch World Position Offset im Material umgesetzt wird, und die Berechnung wird ständig ausgeführt, auch wenn das Wackeln nicht optisch dargestellt wird (in diesem Fall wird eine Null-Gewichtung verwendet).

Aufgrund der nicht unerheblichen Anzahl von Gebäude-Meshes in Fortnite implementierten wir eine Optimierung für dieses Muster, über die ein besonderer Modus aktiviert werden kann (r.OptimizedWPO). Unabhängig davon, ob ein Material eine mit World Position Offset zusammenhängende Logik hat, wird Nanite dann diese Logik nur berechnen, wenn für das jeweilige primitive Element „World Position Offset berechnen“ aktiv ist (dies ist die Standard-Einstellung).
Wenn Nanite den zuvor erwähnten „Rasterizer-Behälter“-Vorgang durchführt, werden sämtliche primitiven Elemente, die sonst den programmierbaren Pfad genommen hätten, bei denen jetzt aber „World Position Offset berechnen“ deaktiviert ist, auf den normalen Rasterizer-Pfad mit festen Funktionen gesetzt.

Diese Optimierung hat sich an mehreren Stellen als nützlich erwiesen (eingeschlossen der Deaktivierung von World Position Offset in der Ferne), war für das Wackeln von Gebäuden aber extrem wichtig. Wir deaktivierten „World Position Offset berechnen“ standardmäßig bei allen Gebäude-Meshes und passten den Spiel-Code in Fortnite an, um diesen Wert programmatisch daran festzulegen, ob ein Gebäude aktuell Schaden erleidet (und wackelt).
Neben dieser neuen Optimierung fügten wir eine Nanite-Debug-Ansicht für „WPO berechnen“ hinzu (r.Nanite.Visualize EvaluateWPO), die grün anzeigt, wenn ein Mesh aktuell World Position Offset berechnet und rot, wenn nicht.
Mit dieser Optimierung nehmen beinahe alle Gebäude-Meshes den Pfad der festen Funktion, abgesehen von einer Handvoll Meshes, die ab und zu den programmierbaren Wackel-Pfad nehmen, wenn das nötig ist.
Beispielbild
r.OptimizedWPO aus vs. an

Bäume

Der Bereich, in dem wir die meisten Prototypen und die meiste Entwicklungszeit versenkten, waren Bäume. In Kapitel 4 wollten wir üppige Wälder, also brauchen wir eine effiziente Lösung mit vorhersagbarer Performance. Die Bäume bauen auf völlig neuen Funktionen in Nanite auf, die wir noch nie in einem Titel veröffentlicht hatten. Also waren einige Prototypen und Optimierungen nötig, bis wir eine Herangehensweise gefunden hatten, die wir schließlich benutzten.
Konstruktion
In unseren ersten Experimenten verwendeten wir für die Bäume maskierte Materialien und Karten.
Bei Fortnite-Inhalten fanden wir heraus, dass es oft schneller war, maskierte Materialien zu vermeiden und uns stattdessen auf höhere Dreieckszahlen im Mesh zu verlassen und die Materialien opak zu lassen, gerade bei Bäumen und Gras. Der Grund dafür ist hauptsächlich, dass maskierte Materialien in Nanite recht hohe Rechenkosten beim Base Pass Shading verursachen, da wir die baryzentrischen Koordinaten eines Dreiecks pro Pixel berechnen müssen, und negativer Raum in den Alpha-Maps erhöht den Overdraw.

Bewahrungsbereich

Nachdem wir die Fortnite-Bäume zu Nanite konvertierten, bemerkten wir, dass diese aufgrund des Vereinfachungsvorgangs in der Ferne ihr Blattwerk verloren; entweder dünnte es aus oder verschwand in manchen Fällen plötzlich ganz. Es kam einfach der Punkt, an dem jedes einzelne Blatt nicht weiter als bis zu einem einzigen Dreieck vereinfacht werden konnte, und Blätter mussten entfernt werden, um die Dreiecksanzahl zu verringern. Und jedes so entfernte Blatt führte dazu, dass das Blattwerk optisch immer dünner wurde.

Um dieses Problem zu beheben, fügten wir dem Nanite-Builder eine neue Logik hinzu (eine Option namens „Preserve Area“, der in den Nanite-Einstellungen des Meshes aktiviert werden kann), mit der der verlorene Bereich auf die übrigen Dreiecke umverteilt wird, indem die offenen Grenzkanten verzerrt werden. Im Fall der Blätter führt das effektiv dazu, dass die übrigen Blätter größer werden. Das sieht aus der Nähe natürlich seltsam aus, aber auf die Entfernung, auf die der Bewahrungsbereich aktiviert wird, sorgt es stattdessen aber dafür, dass die erwartete Dichte erhalten bleibt.

Diese Funktion sollte nur für Laub-Meshes verwendet werden, bei denen dieses Problem auftritt, und auf nichts anderes.
 
Ohne Bewahrungsbereich

 
Mit Bewahrungsbereich
Windanimation
Es würde seltsam aussehen, wenn alle Bäume mit hoher Wiedergabetreue dargestellt sind, aber im Wind nicht animiert werden. Bisher erreichte Fortnite Windanimationen mit komplexer Logik, die World Position Offset auslöst. Da Nanite-Bäume wesentlich mehr Scheitelpunkte haben (~300.000–500.000) als ihre Nicht-Nanite-Gegenstücke (~10.000–20.000) und die Weltposition nun im Rasterizer von Nanite berechnet wird, sahen wir uns nach anderen Wegen um, die Bäume zu animieren und dabei die Berechnungskosten pro Scheitelpunkt zu verringern.
In unseren Experimenten konnten wir schlussendlich eine komplexe Windsimulation in eine Textur baken, die wir beim Auslösen des World Position Offset aufrufen können, anstatt tonnenweise komplexe Mathematik für jeden Scheitelpunkt berechnen zu müssen.
Von der Baumgeometrie ausgehend, können wir den Bewegungspunkt jedes Asts und die Stufe innerhalb der Geometrie auslesen, genau wie für jeden übergeordneten Ast. Mit diesen Informationen können wir ein Skelett erstellen und dieses in eine Houdini-Vellum- Simulation eingeben.
Bewegungspunkt und Orientierung jedes Asts in der Simulation können als Pixelwert in ein Bild kodiert werden, in einem Material-Shader genutzt und über einen benutzerdefinierten UV-Wert in das Mesh-Asset indiziert werden, mit dem die passende Pixelreihe für den Ast ausgewählt wird.

Aus diesem Grund muss der Nanite-Rasterizer nur eine einzige Position und ein einziges Quaternion aufrufen, um den Offset zu berechnen – mehrfaches voneinander abhängiges Texturauslesen ist nicht mehr nötig. Diese Herangehensweise unterstützt aktuell nur steife Animationen, da jeder Ast den gleichen UV-Wert hat.
Distanz-Culling
Die Windsimulation an Bäumen ist nah an der Kamera äußerst wichtig, aber in der Entfernung mehr oder weniger nicht zu erkennen. Als Performance-Optimierung haben wir für Nanite eine Möglichkeit hinzugefügt, Berechnungen für World Position Offset ab einer vom Entwickler festgelegten Entfernung zu deaktivieren. Diese Optimierung baut auf dem Modus „World Position Offset berechnen“ auf.

Gras

Wir haben Unterstützung für Landschafts-Gras hinzugefügt, Nanite-Mesh-Instanzen erscheinen zu lassen, einschließlich Unterstützung für das in diesem System eingebaute Distanz-Culling. Die Asset-Herangehensweise ist dabei ähnlich wie bei dem für Bäume. Wir verwendeten opakes Material mit tatsächlicher Geometrie für die Grashalme, aber einfache Mathematik für die Animation des World Position Offsets.
 

 

Landschaft (Nanite)

Aufgrund der Funktionsweise von Virtual Shadow Maps rendern große Nicht-Nanite-Meshes Schatten sehr viel langsamer als ein Nanite-Mesh der gleichen Größe. Dies ist insbesondere für Landschaft ein Problem, da es sich dabei um ein gigantisches Nicht-Nanite-Mesh handelt, das eine Menge GPU-Zeit kostet. Wir haben für Landschaft eine experimentelle Nanite-Renderfunktion implementiert und mit UE 5.1 veröffentlicht, die das Landschafts-Höhenfeld zur Build-Zeit in ein Nanite-Mesh umwandelt. Diese Umwandlung erhöht die visuelle Qualität nicht, bietet aber die Performance-Eigenschaften von Nanite-Culling und Rasterisierung, wenn für den Base Pass oder Virtual Shadow Maps gerendert wird.
Lumen hat einen speziellen Pfad für Tracing gegen das Landschafts-Höhenfeld, und Nanite unterstützt aktuell noch kein Rendering in Virtuelle Laufzeit-Texturen – in diesen Fällen wird die Nanite-Repräsentation nicht verwendet, sondern stattdessen das Höhenfeld.
 

Zukünftige Arbeit

Vermutlich das Wichtigste, was noch zu tun ist, ist die akkurate (pro Bild) Volumenberechnung für Instanzen und Cluster Bounding, die zu den Animationen des World Position Offsets passt. Dies ist extrem wichtig, um sicherzustellen, dass das Verdeckungs-Culling von Nanite vor der Rasterisierung so viele Cluster wie möglich entfernt.
 

Im Moment verwenden wir Referenz-Posengrenzen (und ignorieren World Position Offset), was entweder zu konservativ ist und mehr Cluster als nötig rastert, oder visuelle Artefakte verursacht, wenn das World Position Offset Cluster animiert, die vollständig außerhalb der Referenz-Posengrenzen sind (Teile des Meshs verschwinden). Wir haben erste Unterstützung für Grenzen-Größen bei primitiven Komponenten implementiert (ähnlich, wie Nicht-Nanite-Funktionen dies lösen), was das Vergrößern der Grenzen um einen beliebigen Wert ermöglicht, aber dies macht das Culling noch konservativer und kostet unnötig Performance.

Wir planen, die Optimierung des Material-Systems in Nanite fortzusetzen, um die Kosten von maskierten Materialien sowie Materialien mit Pixel Depth Offset zu verringern.

Wir freuen uns sehr, diese neuen Funktionen für Nanite mit Unreal Engine 5.1 zu veröffentlichen und sind gespannt auf all die großartigen neuen Inhalte, die Entwickler damit erstellen werden!
Weitere Informationen zu den in diesem Blog erwähnten Funktionen finden Sie in der Nanite- Dokumentation.

    Sichern Sie sich die Unreal Engine noch heute!

    Sichern Sie sich das offenste und fortschrittlichste Werkzeug der Welt.
    Die Unreal Engine wird mit allen Funktionen und vollem Zugriff auf den Quellcode geliefert und ist sofort einsatzbereit.