
Vor einigen Monaten haben wir das Add-on Evergine.Cesium vorgestellt, das eine gestreamte digitale Erde im globalen Maßstab in Ihre .NET-Anwendungen bringt. Seitdem hat sich unsere Arbeit daran vor allem auf ein Ziel konzentriert: es schneller zu machen. Wir haben die Art und Weise, wie das Add-on Cesium-Kacheln in Pixel umwandelt, von Grund auf neu entwickelt. Der neue Renderer streamt flüssiger, zeichnet bei gleicher CPU-Zeit weit mehr Kacheln und hält den Speicherverbrauch vorhersehbar.
Kurz gesagt: Kacheln sind keine Szenen-Entitäten mehr. Ihre Geometrie landet in einem großen gemeinsamen Puffer und ihr Bildmaterial in einem einzigen Texturatlas. Die GPU entscheidet, was gezeichnet wird, und rendert alle sichtbaren Kacheln mit einem einzigen Indirect Draw Call, während Cesium Native in einem eigenen Thread arbeitet, statt mit Ihrem Frame zu konkurrieren. Im weiteren Verlauf dieses Artikels erklären wir, warum wir diese Änderungen vorgenommen haben und wie sie zusammenspielen.
In diesem Artikel
- Warum eine digitale Erde einen Renderer stark fordert
- Wo der bisherige Ansatz an seine Grenzen stieß
- Der neue Renderer im Überblick
- Was ist neu
- Die Ergebnisse
- Erste Schritte
- Upgrade von der vorherigen Version
- Probieren Sie es aus und sagen Sie uns Ihre Meinung
Warum eine digitale Erde einen Renderer stark fordert
Ein 3D-Globus wird in Kacheln gestreamt. Cesium Native prüft, wo sich die Kamera befindet, und ermittelt, welche Teile von Gelände, Bildmaterial und Gebäuden benötigt werden und in welcher Detailstufe. Beim Überflug einer Stadt können Hunderte Kacheln gleichzeitig auf dem Bildschirm sein. Bewegt sich die Kamera, werden viele davon ersetzt: Neue treffen über das Netzwerk ein, während alte verworfen werden.
Das ist eine völlig andere Arbeitslast als ein typisches Spiellevel. Ein Level lädt seine Meshes einmal und zeichnet sie dann minutenlang. Ein Globus erzeugt und zerstört dagegen während des Renderings ständig GPU-Ressourcen, lädt jede eintreffende Kachel hoch und gibt jede ausscheidende Kachel frei – und all das muss geschehen, ohne dass die Bildrate darunter leidet. Entscheidend sind daher nicht nur die Kosten für das Zeichnen einer Kachel, sondern auch, wie günstig es ist, eine Kachel zu laden, vorzuhalten und wieder freizugeben.
Wo der bisherige Ansatz an seine Grenzen stieß
Die erste Version des Add-ons wählte den direktesten Weg und machte aus jeder Kachel ein reguläres Evergine-Modell. Cesium übergab uns die Kachel als glTF-Modell, das wir im Speicher als GLB-Datei ausgaben und anschließend mit dem Modell-Loader von Evergine erneut parsten. Der Loader erzeugte eine Entität mit eigenem Mesh und Material, die Entität wurde der Szene hinzugefügt, und jedes Stück Bildmaterial wurde zu einer separaten Textur, die später dem passenden Material zugewiesen wurde. Cesium selbst wurde im Haupt-Thread aktualisiert, zusammen mit den Warteschlangen, die diese Entitäten hinzufügten und entfernten.
Das war einfach und funktionierte bei kleinen Szenen gut, skalierte aber nicht. Jede eintreffende Kachel kostete einen GLB-Umweg plus einen kompletten Satz neuer Entitäten, Meshes, Materialien und Texturen, sodass der Frame stockte, wenn viele Kacheln gleichzeitig eintrafen. Außerdem wurde jede Kachel in jedem Pass einzeln gezeichnet. Bei Hunderten Kacheln auf dem Bildschirm verbrachte die CPU daher einen guten Teil des Frames allein mit dem Absetzen von Draw Calls, und Schatten vervielfachten diese Kosten. Hinzu kam, dass jede Minute Tausende kleiner Puffer und Texturen erzeugt und zerstört wurden, was den Speicherverbrauch schwer vorhersehbar machte – und all das lief im selben Thread wie Ihre Anwendung.
Der neue Renderer im Überblick
Der neue Renderer behandelt Kacheln als Daten statt als Entitäten. Statt für jede Kachel Ressourcen zu erzeugen, legt er einmalig einige wenige große Ressourcen an und vergibt Teile davon, während Kacheln hinzukommen und wegfallen.
Aus Sicht Ihrer Anwendung ändert sich sehr wenig: Sie fügen Ihrer Szene weiterhin einen CesiumCoordinator hinzu, und der Globus erscheint. Unter der Haube ist jedoch fast alles neu.
Neuerungen
Die gesamte Geometrie in einem Puffer
Die gesamte Kachelgeometrie liegt jetzt in einem einzigen Vertex-Buffer und einem einzigen Index-Buffer, die beide einmalig beim Start erzeugt werden. Trifft eine Kachel ein, liest das Add-on ihre Positionen, Normalen, Texturkoordinaten und Indizes direkt aus den glTF-Accessors von Cesium – ohne GLB-Umweg –, packt sie in ein verschachteltes Vertex-Format und kopiert sie direkt in den eigenen Abschnitt der Kachel in den gemeinsamen Puffern.
Die Vergabe dieser Abschnitte muss schnell gehen, denn sie erfolgt für jede eingestreamte Kachel. Dafür verwenden wir eine C#-Portierung des OffsetAllocator von Sebastian Aaltonen, der genau für diese Aufgabe entwickelt wurde. Er sortiert freien Speicher in Größenklassen, sodass das Zuweisen oder Freigeben eines Blocks gleich viel Zeit benötigt – egal ob der Puffer leer oder fast voll ist –, und hält die Fragmentierung gering. Neben der Geometrie hat jede Kachel einen kleinen Eintrag in einem Metadatenpuffer auf der GPU, der festhält, wo ihre Vertices, Indizes und ihr Bildmaterial liegen und wie sie transformiert wird. Das Hinzufügen oder Entfernen einer Kachel betrifft daher immer nur ihren eigenen Eintrag.
Das gesamte Bildmaterial in einem Atlas
Bildmaterial wie Satellitenfotos wird über das Gelände gelegt, und Cesium liefert es in kleinen Stücken. Statt für jedes Stück eine Textur zu erzeugen, packt das Add-on sie jetzt alle in einen großen Texturatlas. Der Atlas ist in Zeilen, sogenannten Shelves, organisiert, und jedes Bild kommt in die Zeile, die am besten passt. Wird eine Kachel freigegeben, wird ihr Platz vom nächsten Bild wiederverwendet, das ihn benötigt.
Die Pixel gelangen in den Atlas, ohne die GPU auszubremsen. Die Lade-Threads von Cesium schreiben die Bilder in einen Staging-Buffer, und zwei dieser Puffer wechseln sich ab, sodass die CPU einen füllen kann, während die GPU aus dem anderen liest. Einmal pro Frame kopiert ein Compute Shader jedes ausstehende Bild an seinen Platz im Atlas und konvertiert dabei seine Farben von sRGB nach linear. Jedes Bild ist zudem von einem vier Texel breiten Rand umgeben, der seine Randpixel wiederholt, sodass die Texturfilterung nie Farbe aus einem benachbarten Bild übernimmt und Kacheln keine Nähte zeigen. Füllt sich der Atlas schließlich über einen Schwellenwert hinaus, fordert das Add-on Cesium auf, nicht mehr benötigte Kacheln freizugeben. So bleibt der Speicher für Bildmaterial auch bei langen Flügen unter Kontrolle.
Die GPU erstellt die Draw-Liste
Dies ist die größte Änderung. In einem klassischen Renderer durchläuft die CPU jedes Objekt in einer Schleife und zeichnet dafür einen Draw Call auf. Bei Hunderten Kacheln und mehreren Passes wird diese Schleife schnell zum Engpass. Der neue Renderer verlagert sie auf die GPU.
Nachdem Cesium die Kacheln für die aktuelle Ansicht ausgewählt hat, besteht die einzige Aufgabe der CPU darin, eine Sichtbarkeitsmaske mit einem Bit pro Kachel zu schreiben. Auf der GPU führt ein Compute Shader einen Thread pro Kachel aus und liest dieses Bit. Jede sichtbare Kachel belegt einen Slot und schreibt ihren Draw-Befehl in einen Indirect-Arguments-Buffer. Derselbe Shader berechnet außerdem die World- und World-View-Projection-Matrizen der Kachel, sodass der Vertex Shader diese Multiplikation nicht für jeden Vertex wiederholen muss. Anschließend setzt der Renderer einen einzigen Indirect Draw ab, der alle sichtbaren Kacheln auf einmal rendert und dabei die Geometrie aus den gemeinsamen Puffern und das Bildmaterial aus dem Atlas liest.
Im Ergebnis wächst die Arbeit der CPU nicht mehr mit der Anzahl der Kacheln auf dem Bildschirm. Ob fünfzig oder fünfhundert Kacheln: Sie schreibt eine kleine Bitmaske, startet einen Compute Shader und setzt einen Draw pro Pass ab. Sollten Sie einmal Ergebnisse vergleichen oder einem Treiberproblem auf den Grund gehen müssen, schaltet die Option UseCPUDrawCalls wieder auf das Erstellen der Draw-Befehle auf der CPU um – weiterhin mit den gemeinsamen Puffern und dem Atlas.
Cesium erhält einen eigenen Thread
Cesium Native läuft jetzt in einem dedizierten Thread. In jedem Frame übergibt ihm der Haupt-Thread die aktuelle Kamera, und der Cesium-Thread wählt Kacheln aus, verteilt die Ladearbeit und erzeugt eine neue Sichtbarkeitsmaske. Am Ende jedes Updates tauschen die beiden Threads ihre Masken, sodass der Renderer stets einen vollständigen, konsistenten Satz an Kacheln zeichnet.
Der Cesium-Thread läuft mit niedrigerer Priorität, sodass auf einem ausgelasteten Rechner Ihr Frame Vorrang hat. Dauert ein Update länger als ein Frame, zeichnet der Renderer einfach weiter den letzten vollständigen Kachelsatz, bis Cesium aufgeholt hat. Abfragen der Geländehöhe laufen ebenfalls über diesen Thread, und ihre Ergebnisse werden Ihnen während des Szenen-Updates zurückgegeben.
Weiterhin vollwertig in Evergine integriert
Kacheln auf eigene Weise zu rendern heißt nicht, die Engine hinter sich zu lassen. Kacheln werden mit einem eigenen City-Effekt gezeichnet, der auf der Standardbeleuchtung von Evergine aufbaut, und sie nehmen an den üblichen Passes teil. Sie werfen und empfangen Schatten, funktionieren also mit dem neuen Schattensystem und mit einer Sonne, die im Tagesverlauf wandert, und sie sind wie jede andere opake Geometrie der Szene Teil des Depth Pre-Pass. Die gesamte Pipeline läuft unter DirectX 11, DirectX 12 und Vulkan.
Wissen, was auf der GPU liegt
Bei Puffern fester Größe ist es wichtig zu wissen, wie voll sie sind. CesiumCoordinator.Diagnostics meldet jetzt, wie viele Kacheln in jedem Tileset sichtbar und wie viele insgesamt resident sind, zusammen mit Kapazität und Auslastung von Vertex-Buffer, Index-Buffer und Texturatlas. Mit diesen Zählern können Sie ganz einfach ein eigenes Profiling-Panel erstellen. Der Atlas selbst wird über TextureAtlas bereitgestellt, sodass Sie ihn anzeigen und in Echtzeit verfolgen können, wie das Bildmaterial hineingepackt wird.
Die Ergebnisse
Um zu sehen, was all das zusammen bewirkt, haben wir die vorherige Version des Add-ons unter denselben Bedingungen mit der neuen verglichen. Beide verwendeten dieselbe Cesium-Konfiguration, wobei die Kamera die gesamte Stadt Madrid auf einem 4K-Display erfasste, auf einem Intel Core i7-14700K mit einer NVIDIA GeForce RTX 4080 SUPER. Damit sich der Vergleich auf den Kachel-Renderer selbst konzentriert, haben wir den Depth Pre-Pass, den G-Buffer-Pass und das Post-Processing deaktiviert und die Frame-Zeiten mit CapFrameX aufgezeichnet.
Der Unterschied ist größer als erwartet. Im Durchschnitt stieg die Bildrate von 40,4 FPS auf 332,4 FPS. Ein Frame dauert jetzt also etwa 3 ms statt fast 25 ms: rund 8,2-mal schneller.
| Messgröße | Vorherige Version | Neuer Renderer |
|---|---|---|
| Durchschnitt | 40,4 FPS (24,75 ms) | 332,4 FPS (3,01 ms) |
| P95 | 44,1 FPS (22,68 ms) | 355,5 FPS (2,81 ms) |
| P5 | 37,8 FPS (26,46 ms) | 290,1 FPS (3,45 ms) |
| P1 | 35,9 FPS (27,86 ms) | 281,2 FPS (3,56 ms) |
| Durchschnitt der langsamsten 1 % | 33,8 FPS (29,59 ms) | 274,9 FPS (3,64 ms) |
| Durchschnitt der langsamsten 0,1 % | 30,7 FPS (32,57 ms) | 257,4 FPS (3,89 ms) |
Der Durchschnitt ist nur die halbe Wahrheit. Die Lows, also der Durchschnitt der langsamsten 1 % und 0,1 % der Frames, zeigen, wie sich der Renderer in seinen schlechtesten Momenten verhält – und genau diese Frames nehmen Sie tatsächlich als Ruckeln wahr. Auch hier ist der Gewinn ebenso deutlich: Die langsamsten Frames sanken von über 32 ms auf unter 4 ms. Selbst die schlechtesten Frames des neuen Renderers sind mehr als fünfmal schneller als die besten Frames des alten, und der Abstand zwischen den schnellen Frames (P95) und den langsamsten (0,1 % Low) ist von etwa 10 ms auf etwa 1 ms geschrumpft. Der Globus läuft also nicht nur schneller, sondern auch deutlich gleichmäßiger.
Erste Schritte
Wenn Sie das Add-on bereits verwenden, genügt ein Update des NuGet-Pakets Evergine.Cesium, um den neuen Renderer zu erhalten. Fügen Sie bei einem neuen Projekt Ihrer Szene einen CesiumCoordinator zusammen mit den Tilesets hinzu, die Sie streamen möchten:
using Evergine.Cesium;
using Evergine.Cesium.Core;
using Evergine.Cesium.Utils;
using Evergine.Framework;
public class MyScene : Scene
{
public override void RegisterManagers()
{
base.RegisterManagers();
// The scene must contain an entity tagged as "MainCamera" with a Camera3D component.
var cesium = new CesiumCoordinator()
{
AccessToken = "<YOUR_CESIUM_ION_TOKEN>",
GeocodingService = new AzureMapsGeocodingService("<YOUR_OPTIONAL_AZURE_MAPS_KEY>"),
EntityManager = this.Managers.EntityManager,
};
// Cesium World Terrain with Bing Maps imagery, plus OpenStreetMap buildings
cesium.AddOverlayedTileset(1, RasterOverlayProvider.BingAerial, "Terrain");
cesium.AddTileset(96188, "Buildings");
this.Managers.AddManager(cesium);
}
}
Upgrade von der vorherigen Version
Der neue Renderer bringt einige API-Änderungen mit sich. Tilesets werden jetzt explizit hinzugefügt: AddOverlayedTileset ist für Gelände mit darübergelegtem Bildmaterial gedacht, AddTileset für Geometrie wie Gebäude. Sie können beliebig viele hinzufügen und sie später über ihren Namen oder ihre Asset-ID wieder entfernen.
Auch das Geocoding ist jetzt austauschbar. Die Eigenschaft AzureMapsKey wurde durch GeocodingService ersetzt, die jede Implementierung von IGeocodingService akzeptiert. Das Add-on enthält AzureMapsGeocodingService und einen neuen GoogleMapsGeocodingService, und Sie können auch eigene schreiben.
Da Kacheln keine Entitäten mehr sind, findet Code, der die Szene nach Kachel-Entitäten durchsucht hat, diese nicht mehr. Verwenden Sie stattdessen Diagnostics, um zu prüfen, was geladen ist, und QueryTerrainMinHeight, um das Gelände abzutasten. Und schließlich: Der neue Renderer basiert auf Compute Shadern und Effekten, die mit den Assets des Add-ons ausgeliefert werden. Achten Sie daher darauf, dass Assembly und Assets aus derselben Version stammen.
Probieren Sie es aus und sagen Sie uns Ihre Meinung
Bereit für schnellere Flüge? Aktualisieren Sie Evergine, fügen Sie das neueste Paket Evergine.Cesium hinzu und streamen Sie Ihre Lieblingsstadt. Wenn Sie das Add-on noch nicht kennen, ist die Einführung in Evergine.Cesium ein guter Einstieg, und die Evergine-Dokumentation enthält die vollständige Referenz. Wenn Sie auf ein Problem stoßen oder eine Idee haben, teilen Sie es uns im Evergine-Feedback-Repository mit.
Wir sind gespannt, was Sie damit entwickeln.
