@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,523 @@
1
+ // Persistent shoreline state update pass.
2
+ //
3
+ // Static bed texture (Float32 RGBA):
4
+ // R = world-space bed height, G/B = dHeight/dX / dHeight/dZ,
5
+ // A = valid-domain mask.
6
+ // Persistent state texture (RGBA16F):
7
+ // R = moisture, G = fresh surface film/sheen,
8
+ // B = active advected foam, A = stranded foam residue.
9
+ //
10
+ // This intentionally stays a small fullscreen fragment pass. The main water
11
+ // material only needs to sample the resulting texture once, avoiding more FBM
12
+ // stacks and private variables in the already-dense StylizedWater pipeline.
13
+
14
+ import * as THREE from 'three';
15
+ import {
16
+ abs,
17
+ clamp,
18
+ cos,
19
+ dot,
20
+ exp,
21
+ float,
22
+ Fn,
23
+ If,
24
+ length,
25
+ max,
26
+ min,
27
+ mix,
28
+ positionGeometry,
29
+ select,
30
+ sin,
31
+ smoothstep,
32
+ texture,
33
+ uniform,
34
+ uv,
35
+ vec2,
36
+ vec4,
37
+ } from 'three/tsl';
38
+ import { NodeMaterial } from 'three/webgpu';
39
+
40
+ function hash01(value) {
41
+ const n = Math.sin(value * 127.1 + 311.7) * 43758.5453123;
42
+ return n - Math.floor(n);
43
+ }
44
+
45
+ function periodicValueNoise(x, y, cells, seed) {
46
+ const gridX = (x / 128) * cells;
47
+ const gridY = (y / 128) * cells;
48
+ const x0 = Math.floor(gridX);
49
+ const y0 = Math.floor(gridY);
50
+ const tx0 = gridX - x0;
51
+ const ty0 = gridY - y0;
52
+ const tx = tx0 * tx0 * (3 - 2 * tx0);
53
+ const ty = ty0 * ty0 * (3 - 2 * ty0);
54
+ const sample = (ix, iy) => {
55
+ const wrappedX = ((ix % cells) + cells) % cells;
56
+ const wrappedY = ((iy % cells) + cells) % cells;
57
+ return hash01(wrappedX * 17.17 + wrappedY * 43.31 + seed);
58
+ };
59
+ const a = THREE.MathUtils.lerp(sample(x0, y0), sample(x0 + 1, y0), tx);
60
+ const b = THREE.MathUtils.lerp(sample(x0, y0 + 1), sample(x0 + 1, y0 + 1), tx);
61
+ return THREE.MathUtils.lerp(a, b, ty);
62
+ }
63
+
64
+ let shorelineNoiseTexture = null;
65
+ function getShorelineNoiseTexture() {
66
+ if (shorelineNoiseTexture) return shorelineNoiseTexture;
67
+ const size = 128;
68
+ const data = new Uint8Array(size * size * 4);
69
+ for (let y = 0; y < size; y += 1) {
70
+ for (let x = 0; x < size; x += 1) {
71
+ const index = (y * size + x) * 4;
72
+ const broadA = periodicValueNoise(x, y, 8, 3.17);
73
+ const detailA = periodicValueNoise(x, y, 23, 11.93);
74
+ const broadB = periodicValueNoise(x, y, 11, 29.41);
75
+ const detailB = periodicValueNoise(x, y, 31, 47.77);
76
+ data[index] = Math.round(THREE.MathUtils.clamp(broadA * 0.72 + detailA * 0.28, 0, 1) * 255);
77
+ data[index + 1] = Math.round(THREE.MathUtils.clamp(broadB * 0.7 + detailB * 0.3, 0, 1) * 255);
78
+ data[index + 2] = 0;
79
+ data[index + 3] = 255;
80
+ }
81
+ }
82
+ shorelineNoiseTexture = new THREE.DataTexture(
83
+ data,
84
+ size,
85
+ size,
86
+ THREE.RGBAFormat,
87
+ THREE.UnsignedByteType,
88
+ );
89
+ shorelineNoiseTexture.name = 'waterShoreStateNoise';
90
+ shorelineNoiseTexture.wrapS = THREE.RepeatWrapping;
91
+ shorelineNoiseTexture.wrapT = THREE.RepeatWrapping;
92
+ shorelineNoiseTexture.minFilter = THREE.LinearFilter;
93
+ shorelineNoiseTexture.magFilter = THREE.LinearFilter;
94
+ shorelineNoiseTexture.generateMipmaps = false;
95
+ shorelineNoiseTexture.colorSpace = THREE.NoColorSpace;
96
+ shorelineNoiseTexture.needsUpdate = true;
97
+ return shorelineNoiseTexture;
98
+ }
99
+
100
+ let fallbackStateTexture = null;
101
+ function getFallbackStateTexture() {
102
+ if (fallbackStateTexture) return fallbackStateTexture;
103
+ fallbackStateTexture = new THREE.DataTexture(
104
+ new Float32Array([0, 0, 0, 0]),
105
+ 1,
106
+ 1,
107
+ THREE.RGBAFormat,
108
+ THREE.FloatType,
109
+ );
110
+ fallbackStateTexture.name = 'waterShoreStateFallback';
111
+ fallbackStateTexture.minFilter = THREE.NearestFilter;
112
+ fallbackStateTexture.magFilter = THREE.NearestFilter;
113
+ fallbackStateTexture.generateMipmaps = false;
114
+ fallbackStateTexture.colorSpace = THREE.NoColorSpace;
115
+ fallbackStateTexture.needsUpdate = true;
116
+ return fallbackStateTexture;
117
+ }
118
+
119
+ let fallbackBedTexture = null;
120
+ function getFallbackBedTexture() {
121
+ if (fallbackBedTexture) return fallbackBedTexture;
122
+ fallbackBedTexture = new THREE.DataTexture(
123
+ new Float32Array([0, 0, 0, 0]),
124
+ 1,
125
+ 1,
126
+ THREE.RGBAFormat,
127
+ THREE.FloatType,
128
+ );
129
+ fallbackBedTexture.name = 'waterShoreBedFallback';
130
+ fallbackBedTexture.minFilter = THREE.NearestFilter;
131
+ fallbackBedTexture.magFilter = THREE.NearestFilter;
132
+ fallbackBedTexture.generateMipmaps = false;
133
+ fallbackBedTexture.colorSpace = THREE.NoColorSpace;
134
+ fallbackBedTexture.needsUpdate = true;
135
+ return fallbackBedTexture;
136
+ }
137
+
138
+ let fallbackCurrentTexture = null;
139
+ function getFallbackCurrentTexture() {
140
+ if (fallbackCurrentTexture) return fallbackCurrentTexture;
141
+ // Zero signed velocity is encoded at the midpoint. B/A are zero so even a
142
+ // filtered sample cannot contribute when no WaterCurrentField is attached.
143
+ fallbackCurrentTexture = new THREE.DataTexture(
144
+ new Uint8Array([128, 128, 0, 0]),
145
+ 1,
146
+ 1,
147
+ THREE.RGBAFormat,
148
+ THREE.UnsignedByteType,
149
+ );
150
+ fallbackCurrentTexture.name = 'waterCurrentFieldFallback';
151
+ fallbackCurrentTexture.minFilter = THREE.NearestFilter;
152
+ fallbackCurrentTexture.magFilter = THREE.NearestFilter;
153
+ fallbackCurrentTexture.generateMipmaps = false;
154
+ fallbackCurrentTexture.colorSpace = THREE.NoColorSpace;
155
+ fallbackCurrentTexture.needsUpdate = true;
156
+ return fallbackCurrentTexture;
157
+ }
158
+
159
+ /**
160
+ * Fullscreen update material for WaterShoreStateField.
161
+ *
162
+ * `uRegion = (centerX, centerZ, halfWidth, halfDepth)` in world metres.
163
+ * Swash progress/distance are supplied by the CPU so this pass and the visible
164
+ * water can share one event rather than reconstructing separate loop state.
165
+ */
166
+ export function createWaterShoreStateSimulationNodeMaterial({
167
+ resolutionX = 512,
168
+ resolutionY = 128,
169
+ } = {}) {
170
+ const u = {
171
+ uPrevState: texture(getFallbackStateTexture()),
172
+ uBedMap: texture(getFallbackBedTexture()),
173
+ uNoiseMap: texture(getShorelineNoiseTexture()),
174
+ uCurrentMap: texture(getFallbackCurrentTexture()),
175
+ uTexel: uniform(new THREE.Vector2(1 / resolutionX, 1 / resolutionY)),
176
+ uRegion: uniform(new THREE.Vector4(0, 0, 16, 8)),
177
+ uCurrentRegion: uniform(new THREE.Vector4(0, 0, 1, 1)),
178
+ uUseCurrentMap: uniform(0),
179
+ uCurrentMaxSpeed: uniform(8),
180
+ uCurrentStrength: uniform(1),
181
+ uDelta: uniform(1 / 30),
182
+ uTime: uniform(0),
183
+ uWaterLevel: uniform(0),
184
+
185
+ uSwashEnabled: uniform(0),
186
+ uExplicitRunup: uniform(0),
187
+ uSwashCycle: uniform(0),
188
+ uSwashCycleSpeed: uniform(0),
189
+ uSwashProgress: uniform(0),
190
+ uSwashProgressSpeed: uniform(0),
191
+ uSwashEdgeDistance: uniform(0),
192
+ uSwashEdgeSpeed: uniform(0),
193
+ uPrimaryDirection: uniform(new THREE.Vector2(1, 0)),
194
+ uShorelineRunup: uniform(0.6),
195
+ uWaveEnergy: uniform(0.3),
196
+ uEventNoiseOffset: uniform(new THREE.Vector2()),
197
+ uSwashEdgeShape: uniform(new THREE.Vector3(0, 0.1, 0)),
198
+
199
+ uMoistureWetTime: uniform(0.12),
200
+ uMoistureDryTime: uniform(120),
201
+ uFilmWetTime: uniform(0.08),
202
+ uFilmDryTime: uniform(2),
203
+ uFoamWetLifetime: uniform(6),
204
+ uFoamDryLifetime: uniform(2),
205
+ uResidueLifetime: uniform(12),
206
+ uFoamGain: uniform(7),
207
+ uFoamAdvection: uniform(1),
208
+ uFoamDiffusion: uniform(0.012),
209
+ uFoamAmount: uniform(1),
210
+ uNoiseScale: uniform(0.38),
211
+ uBaseCurrent: uniform(new THREE.Vector2()),
212
+ };
213
+
214
+ const material = new NodeMaterial();
215
+ material.name = 'WaterShoreStateSimulation';
216
+ material.lights = false;
217
+ material.fog = false;
218
+ material.depthTest = false;
219
+ material.depthWrite = false;
220
+ material.vertexNode = Fn(() => vec4(positionGeometry.xy, 0.0, 1.0))();
221
+
222
+ const vUv = uv();
223
+ material.fragmentNode = Fn(() => {
224
+ const result = vec4(0.0).toVar();
225
+ const bed = u.uBedMap.sample(vUv).level(0).toVar();
226
+
227
+ If(bed.a.greaterThan(0.5), () => {
228
+ const worldSize = u.uRegion.zw.mul(2.0).toVar();
229
+ const worldXZ = u.uRegion.xy.add(vUv.sub(0.5).mul(worldSize)).toVar();
230
+ const bedGradient = bed.yz.toVar();
231
+ const gradientLength = length(bedGradient).toVar();
232
+ const bedSlope = max(gradientLength, 0.005).toVar();
233
+ const shoreDirection = bedGradient.div(bedSlope).toVar();
234
+ const alongDirection = vec2(shoreDirection.y, shoreDirection.x.negate()).toVar();
235
+ const alongCoordinate = dot(worldXZ, alongDirection).toVar();
236
+ const restDepth = u.uWaterLevel.sub(bed.r).toVar();
237
+
238
+ // Exact connected-edge shape used by the current visible swash. The CPU
239
+ // owns event progression; this pass only evaluates its spatial offset.
240
+ const progress = clamp(u.uSwashProgress, 0.0, 1.0).toVar();
241
+ const edgeEnvelope = mix(0.18, 1.0, sin(progress.mul(Math.PI))).toVar();
242
+ const envelopeSpeed = cos(progress.mul(Math.PI))
243
+ .mul(Math.PI * 0.82).mul(u.uSwashProgressSpeed).toVar();
244
+ const incidenceSlope = clamp(u.uPrimaryDirection.x.mul(0.52), -0.2, 0.2).toVar();
245
+ const rawEdgeTilt = alongCoordinate.mul(incidenceSlope).negate().toVar();
246
+ const edgeTilt = rawEdgeTilt.div(abs(rawEdgeTilt).div(2.0).add(1.0)).toVar();
247
+
248
+ const scallopA = alongCoordinate.mul(0.32).sub(u.uTime.mul(0.35)).toVar();
249
+ const scallopB = alongCoordinate.mul(0.91).add(u.uTime.mul(0.18)).toVar();
250
+ const edgeScallop = sin(scallopA)
251
+ .sub(sin(u.uTime.mul(-0.35)))
252
+ .add(sin(scallopB).sub(sin(u.uTime.mul(0.18))).mul(0.35))
253
+ .mul(0.4)
254
+ .toVar();
255
+ const scallopSpeed = cos(scallopA).mul(-0.35)
256
+ .add(cos(u.uTime.mul(-0.35)).mul(0.35))
257
+ .add(
258
+ cos(scallopB).mul(0.18)
259
+ .sub(cos(u.uTime.mul(0.18)).mul(0.18))
260
+ .mul(0.35),
261
+ )
262
+ .mul(0.4)
263
+ .toVar();
264
+ const macroAngle = u.uSwashCycle.mul(Math.PI).toVar();
265
+ const macroWave = sin(macroAngle).toVar();
266
+ const macroEnvelope = macroWave.mul(macroWave).toVar();
267
+ const macroEnvelopeSpeed = macroWave.mul(cos(macroAngle))
268
+ .mul(Math.PI * 2).mul(u.uSwashCycleSpeed).toVar();
269
+ const secondaryPhase = u.uSwashEdgeShape.x.mul(-0.71).toVar();
270
+ const macroBase = sin(
271
+ alongCoordinate.mul(u.uSwashEdgeShape.y).add(u.uSwashEdgeShape.x),
272
+ ).sub(sin(u.uSwashEdgeShape.x))
273
+ .add(
274
+ sin(
275
+ alongCoordinate.mul(u.uSwashEdgeShape.y).mul(2.35).add(secondaryPhase),
276
+ ).sub(sin(secondaryPhase)).mul(0.42),
277
+ )
278
+ .mul(u.uSwashEdgeShape.z)
279
+ .toVar();
280
+ const edgeOffset = edgeTilt.add(edgeScallop).mul(edgeEnvelope)
281
+ .add(macroBase.mul(macroEnvelope)).toVar();
282
+ const edgeOffsetSpeed = scallopSpeed.mul(edgeEnvelope)
283
+ .add(edgeTilt.add(edgeScallop).mul(envelopeSpeed))
284
+ .add(macroBase.mul(macroEnvelopeSpeed))
285
+ .toVar();
286
+
287
+ // In automatic mode the horizontal distance contains 1/slope, so its
288
+ // vertical head reduces to shorelineRunup * energy * progress.
289
+ const automaticHead = u.uShorelineRunup.mul(u.uWaveEnergy).mul(progress).toVar();
290
+ const explicitHead = u.uSwashEdgeDistance.mul(bedSlope).toVar();
291
+ const baseHead = mix(automaticHead, explicitHead, u.uExplicitRunup).toVar();
292
+ const edgeHead = baseHead.add(edgeOffset.mul(bedSlope))
293
+ .mul(u.uSwashEnabled)
294
+ .toVar();
295
+ const filmHead = restDepth.add(edgeHead).toVar();
296
+ const beachMask = smoothstep(0.06, 0.22, restDepth).oneMinus().toVar();
297
+ const wetBehindEdge = smoothstep(-0.004, 0.012, filmHead).toVar();
298
+ const instantWet = mix(1.0, wetBehindEdge, beachMask).toVar();
299
+ const foamWidth = max(bedSlope.mul(0.68), 0.018).toVar();
300
+
301
+ // Moisture and sheen belong to fixed ground coordinates, never to the
302
+ // advected foam sample. Exponential response is update-rate independent.
303
+ const stationaryState = u.uPrevState.sample(vUv).level(0).toVar();
304
+ const moistureTau = select(
305
+ instantWet.greaterThan(stationaryState.r),
306
+ u.uMoistureWetTime,
307
+ u.uMoistureDryTime,
308
+ ).toVar();
309
+ const moistureBlend = exp(
310
+ u.uDelta.div(max(moistureTau, 1e-3)).negate(),
311
+ ).oneMinus();
312
+ const moisture = mix(stationaryState.r, instantWet, moistureBlend).toVar();
313
+
314
+ const filmTau = select(
315
+ instantWet.greaterThan(stationaryState.g),
316
+ u.uFilmWetTime,
317
+ u.uFilmDryTime,
318
+ ).toVar();
319
+ const filmBlend = exp(u.uDelta.div(max(filmTau, 1e-3)).negate()).oneMinus();
320
+ const surfaceFilm = mix(stationaryState.g, instantWet, filmBlend).toVar();
321
+
322
+ // Analytic shore-normal flow follows the same changing edge, including
323
+ // the local scallop velocity. Optional baseCurrent is a future river /
324
+ // channel hook and is deliberately independent of visual UV scrolling.
325
+ const automaticHeadSpeed = u.uShorelineRunup.mul(u.uWaveEnergy)
326
+ .mul(u.uSwashProgressSpeed).toVar();
327
+ const explicitHeadSpeed = u.uSwashEdgeSpeed.mul(bedSlope).toVar();
328
+ const baseHeadSpeed = mix(
329
+ automaticHeadSpeed,
330
+ explicitHeadSpeed,
331
+ u.uExplicitRunup,
332
+ ).toVar();
333
+ const normalSpeed = baseHeadSpeed.add(edgeOffsetSpeed.mul(bedSlope))
334
+ .div(bedSlope)
335
+ .mul(u.uSwashEnabled)
336
+ .toVar();
337
+ const clampedNormalSpeed = clamp(normalSpeed, -3.5, 3.5).toVar();
338
+ // Oblique breakers generate a modest longshore current. Carrying rafts
339
+ // sideways prevents successive cycles from stacking foam at identical
340
+ // X positions while remaining tied to real incidence, not random drift.
341
+ const alongshoreSpeed = dot(u.uPrimaryDirection, alongDirection)
342
+ .mul(abs(clampedNormalSpeed)).mul(0.35).toVar();
343
+ // Optional authored world-space flow map. The RGBA8 encoding is shared
344
+ // with WaterCurrentField: signed velocity in RG, fluid weight in B,
345
+ // valid-domain weight in A. It is sampled only in this small transport
346
+ // pass; the main visible-water shader remains unchanged.
347
+ const currentWorldSize = max(u.uCurrentRegion.zw.mul(2.0), vec2(1e-3)).toVar();
348
+ const currentUv = worldXZ.sub(u.uCurrentRegion.xy)
349
+ .div(currentWorldSize).add(0.5).toVar();
350
+ const currentInside = currentUv.x.greaterThanEqual(0.0)
351
+ .and(currentUv.x.lessThanEqual(1.0))
352
+ .and(currentUv.y.greaterThanEqual(0.0))
353
+ .and(currentUv.y.lessThanEqual(1.0));
354
+ const currentInsideMask = select(currentInside, float(1.0), float(0.0)).toVar();
355
+ const encodedCurrent = u.uCurrentMap.sample(clamp(currentUv, 0.0, 1.0))
356
+ .level(0).toVar();
357
+ // WaterCurrentField reserves byte 128 as exact zero. Decode in byte
358
+ // space instead of using `rg * 2 - 1`, whose UNORM midpoint would add
359
+ // a small but persistent preferred drift to still-water foam.
360
+ const spatialCurrent = encodedCurrent.rg.mul(255.0).sub(128.0).div(127.0)
361
+ .mul(u.uCurrentMaxSpeed)
362
+ .mul(encodedCurrent.b.mul(encodedCurrent.a))
363
+ .mul(currentInsideMask)
364
+ .mul(u.uUseCurrentMap)
365
+ .mul(u.uCurrentStrength)
366
+ .toVar();
367
+ const velocity = shoreDirection.mul(clampedNormalSpeed)
368
+ .add(alongDirection.mul(alongshoreSpeed))
369
+ .mul(beachMask).mul(instantWet)
370
+ .add(u.uBaseCurrent.mul(instantWet))
371
+ .add(spatialCurrent.mul(instantWet))
372
+ .mul(u.uFoamAdvection)
373
+ .toVar();
374
+ const backUv = vUv.sub(velocity.mul(u.uDelta).div(max(worldSize, vec2(1e-3))))
375
+ .toVar();
376
+ const backInside = backUv.x.greaterThanEqual(0.0).and(backUv.x.lessThanEqual(1.0))
377
+ .and(backUv.y.greaterThanEqual(0.0)).and(backUv.y.lessThanEqual(1.0));
378
+ const backMask = select(backInside, float(1.0), float(0.0)).toVar();
379
+ const halfTexel = u.uTexel.mul(0.5).toVar();
380
+ const clampedBackUv = clamp(backUv, halfTexel, halfTexel.oneMinus()).toVar();
381
+ const advectedState = u.uPrevState.sample(clampedBackUv).level(0).toVar();
382
+ const advectedFoam = advectedState.b.mul(backMask).toVar();
383
+
384
+ // Small physical-space diffusion disperses rafts without tying the
385
+ // result to atlas resolution. The coefficient is capped to the explicit
386
+ // diffusion stability limit even if callers supply an extreme value.
387
+ const leftFoam = u.uPrevState.sample(
388
+ clamp(clampedBackUv.sub(vec2(u.uTexel.x, 0.0)), halfTexel, halfTexel.oneMinus()),
389
+ ).level(0).b.mul(backMask);
390
+ const rightFoam = u.uPrevState.sample(
391
+ clamp(clampedBackUv.add(vec2(u.uTexel.x, 0.0)), halfTexel, halfTexel.oneMinus()),
392
+ ).level(0).b.mul(backMask);
393
+ const downFoam = u.uPrevState.sample(
394
+ clamp(clampedBackUv.sub(vec2(0.0, u.uTexel.y)), halfTexel, halfTexel.oneMinus()),
395
+ ).level(0).b.mul(backMask);
396
+ const upFoam = u.uPrevState.sample(
397
+ clamp(clampedBackUv.add(vec2(0.0, u.uTexel.y)), halfTexel, halfTexel.oneMinus()),
398
+ ).level(0).b.mul(backMask);
399
+ const texelWorld = worldSize.mul(u.uTexel).toVar();
400
+ const invDx2 = float(1.0).div(max(texelWorld.x.mul(texelWorld.x), 1e-6)).toVar();
401
+ const invDz2 = float(1.0).div(max(texelWorld.y.mul(texelWorld.y), 1e-6)).toVar();
402
+ const laplacian = leftFoam.add(rightFoam).sub(advectedFoam.mul(2.0)).mul(invDx2)
403
+ .add(downFoam.add(upFoam).sub(advectedFoam.mul(2.0)).mul(invDz2))
404
+ .toVar();
405
+ const diffusionDt = min(
406
+ u.uFoamDiffusion.mul(u.uDelta),
407
+ float(0.24).div(max(invDx2.add(invDz2), 1e-6)),
408
+ ).toVar();
409
+ const dispersedFoam = clamp(advectedFoam.add(laplacian.mul(diffusionDt)), 0.0, 1.0)
410
+ .toVar();
411
+
412
+ // A deterministic, world-anchored source with a per-event offset. There
413
+ // is intentionally no nonzero coverage floor: the front is broken into
414
+ // patches at injection, while temporal state supplies continuity.
415
+ const noiseSample = u.uNoiseMap.sample(
416
+ worldXZ.mul(u.uNoiseScale).add(u.uEventNoiseOffset),
417
+ ).level(0).rg.toVar();
418
+ const fineNoise = u.uNoiseMap.sample(
419
+ // A second independent but still resolvable scale supplies holes
420
+ // inside the broad rafts. The former 2.37 multiplier approached the
421
+ // state atlas footprint and read as regular cut-out scallops.
422
+ worldXZ.mul(u.uNoiseScale.mul(1.65))
423
+ .add(u.uEventNoiseOffset.mul(1.71)).add(vec2(0.31, 0.67)),
424
+ ).level(0).rg.toVar();
425
+ // The signed film head is also what clips the visible water. Keep the
426
+ // source centered on that physical silhouette and let noise decide
427
+ // whether a raft exists along it. The old +/-0.7*foamWidth cross-shore
428
+ // displacement could move a patch ~48 cm inside a 1:20 beach, leaving
429
+ // a clear strip between sand and white and making foam read as a
430
+ // reflection floating in the sheet.
431
+ const edgeJitter = noiseSample.r.sub(0.5)
432
+ .mul(min(foamWidth.mul(0.22), 0.004)).toVar();
433
+ const sourceHead = filmHead.add(edgeJitter).toVar();
434
+ // Randomize the distance each attached raft reaches into the water,
435
+ // rather than giving every surviving patch the same band width. On the
436
+ // lab's 1:20 beach this spans roughly 0.35-1.7 m and recreates the
437
+ // believable tongues/holes of the earlier foam without detaching them
438
+ // from the silhouette.
439
+ const raftReach = foamWidth
440
+ .mul(mix(0.5, 1.9, noiseSample.g))
441
+ .mul(mix(0.8, 1.25, fineNoise.r))
442
+ .toVar();
443
+ const frontBand = smoothstep(-0.01, -0.001, sourceHead)
444
+ .mul(smoothstep(raftReach.mul(0.55), raftReach, sourceHead).oneMinus())
445
+ .toVar();
446
+ const trailBand = smoothstep(raftReach.mul(0.42), raftReach.mul(0.82), sourceHead)
447
+ .mul(smoothstep(raftReach.mul(1.55), raftReach.mul(2.8), sourceHead).oneMinus())
448
+ .toVar();
449
+ const speed = clamp(abs(clampedNormalSpeed).div(3.5), 0.0, 1.0).toVar();
450
+ const backwash = clamp(clampedNormalSpeed.negate().div(3.5), 0.0, 1.0).toVar();
451
+ const frontSource = frontBand
452
+ .mul(smoothstep(0.35, 0.58, noiseSample.r))
453
+ .mul(smoothstep(0.3, 0.55, fineNoise.g))
454
+ // The moving front injects strongly, but near maximum run-up its
455
+ // speed approaches zero. A large stationary floor used to pile foam
456
+ // into the same crest-shaped stripe for several frames; temporal
457
+ // retention already carries the moving rafts through that pause.
458
+ .mul(mix(0.32, 1.0, speed));
459
+ const trailSource = trailBand
460
+ .mul(smoothstep(0.55, 0.72, noiseSample.g))
461
+ .mul(smoothstep(0.38, 0.6, fineNoise.r))
462
+ .mul(backwash)
463
+ .mul(0.18);
464
+ const source = max(frontSource, trailSource)
465
+ .mul(beachMask)
466
+ .mul(u.uSwashEnabled)
467
+ .mul(clamp(u.uFoamAmount, 0.0, 2.0))
468
+ .toVar();
469
+
470
+ const foamLifetime = mix(u.uFoamDryLifetime, u.uFoamWetLifetime, instantWet).toVar();
471
+ const retainedFoam = dispersedFoam.mul(
472
+ exp(u.uDelta.div(max(foamLifetime, 1e-3)).negate()),
473
+ ).toVar();
474
+ const injectedFoam = source.mul(
475
+ exp(u.uFoamGain.mul(u.uDelta).negate()).oneMinus(),
476
+ ).toVar();
477
+ // Latch only the already-gated current lip into the visible range. A
478
+ // fast 6-7 m/s uprush can move several state texels during the 2-5
479
+ // frames needed for ordinary temporal accumulation, which left the
480
+ // bright raft visibly behind the water silhouette. Because frontSource
481
+ // has two true zero-floor gates, this adds immediate attached patches
482
+ // without drawing a continuous fallback line.
483
+ const attachedLip = frontSource.mul(0.72).toVar();
484
+ const activeFoam = clamp(
485
+ max(
486
+ retainedFoam.add(injectedFoam.mul(retainedFoam.oneMinus())),
487
+ attachedLip,
488
+ ),
489
+ 0.0,
490
+ 1.0,
491
+ ).toVar();
492
+
493
+ // Residue follows foam while covered, then sticks to exposed sand. It
494
+ // fades slowly and washes out gently on the next inundation.
495
+ const advectedResidue = advectedState.a.mul(backMask).toVar();
496
+ const residueBase = mix(stationaryState.a, advectedResidue, instantWet).toVar();
497
+ const residueRetained = residueBase
498
+ .mul(exp(u.uDelta.div(max(u.uResidueLifetime, 1e-3)).negate()))
499
+ .mul(exp(instantWet.mul(u.uDelta).mul(-0.18)))
500
+ .toVar();
501
+ const residueDeposit = activeFoam.mul(beachMask).mul(instantWet.oneMinus())
502
+ .mul(exp(u.uDelta.mul(-2.5)).oneMinus())
503
+ .add(
504
+ trailSource.mul(backwash)
505
+ .mul(exp(u.uDelta.mul(-0.8)).oneMinus()),
506
+ )
507
+ .toVar();
508
+ const residue = clamp(
509
+ residueRetained.add(residueDeposit.mul(residueRetained.oneMinus())),
510
+ 0.0,
511
+ 1.0,
512
+ ).toVar();
513
+
514
+ result.assign(vec4(moisture, surfaceFilm, activeFoam, residue));
515
+ });
516
+
517
+ return result;
518
+ })();
519
+
520
+ material.uniforms = u;
521
+ material.userData.isWaterShoreStateSimulation = true;
522
+ return material;
523
+ }