Créer un monde ouvert dans le navigateur, partie 22 : des nuages que l’on peut éclairer et un culling qu’il faut alimenter
Par Oleg Sidorkin, CTO et cofondateur de Cinevva
Vous découvrez la série ? Consultez le guide de la série. Il explique ce qu’est un spike et renvoie vers toutes les parties.
La partie 21 portait sur une technique de rendu qui n’a pas tenu ses promesses. Cette partie en présente une qui les a tenues, et une autre qui a nécessité une correction minutieuse pour fonctionner tout court. Le spike 43 concerne le ciel : une atmosphère physiquement réaliste et des nuages volumétriques, la base qui donne à une scène l’allure d’un véritable lieu plutôt que d’une démo technique. Le spike 44 concerne le culling GPU de type meshlet, avec une leçon essentielle : la qualité du test d’occlusion dépend directement des occludeurs qu’on lui fournit.
Un ciel issu de la physique, pas d’un dégradé
Ouvrir le spike 43 dans un nouvel onglet ↗ · Voir le code source
Presque tous les effets météorologiques cinématographiques dépendent de deux éléments d’infrastructure : une atmosphère physiquement réaliste, afin que la couleur du ciel et celle du soleil évoluent avec l’heure de la journée selon les lois de la physique plutôt qu’à partir d’un dégradé réglé à la main, et un volume de nuages volumétriques, afin que le ciel possède une structure 3D plutôt qu’une cubemap précalculée. Le spike 43 construit exactement cette paire sur la pile WebGPU et TSL existante, sans rien ajouter, car une fois ces deux éléments en place, le reste de la pile météorologique — brouillard, rayons crépusculaires, surfaces mouillées, neige — devient une succession d’extensions plus petites et déjà bien maîtrisées.
L’atmosphère repose sur le modèle de Hillaire 2020, un ensemble de tables de correspondance calculées dans des compute shaders WGSL. Une table de transmittance intègre la lumière du soleil à travers les profils de densité de Rayleigh, de Mie et de l’ozone, et n’est recalculée que lorsque le soleil se déplace. Une table de vue du ciel est recalculée à chaque frame, car elle est suffisamment peu coûteuse pour qu’un mécanisme conditionnel ne justifie pas le code supplémentaire, avec une paramétrisation non linéaire autour de l’horizon afin d’éviter les bandes. Pour l’instant, la diffusion multiple utilise une approximation analytique à la place de la table complète, et les couchers de soleil restent convaincants : ce raccourci se fait donc bien oublier. Les nuages reposent sur un ray marching de style Schneider Nubis à travers une couche horizontale, dont la forme provient d’une texture Perlin-Worley 128³ érodée par une texture Worley 32³. Toutes deux sont générées au démarrage par calcul GPU, sans aucun téléchargement réseau. L’éclairage combine l’extinction selon la loi de Beer, une fonction de phase à deux lobes et une approximation de l’effet de poudre. Le couplage essentiel vient du fait que la couleur solaire du nuage échantillonne la même table de transmittance à chaque étape, de sorte que l’éclairage des nuages suit le coucher du soleil sans nécessiter une seconde passe de réglage.
Sur un M1, l’ensemble prend entre 1,1 et 2,0 ms avec des nuages en demi-résolution, bien en dessous du budget de 6 ms, utilise environ 14 Mo de mémoire GPU et tourne à plus de 100 FPS. Les deux hypothèses principales se sont vérifiées en pratique. Le coucher de soleil est le plan phare, le moment qui donne au moteur de rendu une dimension cinématographique, et il découle directement de la physique sans réglage propre à chaque heure de la journée. Autre heureux hasard : avec une couche nuageuse paramétrée entre 800 m et 4 000 m, les nuages lointains à l’horizon ressemblent à des crêtes montagneuses sombres depuis une caméra basse, ce qui dote le monde d’un relief d’arrière-plan sans que personne ait à le modéliser.
Une remarque d’architecture mérite d’être conservée. L’approche naturelle consiste à peindre d’abord le ciel dans la swap chain, puis à laisser three.js dessiner la géométrie par-dessus avec autoClear = false. Cela ne fonctionne pas avec le moteur de rendu WebGPU de la r184, car ce drapeau ne contrôle pas l’opération de chargement de la couleur comme il le fait dans WebGL : three.js écrase donc le ciel à chaque frame. La solution consiste à effectuer le rendu three.js dans une cible hors écran, puis à réaliser la composition finale (mix(skyCloud, scene, scene.alpha), suivie d’ACES puis de sRGB) dans notre propre passe, qui possède la swap chain.
Un culling qui ne vaut que par ses occludeurs
Ouvrir le spike 44 dans un nouvel onglet ↗ · Voir le code source
Le spike 44 compare les performances de quatre modes de rendu : rendu forward classique, culling des clusters sur CPU, culling par compute GPU et buffer de visibilité avec culling d’occlusion Hi-Z. Le chemin Hi-Z est le plus intéressant, et il cachait un bug discret : son HUD indiquait que l’occlusion était activée, mais le compteur « Hi-Z killed » restait éternellement bloqué à exactement 0,0 %. Le culling du frustum fonctionnait, donc tout ce qui alimentait le shader de culling était correct. La moitié consacrée à l’occlusion ne faisait rien, tout en coûtant autant que si elle fonctionnait.
Un test d’occlusion Hi-Z projette à l’écran la boîte englobante d’un cluster, choisit un niveau mip de la pyramide de profondeur afin que le rectangle à l’écran couvre environ 2×2 texels, échantillonne la profondeur de l’occludeur le plus éloigné dans ce rectangle, puis rejette le cluster si son point le plus proche se trouve toujours derrière cet occludeur. La pyramide de profondeur est construite à chaque frame en initialisant le mip 0 à partir d’une prépasse de profondeur opaque, puis en appliquant des réductions successives par maximum. La prépasse opaque ne contient délibérément que des occludeurs solides — le sol et des proxies de troncs pour chaque arbre — car le feuillage avec test alpha créerait des trous susceptibles de tromper une réduction par maximum.
Le bug était géométrique, et non logique. Le proxy du tronc était une boîte de 0,5 m sur 4 m sur 0,5 m. À 30 m, il occupe environ 17 pixels à l’écran. Mais un cluster d’herbe typique de 50 m sélectionne le mip 5, où chaque texel couvre 32 pixels de la source. Un tronc de 17 pixels ne recouvre entièrement aucun texel du mip 5 ; chaque texel touchant le tronc touche donc aussi le sol environnant. Dès la toute première réduction 2×2 par maximum, la valeur de profondeur la plus élevée est retenue, c’est-à-dire celle du sol plus éloigné derrière le tronc, et la profondeur du tronc disparaît dès la première réduction. Au mip 5, la pyramide contient alors presque partout la profondeur du sol. Le cluster n’est jamais plus éloigné que le sol, et rien n’est donc jamais occulté.
La solution consiste à agrandir suffisamment le proxy pour qu’il domine les texels qu’il occupe, en le dimensionnant selon la silhouette de l’arbre plutôt que selon son bois. Un proxy d’environ 2 m sur 6 m sur 2 m reste plus petit que la canopée réelle, de sorte que les feuilles visibles à travers les trouées ne sont jamais éliminées à tort, mais il est assez grand pour survivre à la réduction par maximum jusqu’aux distances pertinentes. Le compteur d’occlusion est immédiatement passé au-dessus de zéro. On peut généraliser cette conclusion en une règle pour le moteur de production : tout objet utilisé comme occludeur Hi-Z fiable doit être dimensionné selon sa silhouette à l’écran, car l’efficacité du Hi-Z dans les scènes ouvertes riches en végétation dépend davantage de la couverture des occludeurs au niveau mip concerné que de l’élégance des calculs du test de profondeur. L’occlusion de l’herbe par l’herbe ne peut de toute façon pas se déclencher, puisqu’un brin se trouve à la même profondeur que le sol situé sous lui. Les vrais gains viennent donc des arbres qui occultent la végétation lointaine et des arbres qui en occultent d’autres.
Technologies abordées dans ce chapitre
LUT d’atmosphère de Hillaire 2020. Une table de transmittance — la lumière solaire traversant les profils de Rayleigh, de Mie et de l’ozone — recalculée uniquement lorsque le soleil se déplace, associée à une table de vue du ciel recalculée à chaque frame avec une paramétrisation non linéaire de l’horizon, produit des couleurs de ciel et de soleil physiquement réalistes qui suivent l’heure de la journée sans dégradé réglé à la main. Une approximation analytique de la diffusion multiple remplace la table complète jusqu’à ce qu’un artefact impose son calcul réel. Le coucher de soleil découle directement de la physique, sans réglage propre à chaque heure de la journée.
Nuages volumétriques Schneider Nubis. Un ray marching à travers une couche horizontale, façonnée par une texture Perlin-Worley 128³ générée au démarrage et érodée par une texture Worley 32³, avec un éclairage fondé sur l’extinction selon la loi de Beer, une phase à deux lobes et un terme de poudre. Échantillonner à chaque étape la couleur solaire des nuages depuis la même table de transmittance permet à leur éclairage de suivre gratuitement le lever et le coucher du soleil. Le ray marching en demi-résolution coûte environ quatre fois moins cher que celui en pleine résolution, sans perte de qualité visible aux distances habituelles : c’est le compromis standard en production.
Composition de WebGPU brut avec three.js sur la r184. Peindre le ciel dans la swap chain puis dessiner la géométrie three.js par-dessus avec autoClear = false échoue, car ce drapeau ne contrôle pas l’opération de chargement de la couleur dans le backend WebGPU. Il faut effectuer le rendu three.js dans une cible RGBA16F hors écran, puis appliquer le mix final, le tone mapping et la conversion sRGB dans une passe qui possède la swap chain.
Culling d’occlusion Hi-Z et dimensionnement des occludeurs. Une pyramide de profondeur construite par réduction successive par maximum permet à une passe de culling GPU de rejeter les clusters dont le point le plus proche se trouve derrière l’occludeur le plus éloigné dans leur rectangle à l’écran. Le test peut silencieusement ne rien faire si les occludeurs sont trop petits pour dominer un texel au niveau mip sélectionné, car la première réduction par maximum remplace la profondeur de l’occludeur par celle de l’arrière-plan plus éloigné situé derrière lui. Les occludeurs doivent être dimensionnés selon leur silhouette à l’écran, et non selon leur noyau physique. Voir LOD piloté par le GPU.
Partie 22 sur 29. Précédent : Partie 21 — Un moteur de rendu plus rapide qui ne l’était pas Suivant : Partie 23 — Cinquante avatars et une voix dans la pièce Guide de la série : /fr/blog/2026-02-25-open-world-browser-series-guide