@call-me-sensei/toonlab 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (205) hide show
  1. package/AGENTS.md +127 -4
  2. package/ATTRIBUTION.md +41 -3
  3. package/README.md +234 -34
  4. package/docs/characters.md +144 -0
  5. package/docs/debug-panel.md +126 -0
  6. package/docs/docs.css +589 -0
  7. package/docs/environment.md +186 -0
  8. package/docs/getting-started.md +180 -0
  9. package/docs/index.html +14 -0
  10. package/docs/lab-architecture.md +100 -0
  11. package/docs/lighting.md +825 -0
  12. package/docs/main.jsx +739 -0
  13. package/docs/mcp.md +97 -0
  14. package/docs/post-processing.md +98 -0
  15. package/docs/settings-reference.md +1878 -0
  16. package/docs/shader-constants.md +77 -0
  17. package/docs/sky.md +182 -0
  18. package/docs/style-labs.md +309 -0
  19. package/docs/texture-lab.md +135 -0
  20. package/docs/toon-shading.md +183 -0
  21. package/docs/tsl-conventions.md +167 -0
  22. package/docs/vegetation-sky.md +275 -0
  23. package/docs/water.md +430 -0
  24. package/docs/weather.md +200 -0
  25. package/docs/world-scale.md +111 -0
  26. package/mcp/server.mjs +610 -0
  27. package/mcp/style-lab-tools.mjs +371 -0
  28. package/mcp/vite-plugin.mjs +174 -0
  29. package/mcp/workspace.mjs +397 -0
  30. package/package.json +64 -5
  31. package/src/ambientfx/INTEGRATION.md +164 -0
  32. package/src/ambientfx/ambientFxPresets.js +83 -0
  33. package/src/ambientfx/ambientFxSettings.js +368 -0
  34. package/src/ambientfx/emitters.js +169 -0
  35. package/src/ambientfx/index.js +5 -0
  36. package/src/ambientfx/particleBackbone.js +493 -0
  37. package/src/ambientfx/stylizedAmbientFx.js +450 -0
  38. package/src/assetlib/ambientcg.js +117 -0
  39. package/src/assetlib/assetRef.js +91 -0
  40. package/src/assetlib/importedEntry.js +40 -0
  41. package/src/assetlib/index.js +23 -0
  42. package/src/assetlib/kaykit.js +192 -0
  43. package/src/assetlib/kaykitStaticIndex.js +700 -0
  44. package/src/assetlib/loadImported.js +143 -0
  45. package/src/assetlib/opensource3d.js +113 -0
  46. package/src/assetlib/polyhaven.js +182 -0
  47. package/src/assetlib/polypizza.js +115 -0
  48. package/src/assetlib/smithsonian.js +214 -0
  49. package/src/assetlib/sources.js +279 -0
  50. package/src/assetlib/zip.js +58 -0
  51. package/src/biome/biomeGenerator.js +385 -0
  52. package/src/biome/biomeRuntime.js +299 -0
  53. package/src/biome/index.js +2 -0
  54. package/src/buildinggen/buildingAsset.js +44 -0
  55. package/src/buildinggen/buildingGrammar.js +311 -0
  56. package/src/buildinggen/buildingMesh.js +451 -0
  57. package/src/buildinggen/buildingPresets.js +46 -0
  58. package/src/buildinggen/buildingRecipe.js +100 -0
  59. package/src/buildinggen/buildingSettings.js +238 -0
  60. package/src/buildinggen/index.js +6 -0
  61. package/src/camera/cameraDirector.js +157 -0
  62. package/src/camera/cameraGenerator.js +367 -0
  63. package/src/camera/cameraRig.js +570 -0
  64. package/src/camera/cameraSettings.js +236 -0
  65. package/src/camera/index.js +7 -0
  66. package/src/catalog/builtinEntries.js +245 -0
  67. package/src/catalog/catalog.js +210 -0
  68. package/src/catalog/index.js +3 -0
  69. package/src/catalog/manifest.js +84 -0
  70. package/src/core/generation.js +529 -0
  71. package/src/environment/environmentRigs.js +21 -1
  72. package/src/environment/environmentSettings.js +4 -0
  73. package/src/environment/environmentSunShadowPass.js +8 -0
  74. package/src/fauna/INTEGRATION.md +174 -0
  75. package/src/fauna/boids.js +861 -0
  76. package/src/fauna/faunaBodies.js +492 -0
  77. package/src/fauna/faunaPresets.js +52 -0
  78. package/src/fauna/faunaSettings.js +525 -0
  79. package/src/fauna/index.js +5 -0
  80. package/src/fauna/stylizedFauna.js +395 -0
  81. package/src/game-feel/gameFeelGenerator.js +402 -0
  82. package/src/game-feel/gameFeelRuntime.js +549 -0
  83. package/src/game-feel/index.js +2 -0
  84. package/src/index.js +17 -6
  85. package/src/lighting/colorIntensity.js +177 -0
  86. package/src/lighting/index.js +161 -0
  87. package/src/lighting/lightDescriptors.js +249 -0
  88. package/src/lighting/lightingCapabilities.js +79 -0
  89. package/src/lighting/lightingDocuments.js +247 -0
  90. package/src/lighting/lightingFixtures.js +446 -0
  91. package/src/lighting/lightingGenerator.js +449 -0
  92. package/src/lighting/lightingPresets.js +319 -0
  93. package/src/lighting/lightingRuntime.js +723 -0
  94. package/src/lighting/lightingStyle.js +386 -0
  95. package/src/lighting/lightingSystem.js +774 -0
  96. package/src/lighting/unrealExport.js +186 -0
  97. package/src/lighting/utils.js +87 -0
  98. package/src/motion/index.js +5 -0
  99. package/src/motion/motionClip.js +441 -0
  100. package/src/motion/motionController.js +628 -0
  101. package/src/motion/motionDocuments.js +225 -0
  102. package/src/motion/motionGraph.js +307 -0
  103. package/src/motion/motionSettings.js +222 -0
  104. package/src/pathgen/index.js +7 -0
  105. package/src/pathgen/pathBridge.js +232 -0
  106. package/src/pathgen/pathPresets.js +35 -0
  107. package/src/pathgen/pathRibbon.js +410 -0
  108. package/src/pathgen/pathRouter.js +380 -0
  109. package/src/pathgen/pathSettings.js +335 -0
  110. package/src/pathgen/pathTextures.js +123 -0
  111. package/src/pathgen/stylizedPaths.js +453 -0
  112. package/src/post/index.js +1 -0
  113. package/src/post/postGenerator.js +177 -0
  114. package/src/post/postProcessing.js +41 -0
  115. package/src/propgen/generatorsWave1.js +379 -0
  116. package/src/propgen/generatorsWave2.js +462 -0
  117. package/src/propgen/index.js +5 -0
  118. package/src/propgen/propAsset.js +323 -0
  119. package/src/propgen/propParts.js +170 -0
  120. package/src/propgen/propPlacement.js +459 -0
  121. package/src/propgen/propPresets.js +82 -0
  122. package/src/propgen/propSettings.js +395 -0
  123. package/src/shaders-tsl/chunks/projected-water-caustics.js +242 -0
  124. package/src/shaders-tsl/chunks/vegetation-style.js +360 -0
  125. package/src/shaders-tsl/chunks/water-shore-state.js +31 -0
  126. package/src/shaders-tsl/chunks/water-waves.js +90 -11
  127. package/src/shaders-tsl/environment.js +15 -1
  128. package/src/shaders-tsl/flower.js +279 -30
  129. package/src/shaders-tsl/grass.js +60 -33
  130. package/src/shaders-tsl/sky.js +125 -31
  131. package/src/shaders-tsl/tree-leaf.js +61 -25
  132. package/src/shaders-tsl/water-breaker.js +7 -4
  133. package/src/shaders-tsl/water-shore-state-simulation.js +523 -0
  134. package/src/shaders-tsl/water.js +439 -49
  135. package/src/shaders-tsl/woody-surface.js +154 -0
  136. package/src/sky/sceneOverrideLayers.js +10 -0
  137. package/src/sky/skyQuality.js +26 -0
  138. package/src/sky/stylizedSky.js +753 -45
  139. package/src/soundscape/index.js +4 -0
  140. package/src/soundscape/soundscapeGenerator.js +179 -0
  141. package/src/soundscape/soundscapeRuntime.js +806 -0
  142. package/src/soundscape/soundscapeSettings.js +292 -0
  143. package/src/styles/index.js +13 -0
  144. package/src/styles/styleBundle.js +325 -0
  145. package/src/stylizedTerrain.js +32 -2
  146. package/src/stylizedWorld.js +423 -20
  147. package/src/texgen/evaluateTexture.js +675 -0
  148. package/src/texgen/index.js +60 -0
  149. package/src/texgen/noise2.js +210 -0
  150. package/src/texgen/textureAi.js +436 -0
  151. package/src/texgen/textureGenerators.js +516 -0
  152. package/src/texgen/texturePresets.js +490 -0
  153. package/src/texgen/textureSettings.js +342 -0
  154. package/src/texgen/textureThree.js +59 -0
  155. package/src/vegetation/flowerSpecies.js +15 -3
  156. package/src/vegetation/grassPalettes.js +153 -0
  157. package/src/vegetation/index.js +6 -0
  158. package/src/vegetation/stylizedBush.js +2 -0
  159. package/src/vegetation/stylizedFlower.js +82 -0
  160. package/src/vegetation/stylizedFlowers.js +48 -7
  161. package/src/vegetation/stylizedForest.js +29 -1
  162. package/src/vegetation/stylizedGrass.js +291 -56
  163. package/src/vegetation/stylizedTree.js +38 -2
  164. package/src/vegetation/stylizedTreeFoliage.js +2 -1
  165. package/src/vegetation/vegetationShaders.js +1110 -0
  166. package/src/vfxgen/INTEGRATION.md +145 -0
  167. package/src/vfxgen/core/burstBackbone.js +380 -0
  168. package/src/vfxgen/core/projectileCore.js +92 -0
  169. package/src/vfxgen/core/spriteShapes.js +98 -0
  170. package/src/vfxgen/core/trailRibbon.js +272 -0
  171. package/src/vfxgen/core/vfxRandom.js +29 -0
  172. package/src/vfxgen/effects/emitHelpers.js +37 -0
  173. package/src/vfxgen/effects/magicEffects.js +162 -0
  174. package/src/vfxgen/effects/movementEffects.js +87 -0
  175. package/src/vfxgen/effects/weaponEffects.js +118 -0
  176. package/src/vfxgen/index.js +18 -0
  177. package/src/vfxgen/moves/moveController.js +146 -0
  178. package/src/vfxgen/moves/moveLibrary.js +335 -0
  179. package/src/vfxgen/vfxPresets.js +98 -0
  180. package/src/vfxgen/vfxSettings.js +384 -0
  181. package/src/vfxgen/vfxSystem.js +449 -0
  182. package/src/vfxgen/weapons/stylizedWeapons.js +137 -0
  183. package/src/villagegen/index.js +4 -0
  184. package/src/villagegen/stylizedVillage.js +490 -0
  185. package/src/villagegen/villageArchetypes.js +160 -0
  186. package/src/villagegen/villageNames.js +40 -0
  187. package/src/villagegen/villageSites.js +105 -0
  188. package/src/water/sceneOverrideLayers.js +23 -0
  189. package/src/water/water.js +5 -0
  190. package/src/water/waterBreakerSystem.js +15 -1
  191. package/src/water/waterCurrentField.js +447 -0
  192. package/src/water/waterMaterial.js +12 -0
  193. package/src/water/waterNearshorePhase.js +320 -0
  194. package/src/water/waterScenePasses.js +83 -30
  195. package/src/water/waterSettings.js +325 -13
  196. package/src/water/waterShoreMaterial.js +322 -0
  197. package/src/water/waterShoreStateField.js +605 -0
  198. package/src/water/waterSurface.js +797 -28
  199. package/src/weather/index.js +6 -0
  200. package/src/weather/weatherPrecipitation.js +221 -0
  201. package/src/weather/weatherPresets.js +258 -0
  202. package/src/weather/weatherSettings.js +269 -0
  203. package/src/weather/weatherSystem.js +871 -0
  204. package/src/worldMinimap.js +62 -0
  205. package/src/worldPresets.js +4 -1
