@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,144 @@
1
+ # Characters and animation
2
+
3
+ Bring-your-own-model is a first-class feature: any humanoid character in a
4
+ common format gets loaded, toon-shaded, and animated — without code changes
5
+ in the labs, with rig helpers in `@call-me-sensei/toonlab/character` and optional loader
6
+ helpers in `@call-me-sensei/toonlab/loaders` for your own app.
7
+
8
+ ## Loading models
9
+
10
+ ```js
11
+ import { loadModelAsset, SUPPORTED_MODEL_FORMATS } from '@call-me-sensei/toonlab/loaders';
12
+
13
+ const asset = await loadModelAsset('models/hero.vrm');
14
+ // asset: { root, clips, format, url, resourcePath, vrm? }
15
+ scene.add(asset.root);
16
+ ```
17
+
18
+ (Inside this repo the labs import from `../../src/character/...`.)
19
+
20
+ Supported formats (`SUPPORTED_MODEL_FORMATS`): `pmx`, `pmd`, `fbx`, `glb`,
21
+ `gltf`, `vrm` (0 and 1), `obj` (pass `materialUrl` for the MTL), and
22
+ text-based `usdz`. VRM models are detected automatically on glTF load,
23
+ rotated to the +Z convention, and returned with the `vrm` instance.
24
+
25
+ Draco or KTX2-compressed glTF assets need an explicit decoder directory:
26
+
27
+ ```js
28
+ await loadModelAsset('models/compressed.glb', {
29
+ decoderBasePath: '/vendor/three-decoders/',
30
+ renderer,
31
+ });
32
+ ```
33
+
34
+ In the labs, use the HUD **Model URL** input or `?model=` — local paths and
35
+ hosted URLs both work (hosted URLs need CORS headers on the server; load
36
+ failures surface in a HUD banner). Private test models go in the gitignored
37
+ `assets-local/models/` drop-in folder: after `npm run assets:local` they
38
+ surface in every model-aware lab's Model select, no code changes. See
39
+ [getting-started.md](getting-started.md#loading-your-own-models) for the
40
+ folder shapes the scanner recognizes.
41
+
42
+ ## Bone-role adapters (`characterRig`)
43
+
44
+ Animation is rig-agnostic because bone *names* are resolved to canonical
45
+ humanoid *roles* (VRM humanoid naming, `HUMANOID_ROLES`) once, up front:
46
+
47
+ ```js
48
+ import { resolveCharacterRig, targetBoneNameForRole } from '@call-me-sensei/toonlab/character';
49
+
50
+ const rig = resolveCharacterRig(skinnedMesh, { vrm: asset.vrm });
51
+ // rig: { type, targetToMixamo, mixamoToTarget, hipCarrierName } — or null
52
+ targetBoneNameForRole(rig, 'head'); // the model's actual head bone name
53
+ ```
54
+
55
+ `resolveCharacterRig` tries each convention adapter in order:
56
+
57
+ 1. **VRM** — the humanoid bone map from the VRM extension (spec'd, exact).
58
+ 2. **MMD/PMX** — Japanese bone names (`頭`, `下半身`, ...), including the
59
+ center-bone hip carrier.
60
+ 3. **Mixamo-named** — skeletons already using `mixamorig` names (normalized
61
+ for the common prefix variants).
62
+ 4. **Rigify** — Blender Rigify `DEF-` deform bones, matched loosely
63
+ (heuristic name matching handles export renaming).
64
+
65
+ If no adapter matches, retargeting is impossible and models fall back to
66
+ their embedded clips. Everything downstream (retargeting, the procedural
67
+ swim clip, head tracking) works through roles, so a new rig convention is
68
+ one name table, not a new pipeline.
69
+
70
+ ## Native-clip fast path
71
+
72
+ Models that ship their own locomotion clips skip retargeting entirely. A
73
+ model qualifies when at least idle + walk resolve against these clip-name
74
+ conventions (`NATIVE_LOCOMOTION_CLIP_NAMES` in
75
+ `labs/playground/animationPipeline.js`; first match per role wins):
76
+
77
+ | Role | Accepted clip names |
78
+ |---|---|
79
+ | idle | `Idle_Loop`, `Idle` |
80
+ | walk | `Walk_Loop`, `Walking`, `Walk` |
81
+ | run | `Sprint_Loop`, `Jog_Fwd_Loop`, `Running`, `Run` |
82
+ | jump | `Jump_Start`, `Jump_Loop`, `Jump` |
83
+ | swim | `Swim_Fwd_Loop`, `Swimming`, `Swim` |
84
+ | tread | `Swim_Idle_Loop`, `Treading_Water`, `TreadingWater` |
85
+
86
+ The bundled CC0 mannequin (Quaternius Universal Animation Library, 45
87
+ embedded clips) qualifies out of the box — that is why a fresh clone
88
+ animates with zero downloads. Author your own GLBs to these names and they
89
+ play natively too.
90
+
91
+ ## Mixamo retarget pipeline
92
+
93
+ For models without native clips, the Playground retargets Mixamo FBX clips
94
+ onto the resolved rig with a world-space bake: only the role mapping comes
95
+ from `characterRig`; rest-pose differences (T-pose vs. A-pose) and hierarchy
96
+ mismatches are solved numerically. This is how a PMX character, a VRM
97
+ avatar, and a Rigify export all play the same `Walking.fbx`.
98
+
99
+ Adobe's terms do not permit redistributing Mixamo clips, so the repo ships
100
+ none. Download clips with your own Adobe account from
101
+ [mixamo.com](https://www.mixamo.com) and drop them into
102
+ `assets-local/animations/` (`Idle.fbx`, `Walking.fbx`, `Running.fbx`,
103
+ `Jump.fbx`, `Swimming.fbx`, `Treading_Water.fbx`, ...). Override individual
104
+ clips per URL: `?swimAnim=`, `?treadAnim=`, `?diveAnim=`, `?anim=`. See
105
+ [ATTRIBUTION.md](../ATTRIBUTION.md).
106
+
107
+ When a swim/tread retarget source is missing, the pipeline falls back to the
108
+ model's native swim clips if it has them.
109
+
110
+ ## Procedural freestyle swim
111
+
112
+ `createFreestyleSwimClip(targetMesh, rig, options)` generates a freestyle
113
+ (front crawl) swim clip with no FBX source: windmilling catch-up arm
114
+ strokes, flutter kick, body roll, and a breath every fourth stroke. Motion
115
+ is authored as world-space rotation deltas against the bind pose, so the
116
+ same code bakes onto any supported skeleton (VRM, MMD, Mixamo-named,
117
+ Rigify).
118
+
119
+ ```js
120
+ import { createFreestyleSwimClip } from '@call-me-sensei/toonlab/character';
121
+
122
+ // Skeleton must be in bind pose when you call this.
123
+ const clip = createFreestyleSwimClip(targetMesh, rig, {
124
+ clipName: 'FreestyleSwim',
125
+ trackNameStyle: 'skeleton', // 'skeleton' for retarget-style mixers, 'node' for native-clip mixers
126
+ });
127
+ mixer.clipAction(clip).play();
128
+ ```
129
+
130
+ The Playground uses it as the default swim style (`?freestyleAnim=none`
131
+ disables it).
132
+
133
+ ## swimVisualLift
134
+
135
+ Swim physics floats every model at the same capsule depth below the wave
136
+ surface — the whole swim backend (vertical control, clamps, ray geometry,
137
+ wave exposure) is tuned as a package around one value, and giving models
138
+ different physics floats re-exposes tuning bugs. Models that need to *look*
139
+ higher or lower in the water get `swimVisualLift`: a visual-only vertical
140
+ offset applied to the model group while swimming, which the physics never
141
+ sees. The compact mannequin uses `0.18` to read as surface freestyle; a
142
+ character with tall hair volume needs none. Per-model values live in the
143
+ lab's character options (`labs/shared/sceneHub.js`); override for testing
144
+ with `?swimVisualLift=`.
@@ -0,0 +1,126 @@
1
+ # Debug panel
2
+
3
+ `@call-me-sensei/toonlab/debug` is an optional, DOM-dependent tuning GUI: it turns any
4
+ settings module's group/field schema into live controls. One call gives you
5
+ the same panel the labs use — ship it behind a debug flag, or not at all
6
+ (nothing else imports it).
7
+
8
+ The labs now expose one TSL renderer stack: native WebGPU by default and
9
+ `?renderer=webgl` for the WebGL2 fallback. The panel writes the same settings
10
+ surface on both backends.
11
+
12
+ ## createSettingsPanel
13
+
14
+ ```js
15
+ import { createSettingsPanel } from '@call-me-sensei/toonlab/debug';
16
+ import { readFieldValueFromSettings } from '@call-me-sensei/toonlab/debug';
17
+ import {
18
+ TOON_SETTING_GROUPS,
19
+ TOON_SETTING_FIELD_SCHEMA,
20
+ applyToonSettingsToMaterial,
21
+ } from '@call-me-sensei/toonlab/toon';
22
+
23
+ const panel = createSettingsPanel({
24
+ container: document.getElementById('toonSettingGroups'),
25
+ groups: TOON_SETTING_GROUPS,
26
+ fieldSchema: TOON_SETTING_FIELD_SCHEMA,
27
+ getValue: (field) => readFieldValueFromSettings(settings, field),
28
+ onChange: (field, value) => {
29
+ settings[field.group][field.key] = value;
30
+ applyToonSettingsToMaterial(characterRoot, settings);
31
+ },
32
+ });
33
+
34
+ panel.refresh(); // re-pull every control value via getValue (e.g. after a preset switch)
35
+ ```
36
+
37
+ (Inside this repo the labs import from `../../src/debug/...`.)
38
+
39
+ The panel renders one collapsible `<details>` group per schema group, with a
40
+ typed control per field — checkbox for `boolean`, `<select>` for `select`
41
+ (using the schema's `options`/`optionLabels`), color input for `color`,
42
+ range slider for `number` (using the schema's `range`), text input
43
+ otherwise — each with a label, tooltip description, and live `<output>`
44
+ readout.
45
+
46
+ The host owns the settings state: the panel only reads through `getValue`
47
+ and reports edits through `onChange`. It never mutates settings itself,
48
+ which is what makes it schema-generic.
49
+
50
+ ### Options
51
+
52
+ Beyond the required five, `createSettingsPanel` accepts:
53
+
54
+ - `fieldFilter(field)` — hide fields (e.g. non-serializable texture slots).
55
+ - `isDisabled(field)` — gray controls out (re-evaluated on `refresh()`).
56
+ - `formatValue(value, field)` — custom `<output>` formatting.
57
+ - `isGroupOpen(group)` — which groups start expanded.
58
+ - `prepend` — DOM nodes to insert before the groups (preset pickers, etc.).
59
+ - `dataAttribute`, `idPrefix`, `rowClassName`, `groupClassName`,
60
+ `fieldsClassName` — styling/data hooks.
61
+
62
+ ### It works with every schema
63
+
64
+ Any module following the groups + field-schema convention plugs in — this is
65
+ the full list (see the generated
66
+ [settings reference](settings-reference.md) for their contents):
67
+
68
+ | Module | groups / fieldSchema |
69
+ |---|---|
70
+ | `@call-me-sensei/toonlab/toon` | `TOON_SETTING_GROUPS` / `TOON_SETTING_FIELD_SCHEMA` |
71
+ | `@call-me-sensei/toonlab/environment` | `ENVIRONMENT_SETTING_GROUPS` / `ENVIRONMENT_SETTING_FIELD_SCHEMA` |
72
+ | `@call-me-sensei/toonlab/water` | `WATER_SETTING_GROUPS` / `WATER_SETTING_FIELD_SCHEMA_BY_GROUP` |
73
+ | `@call-me-sensei/toonlab/post` | `POST_PROCESSING_SETTING_GROUPS` / `POST_PROCESSING_SETTING_FIELD_SCHEMA` |
74
+ | `@call-me-sensei/toonlab/vegetation` (grass) | `GRASS_SETTING_GROUPS` / `GRASS_SETTING_FIELD_SCHEMA` |
75
+ | `@call-me-sensei/toonlab/vegetation` (flowers) | `FLOWER_SETTING_GROUPS` / `FLOWER_SETTING_FIELD_SCHEMA` |
76
+ | `@call-me-sensei/toonlab/vegetation` (trees) | `STYLIZED_TREE_SETTING_GROUPS` / `STYLIZED_TREE_SETTING_FIELD_SCHEMA` |
77
+ | `@call-me-sensei/toonlab/sky` | `SKY_SETTING_GROUPS` / `SKY_SETTING_FIELD_SCHEMA` |
78
+
79
+ Note the value-read difference: toon/environment/post/tree settings are
80
+ nested (`settings[field.group][field.key]`), water/grass/flower/sky settings
81
+ are flat (`settings[field.key]`). `readFieldValueFromSettings` handles the
82
+ nested case; for flat settings pass
83
+ `getValue: (field) => settings[field.key]`.
84
+
85
+ ## createSelectRow
86
+
87
+ A plain labeled `<select>` row for the controls that live alongside the
88
+ schema-generated groups (preset pickers, debug-view selectors):
89
+
90
+ ```js
91
+ import { createSelectRow } from '@call-me-sensei/toonlab/debug';
92
+
93
+ const row = createSelectRow({
94
+ id: 'toonPreset',
95
+ label: 'Preset',
96
+ options: getToonPresetOptions().map((p) => ({ value: p.id, label: p.label })),
97
+ value: currentPreset,
98
+ onChange: (value) => setLabParams({ toonPreset: value }),
99
+ });
100
+ panelHeader.append(row);
101
+ ```
102
+
103
+ ## Field value helpers (`fieldValues.js`)
104
+
105
+ The coercion layer between schema-typed values and DOM control values, used
106
+ by the panel and exported for custom UIs:
107
+
108
+ - `readFieldValueFromSettings(settings, field)` — nested group/key read.
109
+ - `readFieldValueFromControl(control, field)` — checkbox/range/color/select
110
+ → typed value (colors become `[r, g, b]` arrays, numbers parse, etc.).
111
+ - `writeFieldControlValue(control, output, field, value)` — push a value
112
+ into a control and its `<output>`.
113
+ - `formatFieldValue(value, field)` — display formatting.
114
+ - `colorToHex(value)` / `hexToColorArray(value)` — `[r, g, b]` ↔ `#rrggbb`.
115
+ - `vectorToArray(value)` — vector-ish → plain array.
116
+
117
+ ## How the labs use it
118
+
119
+ Character Shader Lab and Environment Shader Lab consume their respective
120
+ schemas independently. Each lab's panel is a thin consumer that wires
121
+ `getValue`/`onChange` to its preset store and to
122
+ `applyToonSettingsToMaterial` or `applyEnvironmentSettingsToMaterial` for
123
+ live uniform-safe edits. Preset switching refreshes the controls from the
124
+ normalized package settings. The same schema-consumer pattern drives Water,
125
+ Sky, Grass, Tree, and Flower Labs; no lab keeps a private copy of its runtime
126
+ parameter contract.