
Il y a quelques mois, nous avons présenté l’add-on Evergine.Cesium, qui apporte à vos applications .NET une Terre numérique à l’échelle du globe, diffusée en streaming. Depuis, l’essentiel de notre travail s’est concentré sur un seul objectif : le rendre plus rapide. Nous avons repensé la manière dont l’add-on transforme les tuiles Cesium en pixels, et le nouveau moteur de rendu offre un streaming plus fluide, affiche beaucoup plus de tuiles pour le même temps CPU et maintient une consommation mémoire prévisible.
En résumé, les tuiles ne sont plus des entités de la scène. Leur géométrie est placée dans un grand buffer partagé et leur imagerie dans un unique atlas de textures. C’est le GPU qui décide quoi dessiner et qui rend toutes les tuiles visibles en un seul draw call indirect, tandis que Cesium Native travaille sur son propre thread au lieu de concurrencer votre frame. La suite de cet article explique pourquoi nous avons fait ces changements et comment ils s’articulent.
Dans cet article
- Pourquoi une Terre numérique met le moteur de rendu à rude épreuve
- Les limites de l’ancienne approche
- Le nouveau moteur de rendu en un coup d’œil
- Les nouveautés
- Les résultats
- Prise en main
- Migration depuis la version précédente
- Essayez-le et donnez-nous votre avis
Pourquoi une Terre numérique met le moteur de rendu à rude épreuve
Un globe 3D est diffusé en streaming sous forme de tuiles. Cesium Native regarde où se trouve la caméra et détermine quels éléments de terrain, d’imagerie et de bâtiments sont nécessaires, et avec quel niveau de détail. Survolez une ville et il peut y avoir des centaines de tuiles à l’écran en même temps. Déplacez la caméra et nombre d’entre elles sont remplacées : de nouvelles arrivent du réseau tandis que les anciennes sont supprimées.
C’est une charge de travail très différente de celle d’un niveau de jeu classique. Un niveau charge ses meshes une fois, puis les dessine pendant de longues minutes. Un globe, lui, crée et détruit en permanence des ressources GPU pendant le rendu, en téléversant chaque tuile qui arrive et en libérant chaque tuile qui part, et tout cela doit se faire sans que le frame rate en pâtisse. Le coût qui compte vraiment n’est donc pas seulement la rapidité avec laquelle une tuile peut être dessinée, mais le coût de son arrivée, de son maintien en mémoire et de sa libération.
Les limites de l’ancienne approche
La première version de l’add-on suivait la voie la plus directe et transformait chaque tuile en un modèle Evergine classique. Cesium nous fournissait la tuile sous forme de modèle glTF, que nous écrivions en mémoire sous forme de fichier GLB avant de l’analyser de nouveau avec le chargeur de modèles d’Evergine. Le chargeur créait une entité avec son propre mesh et son propre matériau, l’entité était ajoutée à la scène, et chaque élément d’imagerie devenait une texture distincte, attachée ensuite au matériau correspondant. Cesium lui-même était mis à jour sur le thread principal, tout comme les files d’attente qui ajoutaient et supprimaient ces entités.
C’était simple et cela fonctionnait bien pour les petites scènes, mais cela ne passait pas à l’échelle. Chaque tuile entrante payait un aller-retour GLB ainsi que la création de toute une série de nouvelles entités, meshes, matériaux et textures, si bien que lorsque de nombreuses tuiles arrivaient en même temps, la frame saccadait. Chaque tuile était en outre dessinée séparément, dans chaque passe : avec des centaines de tuiles à l’écran, le CPU passait une bonne partie de la frame à simplement soumettre des draw calls, et les ombres multipliaient ce coût. De plus, des milliers de petits buffers et de petites textures étaient créés et détruits chaque minute, ce qui rendait la consommation mémoire difficile à prévoir, et tout cela s’exécutait sur le même thread que votre application.
Le nouveau moteur de rendu en un coup d’œil
Le nouveau moteur de rendu traite les tuiles comme des données plutôt que comme des entités. Au lieu de créer des ressources pour chaque tuile, il crée une fois pour toutes quelques grandes ressources et en distribue des portions au fur et à mesure que les tuiles arrivent et repartent.
Du point de vue de votre application, très peu de choses changent : vous ajoutez toujours un CesiumCoordinator à votre scène et le globe apparaît. Sous le capot, en revanche, presque tout est nouveau.
Quoi de neuf
Toute la géométrie dans un seul buffer
Toute la géométrie des tuiles se trouve désormais dans un seul vertex buffer et un seul index buffer, tous deux créés une fois au démarrage. Lorsqu’une tuile arrive, l’add-on lit ses positions, normales, coordonnées de texture et indices directement depuis les accessors glTF de Cesium, sans aller-retour GLB, les regroupe dans un format de sommets entrelacé et les copie directement dans la portion des buffers partagés réservée à la tuile.
La distribution de ces portions doit être rapide, car elle a lieu pour chaque tuile reçue en streaming. Pour cela, nous utilisons un portage C# de l’OffsetAllocator de Sebastian Aaltonen, conçu précisément pour cette tâche. Il répartit l’espace libre en classes de taille, de sorte qu’allouer ou libérer un bloc prend le même temps constant, que le buffer soit vide ou presque plein, et il limite la fragmentation. En plus de la géométrie, chaque tuile dispose d’une petite entrée dans un buffer de métadonnées sur le GPU, qui indique où se trouvent ses sommets, ses indices et son imagerie ainsi que sa transformation : ajouter ou supprimer une tuile ne touche donc jamais que sa propre entrée.
Toute l’imagerie dans un seul atlas
L’imagerie, comme les photos satellite, est drapée sur le terrain, et Cesium la fournit par petits morceaux. Plutôt que de créer une texture pour chaque morceau, l’add-on les regroupe désormais tous dans un grand atlas de textures. L’atlas est organisé en rangées, ou étagères, et chaque image est placée dans l’étagère qui lui convient le mieux ; lorsqu’une tuile est libérée, son espace est réutilisé par la prochaine image qui en a besoin.
Les pixels sont copiés dans l’atlas sans bloquer le GPU. Les threads de chargement de Cesium écrivent les images dans un staging buffer, et deux de ces buffers sont utilisés en alternance afin que le CPU puisse remplir l’un pendant que le GPU lit l’autre. Une fois par frame, un compute shader copie chaque image en attente à sa place dans l’atlas et convertit ses couleurs de sRGB en linéaire au passage. Chaque image est également entourée d’une bordure de quatre texels qui reproduit ses pixels de bord, afin que le filtrage de texture ne récupère jamais la couleur d’une image voisine et que les tuiles ne présentent pas de jointures visibles. Enfin, lorsque le remplissage de l’atlas dépasse un seuil, l’add-on demande à Cesium de libérer les tuiles dont il n’a plus besoin, ce qui permet de maîtriser la mémoire consacrée à l’imagerie lors des longs survols.
Le GPU construit la liste de dessin
C’est le changement le plus important. Dans un moteur de rendu classique, le CPU parcourt chaque objet et enregistre un draw call pour chacun, et avec des centaines de tuiles et plusieurs passes, cette boucle devient vite le goulot d’étranglement. Le nouveau moteur de rendu la transfère au GPU.
Une fois que Cesium a sélectionné les tuiles pour la vue courante, le seul travail du CPU consiste à écrire un masque de visibilité avec un bit par tuile. Sur le GPU, un compute shader exécute un thread par tuile et lit ce bit. Chaque tuile visible réserve un emplacement et écrit sa commande de dessin dans un buffer d’arguments indirects ; le même shader calcule également les matrices world et world-view-projection de la tuile, si bien que le vertex shader n’a pas à répéter cette multiplication pour chaque sommet. Le moteur de rendu émet ensuite un seul draw indirect qui rend toutes les tuiles visibles en une fois, en lisant la géométrie depuis les buffers partagés et l’imagerie depuis l’atlas.
Résultat : le travail du CPU n’augmente plus avec le nombre de tuiles à l’écran. Qu’il y ait cinquante ou cinq cents tuiles, il écrit un petit masque de bits, lance un compute shader et émet un draw par passe. Si vous avez besoin de comparer des résultats ou de traquer un problème de pilote, l’option UseCPUDrawCalls permet de revenir à la construction des commandes de dessin sur le CPU, tout en utilisant toujours les buffers partagés et l’atlas.
Cesium obtient son propre thread
Cesium Native s’exécute désormais sur un thread dédié. À chaque frame, le thread principal lui transmet la dernière position de la caméra, et le thread Cesium sélectionne les tuiles, lance les tâches de chargement et produit un nouveau masque de visibilité. Les deux threads échangent leurs masques à la fin de chaque mise à jour, de sorte que le moteur de rendu dessine toujours un ensemble de tuiles complet et cohérent.
Le thread Cesium s’exécute avec une priorité plus faible : sur une machine chargée, votre frame passe en premier. Si une mise à jour dure plus d’une frame, le moteur de rendu continue simplement à dessiner le dernier ensemble complet de tuiles pendant que Cesium rattrape son retard. Les requêtes de hauteur du terrain passent elles aussi par ce thread, et leurs résultats vous sont renvoyés lors de la mise à jour de la scène.
Toujours pleinement intégré à Evergine
Rendre les tuiles de manière personnalisée ne signifie pas laisser le moteur de côté. Les tuiles sont dessinées avec un effet dédié aux villes, basé sur l’éclairage standard d’Evergine, et elles participent aux passes habituelles. Elles projettent et reçoivent des ombres, ce qui leur permet de fonctionner avec le nouveau système d’ombres et avec un soleil qui se déplace au fil de la journée, et elles participent à la depth pre-pass comme toute autre géométrie opaque de la scène. L’ensemble du pipeline fonctionne sous DirectX 11, DirectX 12 et Vulkan.
Savoir ce qui se trouve sur le GPU
Avec des buffers de taille fixe, il est important de connaître leur taux de remplissage. CesiumCoordinator.Diagnostics indique désormais combien de tuiles sont visibles dans chaque tileset et combien sont résidentes au total, ainsi que la capacité et l’utilisation du vertex buffer, de l’index buffer et de l’atlas de textures. Ces compteurs permettent de créer facilement votre propre panneau de profilage, et l’atlas lui-même est exposé via TextureAtlas, ce qui vous permet de l’afficher et de voir l’imagerie s’y ranger en temps réel.
Les résultats
Pour mesurer l’effet de tout cela, nous avons comparé la version précédente de l’add-on à la nouvelle dans les mêmes conditions. Les deux utilisaient la même configuration Cesium, avec la caméra cadrant toute la ville de Madrid sur un écran 4K, sur un Intel Core i7-14700K et une NVIDIA GeForce RTX 4080 SUPER. Pour que la comparaison porte sur le moteur de rendu des tuiles lui-même, nous avons désactivé la depth pre-pass, la passe G-buffer et le post-traitement, et nous avons enregistré les temps de frame avec CapFrameX.
La différence est plus importante que prévu. En moyenne, le frame rate est passé de 40,4 FPS à 332,4 FPS : une frame prend désormais environ 3 ms au lieu de près de 25 ms, soit environ 8,2 fois plus rapide.
| Métrique | Version précédente | Nouveau moteur de rendu |
|---|---|---|
| Moyenne | 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) |
| Moyenne 1 % low | 33,8 FPS (29,59 ms) | 274,9 FPS (3,64 ms) |
| Moyenne 0,1 % low | 30,7 FPS (32,57 ms) | 257,4 FPS (3,89 ms) |
La moyenne ne raconte que la moitié de l’histoire. Les valeurs « low », qui font la moyenne des 1 % et 0,1 % de frames les plus lentes, montrent comment le moteur de rendu se comporte dans ses pires moments, et ce sont ces frames que l’on perçoit réellement comme des saccades. Là aussi, le gain se confirme : les frames les plus lentes sont passées de plus de 32 ms à moins de 4 ms. Même les pires frames du nouveau moteur de rendu sont plus de cinq fois plus rapides que les meilleures frames de l’ancien, et l’écart entre les frames rapides (P95) et les plus lentes (0,1 % low) est passé d’environ 10 ms à environ 1 ms : le globe ne tourne donc pas seulement plus vite, il tourne aussi de manière beaucoup plus régulière.
Prise en main
Si vous utilisez déjà l’add-on, il suffit de mettre à jour le package NuGet Evergine.Cesium pour bénéficier du nouveau moteur de rendu. Pour un nouveau projet, ajoutez un CesiumCoordinator à votre scène ainsi que les tilesets que vous souhaitez diffuser en streaming :
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);
}
}
Migration depuis la version précédente
Le nouveau moteur de rendu s’accompagne de quelques changements d’API. Les tilesets sont désormais ajoutés explicitement : AddOverlayedTileset sert au terrain avec l’imagerie drapée dessus, et AddTileset à la géométrie comme les bâtiments. Vous pouvez en ajouter autant que nécessaire et les supprimer ensuite par nom ou par ID d’asset.
Le géocodage est également devenu modulable. La propriété AzureMapsKey a été remplacée par GeocodingService, qui accepte n’importe quelle implémentation de IGeocodingService. L’add-on est fourni avec AzureMapsGeocodingService et un nouveau GoogleMapsGeocodingService, et vous pouvez écrire le vôtre.
Comme les tuiles ne sont plus des entités, le code qui recherchait des entités de tuiles dans la scène ne les trouvera plus. Utilisez plutôt Diagnostics pour inspecter ce qui est chargé et QueryTerrainMinHeight pour échantillonner le terrain. Enfin, le nouveau moteur de rendu s’appuie sur des compute shaders et des effets fournis avec les assets de l’add-on : veillez donc à ce que l’assembly et les assets proviennent de la même version.
Essayez-le et donnez-nous votre avis
Envie d’aller plus vite ? Mettez à jour Evergine, ajoutez le dernier package Evergine.Cesium et diffusez en streaming votre ville préférée. Si vous découvrez l’add-on, la présentation d’Evergine.Cesium est un bon point de départ, et la documentation d’Evergine contient la référence complète. Si vous rencontrez un problème ou avez une idée, faites-le-nous savoir dans le dépôt de feedback d’Evergine.
Nous avons hâte de voir ce que vous allez créer avec.
