Aller au contenu
learn

Journal

  • three 0.186.0, @types/three 0.186.0, Vite 8.3.0, TypeScript 7.0.2, Playwright 1.63.0 et glTF Transform 4.5.0 épinglés dans le package.json et le fichier de verrouillage propres au cours
  • check.sh : la vérification des types, les scripts Node.js, chaque page de leçon sondée dans Chromium headless, le build de production, comparés à expected/
  • CI sur Ubuntu, Windows et macOS
  • Leçon 1 : scène, caméra, moteur de rendu
  • Leçon 2 : géométries, matériaux, lumières et ombres
  • Leçon 3 : couleur, tone mapping et environnements HDR
  • Leçon 4 : modèles glTF et animations
  • Leçon 5 : interaction, Raycaster et contrôles de caméra
  • Leçon 6 : TSL et matériaux à nœuds
  • Leçon 7 : post-traitement avec RenderPipeline
  • Leçon 8 : la performance, mesurée
  • Leçon 9 : React Three Fiber et drei
  • Leçon 10 : physique avec Rapier en WebAssembly
  • Leçon 11 : WebXR, émulé
  • Leçon 12 : tests et CI
  • Leçon 13 : projet, le manche de guitare 3D de GuitarAlchemist
  • Leçon 14 : labo, Guitar Alchemist en 3D, quinze expériences mesurées
  • Cours terminé
  • Démos en direct : chaque page de leçon et cinq scènes complètes, publiées sous le site avec un panneau Try it

three.js r186 et les bibliothèques autour sont le logiciel de quelqu’un d’autre ; ce tableau retient ce que les leçons y ont trouvé, pas ce qu’elles ont trouvé dans les pages du cours. L’essentiel vient du passage à WebGPURenderer, assez récent pour que ce soit souvent l’habitude WebGL qui ait tort. Rien n’a été signalé en amont, sauf là où la colonne État nomme un ticket existant.

Attendu Ce qui se passe Où Mesure État
WebGPURenderer compte les appels de dessin de la scène, comme WebGLRenderer Avec un tone mapping, ou un espace colorimétrique de sortie différent de l’espace de travail, il rend dans une cible puis la dessine dans une passe de sortie three.js r186 WebGPURenderer 2 appels de dessin et 13 triangles pour un cube, contre 1 sur WebGL ; les deux objets info n’ont d’ailleurs pas la même forme Reproduit 2026-09-16
@types/three décrit toJSON() tel que le runtime l’écrit En 0.186.0, Object3DJSON déclare children comme des chaînes et n’a ni geometries ni materials ; le runtime écrit des objets et les deux tableaux @types/three 0.186.0 Les erreurs du compilateur sont conservées dans errors/l01_tojson_types.ts Reproduit. Aucun ticket amont cherché, aucun ouvert 2026-09-16
PCFSoftShadowMap fonctionne sur WebGPURenderer Retiré en r186, et il avertit. R3F le pose pour tout shadows vrai et récolte le même avertissement à chaque rendu de <Canvas> three.js r186, @react-three/fiber 9.7.0 LunarLanderEngine.ts et MinimalThreeInstrument.tsx de GA l’associent encore à WebGPURenderer Reproduit 2026-09-16 · 2026-09-16 · Pas encore dans GA, qui épingle three 0.180.0, où il fonctionne toujours ; 2026-09-24
Une lumière ponctuelle et un spot convertissent les lumens en candelas de la même façon SpotLight.power utilise π au lieu de 4π et ne varie pas avec l’angle du cône three.js SpotLight.power 800 lm donnent 63,66 cd sur une ponctuelle et 254,65 sur un spot Reproduit 2026-09-16
Construire un environnement équirectangulaire coûte à peu près une passe Le PMREM coûte 17 rendus : 1 dans le premier niveau, puis 2 pour chacun des 8 autres, même pour une simple texture équirectangulaire three.js PMREMGenerator 17 rendus pour un environnement de 256 sur 128 Reproduit, par conception 2026-09-16
Le bundler embarque les décodeurs Draco que le navigateur charge Depuis r185, DRACOLoader les résout avec import.meta.url : Vite les copie donc tous les cinq three.js r185+ DRACOLoader, Vite 1,31 Mo embarqués là où un navigateur en charge deux ; le build avertit aussi que three.webgpu dépasse 500 ko Reproduit, comportement changé depuis r184 2026-09-16
La compression garde tous les sommets de la géométrie Draco et meshopt suppriment tous deux les deux sommets polaires que SphereGeometry crée et qu’aucun triangle n’utilise ; le weld() de Draco fusionne en plus les coins du corps non indexé Draco, meshopt, glTF Transform 4.5.0 Les comptes de sommets avant et après, dans expected/ Reproduit, défendable 2026-09-16
Le <Text> de drei se dessine sur WebGPURenderer comme sur WebGL Il dessine un quad vide : troika-three-text injecte son shader par onBeforeCompile, que le renderer à nœuds n’appelle jamais drei 10.7.8 Quad vide Reproduit. Reste ouvert : l’existence d’un troika-three-text prêt pour WebGPU 2026-09-16
setColorAt colore les instances quoi qu’ait fait le premier rendu Un InstancedMesh l’ignore si son premier rendu n’avait pas de couleurs d’instance three.js InstancedMesh Les marqueurs du manche sont restés blancs jusqu’à ce que l’attribut soit alloué d’avance Reproduit, contourné 2026-09-16
WebXR sur WebGPURenderer se comporte comme sur WebGLRenderer Il lui faut XRGPUBinding et la fonctionnalité webgpu ; son message d’erreur nomme un VRButtonGPU qui n’existe pas ; WebGLXRFallback échange les renderers mais les contrôleurs pris au premier cessent de recevoir des événements three.js r186, WebXR sur WebGPU Seul le WebGLRenderer classique a dessiné les deux yeux sous IWER Reproduit 2026-09-16
InstancedMesh fonctionne avec des morph targets Il échoue avec une seule instance, morphTargetInfluences n’étant jamais créé, et autrement avec plusieurs, dans UniformArrayNode.updateBuffer. Il lui faut aussi geometry.morphTargetsRelative = true, sans le dire three.js r186 Deux échecs distincts, un par nombre d’instances Reproduit 2026-09-17
BatchedMesh regroupe en peu d’appels de dessin sur tous les backends Sur WebGPU, il dessine un appel par objet three.js r186 BatchedMesh, backend WebGPU 44 appels pour un manche contre 3 sur WebGL 2, qui utilise WEBGL_multi_draw Reproduit 2026-09-17
renderer.info.memory.texturesSize compte les textures compressées Il compte 0 octet pour les textures KTX2, et compte les cibles de rendu à la place three.js r186 renderer.info.memory 0 octet pour le jeu de bois compressé, ce qui a empêché de finir l’expérience 6 Reproduit. Reste ouvert : mesurer la mémoire GPU KTX2 avec un outil GPU 2026-09-17
powerPreference choisit un GPU Chromium sous Windows l’ignore et avertit ; les autres OS non Chromium 153 check.sh filtre la ligne Connu en amont, ticket Chromium 369219127 2026-09-16
L’addon RapierPhysics de three.js suit une version courante de Rapier Celui de r186 charge encore 0.17.3 depuis skypack.dev, alors que le cours utilise 0.20.0 three.js r186 examples/jsm/physics/RapierPhysics Deux versions mineures d’écart Reproduit, le cours n’utilise pas l’addon 2026-09-16
Le build WebGPU de R3F n’embarque que le point d’entrée WebGPU Il importe three à côté de three/webgpu, donc le cœur WebGL est embarqué aussi @react-three/fiber 9.7.0 565 ko gzip contre 210 ko pour la même scène sans React Reproduit. Reste ouvert : aliaser three vers three/webgpu 2026-09-17

