100K entités à 60 FPS : WebGPU compute shaders en JavaScript
Comment j'ai simulé 100 000 entités autonomes à 60 FPS dans un navigateur grâce aux compute shaders WebGPU, au spatial hashing GPU, et à une architecture Structure-of-Arrays zero-GC.
Cet article décortique le projetPoissonLe défi
Simuler un écosystème aquatique avec 100 000+ entités autonomes. Chaque poisson doit : se déplacer (flocking), éviter les obstacles, chasser ou fuir, se reproduire, muter génétiquement. Le tout à 60 FPS. Dans un navigateur.
Avec JavaScript pur et le CPU, j'atteignais 2 000 entités avant de tomber sous 30 FPS. Il fallait une approche radicalement différente.
L'architecture SoA (Structure-of-Arrays)
Premier changement fondamental : abandonner les objets JavaScript.
// ❌ Array of Structures (classique JS)
const fish = [{ x: 10, y: 20, vx: 1, vy: 0, health: 100 }, ...]
// ✅ Structure of Arrays (GPU-friendly)
const positions = new Float32Array(MAX_ENTITIES * 2); // [x0,y0, x1,y1, ...]
const velocities = new Float32Array(MAX_ENTITIES * 2);
const health = new Float32Array(MAX_ENTITIES);
Pourquoi ? Les GPU traitent les données en parallèle par blocs contigus. Un SoA permet de transférer positions seul au GPU sans embarquer les données inutiles. Bonus : zero garbage collection puisque les typed arrays sont pré-alloués.
Le pipeline GPU en 8 passes
Chaque frame exécute 8 compute shaders séquentiels :
- Biologie — métabolisme, faim, vieillissement
- Grid Clear — reset du spatial hash grid
- Grid Bin — binning atomique des entités dans les cellules
- Prefix Sum — Blelloch parallel prefix sum pour les offsets
- Reorder — réordonnancement des entités par cellule pour accès cache-cohérent
- Flocking — séparation, alignement, cohésion (Boids) + forces environnementales
- Physics Integration — drag, speed clamp, position update, edge behavior
- Hunt — détection de proies, poursuite
Le spatial hashing est la clé. Au lieu de vérifier 100K × 100K paires (O(n²)), chaque entité ne regarde que sa cellule et ses 8 voisines. Le Blelloch prefix-sum construit l'index en O(n) sur le GPU.
Les résultats
| Entités | FPS (CPU) | FPS (WebGPU) | Speedup |
|---|---|---|---|
| 1 000 | 60 | 60 | 1x |
| 10 000 | 12 | 60 | 5x |
| 50 000 | 2 | 60 | 30x |
| 100 000 | < 1 | 58-60 | > 60x |
Le fallback automatique détecte les capacités du navigateur : WebGPU → WebGL → Canvas2D. Même en Canvas2D, le SoA et le spatial hashing permettent 5 000 entités à 60 FPS.
Leçons apprises
-
Le GPU change les règles. Les algorithmes optimaux sur CPU (arbres, listes chaînées) sont les pires sur GPU. Préférez les structures plates et les passes parallèles.
-
Pré-allouez tout. Zéro
newen boucle de simulation. Les typed arrays sont vos amis. -
Profilez sur de vrais devices. Mon MacBook Pro faisait tourner 200K entités. Un laptop moyen plafonne à 80K. Le fallback gracieux est essentiel.
Le projet est publié sur npm avec 70 tests automatisés. L'architecture multi-couches permet de réutiliser le moteur pour d'autres types de simulations.
Florian Sola
Lead Technique · Haute performance temps réel · 9 ans d'expérience