@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.
- package/NPM-LIBRARY.md +87 -27
- package/README.md +11 -10
- package/agents/PROMPTS.md +17 -3
- package/agents/README.md +13 -3
- package/agents/claude/CLAUDE.md +3 -0
- package/agents/codex/AGENTS.md +112 -159
- package/agents/cursor/toonlab.mdc +7 -3
- package/agents/examples/game-foundation.mjs +104 -0
- package/agents/references/game-lifecycle.md +57 -0
- package/agents/references/runtime-entry-points.md +26 -0
- package/agents/references/style-bundles.md +4 -1
- package/agents/skills/claude/asset-sourcing/SKILL.md +7 -4
- package/agents/skills/claude/environment/SKILL.md +4 -0
- package/agents/skills/claude/game-dev/SKILL.md +63 -87
- package/agents/skills/claude/lighting/SKILL.md +36 -0
- package/agents/skills/claude/post-processing/SKILL.md +8 -3
- package/agents/skills/claude/rock-ground-shaders/SKILL.md +10 -15
- package/agents/skills/claude/rockgen/SKILL.md +52 -21
- package/agents/skills/claude/scene-style-application/SKILL.md +4 -0
- package/agents/skills/claude/style-presets/SKILL.md +11 -8
- package/agents/skills/claude/toon-shading/SKILL.md +4 -0
- package/agents/skills/claude/vegetation-sky/SKILL.md +13 -7
- package/agents/skills/claude/visual-verification/SKILL.md +45 -119
- package/agents/skills/claude/water/SKILL.md +6 -2
- package/agents/skills/codex/asset-sourcing/SKILL.md +7 -4
- package/agents/skills/codex/environment/SKILL.md +4 -0
- package/agents/skills/codex/game-dev/SKILL.md +63 -87
- package/agents/skills/codex/lighting/SKILL.md +36 -0
- package/agents/skills/codex/post-processing/SKILL.md +8 -3
- package/agents/skills/codex/rock-ground-shaders/SKILL.md +10 -15
- package/agents/skills/codex/rockgen/SKILL.md +52 -21
- package/agents/skills/codex/scene-style-application/SKILL.md +4 -0
- package/agents/skills/codex/style-presets/SKILL.md +11 -8
- package/agents/skills/codex/toon-shading/SKILL.md +4 -0
- package/agents/skills/codex/vegetation-sky/SKILL.md +13 -7
- package/agents/skills/codex/visual-verification/SKILL.md +45 -119
- package/agents/skills/codex/water/SKILL.md +6 -2
- package/database/apply-one-catalog-seed.mjs +63 -0
- package/database/providers.mjs +1 -0
- package/database/seeds/catalog/0005_2026-08-c8-first12.sql +1101 -0
- package/database/seeds/catalog/0006_2026-09-c8-first100.sql +14245 -0
- package/database/seeds/catalog/0007_2026-09-c8-first100-primary-model.sql +14845 -0
- package/mcp/lab-management.mjs +11 -8
- package/mcp/public-catalog.mjs +0 -16
- package/mcp/tree-lab-contract.mjs +2381 -0
- package/mcp/vite-plugin.mjs +5 -1
- package/package.json +29 -18
- package/src/catalog/officialCatalog.js +1 -0
- package/src/catalog/officialCatalogAssetRuntime.js +98 -1
- package/src/catalog/officialCatalogLod.js +160 -16
- package/src/catalog/officialCatalogPlacement.js +52 -12
- package/src/catalog/officialCatalogProvider.js +79 -8
- package/src/catalog/officialCatalogRockPackage.js +172 -0
- package/src/cloud/cloudReprojection.js +40 -7
- package/src/cloud/cloudVolume.js +44 -7
- package/src/cloud/noise/baseShapeVolume.js +8 -6
- package/src/cloud/noise/cirrusMap.js +2 -1
- package/src/cloud/noise/curlNoise.js +2 -1
- package/src/cloud/noise/erosionVolume.js +2 -1
- package/src/cloud/noise/textureCache.js +77 -0
- package/src/cloud/noise/weatherMap.js +2 -1
- package/src/core/sha256.js +83 -0
- package/src/ground-shader/groundShaderMaterial.js +12 -3
- package/src/rock-shader/rockMaterial.js +205 -28
- package/src/rock-shader/rockShaderRuntime.js +17 -1
- package/src/rock-shader/rockShaderSettings.js +16 -0
- package/src/rockgen/index.js +9 -0
- package/src/rockgen/lod/index.js +1 -0
- package/src/rockgen/lod/rockDenseFieldPolicy.js +143 -0
- package/src/rockgen/rockDocument.js +899 -35
- package/src/rockgen/surface/c7GeologySurface.js +39 -9
- package/src/rockgen/surface/naturalRockSurface.js +1948 -0
- package/src/shaders-tsl/chunks/projected-water-caustics.js +14 -2
- package/src/shaders-tsl/chunks/water-foam.js +6 -6
- package/src/shaders-tsl/chunks/water-lighting.js +28 -5
- package/src/shaders-tsl/chunks/water-shore-state.js +5 -2
- package/src/shaders-tsl/chunks/water-waves.js +10 -5
- package/src/shaders-tsl/flower.js +7 -4
- package/src/shaders-tsl/grass.js +4 -3
- package/src/shaders-tsl/water-shore-state-simulation.js +3 -2
- package/src/shaders-tsl/water.js +209 -41
- package/src/sky/skySystem.js +30 -7
- package/src/toon/settings/perspectiveRemovalSettings.js +1 -1
- package/src/toon/toonSettings.js +74 -0
- package/src/vegetation/stylizedFlower.js +6 -0
- package/src/version.js +1 -1
- package/src/water/waterDetailSpectrum.js +91 -0
- package/src/water/waterDynamics.js +422 -0
- package/src/water/waterFoamParticles.js +196 -0
- package/src/water/waterFoamTexture.js +57 -0
- package/src/water/waterHydrodynamics.js +209 -0
- package/src/water/waterMaterial.js +1 -0
- package/src/water/waterRenderExtension.js +107 -0
- package/src/water/waterScenePasses.js +14 -6
- package/src/water/waterSettings.js +53 -26
- package/src/water/waterShoreMaterial.js +38 -14
- package/src/water/waterSpectralOcean.js +137 -0
- package/src/water/waterStageSettings.js +19 -1
- package/src/water/waterSurface.js +62 -18
- package/src/water/waterUnderwaterAtmosphere.js +30 -3
- package/types/catalog/officialCatalog.d.ts +1 -0
- package/types/catalog/officialCatalogPlacement.d.ts +4 -0
- package/types/catalog/officialCatalogRockPackage.d.ts +68 -0
- package/types/cloud/noise/baseShapeVolume.d.ts +1 -1
- package/types/cloud/noise/textureCache.d.ts +12 -0
- package/types/core/sha256.d.ts +2 -0
- package/types/index.d.ts +30 -0
- package/types/rock-shader/rockMaterial.d.ts +3 -0
- package/types/rock-shader/rockShaderSettings.d.ts +2 -0
- package/types/rockgen/index.d.ts +1 -0
- package/types/rockgen/lod/index.d.ts +1 -0
- package/types/rockgen/lod/rockDenseFieldPolicy.d.ts +181 -0
- package/types/rockgen/rockDocument.d.ts +654 -71
- package/types/rockgen/surface/naturalRockSurface.d.ts +100 -0
- package/types/shaders-tsl/chunks/projected-water-caustics.d.ts +7 -1
- package/types/shaders-tsl/chunks/water-lighting.d.ts +4 -3
- package/types/shaders-tsl/water.d.ts +2 -1
- package/types/version.d.ts +1 -1
- package/types/water/waterDetailSpectrum.d.ts +13 -0
- package/types/water/waterDynamics.d.ts +89 -0
- package/types/water/waterFoamParticles.d.ts +32 -0
- package/types/water/waterFoamTexture.d.ts +1 -0
- package/types/water/waterHydrodynamics.d.ts +49 -0
- package/types/water/waterRenderExtension.d.ts +16 -0
- package/types/water/waterScenePasses.d.ts +1 -1
- package/types/water/waterSettings.d.ts +1 -0
- package/types/water/waterSpectralOcean.d.ts +17 -0
- package/types/water/waterStageSettings.d.ts +1 -1
- package/types/water/waterUnderwaterAtmosphere.d.ts +19 -4
|
@@ -340,9 +340,9 @@ const WATER_PRESETS = Object.freeze({
|
|
|
340
340
|
whitecapAmount: 0.08,
|
|
341
341
|
}),
|
|
342
342
|
coast: Object.freeze({
|
|
343
|
-
waveIntensity: 0.
|
|
344
|
-
waveAmplitude: 0.
|
|
345
|
-
waveLength:
|
|
343
|
+
waveIntensity: 0.6,
|
|
344
|
+
waveAmplitude: 0.65,
|
|
345
|
+
waveLength: 18.0,
|
|
346
346
|
// Onshore swell: surf only exists when waves travel toward the shallows.
|
|
347
347
|
waveDirection: [0.15, -1.0],
|
|
348
348
|
waveDirectionSpread: 0.5,
|
|
@@ -603,8 +603,9 @@ export function createWaterSettings(options = {}) {
|
|
|
603
603
|
// `detailNormalStrength` or `reflectionStrength` alongside `colorTone:
|
|
604
604
|
// 'anime'` was silently inert, which is exactly what §6.4 of the launch spec
|
|
605
605
|
// asks for and could not get (D19-005). Callers that pass nothing are
|
|
606
|
-
//
|
|
607
|
-
const
|
|
606
|
+
// resolved from the authored style recipe, then the tone recommendation.
|
|
607
|
+
const styleRecipe = styleEntry?.presets?.[preset] ?? styleEntry?.scenarios?.[preset] ?? {};
|
|
608
|
+
const toneScalar = (key) => source[key] ?? styleRecipe[key] ?? tone[key];
|
|
608
609
|
|
|
609
610
|
const quality = WATER_QUALITY_LEVELS.includes(String(source.quality).toLowerCase())
|
|
610
611
|
? String(source.quality).toLowerCase()
|
|
@@ -806,15 +807,6 @@ export function buildGerstnerWaves(settings) {
|
|
|
806
807
|
});
|
|
807
808
|
}
|
|
808
809
|
|
|
809
|
-
// Gerstner surfaces self-intersect when the summed steepness exceeds 1.
|
|
810
|
-
// Normalize so the whole stack stays stable at any dial position.
|
|
811
|
-
const steepnessSum = waves.reduce(
|
|
812
|
-
(sum, wave) => sum + wave.steepness * wave.waveNumber * wave.amplitude, 0);
|
|
813
|
-
if (steepnessSum > 0.9) {
|
|
814
|
-
const scale = 0.9 / steepnessSum;
|
|
815
|
-
for (const wave of waves) wave.steepness *= scale;
|
|
816
|
-
}
|
|
817
|
-
|
|
818
810
|
// Wave sets: real swell arrives in groups — the big waves come minutes
|
|
819
811
|
// apart, not every period. Splitting the primary component into a beat
|
|
820
812
|
// pair (slots 0 and 1: same direction, slightly different frequency) makes
|
|
@@ -826,19 +818,43 @@ export function buildGerstnerWaves(settings) {
|
|
|
826
818
|
const setStrength = resolved.waveSetStrength;
|
|
827
819
|
if (setStrength > 0.001 && waves.length >= 2) {
|
|
828
820
|
const primary = waves[0];
|
|
829
|
-
|
|
830
|
-
|
|
821
|
+
// Detune in intrinsic frequency, then apply the common time scale.
|
|
822
|
+
// The old fixed omega offset kept the beat moving at speed=0 and made
|
|
823
|
+
// k explode as speed approached zero. A group requires nearby frequencies.
|
|
824
|
+
const intrinsicOmega = Math.sqrt(9.81 * primary.waveNumber);
|
|
825
|
+
const speedScale = resolved.waveSpeed;
|
|
826
|
+
const detuning = Math.min(
|
|
827
|
+
(2 * Math.PI) / (Math.max(resolved.waveSetPeriod, 1) * Math.max(speedScale, 1e-3)),
|
|
828
|
+
intrinsicOmega * 0.25,
|
|
829
|
+
);
|
|
830
|
+
const beatIntrinsicOmega = intrinsicOmega + detuning;
|
|
831
|
+
const beatOmega = beatIntrinsicOmega * speedScale;
|
|
831
832
|
const beat = {
|
|
832
833
|
...primary,
|
|
833
834
|
amplitude: primary.amplitude * setStrength * 0.5,
|
|
834
835
|
omega: beatOmega,
|
|
835
|
-
waveNumber:
|
|
836
|
+
waveNumber: beatIntrinsicOmega ** 2 / 9.81,
|
|
836
837
|
phase: primary.phase + 2.39,
|
|
837
838
|
};
|
|
838
839
|
primary.amplitude *= 1 - setStrength * 0.5;
|
|
839
840
|
waves.pop(); // drop the smallest tail component to keep the count fixed
|
|
840
841
|
waves.splice(1, 0, beat);
|
|
841
842
|
}
|
|
843
|
+
// Budget the FINAL spectrum, including its beat partner. Per-component
|
|
844
|
+
// ka limits alone allow eight individually legal waves to form spike-like
|
|
845
|
+
// sums. Stretch the spectrum together, preserving height and relative
|
|
846
|
+
// wavelengths, with a conservative aggregate slope budget (not a breaking
|
|
847
|
+
// law). Recompute frequencies through gravity-wave dispersion.
|
|
848
|
+
const slopeSum = waves.reduce((sum, wave) => sum + wave.waveNumber * wave.amplitude, 0);
|
|
849
|
+
const stretch = Math.max(1, slopeSum / 0.8);
|
|
850
|
+
for (const wave of waves) {
|
|
851
|
+
wave.waveNumber /= stretch;
|
|
852
|
+
wave.omega = Math.sqrt(9.81 * wave.waveNumber) * resolved.waveSpeed;
|
|
853
|
+
}
|
|
854
|
+
const steepnessSum = waves.reduce(
|
|
855
|
+
(sum, wave) => sum + wave.steepness * wave.waveNumber * wave.amplitude, 0);
|
|
856
|
+
const chopScale = Math.min(1, 0.85 / Math.max(steepnessSum, 1e-8));
|
|
857
|
+
for (const wave of waves) wave.steepness *= chopScale;
|
|
842
858
|
return waves;
|
|
843
859
|
}
|
|
844
860
|
|
|
@@ -1000,6 +1016,15 @@ export function sampleSwashCycleVariation(cycleIndex) {
|
|
|
1000
1016
|
};
|
|
1001
1017
|
}
|
|
1002
1018
|
|
|
1019
|
+
// Numerical extinction near zero forcing. This does not turn the authored
|
|
1020
|
+
// run-up animation into a hydrodynamic solver; it prevents a dry/flat spectrum
|
|
1021
|
+
// from driving a full-distance swash event. Fade over the last 5 cm of summed
|
|
1022
|
+
// amplitude so the limit is continuous instead of an on/off snap.
|
|
1023
|
+
function swashActivity(waves) {
|
|
1024
|
+
const amplitude = (waves ?? []).reduce((sum, wave) => sum + Math.abs(wave.amplitude), 0);
|
|
1025
|
+
return THREE.MathUtils.smoothstep(amplitude, 0, 0.05);
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1003
1028
|
// Continuous centerline position of the swash edge in metres along the beach.
|
|
1004
1029
|
// Event N begins exactly at event N-1's rundown endpoint, rises to its own
|
|
1005
1030
|
// varying inland maximum, then drains to a new endpoint without a reset jump.
|
|
@@ -1009,9 +1034,9 @@ export function sampleSwashDistance(waves, time, runupDistance, uprushFraction =
|
|
|
1009
1034
|
const previous = sampleSwashCycleVariation(sequence.index - 1);
|
|
1010
1035
|
const progress = shapeSwashProgress(sequence.cycle, uprushFraction);
|
|
1011
1036
|
const peak = Math.max(Number(runupDistance) || 0, 0) * current.runupScale;
|
|
1012
|
-
return sequence.cycle <= uprushFraction
|
|
1037
|
+
return swashActivity(waves) * (sequence.cycle <= uprushFraction
|
|
1013
1038
|
? THREE.MathUtils.lerp(previous.rundownOffset, peak, progress)
|
|
1014
|
-
: THREE.MathUtils.lerp(current.rundownOffset, peak, progress);
|
|
1039
|
+
: THREE.MathUtils.lerp(current.rundownOffset, peak, progress));
|
|
1015
1040
|
}
|
|
1016
1041
|
|
|
1017
1042
|
// One CPU-authored frame shared by the visible swash and the persistent
|
|
@@ -1036,6 +1061,7 @@ export function sampleSwashFrameState(
|
|
|
1036
1061
|
shapeSwashProgress(afterSequence.cycle, uprushFraction) -
|
|
1037
1062
|
shapeSwashProgress(beforeSequence.cycle, uprushFraction)
|
|
1038
1063
|
) / (derivativeStep * 2);
|
|
1064
|
+
const activity = swashActivity(waves);
|
|
1039
1065
|
const maximumDistance = Math.max(Number(runupDistance) || 0, 0);
|
|
1040
1066
|
const edgeDistance = maximumDistance > 0
|
|
1041
1067
|
? sampleSwashDistance(waves, time, maximumDistance, uprushFraction)
|
|
@@ -1057,9 +1083,10 @@ export function sampleSwashFrameState(
|
|
|
1057
1083
|
primaryDirectionZ: waves?.[0]?.dirZ ?? -1,
|
|
1058
1084
|
progress,
|
|
1059
1085
|
progressSpeed,
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1086
|
+
activity,
|
|
1087
|
+
runupScale: current.runupScale * activity,
|
|
1088
|
+
startOffset: previous.rundownOffset * activity,
|
|
1089
|
+
endOffset: current.rundownOffset * activity,
|
|
1063
1090
|
edgeShape,
|
|
1064
1091
|
};
|
|
1065
1092
|
}
|
|
@@ -1125,7 +1152,7 @@ export const WATER_SETTING_GROUPS = Object.freeze([
|
|
|
1125
1152
|
const FIELD_METADATA = {
|
|
1126
1153
|
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.' },
|
|
1127
1154
|
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.' },
|
|
1128
|
-
waveAmplitude: { group: 'waves', label: 'Wave Amplitude', min: 0, max: 5, step: 0.01, description: '
|
|
1155
|
+
waveAmplitude: { group: 'waves', label: 'Wave Amplitude', min: 0, max: 5, step: 0.01, description: 'Primary amplitude in meters at full intensity, before splitting into a wave set. The combined surface includes smaller waves; its wavelengths lengthen to keep slopes bounded.' },
|
|
1129
1156
|
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).' },
|
|
1130
1157
|
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.' },
|
|
1131
1158
|
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.' },
|
|
@@ -1140,7 +1167,7 @@ const FIELD_METADATA = {
|
|
|
1140
1167
|
waveSpeed: { group: 'waves', label: 'Wave Speed', min: 0, max: 4, step: 0.01, description: 'Phase speed multiplier over the deep-water dispersion.' },
|
|
1141
1168
|
waveDirection: { group: 'waves', label: 'Wave Direction', type: 'vector2', description: 'Main travel direction of the swell in the XZ plane.' },
|
|
1142
1169
|
waveDirectionSpread: { group: 'waves', label: 'Direction Spread', min: 0, max: 1, step: 0.01, description: '0 keeps all waves aligned (river); 1 spreads them omnidirectionally (open sea). The primary swell always follows Wave Direction exactly.' },
|
|
1143
|
-
waveSetPeriod: { group: 'waves', label: 'Set Period (s)', min: 8, max: 600, step: 1, description: '
|
|
1170
|
+
waveSetPeriod: { group: 'waves', label: 'Set Period (s)', min: 8, max: 600, step: 1, description: 'Requested minimum interval between wave-set peaks. Stability limits and slow motion can lengthen it.' },
|
|
1144
1171
|
waveSetStrength: { group: 'waves', label: 'Set Strength', min: 0, max: 1, step: 0.01, description: 'Depth of the set/lull cycle: 0 = constant swell, 1 = the swell dies completely between sets.' },
|
|
1145
1172
|
detailNormalStrength: { group: 'waves', label: 'Detail Normals', min: 0, max: 2, step: 0.01, description: 'Strength of the procedural micro-ripple normal detail.' },
|
|
1146
1173
|
detailScale: { group: 'waves', label: 'Detail Scale', min: 0.05, max: 8, step: 0.05, description: 'Spatial frequency of the micro-ripple detail.' },
|
|
@@ -1172,7 +1199,7 @@ const FIELD_METADATA = {
|
|
|
1172
1199
|
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.' },
|
|
1173
1200
|
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.' },
|
|
1174
1201
|
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.' },
|
|
1175
|
-
causticsStrength: { group: 'surface', label: 'Caustics', min: 0, max: 3, step: 0.01, description: 'Brightness of the
|
|
1202
|
+
causticsStrength: { group: 'surface', label: 'Caustics', min: 0, max: 3, step: 0.01, description: 'Brightness of focused sunlight on the submerged bed. The dynamics path derives focusing from the water normals.' },
|
|
1176
1203
|
causticsScale: { group: 'surface', label: 'Caustics Scale', min: 0.05, max: 8, step: 0.05, description: 'Spatial frequency of the caustic web.' },
|
|
1177
1204
|
causticsSpeed: { group: 'surface', label: 'Caustics Speed', min: 0, max: 4, step: 0.01, description: 'Animation speed of the caustic web.' },
|
|
1178
1205
|
|
|
@@ -1654,7 +1681,7 @@ registerWaterStyle('call_me_sensei', {
|
|
|
1654
1681
|
presets: {
|
|
1655
1682
|
lake: { flowSpeed: 0.4, sparkleStrength: 0.55, waveIntensity: 0.32 },
|
|
1656
1683
|
river: { flowSpeed: 1.15, foamLineSpacing: 0.38, sparkleStrength: 0.45, waveIntensity: 0.3 },
|
|
1657
|
-
coast: { flowSpeed: 0.45, swashFoamAmount: 1.4, sparkleStrength: 0
|
|
1684
|
+
coast: { flowSpeed: 0.45, swashFoamAmount: 1.4, causticsStrength: 1.25, sparkleStrength: 0, waveIntensity: 0.6 },
|
|
1658
1685
|
ocean: { flowSpeed: 0.5, sparkleStrength: 0.7, waveIntensity: 0.7, whitecapAmount: 0.45 },
|
|
1659
1686
|
},
|
|
1660
1687
|
});
|
|
@@ -2,6 +2,7 @@ import * as THREE from 'three';
|
|
|
2
2
|
|
|
3
3
|
import { MeshPhysicalNodeMaterial } from 'three/webgpu';
|
|
4
4
|
import {
|
|
5
|
+
abs,
|
|
5
6
|
clamp,
|
|
6
7
|
float,
|
|
7
8
|
luminance,
|
|
@@ -21,6 +22,7 @@ import {
|
|
|
21
22
|
} from 'three/tsl';
|
|
22
23
|
|
|
23
24
|
import { projectedWaterCaustics } from '../shaders-tsl/chunks/projected-water-caustics.js';
|
|
25
|
+
import { getWaterFoamTexture } from './waterFoamTexture.js';
|
|
24
26
|
import { sampleEnvironmentSunShadow } from '../shaders-tsl/chunks/environment-sun-shadow.js';
|
|
25
27
|
import { sampleEnvironmentCloudShadow } from '../sky/cloudShadow.js';
|
|
26
28
|
|
|
@@ -145,6 +147,11 @@ export function updateWaterShoreMaterial(material, {
|
|
|
145
147
|
if (stateField !== undefined) material.userData.waterShoreStateField = stateField;
|
|
146
148
|
const field = material.userData.waterShoreStateField ?? null;
|
|
147
149
|
uniforms.uShoreStateMap.value = resolveStateTexture(field);
|
|
150
|
+
uniforms.uShoreFoamCoordinates.value = field?.foamCoordinateTexture ?? getFallbackStateTexture();
|
|
151
|
+
if (uniforms.uShoreSolvedDepth) uniforms.uShoreSolvedDepth.value = field?.isWaterDynamics ? 1 : 0;
|
|
152
|
+
if (uniforms.uShoreContinuation) uniforms.uShoreContinuation.value.set(
|
|
153
|
+
field?.level ?? 0, field?.options?.renderExtent && field?.mode === 'coast' ? 1 : 0,
|
|
154
|
+
);
|
|
148
155
|
resolveStateRegion(field, regionScratch);
|
|
149
156
|
uniforms.uShoreStateRegion.value.copy(regionScratch);
|
|
150
157
|
|
|
@@ -184,6 +191,10 @@ export function createWaterShoreMaterial({
|
|
|
184
191
|
} = {}) {
|
|
185
192
|
const uniforms = {
|
|
186
193
|
uShoreStateMap: texture(getFallbackStateTexture()),
|
|
194
|
+
uShoreFoamCoordinates: texture(getFallbackStateTexture()),
|
|
195
|
+
uShoreFoamBubbles: texture(getWaterFoamTexture()),
|
|
196
|
+
uShoreSolvedDepth: uniform(0),
|
|
197
|
+
uShoreContinuation: uniform(new THREE.Vector2(0,0)),
|
|
187
198
|
// (centerX, centerZ, halfWidth, halfDepth), matching the ripple-region
|
|
188
199
|
// convention used by the rest of the water system.
|
|
189
200
|
uShoreStateRegion: uniform(new THREE.Vector4(0, 0, 0.5, 0.5)),
|
|
@@ -198,17 +209,31 @@ export function createWaterShoreMaterial({
|
|
|
198
209
|
.sub(uniforms.uShoreStateRegion.xy)
|
|
199
210
|
.div(max(uniforms.uShoreStateRegion.zw.mul(2.0), vec2(1e-3)))
|
|
200
211
|
.add(0.5);
|
|
201
|
-
const
|
|
212
|
+
const insideX = step(0.0, fieldUv.x).mul(step(fieldUv.x, 1.0));
|
|
213
|
+
const inside = mix(insideX,1.0,uniforms.uShoreContinuation.y)
|
|
202
214
|
.mul(step(0.0, fieldUv.y)).mul(step(fieldUv.y, 1.0));
|
|
203
215
|
// State render targets have no mip chain. Explicit level zero is required
|
|
204
216
|
// by WGSL in non-uniform control flow and is harmless on the GLSL builder.
|
|
205
|
-
const
|
|
217
|
+
const rawState = uniforms.uShoreStateMap
|
|
206
218
|
.sample(clamp(fieldUv, vec2(0.0), vec2(1.0)))
|
|
207
|
-
.level(0)
|
|
208
|
-
|
|
219
|
+
.level(0);
|
|
220
|
+
const state = rawState.mul(inside);
|
|
221
|
+
const boundedUv=clamp(fieldUv,vec2(0),vec2(1));
|
|
222
|
+
const foamCoordinates=uniforms.uShoreFoamCoordinates.sample(boundedUv).level(0).rg
|
|
223
|
+
.add(fieldUv.sub(boundedUv).mul(uniforms.uShoreStateRegion.zw).mul(2));
|
|
224
|
+
const foamBubbles=uniforms.uShoreFoamBubbles.sample(foamCoordinates.div(.72));
|
|
225
|
+
|
|
226
|
+
// Submerged continuation cannot suddenly become dry sand at the atlas
|
|
227
|
+
// boundary. Carry boundary moisture outward, tending to rest submersion.
|
|
228
|
+
const outside = max(abs(positionWorld.xz.sub(uniforms.uShoreStateRegion.xy))
|
|
229
|
+
.sub(uniforms.uShoreStateRegion.zw),vec2(0.0));
|
|
230
|
+
const nearBoundary = smoothstep(0.0,80.0,max(outside.x,outside.y)).oneMinus();
|
|
231
|
+
const restWet = smoothstep(0.002,0.04,uniforms.uShoreContinuation.x.sub(positionWorld.y));
|
|
232
|
+
const continuationWet = max(restWet,rawState.r.mul(nearBoundary))
|
|
233
|
+
.mul(inside.oneMinus()).mul(uniforms.uShoreContinuation.y);
|
|
209
234
|
|
|
210
|
-
const moisture = clamp(state.r, 0.0, 1.0);
|
|
211
|
-
const film = clamp(state.g, 0.0, 1.0);
|
|
235
|
+
const moisture = clamp(max(state.r,continuationWet), 0.0, 1.0);
|
|
236
|
+
const film = clamp(max(mix(state.g, state.g.div(0.04), uniforms.uShoreSolvedDepth),continuationWet), 0.0, 1.0);
|
|
212
237
|
const foamAmountNode = clamp(uniforms.uShoreFoamAmount, 0.0, 2.0);
|
|
213
238
|
const activeFoam = clamp(state.b, 0.0, 1.0);
|
|
214
239
|
const residue = clamp(state.a, 0.0, 1.0);
|
|
@@ -270,7 +295,7 @@ export function createWaterShoreMaterial({
|
|
|
270
295
|
visibleActiveFoam.mul(0.72).add(visibleResidue.mul(0.12)).mul(foamAmountNode),
|
|
271
296
|
0.0,
|
|
272
297
|
0.74,
|
|
273
|
-
);
|
|
298
|
+
).mul(mix(1.0, smoothstep(0.0001, 0.0015, state.g).oneMinus().mul(foamBubbles.g.mul(.7).add(.3)), uniforms.uShoreSolvedDepth));
|
|
274
299
|
const receiverVisibility = sampleEnvironmentSunShadow(positionWorld)
|
|
275
300
|
.mul(sampleEnvironmentCloudShadow(positionWorld, 1))
|
|
276
301
|
.toVar();
|
|
@@ -279,7 +304,7 @@ export function createWaterShoreMaterial({
|
|
|
279
304
|
vec3(1.0),
|
|
280
305
|
receiverVisibility,
|
|
281
306
|
);
|
|
282
|
-
const shadowedFoamColor = uniforms.uShoreFoamColor.mul(shadowTint);
|
|
307
|
+
const shadowedFoamColor = uniforms.uShoreFoamColor.mul(shadowTint).mul(mix(1,foamBubbles.r,uniforms.uShoreSolvedDepth));
|
|
283
308
|
const shoreColor = mix(wetBaseColor, shadowedFoamColor, foamCoverage)
|
|
284
309
|
.mul(shadowTint);
|
|
285
310
|
|
|
@@ -301,12 +326,11 @@ export function createWaterShoreMaterial({
|
|
|
301
326
|
|
|
302
327
|
const material = new MeshPhysicalNodeMaterial({ vertexColors: false });
|
|
303
328
|
material.name = 'WaterShoreMaterial';
|
|
304
|
-
|
|
305
|
-
//
|
|
306
|
-
//
|
|
307
|
-
//
|
|
308
|
-
|
|
309
|
-
material.colorNode = shoreColor.add(
|
|
329
|
+
material.colorNode = shoreColor;
|
|
330
|
+
// Focused sunlight is extra incident illumination, not a brighter sand
|
|
331
|
+
// pigment. Keeping it out of albedo preserves its contrast under tone
|
|
332
|
+
// mapping and avoids multiplying it by the ordinary sun a second time.
|
|
333
|
+
material.emissiveNode = shoreColor.mul(
|
|
310
334
|
projectedWaterCaustics(positionWorld, normalWorld).mul(receiverVisibility),
|
|
311
335
|
);
|
|
312
336
|
material.metalnessNode = float(0.0);
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
// Independent spectral ocean implementation: seeded Gaussian amplitudes,
|
|
2
|
+
// JONSWAP-shaped directional frequency density and finite-depth dispersion.
|
|
3
|
+
// Three non-overlapping wave-number bands. FFT evaluates the linear wave
|
|
4
|
+
// solution; it is not a nonlinear overturning/breaking solver.
|
|
5
|
+
const TAU = 2 * Math.PI;
|
|
6
|
+
const G = 9.81;
|
|
7
|
+
function random(seed) { let s = seed >>> 0; return () => { s = (Math.imul(1664525, s) + 1013904223) >>> 0; return (s + 0.5) / 4294967296; }; }
|
|
8
|
+
export function inverseFFT2D(real, imaginary, size) {
|
|
9
|
+
const line = (offset, stride) => {
|
|
10
|
+
for (let i = 1, j = 0; i < size; i++) {
|
|
11
|
+
let bit = size >> 1;
|
|
12
|
+
for (; j & bit; bit >>= 1)
|
|
13
|
+
j ^= bit;
|
|
14
|
+
j ^= bit;
|
|
15
|
+
if (i < j) {
|
|
16
|
+
const a = offset + i * stride, b = offset + j * stride;
|
|
17
|
+
let temp = real[a];
|
|
18
|
+
real[a] = real[b];
|
|
19
|
+
real[b] = temp;
|
|
20
|
+
temp = imaginary[a];
|
|
21
|
+
imaginary[a] = imaginary[b];
|
|
22
|
+
imaginary[b] = temp;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
for (let len = 2; len <= size; len <<= 1) {
|
|
26
|
+
const wr0 = Math.cos(TAU / len), wi0 = Math.sin(TAU / len);
|
|
27
|
+
for (let start = 0; start < size; start += len) {
|
|
28
|
+
let wr = 1, wi = 0;
|
|
29
|
+
for (let j = 0; j < len / 2; j++) {
|
|
30
|
+
const a = offset + (start + j) * stride, b = offset + (start + j + len / 2) * stride;
|
|
31
|
+
const tr = real[b] * wr - imaginary[b] * wi, ti = real[b] * wi + imaginary[b] * wr;
|
|
32
|
+
real[b] = real[a] - tr;
|
|
33
|
+
imaginary[b] = imaginary[a] - ti;
|
|
34
|
+
real[a] += tr;
|
|
35
|
+
imaginary[a] += ti;
|
|
36
|
+
const next = wr * wr0 - wi * wi0;
|
|
37
|
+
wi = wr * wi0 + wi * wr0;
|
|
38
|
+
wr = next;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
for (let z = 0; z < size; z++)
|
|
44
|
+
line(z * size, 1);
|
|
45
|
+
for (let x = 0; x < size; x++)
|
|
46
|
+
line(x, size);
|
|
47
|
+
const norm = 1 / (size * size);
|
|
48
|
+
for (let i = 0; i < real.length; i++) {
|
|
49
|
+
real[i] *= norm;
|
|
50
|
+
imaginary[i] *= norm;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
export class WaterSpectralOcean {
|
|
54
|
+
constructor({ resolution = 64, seed = 731 } = {}) {
|
|
55
|
+
if (resolution < 8 || (resolution & (resolution - 1)))
|
|
56
|
+
throw new RangeError('FFT resolution must be a power of two');
|
|
57
|
+
this.resolution = resolution;
|
|
58
|
+
this.seed = seed;
|
|
59
|
+
this.bands = [];
|
|
60
|
+
this.signature = '';
|
|
61
|
+
this.time = 0;
|
|
62
|
+
}
|
|
63
|
+
configure(settings, depth = 20, minWavelength = 0) {
|
|
64
|
+
const { waveAmplitude = 0.3, waveIntensity = 0.3, waveLength = 13, waveDirection = [1, 0], waveDirectionSpread = 0.5 } = settings;
|
|
65
|
+
const peakLength = Math.max(waveLength, waveAmplitude * Math.pow(waveIntensity, 1.35) * TAU / 0.18, 2);
|
|
66
|
+
const signature = JSON.stringify([peakLength, waveAmplitude, waveIntensity, waveDirection, waveDirectionSpread, depth, minWavelength]);
|
|
67
|
+
if (signature === this.signature)
|
|
68
|
+
return;
|
|
69
|
+
this.signature = signature;
|
|
70
|
+
const targetRms = waveAmplitude * Math.pow(waveIntensity, 1.35) * 0.707;
|
|
71
|
+
const n = this.resolution, dk0 = TAU / (peakLength * 8), kp = TAU / peakLength;
|
|
72
|
+
const direction = Math.atan2(waveDirection[1], waveDirection[0]);
|
|
73
|
+
this.bands = [];
|
|
74
|
+
// Separate k ranges avoid counting the same energy in several cascades.
|
|
75
|
+
for (let band = 0; band < 3; band++) {
|
|
76
|
+
const length = peakLength * 8 / Math.pow(4, band), dk = TAU / length, rand = random(this.seed + band * 101);
|
|
77
|
+
const data = { length, real: new Float64Array(n * n), imag: new Float64Array(n * n), h0r: new Float64Array(n * n), h0i: new Float64Array(n * n), omega: new Float64Array(n * n), size: n };
|
|
78
|
+
for (let z = 0; z < n; z++)
|
|
79
|
+
for (let x = 0; x < n; x++) {
|
|
80
|
+
const i = z * n + x, kx = (x <= n / 2 ? x : x - n) * dk, kz = (z <= n / 2 ? z : z - n) * dk, k = Math.hypot(kx, kz);
|
|
81
|
+
if (k < dk0 || k < (band === 0 ? 0 : dk * n / 16) || k >= dk * n / 4 || (minWavelength > 0 && k > TAU / minWavelength))
|
|
82
|
+
continue;
|
|
83
|
+
const omega = Math.sqrt(G * k * Math.tanh(k * Math.max(depth, 0.2))), wp = Math.sqrt(G * kp * Math.tanh(kp * Math.max(depth, 0.2)));
|
|
84
|
+
const sigma = omega <= wp ? 0.07 : 0.09, r = Math.exp(-((omega / wp - 1) ** 2) / (2 * sigma * sigma));
|
|
85
|
+
const density = Math.pow(omega, -5) * Math.exp(-1.25 * Math.pow(wp / omega, 4)) * Math.pow(3.3, r);
|
|
86
|
+
const c = Math.cos(Math.atan2(kz, kx) - direction);
|
|
87
|
+
const directional = Math.pow(Math.max(c, 0), 2 + 14 * (1 - waveDirectionSpread)) + 0.015 * waveDirectionSpread;
|
|
88
|
+
const kh = k * Math.max(depth, 0.2), t = Math.tanh(kh);
|
|
89
|
+
const derivative = G * (t + kh * (1 - t * t)) / (2 * omega);
|
|
90
|
+
const amplitude = Math.sqrt(Math.max(density * directional * derivative / k * dk * dk, 0));
|
|
91
|
+
const gaussian = Math.sqrt(-2 * Math.log(rand())), phase = TAU * rand();
|
|
92
|
+
data.h0r[i] = amplitude * gaussian * Math.cos(phase);
|
|
93
|
+
data.h0i[i] = amplitude * gaussian * Math.sin(phase);
|
|
94
|
+
data.omega[i] = omega;
|
|
95
|
+
}
|
|
96
|
+
this.bands.push(data);
|
|
97
|
+
}
|
|
98
|
+
const expectedVariance = this.bands.reduce((sum, b) => sum + b.h0r.reduce((a, v, i) => a + 2 * (v * v + b.h0i[i] ** 2), 0) / (n ** 4), 0);
|
|
99
|
+
const scale = targetRms / Math.sqrt(Math.max(expectedVariance, 1e-30));
|
|
100
|
+
for (const b of this.bands)
|
|
101
|
+
for (let i = 0; i < n * n; i++) {
|
|
102
|
+
b.h0r[i] *= scale;
|
|
103
|
+
b.h0i[i] *= scale;
|
|
104
|
+
}
|
|
105
|
+
this.peakLength = peakLength;
|
|
106
|
+
this.rms = targetRms;
|
|
107
|
+
}
|
|
108
|
+
update(time) {
|
|
109
|
+
this.time = time;
|
|
110
|
+
const n = this.resolution;
|
|
111
|
+
for (const b of this.bands) {
|
|
112
|
+
for (let z = 0; z < n; z++)
|
|
113
|
+
for (let x = 0; x < n; x++) {
|
|
114
|
+
const i = z * n + x, j = ((n - z) % n) * n + (n - x) % n;
|
|
115
|
+
// Use negative phase: an h0(k) travelling component moves along +k.
|
|
116
|
+
const phase = b.omega[i] * time, c = Math.cos(phase), s = Math.sin(phase);
|
|
117
|
+
const cj = c, sj = s;
|
|
118
|
+
b.real[i] = b.h0r[i] * c + b.h0i[i] * s + b.h0r[j] * cj + b.h0i[j] * sj;
|
|
119
|
+
b.imag[i] = b.h0i[i] * c - b.h0r[i] * s - b.h0i[j] * cj + b.h0r[j] * sj;
|
|
120
|
+
}
|
|
121
|
+
inverseFFT2D(b.real, b.imag, n);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
sample(x, z, minWavelength = 0) {
|
|
125
|
+
let h = 0;
|
|
126
|
+
const n = this.resolution;
|
|
127
|
+
for (const b of this.bands) {
|
|
128
|
+
// Geometry excludes bands below its spatial sampling limit. Full-band
|
|
129
|
+
// normals/caustics may use sample(...,0) on their denser grid.
|
|
130
|
+
const gx = ((x / b.length % 1) + 1) % 1 * n, gz = ((z / b.length % 1) + 1) % 1 * n;
|
|
131
|
+
const ix = Math.floor(gx), iz = Math.floor(gz), tx = gx - ix, tz = gz - iz;
|
|
132
|
+
const a = iz * n + ix, bb = iz * n + (ix + 1) % n, c = ((iz + 1) % n) * n + ix, d = ((iz + 1) % n) * n + (ix + 1) % n;
|
|
133
|
+
h += (b.real[a] * (1 - tx) + b.real[bb] * tx) * (1 - tz) + (b.real[c] * (1 - tx) + b.real[d] * tx) * tz;
|
|
134
|
+
}
|
|
135
|
+
return h;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
|
|
6
6
|
export const BEACH_RUNUP_DISTANCE = 10;
|
|
7
7
|
export const BEACH_WATER_LEVEL = 0.36;
|
|
8
|
-
export const BEACH_WAVE_SPEED = 0.
|
|
8
|
+
export const BEACH_WAVE_SPEED = 0.65;
|
|
9
9
|
export const BEACH_DIRECTION_SPREAD = 0.18;
|
|
10
10
|
|
|
11
11
|
export const SHOREWARD_ORIENTATION = Object.freeze({
|
|
@@ -14,8 +14,13 @@ export const SHOREWARD_ORIENTATION = Object.freeze({
|
|
|
14
14
|
});
|
|
15
15
|
|
|
16
16
|
export function waterStageOverrides(stage, baseSettings = {}) {
|
|
17
|
+
const shoreResponse = {
|
|
18
|
+
...(Number.isFinite(baseSettings.shorelineRunup) ? { shorelineRunup: baseSettings.shorelineRunup } : {}),
|
|
19
|
+
...(Number.isFinite(baseSettings.breakerAmount) ? { breakerAmount: baseSettings.breakerAmount } : {}),
|
|
20
|
+
};
|
|
17
21
|
if (stage === 'beach') {
|
|
18
22
|
return {
|
|
23
|
+
...shoreResponse,
|
|
19
24
|
flowDirection: [...SHOREWARD_ORIENTATION.flowDirection],
|
|
20
25
|
runupDistance: BEACH_RUNUP_DISTANCE,
|
|
21
26
|
waterLevel: BEACH_WATER_LEVEL,
|
|
@@ -24,8 +29,20 @@ export function waterStageOverrides(stage, baseSettings = {}) {
|
|
|
24
29
|
waveDirectionSpread: BEACH_DIRECTION_SPREAD,
|
|
25
30
|
};
|
|
26
31
|
}
|
|
32
|
+
if (String(stage ?? '').startsWith('river')) {
|
|
33
|
+
return {
|
|
34
|
+
flowDirection: [1, 0],
|
|
35
|
+
waveDirection: [1, 0],
|
|
36
|
+
runupDistance: 0,
|
|
37
|
+
shorelineRunup: 0,
|
|
38
|
+
breakerAmount: 0,
|
|
39
|
+
...(Number.isFinite(baseSettings.waterLevel) ? { waterLevel: baseSettings.waterLevel } : {}),
|
|
40
|
+
...(Number.isFinite(baseSettings.waveSpeed) ? { waveSpeed: baseSettings.waveSpeed } : {}),
|
|
41
|
+
};
|
|
42
|
+
}
|
|
27
43
|
if (stage === 'shore') {
|
|
28
44
|
return {
|
|
45
|
+
...shoreResponse,
|
|
29
46
|
flowDirection: [...SHOREWARD_ORIENTATION.flowDirection],
|
|
30
47
|
runupDistance: 0,
|
|
31
48
|
...(Number.isFinite(baseSettings.waterLevel)
|
|
@@ -42,6 +59,7 @@ export function waterStageOverrides(stage, baseSettings = {}) {
|
|
|
42
59
|
}
|
|
43
60
|
if (stage === 'open') {
|
|
44
61
|
return {
|
|
62
|
+
...shoreResponse,
|
|
45
63
|
...(baseSettings.flowDirection ? { flowDirection: [...baseSettings.flowDirection] } : {}),
|
|
46
64
|
runupDistance: 0,
|
|
47
65
|
...(Number.isFinite(baseSettings.waterLevel)
|