La leçon 14 a mené quinze expériences dans le labo 3D Guitar Alchemist. Chaque hypothèse et ses prédictions chiffrées ont été versées le 16 septembre, la veille des mesures : c’est ce qui permet à une ligne ci-dessous de dire réfutée. Sur les 61 prédictions, 19 étaient fausses en tout ou partie et 2 n’ont pas pu être mesurées. Mesuré sur une RTX 5080 sous Chromium 153, temps GPU par requêtes d’horodatage WebGPU. Le détail est dans la leçon 14.

Question Hypothèse Résultat Verdict
Quelle façon de dessiner 16 manches de guitare coûte le moins : celle de GA, instanciée, batchée ou fusionnée ? Les appels de dessin dictent le temps CPU ; le GPU bouge à peine de l’une à l’autre La version batchée sur WebGPU a tiré 44 appels, et non les 4 ou moins prédits. Les 16 manches de GA coûtent 2,9 ms de CPU, et non les 10 ms ou plus prédites Majoritairement réfutée
Réécrire les sommets d’une corde vibrante sur le CPU contre le faire en TSL, jusqu’à 600 cordes À 600 cordes, la réécriture CPU coûte davantage, et le coût GPU est comparable CPU 1,8 ms contre TSL 0,36 ms, un facteur 5, comme prédit. Mais TSL a coûté 1,7 fois le temps GPU de la version CPU Confirmée côté CPU, réfutée côté GPU
Désigner une note parmi 10 000 : Raycaster contre une passe d’identifiants sur GPU La sélection GPU l’emporte à 10 000 objets Raycaster 0,465 ms contre 3,395 ms pour la sélection GPU Réfutée
Transposer 49 152 bracelets de classes de hauteurs, sur CPU contre TSL Le CPU paie la reconstruction de 49 152 matrices par image : 4 ms ou plus CPU 2,455 ms, en dessous de la prédiction. TSL 0,29 ms, comme prédit Confirmée pour TSL, réfutée pour le CPU
Un nuage de points OPTIC-K jusqu’à 1 000 000 de points, et si le calcul TSL exige WebGPU Les points restent bon marché jusqu’au million ; le compute shader ne tourne pas sur WebGL 2 Les points ont tenu. Le compute shader a bien tourné sur WebGL 2, par transform feedback, à 2,8 ms GPU Confirmée pour les points, réfutée pour le backend
Ce que coûtent les textures de bois non compressées, et ce que KTX2 économise RGBA8 coûte environ 22 Mo ; KTX2 en BC7 en coûte environ le quart 21,33 Mo confirmés. Le chiffre KTX2 n’a pas pu être lu : renderer.info compte 0 pour les textures compressées Moitié confirmée, moitié non mesurable
Une grille de matériaux PBR : combien de programmes de shader, et ce qu’ajoute MeshPhysicalMaterial Un programme par type de matériau, partagé sur la grille ; le matériau physique coûte plus de temps GPU 16 programmes dans les deux cas, comme prédit. Le matériau physique a coûté 1,46 fois le temps GPU Confirmée
Ce que coûte une shadow map dynamique face à une cuite Une shadow map dynamique double à peu près les appels de dessin 13 appels contre 8, un facteur 1,6. La cuisson a pris 398 ms, comme prédit Confirmée en gros
Ce que coûte le bloom en 1080p Entre 0,5 et 1,5 ms GPU 0,135 ms, quatre fois sous la prédiction la plus basse Réfutée
Un mur de 1 000 guitares avec LOD et frustum culling Le LOD divise les triangles par environ 10 Par 11,2 Confirmée
Si le CCD de Rapier empêche un médiator cinématique de traverser une corde Le CCD empêche la traversée Le médiator a traversé à 30 comme à 60 pas par seconde, CCD activé ou non Réfutée
Si un manche est à l’échelle 1:1 en WebXR, sous l’émulation IWER Du sillet à la douzième case, on mesure 0,324 m 0,324 m Confirmée
De combien il faut programmer une note à l’avance pour que WebAudio et l’image s’accordent L’avance naïve vaut à peu près la somme des latences, 50 ms, à une image près 31,8 ms, soit 18 ms de moins que la somme Réfutée
Ce que coûte React Three Fiber face à three.js nu, pour la même scène Peu cher par image, cher au démarrage (+30 à 150 ms) et dans le bundle (+50 à 90 ko) Peu cher par image, comme prédit. Première image +430 ms et bundle +195 ko, tous deux très au-dessus Confirmée par image, réfutée sur les deux coûts
Si un ratio de pixels adaptatif se stabilise sur un GPU faible Le temps GPU est proportionnel au nombre de pixels, et le régulateur se stabilise en moins de 60 images Il s’est stabilisé à 0,655 en moins de 60 images Confirmée
  • npm view three donne 0.186.0, publiée le 8 septembre 2026, et @types/three 0.186.0. Le cours les épingle exactement dans code/threejs/package.json, avec Vite 8.3.0, TypeScript 7.0.2, Playwright 1.63.0 et son Chromium 153, glTF Transform 4.5.0, draco3dgltf 1.5.7, meshoptimizer 1.2.0 et pngjs 7.0.0, sur Node.js 24.21.0 en CI.
  • La release r186 correspond à la version npm 0.186.0. Les leçons citent le code source au tag r186 de mrdoob/three.js.
  • require('three/package.json') échoue : la table exports du paquet ne le liste pas. La vérification de version dans check.sh lit donc le fichier avec fs.
  • Les pages de documentation sont à https://threejs.org/docs/pages/<Class>.html et https://threejs.org/manual/pages/<page>.html ; les leçons pointent vers ces URL, toutes vérifiées.

