@call-me-sensei/toonlab 0.4.21 → 0.4.23

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 (129) hide show
  1. package/NPM-LIBRARY.md +87 -27
  2. package/README.md +11 -10
  3. package/agents/PROMPTS.md +17 -3
  4. package/agents/README.md +13 -3
  5. package/agents/claude/CLAUDE.md +3 -0
  6. package/agents/codex/AGENTS.md +112 -159
  7. package/agents/cursor/toonlab.mdc +7 -3
  8. package/agents/examples/game-foundation.mjs +104 -0
  9. package/agents/references/game-lifecycle.md +57 -0
  10. package/agents/references/runtime-entry-points.md +26 -0
  11. package/agents/references/style-bundles.md +4 -1
  12. package/agents/skills/claude/asset-sourcing/SKILL.md +7 -4
  13. package/agents/skills/claude/environment/SKILL.md +4 -0
  14. package/agents/skills/claude/game-dev/SKILL.md +63 -87
  15. package/agents/skills/claude/lighting/SKILL.md +36 -0
  16. package/agents/skills/claude/post-processing/SKILL.md +8 -3
  17. package/agents/skills/claude/rock-ground-shaders/SKILL.md +10 -15
  18. package/agents/skills/claude/rockgen/SKILL.md +52 -21
  19. package/agents/skills/claude/scene-style-application/SKILL.md +4 -0
  20. package/agents/skills/claude/style-presets/SKILL.md +11 -8
  21. package/agents/skills/claude/toon-shading/SKILL.md +4 -0
  22. package/agents/skills/claude/vegetation-sky/SKILL.md +13 -7
  23. package/agents/skills/claude/visual-verification/SKILL.md +45 -119
  24. package/agents/skills/claude/water/SKILL.md +6 -2
  25. package/agents/skills/codex/asset-sourcing/SKILL.md +7 -4
  26. package/agents/skills/codex/environment/SKILL.md +4 -0
  27. package/agents/skills/codex/game-dev/SKILL.md +63 -87
  28. package/agents/skills/codex/lighting/SKILL.md +36 -0
  29. package/agents/skills/codex/post-processing/SKILL.md +8 -3
  30. package/agents/skills/codex/rock-ground-shaders/SKILL.md +10 -15
  31. package/agents/skills/codex/rockgen/SKILL.md +52 -21
  32. package/agents/skills/codex/scene-style-application/SKILL.md +4 -0
  33. package/agents/skills/codex/style-presets/SKILL.md +11 -8
  34. package/agents/skills/codex/toon-shading/SKILL.md +4 -0
  35. package/agents/skills/codex/vegetation-sky/SKILL.md +13 -7
  36. package/agents/skills/codex/visual-verification/SKILL.md +45 -119
  37. package/agents/skills/codex/water/SKILL.md +6 -2
  38. package/database/apply-one-catalog-seed.mjs +63 -0
  39. package/database/providers.mjs +1 -0
  40. package/database/seeds/catalog/0005_2026-08-c8-first12.sql +1101 -0
  41. package/database/seeds/catalog/0006_2026-09-c8-first100.sql +14245 -0
  42. package/database/seeds/catalog/0007_2026-09-c8-first100-primary-model.sql +14845 -0
  43. package/mcp/lab-management.mjs +11 -8
  44. package/mcp/public-catalog.mjs +0 -16
  45. package/mcp/tree-lab-contract.mjs +2381 -0
  46. package/mcp/vite-plugin.mjs +5 -1
  47. package/package.json +29 -18
  48. package/src/catalog/officialCatalog.js +1 -0
  49. package/src/catalog/officialCatalogAssetRuntime.js +98 -1
  50. package/src/catalog/officialCatalogLod.js +160 -16
  51. package/src/catalog/officialCatalogPlacement.js +52 -12
  52. package/src/catalog/officialCatalogProvider.js +79 -8
  53. package/src/catalog/officialCatalogRockPackage.js +172 -0
  54. package/src/cloud/cloudReprojection.js +40 -7
  55. package/src/cloud/cloudVolume.js +44 -7
  56. package/src/cloud/noise/baseShapeVolume.js +8 -6
  57. package/src/cloud/noise/cirrusMap.js +2 -1
  58. package/src/cloud/noise/curlNoise.js +2 -1
  59. package/src/cloud/noise/erosionVolume.js +2 -1
  60. package/src/cloud/noise/textureCache.js +77 -0
  61. package/src/cloud/noise/weatherMap.js +2 -1
  62. package/src/core/sha256.js +83 -0
  63. package/src/ground-shader/groundShaderMaterial.js +12 -3
  64. package/src/rock-shader/rockMaterial.js +205 -28
  65. package/src/rock-shader/rockShaderRuntime.js +17 -1
  66. package/src/rock-shader/rockShaderSettings.js +16 -0
  67. package/src/rockgen/index.js +9 -0
  68. package/src/rockgen/lod/index.js +1 -0
  69. package/src/rockgen/lod/rockDenseFieldPolicy.js +143 -0
  70. package/src/rockgen/rockDocument.js +899 -35
  71. package/src/rockgen/surface/c7GeologySurface.js +39 -9
  72. package/src/rockgen/surface/naturalRockSurface.js +1948 -0
  73. package/src/shaders-tsl/chunks/projected-water-caustics.js +14 -2
  74. package/src/shaders-tsl/chunks/water-foam.js +6 -6
  75. package/src/shaders-tsl/chunks/water-lighting.js +28 -5
  76. package/src/shaders-tsl/chunks/water-shore-state.js +5 -2
  77. package/src/shaders-tsl/chunks/water-waves.js +10 -5
  78. package/src/shaders-tsl/flower.js +7 -4
  79. package/src/shaders-tsl/grass.js +4 -3
  80. package/src/shaders-tsl/water-shore-state-simulation.js +3 -2
  81. package/src/shaders-tsl/water.js +209 -41
  82. package/src/sky/skySystem.js +30 -7
  83. package/src/toon/settings/perspectiveRemovalSettings.js +1 -1
  84. package/src/toon/toonSettings.js +74 -0
  85. package/src/vegetation/stylizedFlower.js +6 -0
  86. package/src/version.js +1 -1
  87. package/src/water/waterDetailSpectrum.js +91 -0
  88. package/src/water/waterDynamics.js +422 -0
  89. package/src/water/waterFoamParticles.js +196 -0
  90. package/src/water/waterFoamTexture.js +57 -0
  91. package/src/water/waterHydrodynamics.js +209 -0
  92. package/src/water/waterMaterial.js +1 -0
  93. package/src/water/waterRenderExtension.js +107 -0
  94. package/src/water/waterScenePasses.js +14 -6
  95. package/src/water/waterSettings.js +53 -26
  96. package/src/water/waterShoreMaterial.js +38 -14
  97. package/src/water/waterSpectralOcean.js +137 -0
  98. package/src/water/waterStageSettings.js +19 -1
  99. package/src/water/waterSurface.js +62 -18
  100. package/src/water/waterUnderwaterAtmosphere.js +30 -3
  101. package/types/catalog/officialCatalog.d.ts +1 -0
  102. package/types/catalog/officialCatalogPlacement.d.ts +4 -0
  103. package/types/catalog/officialCatalogRockPackage.d.ts +68 -0
  104. package/types/cloud/noise/baseShapeVolume.d.ts +1 -1
  105. package/types/cloud/noise/textureCache.d.ts +12 -0
  106. package/types/core/sha256.d.ts +2 -0
  107. package/types/index.d.ts +30 -0
  108. package/types/rock-shader/rockMaterial.d.ts +3 -0
  109. package/types/rock-shader/rockShaderSettings.d.ts +2 -0
  110. package/types/rockgen/index.d.ts +1 -0
  111. package/types/rockgen/lod/index.d.ts +1 -0
  112. package/types/rockgen/lod/rockDenseFieldPolicy.d.ts +181 -0
  113. package/types/rockgen/rockDocument.d.ts +654 -71
  114. package/types/rockgen/surface/naturalRockSurface.d.ts +100 -0
  115. package/types/shaders-tsl/chunks/projected-water-caustics.d.ts +7 -1
  116. package/types/shaders-tsl/chunks/water-lighting.d.ts +4 -3
  117. package/types/shaders-tsl/water.d.ts +2 -1
  118. package/types/version.d.ts +1 -1
  119. package/types/water/waterDetailSpectrum.d.ts +13 -0
  120. package/types/water/waterDynamics.d.ts +89 -0
  121. package/types/water/waterFoamParticles.d.ts +32 -0
  122. package/types/water/waterFoamTexture.d.ts +1 -0
  123. package/types/water/waterHydrodynamics.d.ts +49 -0
  124. package/types/water/waterRenderExtension.d.ts +16 -0
  125. package/types/water/waterScenePasses.d.ts +1 -1
  126. package/types/water/waterSettings.d.ts +1 -0
  127. package/types/water/waterSpectralOcean.d.ts +17 -0
  128. package/types/water/waterStageSettings.d.ts +1 -1
  129. package/types/water/waterUnderwaterAtmosphere.d.ts +19 -4
