Skip to content

Construire un monde ouvert dans le navigateur, partie 14 : le monde prend vie

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.

Après la partie 13, nous pouvions sculpter le terrain. Le surélever, creuser des grottes, lisser des falaises, le tout en temps réel et sans rompre les raccords. Mais le monde ressemblait encore à une démo technique. Ombrage plat aux couleurs de débogage, superpositions en fil de fer, géométrie grise avec des couleurs de LOD pour distinguer les chunks. On pouvait le modifier. On ne pouvait pas encore le ressentir.

Trois spikes ont changé cela. Pas l'architecture. Exactement le même pipeline, les mêmes buffers et les mêmes raccords de jointures que dans la partie 13. Nous avons simplement ajouté les couches qui donnent au terrain l'impression d'être un véritable lieu : des surfaces qui réagissent à leur forme, de la vie qui pousse sur ces surfaces et un corps qui les parcourt.

La différence entre « techniquement fonctionnel » et « j'ai envie de rester ici » s'est révélée étonnamment ténue.

Sculptez une falaise et regardez-la se changer en roche

Le spike 28 posait une question précise : un matériau à quatre couches avec projection triplanaire peut-il fonctionner sur un terrain généré par calcul sans faire exploser le budget d'une frame ? La réponse était oui, mais le plus intéressant est ce qui s'est produit ensuite.

Ouvrir le spike 28 dans un nouvel onglet ↗ · Voir le code source

Quatre textures procédurales (herbe, roche, sable et neige) générées à partir d'un bruit FBM au démarrage. Aucun fichier externe, aucun pipeline d'assets, seulement des mathématiques et une DataTexture. Les poids des matériaux proviennent de la surface elle-même : sa pente et son altitude. Les zones planes situées sous la limite des arbres reçoivent de l'herbe. Les faces abruptes reçoivent de la roche. Les terrains bas reçoivent du sable. Les sommets élevés reçoivent de la neige. Chaque poids brut provient d'un smoothstep appliqué à la pente et à l'altitude, puis les poids sont normalisés pour chaque fragment afin que iwi=1 et que la couleur finale soit simplement le mélange pondéré iwici. Cette normalisation empêche la surface de devenir trop sombre ou surexposée là où les biomes se chevauchent. La projection triplanaire se charge de la projection UV pour les chunks MC, dont les triangles ne possèdent pas de coordonnées UV pertinentes.

Le moment décisif : surélevez le terrain avec la brosse pour créer une falaise abrupte, et une texture rocheuse apparaît sur la nouvelle face au cours de la même frame. Aplanissez-la de nouveau, et l'herbe reconquiert la surface. Le matériau ne sait rien de la brosse. Il lit simplement la position dans le monde et la normale de la surface, les mêmes données qui ont servi à construire la géométrie. La boucle de rétroaction entre sculpture et rendu visuel est instantanée et non scriptée.

Il y a un mois et demi, le spike 8 avait testé le coût des matériaux de terrain avec un maillage statique sous WebGL. Le spike 28 prouve que cela fonctionne sur un terrain dynamique généré par calcul, avec tout le pipeline de sculpture en dessous. Nous avions prévu le budget nécessaire, mais le voir tenir les 60 fps avec tout le reste en fonctionnement a tout de même été un soulagement.

80 000 touffes d'herbe et le plafond d'une grotte

Le spike 29 est celui où nos instincts de développeurs de jeux ont pris le dessus.

Ouvrir le spike 29 dans un nouvel onglet ↗ · Voir le code source

Nous voulions l'herbe de Breath of the Wild. Pas son nombre de polygones, mais ses sensations. Une herbe qui couvre les bons endroits, bouge avec le vent et donne envie de courir dedans.

Chaque touffe d'herbe se compose de trois quadrilatères entrecroisés à des angles de 60 degrés, avec quatre segments verticaux pour permettre la flexion. Cette forme en croix paraît volumétrique sous tous les angles, sans astuces de billboard. Un InstancedMesh par chunk, 80 000 touffes réparties dans le monde, soit environ 2,4 millions de sommets d'herbe au total.

