Planète en JavaScript : LOD optimisés grâce aux Web Workers

Développeur web, j’aime aussi la 3D, alors quand on peut lier les deux, j’ai donc souhaité relever le défi de créer une planète entièrement explorable en temps réel, en tirant parti d’une architecture optimisée basée sur des techniques éprouvés telles que le CDLOD, les Web Workers et les shaders personnalisés.
Ce projet repose sur Babylon.js, une librairie JavaScript permettant de créer des expériences 3D interactives directement dans le navigateur. L’objectif principal était de développer une planète échelle réelle (1/1) et explorable en temps réel directement sur un navigateur web.
Floating Origin
Premier problème, quand créé une scène 3D à très grande échelle, comme une planète entière, voir le système solaire à l’échelle réelle pose des problèmes liés aux limites de précision des nombres à virgule flottante (float32).
C’est une limitation bien connue en 3D : pour des raisons de performance, la plupart des moteurs 3D utilisent des coordonnées en float 32 bits. Cependant, cette précision devient insuffisante à grande distance de l’origine.
Pourquoi les float32 deviennent imprécis à grande échelle ?
Les float32 perdent en précision à mesure que les valeurs augmentent. Plus un objet est éloigné de l’origine (0,0,0), plus les écarts entre deux positions adjacentes deviennent grands.
Exemple :
- À 1 000 unités, un float32 peut stocker des écarts de 0.0001
- À 100 000 unités, l’écart passe à 0.01
- Au-delà d’1 000 000 d’unités, un objet censé être à 1 000 000.05 sera arrondi à 1 000 000.1
Et donc des erreurs de calcul se manifestent :
- Perte de précision dans les coordonnées des objets, entraînant des artefacts visuels, tremblements ou des mouvements saccadés souvent appelé « jittering »
- Z-fighting : les surfaces proches les unes des autres commencent à « clignoter » ou à se superposer de manière incorrecte, car le Z-buffer ne peut plus différencier avec précision leur profondeur relative
- Fluctuation des transformations et des collisions, rendant la navigation et l’interaction avec le monde 3D de plus en plus imprécises à mesure que l’on s’éloigne

Afin de pallier ces limitations, il est nécessaire d’utiliser une caméra à origine flottante (Floating Origin). Cette approche consiste à fixer la caméra au point d’origine de la scène et à déplacer les objets autour d’elle, tout simplement !
Autrement dit, plutôt que de déplacer la caméra dans un espace infiniment grand, celle-ci reste fixe au centre de la scène et c’est l’environnement qui se déplace autour d’elle. En maintenant la scène proche de l’origine des coordonnées (0,0,0), je réduis drastiquement les effets d’imprécision des flottants et améliore la stabilité du rendu.
Et la planète ?
Et bien, la planète est créée à partir d’un cube ! Plus précisément, une Quadsphère, qui repose sur une projection cubique, donc les 6 faces d’un cube pour former une sphère.

En fait il nous faut deux prérequis :
- Un maillage 3D le plus uniforme possible afin d’éviter les problèmes de distorsion géométrique, qui pourraient entraîner des zones surdensifiées en triangles et d’autres sous-échantillonnées, rendant l’affichage irrégulier et impactant les calculs de shading et de collisions.
- Un maillage quadrangulaire, parce que les carrés c’est le bonheur et essentiel pour permettre une subdivision hiérarchique efficace, mais, on va voir ça un peu plus tard.

