En bref
apps/dev-tools/asset-pipeline est un CLI (bun run asset …) qui enchaîne Blender en arrière-plan
(modélisation par script Python, bake), glTF-Transform (optimisation, mesure), Poly Haven
(bibliothèque CC0) et wrangler (publication R2). Tout est scriptable : un agent de code écrit une
recette, la construit, lit la planche de rendus produite et itère sans intervention humaine.
Le mode d’emploi opérationnel (commandes, contrat de recette, API Python, pièges) vit dans
apps/dev-tools/asset-pipeline/CLAUDE.md. Cette page explique les choix.
Le point de départ
- Les scènes sont modelées en TypeScript procédural (
packages/core/src/engines/props/) : mug en lathe, lampe, livres. Parfait pour ce qui est paramétré ou animé, coûteux et plafonné pour une forme travaillée (un nœud de ruban, un sceau, une plante). - Les textures du décor de la lettre suivent déjà la convention Poly Haven (
-diff,-nor,-arm, WebP 1024), publiées à la main. - Deux GLB existent hors pipeline, non optimisés :
gift-card-coffret.glb(35 Mo) sur la landing, etfete-des-laurens-v2.glb, absent du CDN à ce jour. - La doc de publication citait un
scripts/upload-scene-assets.shqui n’existe pas.
Les briques retenues
Blender headless plutôt que three.js ou un modeleur SDF
Blender 5.1 est installé, gratuit, et son API Python (bpy) couvre tout : primitives, modificateurs
(bevel, booléens exacts, remesh, displace), courbes et texte, bake Cycles sur GPU Metal, export glTF
maintenu par Khronos. En arrière-plan il démarre en ~2 s et rend une vue EEVEE en ~1 s.
Écartés : modeler en three.js côté Node (pas de booléens fiables, pas de bake, pas de rendu de
contrôle) ; OpenSCAD/CadQuery (CAO, pas de materials PBR ni de bake) ; Houdini (licence, lourdeur).
Le rendu de contrôle, cœur de la boucle
Chaque build rend une planche de 4 vues depuis le GLB livré : il est d’abord décompressé (Blender ne lit pasEXT_meshopt_compression), puis réimporté. Ce qu’on voit est ce qui part en prod,
pertes d’export comprises — un material procédural non cuit sort gris sur la planche comme dans la scène.
C’est ce qui rend le pipeline pilotable par un agent : il juge une image, pas un log.
EEVEE plutôt que Cycles pour les aperçus : même lecture, 8× plus rapide (6 s contre 51 s pour 4 vues).
glTF-Transform plutôt que gltfpack seul
glTF-Transform est la bibliothèque de référence (celle de gltf.report), programmable en TypeScript, avec meshoptimizer pour la géométrie et sharp pour les images. La chaîne :dedup → weld → [simplify] → resample → prune → textureCompress (WebP) → meshopt.
Les nœuds d’une recette ne sont jamais fusionnés (join) : la recette nomme ce que la scène anime
(le couvercle), elle fusionne elle-même le reste. --merge n’est utilisé que pour les modèles externes.
Meshopt plutôt que Draco
Nos assets sont petits et nombreux : le décodeur externe de Draco coûterait plus que ce qu’il gagne.
KTX2 là où la mémoire GPU déborde
Un WebP est décodé par le navigateur puis envoyé non compressé au GPU : 1024² = 4 Mo de VRAM (5,3 avec les mipmaps). KTX2/Basis ETC1S reste compressé en VRAM : ETC2 sur mobile (8× moins), BC7 sur desktop (4× moins), pour un téléchargement un peu plus lourd que le WebP (+12 % sur le codex, +30 % sur le 3d-book). Le seuil est franchi par les livres, dont toutes les pages restent résidentes (chaque feuillet est monté, envoyé au GPU au montage, et rien n’est libéré) :
Deux familles, un seul transcodeur (
packages/core/src/lib/gpu-textures.ts) :
- Textures d’un GLB (
build --ktx2) : le GLB porteKHR_texture_basisu,ModelProple décode. - Images statiques (pages, tirages) : la source reste en WebP, le
.ktx2est un artefact posé à côté (bun run asset textures <ns>). Le registre des scènes déclare les fichiers concernés et le drapeau qui les sert ; sans drapeau, rien ne change, et un KTX2 illisible retombe sur son WebP.
ktx2-encoder (Basis Universal en WASM) plutôt que toktx, rien à installer ;
le KTX2Loader des addons three plutôt que celui de three-stdlib (et useKTX2 de drei), qui lit
renderer.extensions du WebGLRenderer et plante sous WebGPU. En r184, three et three/webgpu
partagent three.core.js : importer l’addon ne double pas le cœur. Le transcodeur est servi par
notre CDN dans un dossier nommé par la révision de three (basis/v1/r184/) : son WASM va par paire
avec le worker du loader.
Poly Haven plutôt qu’ambientCG ou Sketchfab
API publique sans clé, tout en CC0, textures aux cartes déjà empaquetées en ARM (la convention de l’occlusionTexture et de la metallicRoughnessTexture glTF), normales en convention OpenGL (celle
de glTF et de three), HDRI et modèles glTF.
ambientCG est l’alternative CC0 pour les textures ; Sketchfab mêle les licences (CC-BY à créditer).
Génération par IA : un point d’entrée, pas une intégration
Meshy, Tripo, Rodin (Hyper3D), Hunyuan3D et TRELLIS sortent des GLB exploitables comme ébauche. Leurs défauts sont structurels pour Wrappy : topologie triangulée dense, éclairage cuit dans la couleur (qui jure sous nos lumières), UV éclatées, style difficile à tenir d’un objet à l’autre, licences variables selon l’offre. Ils entrent donc parbun run asset optimize --merge --simplify, comme tout
modèle externe, sans intégration d’API payante. À réévaluer si un besoin d’objets à la demande
(générés depuis la saisie de l’émetteur) apparaît dans le produit.
Budgets
Par rôle, mobile d’abord (src/budgets.ts) : prop 5 000 triangles / 3 draw calls / 512 px / 200 ko,
decor 15 000 / 4 / 1024 px / 600 ko, hero 40 000 / 8 / 2048 px / 1,5 Mo. Un asset hors budget
n’est pas posé dans le miroir CDN sans --force.
Mesures (Apple M4, Blender 5.1.1)
Le premier bake de la machine compile les kernels Cycles Metal : plusieurs minutes, une seule fois.
Test : les props fixes du bureau de la lettre
Les props procéduraux à géométrie fixe deletter/components/desk-room.tsx ont été portés en
recettes, cotes et poses reprises telles quelles du TS (mug, succulent, tape-roll,
desk-lamp). Palette et AO cuite dans les couleurs de sommets, sans texture.
Draw calls divisés par plus de trois (et autant à la passe d’ombre), pour 60 % de triangles et
179 ko de téléchargement en plus. Gagné en rendu : occlusion au fond des mugs, au cœur de la
rosette et sous les stylos, feuilles effilées au lieu d’ellipsoïdes, abat-jour avec épaisseur,
arêtes arrondies. Ces cinq GLB remplacent les composants TS dans
desk-room.tsx via ModelProp
(Mug, Succulent, rosette, DeskLamp supprimés ; Pen et TapeRoll restent pour le livre
d’or). Restent en TS : les livres et le carnet (leur valeur est dans les textures
peintes au canvas), le spot de la lampe, et tout ce que l’émetteur paramètre.
Publication
bun run asset publish <ns> compare le dossier <ns>/<vN> local au CDN (requêtes HEAD) et n’envoie
que ce qui manque, avec Cache-Control: immutable — cohérent avec des dossiers de version jamais
réécrits. Simulation par défaut, --apply pour envoyer. Remplace l’envoi fichier par fichier décrit dans
Assets de scène & CDN.
Suites possibles
- Un contrôle CI :
inspectsur chaque GLB deapps/open/public/templates/scenes, échec hors budget. - Faire passer
gift-card-coffret.glb(35 Mo) paroptimize. - Le marbre du livre d’or (
/images/marble.webp, public de l’app) fait 3000 × 4500 : 72 Mo de VRAM à lui seul, pour une texture répétée 4 × 4. Le ramener à 1024 px, le déplacer dans le namespaceguestbooket le déclarer en texture GPU (0,7 Mo en ETC2).