Le placement est la partie essentielle. Le CPU parcourt une grille légèrement irrégulière sur chaque chunk et évalue la même logique de pente et d'altitude que celle utilisée par le shader de matériau sur le GPU. Là où le système de matériaux indique « herbe », l'herbe pousse. Là où la roche ou le sable domine, la densité tombe à zéro. La transition est progressive, car les poids smoothstep sous-jacents produisent des gradients continus aux frontières des biomes. On ne remarque aucune limite, puisqu'il n'y en a pas.

Nous avons ensuite tenté quelque chose dont je n'étais pas certain. De l'herbe sur des surfaces SDF. La fonction de dispersion parcourt chaque colonne du volume SDF et recherche les passages par zéro entre des voxels adjacents. Là où la SDF passe d'une valeur négative à une valeur positive, il existe une surface. La normale de cette surface est simplement le gradient normalisé du champ, n^=ϕϕ, calculé à partir des différences entre voxels, et la pente vaut arccos(n^y^), soit l'angle par rapport à la verticale. Si cet angle est suffisamment faible, on place de l'herbe.

Creusez une grotte avec la brosse SDF dans le spike 27. Revenez dans le spike 29 et vous trouverez de l'herbe qui pousse au-dessus du plafond de la grotte. Le code de dispersion ne sait pas ce qu'est une grotte. Il voit simplement une surface présentant la bonne pente à la bonne altitude. C'est ce type de comportement émergent qui rend les systèmes de mondes ouverts si plaisants à construire.

Le vent est une onde sinusoïdale dans le shader de sommets TSL, modulée par la coordonnée V du brin afin que les pointes oscillent tandis que les racines restent ancrées. Appuyez sur V pour alterner entre vent léger, vent fort et absence de vent. Le vent agit simultanément sur les 80 000 touffes sans aucun coût CPU, car tout se passe dans le shader de sommets.

Le spike 7 avait testé 50 000 instances d'herbe et nous craignions d'atteindre la limite. Le spike 29 en fait tourner 80 000 par-dessus le terrain calculé, les raccords de jointures, le texturage multimatériau et les brosses. Regrouper les instances dans un nombre réduit d'appels de rendu InstancedMesh reste plus important que de réduire le nombre de sommets par brin. La leçon du spike 7 s'est confirmée.

Un point que nous avons remis à plus tard : l'herbe ne se met pas à jour lorsque vous sculptez le terrain sous elle. Les matrices d'instance sont définies au moment de la dispersion. Transformez une colline en vallée et l'herbe flottera dans les airs jusqu'à ce que vous appuyiez sur G pour relancer la dispersion. C'est suffisant pour un spike. En production, il faudra relancer la dispersion sur les chunks modifiés.

Le premier pas

Le spike 30 est celui auquel je reviens sans cesse.

Ouvrir le spike 30 dans un nouvel onglet ↗ · Voir le code source

Aucune bibliothèque de physique. Une capsule personnalisée avec un pas de temps fixe à 120 Hz. Le code de production utilise Rapier dans un web worker (le spike 2 a prouvé que la latence convenait), mais ce spike devait valider les requêtes de collision elles-mêmes. Un personnage peut-il parcourir un terrain à heightmap, monter sur un terrain SDF sculpté et ne pas passer au travers ?

Le cœur du système est une fonction appelée terrainQuery(x, y, z). Elle vérifie si la position se trouve dans un chunk verrouillé en MC. Si c'est le cas, elle interpole de façon trilinéaire la copie CPU de la SDF et renvoie le gradient comme normale de surface. Sinon, elle effectue une lecture de la heightmap avec une normale calculée par différences centrales. Le personnage ne sait pas sur quel système de terrain il se tient. Il demande simplement où se trouve le sol et obtient une réponse.

