# Terrain — direction artistique retenue : **BOIS NU** > **Décision prise.** Après 13 directions artistiques comparées sur le même monde, > c'est **« Bois nu »** qui est retenue pour le terrain du jeu. > Les 12 autres restent consultables (voir §6) mais ne sont plus des candidates. --- ## 1. Le principe en une phrase Le monde est **une carte de guerre en relief, sculptée dans un seul bloc de bois massif, puis teintée et peinte à la main** — un objet d'artisan posé sur une table d'atelier, sous une lampe chaude. Et la particularité qui définit cette variante précise : > **Le dessus du plateau ne montre AUCUN FIL.** > Pas une rainure, pas une cathédrale, pas un nœud. C'est un choix, pas un oubli. Les versions précédentes montraient le veinage sur toute la surface : à la distance de jeu ça lisait comme du marbre ou des courbes de niveau, pas comme du bois. Ici le bois s'annonce autrement. --- ## 2. Comment le bois se lit, puisqu'il n'a plus de fil Quatre porteurs, et il faut les quatre — retirer l'un d'eux fait basculer la surface vers le plastique ou la pierre polie. **1. La teinte, qui a pris de façon inégale.** Des nuées douces, larges de 0,7 à 5 unités monde, qui se recouvrent à plusieurs échelles. Chacune est un dégradé radial à profil adouci, légèrement ovalisé et **tourné au hasard** : il ne reste aucune direction lisible, donc rien qui puisse se relire comme une rainure. Un virage chaud est couplé à la profondeur de teinte — plus ça a bu, plus c'est chaud. **2. Les flancs sciés du bloc.** C'est là, et **seulement là**, que le matériau s'annonce franchement : le bois de bout et ses couches. Piloté par l'attribut de sommet `aFlank` (0 sur le dessus, 1 sur les flancs). Sur les captures c'est le liseré miel tout autour de la carte — le signal « bois » le plus efficace de la maquette. **3. Le satiné de la cire.** Une spéculaire large et douce, inégale comme un lustrage au chiffon, **jamais brillante**. Rugosité 0,66 qui descend à 0,49 dans les plages lustrées. **4. Le grain de ponçage.** Un piqueté **sans direction** : ±8 % d'albédo et une modulation de rugosité, perceptible uniquement en lumière rasante. C'est lui qui empêche la surface de tourner à la pâte. Sans direction = jamais de ligne. **Un terme supplémentaire :** « le bois de bout boit la teinte » — plus la pente coupe la fibre, plus la couleur est profonde et chaude. Effet secondaire heureux : le relief paraît plus sculpté que dans les variantes avec fil. **Exception assumée :** les **pièces de jeu gardent leur fil axial**. Elles sont tournées au tour, le fil s'y voit, c'est cohérent. Le « sans fil » ne concerne que la surface du plateau. --- ## 3. Le reste de la direction (commun à la famille « bois ») - **Une seule essence de bois.** Toute la couleur vient de la teinte et de la peinture, donc les matières se fondent en **dégradés larges** — jamais de découpe nette, et **surtout jamais une seule case visible**. - **La peinture est mate et fine**, elle s'use sur les arêtes et les sommets, elle s'encrasse dans les creux. - **La rivière et la coulée de lave sont de la résine coulée** dans des saignées creusées, à la manière d'une table « resin river ». Absorption Beer-Lambert : claire et mince au bord, profonde et saturée au cœur. La lave rougeoie de l'intérieur. - **Le centre est du bois brûlé et noirci** (shou sugi ban) : c'est le basalte, là où la rivière rencontre la coulée. Seule vraie tache sombre de la carte. - **Aucune case, aucune marche, aucun escalier — sur le relief naturel.** La grille 96×96 n'est qu'une source de données : le relief est lissé puis rééchantillonné à **3 sommets par cellule en bicubique Catmull-Rom**, les masques de matière sont fortement diffusés, et les rives de résine sont découpées sur une **isoligne**, pas sur des cellules. ⚠️ **Une exception, et une seule : ce que le joueur creuse.** Là, l'arête est franche — sinon le geste ne se verrait pas et le jeu deviendrait injouable. Le partage exact est dans §5, c'est la décision la plus structurante du rendu. - **Éclairage d'atelier** : une lampe chaude latérale, environnement PMREM depuis un dégradé peint en canvas, `environmentIntensity` 0,36, le reste dans l'ombre. Feutrine sombre sous le bloc, table en dessous. - **Zéro asset externe.** Toute la géométrie est écrite en code, toutes les textures sont dessinées en ``. Aucune image, aucun modèle, aucune police réseau. ### Palette ```js const WOOD_RAW = 0xc0925a; // bois nu, miel — sous la peinture const WOOD_DEEP = 0x9e7440; // bois plus foncé — tranche, pièces const WOOD_DARK = 0x63452a; // noyer — socles et charpentes const PAINT = { GRASS: 0x6f8842, // vert-de-gris tendre : la plaine des Hommes FIELD: 0xc0912e, // ocre blé DIRT: 0x6d5840, // terre battue ROCK: 0x4a6480, // gris-bleu : le massif nain SAND: 0x93825a, // crème sale : les éboulis WATER: 0x3a504c, // fond de la saignée, sous la résine LAVA: 0x402013, // fond de faille calciné BASALT: 0x1a1615, // bois brûlé (le shader finit le travail) ASH: 0x434655, // cendre froide, grise et mauve : les Démons MOSS: 0x40602c, // vert forêt sourd : les Elfes }; ``` --- ## 4. Où est le code ### ✅ Le rendu du jeu : `client/render/` > ## `/srv/obsidian/client/render/` — **c'est le rendu du jeu, celui qui tourne** Le portage est fait. Le terrain est découpé en **chunks de 16×16 cellules** (exactement le découpage de `sim/world.ts`), il se met à jour par `updateRange`, et il lit `world.terrain` + les compteurs de version de `world.derived`. **Il ne modifie jamais l'état.** | Ce que tu cherches | Où | |---|---| | Mode d'emploi, décisions, mesures | `client/render/README.md` | | **Le pipeline CPU incrémental — le cœur** | `client/render/fields.js` | | Chunks, `addUpdateRange`, flancs, résines | `client/render/terrain.js` | | **Le décor des gisements** (arbres, rochers) | `client/render/props.js` | | Matériau du bloc + son `onBeforeCompile`, décor, résines | `client/render/materials.js` | | Textures dessinées en `` | `client/render/textures.js` | | Palette, miroir de `Surface` / `Fluid` | `client/render/palette.js` | | Lampe d'atelier, environnement, feutrine, table | `client/render/studio.js` | | Page d'essai (creuse et mesure) | `client/render/demo/` | ```bash export PATH=/opt/bun/bin:/opt/node22/bin:$PATH node client/render/build-sim.mjs # compile sim/world.ts pour la page d'essai node serve.js & node client/render/capture.mjs jeu # 70 unités : la distance de jeu node client/render/capture.mjs decor # la lisière forêt/plaine DECOR=0 node client/render/capture.mjs decor # …et la même sans décor bun run client/render/bench.mjs # le coût CPU, sans navigateur ``` ### La maquette de référence — conservée > ## `/srv/obsidian/mockups/style-boisnu.js` — la référence artistique C'est le fichier dont `client/render/` est le portage : en cas de doute sur un réglage de matière, c'est lui qui dit ce que la direction retenue voulait. Il reste autonome (il ne dépend que de `common.js` et de three.js) et **il n'a pas à être modifié** : `client/render/` ne l'importe pas, il le reproduit — un module du moteur n'a pas le droit de dépendre de `mockups/` (`rules/architecture.md` §5). | Ce que tu cherches | Où, dans `style-boisnu.js` | |---|---| | Constantes de résolution (`SUB`, `FN`, `FW`), hauteurs du bloc, feutrine, table | l. 46-60 | | Bruit, aléa déterministe, fbm | l. 75-118 | | **Palette bois + peinture** | l. 120-148 | | `drawGrain` — le veinage *(conservé pour les pièces uniquement)* | l. 171 | | **`drawStain` — la teinte en nuées, le cœur de « bois nu »** | l. 258 | | **Teinte du bloc, 96 unités sans répétition (`TEX.stain`)** | l. 291-306 | | **Cire / lustre au chiffon (`TEX.wax`)** | l. 309-320 | | **Grain de ponçage sans direction (`TEX.sand`)** | l. 323-351 | | Fibre coupée, les couches lues sur les flancs (`TEX.cut`) | l. 354-367 | | Peinture : R = coup de pinceau, G = éraflures, B = salissure | l. 370-403 | | Bois de bout, cernes concentriques (dessus des souches) | l. 472 | | Feutrine, table, halo, résine | l. 501 | | **Relief : lissage puis rééchantillonnage bicubique** | l. 625 | | **Masques de matière, fortement diffusés** | l. 658 | | **Saignées de résine : distance signée aux rubans** | l. 695 | | Champs fins | l. 723 | | **Flancs du bloc, bois de bout non peint (attribut `aFlank`)** | l. 899-940 | | Éclairage d'atelier, environnement PMREM | l. 1034-1058 | | **`matLand` — LE matériau du bloc + son `onBeforeCompile`** | l. 1060-1063 | | Matériau des pièces (fil axial conservé) | l. 1187-1193 | | **Résine coulée, absorption Beer-Lambert** | l. 1230-1249 | | Formes des pièces (sapin, feuillu, moulin, tour, mine, portail…) | l. 1373-1600 | | Assemblage : bloc sculpté, résine, feutrine, instanciation | l. 1603-1774 | | Ailes du moulin, porte du portail, bannières | l. 1774-1900 | | Vignette dessinée à la main | l. 1910 | | Boucle de rendu | l. 1937 | ### Les fichiers dont il dépend | Fichier | Rôle | |---|---| | `/srv/obsidian/mockups/common.js` | **Le monde partagé** : terrain 96×96, surfaces, biomes, ~280 props, les deux caméras. C'est le contrat qu'utilisaient les 13 maquettes. Purement des données, aucun rendu. | | `/srv/obsidian/mockups/boisnu.html` | Page hôte : importmap + canvas. Minimale. | | `/srv/obsidian/mockups/style.css` | Légende et bouton retour. | | `/srv/obsidian/vendor/three-build/` | three.js r180, vendorisé. | | `/srv/obsidian/vendor/three-examples/controls/` | `OrbitControls` uniquement. | ⚠️ **Aucun module de post-traitement n'est vendorisé** (pas d'`EffectComposer`). Tous les effets sont faits à la main — la vignette est un plan dessiné en canvas. ### Voir le résultat ```bash export PATH=/opt/node22/bin:$PATH # Node 22 isolé, comme sur /srv/spaceship systemctl status obsidian # le serveur statique tourne déjà (port 10570) ``` - **Le rendu du jeu** : https://obsidian.chalco.website/client/render/demo/?vue=jeu (`?vue=decor` pour la lisière forêt/plaine, `?vue=clairiere`, `?vue=carriere`, `?vue=proche`, `?vue=large`, `?trous=0` pour l'image témoin, `?decor=0` sans arbres ni rochers, `?explore=22` pour le décor sous le brouillard) - **La maquette d'origine**, vue de jeu : https://obsidian.chalco.website/mockups/boisnu.html - **La maquette**, vue cinématique : https://obsidian.chalco.website/mockups/boisnu.html?cam=cine Recapturer en headless (Puppeteer + WebGL logiciel, ~40 s) : ```bash cd /srv/obsidian && node shot.mjs boisnu 1500 950 cd /srv/obsidian && CAM=cine node shot.mjs boisnu 1500 950 ``` Le script échoue si la console du navigateur contient une erreur. **Regarde réellement les captures** dans `/srv/obsidian/shots/`, ne te contente jamais de « ça a tourné ». --- ## 5. ✅ Le lissage contre la lisibilité — comment c'est tranché C'était la question ouverte du portage, et c'est réglé. Rappel du conflit : - la direction dit « **aucune case, aucune marche, aucun escalier** » ; - le gameplay dit *creuser une fois ralentit, creuser deux fois bloque* (`terraformation.md`), donc **un trou d'une seule cellule doit se voir** — c'est un geste tactique, pas un détail décoratif. **Les deux tiennent, parce qu'on ne lisse pas la même chose de la même façon.** La question n'était pas « combien lisser » mais « lisser quoi ». Le partage retenu : | Ce qu'on rend | Lissage | Pourquoi | |---|---|---| | **Relief naturel** (`altitudeOrigine`) | généreux, σ ≈ 2 cellules | fondre les terrasses du générateur : aucune case ne doit se voir | | …mais une **falaise** (≥ 2 niveaux, donc infranchissable) | on revient à 62 % vers un lissage court | une falaise doit se lire comme une falaise, pas comme un talus — sinon le rendu ment au joueur sur ce qui est franchissable | | **Creusage du joueur** (`altitudeOrigine − altitude`) | **franc** : filtre en toit d'**un tiers de cellule** | une cellule creusée d'un niveau descend de 0,5 unité monde en 1/3 d'unité : paroi à 56°, mais surface toujours continue — franc n'est pas facetté | | **Obsidienne** | franche aussi | c'est un obstacle permanent : on doit voir **quelle** cellule est prise | | **Matières** (herbe, roche, cendre…) | diffusées, σ ≈ 2,5 cellules | ce sont des plages, pas des cases | | **Rive d'une résine** | lissée à σ ≈ 0,5 cellule, découpée sur isoligne | une coulée ne prend pas la forme en escalier de son moule | **Et le trou ne compte pas que sur sa géométrie.** Le creusage sort aussi en attribut par sommet (`aDig`), dont le shader tire deux signaux de plus, exactement ceux que ce document avait proposés : **de la terre fraîche non peinte** sur ce qu'on vient d'ouvrir, et une **ombre portée franche** au fond. Trois signaux valent mieux qu'un, et c'est ce qui fait qu'une cellule creusée d'un seul niveau se voit encore à 70 unités de recul — **vérifié sur capture** : `shots/terrain-jeu.png` (et `terrain-jeu-lisse.png`, la même image sans un seul coup de pelle, pour comparer). ### 🟡 Conséquence assumée : une saignée en diagonale montre ses cellules Un fossé qui coupe la carte en biais est un escalier de cellules, et on le rend comme tel. Ce n'est pas un défaut à corriger : **le joueur doit pouvoir lire exactement quelles cellules sont creusées**, c'est ce qui décide du passage. La rive de la résine, elle, est adoucie — le bois reste franc, l'eau non. ### ✅ Le coût, mesuré Le pipeline de la maquette (flou, distance signée, bicubique, découpe d'isoligne, couleurs, usure) coûtait **~1 s au chargement** et repassait sur toute la carte à la moindre modification. Le portage tient le budget annoncé : | | Mesuré | |---|---| | Construction complète (96×96, 36 chunks, 289×289 sommets) | **≈ 300 ms** dans le navigateur, ≈ 180 ms sous Bun | | **Une pelletée** (altitude + surface d'une cellule) | **0,4 ms** médiane · 0,6 ms p95 · 1,3 ms max — 129 mesures, Chrome/SwiftShader | | Altitude seule | 0,04 ms | | Une cellule de fluide (résines refaites en entier) | 0,3 ms | | Réémission GPU par pelletée | ≈ 2 800 sommets, 313 Kio en `bufferSubData` | Ce qui rend ça possible, et qui vaut d'être su avant d'y toucher : - les flous sont des **convolutions séparables restreintes à un rectangle**, exactement égales au calcul complet — donc aucune couture entre une zone recalculée et sa voisine ; - un compteur de version de chunk couvre 256 cellules : le rendu tient une **copie d'ombre** du terrain et diffe les cellules des seuls chunks signalés, ce qui ramène une pelletée à **une** cellule ; - les cinq bruits statiques (ciseau, grain fin, bains de teinte, suie, usure) sont calculés **une fois** et mis en cache par sommet ; - **normales et courbure sont séparées de l'ombrage** : un changement de surface ne touche pas la géométrie, donc ne les recalcule pas ; - l'amplitude du coup de ciseau décrit **la taille du bloc**, pas l'état courant d'une cellule : une pelletée ne redessine jamais la sculpture alentour. Le shader, lui, ne coûte rien de plus : teinte, cire, ponçage, usure, patine et brûlé sont des textures et des attributs par sommet, indépendants de la topologie. --- ## 5 bis. ✅ Le décor des gisements — une forêt est une **information de jeu** C'était le trou laissé par le portage, et il était grave : le terrain seul **ne peut pas dire où récolter**. §3 lui interdit de montrer une case, donc les matières sont diffusées sur σ ≈ 2,45 cellules et une forêt n'est qu'une teinte verte aux bords fondus. Or récolter exige de cliquer **une** cellule précise (`rules/economie.md`). La maquette, elle, avait toujours eu des arbres et des rochers ; le rendu du jeu ne les avait pas. **La règle, et elle ne souffre aucune exception :** | ce qu'on voit | ce que ça veut dire | |---|---| | un arbre — sapin, feuillu, ou un géant çà et là | `Surface.Forest` **et** `resource > 0` : du bois à prendre | | un rocher — caillou ou gros bloc | `Surface.Rock` **et** `resource > 0` : de la pierre à prendre | | rien | rien à prendre ici | **Pas de décor d'ambiance.** Le massif nain fait 2 200 cellules de roche, la carrière en fait 250 : seules ces 250 portent une pièce. Une pièce qui ne veut rien dire, le joueur apprend à ne plus la regarder — et le jour où elle veut dire quelque chose, il ne la voit plus. C'est la même exigence que « un trou d'une cellule doit se voir », prise par l'autre bout. Un gisement épuisé **perd sa pièce dans la même image** : `sim/economy.ts` repasse une forêt abattue en `Surface.Dirt`, une carrière vidée tombe à `resource = 0`, et le décor relit l'état comme le reste du rendu. **Ce qui est décidé, et pourquoi :** - **Une pièce par cellule récoltable**, pas un semis décoratif. La densité *est* l'information : la taille de la forêt à l'écran est la quantité de bois disponible. - **Les formes sont celles de la maquette** (`mockups/style-boisnu.js` l. 1378-1568), reproduites et non importées — un module du moteur ne dépend pas de `mockups/` (`architecture.md` §5). Ramenées à l'échelle d'une case : ≈ 1 unité de large, 2 de haut, soit 4 niveaux d'altitude. Assez pour se lire à 70 unités sans masquer le relief. - **Tout est tiré de l'index de cellule** (`hashF(x, y, graine)`), jamais d'un `Math.random` : deux clients voient la même forêt, et un arbre replanté revient exactement où il était. - **Le décentrage d'une pièce dans sa cellule reste sous une demi-cellule** : le pied ne sort jamais de la cellule qu'il désigne, sinon le clic tombe à côté du gisement visé. Mais il ne descend pas non plus vers zéro — à 0,18 les arbres s'alignent en damier, ce que §3 interdit. Retenu : **0,32**. - **Sous le brouillard, une pièce jamais explorée n'est pas affichée** : le voile ne flotte qu'à 0,26 unité au-dessus du bois, un arbre le percerait et trahirait une forêt qu'on n'a pas encore vue (`brouillard.md` §1). **Le coût, mesuré** (Chrome/SwiftShader, carte d'essai à 2 300 pièces — le jeu en a 680 à 1 070 selon la graine) : | | Mesuré | |---|---| | Construction du décor complet | **35 à 45 ms** (le terrain seul : ≈ 290 ms) | | **Un gisement épuisé** — l'arbre disparaît | **< 0,05 ms**, sous la résolution de l'horloge | | **Une pelletée sous bois** — le sol descend, les arbres se reposent | **0,1 à 0,2 ms** | | Brouillard, relecture sans changement (10 Hz) | ≈ 0,1 ms | | Dessin | 380 k triangles pour 2 300 pièces, **5 appels de dessin** | Le budget de §5 est de 1 à 2 ms par édition locale, dont le terrain prend déjà 0,4 : le décor en prend **un dixième**. Ce qui rend le cas fréquent gratuit, c'est une copie d'ombre des deux altitudes — récolter ne change que `surface` et `resource`, donc aucune pièce n'a à être reposée. **Les images** : `shots/terrain-decor.png` contre `shots/terrain-decor-nu.png` — la même lisière avec et sans décor, et sans, toute une forêt est un aplat vert informe. `shots/terrain-clairiere.png` contre `…-avant.png` pour la clairière abattue, `shots/terrain-carriere.png` pour la fosse des Nains, `shots/terrain-decor-brouillard.png` pour le voile. Le code est [`client/render/props.js`](../client/render/props.js), le mode d'emploi dans le README du dossier. --- ### Défauts connus de la maquette — ce qu'il en reste 1. **Le massif nain lit en marbre.** Toujours vrai, et c'est le prix de l'absence de fil. Point n°1 à retravailler côté matière. 2. ~~L'image est globalement sombre~~ — **corrigé** : lampe à 3,35, environnement à 0,39, exposition à 0,96 dans `client/render/studio.js`. Le bloc se lit à la distance de jeu. 3. ~~Le relief manque de nerf~~ — **corrigé** par le lissage sélectif : les ruptures de pente restent des ruptures. 4. **Les plaines très plates frôlent le lisse-plastique** — toujours vrai, c'est le grain de ponçage qui les sauve. À surveiller si on retouche l'éclairage. --- ## 6. Les 12 autres directions — conservées Rien n'est supprimé. La galerie de comparaison **reste en ligne et activée au démarrage** (`obsidian.service`), elle pourra resservir. > ### Galerie : **https://obsidian.chalco.website/** - Les 13 maquettes vivent dans `/srv/obsidian/mockups/` (`style-*.js` + leur `.html`). - Les captures — deux angles chacune — sont dans `/srv/obsidian/shots/`. - L'écran de présentation est `/srv/obsidian/mockups/index.html` (autonome, tout le CSS inline). Les 13, dans l'ordre où elles ont été explorées : voxel · sculpt · diorama · enlumine · tellurique — puis tapisserie · papier · plateau — puis marqueterie · boispeint — puis **boisnu (retenue)** · boisfil · boisuse. Chacune rend **exactement le même terrain, les mêmes props et la même caméra** : c'est ce qui rendait la comparaison honnête, et c'est ce qui permettra de rejuger sur pièce plus tard.