> ## Documentation Index
> Fetch the complete documentation index at: https://docs.wrappy.app/llms.txt
> Use this file to discover all available pages before exploring further.

# Pipeline d'assets 3D

> Comment Wrappy fabrique, optimise, contrôle et publie ses modèles et textures : les choix, les alternatives écartées, les mesures.

## 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, et
  `fete-des-laurens-v2.glb`, absent du CDN à ce jour.
* La doc de publication citait un `scripts/upload-scene-assets.sh` qui 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 pas `EXT_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](https://gltf-transform.dev) 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

| | Meshopt | Draco |
| - | - | - |
| Décodeur | inclus dans drei/three-stdlib | \~300 ko chargés depuis gstatic.com |
| Décodage | très rapide | plus lent, en worker |
| Animations, morph targets | compressés | non |
| Ratio sur mesh dense | bon, excellent après brotli du CDN | meilleur brut |

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é) :

| Scène | Textures statiques | RGBA8 + mips | ETC2 (mobile) | BC7 (desktop) |
| - | - | - | - | - |
| codex | 44 pages 673 × 917 | 145 Mo | 18 Mo | 36 Mo |
| 3d-book (album par défaut) | 14 faces | 46 Mo | 6 Mo | 12 Mo |
| lettre (tirages punaisés) | 3 photos 600 × 400 | 3,8 Mo | 0,5 Mo | 1 Mo |

Deux familles, un seul transcodeur (`packages/core/src/lib/gpu-textures.ts`) :

* **Textures d'un GLB** (`build --ktx2`) : le GLB porte `KHR_texture_basisu`, `ModelProp` le décode.
* **Images statiques** (pages, tirages) : la source reste en WebP, le `.ktx2` est 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.

Jamais pour les uploads, les faces peintes au canvas ni les vidéos.

Choix techniques : `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 par `bun 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)

| Asset | Chemin | Triangles | Draw calls | Poids | Build |
| - | - | - | - | - | - |
| `gift-box` | recette, materials simples | 2 084 | 4 | 21 ko | \~6 s |
| `wax-seal` | recette, bake high → low 512 px | 976 | 1 | 48 ko | \~15 s |
| `alarm_clock_01` | Poly Haven, `--simplify 0.4` | \< 5 000 | 2 | 164 ko | \~25 s |

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 de `letter/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.

| Prop | TS : triangles | TS : draw calls | GLB : triangles | GLB : draw calls | GLB : poids |
| - | - | - | - | - | - |
| mug aux stylos | 1 824 | 9 | 3 328 | 1 | 38 ko |
| mug de café et sous-verre | 1 504 | 3 | 3 884 | 1 | 41 ko |
| succulente | 4 512 | 3 | 4 560 | 1 | 72 ko |
| rouleau de scotch | 240 | 2 | 768 | 1 | 9 ko |
| lampe (sans le spot) | 768 | 3 | 1 620 | 2 | 19 ko |
| **total** | **8 848** | **20** | **14 160** | **6** | **179 ko** |

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](/engineering/scene-assets).

## Suites possibles

* Un contrôle CI : `inspect` sur chaque GLB de `apps/open/public/templates/scenes`, échec hors budget.
* Faire passer `gift-card-coffret.glb` (35 Mo) par `optimize`.
* 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
  namespace `guestbook` et le déclarer en texture GPU (0,7 Mo en ETC2).