2026-09-16 — Mesurer des pages dans un navigateur headless

Section intitulée « 2026-09-16 — Mesurer des pages dans un navigateur headless »
  • Chaque page de leçon rapporte ce qu’a fait le moteur de rendu à travers une promesse window.probe, et scripts/probe.mjs démarre Vite, ouvre la page avec Playwright en 800 par 450 pixels avec un pixel ratio de 1, et affiche le rapport. Les pages animées avancent par pas fixes quand elles sont sondées, si bien que la troisième image est la même sur toutes les machines.
  • Ma première sonde restait bloquée sur la page WebGLRenderer : la page remplaçait window.probe après que page.evaluate avait commencé à attendre l’ancienne promesse. Les pages résolvent désormais une promesse créée une seule fois, dans src/probe.ts, et probe.mjs abandonne au bout de 30 secondes.
  • Les couleurs sont lues dans la capture d’écran, aux positions que la page indique.
  • Le mode headless par défaut de Playwright, sous Windows, donne WebGPU sur la RTX 5080 de l’auteur (« nvidia, blackwell »). chromium-headless-shell, la version allégée, n’a pas d’adaptateur WebGPU : WebGPURenderer se replie sur WebGL 2, avec SwiftShader. check.sh exécute la leçon 1 dans les deux.

2026-09-16 — Ce avec quoi les runners de CI dessinent

