@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
package/docs/water.md ADDED
@@ -0,0 +1,430 @@
1
+ # Water
2
+
3
+ A modern anime-style stylized, interactive water system. Fully procedural — no
4
+ texture assets — with the whole wave spectrum mirrored on the CPU so physics
5
+ and rendering always agree.
6
+
7
+ Water materials and simulation passes are TSL-only. They run on native WebGPU
8
+ by default and on the TSL WebGL2 fallback with `?renderer=webgl`.
9
+
10
+ Water Lab authors one complete runtime-system preset. Its Surface, Foam, and
11
+ Lighting groups are the embedded water-shader controls; Waves, Ripples,
12
+ Splashes, and Quality own the coupled runtime behavior. ToonLab intentionally
13
+ does not split those values into a separate Water Shader Lab, which would
14
+ produce two documents with overlapping ownership. See
15
+ [Lab responsibilities](lab-architecture.md).
16
+
17
+ ## Quickstart
18
+
19
+ ```js
20
+ import { WaterSurface } from '@call-me-sensei/toonlab/water';
21
+ import { StylizedSky } from '@call-me-sensei/toonlab/sky';
22
+
23
+ const water = new WaterSurface({
24
+ width: 200, depth: 200, preset: 'lake',
25
+ // Optional terrain sampler enabling surf mechanics: waves shoal, break,
26
+ // and wash a swash film up the beach. breakerAmount > 0 adds plunging
27
+ // breaker shells that ride each set wave to the break line.
28
+ bedHeight: (x, z) => terrainHeightAt(x, z),
29
+ });
30
+ water.position.y = 0.4;
31
+ scene.add(water);
32
+
33
+ const sky = new StylizedSky(); // shows up in the water's reflections
34
+ scene.add(sky);
35
+
36
+ water.addInteractor(characterObject3D, { radius: 0.35 });
37
+ water.setFollowTarget(characterObject3D); // ripple window follows across big water
38
+
39
+ // Current scene light may replace the preset's standalone fallback values.
40
+ water.setSceneOverrides({
41
+ sunDirection: lightingState.sunDirection,
42
+ sunColor: lightingState.sunColor,
43
+ skyZenithColor: lightingState.skyZenithColor,
44
+ skyHorizonColor: lightingState.skyHorizonColor,
45
+ });
46
+
47
+ // per frame, before renderer.render(scene, camera):
48
+ water.update(renderer, scene, camera, delta);
49
+ sky.update(delta, camera);
50
+
51
+ // interactions & physics
52
+ water.splash({ x, y, z }, { strength: 1.2 });
53
+ water.addRipple({ x, z }, { radius: 0.3, strength: 0.5 });
54
+ const surfaceY = water.getHeightAt(x, z); // buoyancy — includes swell + breakers
55
+ const flow = water.getFlowAt(x, z); // surge velocity (breaker whitewater)
56
+ ```
57
+
58
+ (Inside this repo the labs import from `../../src/water/...`.)
59
+
60
+ Constraints: the surface must stay axis-aligned (translation only), and
61
+ depth-based effects assume a perspective camera. Above water,
62
+ `WaterScenePasses` renders a submerged-scene color+depth grab and a planar
63
+ reflection. Below water, that color target becomes a same-pose air-side
64
+ transmission capture with an oblique water-plane clip, while the planar
65
+ reflection and scene-depth pass are skipped. Exclude objects with
66
+ `userData.waterExclude` (all water passes), `userData.waterGrabExclude`
67
+ (above-water grab only), or `userData.waterReflectionExclude` (reflection
68
+ only).
69
+
70
+ ## Settings, presets, tones
71
+
72
+ Water settings are flat (`createWaterSettings({ preset: 'ocean',
73
+ waveIntensity: 0.6 })`); all 82 fields across 7 groups (waves, surface,
74
+ foam, lighting, ripples, splashes, quality) are in the
75
+ [settings reference](settings-reference.md). Highlights:
76
+
77
+ - **`waveIntensity`** — the authored baseline scales the whole Gerstner
78
+ spectrum from glassy mirror to storm swell. Components are slope-limited,
79
+ so big dials stretch to long wavelengths instead of spiking. Current
80
+ Weather may transiently modulate this baseline without editing the preset.
81
+ - **Wave sets** — `waveSetPeriod`/`waveSetStrength` make big waves arrive in
82
+ groups with lulls between, marching at group velocity.
83
+ - **Body color** — three-stop absorption (`shallowColor → midColor →
84
+ deepColor`), separate from wave motion. `colorTone` picks a named palette
85
+ from `WATER_COLOR_TONES`: `classic`, `anime`, `teal`, `caribbean`,
86
+ `lagoon`, `deepOcean`.
87
+ - **Underwater transmission** — `indexOfRefraction` anchors the Snell window
88
+ and total internal reflection; `underwaterTransmission` controls how much
89
+ of the captured air-side scene comes through; `underwaterTintStrength`
90
+ keeps that view inside the authored water palette.
91
+ - **Shore** — `shoalingDepth`, `shorelineWaves`, `shorelineRunup`, and
92
+ `runupDistance` tune surf and swash reach. With an explicit
93
+ `runupDistance`, event peaks vary over 80–100% of that bound and the next
94
+ uprush starts at the preceding rundown endpoint rather than resetting.
95
+ `breakerEnabled/breakerAmount/breakerCurl/breakerScale/breakerPeel`
96
+ control the plunging-breaker shells.
97
+ - **Persistent beach state** — `swashFoamAmount`, `swashFoamLifetime`,
98
+ `swashFoamResidueLifetime`, `wetSandDryTime`, `wetSandDarkening`, and
99
+ `wetSandSheen` are separate from offshore `foamAmount`. They take effect
100
+ when the surface is constructed with a `shoreState` field and the beach
101
+ uses a shore-state material.
102
+
103
+ Built-in presets (`WATER_PRESET_NAMES`): `mirror`, `calm`, `lake`, `river`,
104
+ `coast`, `ocean`, `storm` — plus `call_me_sensei`, the studio-managed
105
+ signature preset (curated and updated over releases). Apply at construction
106
+ (`preset:`) or live with `water.setPreset(name, overrides)` /
107
+ `water.applySettings(options)`.
108
+
109
+ ### Authored baseline vs. current scene
110
+
111
+ `water.settings` is always the authored, portable baseline. Body palette,
112
+ wave structure, foam, ripple/splash response, and water-specific lighting
113
+ response belong there. `waveIntensity` is also saved as the intended calmness
114
+ of that body of water.
115
+
116
+ `sunDirection`, `sunColor`, `skyZenithColor`, and `skyHorizonColor` remain
117
+ portable authored fallbacks so a water asset renders correctly in isolation,
118
+ previews, and scenes without a connected lighting rig. A live scene should
119
+ replace those current values transiently. Weather may likewise add temporary
120
+ wave energy:
121
+
122
+ ```js
123
+ import {
124
+ WATER_SCENE_OVERRIDE_PRIORITIES,
125
+ } from '@call-me-sensei/toonlab/water';
126
+
127
+ const weatherLayer = Symbol('weather-water');
128
+ water.setSceneOverrideLayer(weatherLayer, (base) => ({
129
+ waveIntensity: Math.min(base.waveIntensity + currentWaterWaveBoost, 1),
130
+ }), { priority: WATER_SCENE_OVERRIDE_PRIORITIES.weather });
131
+
132
+ // Removes Weather only; a separate Lighting layer remains active.
133
+ water.clearSceneOverrideLayer(weatherLayer);
134
+ ```
135
+
136
+ `setSceneOverrides()` is the convenience single-scene layer, and
137
+ `clearSceneOverrides()` removes only that layer. Named owners should clear their
138
+ own layer with `clearSceneOverrideLayer(id)`; `clearAllSceneOverrideLayers()` is
139
+ available for an explicit full teardown. `applySettings()` and `setPreset()`
140
+ keep their existing behavior as authored edits and automatically recompose
141
+ active runtime layers. `water.renderedSettings` exposes the composed state while
142
+ exports continue to read `water.settings`. Runtime layers accept only
143
+ `waveIntensity` and the four fallback sun/sky fields, so scene code cannot
144
+ accidentally overwrite the water palette or wave structure.
145
+
146
+ ### Registering and sharing presets
147
+
148
+ ```js
149
+ import {
150
+ registerWaterPreset,
151
+ serializeWaterPreset,
152
+ parseWaterPresetDocument,
153
+ registerSerializedWaterPreset,
154
+ } from '@call-me-sensei/toonlab/water';
155
+
156
+ registerWaterPreset('harbor', { waveIntensity: 0.25, colorTone: 'teal' });
157
+
158
+ // Versioned JSON document ('toonlab/water-preset'), same pattern as toon presets:
159
+ const json = serializeWaterPreset('harbor', { label: 'Harbor' });
160
+ const result = parseWaterPresetDocument(json);
161
+ if (result.ok) registerWaterPreset(result.value.id, result.value, { overwrite: true });
162
+ // or in one step: registerSerializedWaterPreset(json, { overwrite: true });
163
+ ```
164
+
165
+ `getWaterPresetOptions()` lists built-ins plus registrations (for HUDs);
166
+ `validateWaterPresetDocument` / `createWaterPresetDocument` /
167
+ `sanitizeWaterPresetSettings` round out the document API.
168
+
169
+ ## Persistent swash, foam, and wet sand
170
+
171
+ `shoreState` is opt-in because a lake or open-ocean tile does not necessarily
172
+ need a fixed beach-history atlas. It requires `bedHeight`, and its `region` is
173
+ world-anchored rather than camera-following. The four state channels are:
174
+
175
+ | Channel | Stored beach history |
176
+ |---|---|
177
+ | R | persistent sediment moisture |
178
+ | G | short-lived surface film |
179
+ | B | active aerated foam |
180
+ | A | stranded and drying foam residue |
181
+
182
+ The water samples this atlas so swash foam remains attached to the moving
183
+ wet/dry edge. A beach material can sample the same atlas, preserving wet sand,
184
+ the glossy draining film, and foam that has just crossed onto exposed sand.
185
+ The ground mesh must provide a `color` vertex attribute because
186
+ `createWaterShoreMaterial` uses it as the dry albedo. Optional `albedoMap`,
187
+ Poly Haven-style `armMap` (AO/roughness/metalness), `normalMap`, and
188
+ `textureRepeat` inputs can layer tiling grain detail under the same live wet
189
+ sand, foam, and projected-caustic response.
190
+
191
+ ```js
192
+ import {
193
+ WaterSurface,
194
+ createWaterShoreMaterial,
195
+ } from '@call-me-sensei/toonlab/water';
196
+
197
+ const water = new WaterSurface({
198
+ width: 80,
199
+ depth: 40,
200
+ preset: 'coast',
201
+ bedHeight: (x, z) => terrainHeightAt(x, z),
202
+ runupDistance: 10, // event maxima vary from about 8 m to 10 m
203
+ nearshorePhase: true,
204
+ shoreState: {
205
+ region: { centerX: 0, centerZ: 0, width: 80, depth: 40 },
206
+ resolution: { x: 512, y: 256 },
207
+ },
208
+ });
209
+
210
+ const beachMaterial = createWaterShoreMaterial({
211
+ stateField: water.shoreState,
212
+ foamColor: water.settings.foamColor,
213
+ foamAmount: water.settings.swashFoamAmount,
214
+ wetDarkening: water.settings.wetSandDarkening,
215
+ });
216
+ beach.material = beachMaterial;
217
+ water.attachShoreStateMaterial(beachMaterial);
218
+ ```
219
+
220
+ Call `water.attachShoreStateMaterial(material)` rather than binding only the
221
+ initial texture. The shore state uses ping-pong targets, and the attachment
222
+ refreshes the material after every swap. The field is a visual history model,
223
+ not a sediment, air-entrainment, or two-phase-fluid simulation.
224
+
225
+ ## Quality tiers
226
+
227
+ `quality: 'low' | 'medium' | 'high'` gates the most expensive fragment
228
+ features (`WATER_QUALITY_TIERS`):
229
+
230
+ | Tier | Caustics/sparkles | Detail octaves | Foam octaves |
231
+ |---|---|---|---|
232
+ | `low` | off | 2 | 2 |
233
+ | `medium` | caustics + sparkles | 3 | 3 |
234
+ | `high` | + chromatic caustics | 4 | 3 |
235
+
236
+ Custom tiers are a plain object:
237
+
238
+ ```js
239
+ new WaterSurface({ quality: { qualityLevel: 'high', detailOctaves: 5, foamOctaves: 4 } });
240
+ ```
241
+
242
+ `resolveWaterQualityDefines(quality)` is the resolver if you build materials
243
+ directly (`createWaterMaterial`).
244
+
245
+ Water quality is a compile-time TSL graph policy. Pass it when constructing
246
+ `WaterSurface`; changing tiers requires replacing the surface/material graph.
247
+ Water Lab performs that rebuild while preserving the authored document and
248
+ stage state. `applySettings()` hot-updates art/simulation uniforms but should
249
+ not be used as a runtime quality switch. The saved `quality` value is the
250
+ preset's preferred deployment default; a host may replace it per device when
251
+ constructing the surface.
252
+
253
+ ## The systems
254
+
255
+ `WaterSurface` orchestrates these modules (all exported from
256
+ `@call-me-sensei/toonlab/water` for standalone use):
257
+
258
+ - **`WaterRippleSimulation`** — GPU ping-pong heightfield with velocity,
259
+ foam energy, absorbing borders, and a texel-exact moving window that
260
+ follows a target across large surfaces.
261
+ - **`WaterCurrentField`** — optional CPU-authored, world-space horizontal
262
+ current atlas mirrored to a compact GPU texture. It can project authored
263
+ flow away from signed-distance obstacles and feeds gameplay queries plus
264
+ shore-foam transport. It does not solve pressure, circulation, separation,
265
+ or turbulence.
266
+ - **`WaterShoreStateField`** — optional world-anchored GPU ping-pong atlas for
267
+ moisture, surface film, active swash foam, and residue. Pair it with
268
+ **`createWaterShoreMaterial`** and `water.attachShoreStateMaterial(...)` so
269
+ the water and beach render the same history instead of looking like two
270
+ independent systems.
271
+ - **`WaterSplashSystem`** — GPU-ballistic droplet points, procedural spray
272
+ crown, expanding foam rings; all in-shader, no sprite atlas.
273
+ - **`WaterBreakerSystem`** — dedicated curl-shell geometry swept along the
274
+ break line: shells swell out of the ambient sea, pitch a plunging lip,
275
+ peel alongshore, and decay into a whitewater bore. Physical: mirrored on
276
+ the CPU (`sampleAt`) so `getHeightAt` rides objects over the passing face
277
+ and `getFlowAt` surges them shoreward. `breakerEnabled: false` removes the
278
+ whole system for perf A/B.
279
+ - **`WaterInteractionManager`** (via `water.addInteractor`) — objects
280
+ entering fast splash automatically, submerged movement leaves wakes with
281
+ bow spray, exits splash lighter. Interactors take a radius plus a height
282
+ (optionally a function for pose-dependent bodies).
283
+ - **`WaterRain`** — GPU-looping rain streaks to pair with ripple dimples.
284
+ - **`WaterKelpField`** — instanced kelp blades swaying with the flow.
285
+ - **Underwater view** — when the camera dips below the waterline, a clipped
286
+ same-pose scene capture supplies the real sky, clouds, and above-water
287
+ objects to a stylized IOR-based Snell window with total internal
288
+ reflection and compact Beer–Lambert-inspired tinting.
289
+ - **Projected floor caustics** — the environment and shore materials receive
290
+ a runtime-generated seamless Voronoi web sampled in two independently
291
+ moving layers. It is distorted by waves and attenuated by water depth,
292
+ receiver angle, and the active water region. This is the shimmering light
293
+ commonly mistaken for a floor reflection; it is not mirror reflection.
294
+
295
+ Full-screen underwater fog, image distortion, waterline meniscus, and sun
296
+ shafts remain scene/post-processing responsibilities rather than surface
297
+ material features. The Water Lab changes scene fog and background below the
298
+ surface, but the water module does not force those artistic choices on every
299
+ host application.
300
+
301
+ Scene shadowing and cloud shadows: the surface darkens under cast shadows
302
+ (`sceneShadowStrength`) and shares the global cloud-shadow field
303
+ (`water.setCloudShadow({ strength, coverage, scale, velocity })`) with
304
+ grass, trees, and the environment shader.
305
+
306
+ ## CPU/GPU spectrum mirror
307
+
308
+ `buildGerstnerWaves(settings)` builds the 8-component Gerstner spectrum that
309
+ both the vertex shader and the CPU sampler consume;
310
+ `sampleGerstnerHeight(waves, x, z, time)` (wrapped by
311
+ `water.getHeightAt(x, z)`) evaluates the exact same math for buoyancy,
312
+ swimming, and interaction tests. The spectrum constants (wavelength falloff,
313
+ slope limit, gravity) are deliberately not settings because the two sides
314
+ must stay in lockstep — see
315
+ [shader-constants.md](shader-constants.md#water) for the full list and
316
+ where each lives.
317
+
318
+ On a shoaling surface, `nearshorePhase: true` optionally bakes a static-bed,
319
+ one-way mild-slope/ray approximation for the two dominant swell components.
320
+ It keeps the incident period while shortening and turning those components in
321
+ shallower water, and CPU height queries sample the same field. The shader's
322
+ analytic macro normal uses the corresponding baked phase gradient. This
323
+ is intentionally bounded: it does not model diffraction, reflection, a ray
324
+ turning back toward the incident boundary, moving bathymetry, or every detail
325
+ wave. Dedicated breaker-shell mode currently bypasses this phase solve.
326
+
327
+ Swash also has one CPU-authored frame per event. The visible water, gameplay
328
+ queries, and persistent shore-state pass share the same event index, oblique
329
+ edge shape, uprush/backwash progress, and endpoints. That shared frame is what
330
+ prevents foam, wetness, and the moving water edge from becoming separately
331
+ looping animations.
332
+
333
+ ## Debug views
334
+
335
+ `?waterDebug=<mode>` in the labs or `water.setDebugMode(mode)`:
336
+
337
+ ```text
338
+ depth | foam | normal | ripple | reflection | caustics | specular | fresnel | crest | shoreState
339
+ ```
340
+
341
+ `shoreState` displays moisture in red, surface film in green, and the stronger
342
+ of active foam/residue in blue. It is the quickest way to tell whether a visual
343
+ gap is caused by state generation, state/material binding, or final shading.
344
+
345
+ ## Technique and research matrix
346
+
347
+ This table separates what the renderer actually implements from useful
348
+ coastal-engineering reference models. The references constrain terminology
349
+ and expected behavior; they are not evidence that this stylized system has
350
+ been physically validated.
351
+
352
+ | Technique | ToonLab status | Scope and important boundary |
353
+ |---|---|---|
354
+ | Eight-component Gerstner spectrum | **Implemented** | Drives open-water geometry and has a CPU mirror for height queries. This is a compact directional spectrum, not an FFT ocean and not a fluid solver. |
355
+ | Finite-depth nearshore phase/refraction | **Implemented, opt-in** | A static-bed, one-way mild-slope/ray bake affects the dominant swell pair. It approximates wavelength shortening and refraction but omits diffraction, reflection, turning rays, moving beds, and the six detail waves. The physical phenomena and numerical methods are covered in the [USACE Coastal Engineering Manual, Part II](https://www.publications.usace.army.mil/Portals/76/Publications/EngineerManuals/EM_1110-2-1100_Part-02.pdf). |
356
+ | Shoaling and breaking | **Implemented, stylized** | Bed depth attenuates/amplifies the authored swell and can drive a separate curling-breaker shell. It is not an energy-conserving Boussinesq, shallow-water, or two-phase breaking calculation; dedicated breaker-shell mode currently bypasses the nearshore phase bake. See the [USACE Coastal Engineering Manual, Part II](https://www.publications.usace.army.mil/Portals/76/Publications/EngineerManuals/EM_1110-2-1100_Part-02.pdf) for the engineering treatment of wave transformations and surf-zone processes. |
357
+ | Swash run-up and backwash | **Implemented, stylized** | A continuous event state gives fast uprush, slower return, 80–100% peak variation, correlated sets, oblique macro-shape, and endpoint handoff. `runupDistance` is a horizontal art/calibration control, not the `R2%` elevation predicted by an empirical model. [Carrier & Greenspan (1958)](https://www.cambridge.org/core/journals/journal-of-fluid-mechanics/article/abs/water-waves-of-finite-amplitude-on-a-sloping-beach/9628CB59A4761A52C12E098ACCE3F1C6) derive nonlinear shallow-water run-up on a plane slope; [Stockdon et al. (2006)](https://pubs.usgs.gov/publication/70030520) show observed run-up depends on setup plus incident- and infragravity-band swash and on offshore conditions and beach properties. ToonLab does not solve or fit either model. |
358
+ | Persistent foam, film, and wet sand | **Implemented, opt-in** | A world-anchored RGBA state atlas is shared by water and ground materials. It preserves visual history but does not simulate bubbles, air entrainment, sediment transport, infiltration, or two-phase flow. |
359
+ | Interactive ripple heightfield | **Implemented, local** | A camera-following 2D GPU heightfield handles splashes and wakes. It is not mass/momentum conserving, is not coupled to the offshore spectrum, and is not a wetting/drying shoreline solver. |
360
+ | Flow maps / spatial currents | **Foundation implemented; authored input required** | `WaterCurrentField` stores authored XZ velocity and an optional domain/obstacle mask for gameplay and shore-foam advection. Its signed-distance projection discourages bank penetration but cannot infer pressure, circulation, wakes, separation, rip currents, or turbulence. Those patterns still need authored/baked vectors or a solver upstream. |
361
+ | Shallow-water equations (SWE) | **Not implemented** | There is no depth-and-momentum time integration, conservative wetting/drying front, obstacle-coupled flow, or numerical run-up solution. The current swash and ripple systems must not be described as SWE. [Carrier & Greenspan (1958)](https://www.cambridge.org/core/journals/journal-of-fluid-mechanics/article/abs/water-waves-of-finite-amplitude-on-a-sloping-beach/9628CB59A4761A52C12E098ACCE3F1C6) is a useful primary reference for the nonlinear shallow-water model class on a plane beach. |
362
+ | FFT spectral ocean | **Not implemented** | No frequency-domain spectrum is transformed into a displacement field. FFT could be a future open-ocean option, but it would not by itself supply beach wetting/drying, swash history, or obstacle-aware currents. |
363
+ | Full CFD / two-phase foam | **Not implemented** | Spray droplets and foam are rendering/simulation effects, not Navier–Stokes water/air volume fractions. |
364
+
365
+ ### Research references
366
+
367
+ - G. F. Carrier and H. P. Greenspan, [“Water waves of finite amplitude on a
368
+ sloping beach”](https://www.cambridge.org/core/journals/journal-of-fluid-mechanics/article/abs/water-waves-of-finite-amplitude-on-a-sloping-beach/9628CB59A4761A52C12E098ACCE3F1C6),
369
+ *Journal of Fluid Mechanics* 4(1), 1958. Primary analytic nonlinear
370
+ shallow-water treatment of shoreline motion on a plane slope.
371
+ - H. F. Stockdon, R. A. Holman, P. A. Howd, and A. H. Sallenger Jr.,
372
+ [“Empirical parameterization of setup, swash, and
373
+ runup”](https://pubs.usgs.gov/publication/70030520), *Coastal Engineering*
374
+ 53(7), 2006. Primary field-data study separating setup, incident swash, and
375
+ infragravity swash in run-up estimates.
376
+ - U.S. Army Corps of Engineers, [*Coastal Engineering Manual, Part
377
+ II*](https://www.publications.usace.army.mil/Portals/76/Publications/EngineerManuals/EM_1110-2-1100_Part-02.pdf),
378
+ EM 1110-2-1100, 2002. Official engineering reference for wave mechanics,
379
+ transformations, breaking, and surf-zone processes.
380
+
381
+ ## Water Lab visual acceptance gates
382
+
383
+ These are regression targets derived from the reported Water Lab captures,
384
+ not a claim that every current build already passes. Use **Ground → Beach
385
+ (swash)**, a 20 m test beach, `runupDistance: 10`, and a camera that can inspect
386
+ the shoreline both alongshore and from above. Observe at least eight complete
387
+ events; a single attractive still does not prove continuity.
388
+
389
+ - **Reach and continuity:** centerline inland maxima fall between 8 m and
390
+ 10 m, consecutive events visibly differ, and the edge returns continuously
391
+ to an event-dependent rundown endpoint. No teleport/reset is allowed when a
392
+ cycle changes.
393
+ - **Connected, oblique edge:** the water/sand silhouette remains one connected
394
+ front with an incidence angle, broad tongues, smaller scallops, and real
395
+ gaps. It must not become a ruler-straight or clamped plateau, release one
396
+ mesh column/block at a time, or repeat the same outline every cycle.
397
+ - **Foam belongs to the edge:** the strongest fresh foam intersects the actual
398
+ wet/dry silhouette on both water and sand sides. It may tear into patches and
399
+ leave residue, but a detached interior white strip that looks like a
400
+ reflection or a second looping system is a failure.
401
+ - **Shape variety:** compare several cycles at the same camera. Broad tongue
402
+ widths, holes, breaks, and alongshore positions change while remaining
403
+ spatially coherent; avoid evenly spaced teeth or nearly uniform ribbons.
404
+ - **One optical system:** shallow color, opacity, refraction, and caustic
405
+ motion transition into the swash film without a sharp internal handoff.
406
+ Caustics may fade as the film becomes physically thin and foam may occlude
407
+ them, but they must not stop at a fixed line inside connected water.
408
+ - **Retreat history:** active foam thins into residue, recently exposed sand
409
+ stays darker and briefly glossier, and those marks decay on their configured
410
+ lifetimes instead of disappearing at the procedural cycle boundary.
411
+ - **Coverage:** supported orbit, pan, and zoom views do not reveal a rectangular
412
+ water-tile edge, skirt gap, or finite patch against the horizon. Either the
413
+ water covers the view or scene composition hides its boundary.
414
+ - **Ground switching and controls:** after switching into Beach (swash), orbit,
415
+ pan, and zoom remain responsive; a rendered scene followed by a multi-second
416
+ input freeze is a failure.
417
+ - **Backend safety:** native WebGPU and the WebGL2 fallback retain displaced
418
+ waves and swash. Neither may log a WebGPU private-address-space pipeline
419
+ error or silently fall back to a completely flat surface.
420
+ - **From below:** use the Water Lab's **From below** shortcut. The real sky,
421
+ clouds, and above-water silhouettes remain visible inside the Snell window;
422
+ grazing rays transition into a stylized total-internal-reflection band
423
+ without turning the whole underside transparent.
424
+ - **Seabed:** use the **Seabed** shortcut on open ground. A moving two-layer
425
+ caustic web stays attached to actual receiving geometry, follows the water
426
+ palette and sun tint, fades with depth, and never appears on dry ground.
427
+ - **Debug cross-check:** in `shoreState`, the blue foam signal follows the same
428
+ event as the moving edge, green film trails the retreat, and red moisture
429
+ outlives both. In `caustics` and `foam`, no fixed internal seam should reveal
430
+ two independently phased systems.
@@ -0,0 +1,200 @@
1
+ # Weather system
2
+
3
+ Weather is a separate, reusable world system rather than an environment
4
+ shader preset. The environment cluster still owns how a surface is shaded;
5
+ weather coordinates the temporary state that many systems must share:
6
+ clouds, fog, sun and ambient light, wind, precipitation, lightning, water
7
+ agitation, wetness, snow cover, and ice.
8
+
9
+ Sky Lab owns the reusable baseline sky appearance; Weather owns the current
10
+ condition and only modulates that baseline. Lighting remains authoritative for
11
+ the real directional light and shadow policy. See
12
+ [Lab responsibilities](lab-architecture.md).
13
+
14
+ Open the standalone editor at `/weather-lab/`. It exposes the complete
15
+ schema, smooth preset transitions, a lightning test, local saves, and
16
+ portable JSON import/export.
17
+
18
+ ## Composed-world usage
19
+
20
+ `createStylizedWorld` creates the weather coordinator by default with the
21
+ Call Me Sensei preset:
22
+
23
+ ```js
24
+ const world = await createStylizedWorld({
25
+ renderer,
26
+ scene,
27
+ camera,
28
+ terrain: { root: terrainRoot, heightAt, size: 1000 },
29
+ water: { level: 0 },
30
+ followTarget: character,
31
+ weather: { preset: 'partlyCloudy', seed: 42 },
32
+ });
33
+
34
+ // Smoothly change the whole world, not just the particle effect.
35
+ world.setWeather('thunderstorm', { duration: 5 });
36
+
37
+ // Keep calling the normal composed-world update.
38
+ world.update(delta);
39
+
40
+ // Optional authored day cycle. This binds Sky, Water, the world-owned sun
41
+ // direction, environment materials, and Weather's modulation bridge.
42
+ const lighting = createLightingSystem({ renderer, scene, style: 'call-me-sensei' });
43
+ lighting.attachWorld(world);
44
+ ```
45
+
46
+ The coordinator automatically adapts the world sky, sun rig, scene fog,
47
+ ambient lights, cloud shadows, grass, flowers, trees, fauna, ambient VFX,
48
+ and water when those systems are present. `followTarget` centers the GPU
49
+ precipitation window; otherwise it follows the camera.
50
+
51
+ Without a Lighting system, Weather writes the world sun/ambient/fog fallback
52
+ directly. `lighting.attachWorld(world)` calls `weather.setLightingSystem()`
53
+ automatically; from then on Lighting is the sole writer and Weather supplies
54
+ sun tint/intensity, ambient, and fog-color modulation. Detaching Lighting hands
55
+ the same current condition back to Weather without a visible ownership race.
56
+
57
+ Pass `weather: false` for an indoor or fully host-managed scene. Indoor and
58
+ outdoor are not separate weather implementations: a building, cave, or
59
+ vehicle can instead decide how much outside weather reaches its visible
60
+ surfaces and audio. Weather remains one source of truth across transitions
61
+ between spaces.
62
+
63
+ ## Built-in conditions
64
+
65
+ There are 22 presets:
66
+
67
+ - Signature and fair: `call_me_sensei`, `clear`, `partlyCloudy`, `cloudy`,
68
+ `overcast`, `windy`
69
+ - Visibility: `haze`, `mist`, `fog`
70
+ - Rain and storms: `drizzle`, `rain`, `heavyRain`, `thunderstorm`,
71
+ `tropicalStorm`
72
+ - Winter: `snow`, `heavySnow`, `blizzard`, `sleet`, `freezingRain`, `hail`
73
+ - Arid: `dustStorm`, `sandstorm`
74
+
75
+ Precipitation is a single instanced draw with static seeded attributes and
76
+ GPU-looped motion. The renderer supports `rain`, `snow`, `sleet`, `hail`,
77
+ and `dust`; preset intensity changes the instance count without per-particle
78
+ CPU updates.
79
+
80
+ ## Standalone coordinator and lab adapters
81
+
82
+ Other labs can surface the same weather registry without constructing a
83
+ complete world:
84
+
85
+ ```js
86
+ import {
87
+ createWeatherSystem,
88
+ getWeatherPresetOptions,
89
+ } from '@call-me-sensei/toonlab/weather';
90
+
91
+ const weather = createWeatherSystem({
92
+ renderer,
93
+ scene,
94
+ camera,
95
+ followTarget: player,
96
+ groundHeightAt: terrain.heightAt,
97
+ preset: 'snow',
98
+ sky,
99
+ sunRig,
100
+ water,
101
+ grass,
102
+ flowers,
103
+ forest,
104
+ fauna,
105
+ ambientFx,
106
+ // Optional whole-world scene-light adapter supplied by your host. This
107
+ // keeps custom vegetation inputs aligned when Lighting is absent.
108
+ getSun: readSceneSun,
109
+ setSun: applySceneSun,
110
+ // Needed only when an opaque host adapter cannot be inspected. These are
111
+ // the exact values restored by dispose().
112
+ cloudShadowBaseline: currentCloudShadow,
113
+ surfaceBaseline: currentSurfaceState,
114
+ onSurfaceChange: ({ wetness, snowCover, ice }) => {
115
+ terrainMaterial.uniforms.wetness.value = wetness;
116
+ terrainMaterial.uniforms.snowCover.value = snowCover;
117
+ terrainMaterial.uniforms.ice.value = ice;
118
+ },
119
+ });
120
+
121
+ for (const option of getWeatherPresetOptions()) {
122
+ addPresetToLab(option.id, option.label, option.description);
123
+ }
124
+
125
+ weather.transitionTo('hail', { duration: 3 });
126
+ weather.update(delta);
127
+ ```
128
+
129
+ Consumers are optional. A Tree Lab can provide only `forest` and `sky`; a
130
+ Water Lab can provide only `water`; an indoor material lab can consume only
131
+ the normalized surface outputs. This keeps each lab focused while using the
132
+ same preset document and transition semantics.
133
+
134
+ `getSun` / `setSun` form the preferred standalone world-light bridge. The
135
+ state is `{ direction, color, sky }`; Weather captures the getter once, applies
136
+ temporary tint/darkening through the setter, and restores that baseline on
137
+ teardown. `setCloudShadow` and `onSurfaceChange` are write-only adapters, so
138
+ pass `cloudShadowBaseline` / `surfaceBaseline` when their current values are
139
+ not otherwise inspectable. `createStylizedWorld` wires these responsibilities
140
+ for its own systems.
141
+
142
+ ## Settings groups
143
+
144
+ `WEATHER_SETTING_FIELD_SCHEMA` is the canonical UI schema. Settings are
145
+ grouped into:
146
+
147
+ - `atmosphere`: cloud coverage/speed/shadows, sky and sun multipliers,
148
+ ambient light, fog color and range
149
+ - `wind`: direction, strength, speed, and vegetation gust controls
150
+ - `precipitation`: type, intensity, appearance, follow volume, speed, and
151
+ particle budget
152
+ - `lightning`: enabled state, strike rate, color, intensity, and duration
153
+ - `surface`: water wave/ripple response plus host-facing wetness, snow, and
154
+ ice targets
155
+
156
+ Weather multiplies the current lower-priority Lighting result, so time of day
157
+ remains independent. For example, `rain` at noon and `rain` at sunset share the
158
+ same condition while retaining their different underlying sun colors and
159
+ angles. On `StylizedSky`, Weather owns a private priority-200 resolver layer;
160
+ on `WaterSurface`, it owns a private layer that adds `waterWaveBoost` over the
161
+ authored wave baseline. `sky.settings` and `water.settings` therefore remain
162
+ portable, while their `renderedSettings` expose the current composition.
163
+
164
+ `weather.dispose()` clears only Weather-owned Sky and Water layers, resets
165
+ Lighting modulation, and restores the captured sun, cloud-shadow, vegetation
166
+ wind, wetness/snow, Ambient FX wind, and host surface baselines. Disposed
167
+ coordinators ignore later refresh/update calls, so teardown cannot resurrect
168
+ stale world state. The visible dome-cloud pattern
169
+ and projected cloud-shadow field are intentionally separate angular/spatial
170
+ procedures; Weather coordinates their condition and motion policy but does not
171
+ claim texel-identical registration.
172
+
173
+ ## Lightning, thunder, and surface events
174
+
175
+ Lightning timing is seeded. The visual flash stays inside the renderer;
176
+ thunder is emitted as a delayed host event based on strike distance so a
177
+ game can choose and spatialize its own audio:
178
+
179
+ ```js
180
+ weather.addEventListener('lightning', ({ position, distance, thunderDelay }) => {
181
+ spawnBolt(position);
182
+ });
183
+
184
+ weather.addEventListener('thunder', ({ distance }) => {
185
+ audio.playThunder({ distance });
186
+ });
187
+ ```
188
+
189
+ `weather.root.userData.weatherSurface` always contains the current
190
+ `{ wetness, snowCover, ice, waterWaveBoost, waterRippleRate }` state.
191
+ `onSurfaceChange` is the preferred adapter for custom terrain, prop,
192
+ character, or post-processing materials.
193
+
194
+ ## Custom presets
195
+
196
+ Use `createWeatherPresetDocument`, `parseWeatherPresetDocument`, and
197
+ `serializeWeatherPresetDocument` for portable documents. Register an
198
+ imported document with `registerWeatherPresetDocument(document)`; all labs
199
+ that read `getWeatherPresetOptions()` can then expose it without maintaining
200
+ a second preset list.