La Quadsphère répond parfaitement à ces contraintes : elle garantit une répartition homogène des sommets sur toute la surface et permet une subdivision progressive naturelle, idéale pour une planète explorée à différentes altitudes.
Il existe plusieurs manières de générer une sphère, notamment l’icosphère, construite à partir d’un icosaèdre subdivisé, elle offre un maillage en triangles équilatéraux, ce qui garantit une répartition parfaitement homogène des vertex, mais rend la gestion des LOD plus complexe et surtout pose un problème pour le normal mapping car elle ne permet pas d’obtenir un champ de tangentes et bitangentes continu sur toute la surface. En raison du théorème du Hairy Ball, il y aura toujours des discontinuités dans ces vecteurs, ce qui perturbe l’interpolation dans les shaders et crée des artefacts visuels, surtout sur les reliefs comme les montagnes et les cratères.
Gestion des niveaux de détail
Nous avons notre planète, mais pour garantir un rendu détaillé au sol sans surcharger inutilement le GPU en altitude ou dans l’espace, nous devons ajuster dynamiquement le nombre de triangles affichés. Deux solutions permettent de gérer cela : le Quadtree, qui structure le terrain en subdivisions hiérarchiques, et le CDLOD, qui assure une transition fluide entre les différents niveaux de détail.
Quadtree : structure hiérarchique pour le terrain

Un Quadtree divise chaque face de la Quadsphère en quatre sous-régions (quads) de manière récursive. Plus la caméra s’approche du sol, plus ces régions sont subdivisées, permettant d’afficher uniquement les parties du terrain nécessaires. Cette approche optimise les performances en réduisant le nombre de draw calls et l’utilisation mémoire, tout en garantissant un niveau de détail adapté à la distance d’observation.
Continuous Distance-Dependent Level of Detail (CDLOD)
Le CDLOD (Continuous Distance-Dependent Level of Detail) fonctionne en superposition avec le Quadtree, c’est une méthode avancée qui permet d’adapter dynamiquement la résolution du terrain en fonction de la distance de la caméra, tout en évitant les artefacts de transition entre niveaux de détail.
Intégration avec le Quadtree
Le CDLOD repose notre structure Quadtree, où chaque niveau de l’arbre représente un LOD différent qu’on va appeler chunk :
- Les chunks supérieurs couvrent de grandes zones avec peu de détails
- Les chunks inférieurs subdivisent ces zones en affichant plus de détails là où c’est nécessaire
Chaque chunks du Quadtree correspond à une grille de maillage unique. Lors du rendu, les données de hauteur sont récupérées dans le vertex shader, permettant d’adapter dynamiquement la topographie du terrain.
Sélection dynamique des nœuds à afficher
Avant chaque trame, le moteur sélectionne les nœuds à afficher en fonction de leur distance avec la caméra :
- Les petits chunks sont utilisés pour les niveaux de détail élevés à proximité de la camera
- Les chunks plus grands sont privilégiés pour les zones lointaines, réduisant le nombre de triangles
En combinant Quadtree et CDLOD, on a un terrain dynamique qui affiche des détails fins de près tout en conservant un nombre limité de triangles à grande distance. Cette approche garantit un rendu détaillé là où c’est nécessaire, sans impacter les performances.