Section intitulée « 2026-09-16 — Ce avec quoi les runners de CI dessinent »
  • La première exécution de la CI, 35114590607, a échoué sur les trois OS, pour trois raisons : le coordinateSystem rapporté par la page dépend du backend ; quelques couleurs diffèrent d’une unité par rapport au GPU de l’auteur ; et sous macOS, l’avertissement de Vite, sur stderr, arrivait avant ses lignes sur stdout. Les sondes filtrent maintenant les lignes qui dépendent du backend, compare.mjs accepte un écart de 2 par canal dans les couleurs, et le stderr du build est affiché après son stdout.
  • L’exécution 35115118564 est passée. Ses logs montrent avec quoi dessine chaque runner. ubuntu-24.04 et windows-2025-vs2026 n’ont pas de GPU : WebGPU n’est pas disponible, et WebGPURenderer comme forceWebGL tournent en WebGL 2 sur SwiftShader (Subzero). macos-26-arm64 a WebGPU, sur un adaptateur « apple », et WebGL 2 sur « ANGLE Metal Renderer: Apple Paravirtual device » ; son headless shell utilise SwiftShader (LLVM 10.0.0).
  • Deux couleurs diffèrent d’une unité sur les runners : ACES transforme le linéaire 0.18 en 127 sur le GPU de l’auteur et en 128 en CI, et Neutral transforme 0.05 en 33 et en 34.
  • Une CI au vert prouve donc que les scènes se construisent, se chargent, et produisent les mêmes compteurs et les mêmes couleurs, à 2 près, sur un rastériseur logiciel et sur le GPU d’Apple. Elle ne prouve rien sur WebGPU sous Windows ou Linux.
  • WebGPURenderer compte un draw call de plus que la scène n’en a. Avec du tone mapping, ou avec un espace colorimétrique de sortie différent de l’espace de travail, il rend la scène dans une cible et dessine le résultat dans une passe de sortie : 2 draw calls et 13 triangles pour un cube. WebGLRenderer convertit dans chaque shader et rapporte 1. Les objets info des deux moteurs de rendu n’ont pas non plus la même forme.
  • @types/three décrit mal toJSON(). Dans 0.186.0, Object3DJSON déclare children comme des chaînes et n’a ni geometries ni materials, alors que l’exécution écrit des objets et les deux tableaux. La leçon 1 conserve les erreurs du compilateur dans errors/l01_tojson_types.ts. Je n’ai pas cherché d’issue existante dans three-types/three-ts-types, et je n’en ai pas ouvert.
  • La boîte de la caméra d’ombre n’est lue qu’une fois par WebGPURenderer. ShadowNode appelle updateProjectionMatrix() quand il construit l’ombre ; un changement ultérieur demande votre propre appel.
  • PCFSoftShadowMap a disparu de WebGPURenderer dans r186 et produit un avertissement. LunarLanderEngine.ts et MinimalThreeInstrument.tsx de GA l’utilisent encore avec WebGPURenderer.
  • Une lumière ponctuelle de 800 lumens fait 63.66 candelas, et un spot de 800 lumens 254.65 : SpotLight.power utilise π, pas 4π, et ne change pas avec l’angle du cône.
  • La PMREM coûte 17 rendus pour un environnement de 256 par 128 : 1 dans le premier niveau, puis 2 pour chacun des 8 autres. Elle a lieu même quand scene.environment est une simple texture équirectangulaire.
  • Depuis r185, DRACOLoader trouve ses décodeurs avec import.meta.url, ce que r184 ne faisait pas. Vite copie alors les cinq fichiers de décodeur dans le build, 1.31 MB, alors qu’un navigateur en charge deux. Le build avertit aussi que three.webgpu dépasse 500 kB.
  • Draco et meshopt éliminent des sommets. Tous deux éliminent les deux sommets de pôle que SphereGeometry crée et qu’aucun triangle n’utilise ; le weld() de Draco fusionne en plus les coins du corps non indexé.
  • Le meshopt() de glTF Transform affiche des lignes prune: Removed types... Accessor sur stdout ; elles figurent telles quelles dans expected/.

Le cours lit GA au commit 05c8eda, qui utilise three 0.180. 139 fichiers sous des répertoires src importent three ; 33 d’entre eux appellent requestAnimationFrame, 3 setAnimationLoop ; 29 créent un WebGLRenderer et 8 un WebGPURenderer ; 15 utilisent Clock, déprécié dans r183. Rien de ce qui suit n’a été signalé à GA.

  • ThreeFretboard.tsx s’attend à ce que WebGPURenderer lève une exception quand WebGPU manque, et affiche « Using WebGPU renderer » sur toutes les machines, puisque le moteur de rendu se replie plutôt sur WebGL 2 (leçon 1). Ses ombres ne sont activées que pour un WebGLRenderer, donc le chemin de repli n’en a aucune (leçon 2).
  • ThreeFretboard.tsx demande samples: 8 ; la spécification WebGPU autorise 1 ou 4 échantillons pour une texture. Ce que fait r180 avec 8 sur chaque backend est à vérifier.
  • Sa lumière ponctuelle de 0.6 cd avec distance: 20 donne environ 5 % de la lumière principale là où elle est le plus proche du manche (leçon 2), et une seule texture de canvas sert de carte de couleur, de rugosité, de normales et de relief.
  • Sa texture de bois n’a pas de colorSpace, elle s’affiche donc plus claire que peinte, et son environnement est un dégradé sur 8 bits dont le moteur de rendu construit quand même la PMREM (leçon 3). L’écart de couleur du bois à l’écran est à vérifier dans un navigateur.
  • Guitar3D.tsx construit un environnement noir avec fromScene sur une scène qui ne contient qu’une lumière ambiante, et annonce la prise en charge de KTX2 et de meshopt sans régler aucun des deux décodeurs (leçons 3 et 4).
  • Ocean.tsx charge les décodeurs de Draco depuis gstatic.com ; DemerzelFaceOverlay.tsx masque les erreurs KTX2 en remplaçant console.error pendant un chargement ; HandAnimationTest.tsx met à jour un AnimationMixer qui n’a aucune action (leçon 4).
  • Experiments/ThreeJS-BSP-Loader importe WebGPURenderer depuis un chemin supprimé dans r167 ; le visualiseur d’embeddings du tableau de bord retire un écouteur de redimensionnement avec un nouveau bind, ce qui ne retire rien.
  • ThreeHeadstock.tsx ajoute un écouteur de clic au même canvas à chaque exécution de son effet et ne le retire jamais ; TonalOrbit.tsx et MinimalThreeInstrument.tsx lancent un rayon à chaque événement de souris, le second avec un nouveau Raycaster à chaque fois ; InteractionHandler.ts, dans Prime Radiant, le fait comme la leçon ; 4 des 25 fichiers qui utilisent OrbitControls ne les libèrent jamais (leçon 5).
  • Le ciel GLSL de Sunburst3D.tsx, copié dans ImmersiveMusicalWorld.tsx, ne peut pas tourner sur WebGPURenderer ; FresnelGlowTSL.ts fige l’intensité et la couleur de sa couronne dans le shader sous forme de constantes ; MoebiusPassTSL.ts est un ShaderPass GLSL malgré son nom (leçon 6).
  • Ocean.tsx et LunarLanderEngine.ts utilisent PostProcessing, renommé RenderPipeline dans r183, et Ocean.tsx ne libère jamais le pipeline ni ses nœuds ; ForceRadiant.tsx tourne sur WebGPURenderer sans les effets de bloom et de ShaderPass de son chemin WebGL, une « tâche de suivi » ; les 10 fichiers qui utilisent EffectComposer s’en servent correctement pour WebGL (leçon 7).
  • ThreeFretboard.tsx crée une géométrie et un matériau par frette et par repère, reconstruit sa scène chaque fois que sa prop positions est un nouveau tableau, y compris le [] par défaut, et ne libère pas ses 29 sprites ; NodeInstancer.ts instancie bien ses nœuds, avec le culling désactivé ; GA n’utilise ni BatchedMesh ni THREE.LOD (leçon 8).