Les collisions SDF étaient la partie que je m'attendais à trouver difficile. Sept points de sondage autour de la capsule (bas, centre, haut et quatre décalages cardinaux). Un point de sondage à la position x est en pénétration chaque fois que ϕ(x)<r, avec une profondeur de pénétration p=rϕ(x), et la direction de repoussement est le gradient du champ n^=ϕϕ. Pour empêcher la capsule de rentrer de nouveau dans la surface, la vitesse est projetée afin d'annuler sa composante entrante :

v=v(vn^)n^

Il ne reste ainsi que la composante qui glisse le long de la surface. Ce n'est pas un moteur physique. Ce sont des requêtes géométriques et une réponse simple. Mais cela gère les grottes, les surplombs et les tunnels sculptés sans aucun code spécifique à chaque forme. Vous entrez dans une grotte creusée dix secondes plus tôt et la capsule suit exactement le contour du plafond.

Le modèle de déplacement a commencé de manière pragmatique, puis il est devenu amusant. Marche, course, sprint, saut, glissade sur les pentes de plus de 45 degrés. J'ai ensuite ajouté un parapente façon BotW et le spike est devenu quelque chose que je n'avais plus envie de fermer.

Appuyez sur Espace lorsque vous êtes en l'air. La gravité passe de -30 à -4. La vitesse de chute est plafonnée à -3. Une aile delta se déploie depuis la capsule avec une interpolation d'échelle. Virez à gauche et l'aile s'incline. L'orientation de la capsule s'aligne automatiquement sur le vecteur de vitesse, afin que vous regardiez toujours dans la direction de votre déplacement. Relâchez Espace pour tomber. Touchez le sol et vous repartez en courant.

La caméra recule pour passer à la troisième personne (touche P). Elle suit le joueur par-derrière avec un lissage du lacet. Un ray marching du joueur vers la caméra teste le terrain en 20 étapes. Entrez en vol dans une grotte et le bras de caméra se raccourcit progressivement au lieu de traverser la roche. Ressortez de l'autre côté et il s'allonge de nouveau.

Voici le moment qui a rendu ce spike si précieux : sculptez une haute falaise avec la brosse de heightmap. Passez à la caméra à la troisième personne. Courez jusqu'au bord. Sautez. Déployez le parapente. Virez à gauche au-dessus du terrain que vous venez de sculpter, tandis que l'herbe cultivée dans le spike 29 ondule en dessous, que la texture rocheuse du spike 28 couvre la falaise et que les raccords Transvoxel tiennent à chaque limite de chunk. Atterrissez de l'autre côté. Tout fonctionne ensemble dans un seul onglet de navigateur.

Sculpter sous ses propres pieds

Une chose me laissait dans le doute : que se passe-t-il lorsque vous sculptez le terrain sur lequel se tient le personnage ? Les modifications de la heightmap se propagent instantanément dans terrainQuery(), car la fonction lit directement le buffer CPU. Les modifications SDF se propagent par l'intermédiaire de la copie CPU de la SDF. La capsule résout la pénétration au tick de physique suivant, ce qui, à 120 Hz, se produit en moins de 11208.3 ms.

Cela fonctionne, tout simplement. Surélevez le sol sous le joueur et il monte avec lui. Creusez le sol et il tombe. Aucun traitement particulier. La physique s'exécute assez rapidement pour que les modifications du terrain sur une seule frame ne produisent jamais de pénétrations importantes. Ce fut un heureux hasard lié au choix d'un pas de temps à 120 Hz. Nous l'avions retenu pour la fluidité des déplacements, et il a également rendu l'édition du terrain sûre sans effort supplémentaire.

30 spikes plus tard

Nous avons commencé cette série avec une heightmap plane et 500 cubes. Nous disposons maintenant d'un terrain sculpté avec des grottes volumétriques, des raccords de jointures Transvoxel, un texturage multimatériau qui réagit à la forme de la surface, 80 000 touffes d'herbe ondulant dans le vent et un personnage qui marche, sprinte, saute et plane au-dessus de l'ensemble.

