Aller au contenu
Accueil

2026, seul, de bout en boutNext.js · React Three Fiber · GLSL · WebAudio

Éclosion

Une expérience web 3D où le défilement est le temps : huit actes, du Néant à l'Aube, traversés à la molette, du premier battement d'une graine jusqu'au premier lever de soleil du monde.

Cent pour cent procédurale.

Aucun modèle 3D, aucune texture, aucun fichier audio, aucune requête externe au runtime. La géométrie, les matériaux (bruit et FBM en GLSL), plus de cent mille particules et la musique elle-même (synthèse WebAudio pure) sont produits par le code. Seule exception assumée : la typographie, committée dans le dépôt.

Environ 11 600 lignes réparties sur 195 fichiers. Next.js 15, React 19, TypeScript strict, Three.js et React Three Fiber, GSAP ScrollTrigger, Lenis, zustand, WebAudio.

L'aube d'Éclosion : le soleil affleure l'horizon au-dessus d'un relief procédural.
L'Aube, dernier acte. Le disque solaire ignore le brouillard pour passer le seuil du bloom.

Décisions techniques

Trois choix qui structurent tout

Le contrat de scroll : zéro render React à 60 images par seconde

Un seul lissage (Lenis), un seul ScrollTrigger maître qui scrube une timeline GSAP de durée 1, et un registre d'uniforms mutables que les tweens écrivent et que le rendu lit à chaque frame, sans jamais passer par un render React. React ne se réveille qu'aux changements d'acte. Le texte DOM suit via un unique MotionValue, hors React lui aussi.

Un seul monde, une seule vérité

Une fonction analytique groundHeight(x, z) génère le terrain visible, contraint la marche en exploration libre, fait rebondir la physique et borne les vols d'oiseaux. Ce qu'on voit est exactement ce qu'on touche. Même logique pour la caméra : une seule courbe CatmullRom continue, chaque acte voyageant jusqu'au premier point du suivant. Aucune coupure sur tout le film.

Des particules sans état

Chaque comportement (pluie, lucioles en curl-noise, braises, vortex du final) est une fonction fermée de (graine, temps) évaluée dans le vertex shader. Pas de simulation à maintenir, pas d'aller-retour GPU. Les buffers sont alloués une fois au niveau de qualité maximal ; changer de palier ne fait que déplacer un setDrawRange.

Mesures

L'ingénierie, chiffrée

Banc GPU intégré forcé au palier maximal, le pire cas mesurable.
MesureAvantAprès
Charge de première visite422 Kio342 Kio
Pire frame aux frontières d'acte617 ms167 ms
Arbres de la forêt, à coût de frame égal420630
Lighthouse : accessibilité, bonnes pratiques, SEO100 / 100 / 100

Les −19 %

La police italique, 80 Ko préchargés en priorité haute, n'était rendue nulle part. Une seule occurrence du mot « italic » dans tout le code : sa propre déclaration.

Les 617 ms

Trois blocages empilés à chaque frontière d'acte, démêlés au profileur CPU et aux hooks GL posés sur linkProgram :

  • le piège de la clé de cache : le post-traitement rend la scène sous son propre état renderer ; tout le préchauffage de shaders compilait donc des programmes que le rendu réel n'utilisait jamais, et la première frame visible relinkait tout ;
  • l'attente de link : un acte se dessinait dès que la caméra le balayait, prêt ou pas ;
  • la réflexion différée du pilote : même linké, un programme paie sa première introspection, environ 180 ms chacun sur GPU intégré.

Réponse : chaque acte monte invisible, compile sous l'état du composer, se fait introspecter un programme par frame, puis se révèle, le tout couvert par le brouillard de la scène. Aucun changement visuel, frontières fluides.

Rien ne se corrige sans être vu.

Un harnais CDP pilote un vrai Chrome : entrée dans l'expérience, saut à un instant précis, capture, avant et après, pour chaque changement de rendu. Les effets liés à la vitesse se capturent en plein défilement, une capture posée ne montre rien. Le palier GPU se force en injectant la chaîne du renderer.

Le soleil qui ne s'embrasait pas

Le disque de l'aube rendait gris. Le brouillard exponentiel en mangeait 99,5 % à cette distance, et son plafond de luminance restait sous le seuil du bloom : « bright enough to bloom », disait le commentaire ; c'était faux depuis toujours. Correctif : le soleil ignore le brouillard (la lune, elle, le ré-émule à l'identique en JavaScript) et une courbe d'embrasement passe le seuil : braise sous l'horizon, or à l'affleurement, crème au zénith.

Les lignes de l'océan

Un même symptôme, « des traits blancs », s'est révélé être cinq défauts empilés, chacun masquant le suivant : ghosting du flou de vitesse, arêtes des fûts de lumière, dôme de ciel nocturne visible sous l'eau, bord de la nappe de Gerstner, plancher trop éclairé. La méthode qui a fini par gagner : masquer les composants un à un pour isoler la source avant de toucher au moindre shader.

Le Néant : des constellations tracées sur un ciel vide.
Le Néant, constellations générées, sans horizon.
L'orage : pluie dense et éclairs au-dessus du relief.
L'Orage, capturé en plein défilement, seul moyen de voir le flou de vitesse.
La forêt : des centaines d'arbres générés en instances.
La Forêt, 630 arbres en cinq InstancedMesh, à coût de frame inchangé.
L'océan : une nappe de Gerstner traversée de fûts de lumière.
L'Océan, nappe de Gerstner et fûts de lumière sous la surface.

Qualité et accessibilité

Tenir la qualité dans le temps

  • Intégration continue à chaque push : lint strict (le type any est interdit), TypeScript strict, 16 tests unitaires (pavage des actes, générateur pseudo-aléatoire seedé, sol analytique, parité des textes FR et EN), build.
  • Un test de bout en bout joue le film entier, démarrage, entrée, chaque acte, l'émergence hors de l'eau, le repli, dans les deux langues, et exige zéro erreur de console. Il a attrapé une vraie course le jour de sa mise en service : une libération de ressources pendant un link de shader.
  • Sans WebGL2 : une page de repli sert le récit complet en HTML rendu côté serveur, sans JavaScript, dans les deux langues.
  • Mouvement réduit : défilement natif, particules à 4 %, aucune secousse, son coupé. Annonces aria-live à chaque acte, parcours clavier complet, focus visible partout.

Suite