2026-09-16 — Leçons 5 à 8 : pointeurs, shaders, effets, comptes

Section intitulée « 2026-09-16 — Leçons 5 à 8 : pointeurs, shaders, effets, comptes »
  • De vrais événements de pointeur dans une page headless. scripts/probe.mjs effectue maintenant les déplacements et les glisser-déposer qu’une page liste, avec la souris de Playwright, et demande à la page ce qu’elle a vu après chacun. Chromium les transforme en événements pointermove, pointerdown et pointerup, que OrbitControls traite comme ceux d’un utilisateur : un glisser de 120 pixels a tourné la caméra de 96°.
  • Une caméra hors de la scène a une matrice périmée dans un script. La première version de l05-raycaster.ts projetait un point à un NDC de −3.6 : le rendu met à jour la matrice monde de la caméra, et un script Node.js ne fait pas de rendu. camera.updateMatrixWorld() l’a corrigé.
  • Une feuille de style partagée fait partie du hash de chaque chunk. Ajouter les règles de mise en page de la leçon 5 à page.css a changé le nom de tous les chunks du build, y compris le 04-gltf-CCDSPzCq.js que cite la leçon 4. La leçon 5 a reçu sa propre feuille de style, et check.sh construit d’abord les pages des leçons 1 à 4 seules, avec un filtre PAGES, avant de tout construire : avec les pages des leçons 5 à 8, Rolldown nomme le chunk WebGPU partagé three.tsl au lieu de three.webgpu.
  • PostProcessing s’appelle RenderPipeline depuis r183, et pipeline.dispose() libère un seul matériau. Les 11 render targets du bloom et celui de la passe de scène restent jusqu’à ce que les nœuds eux-mêmes soient libérés.
  • DirectRenderPipeline.render prend la scène et la caméra, contrairement à RenderPipeline.render(), donc TypeScript le refuse dans une variable RenderPipeline ; et il dessine un fond uni sous la forme d’une sphère de 1 984 triangles.
  • renderer.info.render s’additionne entre les appels à render() d’une même image d’animation. Une page qui rendait 60 images dans une boucle pour les chronométrer rapportait 610 061 draw calls ; la leçon 8 lit d’abord les comptes.

2026-09-16 — Les runners de CI et les leçons 5 à 8

Section intitulée « 2026-09-16 — Les runners de CI et les leçons 5 à 8 »
  • L’exécution 35123228046 a réussi sous macOS (WebGPU sur l’adaptateur « apple ») et échoué sous Linux et Windows, où WebGPURenderer se replie sur WebGL 2 avec SwiftShader. Quatre différences, toutes réelles :
    • Les chaînes de bloom comptent un programme de plus sur WebGL 2 : 14 au lieu de 13, 16 au lieu de 15.
    • Avec FXAA, le pixel du halo vaut 135, 176, 197 sur SwiftShader, là où le GPU de l’auteur donne 132, 174, 195 sur les deux backends : 3 de plus que ce qu’accepte compare.mjs.
    • Un BatchedMesh est dessiné avec WEBGL_multi_draw et compté comme 2 draw calls, pas 10 001.
    • Le fichier de shader WebGPU contient du GLSL, puisque la page tournait sur WebGL 2.
  • Les pages de la leçon 8 à 800 000 triangles dépassaient le délai : le résultat de la sonde arrivait, mais la capture d’écran de Playwright attendait plus de 30 secondes derrière les 60 images chronométrées encore en file dans le rasteriseur logiciel.
  • La correction : check.sh compare une sonde avec expected/<name>.webgl.txt quand ce fichier existe et que la page a tourné sur WebGL 2. Les cinq fichiers viennent de chromium-headless-shell sur la machine de l’auteur, dont le SwiftShader a donné les mêmes valeurs que les runners. La ligne GLSL est comparée, et la ligne WGSL seulement affichée. La leçon 8 chronomètre 20 images, et ses sondes attendent jusqu’à 240 secondes (PROBE_TIMEOUT). L’exécution 35124756498 a réussi sur les trois OS.
  • React 19.3.0 est sorti le 9 septembre 2026, mais @react-three/fiber 9.7.0 déclare react >=19 <19.3 : le cours épingle React 19.2.8, avec drei 10.7.8. R3F 10 et drei 11 sont en alpha. @react-three/test-renderer 9.1.1 et Vitest 5.0.1 testent les composants.
  • Rapier 0.20.0 a été publié le 8 août 2026. L’addon RapierPhysics de three.js r186 charge encore la 0.17.3 depuis skypack.dev, et @react-three/rapier 2.2.0 épingle la 0.19.2. Le cours utilise à la fois @dimforge/rapier3d-compat et @dimforge/rapier3d-deterministic-compat ; leurs classes ont des membres privés, donc TypeScript refuse le type d’un module pour l’autre sans cast.
  • IWER 2.4.0 émule un Quest 3 ; pixelmatch 7.2.0 compare les captures d’écran.

