@call-me-sensei/toonlab 0.2.0 → 0.3.1

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 +231 -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 +752 -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,322 @@
1
+ import * as THREE from 'three';
2
+
3
+ import { MeshPhysicalNodeMaterial } from 'three/webgpu';
4
+ import {
5
+ clamp,
6
+ float,
7
+ luminance,
8
+ max,
9
+ mix,
10
+ normalMap as normalMapNode,
11
+ normalWorld,
12
+ positionWorld,
13
+ smoothstep,
14
+ step,
15
+ texture,
16
+ uniform,
17
+ uv,
18
+ vec2,
19
+ vec3,
20
+ vertexColor,
21
+ } from 'three/tsl';
22
+
23
+ import { projectedWaterCaustics } from '../shaders-tsl/chunks/projected-water-caustics.js';
24
+
25
+ const DEFAULT_FOAM_COLOR = Object.freeze([0.94, 0.98, 1.0]);
26
+ const DEFAULT_WET_DARKENING = 0.3;
27
+ const DEFAULT_WET_ROUGHNESS = 0.28;
28
+ const DEFAULT_WET_CLEARCOAT = 0.68;
29
+ const DEFAULT_FOAM_AMOUNT = 1;
30
+ const DRY_ROUGHNESS = 0.95;
31
+
32
+ const regionScratch = new THREE.Vector4();
33
+ let fallbackStateTexture = null;
34
+
35
+ function getFallbackStateTexture() {
36
+ if (!fallbackStateTexture) {
37
+ // R moisture, G surface film, B active foam, A stranded residue.
38
+ fallbackStateTexture = new THREE.DataTexture(
39
+ new Uint8Array([0, 0, 0, 0]),
40
+ 1,
41
+ 1,
42
+ THREE.RGBAFormat,
43
+ );
44
+ fallbackStateTexture.name = 'waterShoreStateFallback';
45
+ fallbackStateTexture.colorSpace = THREE.NoColorSpace;
46
+ fallbackStateTexture.minFilter = THREE.LinearFilter;
47
+ fallbackStateTexture.magFilter = THREE.LinearFilter;
48
+ fallbackStateTexture.wrapS = THREE.ClampToEdgeWrapping;
49
+ fallbackStateTexture.wrapT = THREE.ClampToEdgeWrapping;
50
+ fallbackStateTexture.generateMipmaps = false;
51
+ fallbackStateTexture.needsUpdate = true;
52
+ }
53
+ return fallbackStateTexture;
54
+ }
55
+
56
+ function resolveStateTexture(stateField) {
57
+ const candidate = stateField?.texture
58
+ ?? stateField?.stateTexture
59
+ ?? stateField?.renderTarget?.texture
60
+ ?? stateField?.target?.texture;
61
+ return candidate?.isTexture ? candidate : getFallbackStateTexture();
62
+ }
63
+
64
+ function copyRegion(out, value) {
65
+ if (value?.isVector4) return out.copy(value);
66
+ if (Array.isArray(value)) {
67
+ return out.set(value[0] ?? 0, value[1] ?? 0, value[2] ?? 0.5, value[3] ?? 0.5);
68
+ }
69
+ if (value && typeof value === 'object') {
70
+ const width = Number(value.width);
71
+ const depth = Number(value.depth);
72
+ const halfWidth = value.halfWidth ?? value.halfSizeX
73
+ ?? (Number.isFinite(width) ? width * 0.5 : 0.5);
74
+ const halfDepth = value.halfDepth ?? value.halfSizeZ
75
+ ?? (Number.isFinite(depth) ? depth * 0.5 : 0.5);
76
+ return out.set(
77
+ value.centerX ?? value.x ?? 0,
78
+ value.centerZ ?? value.z ?? 0,
79
+ halfWidth,
80
+ halfDepth,
81
+ );
82
+ }
83
+ return out.set(0, 0, 0.5, 0.5);
84
+ }
85
+
86
+ function resolveStateRegion(stateField, out) {
87
+ if (!stateField) return out.set(0, 0, 0.5, 0.5);
88
+ if (typeof stateField.getRegion === 'function') {
89
+ out.set(0, 0, 0.5, 0.5);
90
+ const result = stateField.getRegion(out);
91
+ return result && result !== out ? copyRegion(out, result) : out;
92
+ }
93
+ if (stateField.region || stateField.worldRegion) {
94
+ return copyRegion(out, stateField.region ?? stateField.worldRegion);
95
+ }
96
+ return out.set(
97
+ stateField.centerX ?? 0,
98
+ stateField.centerZ ?? 0,
99
+ Math.max((stateField.worldWidth ?? stateField.width ?? 1) * 0.5, 1e-3),
100
+ Math.max((stateField.worldDepth ?? stateField.depth ?? 1) * 0.5, 1e-3),
101
+ );
102
+ }
103
+
104
+ function setSrgbColor(uniformNode, value) {
105
+ if (value === undefined) return;
106
+ if (value?.isColor) {
107
+ uniformNode.value.copy(value);
108
+ return;
109
+ }
110
+ if (Array.isArray(value)) {
111
+ uniformNode.value.setRGB(
112
+ value[0] ?? 1,
113
+ value[1] ?? 1,
114
+ value[2] ?? 1,
115
+ THREE.SRGBColorSpace,
116
+ );
117
+ return;
118
+ }
119
+ uniformNode.value.set(value);
120
+ }
121
+
122
+ function setFiniteClamped(uniformNode, value, min, maximum) {
123
+ if (!Number.isFinite(value)) return;
124
+ uniformNode.value = THREE.MathUtils.clamp(value, min, maximum);
125
+ }
126
+
127
+ /**
128
+ * Refreshes the live state binding and appearance controls of a persistent
129
+ * shore material. Call once after each shore-state ping-pong swap; changing a
130
+ * TextureNode's value updates the binding without rebuilding the node graph.
131
+ */
132
+ export function updateWaterShoreMaterial(material, {
133
+ stateField,
134
+ foamColor,
135
+ foamAmount,
136
+ wetDarkening,
137
+ wetRoughness,
138
+ wetClearcoat,
139
+ } = {}) {
140
+ const uniforms = material?.uniforms;
141
+ if (!uniforms?.uShoreStateMap) return material;
142
+
143
+ if (stateField !== undefined) material.userData.waterShoreStateField = stateField;
144
+ const field = material.userData.waterShoreStateField ?? null;
145
+ uniforms.uShoreStateMap.value = resolveStateTexture(field);
146
+ resolveStateRegion(field, regionScratch);
147
+ uniforms.uShoreStateRegion.value.copy(regionScratch);
148
+
149
+ setSrgbColor(uniforms.uShoreFoamColor, foamColor);
150
+ setFiniteClamped(uniforms.uShoreFoamAmount, foamAmount, 0, 2);
151
+ setFiniteClamped(uniforms.uShoreWetDarkening, wetDarkening, 0, 1);
152
+ setFiniteClamped(uniforms.uShoreWetRoughness, wetRoughness, 0.04, 1);
153
+ setFiniteClamped(uniforms.uShoreWetClearcoat, wetClearcoat, 0, 1);
154
+ return material;
155
+ }
156
+
157
+ /**
158
+ * Creates a lit wettable ground material for shoreline meshes whose base
159
+ * albedo lives in the geometry's `color` attribute.
160
+ *
161
+ * Packed state channels:
162
+ * - R: persistent sediment moisture
163
+ * - G: short-lived surface water film
164
+ * - B: active aerated foam
165
+ * - A: stranded/drying foam residue
166
+ *
167
+ * The vertex color is sampled explicitly so foam can blend toward its actual
168
+ * albedo instead of merely multiplying dark wet sand. Automatic vertex-color
169
+ * modulation is disabled below to avoid applying the base twice.
170
+ */
171
+ export function createWaterShoreMaterial({
172
+ albedoMap = null,
173
+ armMap = null,
174
+ normalMap = null,
175
+ textureRepeat = 1,
176
+ stateField = null,
177
+ foamColor = DEFAULT_FOAM_COLOR,
178
+ foamAmount = DEFAULT_FOAM_AMOUNT,
179
+ wetDarkening = DEFAULT_WET_DARKENING,
180
+ wetRoughness = DEFAULT_WET_ROUGHNESS,
181
+ wetClearcoat = DEFAULT_WET_CLEARCOAT,
182
+ } = {}) {
183
+ const uniforms = {
184
+ uShoreStateMap: texture(getFallbackStateTexture()),
185
+ // (centerX, centerZ, halfWidth, halfDepth), matching the ripple-region
186
+ // convention used by the rest of the water system.
187
+ uShoreStateRegion: uniform(new THREE.Vector4(0, 0, 0.5, 0.5)),
188
+ uShoreFoamColor: uniform(new THREE.Color(1, 1, 1)),
189
+ uShoreFoamAmount: uniform(DEFAULT_FOAM_AMOUNT),
190
+ uShoreWetDarkening: uniform(DEFAULT_WET_DARKENING),
191
+ uShoreWetRoughness: uniform(DEFAULT_WET_ROUGHNESS),
192
+ uShoreWetClearcoat: uniform(DEFAULT_WET_CLEARCOAT),
193
+ };
194
+
195
+ const fieldUv = positionWorld.xz
196
+ .sub(uniforms.uShoreStateRegion.xy)
197
+ .div(max(uniforms.uShoreStateRegion.zw.mul(2.0), vec2(1e-3)))
198
+ .add(0.5);
199
+ const inside = step(0.0, fieldUv.x).mul(step(fieldUv.x, 1.0))
200
+ .mul(step(0.0, fieldUv.y)).mul(step(fieldUv.y, 1.0));
201
+ // State render targets have no mip chain. Explicit level zero is required
202
+ // by WGSL in non-uniform control flow and is harmless on the GLSL builder.
203
+ const state = uniforms.uShoreStateMap
204
+ .sample(clamp(fieldUv, vec2(0.0), vec2(1.0)))
205
+ .level(0)
206
+ .mul(inside);
207
+
208
+ const moisture = clamp(state.r, 0.0, 1.0);
209
+ const film = clamp(state.g, 0.0, 1.0);
210
+ const foamAmountNode = clamp(uniforms.uShoreFoamAmount, 0.0, 2.0);
211
+ const activeFoam = clamp(state.b, 0.0, 1.0);
212
+ const residue = clamp(state.a, 0.0, 1.0);
213
+ const presentedActiveFoam = activeFoam.mul(foamAmountNode);
214
+ const presentedResidue = residue.mul(foamAmountNode);
215
+
216
+ // Optional tiling detail lets authored lab/showcase terrain use this same
217
+ // persistent wet-sand material without weakening its vertex-painted depth
218
+ // tint. ARM follows the Poly Haven convention (AO, roughness, metalness).
219
+ // The maps are fixed when the graph is created; wetness/foam remain live.
220
+ const hasTiledMaps = albedoMap?.isTexture || armMap?.isTexture || normalMap?.isTexture;
221
+ const tiledUv = hasTiledMaps
222
+ ? uv().mul(Math.max(Number(textureRepeat) || 1, 1e-3))
223
+ : null;
224
+ const vertexBaseColor = vertexColor().rgb;
225
+ const armSample = armMap?.isTexture ? texture(armMap).sample(tiledUv) : null;
226
+ const albedoSample = albedoMap?.isTexture
227
+ ? texture(albedoMap).sample(tiledUv).rgb
228
+ : null;
229
+ const albedoLuminance = albedoSample ? luminance(albedoSample) : null;
230
+ const sandContrast = albedoLuminance
231
+ ? clamp(albedoLuminance.mul(1.7).add(0.48), 0.72, 1.24)
232
+ : null;
233
+ const sandChroma = albedoSample
234
+ ? albedoSample.div(max(albedoLuminance, float(0.08)))
235
+ : null;
236
+ const dryBaseColor = albedoSample
237
+ ? mix(
238
+ vertexBaseColor,
239
+ clamp(
240
+ vertexBaseColor
241
+ .mul(sandContrast)
242
+ .mul(mix(vec3(1.0), sandChroma, 0.18)),
243
+ 0.0,
244
+ 1.0,
245
+ ),
246
+ 0.86,
247
+ )
248
+ : vertexBaseColor;
249
+ const dryRoughness = armSample
250
+ ? mix(float(0.82), float(1.0), armSample.g)
251
+ : float(DRY_ROUGHNESS);
252
+
253
+ // Damp sand darkens substantially but remains saturated. Exposed active
254
+ // foam must still read after the thin water geometry has retreated, so blend
255
+ // toward the physical foam albedo; residue receives a quieter contribution.
256
+ const wetScale = float(1.0).sub(moisture.mul(uniforms.uShoreWetDarkening));
257
+ const wetBaseColor = dryBaseColor.mul(wetScale);
258
+ // The water mesh is deliberately clipped at the signed swash head. The
259
+ // same temporal state extends about one source-cell onto exposed wet sand,
260
+ // so render coherent active foam here as the thin sand-side half of the
261
+ // shoreline lace. Without this shared fringe the water material can only
262
+ // show the wet-side half and every raft looks cut off by the mesh edge.
263
+ // Keep a firm onset so weak advected state still cannot become the broad
264
+ // grey clouds that an earlier raw-state blend produced.
265
+ const visibleActiveFoam = smoothstep(0.46, 0.68, activeFoam);
266
+ const visibleResidue = smoothstep(0.3, 0.68, residue);
267
+ const foamCoverage = clamp(
268
+ visibleActiveFoam.mul(0.72).add(visibleResidue.mul(0.12)).mul(foamAmountNode),
269
+ 0.0,
270
+ 0.74,
271
+ );
272
+ const shoreColor = mix(wetBaseColor, uniforms.uShoreFoamColor, foamCoverage);
273
+
274
+ // Saturated grains remain mostly diffuse; the fresh micron-thin film owns
275
+ // the sharp gloss. Aerated/stranded foam pushes the lobe back toward matte.
276
+ const gloss = max(film, moisture.mul(0.24));
277
+ const foamRoughness = clamp(
278
+ presentedActiveFoam.mul(0.72).add(presentedResidue.mul(0.32)),
279
+ 0.0,
280
+ 1.0,
281
+ );
282
+ const roughness = mix(dryRoughness, uniforms.uShoreWetRoughness, gloss);
283
+ const clearcoatOcclusion = clamp(
284
+ presentedActiveFoam.mul(0.72).add(presentedResidue.mul(0.18)),
285
+ 0.0,
286
+ 0.88,
287
+ )
288
+ .oneMinus();
289
+
290
+ const material = new MeshPhysicalNodeMaterial({ vertexColors: false });
291
+ material.name = 'WaterShoreMaterial';
292
+ // Fold the underwater light into albedo instead of assigning emissiveNode.
293
+ // Some node backends replace the normal lit path when an otherwise-zero
294
+ // custom emissive graph is present, which made the dry beach charcoal while
295
+ // the camera was above water. The shared enabled uniform keeps this addend
296
+ // exactly zero outside the underwater pass.
297
+ material.colorNode = shoreColor.add(projectedWaterCaustics(positionWorld, normalWorld));
298
+ material.metalnessNode = float(0.0);
299
+ material.roughnessNode = mix(roughness, float(0.88), foamRoughness);
300
+ if (armSample) material.aoNode = mix(float(0.9), float(1.0), armSample.r);
301
+ if (normalMap?.isTexture) {
302
+ material.normalNode = normalMapNode(
303
+ texture(normalMap).sample(tiledUv).rgb,
304
+ vec2(0.24, 0.24),
305
+ );
306
+ }
307
+ material.clearcoatNode = film.mul(uniforms.uShoreWetClearcoat).mul(clearcoatOcclusion);
308
+ material.clearcoatRoughnessNode = mix(0.32, 0.1, film);
309
+ material.specularIntensityNode = mix(0.5, 1.0, film);
310
+ material.uniforms = uniforms;
311
+ material.userData.waterShoreMaterial = true;
312
+ material.userData.waterShoreStateField = stateField;
313
+
314
+ return updateWaterShoreMaterial(material, {
315
+ stateField,
316
+ foamColor,
317
+ foamAmount,
318
+ wetDarkening,
319
+ wetRoughness,
320
+ wetClearcoat,
321
+ });
322
+ }