@@ -26,6 +26,7 @@
26
26
  // phase resolves the ordering.
27
27
 
28
28
  import * as THREE from 'three';
29
+ import { getWaterFoamTexture } from '../water/waterFoamTexture.js';
29
30
  import {
30
31
  abs,
31
32
  attribute,
@@ -33,6 +34,7 @@ import {
33
34
  cameraProjectionMatrix,
34
35
  cameraViewMatrix,
35
36
  clamp,
37
+ cos,
36
38
  Discard,
37
39
  dot,
38
40
  exp,
@@ -75,6 +77,7 @@ import { createWaterFoamChunk } from './chunks/water-foam.js';
75
77
  import { createWaterLightingChunk } from './chunks/water-lighting.js';
76
78
  import { createWaterRippleChunk } from './chunks/water-ripple.js';
77
79
  import { createWaterShoreStateChunk } from './chunks/water-shore-state.js';
80
+ import { projectedWaterCaustics } from './chunks/projected-water-caustics.js';
78
81
  import { createWaterWavesChunk } from './chunks/water-waves.js';
79
82
 
80
83
  // Node-backend placeholders: same roles as waterMaterial.js's placeholders,
@@ -132,8 +135,10 @@ export function createWaterNodeMaterial({
132
135
  detailOctaves = 4,
133
136
  foamOctaves = 3,
134
137
  shoaling = false,
138
+ dynamics = false,
135
139
  } = {}) {
136
140
  const flags = {
141
+ dynamics: Boolean(dynamics),
137
142
  caustics: qualityLevel >= 1,
138
143
  chromaticCaustics: qualityLevel >= 2,
139
144
  detailOctaves,
@@ -168,6 +173,23 @@ export function createWaterNodeMaterial({
168
173
  uDistanceFogColor: uniform(new THREE.Color(0.66, 0.8, 0.94)),
169
174
  uDistanceFogDensity: uniform(0),
170
175
  uTime: uniform(0),
176
+ uReferenceView: uniform(0),
177
+ uUseShoreContour: uniform(0),
178
+ uFoamCoordinates: texture(holders.ripple),
179
+ uFoamBubbles: texture(getWaterFoamTexture()),
180
+ uFoamParticleRegion: uniform(new THREE.Vector4(0,0,1,1)),
181
+ uFoamParticleCoverage: uniform(0),
182
+ uFoamFrontState: texture(holders.ripple),
183
+ uUseFoamFrontState: uniform(0),
184
+ uContinueShoreAlongX: uniform(0),
185
+ uUseSpectralNormals: uniform(0),
186
+ uSpectralNormal0: texture(holders.ripple),
187
+ uSpectralNormal1: texture(holders.ripple),
188
+ uSpectralNormal2: texture(holders.ripple),
189
+ uSpectralLengths: uniform(new THREE.Vector3(1,1,1)),
190
+ uDetailNormal0: texture(holders.ripple),
191
+ uDetailNormal1: texture(holders.ripple),
192
+ uDetailLengths: uniform(new THREE.Vector2(1,1)),
171
193
  uResolution: uniform(new THREE.Vector2(1, 1)),
172
194
  uCameraNear: uniform(0.05),
173
195
  uCameraFar: uniform(200),
@@ -279,7 +301,7 @@ export function createWaterNodeMaterial({
279
301
  const shoreState = createWaterShoreStateChunk({ u });
280
302
  const color = createWaterColorChunk({ u, flags });
281
303
  const foam = createWaterFoamChunk({ u, foamOctaves });
282
- const lighting = createWaterLightingChunk({ u });
304
+ const lighting = createWaterLightingChunk({ u, physicalSurface: flags.dynamics });
283
305
 
284
306
  const material = new NodeMaterial();
285
307
  material.name = 'StylizedWater';
@@ -309,12 +331,34 @@ export function createWaterNodeMaterial({
309
331
  // is under dry land; the fragment shader discards those fragments so
310
332
  // triangles spanning a cliff face can't poke through the terrain.
311
333
  const vRestDepth = varying(float(), 'vWaterRestDepth');
334
+ const vDynamicsFoam = varying(float(), 'vDynamicsFoam');
335
+ const vDynamicsFlow = varying(vec2(), 'vDynamicsFlow');
336
+ const vWetDistance = varying(float(), 'vWaterWetDistance');
312
337
 
313
338
  material.vertexNode = Fn(() => {
314
339
  const worldPosition = modelWorldMatrix.mul(vec4(positionGeometry, 1.0)).toVar();
315
340
  const restXZ = worldPosition.xz.toVar();
316
341
  vRestWorldXZ.assign(restXZ);
317
342
 
343
+ if (flags.dynamics) {
344
+ const state = attribute('aWaterState', 'vec4');
345
+ vWetDistance.assign(attribute('aWaterWetDistance','float'));
346
+ worldPosition.y.assign(state.x);
347
+ vRestDepth.assign(state.y);
348
+ vSwashZone.assign(smoothstep(0.02, 0.25, state.y).oneMinus());
349
+ vSwashFoamData.assign(vec2(state.y.sub(0.002), 0.02));
350
+ vSwashSurge.assign(vec2(0.0));
351
+ vGerstnerNormal.assign(attribute('aWaterNormal', 'vec3'));
352
+ vCrest.assign(float(0.0));
353
+ vShoal.assign(float(1.0));
354
+ vBreaker.assign(float(0.0));
355
+ vDynamicsFlow.assign(state.zw);
356
+ vDynamicsFoam.assign(shoreState.shoreStateSample(restXZ).b);
357
+ vWorldPosition.assign(worldPosition.xyz);
358
+ return cameraProjectionMatrix.mul(cameraViewMatrix).mul(vec4(worldPosition.xyz, 1.0));
359
+ }
360
+ vDynamicsFoam.assign(float(0.0));
361
+ vDynamicsFlow.assign(vec2(0.0));
318
362
  // Shallow water filters the spectrum: the short cross chop dies out toward
319
363
  // the surf zone, leaving clean parallel lines of the dominant swell.
320
364
  const chopWeight = float(1.0).toVar();
@@ -454,10 +498,13 @@ export function createWaterNodeMaterial({
454
498
  .mul(u.uSwashEdgeShape.z)
455
499
  .mul(macroWave.mul(macroWave))
456
500
  .toVar();
501
+ const swashActivity = smoothstep(0.0, 0.05, u.uWaveEnergy)
502
+ .mul(select(max(u.uRunupDistance, u.uShorelineRunup).greaterThan(0.0), 1.0, 0.0)).toVar();
457
503
  const edgeHead = edgeDistance.mul(bedSlope)
458
504
  .add(
459
505
  edgeTilt.add(edgeScallop).mul(edgeEnvelope)
460
506
  .add(edgeMacro)
507
+ .mul(swashActivity)
461
508
  .mul(bedSlope),
462
509
  ).toVar();
463
510
  const filmHead = restDepth.add(edgeHead).toVar();
@@ -493,7 +540,7 @@ export function createWaterNodeMaterial({
493
540
  const surgeDistance = min(runupDistance.mul(0.32), 3.2).toVar();
494
541
  const swashDrift = shoreDirection.mul(swashProgress).mul(surgeDistance)
495
542
  .add(alongDirection.mul(sin(u.uTime.mul(0.37))).mul(swashProgress).mul(0.55));
496
- vSwashSurge.assign(swashDrift.mul(beach));
543
+ vSwashSurge.assign(swashDrift.mul(beach).mul(swashActivity));
497
544
  waveDisplacement.assign(vec3(
498
545
  rawWave.x.mul(shoal),
499
546
  mix(brokenY, filmY, beach),
@@ -539,7 +586,9 @@ export function createWaterNodeMaterial({
539
586
  // Apply the nonlinear wet threshold after the signed film head has been
540
587
  // interpolated. Evaluating smoothstep at sparse vertices first exposed
541
588
  // individual triangle rows as the water advanced at a grazing angle.
542
- if (flags.shoaling) {
589
+ if (flags.dynamics) {
590
+ Discard(mix(vRestDepth.sub(0.002),vWetDistance,u.uUseShoreContour).lessThan(0.0));
591
+ } else if (flags.shoaling) {
543
592
  const fragmentWet = mix(
544
593
  1.0,
545
594
  smoothstep(-0.004, 0.012, vSwashFoamData.x),
@@ -571,13 +620,16 @@ export function createWaterNodeMaterial({
571
620
  // A centimetres-thin film is optically smooth at this camera scale. Full
572
621
  // open-water normal detail turned every texel into a bright grazing-angle
573
622
  // mirror and made the swash look like an opaque sheet of snow.
574
- const detailStrength = u.uDetailNormalStrength
623
+ const detailStrength = u.uDetailNormalStrength.mul(u.uReferenceView.oneMinus())
575
624
  .mul(mix(0.2, 1.0, detailFade))
576
625
  .mul(mix(0.1, 1.0, horizonFade))
577
626
  .mul(mix(1.0, 0.18, swashOptics)).toVar();
578
627
  const flowOffset = u.uFlowDirection.mul(u.uTime).mul(u.uFlowSpeed).toVar();
579
- const detailUv1 = vRestWorldXZ.mul(u.uDetailScale).add(flowOffset.mul(0.55));
580
- const detailUv2 = waterRotate2d(1.9).mul(vRestWorldXZ).mul(u.uDetailScale.mul(1.63)).sub(flowOffset.mul(0.34));
628
+ // Advect in world space BEFORE frequency/rotation: changing detail scale
629
+ // must not reverse or rotate the current (f(x - U*t) travels with U).
630
+ const advectedXZ = vRestWorldXZ.sub(flowOffset).toVar();
631
+ const detailUv1 = advectedXZ.mul(u.uDetailScale);
632
+ const detailUv2 = waterRotate2d(1.9).mul(advectedXZ).mul(u.uDetailScale.mul(1.63));
581
633
  const waterDetailGradient = (detailUv) => {
582
634
  const uv = vec2(detailUv).toVar();
583
635
  const center = waterFbm(uv, flags.detailOctaves).toVar();
@@ -585,11 +637,31 @@ export function createWaterNodeMaterial({
585
637
  const dy = waterFbm(uv.add(vec2(0.0, 0.11)), flags.detailOctaves);
586
638
  return vec2(dx.sub(center), dy.sub(center)).div(0.11);
587
639
  };
588
- const detailGradient = waterDetailGradient(detailUv1).mul(detailStrength)
589
- .add(waterDetailGradient(detailUv2).mul(detailStrength).mul(0.55)).toVar();
640
+ const detailGradient = vec2(0.0).toVar();
641
+ if (flags.dynamics) {
642
+ // Broadband short waves with seeded phases and dispersion. Two tiles
643
+ // of different extents avoid the eight visible, endlessly repeated
644
+ // ridges from the former plane-wave sum. Mips filter distant glints.
645
+ const activity = clamp(u.uWaveEnergy.div(0.35), 0.0, 1.0);
646
+ const styleGain = mix(u.uDetailNormalStrength.mul(0.45).add(1.0), 1.0, u.uReferenceView);
647
+ const shortSlope = u.uDetailNormal0.sample(advectedXZ.div(u.uDetailLengths.x).add(0.5/64)).rg
648
+ .add(u.uDetailNormal1.sample(advectedXZ.div(u.uDetailLengths.y).add(0.5/64)).rg);
649
+ detailGradient.assign(shortSlope.mul(activity).mul(styleGain).mul(detailFade).mul(smoothstep(0.025,0.22,vRestDepth)));
650
+ } else {
651
+ detailGradient.assign(waterDetailGradient(detailUv1).mul(detailStrength)
652
+ .add(waterDetailGradient(detailUv2).mul(detailStrength).mul(0.55)));
653
+ }
590
654
  const rippleGradient = ripple.rippleGradient(vRestWorldXZ);
591
655
  const rippleState = ripple.rippleSample(vRestWorldXZ).toVar();
592
656
  const waveNormal = waterCombineNormal(gerstnerNormal, rippleGradient).toVar();
657
+ if (flags.dynamics) {
658
+ const spectralSlope = u.uSpectralNormal0.sample(advectedXZ.div(u.uSpectralLengths.x)).level(0).rg
659
+ .add(u.uSpectralNormal1.sample(advectedXZ.div(u.uSpectralLengths.y)).level(0).rg)
660
+ .add(u.uSpectralNormal2.sample(advectedXZ.div(u.uSpectralLengths.z)).level(0).rg);
661
+ const spectralNormal = normalize(vec3(spectralSlope.x.negate(), 1.0, spectralSlope.y.negate()));
662
+ const depthBlend = smoothstep(0.2, 1.5, vRestDepth).mul(u.uUseSpectralNormals);
663
+ waveNormal.assign(normalize(mix(waveNormal, spectralNormal, depthBlend)));
664
+ }
593
665
  const surfaceNormal = waterCombineNormal(waveNormal, detailGradient).toVar();
594
666
  // Refraction reads with most of the micro detail removed.
595
667
  const refractionNormal = waterCombineNormal(waveNormal, detailGradient.mul(0.3)).toVar();
@@ -609,23 +681,21 @@ export function createWaterNodeMaterial({
609
681
  float(1.0).sub(float(1.0).div(eta.mul(eta))),
610
682
  0.0,
611
683
  ).sqrt().toVar();
612
- const snellWindow = smoothstep(
613
- criticalCos.sub(0.04),
614
- criticalCos.add(0.045),
615
- cosIncident,
616
- ).toVar();
684
+ // No transmitted ray exists beyond the critical angle. Fresnel itself
685
+ // approaches full reflection continuously from the transmissive side.
686
+ const snellWindow = select(cosIncident.greaterThan(criticalCos), 1.0, 0.0).toVar();
617
687
  const refractedDirection = refract(
618
688
  incident,
619
689
  surfaceNormal.negate(),
620
690
  eta,
621
691
  ).toVar();
622
- // refract() returns zero under total internal reflection. Blend toward
623
- // the incident ray there to keep the projection numerically stable;
692
+ // refract() returns zero under total internal reflection. Use the
693
+ // incident ray only for a finite unused projection in that branch;
624
694
  // snellWindow prevents that fallback ray from becoming transmission.
625
695
  const transmissionDirection = normalize(mix(
626
696
  incident,
627
697
  refractedDirection,
628
- max(snellWindow, 0.001),
698
+ snellWindow,
629
699
  )).toVar();
630
700
  const transmissionWorld = vWorldPosition.add(
631
701
  transmissionDirection.mul(min(u.uCameraFar.mul(0.75), 400.0)),
@@ -653,10 +723,10 @@ export function createWaterNodeMaterial({
653
723
  clamp(insideCapture.mul(u.uUseSceneColor), 0.0, 1.0),
654
724
  ).toVar();
655
725
 
656
- // Compact Beer-Lambert-inspired attenuation through the camera's water
657
- // column. This uses ToonLab's authored palette, keeping the result
726
+ // Compact Beer-Lambert-inspired attenuation along the camera's water
727
+ // path. This uses ToonLab's authored palette, keeping the result
658
728
  // graphic and readable instead of pursuing the reference's realism.
659
- const cameraDepth = max(vWorldPosition.y.sub(cameraPosition.y), 0.0).toVar();
729
+ const cameraDepth = viewDistance; // optical path to this surface point, not vertical depth
660
730
  const volumeAbsorb = exp(
661
731
  cameraDepth.div(max(
662
732
  u.uDepthFadeDistance.add(u.uDeepFadeDistance.mul(0.5)),
@@ -668,6 +738,10 @@ export function createWaterNodeMaterial({
668
738
  u.uMidColor,
669
739
  clamp(volumeAbsorb.mul(1.35), 0.0, 1.0),
670
740
  ).toVar();
741
+ if (flags.dynamics) {
742
+ const transmittance = exp(vec3(0.20, 0.055, 0.035).mul(viewDistance).negate());
743
+ transmitted.assign(transmitted.mul(transmittance).add(u.uMidColor.mul(transmittance.oneMinus()).mul(0.35)));
744
+ } else {
671
745
  transmitted.mulAssign(mix(
672
746
  vec3(1.0),
673
747
  waterTint.mul(1.12),
@@ -679,10 +753,16 @@ export function createWaterNodeMaterial({
679
753
  volumeAbsorb.mul(u.uUnderwaterTintStrength),
680
754
  ));
681
755
 
682
- const f0 = eta.sub(1.0).div(eta.add(1.0)).pow2().toVar();
683
- const fresnel = f0.add(
684
- f0.oneMinus().mul(pow(cosIncident.oneMinus(), 5.0)),
685
- ).toVar();
756
+ }
757
+
758
+ const cosTransmitted = max(float(1.0).sub(
759
+ eta.mul(eta).mul(float(1.0).sub(cosIncident.mul(cosIncident))),
760
+ ), 0.0).sqrt().toVar();
761
+ const parallel = cosIncident.sub(eta.mul(cosTransmitted))
762
+ .div(max(cosIncident.add(eta.mul(cosTransmitted)), 1e-5));
763
+ const perpendicular = eta.mul(cosIncident).sub(cosTransmitted)
764
+ .div(max(eta.mul(cosIncident).add(cosTransmitted), 1e-5));
765
+ const fresnel = parallel.mul(parallel).add(perpendicular.mul(perpendicular)).mul(0.5).toVar();
686
766
  const transmissionWeight = fresnel.oneMinus()
687
767
  .mul(snellWindow)
688
768
  .mul(u.uUnderwaterTransmission)
@@ -691,7 +771,15 @@ export function createWaterNodeMaterial({
691
771
  u.uDeepColor.mul(0.48),
692
772
  u.uMidColor.mul(0.72),
693
773
  cosIncident.mul(0.3),
694
- );
774
+ ).toVar();
775
+ If(u.uUseReflectionMap.greaterThan(0.5), () => {
776
+ const reflectedClip = u.uReflectionMatrix.mul(vec4(vWorldPosition, 1.0));
777
+ const reflectedUv = reflectedClip.xy.div(max(abs(reflectedClip.w), 1e-5))
778
+ .add(surfaceNormal.xz.mul(u.uReflectionDistortion));
779
+ const reflectedScene = u.uReflectionMap.sample(clamp(reflectedUv, vec2(0.0), vec2(1.0))).level(0).rgb;
780
+ const attenuation = exp(viewDistance.mul(0.06).negate());
781
+ tirColor.assign(mix(u.uMidColor, reflectedScene, attenuation));
782
+ });
695
783
  const underColor = mix(
696
784
  tirColor,
697
785
  transmitted,
@@ -706,10 +794,10 @@ export function createWaterNodeMaterial({
706
794
  criticalCos.add(0.1),
707
795
  cosIncident,
708
796
  ).oneMinus());
709
- underColor.addAssign(u.uFresnelColor.mul(criticalRim).mul(0.12));
710
- underColor.addAssign(
711
- lighting.sparkles(vRestWorldXZ, surfaceNormal, viewDir, viewDistance, u.uTime).mul(0.5),
712
- );
797
+ if (!flags.dynamics) {
798
+ underColor.addAssign(u.uFresnelColor.mul(criticalRim).mul(0.12).mul(u.uReferenceView.oneMinus()));
799
+ underColor.addAssign(lighting.sparkles(vRestWorldXZ, surfaceNormal, viewDir, viewDistance, u.uTime).mul(0.5));
800
+ }
713
801
  // The captured air-side scene is already composited; render the
714
802
  // underside opaque to avoid double-blending the main scene behind it.
715
803
  output.assign(vec4(underColor, 1.0));
@@ -728,7 +816,9 @@ export function createWaterNodeMaterial({
728
816
  max(vWorldPosition.y.sub(groundWorld.y), 0.0),
729
817
  u.uDepthFadeDistance.add(u.uDeepFadeDistance),
730
818
  ).toVar();
731
- const effectiveDepth = columnDepth.add(viewDepthDiff.mul(0.18));
819
+ const effectiveDepth = flags.dynamics
820
+ ? length(groundWorld.sub(vWorldPosition))
821
+ : columnDepth.add(viewDepthDiff.mul(0.18));
732
822
 
733
823
  const { absorb, tint: absorptionTint } = color.absorptionTint(effectiveDepth);
734
824
 
@@ -738,6 +828,26 @@ export function createWaterNodeMaterial({
738
828
  const sceneColor = u.uSceneColor.sample(refractedUv).level(0).rgb;
739
829
  const tintedScene = sceneColor.mul(mix(vec3(1.0), u.uShallowColor.mul(1.25), clamp(absorb.mul(0.6), 0.0, 1.0)));
740
830
  bodyColor.assign(mix(tintedScene, absorptionTint, absorb));
831
+ if (flags.dynamics) {
832
+ const transmission = exp(vec3(0.20,0.055,0.035).mul(effectiveDepth).negate());
833
+ const physicalBody = sceneColor.mul(transmission).add(u.uDeepColor.mul(transmission.oneMinus()));
834
+ // Preserve optical depth in both treatments. Style colours the
835
+ // scattering, rather than replacing clear swash with opaque tint.
836
+ const shallowDistance = max(u.uDepthFadeDistance, 0.05);
837
+ const deepDistance = max(u.uDeepFadeDistance, 0.05);
838
+ const styledScatter = mix(
839
+ mix(u.uShallowColor, u.uMidColor, smoothstep(0.0, shallowDistance, effectiveDepth)),
840
+ u.uDeepColor,
841
+ smoothstep(shallowDistance, shallowDistance.add(deepDistance), effectiveDepth),
842
+ );
843
+ // Style controls optical density as well as hue. At centimetre
844
+ // depths this still transmits the sand; metre-deep water now
845
+ // visibly consumes the authored shallow, mid and deep palette.
846
+ const styledTransmission = transmission.mul(exp(effectiveDepth.div(shallowDistance).mul(-0.65)));
847
+ const styledBody = sceneColor.mul(styledTransmission)
848
+ .add(styledScatter.mul(styledTransmission.oneMinus()));
849
+ bodyColor.assign(mix(styledBody, physicalBody, u.uReferenceView));
850
+ }
741
851
  alpha.assign(1.0);
742
852
  }).Else(() => {
743
853
  bodyColor.assign(absorptionTint);
@@ -749,12 +859,14 @@ export function createWaterNodeMaterial({
749
859
  const clearScene = u.uSceneColor.sample(screenUv).level(0).rgb;
750
860
  const wetScene = clearScene.mul(0.85);
751
861
  const clearSceneWeight = select(u.uUseSceneColor.greaterThan(0.5), 0.48, 0.0);
752
- bodyColor.assign(mix(bodyColor, wetScene, swashOptics.mul(clearSceneWeight)));
753
- bodyColor.assign(mix(bodyColor, u.uShallowColor, swashOptics.mul(0.18)));
862
+ if (!flags.dynamics) {
863
+ bodyColor.assign(mix(bodyColor, wetScene, swashOptics.mul(clearSceneWeight)));
864
+ bodyColor.assign(mix(bodyColor, u.uShallowColor, swashOptics.mul(0.18)));
865
+ }
754
866
 
755
867
  // Steep wave faces act like windows, not lenses: drop caustics there,
756
868
  // and fade the dappling with view distance (WATER_QUALITY >= 1).
757
- const caustics = flags.caustics
869
+ const caustics = flags.caustics && !flags.dynamics
758
870
  ? color.caustics(groundWorld, columnDepth, u.uTime)
759
871
  .mul(smoothstep(0.62, 0.85, gerstnerNormalY))
760
872
  .mul(smoothstep(14.0, 38.0, viewDistance).oneMinus())
@@ -767,13 +879,19 @@ export function createWaterNodeMaterial({
767
879
  // staying glued to rest space while only the envelope moves.
768
880
  const foamXZ = vRestWorldXZ.sub(vSwashSurge).toVar();
769
881
  const pierce = color.pierceProximity(screenUv, waterViewDistance, vWorldPosition.y).toVar();
770
- const shoreFoam = foam.shoreFoam(columnDepth, viewDepthDiff, foamXZ, u.uTime, pierce)
882
+ const shoreFoam = (flags.dynamics ? float(0.0) : foam.shoreFoam(columnDepth, viewDepthDiff, foamXZ, u.uTime, pierce))
771
883
  // The legacy contact mask assumes a water body deeper than the foam
772
884
  // distance. On a centimetre-thin swash film it is almost 1 everywhere,
773
885
  // so keep its familiar offshore/rock character but let the dedicated
774
886
  // turbulent edge above own the beach foam.
775
887
  .mul(mix(1.0, 0.05, clamp(vSwashZone, 0.0, 1.0))).toVar();
776
- const whitecaps = foam.whitecaps(crest, gerstnerNormalY, foamXZ, u.uTime).toVar();
888
+ // Solver foam is an aeration concentration, not already opaque coverage.
889
+ // Map optical thickness to coverage before the toon threshold; passing
890
+ // a dilute swash tracer straight into it erased virtually all the lip.
891
+ const solvedFoamCoverage = exp(vDynamicsFoam.mul(-4.0)).oneMinus()
892
+ .mul(mix(1.0, u.uWhitecapAmount, u.uUseSpectralNormals))
893
+ .mul(mix(1.0, u.uSwashFoamAmount, swashOptics));
894
+ const whitecaps = (flags.dynamics ? solvedFoamCoverage : foam.whitecaps(crest, gerstnerNormalY, foamXZ, u.uTime)).toVar();
777
895
  const rippleFoamRaw = clamp(
778
896
  rippleState.b.mul(u.uRippleFoamStrength).add(abs(rippleState.r).mul(u.uRippleFoamStrength).mul(4.0)),
779
897
  0.0,
@@ -786,7 +904,7 @@ export function createWaterNodeMaterial({
786
904
  const persistentShoreCoverage = flags.shoaling
787
905
  ? shoreState.shoreStateCoverage(vRestWorldXZ)
788
906
  : float(0.0);
789
- if (flags.shoaling) {
907
+ if (flags.shoaling && !flags.dynamics) {
790
908
  // The temporal shore field owns transport, decay, breakup, and
791
909
  // event-to-event variation. Keep this main material deliberately
792
910
  // small: duplicating procedural noise here can exceed WebGPU's hard
@@ -835,7 +953,56 @@ export function createWaterNodeMaterial({
835
953
  // together with offshore/contact foam; never max an unshaped milky mask
836
954
  // over the final result.
837
955
  const foamRaw = max(baseFoamRaw, swashFoamValue).toVar();
838
- const foamValue = foam.foamShape(foamRaw, foamXZ, u.uTime).toVar();
956
+ let dynamicsFoam = foamRaw;
957
+ let bubbleShading = float(1.0);
958
+ if (flags.dynamics) {
959
+ // Transport texture coordinates with the water once per update.
960
+ // Multiplying the local velocity gradient by a long animation phase
961
+ // stretched the former breakup into sharp, shifting white ribbons.
962
+ const fieldUv=foamXZ.sub(u.uShoreStateRegion.xy).div(u.uShoreStateRegion.zw.mul(2.0)).add(0.5);
963
+ const boundedUv=clamp(fieldUv,vec2(0.0),vec2(1.0));
964
+ const outsideOffset=fieldUv.sub(boundedUv).mul(u.uShoreStateRegion.zw).mul(2.0);
965
+ const coordinates=u.uFoamCoordinates.sample(boundedUv).level(0).rg.add(outsideOffset);
966
+ const bubbles=u.uFoamBubbles.sample(coordinates.div(0.72));
967
+ const clusters=u.uFoamBubbles.sample(coordinates.mul(u.uFoamNoiseScale).mul(0.65)).b;
968
+ const coverage=smoothstep(0.32,0.68,clusters);
969
+ const thinFringe=smoothstep(0.012,0.06,vRestDepth).oneMinus();
970
+ const rafts=max(coverage,thinFringe.mul(0.5));
971
+ // Dense rafts expose packed bubble membranes. Dilute fringes break
972
+ // into individual bubbles, never an opaque cut-paper silhouette.
973
+ const packed=smoothstep(0.15,0.65,foamRaw.mul(rafts));
974
+ const pores=mix(bubbles.g,1.0,packed.mul(0.82));
975
+ const particleLocal=abs(foamXZ.sub(u.uFoamParticleRegion.xy)).div(u.uFoamParticleRegion.zw);
976
+ const particleBlend=smoothstep(vec2(.65),vec2(1.0),particleLocal).oneMinus();
977
+ const particleCoverage=particleBlend.x.mul(particleBlend.y).mul(u.uFoamParticleCoverage);
978
+ dynamicsFoam=clamp(foamRaw,0.0,1.0).mul(rafts).mul(pores).mul(mix(1.0,.28,particleCoverage));
979
+ // Coastal particles and the rim own the foam shape. The broad UV
980
+ // mask retained here for open-water whitecaps stretched into large
981
+ // flame-like ribbons on the thin beach sheet, even after advection.
982
+ const front=u.uFoamFrontState.sample(vec2(boundedUv.x,.5)).level(0);
983
+ // Thin cellular strands, not a thresholded broad mask. Cross-shore
984
+ // coordinates translate with local front flow without stretching
985
+ // every cell into a long solid ribbon as the old UV field did.
986
+ const laceUv=vec2(foamXZ.x,foamXZ.y.sub(front.a.mul(u.uUseFoamFrontState))).div(2.4).add(.5/512);
987
+ const lace=u.uFoamBubbles.sample(laceUv).a;
988
+ const trail=exp(max(vWetDistance.sub(.25),0).mul(-.65));
989
+ const laceFoam=clamp(foamRaw,0,1).mul(lace.mul(trail).mul(.65).add(.02)).mul(mix(.65,1,bubbles.g));
990
+ dynamicsFoam=mix(dynamicsFoam,laceFoam,u.uUseShoreContour);
991
+ // Connect the densely packed front rather than rendering individual
992
+ // rafts uniformly over the swash. Actual transported aeration still
993
+ // gates this rim: a calm, foam-free edge remains transparent.
994
+ const retreat=smoothstep(.02,.18,front.g.negate()).mul(u.uUseFoamFrontState);
995
+ const rimWidth=mix(.05,.13,clamp(front.b.mul(2),0,1)).mul(mix(1.0,.55,retreat));
996
+ const rim=smoothstep(.012,rimWidth,vWetDistance).oneMinus().mul(u.uUseShoreContour);
997
+ const rimAeration=max(vDynamicsFoam,front.b.mul(retreat));
998
+ const rimFoam=exp(rimAeration.mul(-12.0)).oneMinus()
999
+ .mul(clamp(u.uSwashFoamAmount,0.0,2.0)).mul(foamAmount)
1000
+ .mul(rim).mul(mix(.75,1.0,bubbles.g)).mul(mix(.32,.14,retreat));
1001
+ dynamicsFoam=max(dynamicsFoam,clamp(rimFoam,0.0,1.0));
1002
+ bubbleShading=bubbles.r;
1003
+ }
1004
+ const foamValue = (flags.dynamics ? dynamicsFoam
1005
+ : mix(foam.foamShape(foamRaw, foamXZ, u.uTime), foamRaw, u.uReferenceView)).toVar();
839
1006
 
840
1007
  // Drifting cloud shadows plus scene shadows (shared sun-shadow pass).
841
1008
  const proceduralCloudShadow = stylizedCloudShadow(
@@ -860,20 +1027,20 @@ export function createWaterNodeMaterial({
860
1027
  // Thin swash still catches the sky at its lip, but the interior should
861
1028
  // mostly reveal wet sand. Attenuate (rather than remove) its Fresnel,
862
1029
  // reflection and glints while keeping offshore water unchanged.
863
- const swashLight = mix(1.0, 0.36, swashOptics).toVar();
1030
+ const swashLight = (flags.dynamics ? float(1.0) : mix(1.0, 0.36, swashOptics.mul(u.uReferenceView.oneMinus()))).toVar();
864
1031
  const surfaceFresnel = fresnel.mul(swashLight).toVar();
865
1032
  const reflectionMix = clamp(
866
- surfaceFresnel.mul(u.uReflectionStrength), 0.0, 0.92,
1033
+ surfaceFresnel.mul(flags.dynamics ? 1.0 : mix(u.uReflectionStrength, 1.0, u.uReferenceView)), 0.0, 1.0,
867
1034
  ).toVar();
868
1035
  const litColor = mix(bodyColor, reflection, reflectionMix).toVar();
869
1036
  litColor.addAssign(
870
- u.uFresnelColor.mul(surfaceFresnel).mul(surfaceFresnel).mul(0.35),
1037
+ u.uFresnelColor.mul(surfaceFresnel).mul(surfaceFresnel).mul(flags.dynamics ? 0.0 : 0.35).mul(u.uReferenceView.oneMinus()),
871
1038
  );
872
1039
  litColor.addAssign(
873
1040
  lighting.specular(viewDir, surfaceNormal, foamValue.mul(0.75).oneMinus())
874
1041
  .mul(sunVisibility).mul(swashLight),
875
1042
  );
876
- if (flags.sparkles) {
1043
+ if (flags.sparkles && !flags.dynamics) {
877
1044
  litColor.addAssign(
878
1045
  lighting.sparkles(vRestWorldXZ, surfaceNormal, viewDir, viewDistance, u.uTime)
879
1046
  .mul(foamValue.oneMinus()).mul(sunVisibility).mul(swashLight),
@@ -897,6 +1064,7 @@ export function createWaterNodeMaterial({
897
1064
  u.uFoamColor.mul(foamLight),
898
1065
  smoothstep(0.5, 0.82, foamRaw),
899
1066
  ).toVar();
1067
+ foamTint.mulAssign(bubbleShading);
900
1068
  // Foam is sky-lit and remains readable in shade, but it cannot remain a
901
1069
  // pure-white post-lighting overlay. Consume the same shared cast-shadow
902
1070
  // visibility as the water body so its white line cools and dims while
@@ -954,7 +1122,7 @@ export function createWaterNodeMaterial({
954
1122
  [3, vec4(surfaceNormal.mul(0.5).add(0.5), 1.0)],
955
1123
  [4, vec4(rippleState.r.mul(2.0).add(0.5), rippleState.b, float(0.5).sub(rippleState.r.mul(2.0)), 1.0)],
956
1124
  [5, vec4(reflection, 1.0)],
957
- [6, vec4(caustics, 1.0)],
1125
+ [6, vec4(flags.dynamics ? projectedWaterCaustics(groundWorld, vec3(0.0,1.0,0.0)) : caustics, 1.0)],
958
1126
  [7, vec4(
959
1127
  lighting.specular(viewDir, surfaceNormal, float(1.0))
960
1128
  .add(lighting.sparkles(vRestWorldXZ, surfaceNormal, viewDir, viewDistance, u.uTime)),
@@ -343,6 +343,8 @@ function createGodRaysGroup(params = {}) {
343
343
  * volumes before the first frame can be rendered.
344
344
  */
345
345
  export class SkySystem {
346
+ /** Host cost policy. Disable only when no receivers need cloud shadows. */
347
+ cloudShadowsEnabled = true;
346
348
  #renderer;
347
349
  #camera;
348
350
  #scene;
@@ -711,6 +713,21 @@ export class SkySystem {
711
713
  return this.#cloudShadow.setLightDirection(direction);
712
714
  }
713
715
 
716
+ /**
717
+ * Opt-in opaque occlusion. Supply an unfiltered, linear color texture whose
718
+ * red channel is positive view-space distance in metres, cleared to >=65000.
719
+ * Render it with this camera BEFORE update(), excluding sky and transparent
720
+ * objects. The host owns the texture. Pass null for legacy backdrop depth.
721
+ */
722
+ setSceneDepthTexture(map = null) {
723
+ this.#assertLive();
724
+ if (map && this.#cloud.sceneDepthNode.value === map) return;
725
+ this.#cloud.setSceneDepthTexture(map);
726
+ this.#cloudLayer.material.depthTest = map === null;
727
+ this.#cloudLayer.material.needsUpdate = true;
728
+ this.#reprojection.reset();
729
+ }
730
+
714
731
  /** The sky dome. */
715
732
  get dome() {
716
733
  return this.#dome;
@@ -942,8 +959,8 @@ export class SkySystem {
942
959
  const enabled = this.#clouds.enabled === true;
943
960
  this.#post.cloudsEnabled.value = enabled ? 1 : 0;
944
961
  this.#cloudLayer.mesh.visible = enabled;
945
- this.#cloudShadow.enabled = enabled;
946
- if (enabled) {
962
+ this.#cloudShadow.enabled = enabled && this.cloudShadowsEnabled;
963
+ if (this.#cloudShadow.enabled) {
947
964
  // The shadow bake samples an explicit mip before the primary cloud pass
948
965
  // runs, so upload the authored 3D mip pyramid here as well as in the
949
966
  // reconstruction path. The helper is idempotent per renderer.
@@ -1221,6 +1238,7 @@ export class SkySystem {
1221
1238
  height,
1222
1239
  historyDiv: this.#quality.cloudHistoryDiv,
1223
1240
  shape: this.#clouds.shape,
1241
+ wind: this.#clouds.wind,
1224
1242
  width,
1225
1243
  });
1226
1244
  }
@@ -1246,11 +1264,9 @@ export class SkySystem {
1246
1264
  * `depthWrite: false` keeps the layer out of everyone else's depth test, and it
1247
1265
  * draws before host transparency, so glass and particles composite over it.
1248
1266
  *
1249
- * One consequence worth naming: the depth is the sphere's radius, not the
1250
- * cloud's own distance, so a mountain 60 km out has cloud drawn over it even
1251
- * where the cloud it hides is nearer. The marcher already computes a
1252
- * transmittance-weighted mean cloud distance; writing that as the layer's depth
1253
- * is the fix, and it needs the reconstruction to carry the depth through.
1267
+ * Without a scene-depth input, occlusion is still based on the carrier.
1268
+ * setSceneDepthTexture opts into clipped integration and disables this
1269
+ * carrier depth test so cloud in front of opaque geometry can composite.
1254
1270
  *
1255
1271
  * Premultiplied over, not alpha blending: the marcher writes
1256
1272
  * `vec4(scatteredRadiance, viewTransmittance)`, and the composite the cloud
@@ -1430,6 +1446,13 @@ export class SkySystem {
1430
1446
  max(atmosphere.fogFarFadeEnd, atmosphere.fogFarFadeStart.add(1)),
1431
1447
  distance,
1432
1448
  ).toVar();
1449
+ // The cloud march already applies aerial perspective at cloud depth.
1450
+ // Fog only the opaque contribution behind it, preserving its radiance.
1451
+ If(this.#cloud.uniforms.sceneDepthEnabled.greaterThan(0.5)
1452
+ .and(this.#post.cloudsEnabled.greaterThan(0.5)), () => {
1453
+ const cloud = this.#cloudTextureNode.sample(screenUV);
1454
+ skyTarget.assign(skyTarget.mul(cloud.a).add(cloud.rgb));
1455
+ });
1433
1456
  rgb.assign(mix(mix(skyTarget, rgb, transmittance), skyTarget, farFade));
1434
1457
  });
1435
1458
 
@@ -52,7 +52,7 @@ export function createPerspectiveRemovalSettings(options = null) {
52
52
 
53
53
  return {
54
54
  amount: enabled ? requestedAmount : 0,
55
- enabled: enabled && requestedAmount > 0,
55
+ enabled,
56
56
  endHeight: numberOption(firstDefined(source, ['endHeight']), DEFAULT_PERSPECTIVE_REMOVAL_SETTINGS.endHeight, { min: -100, max: 1000 }),
57
57
  radius: numberOption(firstDefined(source, ['radius']), DEFAULT_PERSPECTIVE_REMOVAL_SETTINGS.radius, { min: 0.05, max: 50 }),
58
58
  startHeight: numberOption(firstDefined(source, ['startHeight']), DEFAULT_PERSPECTIVE_REMOVAL_SETTINGS.startHeight, { min: -100, max: 1000 }),