@@ -0,0 +1,135 @@
1
+ # Texture Lab and texgen
2
+
3
+ Texture Lab (`/texture-lab/`) generates **seamless procedural PBR textures
4
+ for anything** — stone, ground, wood, metal, fabric, ceramics, creatures,
5
+ liquids, sci-fi panels, stylized prints — entirely on the CPU, deterministic
6
+ per seed, with zero image assets. The generator ships as the
7
+ `@call-me-sensei/toonlab/texgen` cluster so you can bake the same maps in
8
+ your own app or in Node.
9
+
10
+ ## The lab
11
+
12
+ - **Gallery** — 60+ built-in material presets across ten categories, each
13
+ thumbnail baked live by the real generator. Your saved textures appear in
14
+ *Your library*.
15
+ - **Pattern stage** — the base generator (25 tileable patterns: fbm, ridged,
16
+ billow, turbulence, worley/voronoi variants, cracks, caustics, speckle,
17
+ bricks, tiles, hex, checker, grid, stripes, chevron, weave, basket weave,
18
+ scales, dots, marble, wood grain, …) plus two blendable detail layers.
19
+ Sliders irrelevant to the selected pattern hide automatically.
20
+ - **Color stage** — a five-stop height ramp (set *Band smoothness* to 0 for
21
+ hard cel bands), painterly hue/value jitter (optionally per pattern cell —
22
+ per-brick tint shifts), cavity darkening and ridge sheen for the
23
+ hand-painted read, and a final grade (hue/saturation/brightness/contrast/
24
+ gamma).
25
+ - **Overlays stage** — one-knob **wear macros** (*Damage* carves seeded
26
+ scratches and chips and roughens them; *Dirt* pools grime into crevices)
27
+ plus two masked colored overlays for moss, rust, grime, snow, patina,
28
+ stains. *Crevice bias* pools an overlay into recesses (+1) or caps ridges
29
+ (−1); each overlay can also shift roughness, height, and metalness (rust
30
+ strips metal).
31
+ - **Surface stage** — relief depth, derived normal strength, baked AO,
32
+ roughness base + height-correlated contrast, metalness, and an optional
33
+ emissive source (crevices / peaks / band / overlay) for lava, circuits,
34
+ force fields.
35
+ - **Preview** — lit 3D meshes (sphere, cube, cylinder, torus, knot, plane)
36
+ with live displacement, or a flat 2D sheet with a per-map view
37
+ (albedo/height/normal/roughness/metalness/AO/emissive) and 1–4× tiling to
38
+ eyeball the seams. `R` re-rolls the seed; keys 1–5 switch stages;
39
+ backtick opens the all-controls drawer.
40
+
41
+ ## Image base (bring your own image)
42
+
43
+ “Use an image as the base” (Pattern stage, or *From an image* in the
44
+ gallery) turns **a picture of one surface into a tiling toon material** —
45
+ a wall photo, a bark close-up, a fabric scan, a crop from concept art.
46
+ There is **no AI in this path**: it converts, it does not interpret, so a
47
+ whole scene or screenshot just becomes repeating wallpaper (the lab warns
48
+ when an upload looks like a scene — crop the material you want first;
49
+ scene → material-list breakdown is the planned pro tool). Mechanically:
50
+ the bitmap is seamless-ized (torus blend), relief is derived from
51
+ band-split luminance (*Relief detail* / *Relief base*), and an optional
52
+ *Cel bands* control quantizes it toward the toon look. The image replaces
53
+ only the **base layer** — detail layers, wear, overlays (moss a
54
+ photographed wall!), glow, cavity/sheen, and the color grade all still
55
+ apply, and every map derives as usual. The base pattern and height-ramp
56
+ controls disable with a hint while an image is active. Images are stored
57
+ in the browser only; share URLs strip them (Recipe JSON keeps them).
58
+ Library callers: `imageToTextureMaps(imagePixels, { params, settings,
59
+ size })` — decoding stays outside so texgen remains headless.
60
+
61
+ ## AI assist (bring your own key)
62
+
63
+ The **AI** stage maps plain language — *“old leather jacket”*, *“mossy
64
+ castle bricks”*, *“molten lava with glowing cracks”* — onto generator
65
+ parameters:
66
+
67
+ - **Built-in (no key)** — a deterministic offline mapper scores your words
68
+ against the preset library and applies wear/color modifiers (old, wet,
69
+ mossy, rusty, cracked, glowing, chunky, fine, color words…).
70
+ - **Gemini / OpenAI (your key)** — the lab sends a compact schema catalog
71
+ (generated from the live field schema, ~3k tokens) plus your description
72
+ to the model you name, and expects a small JSON recipe back: a base
73
+ preset, a 2–5 color palette, and a parameter patch. Cheap “mini” tiers
74
+ are plenty: the defaults are `gemini-2.5-flash-lite` and `gpt-5-mini`,
75
+ and the model id field is free text so any model you have access to
76
+ works. Every returned value is validated and clamped against the schema
77
+ before it touches your document.
78
+
79
+ Keys are stored in this browser's `localStorage` only and are sent solely
80
+ to `generativelanguage.googleapis.com` / `api.openai.com` — there is no
81
+ ToonLab server in the path. *Refine current* mode patches the texture you
82
+ are editing instead of starting fresh.
83
+
84
+ ## Export
85
+
86
+ The Export dialog bakes at 256–2048 px (independent of the preview) and
87
+ downloads:
88
+
89
+ - individual PNGs per map,
90
+ - one ZIP with the selected maps plus `recipe.json` (re-importable) and
91
+ `material.json` (usage hints),
92
+ - the recipe JSON on its own, or a **share URL** with the whole recipe
93
+ inlined (`?textureRecipe=`).
94
+
95
+ Maps: `albedo` + `emissive` are sRGB; `normal` (OpenGL +Y), `roughness`,
96
+ `metalness`, `ao`, `height`, and the glTF-style `orm` pack
97
+ (R=occlusion, G=roughness, B=metalness) are linear. Everything tiles
98
+ seamlessly — noise wraps its lattice periodically rather than blending
99
+ mirrored copies.
100
+
101
+ ## Library usage
102
+
103
+ ```js
104
+ import {
105
+ createTextureSettings,
106
+ evaluateTextureMaps,
107
+ syncTextureMapTextures,
108
+ findTexturePreset,
109
+ } from '@call-me-sensei/toonlab/texgen';
110
+
111
+ const settings = createTextureSettings(findTexturePreset('rusted-iron').settings);
112
+ const maps = await evaluateTextureMaps(settings, { size: 512 });
113
+
114
+ const { textures } = syncTextureMapTextures(maps); // THREE.DataTexture set
115
+ const material = new THREE.MeshStandardMaterial({
116
+ aoMap: textures.ao,
117
+ map: textures.albedo, // SRGBColorSpace, RepeatWrapping — pre-tagged
118
+ metalnessMap: textures.metalness,
119
+ metalness: 1,
120
+ normalMap: textures.normal,
121
+ roughnessMap: textures.roughness,
122
+ roughness: 1,
123
+ });
124
+ ```
125
+
126
+ `evaluateTextureMaps` is async and chunked (pass `onProgress` /
127
+ `shouldCancel`), headless-safe (no DOM), and deterministic: the same
128
+ settings always produce byte-identical maps. To map language to settings
129
+ yourself, use `keywordTextureRecipe(prompt)` (offline) or
130
+ `buildTextureAiPrompt` + `parseTextureAiResponse` +
131
+ `compileTextureAiRecipe` around any LLM call.
132
+
133
+ Settings documents are versioned (`kind: "toonlab.textureRecipe"`,
134
+ `version: 1`) and validated/clamped by `createTextureSettings` — unknown
135
+ keys are ignored, colors accept hex strings or `[r, g, b]` triplets.
@@ -0,0 +1,183 @@
1
+ # Toon character shading
2
+
3
+ Modern anime character shading for any Three.js character. One call
4
+ converts a loaded model's materials into the toon shader; everything else is
5
+ settings.
6
+
7
+ The implementation is TSL/NodeMaterial-only after the WebGPU cutover. It runs
8
+ on native WebGPU by default and on the TSL WebGL2 fallback with
9
+ `?renderer=webgl`; the old GLSL material path has been removed.
10
+
11
+ ```js
12
+ import { applyToonShader, createToonSettings } from '@call-me-sensei/toonlab/toon';
13
+
14
+ applyToonShader(characterRoot, {
15
+ settings: createToonSettings({
16
+ preset: 'default',
17
+ skinTone: { skinShadowBrightness: 0.94 },
18
+ rimLight: { hairIntensity: 0.2 },
19
+ }),
20
+ });
21
+ ```
22
+
23
+ (Inside this repo the labs import from `../../src/toon/...`.)
24
+
25
+ `applyToonShader(root, options)` traverses the model, classifies material
26
+ roles, converts materials in place, and adds the inverted-hull outline pass.
27
+ Settings groups can also be passed directly as options
28
+ (`applyToonShader(root, { rimLight: {...} })`). To re-tune an already
29
+ converted model at runtime, use
30
+ `applyToonSettingsToMaterial(target, settings)` — it applies uniform-safe
31
+ edits without reconversion (this is what the debug panel calls).
32
+
33
+ ## Settings groups
34
+
35
+ `createToonSettings(options)` merges your overrides over a preset over the
36
+ defaults, and returns the full nested settings object. There are 23 groups
37
+ (`TOON_SETTING_GROUPS`), each with its own module under `src/toon/settings/`:
38
+
39
+ | Group | What it controls |
40
+ |---|---|
41
+ | `baseTexture` | Preserves source texture, source material color, and saturation policy before toon lighting. |
42
+ | `materialRoles` | Classifies materials as skin, face, hair, eyes, costume, metal, transparent overlays, and outline. |
43
+ | `alpha` | Cutout, blend, opacity, eye overlay sorting, and transparent decoration behavior. |
44
+ | `skinTone` | Keeps skin and face shadows warm, readable, and separate from costume/hair shadows. |
45
+ | `faceLighting` | Overrides face-area cel response so noses, cheeks, and eyes do not receive harsh body shadows. |
46
+ | `celShade` | The primary directional cel band threshold, softness, and light-ignore amount. |
47
+ | `shadowColor` | Tints and reshapes lit-to-shadow transitions and fully shadowed regions. |
48
+ | `sceneShadow` | How renderer shadow maps darken character materials. |
49
+ | `selfShadow` | Character-local self shadow (dedicated shadow pass or scene-proxy source). |
50
+ | `averageShadow` | Averaged shadow visibility for softer role-specific shadow damping. |
51
+ | `indirectLight` | Mixes ambient, hemisphere, and environment light into toon shading. |
52
+ | `localLights` | Point and spot light response without overpowering cel bands. |
53
+ | `rimLight` | View-dependent edge light; classic fresnel or screen-space depth-texture mode. |
54
+ | `contactShadow` | Thin screen-space contact shadows (hair-on-face, arm-on-torso) from the depth prepass. |
55
+ | `specular` | Role-aware stylized highlights and optional source specular masks. |
56
+ | `hairHighlight` | Hair-specific highlight bands, optional anisotropic strand response, and source masks. |
57
+ | `eyeHighlight` | Role-aware eye/catchlight boosts and optional source masks. |
58
+ | `materialMaps` | Routes source normal, AO, emissive, MatCap, ramp, detail, roughness, metalness, and specular-color maps. |
59
+ | `outline` | The inverted-hull outline pass, including role-specific widths and colors. |
60
+ | `glitter` | Procedural view-dependent sparkles for costumes/accessories. Off by default. |
61
+ | `sticker` | Blends a decal/overlay texture into the albedo before lighting. Off by default. |
62
+ | `perspectiveRemoval` | Flattens perspective around the tracked head for anime-portrait closeups. Off by default. |
63
+ | `fur` | Opt-in shell fur for matched materials (collars, trims, animal parts). Off by default. |
64
+
65
+ Every field (298 of them) is listed with type, default, and range in the
66
+ generated [settings reference](settings-reference.md). The same schema
67
+ (`TOON_SETTING_FIELD_SCHEMA`) drives the [debug panel](debug-panel.md), so
68
+ each field is also a live slider in the Character Shader Lab.
69
+
70
+ ## Material roles
71
+
72
+ Materials are classified into roles before conversion — `default`,
73
+ `costume`, `skin`, `face`, `hair`, `eye`, `iris`, `pupil`, `sclera`,
74
+ `eyeHighlight`, `catchlight`, `blush`, `transparentOverlay`, `metal`,
75
+ `outline`. Roles decide which settings apply where (skin shadow tint, hair
76
+ highlights, outline widths, alpha behavior).
77
+
78
+ The default classifier is heuristic (names, textures, PMX conventions).
79
+ Override it without touching shader code:
80
+
81
+ ```js
82
+ applyToonShader(characterRoot, {
83
+ materialRoles: {
84
+ byName: { Face: 'face', Hair: 'hair' },
85
+ byUuid: { [material.uuid]: 'skin' },
86
+ patterns: [{ pattern: /eye.*highlight/i, role: 'eyeHighlight' }],
87
+ },
88
+ });
89
+ ```
90
+
91
+ A source material can also declare `material.userData.toonRole`. Inspect the
92
+ resolved roles with `?toonDebug=role`, or programmatically via
93
+ `document.body.dataset.materialRoleSummary` in the labs.
94
+
95
+ ## Presets and preset documents
96
+
97
+ Presets are named partial settings registered in a registry. Built-ins:
98
+ `default`, `call_me_sensei`, `showcase` (`TOON_PRESET_IDS`,
99
+ `getToonPresetOptions()`).
100
+
101
+ ```js
102
+ import {
103
+ registerToonPreset,
104
+ serializeToonPreset,
105
+ parseToonPresetDocument,
106
+ } from '@call-me-sensei/toonlab/toon';
107
+
108
+ // Register in code:
109
+ registerToonPreset('zzz_soft', {
110
+ label: 'ZZZ Soft',
111
+ description: 'Flatter urban anime lighting.',
112
+ settings: { hairHighlight: { mode: 'anisotropic' } },
113
+ });
114
+
115
+ // Share as a versioned JSON document (same shape the Character Shader Lab exports):
116
+ const json = serializeToonPreset('warm_skin_test', {
117
+ label: 'Warm Skin Test',
118
+ settings: { skinTone: { skinShadowBrightness: 0.94 } },
119
+ });
120
+
121
+ // Load one back, with validation:
122
+ const result = parseToonPresetDocument(json);
123
+ if (result.ok) registerToonPreset(result.value.id, result.value, { overwrite: true });
124
+ ```
125
+
126
+ Documents carry `{ type: 'toonlab/toon-preset', version, id,
127
+ label, description, settings }` and are validated field-by-field against the
128
+ schema (`validateToonPresetDocument`). Scalars, booleans, enums, colors, and
129
+ vectors serialize; runtime texture objects (mask maps) are intentionally
130
+ ignored by JSON validation — wire those in code. Related APIs:
131
+ `createToonPresetDocument`, `registerSerializedToonPreset`,
132
+ `sanitizeToonPresetSettings`, `getToonPresetDefinition`.
133
+
134
+ ## Render passes
135
+
136
+ `applyToonShader` alone produces a complete material.
137
+ `createCharacterRenderPasses` adds the per-frame passes that unlock the
138
+ screen-space and shadow-map features:
139
+
140
+ ```js
141
+ import { createCharacterRenderPasses } from '@call-me-sensei/toonlab/toon';
142
+
143
+ const passes = createCharacterRenderPasses({ renderer, scene, camera });
144
+ passes.registerCharacterRoot(modelRoot); // after applyToonShader
145
+
146
+ // in the render loop, before rendering:
147
+ passes.update();
148
+ ```
149
+
150
+ The passes:
151
+
152
+ 1. **Scene depth prepass** — feeds the depth-texture rim light mode and
153
+ contact shadows.
154
+ 2. **Character-only orthographic shadow map** — real self shadow
155
+ (`selfShadow` group; direction follows the main light or a
156
+ camera-relative art-directed angle).
157
+ 3. **Head bone tracking** — head-space face shading
158
+ (`faceLighting.headSpaceMode: 'headBone'`).
159
+ 4. **Average shadow measurement** — per-character uniform scene shadow.
160
+ 5. **Character mask** — character-aware bloom in the
161
+ [post pipeline](post-processing.md).
162
+
163
+ Every pass is auto-gated: it only renders when at least one registered
164
+ material actually consumes its output, so leaving the passes running with
165
+ the features disabled costs almost nothing.
166
+
167
+ ## Debug views
168
+
169
+ `?toonDebug=<mode>` in the labs, or `setToonDebugOutput(root, mode)` in
170
+ code, renders one shader term in isolation:
171
+
172
+ ```text
173
+ sourceAlbedo | albedo | band | shadow | selfShadow | directVisibility |
174
+ contactShadow | rim | depthRim | specular | hairHighlight | eyeHighlight |
175
+ normalMap | aoMap | emissiveMap | matcap | ramp | detailMap | roughnessMap |
176
+ metalnessMap | shadowColor | lit | role | alpha
177
+ ```
178
+
179
+ Typical tuning loop: `?toonDebug=band` while adjusting `celShade`,
180
+ `?toonDebug=shadowColor` for `shadowColor`, `?toonDebug=rim` for `rimLight`,
181
+ `?toonDebug=role` when a model misclassifies. The full mode map is
182
+ `TOON_DEBUG_OUTPUT_MODES` (aliases included); debug branches compile out
183
+ when off.
@@ -0,0 +1,167 @@
1
+ # TSL Porting Conventions (three r185+)
2
+
3
+ House rules for porting the GLSL shader library to TSL/NodeMaterial during
4
+ the WebGPU migration. Everything here was
5
+ verified against `three@0.185.1` on both node backends — the WGSL
6
+ builder (WebGPU) and the GLSL builder (`forceWebGL: true`). Re-validate the
7
+ "r185 quirk" items on any version bump.
8
+
9
+ ## File layout
10
+
11
+ - `src/shaders-tsl/` is the canonical shader library: one module per material
12
+ (`sky.js`, `anime.js`, …), one module per reusable chunk under
13
+ `src/shaders-tsl/chunks/` (`water-common.js`, `character-lighting.js`, …).
14
+ The retired raw GLSL tree (`src/shaders/`) and `shaderSource.js` registry
15
+ were removed in Phase 11.
16
+ - Leaf helpers that map nodes→nodes are `Fn()` exports. Helpers with
17
+ compile-time-constant parameters (loop counts, feature flags) are plain JS
18
+ functions that unroll at graph-build time.
19
+ - Chunks that need material uniforms export a factory
20
+ (`createXxxChunk({ u, tex, v, flags })`) returning the chunk's functions;
21
+ the shader module assembles them. `u` = uniform nodes under GLSL names,
22
+ `tex` = texture nodes, `v` = varyings, `flags` = compile-time booleans.
23
+
24
+ ## The `.uniforms` compatibility surface
25
+
26
+ TSL material factories attach `material.uniforms` — a map of UniformNodes
27
+ (and TextureNodes) keyed by the **exact GLSL uniform names**. UniformNode and
28
+ TextureNode both expose `.value`, so every existing write-through
29
+ (`mat.uniforms.celShadeMidPoint.value = x`, HUD panels, characterRenderPasses
30
+ `setUniform`) works identically on both backends. Do not rename uniforms in
31
+ a port.
32
+
33
+ ## Feature gating
34
+
35
+ - Former GLSL `#ifdef USE_X` sampler-presence gates are now JS
36
+ `if (flags.hasX)` graph-build gates. Absent maps never enter the graph, so
37
+ no texture/bind slots are wasted. Keep these gates: both native WebGPU (with
38
+ default requested limits) and the forced WebGL2 fallback currently have a
39
+ 16-sampled-texture ceiling.
40
+ - GLSL `uniform bool useX` runtime toggles → keep as uniform-driven
41
+ `If()`/`select()`, exactly like the GLSL branch.
42
+ - Debug views are always compiled into TSL materials when available; the
43
+ selector is a pure uniform write.
44
+
45
+ ## r185 gotchas (each cost real debugging time)
46
+
47
+ 1. **`matN()` scalar constructors are three.js ROW-major** (`Matrix2`
48
+ constructor docs), the transpose of GLSL's column-major `matN()`.
49
+ Transpose the scalar order when porting, or write the multiply
50
+ component-wise. Constructors from column *vectors* (`mat4(v0,v1,v2,v3)`)
51
+ match GLSL. Symptom: fbm/rotation patterns differ subtly.
52
+ 2. **`select()` operands must be pure expressions.** A branch that creates a
53
+ var (`.toVar()`) or calls an `Fn()` crashes the GLSL builder with
54
+ `Cannot read properties of undefined (reading 'addToStack')` — the
55
+ ConditionalNode type-resolution fallback builds the operand subtree in a
56
+ detached (stack-less) flow. Referencing vars created *outside* the select
57
+ is fine. Hoist calls into `If()`-assigned vars instead.
58
+ 3. **Deep nested `select()` chains** (the 24-entry debug table) hit the same
59
+ fallback. Use masked arithmetic sums
60
+ (`Σ value.mul(select(cond, 1, 0))`) for big tables, and never leave a
61
+ ConditionalNode as the `fragmentNode` root (wrap with `mix()` or `vec4()`).
62
+ 4. **All mutating node code needs an active stack** — build vertex work
63
+ inside one `Fn()` assigned to `vertexNode`, declare varyings up front
64
+ (`varying(vec3(), 'vName')`) and `.assign()` them inside; the declarative
65
+ `varying(Fn(...)())` form breaks the GLSL builder.
66
+ 5. **Depth-texture sampling types differently per builder** (WGSL: `float`,
67
+ GLSL: `vec4` snippet under a `float` node type → compile error). Passes
68
+ that feed shaders write linear window depth into **float COLOR targets**
69
+ instead; `[0,1]` window depth is numerically identical on both coordinate
70
+ systems (perspective *and* orthographic), so `perspectiveDepthToViewZ`
71
+ works unchanged.
72
+ 6. **Render targets are written top-down on BOTH node backends.** Manual
73
+ shadow/projective sampling (matrix-composed uv) needs a y-flip that the
74
+ classic pipeline didn't; WebGPU additionally has clip z in [0,1] (the GLSL
75
+ `*0.5+0.5` contract needs a `z' = 2z − 1` pre-stretch). Compose both into
76
+ the CPU-side matrix (see characterRenderPasses `shadowClipAdjust*`).
77
+ three's own `screenUV` node already handles orientation — prefer it over
78
+ `gl_FragCoord`-style math.
79
+ 7. **SkinningNode stores bone matrices in a uniform buffer** —
80
+ `GL_MAX_UNIFORM_BLOCK_SIZE` (16KB ≈ 256 bones) breaks MMD-scale skeletons
81
+ on the WebGL2 backend. `withToonStorageSkinning()`
82
+ (chunks/character-skinning.js) reroutes skinned meshes to a
83
+ `storage(...).setPBO(true)` bone buffer (emitted as a DataTexture +
84
+ texelFetch on GLSL). Note: the GLSL builder **replaces the attribute
85
+ array** with a padded copy at PBO setup — per-frame updates must copy
86
+ `skeleton.boneMatrices` into `attribute.array` and flag
87
+ `attribute.pbo.needsUpdate`.
88
+ 8. **Guard uniform-dependent divisions.** GLSL branches skipped
89
+ `direction.xz / (up + 0.28)` below the horizon; a masked straight-line
90
+ port produces NaN·0 = NaN. Keep the `If()` structure (uniform conditions
91
+ are fine in WGSL uniformity analysis) or clamp the divisor.
92
+ 9. **RT-fed textures sample at `.level(0)`** — no mips exist and WGSL forbids
93
+ implicit-derivative sampling in non-uniform control flow.
94
+ 10. **`MeshDepthMaterial` (and other non-node classics) don't auto-convert**
95
+ on the node renderer (`NodeBuilder: Material "MeshDepthMaterial" is not
96
+ compatible`). Standard lit materials do. Pass materials are node-built.
97
+ 11. **`renderer.capabilities` doesn't exist on WebGPURenderer** — use
98
+ `renderer.getMaxAnisotropy()` etc. with optional chaining at seams shared
99
+ by both renderers.
100
+ 12. **KTX2 `detectSupport(renderer)` needs an initialized backend** — gate
101
+ loader configuration (and anything else probing capabilities) on
102
+ `await renderer.init()` / `whenRendererReady()`.
103
+ 13. **`UniformArrayNode.value` is the packed GPU buffer** (null until first
104
+ setup); the authoring array is `.array`. Expose array uniforms on the
105
+ `.uniforms` surface as `{ value: node.array, node }` wrappers so
106
+ `uniforms.uWavesA.value[i].set()` write-throughs keep working (water).
107
+ 14. **Depth-as-color passes get fogged by `NodeMaterial.setupOutput`** in
108
+ fogged scenes — distant depth blends toward the fog color. Null
109
+ `scene.fog` for the pass render and force `fog: false` on the variant
110
+ materials (water depth pass; the environment sun-shadow pass has the
111
+ same latent hazard in any future fogged scene).
112
+ 15. **The node renderer force-rebuilds a camera's projection on
113
+ `coordinateSystem` mismatch** (and on its first reversed-depth pass) —
114
+ copy `camera.coordinateSystem` onto any manual virtual/pass camera or a
115
+ hand-mutated projection matrix (oblique clip) gets wiped on first render
116
+ (planar reflection, water reflection).
117
+ 16. **`geometry.setDrawRange` doesn't limit instanced draws** on the node
118
+ renderer — set `geometry.instanceCount` instead (rain intensity).
119
+ 17. **Shared uniform NODES must be adopted at graph build time** — swapping
120
+ entries in a material's `.uniforms` map after build changes nothing (the
121
+ map is a compatibility view, not the graph). Rebuild the material (or
122
+ build it with the shared nodes from the start) when wiring cross-material
123
+ shared uniforms (`attachWaveUniforms`).
124
+ 18. **The classic pipeline's baked `colorspace_fragment` is per-render-target**
125
+ — `linearToOutputTexel` compiles against the BOUND target's
126
+ `texture.colorSpace`, so an offscreen NoColorSpace target holds LINEAR
127
+ color on classic despite the include. When porting a pass that reads such
128
+ a target, do NOT add a compensating transfer (post composite: a drafted
129
+ OETF-at-read washed the frame +191/255). The single encode happens at the
130
+ canvas draw on both pipelines.
131
+
132
+ ## Scene lights
133
+
134
+ Custom lighting models that need raw light data (main-light direction, toon
135
+ banding per light, shadow mask separate from color) don't fit the node
136
+ LightingModel shape. `chunks/character-scene-lights.js` mirrors the scene's
137
+ lights into shared uniforms once per frame (from `Object3D.onBeforeRender`,
138
+ which both renderers call) replicating three's WebGLLights view-space
139
+ conventions and attenuation math. Materials share the module-level uniform
140
+ nodes, so one sync updates every toon material.
141
+
142
+ ## Renderer/backends
143
+
144
+ - `?renderer=` flag: absent / `webgpu` = native WebGPU; `webgl` = TSL through
145
+ `WebGPURenderer({ forceWebGL: true })`; `webgpu-forced-gl` = compatibility
146
+ alias for `webgl`.
147
+ `labs/shared/rendererKind.js` resolves it; `labs/shared/rendererFactory.js`
148
+ creates the renderer (sync create, async `init()` gate via
149
+ `whenRendererReady`), sets the `src/core/shaderBackend.js` marker, and
150
+ reports `document.body.dataset.rendererKind` / `.rendererBackend`
151
+ (`webgpu` | `webgl2-fallback` — capture scripts assert this).
152
+ - Material factories in `src/` are TSL-first. Some helpers still branch on
153
+ `isTslBackend()` for shared WebGPU/forced-WebGL2 code, but the classic
154
+ `WebGLRenderer`/GLSL material path is gone.
155
+ - Headless WebGPU (Playwright): launch with
156
+ `--enable-unsafe-webgpu --enable-gpu` → hardware Metal adapter on macOS.
157
+ Captures: `TOON_BASELINE_RENDERER=webgpu|webgl|webgpu-forced-gl
158
+ TOON_BASELINE_SCOPE=ganyu npm run baseline:capture` (backend mismatch fails
159
+ the capture).
160
+
161
+ ## Verification
162
+
163
+ WebGL headless captures are byte-deterministic (noise floor zero) — any
164
+ classic-path diff is a real regression. TSL-vs-WebGL diffs bottom out at
165
+ fp-level noise around toon band/fbm thresholds (mean ≈ 1/1020 per pixel);
166
+ structural differences always traced to a real porting bug, so do not accept
167
+ "looks close" while a debug view disagrees.