Rien de tout cela n'est encore en production. Le code de world/client/ fonctionne toujours sous WebGL avec de simples chunks de heightmap. Tout ce qui provient de ces 30 spikes réside dans des pages HTML autonomes. L'intégration vient ensuite : migration vers WebGPURenderer, connexion de la stratégie hybride HM/MC au gestionnaire de chunks et raccordement des systèmes de brosses et de matériaux au multijoueur.

Mais le système de rendu ne constitue plus le risque principal. Les questions ouvertes concernent désormais les flux de données : persistance des modifications, synchronisation réseau des coups de brosse, sculpture collaborative. Ce qui se passe entre les joueurs, pas entre les triangles.

Si vous suivez cette série depuis la partie 1, merci d'être resté malgré les passages difficiles. Si vous venez de la découvrir, revenez à la partie 1. C'est dans les fausses pistes que se cachent les leçons.

Technologies abordées dans ce chapitre

TSL (Three Shading Language). Le système de shaders basé sur des nœuds de Three.js pour le moteur de rendu WebGPU. Les matériaux sont composés à partir de nœuds (positionWorld, normalWorld, smoothstep, triplanarTexture) au moyen de la composition de fonctions en JavaScript. Le graphe de shaders est compilé en WGSL à l'exécution. Pour les cibles WebGPU, TSL remplace le ShaderMaterial GLSL brut et assure l'interopérabilité entre les fonctionnalités standard des matériaux Three.js (éclairages, ombres, brouillard) et une logique personnalisée par fragment.

Projection triplanaire. Une technique de projection de texture qui échantillonne une texture trois fois (plans XY, XZ et YZ) puis mélange les résultats en fonction de la direction de la normale de surface. Cela élimine l'étirement des UV sur une géométrie de maillage arbitraire, ce qui est essentiel pour les résultats de marching cubes dont les triangles ne possèdent pas de coordonnées UV pertinentes. TSL fournit triplanarTexture() comme nœud intégré.

Végétation instanciée avec des brins en quadrilatères croisés. Chaque touffe d'herbe est constituée de trois quadrilatères entrecroisés à des angles de 60 degrés, ce qui crée une apparence volumétrique sous tous les angles. Quatre segments verticaux par quadrilatère permettent une flexion fluide pour l'animation du vent. L'ensemble du champ est rendu sous la forme d'un seul InstancedMesh par chunk. La capacité est surallouée de 25 % afin que de nouvelles instances puissent occuper les emplacements réservés lorsque le terrain est modifié, sans réallouer le buffer GPU. Consultez notre guide de génération de paysages consacré à la végétation. Collision capsule/SDF. Collision du personnage avec un terrain volumétrique sans moteur physique. La capsule est testée en plusieurs points par rapport au SDF. Lorsque la valeur du champ est inférieure au rayon de la capsule, le gradient fournit la normale orientée vers l’extérieur et la différence donne la profondeur de pénétration. Cette méthode gère les grottes, les surplombs et les tunnels sans code propre à chaque forme. Consultez les collisions avec un terrain SDF.

Contrôleur de personnage à pas de temps fixe. La simulation physique s’exécute à 120 Hz, quelle que soit la fréquence d’images, en accumulant le temps réel puis en le consommant par pas de taille fixe. Un nombre maximal de sous-pas évite la spirale de la mort lorsque les images sont lentes. L’adhérence au sol maintient la capsule en contact avec celui-ci lors du déplacement sur les pentes. Le pas de temps fixe garantit un comportement déterministe pour les futures rediffusions multijoueurs.


Partie 14 sur 29. Précédent : Partie 13 – La sculpture du terrain et la mort de la fonction mathématique Suivant : Partie 15 – Remplacer la référence, puis la synchroniser Guide de la série : /fr/blog/2026-02-25-open-world-browser-series-guide