@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
@@ -116,6 +116,7 @@ export const WATER_DEBUG_MODES = Object.freeze({
116
116
  specular: 7,
117
117
  fresnel: 8,
118
118
  crest: 9,
119
+ shoreState: 10,
119
120
  });
120
121
 
121
122
  // The number of Gerstner components evaluated by the shader and the CPU
@@ -126,8 +127,9 @@ export const DEFAULT_WATER_SETTINGS = Object.freeze({
126
127
  preset: 'lake',
127
128
  colorTone: 'classic',
128
129
 
129
- // Master dial: 0 = glassy mirror, 1 = storm swell. Scales wave amplitude,
130
- // steepness, and phase speed before the per-wave spectrum is built.
130
+ // Authored baseline: 0 = glassy mirror, 1 = storm swell. Scales wave
131
+ // amplitude, steepness, and phase speed before the per-wave spectrum is
132
+ // built. Scene weather may transiently override it on WaterSurface.
131
133
  waveIntensity: 0.25,
132
134
  waterLevel: 0.36,
133
135
 
@@ -150,6 +152,10 @@ export const DEFAULT_WATER_SETTINGS = Object.freeze({
150
152
  // face above the rest waterline. Scales with total wave energy, so storms
151
153
  // reach visibly further up the sand. 0 pins the waterline in place.
152
154
  shorelineRunup: 0.6,
155
+ // Maximum horizontal run-up in meters. Individual wave-group events reach
156
+ // 80–100% of this bound, begin at the preceding rundown endpoint, and end
157
+ // at their own varied endpoint. 0 = automatic (energy-scaled run-up).
158
+ runupDistance: 0,
153
159
 
154
160
  // Master switch for the breaker system: false removes the mesh and skips
155
161
  // all rebuild work entirely (handy for A/B perf comparisons).
@@ -191,6 +197,11 @@ export const DEFAULT_WATER_SETTINGS = Object.freeze({
191
197
  deepFadeDistance: 2.2,
192
198
  opacity: 0.8,
193
199
  refractionStrength: 0.35,
200
+ // Physical anchor for the stylized underside. 1.333 produces the familiar
201
+ // water-to-air Snell window and total-internal-reflection cutoff.
202
+ indexOfRefraction: 1.333,
203
+ underwaterTransmission: 1.0,
204
+ underwaterTintStrength: 0.35,
194
205
  causticsStrength: 0.55,
195
206
  causticsScale: 0.8,
196
207
  causticsSpeed: 0.6,
@@ -198,13 +209,28 @@ export const DEFAULT_WATER_SETTINGS = Object.freeze({
198
209
  // Foam.
199
210
  foamColor: [0.94, 1.0, 0.99],
200
211
  foamAmount: 1.0,
212
+ // Independent gain for the foam carried by the beach swash. Keeping this
213
+ // separate prevents stronger run-up foam from turning offshore contact
214
+ // bands and whitecaps into solid paint.
215
+ swashFoamAmount: 1.15,
216
+ // Stateful beach foam remains aerated for a few seconds, then converts to
217
+ // thinner residue instead of disappearing when the procedural cycle resets.
218
+ swashFoamLifetime: 4.0,
219
+ swashFoamResidueLifetime: 10.0,
220
+ // Wet sand remembers inundation much longer than the visible surface film.
221
+ // The short sheen is intentionally derived separately by the shore-state
222
+ // simulation, so a beach can stay dark without looking permanently glazed.
223
+ wetSandDryTime: 120.0,
224
+ wetSandDarkening: 0.58,
225
+ wetSandSheen: 0.78,
201
226
  foamContactDistance: 0.4,
202
227
  foamLineSpacing: 0.55,
203
228
  foamNoiseScale: 0.6,
204
229
  whitecapAmount: 0.05,
205
230
  rippleFoamStrength: 0.8,
206
231
 
207
- // Lighting.
232
+ // Authored lighting/reflection fallback. A scene rig may transiently replace
233
+ // the sun and sky fields on WaterSurface without editing this water asset.
208
234
  sunDirection: [0.35, 0.8, 0.45],
209
235
  sunColor: [1.0, 0.96, 0.86],
210
236
  specularStrength: 0.8,
@@ -304,6 +330,7 @@ const WATER_PRESETS = Object.freeze({
304
330
  whitecapAmount: 0.22,
305
331
  foamContactDistance: 0.6,
306
332
  foamAmount: 1.3,
333
+ swashFoamAmount: 1.35,
307
334
  rippleFoamStrength: 0.9,
308
335
  breakerAmount: 0.55,
309
336
  waveSetPeriod: 50,
@@ -328,6 +355,7 @@ const WATER_PRESETS = Object.freeze({
328
355
  reflectionSoftness: 0.35,
329
356
  whitecapAmount: 0.4,
330
357
  foamAmount: 1.2,
358
+ swashFoamAmount: 1.25,
331
359
  specularStretch: 0.5,
332
360
  sparkleStrength: 0.65,
333
361
  reflectionStrength: 0.6,
@@ -358,6 +386,7 @@ const WATER_PRESETS = Object.freeze({
358
386
  reflectionSoftness: 0.3,
359
387
  causticsStrength: 0.08,
360
388
  foamAmount: 1.25,
389
+ swashFoamAmount: 1.45,
361
390
  whitecapAmount: 0.55,
362
391
  rippleFoamStrength: 1.0,
363
392
  sunColor: [0.9, 0.88, 0.85],
@@ -544,6 +573,7 @@ export function createWaterSettings(options = {}) {
544
573
  shoalingDepth: clampedNumber(source.shoalingDepth, base.shoalingDepth, 0.05, 12),
545
574
  shorelineWaves: clampedNumber(source.shorelineWaves, base.shorelineWaves, 0, 1),
546
575
  shorelineRunup: clampedNumber(source.shorelineRunup, base.shorelineRunup, 0, 3),
576
+ runupDistance: clampedNumber(source.runupDistance, base.runupDistance, 0, 15),
547
577
  breakerEnabled: typeof source.breakerEnabled === 'boolean'
548
578
  ? source.breakerEnabled
549
579
  : base.breakerEnabled !== false,
@@ -566,12 +596,33 @@ export function createWaterSettings(options = {}) {
566
596
  deepFadeDistance: clampedNumber(tone.deepFadeDistance ?? source.deepFadeDistance, base.deepFadeDistance, 0.01, 120),
567
597
  opacity: clampedNumber(source.opacity, base.opacity, 0, 1),
568
598
  refractionStrength: clampedNumber(source.refractionStrength, base.refractionStrength, 0, 3),
599
+ indexOfRefraction: clampedNumber(
600
+ source.indexOfRefraction, base.indexOfRefraction, 1.0001, 1.8,
601
+ ),
602
+ underwaterTransmission: clampedNumber(
603
+ source.underwaterTransmission, base.underwaterTransmission, 0, 1,
604
+ ),
605
+ underwaterTintStrength: clampedNumber(
606
+ source.underwaterTintStrength, base.underwaterTintStrength, 0, 1,
607
+ ),
569
608
  causticsStrength: clampedNumber(tone.causticsStrength ?? source.causticsStrength, base.causticsStrength, 0, 4),
570
609
  causticsScale: clampedNumber(source.causticsScale, base.causticsScale, 0.02, 12),
571
610
  causticsSpeed: clampedNumber(source.causticsSpeed, base.causticsSpeed, 0, 8),
572
611
 
573
612
  foamColor: colorArray(source.foamColor, base.foamColor),
574
613
  foamAmount: clampedNumber(source.foamAmount, base.foamAmount, 0, 2),
614
+ swashFoamAmount: clampedNumber(source.swashFoamAmount, base.swashFoamAmount, 0, 2),
615
+ swashFoamLifetime: clampedNumber(
616
+ source.swashFoamLifetime, base.swashFoamLifetime, 0.25, 30,
617
+ ),
618
+ swashFoamResidueLifetime: clampedNumber(
619
+ source.swashFoamResidueLifetime, base.swashFoamResidueLifetime, 0.5, 60,
620
+ ),
621
+ wetSandDryTime: clampedNumber(source.wetSandDryTime, base.wetSandDryTime, 2, 600),
622
+ wetSandDarkening: clampedNumber(
623
+ source.wetSandDarkening, base.wetSandDarkening, 0, 1,
624
+ ),
625
+ wetSandSheen: clampedNumber(source.wetSandSheen, base.wetSandSheen, 0, 1),
575
626
  foamContactDistance: clampedNumber(source.foamContactDistance, base.foamContactDistance, 0.01, 8),
576
627
  foamLineSpacing: clampedNumber(source.foamLineSpacing, base.foamLineSpacing, 0.05, 8),
577
628
  foamNoiseScale: clampedNumber(source.foamNoiseScale, base.foamNoiseScale, 0.02, 12),
@@ -702,37 +753,289 @@ export function buildGerstnerWaves(settings) {
702
753
  // accurate enough for buoyancy and interaction tests. chopWeight mirrors the
703
754
  // shader's shallow-water spectrum filter: slots 0/1 (the dominant swell and
704
755
  // its set beat partner) always pass at full strength, shorter cross chop
705
- // fades toward the surf zone.
706
- export function sampleGerstnerHeight(waves, x, z, time, chopWeight = 1) {
756
+ // fades toward the surf zone. The optional nearshore sample mirrors the
757
+ // vertex shader's depth-blended q(x,z) phase coordinate for slot 0 and, when
758
+ // the slot mask permits it, its authored same-direction beat partner in slot 1.
759
+ export function sampleGerstnerHeight(
760
+ waves,
761
+ x,
762
+ z,
763
+ time,
764
+ chopWeight = 1,
765
+ nearshore = null,
766
+ ) {
767
+ const nearshoreBlend = THREE.MathUtils.clamp(Number(nearshore?.blend) || 0, 0, 1);
768
+ const nearshoreSlotMask = Number.isFinite(Number(nearshore?.slotMask))
769
+ ? THREE.MathUtils.clamp(Number(nearshore.slotMask), 0, 2)
770
+ : 2;
707
771
  let height = 0;
708
772
  for (let i = 0; i < waves.length; i += 1) {
709
773
  const wave = waves[i];
710
- const theta = wave.waveNumber * (wave.dirX * x + wave.dirZ * z) -
774
+ const baseCoordinate = wave.dirX * x + wave.dirZ * z;
775
+ const slotWeight = i === 0
776
+ ? Math.min(nearshoreSlotMask, 1)
777
+ : i === 1
778
+ ? Math.max(nearshoreSlotMask - 1, 0)
779
+ : 0;
780
+ const slotBlend = nearshoreBlend * slotWeight;
781
+ const phaseCoordinate = slotBlend > 0
782
+ ? THREE.MathUtils.lerp(baseCoordinate, nearshore.phaseCoordinate, slotBlend)
783
+ : baseCoordinate;
784
+ const theta = wave.waveNumber * phaseCoordinate -
711
785
  wave.omega * time + wave.phase;
712
786
  height += wave.amplitude * (i < 2 ? 1 : chopWeight) * Math.sin(theta);
713
787
  }
714
788
  return height;
715
789
  }
716
790
 
791
+ // CPU mirror of createWaterWavesChunk().gerstnerSwellHeight: only the two
792
+ // long components that survive into the surf zone. Swash samples this at a
793
+ // projected shoreline point to keep its edge connected across the beach.
794
+ export function sampleGerstnerSwellHeight(waves, x, z, time, nearshore = null) {
795
+ const nearshoreBlend = THREE.MathUtils.clamp(Number(nearshore?.blend) || 0, 0, 1);
796
+ const nearshoreSlotMask = Number.isFinite(Number(nearshore?.slotMask))
797
+ ? THREE.MathUtils.clamp(Number(nearshore.slotMask), 0, 2)
798
+ : 2;
799
+ let height = 0;
800
+ for (let i = 0; i < Math.min(2, waves.length); i += 1) {
801
+ const wave = waves[i];
802
+ const baseCoordinate = wave.dirX * x + wave.dirZ * z;
803
+ const slotWeight = i === 0
804
+ ? Math.min(nearshoreSlotMask, 1)
805
+ : Math.max(nearshoreSlotMask - 1, 0);
806
+ const slotBlend = nearshoreBlend * slotWeight;
807
+ const phaseCoordinate = slotBlend > 0
808
+ ? THREE.MathUtils.lerp(baseCoordinate, nearshore.phaseCoordinate, slotBlend)
809
+ : baseCoordinate;
810
+ const theta = wave.waveNumber * phaseCoordinate -
811
+ wave.omega * time + wave.phase;
812
+ height += wave.amplitude * Math.sin(theta);
813
+ }
814
+ return height;
815
+ }
816
+
817
+ // 0..1 cycle of the primary crest at the rest shoreline. Zero is crest
818
+ // arrival; the shader uses the same cycle for the connected swash event.
819
+ export function samplePrimarySwellSequence(waves, time) {
820
+ const primary = waves?.[0];
821
+ if (!primary) return { cycle: 0, index: 0 };
822
+ const raw = (primary.omega * time - primary.phase + Math.PI * 0.5) / (Math.PI * 2);
823
+ const index = Math.floor(raw);
824
+ return { cycle: raw - index, index };
825
+ }
826
+
827
+ export function samplePrimarySwellCycle(waves, time) {
828
+ return samplePrimarySwellSequence(waves, time).cycle;
829
+ }
830
+
831
+ // One physical swash event: fast uprush, slower gravity-driven backwash.
832
+ // Unlike a signed sine, this never drains the sea below the rest shoreline.
833
+ export function shapeSwashProgress(cycle, uprushFraction = 0.34) {
834
+ const phase = ((Number(cycle) || 0) % 1 + 1) % 1;
835
+ const riseEnd = THREE.MathUtils.clamp(uprushFraction, 0.1, 0.8);
836
+ if (phase <= riseEnd) {
837
+ return Math.sin((phase / riseEnd) * Math.PI * 0.5);
838
+ }
839
+ const drain = (phase - riseEnd) / (1 - riseEnd);
840
+ return Math.sin((1 - drain) * Math.PI * 0.5);
841
+ }
842
+
843
+ function swashHash(value) {
844
+ let x = ((value * 0.1031) % 1 + 1) % 1;
845
+ x *= x + 33.33;
846
+ x *= x + x;
847
+ return ((x % 1) + 1) % 1;
848
+ }
849
+
850
+ // Low-frequency shoreline shape for one swash event. This is CPU-authored so
851
+ // the visible water, persistent foam pass, and gameplay queries all receive
852
+ // the same bounded tongue pattern without adding procedural noise to the
853
+ // private-memory-heavy visible fragment shader.
854
+ export function sampleSwashEventShape(cycleIndex) {
855
+ const index = Math.floor(Number(cycleIndex) || 0);
856
+ return {
857
+ phase: swashHash(index + 113.17) * Math.PI * 2,
858
+ frequency: THREE.MathUtils.lerp(0.085, 0.16, swashHash(index + 197.31)),
859
+ amplitude: THREE.MathUtils.lerp(0.55, 1.05, swashHash(index + 251.73)),
860
+ };
861
+ }
862
+
863
+ // Per-event forcing for an irregular swash train. A four-wave interpolated
864
+ // group term supplies the observed low-frequency envelope; the individual
865
+ // term keeps neighbouring bores from sharing one reach. Backwash strength is
866
+ // correlated with the event energy but retains its own variability.
867
+ function sampleSwashForcing(cycleIndex) {
868
+ const groupPosition = cycleIndex / 4;
869
+ const groupIndex = Math.floor(groupPosition);
870
+ const groupT = groupPosition - groupIndex;
871
+ const groupEase = groupT * groupT * (3 - 2 * groupT);
872
+ const group = THREE.MathUtils.lerp(
873
+ swashHash(groupIndex + 19.19),
874
+ swashHash(groupIndex + 20.19),
875
+ groupEase,
876
+ );
877
+ const individual = swashHash(cycleIndex + 7.73);
878
+ const baseRunupScale = 0.82 + group * 0.1 + individual * 0.08;
879
+ const normalizedRunup = THREE.MathUtils.clamp((baseRunupScale - 0.8) / 0.2, 0, 1);
880
+ const backwashStrength = THREE.MathUtils.clamp(
881
+ normalizedRunup * 0.62 + swashHash(cycleIndex + 71.37) * 0.38,
882
+ 0,
883
+ 1,
884
+ );
885
+ return { backwashStrength, baseRunupScale };
886
+ }
887
+
888
+ // Bounded stylized event statistics, informed by random-wave run-up and
889
+ // swash-interaction measurements: ordinary peaks cover 80–100% of the user
890
+ // reach, wave groups correlate several events, and a deep preceding rundown
891
+ // can lend a small amount of momentum to the next bore. The carry is capped
892
+ // at 2% of the authored reach (20 cm for the 10 m calibration beach), so it
893
+ // never overwhelms the event's own forcing. `rundownOffset` is metres relative
894
+ // to the still-water shoreline (negative = farther seaward).
895
+ export function sampleSwashCycleVariation(cycleIndex) {
896
+ const index = Math.floor(Number(cycleIndex) || 0);
897
+ const current = sampleSwashForcing(index);
898
+ const previous = sampleSwashForcing(index - 1);
899
+ const backwashCarry = Math.max(previous.backwashStrength - 0.5, 0) * 0.04;
900
+ return {
901
+ backwashStrength: current.backwashStrength,
902
+ backwashCarry,
903
+ baseRunupScale: current.baseRunupScale,
904
+ rundownOffset: THREE.MathUtils.lerp(0.35, -0.9, current.backwashStrength),
905
+ runupScale: THREE.MathUtils.clamp(current.baseRunupScale + backwashCarry, 0.8, 1),
906
+ };
907
+ }
908
+
909
+ // Continuous centerline position of the swash edge in metres along the beach.
910
+ // Event N begins exactly at event N-1's rundown endpoint, rises to its own
911
+ // varying inland maximum, then drains to a new endpoint without a reset jump.
912
+ export function sampleSwashDistance(waves, time, runupDistance, uprushFraction = 0.34) {
913
+ const sequence = samplePrimarySwellSequence(waves, time);
914
+ const current = sampleSwashCycleVariation(sequence.index);
915
+ const previous = sampleSwashCycleVariation(sequence.index - 1);
916
+ const progress = shapeSwashProgress(sequence.cycle, uprushFraction);
917
+ const peak = Math.max(Number(runupDistance) || 0, 0) * current.runupScale;
918
+ return sequence.cycle <= uprushFraction
919
+ ? THREE.MathUtils.lerp(previous.rundownOffset, peak, progress)
920
+ : THREE.MathUtils.lerp(current.rundownOffset, peak, progress);
921
+ }
922
+
923
+ // One CPU-authored frame shared by the visible swash and the persistent
924
+ // shore-state pass. Keeping event identity, incidence, and derivatives here
925
+ // prevents a foam/wetness texture from becoming a second animation with a
926
+ // slightly different phase or direction.
927
+ export function sampleSwashFrameState(
928
+ waves,
929
+ time,
930
+ runupDistance = 0,
931
+ uprushFraction = 0.34,
932
+ ) {
933
+ const sequence = samplePrimarySwellSequence(waves, time);
934
+ const current = sampleSwashCycleVariation(sequence.index);
935
+ const previous = sampleSwashCycleVariation(sequence.index - 1);
936
+ const edgeShape = sampleSwashEventShape(sequence.index);
937
+ const progress = shapeSwashProgress(sequence.cycle, uprushFraction);
938
+ const derivativeStep = 0.02;
939
+ const beforeSequence = samplePrimarySwellSequence(waves, time - derivativeStep);
940
+ const afterSequence = samplePrimarySwellSequence(waves, time + derivativeStep);
941
+ const progressSpeed = (
942
+ shapeSwashProgress(afterSequence.cycle, uprushFraction) -
943
+ shapeSwashProgress(beforeSequence.cycle, uprushFraction)
944
+ ) / (derivativeStep * 2);
945
+ const maximumDistance = Math.max(Number(runupDistance) || 0, 0);
946
+ const edgeDistance = maximumDistance > 0
947
+ ? sampleSwashDistance(waves, time, maximumDistance, uprushFraction)
948
+ : 0;
949
+ const edgeDistanceSpeed = maximumDistance > 0
950
+ ? (
951
+ sampleSwashDistance(waves, time + derivativeStep, maximumDistance, uprushFraction) -
952
+ sampleSwashDistance(waves, time - derivativeStep, maximumDistance, uprushFraction)
953
+ ) / (derivativeStep * 2)
954
+ : 0;
955
+ return {
956
+ cycle: sequence.cycle,
957
+ cycleSpeed: Math.max(Number(waves?.[0]?.omega) || 0, 0) / (Math.PI * 2),
958
+ edgeDistance,
959
+ edgeDistanceSpeed,
960
+ eventIndex: sequence.index,
961
+ isUprush: sequence.cycle < uprushFraction,
962
+ primaryDirectionX: waves?.[0]?.dirX ?? 0,
963
+ primaryDirectionZ: waves?.[0]?.dirZ ?? -1,
964
+ progress,
965
+ progressSpeed,
966
+ runupScale: current.runupScale,
967
+ startOffset: previous.rundownOffset,
968
+ endOffset: current.rundownOffset,
969
+ edgeShape,
970
+ };
971
+ }
972
+
973
+ // Connected oblique lip: the large term follows the incoming crest angle and
974
+ // small traveling scallops prevent a ruler-straight shoreline. A residual
975
+ // envelope at nominal rest/full reach represents the alongshore arrival lag;
976
+ // the centerline remains the 0..runupDistance calibration reference.
977
+ export function sampleSwashEdgeOffset(
978
+ x,
979
+ time,
980
+ progress,
981
+ waveDirectionX = 0,
982
+ cycle = 0,
983
+ edgeShape = null,
984
+ ) {
985
+ const p = THREE.MathUtils.clamp(Number(progress) || 0, 0, 1);
986
+ const envelope = THREE.MathUtils.lerp(0.18, 1, Math.sin(Math.PI * p));
987
+ // A literal infinite oblique line grows without bound across a wide water
988
+ // tile. The old implementation then hard-clamped that line to the event's
989
+ // run-up maximum, pinning tens of metres of shore to one ruler-straight
990
+ // endpoint before releasing it a mesh column at a time. Soft-sign retains
991
+ // the incidence angle around the camera while approaching a finite offset
992
+ // smoothly, so it never creates a saturated plateau.
993
+ const incidenceSlope = THREE.MathUtils.clamp(
994
+ Number(waveDirectionX) * 0.52,
995
+ -0.2,
996
+ 0.2,
997
+ );
998
+ const rawTilt = -x * incidenceSlope;
999
+ const maximumTilt = 2.0;
1000
+ const tilt = rawTilt / (1 + Math.abs(rawTilt) / maximumTilt);
1001
+ const scallop = (
1002
+ Math.sin(x * 0.32 - time * 0.35) - Math.sin(-time * 0.35) +
1003
+ (Math.sin(x * 0.91 + time * 0.18) - Math.sin(time * 0.18)) * 0.35
1004
+ ) * 0.4;
1005
+ const shape = edgeShape ?? { phase: 0, frequency: 0.1, amplitude: 0 };
1006
+ const phase = Number(shape.phase) || 0;
1007
+ const frequency = THREE.MathUtils.clamp(Number(shape.frequency) || 0.1, 0.02, 0.5);
1008
+ const amplitude = THREE.MathUtils.clamp(Number(shape.amplitude) || 0, 0, 2.5);
1009
+ const secondaryPhase = phase * -0.71;
1010
+ const macroBase = (
1011
+ Math.sin(x * frequency + phase) - Math.sin(phase) +
1012
+ (Math.sin(x * frequency * 2.35 + secondaryPhase) - Math.sin(secondaryPhase)) * 0.42
1013
+ ) * amplitude;
1014
+ const macroWave = Math.sin(THREE.MathUtils.clamp(Number(cycle) || 0, 0, 1) * Math.PI);
1015
+ const macroEnvelope = macroWave * macroWave;
1016
+ return (tilt + scallop) * envelope + macroBase * macroEnvelope;
1017
+ }
1018
+
717
1019
  // --- Field schema -----------------------------------------------------------
718
1020
 
719
1021
  export const WATER_SETTING_GROUPS = Object.freeze([
720
1022
  Object.freeze({ id: 'waves', label: 'Waves', description: 'Gerstner swell and detail ripple shaping.' }),
721
1023
  Object.freeze({ id: 'surface', label: 'Surface', description: 'Water body color, refraction, and caustics.' }),
722
1024
  Object.freeze({ id: 'foam', label: 'Foam', description: 'Shoreline foam, whitecaps, and wake foam.' }),
723
- Object.freeze({ id: 'lighting', label: 'Lighting', description: 'Sun glints, sparkles, fresnel, and reflections.' }),
1025
+ Object.freeze({ id: 'lighting', label: 'Lighting', description: 'Authored fallback sun/sky plus water-specific glint, fresnel, and reflection response.' }),
724
1026
  Object.freeze({ id: 'ripples', label: 'Ripples', description: 'Interactive ripple simulation response.' }),
725
1027
  Object.freeze({ id: 'splashes', label: 'Splashes', description: 'Procedural splash droplets, spray, and rings.' }),
726
1028
  Object.freeze({ id: 'quality', label: 'Quality', description: 'Shader quality tier gating caustics, sparkles, and noise octaves.' }),
727
1029
  ]);
728
1030
 
729
1031
  const FIELD_METADATA = {
730
- waveIntensity: { group: 'waves', label: 'Wave Intensity', min: 0, max: 1, step: 0.01, description: 'Master dial from glassy mirror (0) to storm swell (1).' },
1032
+ waveIntensity: { group: 'waves', label: 'Wave Intensity', min: 0, max: 1, step: 0.01, description: 'Authored baseline from glassy mirror (0) to storm swell (1); scene weather can transiently modulate it without changing the preset.' },
731
1033
  waterLevel: { group: 'waves', label: 'Water Level', min: 0, max: 4, step: 0.01, description: 'World-space rest height of the surface; waves and run-up displace around it.' },
732
1034
  waveAmplitude: { group: 'waves', label: 'Wave Amplitude', min: 0, max: 5, step: 0.01, description: 'Largest wave amplitude in meters at full intensity; 5 gives a 10 m crest-to-trough swell.' },
733
1035
  shoalingDepth: { group: 'waves', label: 'Shoaling Depth', min: 0.05, max: 12, step: 0.05, description: 'Column depth in meters at which waves reach full height; shallower water shrinks them (needs a bed height sampler).' },
734
1036
  shorelineWaves: { group: 'waves', label: 'Shoreline Waves', min: 0, max: 1, step: 0.01, description: 'Fraction of wave height that keeps rolling through the shallows as surf before dying at the waterline.' },
735
1037
  shorelineRunup: { group: 'waves', label: 'Shoreline Run-up', min: 0, max: 3, step: 0.05, description: 'How far incoming waves wash a thin foam film up the beach; reach scales with wave energy.' },
1038
+ runupDistance: { group: 'waves', label: 'Max Run-up Distance', min: 0, max: 15, step: 0.5, description: 'Maximum horizontal reach in meters. Wave groups vary each event from 80–100%, and each backwash hands its endpoint into the next uprush. 0 lets wave energy decide.' },
736
1039
  breakerEnabled: { group: 'waves', label: 'Breakers On', type: 'boolean', description: 'Master switch for the breaker system; off removes the mesh and skips all breaker work (for perf A/B).' },
737
1040
  breakerAmount: { group: 'waves', label: 'Surf Breakers', min: 0, max: 1, step: 0.01, description: 'Dedicated curling breaker shells along the break line; 0 disables the system (needs a bed height sampler).' },
738
1041
  breakerCurl: { group: 'waves', label: 'Breaker Curl', min: 0, max: 1, step: 0.01, description: 'Lip pitch: 0 spills down the face, 1 curls a full surfable tunnel.' },
@@ -772,20 +1075,29 @@ const FIELD_METADATA = {
772
1075
  deepFadeDistance: { group: 'surface', label: 'Deep Fade', min: 0.05, max: 24, step: 0.05, description: 'Additional depth where mid fades to the deep tint.' },
773
1076
  opacity: { group: 'surface', label: 'Opacity', min: 0, max: 1, step: 0.01, description: 'Base transparency when no scene color grab pass is bound.' },
774
1077
  refractionStrength: { group: 'surface', label: 'Refraction', min: 0, max: 2, step: 0.01, description: 'Screen-space distortion of the underwater scene.' },
1078
+ indexOfRefraction: { group: 'surface', label: 'Water IOR', min: 1.0001, max: 1.8, step: 0.001, description: 'Index of refraction used by the underwater Snell window and total internal reflection.' },
1079
+ underwaterTransmission: { group: 'surface', label: 'Underwater View', min: 0, max: 1, step: 0.01, description: 'Visibility of the real above-water scene through the surface from below.' },
1080
+ underwaterTintStrength: { group: 'surface', label: 'Underwater Tint', min: 0, max: 1, step: 0.01, description: 'Stylized water-color tint applied to the view through the surface.' },
775
1081
  causticsStrength: { group: 'surface', label: 'Caustics', min: 0, max: 3, step: 0.01, description: 'Brightness of the procedural voronoi caustics on the bottom.' },
776
1082
  causticsScale: { group: 'surface', label: 'Caustics Scale', min: 0.05, max: 8, step: 0.05, description: 'Spatial frequency of the caustic web.' },
777
1083
  causticsSpeed: { group: 'surface', label: 'Caustics Speed', min: 0, max: 4, step: 0.01, description: 'Animation speed of the caustic web.' },
778
1084
 
779
1085
  foamColor: { group: 'foam', label: 'Foam Color', type: 'color', description: 'Color of all foam: shoreline, whitecaps, wakes, and splashes.' },
780
- foamAmount: { group: 'foam', label: 'Foam Amount', min: 0, max: 2, step: 0.01, description: 'Global foam gain.' },
1086
+ foamAmount: { group: 'foam', label: 'Foam Amount', min: 0, max: 2, step: 0.01, description: 'Offshore contact foam, whitecap, and wake gain.' },
1087
+ swashFoamAmount: { group: 'foam', label: 'Swash Foam', min: 0, max: 2, step: 0.01, description: 'Independent gain for torn foam carried up and back down the beach.' },
1088
+ swashFoamLifetime: { group: 'foam', label: 'Swash Foam Life (s)', min: 0.25, max: 30, step: 0.25, description: 'Seconds fresh aerated swash foam remains before thinning into residue.' },
1089
+ swashFoamResidueLifetime: { group: 'foam', label: 'Foam Residue Life (s)', min: 0.5, max: 60, step: 0.5, description: 'Seconds fragmented beach foam persists and drifts after the active front passes.' },
1090
+ wetSandDryTime: { group: 'foam', label: 'Wet Sand Drying (s)', min: 2, max: 600, step: 1, description: 'Seconds saturated sand takes to return to its dry color after the water retreats.' },
1091
+ wetSandDarkening: { group: 'foam', label: 'Wet Sand Darkening', min: 0, max: 1, step: 0.01, description: 'How strongly remembered moisture darkens exposed sand.' },
1092
+ wetSandSheen: { group: 'foam', label: 'Wet Sand Sheen', min: 0, max: 1, step: 0.01, description: 'Strength of the short-lived glossy water film left on freshly exposed sand.' },
781
1093
  foamContactDistance: { group: 'foam', label: 'Contact Distance', min: 0.02, max: 4, step: 0.01, description: 'Depth difference covered by the solid contact foam band.' },
782
1094
  foamLineSpacing: { group: 'foam', label: 'Line Spacing', min: 0.05, max: 4, step: 0.01, description: 'Spacing of the animated lapping foam lines off the shore.' },
783
1095
  foamNoiseScale: { group: 'foam', label: 'Foam Noise Scale', min: 0.05, max: 8, step: 0.05, description: 'Breakup noise frequency for foam edges.' },
784
1096
  whitecapAmount: { group: 'foam', label: 'Whitecaps', min: 0, max: 1, step: 0.01, description: 'Coverage of breaking crests on open water.' },
785
1097
  rippleFoamStrength: { group: 'foam', label: 'Wake Foam', min: 0, max: 3, step: 0.01, description: 'Foam intensity left behind by interactive ripples and wakes.' },
786
1098
 
787
- sunDirection: { group: 'lighting', label: 'Sun Direction', type: 'vector3', description: 'World-space direction toward the sun.' },
788
- sunColor: { group: 'lighting', label: 'Sun Color', type: 'color', description: 'Sun tint used by glints, sparkles, and caustics.' },
1099
+ sunDirection: { group: 'lighting', label: 'Sun Direction', type: 'vector3', description: 'Authored fallback direction toward the sun when no live scene-light override is connected.' },
1100
+ sunColor: { group: 'lighting', label: 'Sun Color', type: 'color', description: 'Authored fallback sun tint for glints, sparkles, and caustics; a live scene rig may replace it transiently.' },
789
1101
  specularStrength: { group: 'lighting', label: 'Specular', min: 0, max: 3, step: 0.01, description: 'Toon sun-glint intensity.' },
790
1102
  specularShininess: { group: 'lighting', label: 'Shininess', min: 4, max: 2000, step: 1, description: 'Glint tightness; higher is smaller and sharper.' },
791
1103
  specularStretch: { group: 'lighting', label: 'Glint Stretch', min: 0, max: 0.95, step: 0.01, description: 'Elongates glints along the sun azimuth into a sparkling sun path.' },
@@ -798,8 +1110,8 @@ const FIELD_METADATA = {
798
1110
  fresnelPower: { group: 'lighting', label: 'Fresnel Power', min: 0.5, max: 12, step: 0.1, description: 'Falloff of the fresnel band toward the horizon.' },
799
1111
  fresnelBias: { group: 'lighting', label: 'Fresnel Bias', min: 0, max: 0.6, step: 0.01, description: 'Sky-tint floor at steep angles; higher reads more anime-blue.' },
800
1112
  fresnelColor: { group: 'lighting', label: 'Fresnel Color', type: 'color', description: 'Additive rim tint at grazing angles.' },
801
- skyZenithColor: { group: 'lighting', label: 'Sky Zenith', type: 'color', description: 'Procedural sky reflection color overhead.' },
802
- skyHorizonColor: { group: 'lighting', label: 'Sky Horizon', type: 'color', description: 'Procedural sky reflection color at the horizon.' },
1113
+ skyZenithColor: { group: 'lighting', label: 'Sky Zenith', type: 'color', description: 'Authored fallback procedural sky-reflection color overhead when no live scene sky is connected.' },
1114
+ skyHorizonColor: { group: 'lighting', label: 'Sky Horizon', type: 'color', description: 'Authored fallback procedural sky-reflection color at the horizon when no live scene sky is connected.' },
803
1115
  reflectionStrength: { group: 'lighting', label: 'Reflection', min: 0, max: 1.5, step: 0.01, description: 'Planar/sky reflection mix, weighted by fresnel.' },
804
1116
  reflectionDistortion: { group: 'lighting', label: 'Reflection Ripple', min: 0, max: 0.3, step: 0.005, description: 'How much waves shatter the reflection.' },
805
1117
  reflectionSoftness: { group: 'lighting', label: 'Reflection Softness', min: 0, max: 1, step: 0.01, description: 'Blends sharp planar reflections toward the soft procedural sky (milky anime look).' },