2026-09-16 — Leçons 9 à 13 : ce qu’ont trouvé les pages

Section intitulée « 2026-09-16 — Leçons 9 à 13 : ce qu’ont trouvé les pages »
  • R3F 9.7.0 émet un avertissement sur WebGPURenderer sans qu’on lui demande rien. Le shadows de <Canvas> vaut false par défaut, et R3F règle PCFSoftShadowMap pour tout booléen ; WebGPURenderer le réinitialise avec un avertissement, une fois par rendu de <Canvas>. Son store crée aussi un THREE.Clock déprécié.
  • Le <Text> de drei dessine un quad vide sur WebGPURenderer : troika-three-text injecte son shader via onBeforeCompile, que le moteur de rendu à nœuds n’appelle jamais.
  • Les comptes de rendus dépendent de la machine. Dans chromium-headless-shell, 10 mises à jour d’état espacées d’une image ont donné 6 rendus au lieu de 11, et un déplacement de souris en 5 étapes moins de rendus que sur le GPU : React regroupe les mises à jour et le navigateur fusionne les événements de pointeur quand les images sont lentes. Les pages utilisent flushSync, et ne rapportent aucun compte après les actions de pointeur.
  • Chromium sous Windows avertit que powerPreference est ignoré chaque fois que R3F le passe ; les autres OS, non. check.sh filtre la ligne.
  • Les builds déterministe et standard de Rapier ont donné le même hash d’état sous Windows x64, dans Node.js et dans le navigateur. Avec un pas variable entre 1/144 et 1/30 s, 16 médiators sur 24 ont atterri ailleurs et un est passé à travers un sol de 20 cm. Une balle rapide a traversé un mur dynamique mince sans CCD, mais pas un mur fixe.
  • IWER a besoin de forceInstall dans Chromium, qui a déjà un navigator.xr, et de stereoEnabled, sinon le viewport de l’œil droit fait 0 pixel de large.
  • La première sonde WebXR est restée bloquée dix minutes en gardant le verrou GPU partagé : le backend WebGL 2 levait une exception à chaque image XR, la page attendait des images indéfiniment, et l’exécution a été arrêtée à la main. La cause est le XRWebGLLayer.framebuffer d’IWER, null, utilisé comme clé de WeakMap dans WebGLState.drawBuffers. La page intercepte maintenant les erreurs de rendu et borne ses attentes, et probe.mjs s’arrête de lui-même après deux fois son délai plus 30 secondes.
  • Dans r186, WebGPURenderer n’entre dans une session WebXR sur WebGPU qu’avec XRGPUBinding et la fonctionnalité webgpu ; son message d’erreur mentionne un VRButtonGPU qui n’existe pas. WebGLXRFallback change de moteur de rendu, mais les contrôleurs que la page a pris au premier moteur de rendu cessent de recevoir des événements. Seul le WebGLRenderer classique a rendu les deux yeux avec IWER.
  • Un InstancedMesh ignore setColorAt si son premier rendu n’avait pas de couleurs d’instance : les marqueurs du portage s’affichaient en blanc jusqu’à ce que l’attribut soit alloué dès le départ.
  • @react-three/test-renderer a besoin de IS_REACT_ACT_ENVIRONMENT, sinon React avertit à chaque mise à jour et R3F libère plus tard ; son build CommonJS charge three.cjs à côté du build en module ES, sans séparer les classes qu’utilise la scène.
  • Pixels : la même page deux fois a donné des octets identiques ; WebGPU face à ANGLE sur le même GPU différait de 294 pixels, face à SwiftShader de 10 330, aucun au-dessus du seuil par défaut de pixelmatch une fois l’anticrénelage écarté.
  • Une image importée par une leçon doit exister avant la page : la leçon 11 a un jour importé une capture d’écran pas encore convertie, et le build Astro de l’arbre de travail partagé a échoué pour les autres sessions qui y travaillaient jusqu’à ce que l’image soit ajoutée.

2026-09-16 — Les leçons 9 à 13 sur les runners de CI