Génération des chunks avec les Web Workers
L’un des défis majeurs de ce projet est la génération en temps réel des chunks, tout en maintenant des performances fluides. Pour éviter que ces calculs lourds ne bloquent le rendu, j’ai exploité les Web Workers, un mécanisme permettant d’exécuter des tâches en parallèle du thread principal dans les navigateurs web.
Mais, qu’est-ce qu’un Web Worker ?
Un Web Worker est un processus qui tourne en arrière-plan, séparé du thread principal d’exécution. Il permet d’effectuer des calculs intensifs sans ralentir l’interface utilisateur. Bien qu’ils soient principalement associés à JavaScript et aux navigateurs web, des mécanismes similaires existent dans d’autres langages sous forme de threads, processus parallèles ou workers dédiés (comme dans Python, Rust ou C++).
Pourquoi les utiliser pour la génération des chunks ?
Dans notre projet, la planète est divisée en chunks via un Quadtree. Chaque chunk nécessite de générer ses vertex, normales et indices, un calcul coûteux en ressources, surtout à des niveaux de détail élevés.
Au départ, avec un quadtree de 6 à 7 niveaux, ces calculs étaient exécutés sur le thread principal, provoquant des ralentissements notables. En déléguant ces tâches aux Web Workers, j’ai pu :
- Décharger le thread principal, ainsi plus de ralentissements
- Accélérer la génération des terrains, en répartissant les calculs sur plusieurs threads
- Atteindre 9 à 11 niveaux de détail, sans impact critique sur les performances
Plusieurs workers grâce au Pool de Web Workers
Plutôt que de créer un worker à chaque requête de génération de chunk, j’ai mis en place un pool de Web Workers. Avec navigator.hardwareConcurrency qui retourne le nombre de cœurs logiques du CPU, le pool maintient un nombre workers pré-alloués, qui sont réutilisés au lieu d’être créés et détruits dynamiquement.
De plus, j’utilise des Blobs pour charger les workers dynamiquement, plutôt que d’importer un fichier externe .js
. Cette approche permet de réduit les requêtes HTTP.
const workerCode = `
self.onmessage = function(event) {
// Worker code ...
self.postMessage({ result: 'Done' });
};
`;
const blob = new Blob([workerCode], { type: "application/javascript" });
const worker = new Worker(URL.createObjectURL(blob));
Après plusieurs tests, la charge CPU oscille autour de 20-25%, contre 7-8% auparavant, ce qui garantit une meilleure exploitation des ressources sans saturer le système.

Dépot Gihub
Demo en ligne
Quelques Pistes d’améliorations
Bien que le projet soit déjà optimisé, plusieurs améliorations permettraient d’en accroître encore les performances.
L’un des principaux goulots d’étranglement reste le nombre de draw calls. Réduire ces appels de rendu en regroupant les meshes des chunks adjacents et en améliorant le frustum culling limiterait la charge GPU et fluidifierait l’affichage.
Précalculer les chunks au chargement de la scène. En générant une zone initiale autour de la camera (ex: un rayon de 3-5 niveaux de LOD), on éviterait les calculs coûteux en temps réel. On pourrait s’appuyer sur un pool de chunks pour réutiliser efficacement les ressources. Associée à un streaming progressif, cette approche permettrait de charger en arrière-plan les zones plus éloignées avant qu’elles ne soient visibles.
On pourrait aussi externaliser la génération des chunks sur un serveur ce qui allégerait encore plus la charge CPU et mémoire du client, surtout sur les appareils moins puissants.
Mais surtout l’utilisation de WebGPU pour la génération et le rendu des chunks permettrait d’exploiter les compute shaders pour générer directement les vertex, indices, normales et UVs sur le GPU, réduisant ainsi la charge CPU et accélérant le rendu.
La vraie évolution
Une approche prometteuse et beaucoup plus moderne pour améliorer encore davantage la gestion du LOD et du rendu temps réel est l’utilisation des Concurrent Binary Trees (CBT), comme décrite dans Concurrent Binary Trees for Large-Scale Game Components par Anis Benyoub et Jonathan Dupuy. Contrairement aux structures de données traditionnelles comme le Quadtree, le CBT est entièrement géré sur GPU, permettant une triangulation adaptative et dynamique en fonction de la distance et du frustum de la caméra. Ce système utilise un algorithme de subdivision binaire efficace qui ajuste en temps réel le nombre de triangles affichés, réduisant ainsi les goulots d’étranglement CPU/GPU.
Conclusion
Cependant, bien que cette méthode éprouvée soit tout à fait viable, la puissance des GPUs modernes ouvre la voie à des solutions encore plus efficaces. L’utilisation des Concurrent Binary Trees (CBT) permettrait d’exploiter pleinement les capacités du GPU pour gérer dynamiquement le LOD et la subdivision du terrain, réduisant ainsi la charge CPU et améliorant la fluidité du rendu. En combinant le CBT avec WebGPU et les Compute Shaders, il serait possible de générer, stocker et afficher des planètes entières avec un niveau de détail extrême, tout en optimisant l’utilisation de la mémoire et des performances en temps réel.