Section intitulée « 2026-09-16 — Les leçons 9 à 13 sur les runners de CI »
  • L’exécution 35175914560 a réussi sur les trois OS du premier coup. Le build standard de Rapier a donné le hash d’état 68235a2bdba192f1 sous Linux x64 et macOS arm64 aussi, dans Node.js et dans le navigateur, le même que le build déterministe : pour cette scène, et pas comme une garantie.
  • macOS a exécuté les pages sur WebGPU avec l’adaptateur « apple » ; Linux et Windows sur le WebGL 2 de SwiftShader, où le ThreeFretboard de GA a produit ses 253 lignes d’erreur WebGL et où R3F a averti 13 fois à propos de PCFSoftShadowMap au lieu de 14.
  • Avant le push, la vérification locale a comparé la page de repli de la leçon 11 à sa sortie WebGL 2 sur le GPU de l’auteur : la page finit sur WebGL 2 après avoir démarré sur WebGPU. check.sh n’utilise maintenant la sortie WebGL 2 que quand la page a tourné sur WebGL 2 dès le départ.

2026-09-16 — GuitarAlchemist/ga, leçons 9 à 13

Section intitulée « 2026-09-16 — GuitarAlchemist/ga, leçons 9 à 13 »

Rien de ce qui suit n’a été signalé à GA.

  • Les composants React de GA utilisent R3F 8, drei 9 et React 18, avec WebGLRenderer dans les 6 fichiers qui ont un <Canvas> ; ThreeFretboard.tsx n’utilise pas R3F. L’explorateur BSP met à jour un état React avec une copie de la position de la caméra à chaque image, et GuitarAlchemistLogo3D.tsx passe un nouvel objet dans args, ce qui reconstruit sa géométrie à chaque rendu (leçon 9).
  • Aucune bibliothèque de physique : Cheese Avalanche est écrit à la main avec un pas variable plafonné à 1/30 s ; le lunar lander utilise un pas fixe de 1/120 s avec un accumulateur (leçon 10).
  • Aucun code WebXR (leçon 11).
  • 22 fichiers de specs Playwright, dont les 7 hors de la suite du tableau de bord, 6 d’entre eux sur des pages 3D, ne sont exécutés par aucun workflow de CI ; des attentes de 3 secondes pour WebGPU ; un toBeTruthy() sur le buffer d’une capture d’écran ; une vérification de canvas noir qui appelle getContext('2d') sur un canvas WebGL et ne peut jamais réussir ; 13 fichiers de résultats de tests commités, issus d’une exécution en échec (leçon 12).
  • ThreeFretboard.tsx, reproduit et mesuré : 69 draw calls et 32 textures pour un manche vide ; 10 rendus de son parent avec les mêmes notes reconstruisent la scène 10 fois et font fuir 29 textures de sprites à chaque fois ; un pixel ratio qui va jusqu’à 6 ; samples: 8, qui ne dessine rien sur le WebGL 2 de SwiftShader ; des cordes deux fois trop épaisses, puisqu’un tirant sert de rayon ; et une prop onPositionClick jamais appelée. Le portage dessine le même manche en 6 draw calls avec 4 textures, et sélectionne des notes (leçon 13).
  • Les pages sont construites par une seconde configuration Vite, vite.demos.config.ts, avec la base /learn/threejs-demos/, dans public/threejs-demos, que le site copie tel quel. Les sources des pages des leçons sont inchangées, puisque les leçons 4, 8 et 13 citent leur build ; un plugin de cette configuration ajoute le panneau Try it aux seules pages construites. Une page ne se pilote pas de l’extérieur : chaque contrôle règle donc un paramètre d’URL et recharge la page. Le panneau est lil-gui 0.17, la copie que three.js livre dans three/addons/libs : aucune nouvelle dépendance.
  • Le build pèse 9.4 Mo en 75 fichiers. Les builds standard et déterministe de Rapier, chacun avec son WebAssembly intégré en base64, font 2.9 Mo chacun ; three.tsl 0.69 Mo. check.sh le reconstruit et le compare à la copie commitée (diff -r, hors modèles et HDR régénérés).
  • Les cadres sont dans un <details> fermé, avec loading="lazy" : rien ne se charge tant qu’un lecteur n’en déplie pas un.
  • La guitare est modélisée en code : aucune guitare glTF sous licence connue n’était disponible. 47 405 triangles en 32 draw calls ; de l’instanciation pour les frettes, les repères, les plots, les pontets, les boutons et les mécaniques.
  • Le body.handle de Rapier est un flottant qui regroupe un index et une génération : handle % 8 n’est pas un index de palette ; chaque médiator garde maintenant la couleur de son numéro de lancer. Et les couleurs réglées avec setColorAt n’atteignaient le GPU que dans une image de setAnimationLoop : une sonde qui appelait render() elle-même voyait des médiators blancs. En r186, InstanceNode les envoie dans sa mise à jour par image.
  • La page VR utilise le WebGLRenderer classique, comme la leçon 11 l’a trouvé nécessaire avec IWER. Pendant une session, son écouteur resize ne doit rien faire : WebGLRenderer avertit « Can’t change size while VR device is presenting ».
  • Sur une page à taille de téléphone (émulation Pixel 7 de Playwright), les cadres mesuraient d’abord 150 pixels de haut : les styles du site écrasent l’attribut height de l’iframe, donc la hauteur est maintenant un style en ligne, min(420px, 70vh). Le panneau démarre replié dans un cadre étroit. Avec un CPU ralenti 4 fois mais le GPU de bureau de l’auteur, les cinq scènes tenaient 60 images par seconde en WebGPU et avec ?webgl ; sur le GPU d’un vrai téléphone de milieu de gamme, c’est à vérifier.
  • La première sonde d’une page qui importe une nouvelle dépendance a échoué avec « Execution context was destroyed » : Vite a optimisé la dépendance et rechargé la page. Le second essai est passé.

2026-09-17 — Leçon 14, le labo 3D Guitar Alchemist

Section intitulée « 2026-09-17 — Leçon 14, le labo 3D Guitar Alchemist »
  • Quinze expériences dans code/threejs/ga-lab, chacune avec une hypothèse et des prédictions chiffrées committées le 16 septembre avant son exécution, mesurées le 17 septembre sur une RTX 5080 dans Chromium 153 avec les requêtes d’horodatage WebGPU. Sur 61 prédictions, 19 étaient fausses en tout ou partie et 2 n’ont pas pu être mesurées ; elles restent dans la leçon.
  • Les images sont rendues en boucle serrée sans requestAnimationFrame, vsync désactivé, et la page fait avancer elle-même renderer._nodes.nodeFrame : sans cela, les shadow maps et le bloom ne se mettent pas à jour. onSubmittedWorkDone() n’a jamais résolu sous 3.5 ms environ : les temps GPU viennent des requêtes d’horodatage.
  • Constats r186 : InstancedMesh avec morph targets échoue avec une instance (pas de morphTargetInfluences) et avec plusieurs quand on les définit ; BatchedMesh fait un appel par objet sur WebGPU ; le compute TSL tourne sur WebGL 2 ; renderer.info.memory.texturesSize compte 0 pour les textures compressées.
  • L’index OPTIC-K de GA contient 313 047 vecteurs de 124 dimensions (OPTK v4), pas 112. Le labo le lit en lecture seule ; la CI utilise un substitut synthétique.
  • Le CCD de Rapier n’arrête pas un médiator cinématique : il traverse la corde à 30 et 60 pas par seconde dans tous les cas.
  • Le build R3F embarquait three à côté de three/webgpu : 565 ko gzip contre 210 pour la même scène sans React.
  • Les textures de bois ComfyUI sont en attente : l’expérience 4 du labo ComfyUI GA n’avait pas encore de sortie.
  • La CI (threejs-ga-lab.yml) compare les compteurs renderer.info sur WebGL 2 sur trois OS, jamais les temps. Une capture Playwright a bloqué 300 s une fois sur macOS : le lanceur garde désormais le résultat quand une capture échoue.
  • La ligne PCFSoftShadowMap ne s’applique pas encore à GA. Le retrait et son avertissement sont réels en r186 (Renderer.js et ShadowNode.js du paquet three@0.186.0), mais GA épingle three 0.180.0 et @react-three/fiber 8.18.0. En 0.180, ShadowNode associe encore PCFSoftShadowMap à son filtre doux : pas d’avertissement, et les ombres douces fonctionnent.
  • Les deux associations de GA avec WebGPURenderer, dans LunarLanderEngine.ts et MinimalThreeInstrument.tsx, commenceront à avertir quand GA passera à r186 ou plus. Les changer maintenant ferait perdre les ombres douces : #733 les laisse pour cette mise à jour et le dit. Lu dans les sources du paquet, pas exécuté (à vérifier).
  • La mémoire GPU des KTX2 avec un outil GPU.
  • Le premier rendu de R3F sur un build de production.
  • Un alias de three vers three/webgpu.
  • Le manche à taille réelle et le décalage de tête de 25 mm d’IWER sur un casque.
  • La latence audio au microphone.
  • powerPreference sur un portable à deux GPU.
  • La démo VR avec un vrai casque : l’entrée dans la session, les rayons des manettes, la note et l’impulsion haptique.
  • Si les médiators lancés sans CCD traversent les frettes de la démo physique.
  • Les cinq scènes complètes sur un téléphone : fréquence d’images, et si la shadow map de 2048 × 2048 et le bloom du studio tiennent.
  • Si une scène modélisée en centimètres a besoin de lumières 10 000 fois plus fortes pour avoir le même aspect qu’en mètres (leçon 2).
  • Les textures KTX2 : chargement, formats GPU choisis sur chaque OS, mémoire.
  • Si ThreeHeadstock.tsx appelle onTuningPegClick une fois par écouteur périmé après les nouvelles exécutions de son effet (leçon 5).
  • Si ThreeFretboard.tsx est reconstruit à chaque rendu de ses parents dans les pages de GA (leçon 8).
  • Le temps GPU, pas seulement le temps CPU, avec le timestamp-query de WebGPU (leçon 8).
  • Draco face à meshopt sur un gros modèle et sur de nombreux petits, sur un vrai réseau (leçon 4, exercice 3).
  • WebGPU sur une machine Linux ou Windows dotée d’un GPU, autre que celle de l’auteur.
  • XRGPUBinding : quels navigateurs et quels casques le proposent, et WebGPURenderer dans une vraie session WebXR (leçon 11).
  • Le backend WebGL 2 et WebGLXRFallback sur un vrai casque, dont le framebuffer de couche n’est pas null (leçon 11).
  • La fréquence d’images, le rendu fovéal et le confort de la page WebXR sur un Quest (leçon 11).
  • S’il existe un troika-three-text compatible avec WebGPU pour le <Text> de drei (leçon 9).
  • La fréquence à laquelle les parents de GA rendent ThreeFretboard et l’explorateur BSP en pratique, avec le profileur de React (leçons 9 et 13).
  • samples: 8 sur le WebGL 2 de vrais GPU, selon leur MAX_SAMPLES (leçon 13).
  • Pourquoi Rapier a arrêté une balle rapide sur un mur fixe mince sans CCD (leçon 10).