@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.
- package/AGENTS.md +127 -4
- package/ATTRIBUTION.md +41 -3
- package/README.md +234 -34
- package/docs/characters.md +144 -0
- package/docs/debug-panel.md +126 -0
- package/docs/docs.css +589 -0
- package/docs/environment.md +186 -0
- package/docs/getting-started.md +180 -0
- package/docs/index.html +14 -0
- package/docs/lab-architecture.md +100 -0
- package/docs/lighting.md +825 -0
- package/docs/main.jsx +739 -0
- package/docs/mcp.md +97 -0
- package/docs/post-processing.md +98 -0
- package/docs/settings-reference.md +1878 -0
- package/docs/shader-constants.md +77 -0
- package/docs/sky.md +182 -0
- package/docs/style-labs.md +309 -0
- package/docs/texture-lab.md +135 -0
- package/docs/toon-shading.md +183 -0
- package/docs/tsl-conventions.md +167 -0
- package/docs/vegetation-sky.md +275 -0
- package/docs/water.md +430 -0
- package/docs/weather.md +200 -0
- package/docs/world-scale.md +111 -0
- package/mcp/server.mjs +610 -0
- package/mcp/style-lab-tools.mjs +371 -0
- package/mcp/vite-plugin.mjs +174 -0
- package/mcp/workspace.mjs +397 -0
- package/package.json +64 -5
- package/src/ambientfx/INTEGRATION.md +164 -0
- package/src/ambientfx/ambientFxPresets.js +83 -0
- package/src/ambientfx/ambientFxSettings.js +368 -0
- package/src/ambientfx/emitters.js +169 -0
- package/src/ambientfx/index.js +5 -0
- package/src/ambientfx/particleBackbone.js +493 -0
- package/src/ambientfx/stylizedAmbientFx.js +450 -0
- package/src/assetlib/ambientcg.js +117 -0
- package/src/assetlib/assetRef.js +91 -0
- package/src/assetlib/importedEntry.js +40 -0
- package/src/assetlib/index.js +23 -0
- package/src/assetlib/kaykit.js +192 -0
- package/src/assetlib/kaykitStaticIndex.js +700 -0
- package/src/assetlib/loadImported.js +143 -0
- package/src/assetlib/opensource3d.js +113 -0
- package/src/assetlib/polyhaven.js +182 -0
- package/src/assetlib/polypizza.js +115 -0
- package/src/assetlib/smithsonian.js +214 -0
- package/src/assetlib/sources.js +279 -0
- package/src/assetlib/zip.js +58 -0
- package/src/biome/biomeGenerator.js +385 -0
- package/src/biome/biomeRuntime.js +299 -0
- package/src/biome/index.js +2 -0
- package/src/buildinggen/buildingAsset.js +44 -0
- package/src/buildinggen/buildingGrammar.js +311 -0
- package/src/buildinggen/buildingMesh.js +451 -0
- package/src/buildinggen/buildingPresets.js +46 -0
- package/src/buildinggen/buildingRecipe.js +100 -0
- package/src/buildinggen/buildingSettings.js +238 -0
- package/src/buildinggen/index.js +6 -0
- package/src/camera/cameraDirector.js +157 -0
- package/src/camera/cameraGenerator.js +367 -0
- package/src/camera/cameraRig.js +570 -0
- package/src/camera/cameraSettings.js +236 -0
- package/src/camera/index.js +7 -0
- package/src/catalog/builtinEntries.js +245 -0
- package/src/catalog/catalog.js +210 -0
- package/src/catalog/index.js +3 -0
- package/src/catalog/manifest.js +84 -0
- package/src/core/generation.js +529 -0
- package/src/environment/environmentRigs.js +21 -1
- package/src/environment/environmentSettings.js +4 -0
- package/src/environment/environmentSunShadowPass.js +8 -0
- package/src/fauna/INTEGRATION.md +174 -0
- package/src/fauna/boids.js +861 -0
- package/src/fauna/faunaBodies.js +492 -0
- package/src/fauna/faunaPresets.js +52 -0
- package/src/fauna/faunaSettings.js +525 -0
- package/src/fauna/index.js +5 -0
- package/src/fauna/stylizedFauna.js +395 -0
- package/src/game-feel/gameFeelGenerator.js +402 -0
- package/src/game-feel/gameFeelRuntime.js +549 -0
- package/src/game-feel/index.js +2 -0
- package/src/index.js +17 -6
- package/src/lighting/colorIntensity.js +177 -0
- package/src/lighting/index.js +161 -0
- package/src/lighting/lightDescriptors.js +249 -0
- package/src/lighting/lightingCapabilities.js +79 -0
- package/src/lighting/lightingDocuments.js +247 -0
- package/src/lighting/lightingFixtures.js +446 -0
- package/src/lighting/lightingGenerator.js +449 -0
- package/src/lighting/lightingPresets.js +319 -0
- package/src/lighting/lightingRuntime.js +723 -0
- package/src/lighting/lightingStyle.js +386 -0
- package/src/lighting/lightingSystem.js +774 -0
- package/src/lighting/unrealExport.js +186 -0
- package/src/lighting/utils.js +87 -0
- package/src/motion/index.js +5 -0
- package/src/motion/motionClip.js +441 -0
- package/src/motion/motionController.js +628 -0
- package/src/motion/motionDocuments.js +225 -0
- package/src/motion/motionGraph.js +307 -0
- package/src/motion/motionSettings.js +222 -0
- package/src/pathgen/index.js +7 -0
- package/src/pathgen/pathBridge.js +232 -0
- package/src/pathgen/pathPresets.js +35 -0
- package/src/pathgen/pathRibbon.js +410 -0
- package/src/pathgen/pathRouter.js +380 -0
- package/src/pathgen/pathSettings.js +335 -0
- package/src/pathgen/pathTextures.js +123 -0
- package/src/pathgen/stylizedPaths.js +453 -0
- package/src/post/index.js +1 -0
- package/src/post/postGenerator.js +177 -0
- package/src/post/postProcessing.js +41 -0
- package/src/propgen/generatorsWave1.js +379 -0
- package/src/propgen/generatorsWave2.js +462 -0
- package/src/propgen/index.js +5 -0
- package/src/propgen/propAsset.js +323 -0
- package/src/propgen/propParts.js +170 -0
- package/src/propgen/propPlacement.js +459 -0
- package/src/propgen/propPresets.js +82 -0
- package/src/propgen/propSettings.js +395 -0
- package/src/shaders-tsl/chunks/projected-water-caustics.js +242 -0
- package/src/shaders-tsl/chunks/vegetation-style.js +360 -0
- package/src/shaders-tsl/chunks/water-shore-state.js +31 -0
- package/src/shaders-tsl/chunks/water-waves.js +90 -11
- package/src/shaders-tsl/environment.js +15 -1
- package/src/shaders-tsl/flower.js +279 -30
- package/src/shaders-tsl/grass.js +60 -33
- package/src/shaders-tsl/sky.js +125 -31
- package/src/shaders-tsl/tree-leaf.js +61 -25
- package/src/shaders-tsl/water-breaker.js +7 -4
- package/src/shaders-tsl/water-shore-state-simulation.js +523 -0
- package/src/shaders-tsl/water.js +439 -49
- package/src/shaders-tsl/woody-surface.js +154 -0
- package/src/sky/sceneOverrideLayers.js +10 -0
- package/src/sky/skyQuality.js +26 -0
- package/src/sky/stylizedSky.js +753 -45
- package/src/soundscape/index.js +4 -0
- package/src/soundscape/soundscapeGenerator.js +179 -0
- package/src/soundscape/soundscapeRuntime.js +806 -0
- package/src/soundscape/soundscapeSettings.js +292 -0
- package/src/styles/index.js +13 -0
- package/src/styles/styleBundle.js +325 -0
- package/src/stylizedTerrain.js +32 -2
- package/src/stylizedWorld.js +423 -20
- package/src/texgen/evaluateTexture.js +675 -0
- package/src/texgen/index.js +60 -0
- package/src/texgen/noise2.js +210 -0
- package/src/texgen/textureAi.js +436 -0
- package/src/texgen/textureGenerators.js +516 -0
- package/src/texgen/texturePresets.js +490 -0
- package/src/texgen/textureSettings.js +342 -0
- package/src/texgen/textureThree.js +59 -0
- package/src/vegetation/flowerSpecies.js +15 -3
- package/src/vegetation/grassPalettes.js +153 -0
- package/src/vegetation/index.js +6 -0
- package/src/vegetation/stylizedBush.js +2 -0
- package/src/vegetation/stylizedFlower.js +82 -0
- package/src/vegetation/stylizedFlowers.js +48 -7
- package/src/vegetation/stylizedForest.js +29 -1
- package/src/vegetation/stylizedGrass.js +291 -56
- package/src/vegetation/stylizedTree.js +38 -2
- package/src/vegetation/stylizedTreeFoliage.js +2 -1
- package/src/vegetation/vegetationShaders.js +1110 -0
- package/src/vfxgen/INTEGRATION.md +145 -0
- package/src/vfxgen/core/burstBackbone.js +380 -0
- package/src/vfxgen/core/projectileCore.js +92 -0
- package/src/vfxgen/core/spriteShapes.js +98 -0
- package/src/vfxgen/core/trailRibbon.js +272 -0
- package/src/vfxgen/core/vfxRandom.js +29 -0
- package/src/vfxgen/effects/emitHelpers.js +37 -0
- package/src/vfxgen/effects/magicEffects.js +162 -0
- package/src/vfxgen/effects/movementEffects.js +87 -0
- package/src/vfxgen/effects/weaponEffects.js +118 -0
- package/src/vfxgen/index.js +18 -0
- package/src/vfxgen/moves/moveController.js +146 -0
- package/src/vfxgen/moves/moveLibrary.js +335 -0
- package/src/vfxgen/vfxPresets.js +98 -0
- package/src/vfxgen/vfxSettings.js +384 -0
- package/src/vfxgen/vfxSystem.js +449 -0
- package/src/vfxgen/weapons/stylizedWeapons.js +137 -0
- package/src/villagegen/index.js +4 -0
- package/src/villagegen/stylizedVillage.js +490 -0
- package/src/villagegen/villageArchetypes.js +160 -0
- package/src/villagegen/villageNames.js +40 -0
- package/src/villagegen/villageSites.js +105 -0
- package/src/water/sceneOverrideLayers.js +23 -0
- package/src/water/water.js +5 -0
- package/src/water/waterBreakerSystem.js +15 -1
- package/src/water/waterCurrentField.js +447 -0
- package/src/water/waterMaterial.js +12 -0
- package/src/water/waterNearshorePhase.js +320 -0
- package/src/water/waterScenePasses.js +83 -30
- package/src/water/waterSettings.js +325 -13
- package/src/water/waterShoreMaterial.js +322 -0
- package/src/water/waterShoreStateField.js +605 -0
- package/src/water/waterSurface.js +797 -28
- package/src/weather/index.js +6 -0
- package/src/weather/weatherPrecipitation.js +221 -0
- package/src/weather/weatherPresets.js +258 -0
- package/src/weather/weatherSettings.js +269 -0
- package/src/weather/weatherSystem.js +871 -0
- package/src/worldMinimap.js +62 -0
- package/src/worldPresets.js +4 -1
|
@@ -1,25 +1,72 @@
|
|
|
1
1
|
import * as THREE from 'three';
|
|
2
2
|
|
|
3
|
+
import { updateProjectedWaterCaustics } from '../shaders-tsl/chunks/projected-water-caustics.js';
|
|
4
|
+
|
|
3
5
|
import {
|
|
4
6
|
applyWaterSettingsToMaterial,
|
|
5
7
|
createWaterMaterial,
|
|
6
8
|
setWaterDebugMode,
|
|
7
9
|
updateWaterMaterialCamera,
|
|
8
10
|
} from './waterMaterial.js';
|
|
9
|
-
import {
|
|
11
|
+
import { WaterCurrentField } from './waterCurrentField.js';
|
|
12
|
+
import {
|
|
13
|
+
buildNearshorePhaseField,
|
|
14
|
+
sampleNearshorePhaseField,
|
|
15
|
+
} from './waterNearshorePhase.js';
|
|
16
|
+
import { shouldUseDedicatedBreakerShell, WaterBreakerSystem } from './waterBreakerSystem.js';
|
|
10
17
|
import { WaterInteractionManager } from './waterInteraction.js';
|
|
11
18
|
import { WaterRippleSimulation } from './waterRippleSimulation.js';
|
|
12
19
|
import { WaterScenePasses } from './waterScenePasses.js';
|
|
20
|
+
import { WaterShoreStateField } from './waterShoreStateField.js';
|
|
21
|
+
import { updateWaterShoreMaterial } from './waterShoreMaterial.js';
|
|
13
22
|
import { WaterSplashSystem } from './waterSplashSystem.js';
|
|
23
|
+
import {
|
|
24
|
+
WATER_SCENE_OVERRIDE_KEYS,
|
|
25
|
+
WATER_SCENE_OVERRIDE_PRIORITIES,
|
|
26
|
+
} from './sceneOverrideLayers.js';
|
|
14
27
|
import {
|
|
15
28
|
createWaterSettings,
|
|
16
29
|
sampleGerstnerHeight,
|
|
30
|
+
sampleSwashEdgeOffset,
|
|
31
|
+
sampleSwashFrameState,
|
|
17
32
|
} from './waterSettings.js';
|
|
18
33
|
|
|
19
34
|
const worldPositionScratch = new THREE.Vector3();
|
|
20
35
|
const localScratch = new THREE.Vector3();
|
|
21
36
|
const breakerSampleScratch = { weight: 0, crestY: 0, flowX: 0, flowZ: 0 };
|
|
22
37
|
const followScratch = new THREE.Vector3();
|
|
38
|
+
const currentSampleScratch = new THREE.Vector2();
|
|
39
|
+
|
|
40
|
+
const waterSceneOverrideKeySet = new Set(WATER_SCENE_OVERRIDE_KEYS);
|
|
41
|
+
|
|
42
|
+
function cleanObject(value) {
|
|
43
|
+
return value && typeof value === 'object' && !Array.isArray(value) ? value : {};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function cloneValue(value) {
|
|
47
|
+
if (Array.isArray(value)) return [...value];
|
|
48
|
+
if (value && typeof value === 'object') return { ...value };
|
|
49
|
+
return value;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function collectWaterSceneOverrides(options) {
|
|
53
|
+
return Object.fromEntries(
|
|
54
|
+
Object.entries(cleanObject(options))
|
|
55
|
+
.filter(([key, value]) => waterSceneOverrideKeySet.has(key) && value !== undefined)
|
|
56
|
+
.map(([key, value]) => [key, cloneValue(value)]),
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function disposeAfterRenderBoundary(disposable) {
|
|
61
|
+
const dispose = () => disposable.dispose();
|
|
62
|
+
if (typeof globalThis.requestAnimationFrame !== 'function') {
|
|
63
|
+
globalThis.setTimeout(dispose, 34);
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
globalThis.requestAnimationFrame(() => {
|
|
67
|
+
globalThis.requestAnimationFrame(dispose);
|
|
68
|
+
});
|
|
69
|
+
}
|
|
23
70
|
|
|
24
71
|
// All-in-one stylized water body: surface mesh + material, GPU ripple
|
|
25
72
|
// simulation, splash particles, interaction tracking, and the scene passes
|
|
@@ -44,6 +91,9 @@ export class WaterSurface extends THREE.Mesh {
|
|
|
44
91
|
splashes = {},
|
|
45
92
|
passes = {},
|
|
46
93
|
interaction = {},
|
|
94
|
+
currentField = null,
|
|
95
|
+
nearshorePhase = false,
|
|
96
|
+
shoreState = false,
|
|
47
97
|
follow = null,
|
|
48
98
|
bedHeight = null,
|
|
49
99
|
...settingsOptions
|
|
@@ -63,7 +113,15 @@ export class WaterSurface extends THREE.Mesh {
|
|
|
63
113
|
this.name = 'WaterSurface';
|
|
64
114
|
this.width = width;
|
|
65
115
|
this.depth = depth;
|
|
116
|
+
this.segmentsX = segmentsX;
|
|
117
|
+
this.segmentsZ = segmentsZ;
|
|
66
118
|
this.settings = this.material.userData.waterSettings;
|
|
119
|
+
this._authoredQualityRequest = settingsOptions.quality && typeof settingsOptions.quality === 'object'
|
|
120
|
+
? { ...settingsOptions.quality }
|
|
121
|
+
: this.settings.quality;
|
|
122
|
+
this._sceneOverrideLayers = new Map();
|
|
123
|
+
this._sceneOverrideSequence = 0;
|
|
124
|
+
this._sceneOverrides = {};
|
|
67
125
|
this.time = 0;
|
|
68
126
|
this.followTarget = follow;
|
|
69
127
|
// Displaced waves escape the flat-plane bounds; skip culling entirely.
|
|
@@ -77,9 +135,29 @@ export class WaterSurface extends THREE.Mesh {
|
|
|
77
135
|
// whenever the surface's world XZ moves (>5cm) from where it was baked —
|
|
78
136
|
// immune to first-frame ordering races where the mesh isn't positioned
|
|
79
137
|
// yet. The terrain itself is assumed static.
|
|
80
|
-
this.
|
|
138
|
+
this.shoalingEnabled = typeof bedHeight === 'function';
|
|
139
|
+
this.bedHeightSampler = this.shoalingEnabled ? bedHeight : null;
|
|
81
140
|
this.shoalingBakedX = NaN;
|
|
141
|
+
this.shoalingBakedY = NaN;
|
|
82
142
|
this.shoalingBakedZ = NaN;
|
|
143
|
+
// Optional mild-slope phase refraction for the primary swell and a true
|
|
144
|
+
// same-direction set-beat partner when that feature is enabled.
|
|
145
|
+
// The graph always reads aNearshorePhase on shoaling surfaces, so even a
|
|
146
|
+
// disabled/unsupported stage receives a disabled fallback attribute.
|
|
147
|
+
const nearshoreOptions = nearshorePhase === true ? {} : nearshorePhase;
|
|
148
|
+
this.nearshorePhaseEnabled = Boolean(this.shoalingEnabled && nearshoreOptions);
|
|
149
|
+
this.nearshorePhaseOptions = this.nearshorePhaseEnabled
|
|
150
|
+
? { ...nearshoreOptions }
|
|
151
|
+
: null;
|
|
152
|
+
this.nearshorePhaseField = null;
|
|
153
|
+
this.nearshorePhaseBakedSignature = '';
|
|
154
|
+
this.nearshorePhaseReference = null;
|
|
155
|
+
this.nearshorePhaseSample = {};
|
|
156
|
+
this.nearshorePhaseStatus = {
|
|
157
|
+
active: false,
|
|
158
|
+
reason: this.nearshorePhaseEnabled ? 'not-baked' : 'disabled',
|
|
159
|
+
};
|
|
160
|
+
if (this.shoalingEnabled) this.writeNearshorePhaseAttribute(null);
|
|
83
161
|
// Dedicated plunging-breaker shells along the break line; created lazily
|
|
84
162
|
// in update() when settings.breakerAmount > 0 and a bed sampler exists.
|
|
85
163
|
this.breakers = null;
|
|
@@ -112,13 +190,208 @@ export class WaterSurface extends THREE.Mesh {
|
|
|
112
190
|
|
|
113
191
|
this.interactions = new WaterInteractionManager(this, interaction || {});
|
|
114
192
|
|
|
193
|
+
// Optional authored world-space current atlas. Passing a WaterCurrentField
|
|
194
|
+
// shares external ownership; passing an options object creates a field
|
|
195
|
+
// owned by this surface. Currents feed CPU gameplay queries and the small
|
|
196
|
+
// shoreline-state pass, not the private-memory-heavy main water shader.
|
|
197
|
+
const currentFieldOptions = currentField === true ? {} : currentField;
|
|
198
|
+
this.ownsCurrentField = Boolean(
|
|
199
|
+
currentFieldOptions && !currentFieldOptions.isWaterCurrentField,
|
|
200
|
+
);
|
|
201
|
+
this.currentField = currentFieldOptions?.isWaterCurrentField
|
|
202
|
+
? currentFieldOptions
|
|
203
|
+
: currentFieldOptions
|
|
204
|
+
? new WaterCurrentField({
|
|
205
|
+
region: { centerX: 0, centerZ: 0, width, depth },
|
|
206
|
+
...currentFieldOptions,
|
|
207
|
+
})
|
|
208
|
+
: null;
|
|
209
|
+
|
|
210
|
+
// Persistent beach history is opt-in because rivers/open-water bodies do
|
|
211
|
+
// not all need a fixed shoreline atlas. It is a separate small GPU pass,
|
|
212
|
+
// so foam transport and wet-sand memory do not enlarge the already-dense
|
|
213
|
+
// main water material (important for WebGPU's private-memory limit).
|
|
214
|
+
const shoreStateOptions = shoreState === true ? {} : shoreState;
|
|
215
|
+
this.shoreState = this.bedHeightSampler && shoreStateOptions
|
|
216
|
+
? new WaterShoreStateField({
|
|
217
|
+
region: {
|
|
218
|
+
centerX: 0,
|
|
219
|
+
centerZ: 0,
|
|
220
|
+
width,
|
|
221
|
+
depth,
|
|
222
|
+
},
|
|
223
|
+
bedHeightSampler: this.bedHeightSampler,
|
|
224
|
+
currentField: this.currentField,
|
|
225
|
+
...(shoreStateOptions || {}),
|
|
226
|
+
})
|
|
227
|
+
: null;
|
|
228
|
+
this.shoreStateMaterials = new Set();
|
|
229
|
+
|
|
115
230
|
this.syncSimulationParameters();
|
|
231
|
+
this.bindShoreState();
|
|
116
232
|
}
|
|
117
233
|
|
|
118
234
|
get gerstnerWaves() {
|
|
119
235
|
return this.material.userData.gerstnerWaves ?? [];
|
|
120
236
|
}
|
|
121
237
|
|
|
238
|
+
dominantNearshoreFrame() {
|
|
239
|
+
const primary = this.gerstnerWaves[0];
|
|
240
|
+
const secondary = this.gerstnerWaves[1];
|
|
241
|
+
const directionLength = Math.hypot(primary?.dirX ?? 0, primary?.dirZ ?? 0) || 1;
|
|
242
|
+
const secondaryDirectionLength = Math.hypot(
|
|
243
|
+
secondary?.dirX ?? 0,
|
|
244
|
+
secondary?.dirZ ?? 0,
|
|
245
|
+
) || 1;
|
|
246
|
+
const directionAgreement = primary && secondary
|
|
247
|
+
? (
|
|
248
|
+
(primary.dirX * secondary.dirX + primary.dirZ * secondary.dirZ) /
|
|
249
|
+
(directionLength * secondaryDirectionLength)
|
|
250
|
+
)
|
|
251
|
+
: -1;
|
|
252
|
+
// The authored set-beat partner copies the primary direction exactly.
|
|
253
|
+
// Leave a tiny tolerance for serialization/normalization roundoff, but do
|
|
254
|
+
// not fold an ordinary spread-spectrum slot into the shared phase field.
|
|
255
|
+
const secondarySharesPhase = (this.settings.waveSetStrength ?? 0) > 0.001 &&
|
|
256
|
+
directionAgreement >= 0.9999;
|
|
257
|
+
const waves = secondarySharesPhase ? [primary, secondary] : [primary];
|
|
258
|
+
let weightedWaveNumber = 0;
|
|
259
|
+
let totalWeight = 0;
|
|
260
|
+
for (const wave of waves) {
|
|
261
|
+
const weight = Math.max(Math.abs(Number(wave?.amplitude) || 0), 1e-6);
|
|
262
|
+
const waveNumber = Number(wave?.waveNumber);
|
|
263
|
+
if (!(waveNumber > 0)) continue;
|
|
264
|
+
weightedWaveNumber += waveNumber * weight;
|
|
265
|
+
totalWeight += weight;
|
|
266
|
+
}
|
|
267
|
+
return {
|
|
268
|
+
directionX: (primary?.dirX ?? 1) / directionLength,
|
|
269
|
+
directionZ: (primary?.dirZ ?? 0) / directionLength,
|
|
270
|
+
deepWaveNumber: totalWeight > 0
|
|
271
|
+
? weightedWaveNumber / totalWeight
|
|
272
|
+
: Math.max(Number(primary?.waveNumber) || 1, 1e-4),
|
|
273
|
+
slotMask: secondarySharesPhase ? 2 : 1,
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
nearshorePhaseSignature() {
|
|
278
|
+
const frame = this.dominantNearshoreFrame();
|
|
279
|
+
const options = this.nearshorePhaseOptions ?? {};
|
|
280
|
+
const dedicatedBreakers = shouldUseDedicatedBreakerShell(
|
|
281
|
+
this.settings,
|
|
282
|
+
Boolean(this.bedHeightSampler),
|
|
283
|
+
);
|
|
284
|
+
const solvesField = this.nearshorePhaseEnabled && !dedicatedBreakers;
|
|
285
|
+
return [
|
|
286
|
+
this.nearshorePhaseEnabled ? 1 : 0,
|
|
287
|
+
dedicatedBreakers ? 1 : 0,
|
|
288
|
+
frame.directionX,
|
|
289
|
+
frame.directionZ,
|
|
290
|
+
solvesField ? frame.slotMask : 0,
|
|
291
|
+
solvesField ? frame.deepWaveNumber : '',
|
|
292
|
+
solvesField ? options.incidentAxis ?? '' : '',
|
|
293
|
+
solvesField ? options.minDepth ?? '' : '',
|
|
294
|
+
solvesField ? options.maxWavenumberRatio ?? '' : '',
|
|
295
|
+
].join('|');
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// Writes packed q,dq/dx,dq/dz,slot-mask vertex data. A null field clears the
|
|
299
|
+
// mask, making the shader path an exact no-op for every wave
|
|
300
|
+
// direction on non-beach stages and whenever the mild-slope solve is unsafe.
|
|
301
|
+
writeNearshorePhaseAttribute(field) {
|
|
302
|
+
if (!this.shoalingEnabled) return this;
|
|
303
|
+
this.updateWorldMatrix(true, false);
|
|
304
|
+
const position = this.geometry.attributes.position;
|
|
305
|
+
const existing = this.geometry.attributes.aNearshorePhase;
|
|
306
|
+
const packed = existing?.count === position.count && existing.itemSize === 4
|
|
307
|
+
? existing.array
|
|
308
|
+
: new Float32Array(position.count * 4);
|
|
309
|
+
const frame = this.dominantNearshoreFrame();
|
|
310
|
+
for (let index = 0; index < position.count; index += 1) {
|
|
311
|
+
localScratch.fromBufferAttribute(position, index).applyMatrix4(this.matrixWorld);
|
|
312
|
+
if (field) {
|
|
313
|
+
packed[index * 4] = field.phaseCoordinate[index];
|
|
314
|
+
packed[index * 4 + 1] = field.waveVector[index * 2];
|
|
315
|
+
packed[index * 4 + 2] = field.waveVector[index * 2 + 1];
|
|
316
|
+
packed[index * 4 + 3] = field.slotMask ?? frame.slotMask;
|
|
317
|
+
} else {
|
|
318
|
+
packed[index * 4] = frame.directionX * localScratch.x +
|
|
319
|
+
frame.directionZ * localScratch.z;
|
|
320
|
+
packed[index * 4 + 1] = frame.directionX;
|
|
321
|
+
packed[index * 4 + 2] = frame.directionZ;
|
|
322
|
+
packed[index * 4 + 3] = 0;
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
if (existing?.array === packed) {
|
|
326
|
+
existing.needsUpdate = true;
|
|
327
|
+
} else {
|
|
328
|
+
this.geometry.setAttribute('aNearshorePhase', new THREE.BufferAttribute(packed, 4));
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
this.nearshorePhaseReference = null;
|
|
332
|
+
if (field) {
|
|
333
|
+
const options = this.nearshorePhaseOptions ?? {};
|
|
334
|
+
const referenceX = Number.isFinite(options.referenceX) ? options.referenceX : 0;
|
|
335
|
+
const referenceZ = Number.isFinite(options.referenceZ) ? options.referenceZ : 0;
|
|
336
|
+
const sample = sampleNearshorePhaseField(field, referenceX, referenceZ, {});
|
|
337
|
+
const directionLength = Math.hypot(sample.waveVectorX, sample.waveVectorZ) || 1;
|
|
338
|
+
this.nearshorePhaseReference = {
|
|
339
|
+
phaseCoordinate: sample.phaseCoordinate,
|
|
340
|
+
directionX: sample.waveVectorX / directionLength,
|
|
341
|
+
directionZ: sample.waveVectorZ / directionLength,
|
|
342
|
+
x: referenceX,
|
|
343
|
+
z: referenceZ,
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
return this;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
nearshoreBlendAt(restDepth) {
|
|
350
|
+
if (!this.nearshorePhaseField) return 0;
|
|
351
|
+
const range = Math.max(
|
|
352
|
+
this.settings.shoalingDepth ?? 1.4,
|
|
353
|
+
Math.max(this.waveEnergy, 1e-3) * 2.2,
|
|
354
|
+
1e-3,
|
|
355
|
+
);
|
|
356
|
+
return 1 - THREE.MathUtils.smoothstep(restDepth, range * 0.8, range * 1.8);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
sampleNearshoreAt(x, z, restDepth) {
|
|
360
|
+
const blend = this.nearshoreBlendAt(restDepth);
|
|
361
|
+
if (!(blend > 0)) return null;
|
|
362
|
+
const sample = sampleNearshorePhaseField(
|
|
363
|
+
this.nearshorePhaseField,
|
|
364
|
+
x,
|
|
365
|
+
z,
|
|
366
|
+
this.nearshorePhaseSample,
|
|
367
|
+
);
|
|
368
|
+
sample.blend = blend;
|
|
369
|
+
sample.slotMask = this.nearshorePhaseField.slotMask ?? 1;
|
|
370
|
+
return sample;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
nearshoreSwashTime(time = this.time) {
|
|
374
|
+
const primary = this.gerstnerWaves[0];
|
|
375
|
+
const coordinate = this.nearshorePhaseReference?.phaseCoordinate;
|
|
376
|
+
if (!primary || !Number.isFinite(coordinate) || !(primary.omega > 1e-6)) return time;
|
|
377
|
+
return time - (primary.waveNumber * coordinate) / primary.omega;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
sampleSwashFrame(time = this.time, runupDistance = this.settings.runupDistance) {
|
|
381
|
+
const frame = sampleSwashFrameState(
|
|
382
|
+
this.gerstnerWaves,
|
|
383
|
+
this.nearshoreSwashTime(time),
|
|
384
|
+
runupDistance,
|
|
385
|
+
);
|
|
386
|
+
const reference = this.nearshorePhaseReference;
|
|
387
|
+
if (reference) {
|
|
388
|
+
frame.primaryDirectionX = reference.directionX;
|
|
389
|
+
frame.primaryDirectionZ = reference.directionZ;
|
|
390
|
+
frame.primaryDirection = [reference.directionX, reference.directionZ];
|
|
391
|
+
}
|
|
392
|
+
return frame;
|
|
393
|
+
}
|
|
394
|
+
|
|
122
395
|
syncSimulationParameters() {
|
|
123
396
|
this.ripples?.setParameters({
|
|
124
397
|
rippleDamping: this.settings.rippleDamping,
|
|
@@ -126,13 +399,105 @@ export class WaterSurface extends THREE.Mesh {
|
|
|
126
399
|
rippleFoamDecay: this.settings.rippleFoamDecay,
|
|
127
400
|
rippleFoamGain: this.settings.rippleFoamGain,
|
|
128
401
|
});
|
|
402
|
+
this.shoreState?.setParameters({
|
|
403
|
+
moistureDryTime: this.settings.wetSandDryTime,
|
|
404
|
+
foamWetLifetime: this.settings.swashFoamLifetime,
|
|
405
|
+
foamDryLifetime: Math.max(this.settings.swashFoamLifetime * 0.45, 0.25),
|
|
406
|
+
residueLifetime: this.settings.swashFoamResidueLifetime,
|
|
407
|
+
});
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
// A ground material can consume the same ping-pong texture as the water.
|
|
411
|
+
// Registering it here refreshes its texture binding immediately after every
|
|
412
|
+
// state swap and before refraction/reflection scene passes are captured.
|
|
413
|
+
attachShoreStateMaterial(material) {
|
|
414
|
+
if (!material) return this;
|
|
415
|
+
this.shoreStateMaterials.add(material);
|
|
416
|
+
updateWaterShoreMaterial(material, { stateField: this.shoreState });
|
|
417
|
+
return this;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
detachShoreStateMaterial(material) {
|
|
421
|
+
this.shoreStateMaterials.delete(material);
|
|
422
|
+
return this;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
bindShoreState() {
|
|
426
|
+
const uniforms = this.material.uniforms;
|
|
427
|
+
if (this.shoreState) {
|
|
428
|
+
uniforms.uShoreStateMap.value = this.shoreState.texture;
|
|
429
|
+
uniforms.uUseShoreState.value = 1;
|
|
430
|
+
this.shoreState.getRegion(uniforms.uShoreStateRegion.value);
|
|
431
|
+
} else {
|
|
432
|
+
uniforms.uUseShoreState.value = 0;
|
|
433
|
+
}
|
|
434
|
+
for (const material of this.shoreStateMaterials ?? []) {
|
|
435
|
+
updateWaterShoreMaterial(material, { stateField: this.shoreState });
|
|
436
|
+
}
|
|
437
|
+
return this;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
/** Settings currently uploaded after transient scene overrides. */
|
|
441
|
+
get renderedSettings() {
|
|
442
|
+
return this.material.userData.waterSettings;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
/** Current effective transient overrides, separate from authored settings. */
|
|
446
|
+
get sceneOverrides() {
|
|
447
|
+
return Object.fromEntries(
|
|
448
|
+
Object.entries(this._sceneOverrides).map(([key, value]) => [key, cloneValue(value)]),
|
|
449
|
+
);
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
/** Ordered runtime layer metadata without exposing mutable layer values. */
|
|
453
|
+
get sceneOverrideLayers() {
|
|
454
|
+
return [...this._sceneOverrideLayers.values()]
|
|
455
|
+
.sort((a, b) => a.priority - b.priority || a.order - b.order)
|
|
456
|
+
.map((layer) => ({ id: layer.id, priority: layer.priority }));
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
_composeSceneSettings() {
|
|
460
|
+
let composed = createWaterSettings(this.settings);
|
|
461
|
+
const layers = [...this._sceneOverrideLayers.values()]
|
|
462
|
+
.sort((a, b) => a.priority - b.priority || a.order - b.order);
|
|
463
|
+
for (const layer of layers) {
|
|
464
|
+
const source = layer.resolve
|
|
465
|
+
? layer.resolve(createWaterSettings(composed))
|
|
466
|
+
: layer.settings;
|
|
467
|
+
composed = createWaterSettings({
|
|
468
|
+
...composed,
|
|
469
|
+
...collectWaterSceneOverrides(source),
|
|
470
|
+
});
|
|
471
|
+
}
|
|
472
|
+
return composed;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
_applyComposedSceneSettings() {
|
|
476
|
+
const composed = this._composeSceneSettings();
|
|
477
|
+
this._sceneOverrides = Object.fromEntries(
|
|
478
|
+
WATER_SCENE_OVERRIDE_KEYS
|
|
479
|
+
.filter((key) => JSON.stringify(composed[key]) !== JSON.stringify(this.settings[key]))
|
|
480
|
+
.map((key) => [key, cloneValue(composed[key])]),
|
|
481
|
+
);
|
|
482
|
+
applyWaterSettingsToMaterial(this.material, {
|
|
483
|
+
...composed,
|
|
484
|
+
quality: this._authoredQualityRequest,
|
|
485
|
+
});
|
|
486
|
+
return this.renderedSettings;
|
|
129
487
|
}
|
|
130
488
|
|
|
131
489
|
// Merges option overrides into the current settings (pass { preset } to
|
|
132
|
-
// switch presets while keeping explicit overrides you re-supply).
|
|
490
|
+
// switch presets while keeping explicit overrides you re-supply). This is
|
|
491
|
+
// the backward-compatible authored path; scene overrides stay separate.
|
|
133
492
|
applySettings(options = {}) {
|
|
134
|
-
|
|
135
|
-
this.settings = this.
|
|
493
|
+
const source = cleanObject(options);
|
|
494
|
+
this.settings = createWaterSettings({ ...this.settings, ...source });
|
|
495
|
+
if (source.quality !== undefined) {
|
|
496
|
+
this._authoredQualityRequest = source.quality && typeof source.quality === 'object'
|
|
497
|
+
? { ...source.quality }
|
|
498
|
+
: this.settings.quality;
|
|
499
|
+
}
|
|
500
|
+
this._applyComposedSceneSettings();
|
|
136
501
|
this.splashSystem?.applySettings(this.settings);
|
|
137
502
|
this.syncSimulationParameters();
|
|
138
503
|
return this.settings;
|
|
@@ -140,13 +505,76 @@ export class WaterSurface extends THREE.Mesh {
|
|
|
140
505
|
|
|
141
506
|
// Loads a preset from scratch (unlike applySettings, prior overrides drop).
|
|
142
507
|
setPreset(name, overrides = {}) {
|
|
143
|
-
|
|
144
|
-
this.settings =
|
|
508
|
+
const source = cleanObject(overrides);
|
|
509
|
+
this.settings = createWaterSettings({ preset: name, ...source });
|
|
510
|
+
this._authoredQualityRequest = source.quality && typeof source.quality === 'object'
|
|
511
|
+
? { ...source.quality }
|
|
512
|
+
: this.settings.quality;
|
|
513
|
+
this._applyComposedSceneSettings();
|
|
145
514
|
this.splashSystem?.applySettings(this.settings);
|
|
146
515
|
this.syncSimulationParameters();
|
|
147
516
|
return this.settings;
|
|
148
517
|
}
|
|
149
518
|
|
|
519
|
+
/**
|
|
520
|
+
* Adds or replaces one transient runtime owner. Static layers accept only
|
|
521
|
+
* {@link WATER_SCENE_OVERRIDE_KEYS}; a resolver receives the result of all
|
|
522
|
+
* lower-priority layers, enabling additive weather over an authored wave
|
|
523
|
+
* baseline without mutating {@link settings}.
|
|
524
|
+
*/
|
|
525
|
+
setSceneOverrideLayer(id, optionsOrResolver = {}, {
|
|
526
|
+
priority = WATER_SCENE_OVERRIDE_PRIORITIES.scene,
|
|
527
|
+
replace = true,
|
|
528
|
+
} = {}) {
|
|
529
|
+
if ((typeof id !== 'string' || id.length === 0) && typeof id !== 'symbol') {
|
|
530
|
+
throw new TypeError('A water scene override layer needs a non-empty string or Symbol id.');
|
|
531
|
+
}
|
|
532
|
+
const existing = this._sceneOverrideLayers.get(id);
|
|
533
|
+
const resolve = typeof optionsOrResolver === 'function' ? optionsOrResolver : null;
|
|
534
|
+
let settings = null;
|
|
535
|
+
if (!resolve) {
|
|
536
|
+
const next = collectWaterSceneOverrides(optionsOrResolver);
|
|
537
|
+
settings = !replace && existing?.settings
|
|
538
|
+
? { ...existing.settings, ...next }
|
|
539
|
+
: next;
|
|
540
|
+
}
|
|
541
|
+
this._sceneOverrideLayers.set(id, {
|
|
542
|
+
id,
|
|
543
|
+
order: existing?.order ?? this._sceneOverrideSequence++,
|
|
544
|
+
priority: Number.isFinite(Number(priority))
|
|
545
|
+
? Number(priority)
|
|
546
|
+
: WATER_SCENE_OVERRIDE_PRIORITIES.scene,
|
|
547
|
+
resolve,
|
|
548
|
+
settings,
|
|
549
|
+
});
|
|
550
|
+
return this._applyComposedSceneSettings();
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
/** Removes one runtime owner without disturbing other active layers. */
|
|
554
|
+
clearSceneOverrideLayer(id) {
|
|
555
|
+
if (!this._sceneOverrideLayers.delete(id)) return this.renderedSettings;
|
|
556
|
+
return this._applyComposedSceneSettings();
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
/** Convenience scene layer for current wave/light state. */
|
|
560
|
+
setSceneOverrides(options = {}, { replace = false } = {}) {
|
|
561
|
+
return this.setSceneOverrideLayer('scene', options, {
|
|
562
|
+
priority: WATER_SCENE_OVERRIDE_PRIORITIES.scene,
|
|
563
|
+
replace,
|
|
564
|
+
});
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
/** Clears only the convenience scene layer, preserving independent owners. */
|
|
568
|
+
clearSceneOverrides() {
|
|
569
|
+
return this.clearSceneOverrideLayer('scene');
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
/** Explicitly clears every transient owner and restores the authored baseline. */
|
|
573
|
+
clearAllSceneOverrideLayers() {
|
|
574
|
+
this._sceneOverrideLayers.clear();
|
|
575
|
+
return this._applyComposedSceneSettings();
|
|
576
|
+
}
|
|
577
|
+
|
|
150
578
|
setDebugMode(mode) {
|
|
151
579
|
setWaterDebugMode(this.material, mode);
|
|
152
580
|
return this;
|
|
@@ -189,6 +617,89 @@ export class WaterSurface extends THREE.Mesh {
|
|
|
189
617
|
return this;
|
|
190
618
|
}
|
|
191
619
|
|
|
620
|
+
// The material graph always supports the packed attribute, so a lab can
|
|
621
|
+
// opt a smooth beach in/out without recompiling WebGPU pipelines.
|
|
622
|
+
setNearshorePhase(nearshorePhase, { bake = true } = {}) {
|
|
623
|
+
const options = nearshorePhase === true ? {} : nearshorePhase;
|
|
624
|
+
this.nearshorePhaseEnabled = Boolean(this.shoalingEnabled && options);
|
|
625
|
+
this.nearshorePhaseOptions = this.nearshorePhaseEnabled ? { ...options } : null;
|
|
626
|
+
this.nearshorePhaseField = null;
|
|
627
|
+
this.nearshorePhaseReference = null;
|
|
628
|
+
this.nearshorePhaseBakedSignature = '';
|
|
629
|
+
this.nearshorePhaseStatus = {
|
|
630
|
+
active: false,
|
|
631
|
+
reason: this.nearshorePhaseEnabled ? 'not-baked' : 'disabled',
|
|
632
|
+
};
|
|
633
|
+
if (this.shoalingEnabled) {
|
|
634
|
+
this.writeNearshorePhaseAttribute(null);
|
|
635
|
+
if (bake && this.bedHeightSampler) this.bakeShoalingDepths();
|
|
636
|
+
}
|
|
637
|
+
return this;
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
// Replace the authored current atlas at runtime. Options objects become
|
|
641
|
+
// surface-owned fields; WaterCurrentField instances remain caller-owned.
|
|
642
|
+
setCurrentField(currentField, { disposePrevious = true } = {}) {
|
|
643
|
+
const options = currentField === true ? {} : currentField;
|
|
644
|
+
const nextOwned = Boolean(options && !options.isWaterCurrentField);
|
|
645
|
+
const next = options?.isWaterCurrentField
|
|
646
|
+
? options
|
|
647
|
+
: options
|
|
648
|
+
? new WaterCurrentField({
|
|
649
|
+
region: { centerX: 0, centerZ: 0, width: this.width, depth: this.depth },
|
|
650
|
+
...options,
|
|
651
|
+
})
|
|
652
|
+
: null;
|
|
653
|
+
const previous = this.currentField;
|
|
654
|
+
const previousOwned = this.ownsCurrentField;
|
|
655
|
+
this.currentField = next;
|
|
656
|
+
this.ownsCurrentField = nextOwned;
|
|
657
|
+
this.shoreState?.setCurrentField(next);
|
|
658
|
+
if (disposePrevious && previousOwned && previous && previous !== next) previous.dispose();
|
|
659
|
+
return this;
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
// Authored large-scale current only. getFlowAt() below adds breakers and
|
|
663
|
+
// swash so boats/characters can choose either signal explicitly.
|
|
664
|
+
getCurrentAt(x, z, out = new THREE.Vector2()) {
|
|
665
|
+
if (!this.currentField) return out.set(0, 0);
|
|
666
|
+
return this.currentField.sampleAt(x, z, out);
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
// Replace static terrain under an existing shoaling surface without
|
|
670
|
+
// rebuilding its material, render passes, ripple simulation, or animation
|
|
671
|
+
// clock. Whether shoaling exists is a graph-build choice, so callers cannot
|
|
672
|
+
// toggle it after construction; they may freely swap one sampler for
|
|
673
|
+
// another. This is useful for labs/worlds with selectable ground profiles.
|
|
674
|
+
setBedHeightSampler(bedHeight, { bake = true } = {}) {
|
|
675
|
+
const sampler = typeof bedHeight === 'function' ? bedHeight : null;
|
|
676
|
+
if (Boolean(sampler) !== this.shoalingEnabled) {
|
|
677
|
+
throw new Error(
|
|
678
|
+
'WaterSurface cannot toggle shoaling after construction; create a new surface instead.',
|
|
679
|
+
);
|
|
680
|
+
}
|
|
681
|
+
if (sampler === this.bedHeightSampler) return this;
|
|
682
|
+
|
|
683
|
+
this.bedHeightSampler = sampler;
|
|
684
|
+
this.shoreState?.setBedHeightSampler(sampler);
|
|
685
|
+
this.shoalingBakedX = NaN;
|
|
686
|
+
this.shoalingBakedY = NaN;
|
|
687
|
+
this.shoalingBakedZ = NaN;
|
|
688
|
+
this.nearshorePhaseBakedSignature = '';
|
|
689
|
+
this.breakerBuilt.x = NaN;
|
|
690
|
+
this.breakerBuilt.y = NaN;
|
|
691
|
+
this.breakerBuilt.z = NaN;
|
|
692
|
+
this.breakerBuilt.energy = NaN;
|
|
693
|
+
if (this.breakers) {
|
|
694
|
+
const staleBreakers = this.breakers;
|
|
695
|
+
this.remove(staleBreakers);
|
|
696
|
+
disposeAfterRenderBoundary(staleBreakers);
|
|
697
|
+
this.breakers = null;
|
|
698
|
+
}
|
|
699
|
+
if (sampler && bake) this.bakeShoalingDepths();
|
|
700
|
+
return this;
|
|
701
|
+
}
|
|
702
|
+
|
|
192
703
|
get waveEnergy() {
|
|
193
704
|
return this.material.userData.waterWaveEnergy ?? 0.3;
|
|
194
705
|
}
|
|
@@ -218,7 +729,14 @@ export class WaterSurface extends THREE.Mesh {
|
|
|
218
729
|
// Mirror of the vertex shader's shallow-water chop filter.
|
|
219
730
|
const chopWeight = THREE.MathUtils.lerp(
|
|
220
731
|
0.15, 1, THREE.MathUtils.smoothstep(restDepth, range * 0.3, range * 1.4));
|
|
221
|
-
const raw = sampleGerstnerHeight(
|
|
732
|
+
const raw = sampleGerstnerHeight(
|
|
733
|
+
this.gerstnerWaves,
|
|
734
|
+
x,
|
|
735
|
+
z,
|
|
736
|
+
this.time,
|
|
737
|
+
chopWeight,
|
|
738
|
+
this.sampleNearshoreAt(x, z, restDepth),
|
|
739
|
+
);
|
|
222
740
|
const deepFactor = THREE.MathUtils.smoothstep(restDepth, 0, range);
|
|
223
741
|
const rearUp = (1 - THREE.MathUtils.smoothstep(restDepth, range * 0.45, range * 1.5)) *
|
|
224
742
|
THREE.MathUtils.smoothstep(restDepth, 0.05, 0.35);
|
|
@@ -229,11 +747,41 @@ export class WaterSurface extends THREE.Mesh {
|
|
|
229
747
|
const brokenY = Math.max(
|
|
230
748
|
Math.min(targetY, capY) + Math.max(targetY - capY, 0) * 0.1,
|
|
231
749
|
-Math.max(restDepth, 0) + 0.04);
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
const
|
|
236
|
-
const
|
|
750
|
+
// Connected irregular swash mirror (keep in sync with water.js): event N
|
|
751
|
+
// begins at event N-1's rundown endpoint, reaches its own bounded peak,
|
|
752
|
+
// then drains to a new endpoint without resetting at the still-water line.
|
|
753
|
+
const explicit = (settings.runupDistance ?? 0) > 0.01;
|
|
754
|
+
const waves = this.gerstnerWaves;
|
|
755
|
+
const d = 0.5;
|
|
756
|
+
const gradX = (this.bedHeightSampler(x + d, z) - this.bedHeightSampler(x - d, z)) / (2 * d);
|
|
757
|
+
const gradZ = (this.bedHeightSampler(x, z + d) - this.bedHeightSampler(x, z - d)) / (2 * d);
|
|
758
|
+
const slope = Math.min(Math.hypot(gradX, gradZ), 1);
|
|
759
|
+
const safeSlope = Math.max(slope, 0.005);
|
|
760
|
+
const gradientLength = Math.hypot(gradX, gradZ) || 1;
|
|
761
|
+
const alongCoordinate = x * (gradZ / gradientLength) - z * (gradX / gradientLength);
|
|
762
|
+
const automaticDistance = (settings.shorelineRunup ?? 0.6) * energy / safeSlope;
|
|
763
|
+
const maximumDistance = explicit ? settings.runupDistance : automaticDistance;
|
|
764
|
+
const swashFrame = this.sampleSwashFrame(this.time, maximumDistance);
|
|
765
|
+
const swashProgress = swashFrame.progress;
|
|
766
|
+
const edgeDistance = explicit
|
|
767
|
+
? swashFrame.edgeDistance
|
|
768
|
+
: maximumDistance * swashProgress;
|
|
769
|
+
const edgeOffset = sampleSwashEdgeOffset(
|
|
770
|
+
alongCoordinate,
|
|
771
|
+
this.time,
|
|
772
|
+
swashProgress,
|
|
773
|
+
swashFrame.primaryDirectionX ?? waves[0]?.dirX ?? settings.flowDirection?.[0] ?? 0,
|
|
774
|
+
swashFrame.cycle,
|
|
775
|
+
swashFrame.edgeShape,
|
|
776
|
+
);
|
|
777
|
+
const edgeHead = edgeDistance * safeSlope + edgeOffset * safeSlope;
|
|
778
|
+
const filmHead = restDepth + edgeHead;
|
|
779
|
+
const film = THREE.MathUtils.clamp(filmHead * 0.45 + 0.008, 0.003, 0.045);
|
|
780
|
+
const filmY = Math.min(
|
|
781
|
+
(bed - surfaceY) + film,
|
|
782
|
+
Math.max(0.5, maximumDistance * safeSlope * 1.1),
|
|
783
|
+
);
|
|
784
|
+
const beach = 1 - THREE.MathUtils.smoothstep(restDepth, 0.06, 0.22);
|
|
237
785
|
const base = THREE.MathUtils.lerp(brokenY, filmY, beach);
|
|
238
786
|
|
|
239
787
|
// Breakers are physical: blend the traveling shell face over the base
|
|
@@ -249,15 +797,68 @@ export class WaterSurface extends THREE.Mesh {
|
|
|
249
797
|
return base;
|
|
250
798
|
}
|
|
251
799
|
|
|
252
|
-
// Horizontal water push velocity (m/s, world space)
|
|
253
|
-
//
|
|
254
|
-
//
|
|
255
|
-
// Feed it to floating bodies and swimmers so surf actually carries them.
|
|
800
|
+
// Horizontal water push velocity (m/s, world space). Breaker shells provide
|
|
801
|
+
// their traveling face flow; explicit swash adds uphill uprush and downhill
|
|
802
|
+
// backwash so gameplay objects move with the rendered shoreline.
|
|
256
803
|
getFlowAt(x, z, out = new THREE.Vector2()) {
|
|
257
804
|
out.set(0, 0);
|
|
805
|
+
if (this.currentField) {
|
|
806
|
+
this.currentField.sampleAt(x, z, currentSampleScratch);
|
|
807
|
+
out.add(currentSampleScratch);
|
|
808
|
+
}
|
|
258
809
|
if (this.breakers) {
|
|
259
810
|
const shell = this.breakers.sampleAt(x, z, breakerSampleScratch);
|
|
260
|
-
out.
|
|
811
|
+
out.x += shell.flowX;
|
|
812
|
+
out.y += shell.flowZ;
|
|
813
|
+
}
|
|
814
|
+
if (this.bedHeightSampler && (this.settings.runupDistance ?? 0) > 0.01) {
|
|
815
|
+
this.getWorldPosition(worldPositionScratch);
|
|
816
|
+
const surfaceY = worldPositionScratch.y;
|
|
817
|
+
const bed = this.bedHeightSampler(x, z);
|
|
818
|
+
const restDepth = surfaceY - bed;
|
|
819
|
+
const d = 0.5;
|
|
820
|
+
const gradX = (this.bedHeightSampler(x + d, z) - this.bedHeightSampler(x - d, z)) / (2 * d);
|
|
821
|
+
const gradZ = (this.bedHeightSampler(x, z + d) - this.bedHeightSampler(x, z - d)) / (2 * d);
|
|
822
|
+
const slope = Math.max(Math.min(Math.hypot(gradX, gradZ), 1), 0.005);
|
|
823
|
+
const gradientLength = Math.hypot(gradX, gradZ) || 1;
|
|
824
|
+
const alongCoordinate = x * (gradZ / gradientLength) - z * (gradX / gradientLength);
|
|
825
|
+
const swashFrame = this.sampleSwashFrame(this.time, this.settings.runupDistance);
|
|
826
|
+
const progress = swashFrame.progress;
|
|
827
|
+
const offset = sampleSwashEdgeOffset(
|
|
828
|
+
alongCoordinate,
|
|
829
|
+
this.time,
|
|
830
|
+
progress,
|
|
831
|
+
swashFrame.primaryDirectionX ?? this.gerstnerWaves[0]?.dirX ?? 0,
|
|
832
|
+
swashFrame.cycle,
|
|
833
|
+
swashFrame.edgeShape,
|
|
834
|
+
);
|
|
835
|
+
const maximumDistance = this.settings.runupDistance;
|
|
836
|
+
const edgeDistance = swashFrame.edgeDistance;
|
|
837
|
+
const head = edgeDistance * slope + offset * slope;
|
|
838
|
+
const filmHead = restDepth + head;
|
|
839
|
+
const swashZone = 1 - THREE.MathUtils.smoothstep(restDepth, 0.06, 0.22);
|
|
840
|
+
const wet = THREE.MathUtils.smoothstep(filmHead, -0.006, 0.025);
|
|
841
|
+
if (swashZone > 0.001 && wet > 0.001) {
|
|
842
|
+
const dt = 0.03;
|
|
843
|
+
const totalEdgeAt = (sampleTime) => {
|
|
844
|
+
const frame = this.sampleSwashFrame(sampleTime, maximumDistance);
|
|
845
|
+
return frame.edgeDistance + sampleSwashEdgeOffset(
|
|
846
|
+
alongCoordinate,
|
|
847
|
+
sampleTime,
|
|
848
|
+
frame.progress,
|
|
849
|
+
frame.primaryDirectionX ?? this.gerstnerWaves[0]?.dirX ?? 0,
|
|
850
|
+
frame.cycle,
|
|
851
|
+
frame.edgeShape,
|
|
852
|
+
);
|
|
853
|
+
};
|
|
854
|
+
const before = totalEdgeAt(this.time - dt);
|
|
855
|
+
const after = totalEdgeAt(this.time + dt);
|
|
856
|
+
const edgeSpeed = THREE.MathUtils.clamp(
|
|
857
|
+
(after - before) / (2 * dt), -3.5, 3.5,
|
|
858
|
+
);
|
|
859
|
+
out.x += (gradX / gradientLength) * edgeSpeed * swashZone * wet;
|
|
860
|
+
out.y += (gradZ / gradientLength) * edgeSpeed * swashZone * wet;
|
|
861
|
+
}
|
|
261
862
|
}
|
|
262
863
|
return out;
|
|
263
864
|
}
|
|
@@ -271,7 +872,7 @@ export class WaterSurface extends THREE.Mesh {
|
|
|
271
872
|
const range = Math.max(
|
|
272
873
|
this.settings.shoalingDepth ?? 1.4, Math.max(this.waveEnergy, 1e-3) * 2.2, 1e-3);
|
|
273
874
|
const deepFactor = THREE.MathUtils.smoothstep(restDepth, 0, range);
|
|
274
|
-
const beach = 1 - THREE.MathUtils.smoothstep(restDepth,
|
|
875
|
+
const beach = 1 - THREE.MathUtils.smoothstep(restDepth, 0.06, 0.22);
|
|
275
876
|
return THREE.MathUtils.lerp(this.settings.shorelineWaves ?? 0.35, 1, deepFactor) *
|
|
276
877
|
(1 - beach);
|
|
277
878
|
}
|
|
@@ -282,23 +883,129 @@ export class WaterSurface extends THREE.Mesh {
|
|
|
282
883
|
bakeShoalingDepths() {
|
|
283
884
|
if (!this.bedHeightSampler) return this;
|
|
284
885
|
this.updateWorldMatrix(true, false);
|
|
886
|
+
this.getWorldPosition(worldPositionScratch);
|
|
887
|
+
const surfaceX = worldPositionScratch.x;
|
|
888
|
+
const surfaceY = worldPositionScratch.y;
|
|
889
|
+
const surfaceZ = worldPositionScratch.z;
|
|
285
890
|
const positionAttr = this.geometry.attributes.position;
|
|
286
891
|
const existing = this.geometry.attributes.aBedHeight;
|
|
892
|
+
const existingSlope = this.geometry.attributes.aBedSlope;
|
|
893
|
+
const existingGradient = this.geometry.attributes.aBedGradient;
|
|
287
894
|
const bedHeights = existing?.count === positionAttr.count
|
|
288
895
|
? existing.array
|
|
289
896
|
: new Float32Array(positionAttr.count);
|
|
897
|
+
const bedSlopes = existingSlope?.count === positionAttr.count
|
|
898
|
+
? existingSlope.array
|
|
899
|
+
: new Float32Array(positionAttr.count);
|
|
900
|
+
const bedGradients = existingGradient?.count === positionAttr.count
|
|
901
|
+
? existingGradient.array
|
|
902
|
+
: new Float32Array(positionAttr.count * 2);
|
|
903
|
+
const useDedicatedBreakers = shouldUseDedicatedBreakerShell(
|
|
904
|
+
this.settings,
|
|
905
|
+
Boolean(this.bedHeightSampler),
|
|
906
|
+
);
|
|
907
|
+
const solveNearshore = this.nearshorePhaseEnabled && !useDedicatedBreakers;
|
|
908
|
+
const restDepths = solveNearshore ? new Float32Array(positionAttr.count) : null;
|
|
909
|
+
const d = 0.5;
|
|
290
910
|
for (let i = 0; i < positionAttr.count; i += 1) {
|
|
291
911
|
localScratch.fromBufferAttribute(positionAttr, i).applyMatrix4(this.matrixWorld);
|
|
292
|
-
|
|
912
|
+
const x = localScratch.x;
|
|
913
|
+
const z = localScratch.z;
|
|
914
|
+
bedHeights[i] = this.bedHeightSampler(x, z);
|
|
915
|
+
if (restDepths) restDepths[i] = surfaceY - bedHeights[i];
|
|
916
|
+
// Local bed gradient magnitude — converts horizontal run-up meters
|
|
917
|
+
// (runupDistance) into the vertical swash reach at this vertex.
|
|
918
|
+
const gradX = (this.bedHeightSampler(x + d, z) - this.bedHeightSampler(x - d, z)) / (2 * d);
|
|
919
|
+
const gradZ = (this.bedHeightSampler(x, z + d) - this.bedHeightSampler(x, z - d)) / (2 * d);
|
|
920
|
+
bedSlopes[i] = Math.min(Math.hypot(gradX, gradZ), 1);
|
|
921
|
+
bedGradients[i * 2] = gradX;
|
|
922
|
+
bedGradients[i * 2 + 1] = gradZ;
|
|
293
923
|
}
|
|
294
924
|
if (existing?.array === bedHeights) {
|
|
295
925
|
existing.needsUpdate = true;
|
|
296
926
|
} else {
|
|
297
927
|
this.geometry.setAttribute('aBedHeight', new THREE.BufferAttribute(bedHeights, 1));
|
|
298
928
|
}
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
929
|
+
if (existingSlope?.array === bedSlopes) {
|
|
930
|
+
existingSlope.needsUpdate = true;
|
|
931
|
+
} else {
|
|
932
|
+
this.geometry.setAttribute('aBedSlope', new THREE.BufferAttribute(bedSlopes, 1));
|
|
933
|
+
}
|
|
934
|
+
if (existingGradient?.array === bedGradients) {
|
|
935
|
+
existingGradient.needsUpdate = true;
|
|
936
|
+
} else {
|
|
937
|
+
this.geometry.setAttribute('aBedGradient', new THREE.BufferAttribute(bedGradients, 2));
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
let phaseField = null;
|
|
941
|
+
let phaseStatus;
|
|
942
|
+
if (!this.nearshorePhaseEnabled) {
|
|
943
|
+
phaseStatus = { active: false, reason: 'disabled' };
|
|
944
|
+
} else if (useDedicatedBreakers) {
|
|
945
|
+
// The separate overhanging shell still evaluates its original plane
|
|
946
|
+
// phase. Until it can consume the same atlas, retain exact coherence by
|
|
947
|
+
// falling the heightfield back rather than showing two crest clocks.
|
|
948
|
+
phaseStatus = { active: false, reason: 'dedicated-breaker-shell' };
|
|
949
|
+
} else if (
|
|
950
|
+
(this.segmentsX + 1) * (this.segmentsZ + 1) !== positionAttr.count
|
|
951
|
+
) {
|
|
952
|
+
phaseStatus = { active: false, reason: 'unexpected-geometry-grid' };
|
|
953
|
+
} else {
|
|
954
|
+
const frame = this.dominantNearshoreFrame();
|
|
955
|
+
const options = this.nearshorePhaseOptions ?? {};
|
|
956
|
+
try {
|
|
957
|
+
const candidate = buildNearshorePhaseField({
|
|
958
|
+
restDepths,
|
|
959
|
+
columns: this.segmentsX + 1,
|
|
960
|
+
rows: this.segmentsZ + 1,
|
|
961
|
+
originX: surfaceX - this.width * 0.5,
|
|
962
|
+
originZ: surfaceZ - this.depth * 0.5,
|
|
963
|
+
stepX: this.width / this.segmentsX,
|
|
964
|
+
stepZ: this.depth / this.segmentsZ,
|
|
965
|
+
directionX: frame.directionX,
|
|
966
|
+
directionZ: frame.directionZ,
|
|
967
|
+
deepWaveNumber: frame.deepWaveNumber,
|
|
968
|
+
incidentAxis: options.incidentAxis,
|
|
969
|
+
minDepth: options.minDepth ?? 0.05,
|
|
970
|
+
maxWavenumberRatio: options.maxWavenumberRatio ?? 4,
|
|
971
|
+
});
|
|
972
|
+
// A one-way eikonal solve cannot represent turning rays, diffraction,
|
|
973
|
+
// or shadow zones. Invalidity propagates downstream in the builder;
|
|
974
|
+
// rejecting the complete atlas avoids a phase seam where a fallback
|
|
975
|
+
// cell would otherwise meet a refracted cell.
|
|
976
|
+
if (candidate.invalidCount === 0) {
|
|
977
|
+
candidate.slotMask = frame.slotMask;
|
|
978
|
+
phaseField = candidate;
|
|
979
|
+
phaseStatus = {
|
|
980
|
+
active: true,
|
|
981
|
+
reason: 'active',
|
|
982
|
+
incidentAxis: candidate.incidentAxis,
|
|
983
|
+
invalidFraction: 0,
|
|
984
|
+
slotMask: candidate.slotMask,
|
|
985
|
+
};
|
|
986
|
+
} else {
|
|
987
|
+
phaseStatus = {
|
|
988
|
+
active: false,
|
|
989
|
+
reason: 'invalid-mild-slope-field',
|
|
990
|
+
incidentAxis: candidate.incidentAxis,
|
|
991
|
+
invalidFraction: candidate.invalidFraction,
|
|
992
|
+
};
|
|
993
|
+
}
|
|
994
|
+
} catch (error) {
|
|
995
|
+
phaseStatus = {
|
|
996
|
+
active: false,
|
|
997
|
+
reason: 'phase-bake-error',
|
|
998
|
+
error: error instanceof Error ? error.message : String(error),
|
|
999
|
+
};
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
this.nearshorePhaseField = phaseField;
|
|
1003
|
+
this.nearshorePhaseStatus = phaseStatus;
|
|
1004
|
+
this.writeNearshorePhaseAttribute(phaseField);
|
|
1005
|
+
this.nearshorePhaseBakedSignature = this.nearshorePhaseSignature();
|
|
1006
|
+
this.shoalingBakedX = surfaceX;
|
|
1007
|
+
this.shoalingBakedY = surfaceY;
|
|
1008
|
+
this.shoalingBakedZ = surfaceZ;
|
|
302
1009
|
return this;
|
|
303
1010
|
}
|
|
304
1011
|
|
|
@@ -306,8 +1013,7 @@ export class WaterSurface extends THREE.Mesh {
|
|
|
306
1013
|
// breakerAmount setting and rebuilds the break line whenever the surface
|
|
307
1014
|
// moves or the swell energy shifts enough to relocate the collapse depth.
|
|
308
1015
|
updateBreakers() {
|
|
309
|
-
const enabled = this.settings.
|
|
310
|
-
this.bedHeightSampler && (this.settings.breakerAmount ?? 0) > 0.001;
|
|
1016
|
+
const enabled = shouldUseDedicatedBreakerShell(this.settings, Boolean(this.bedHeightSampler));
|
|
311
1017
|
if (!enabled) {
|
|
312
1018
|
if (this.breakers) {
|
|
313
1019
|
this.remove(this.breakers);
|
|
@@ -422,8 +1128,18 @@ export class WaterSurface extends THREE.Mesh {
|
|
|
422
1128
|
if (this.bedHeightSampler) {
|
|
423
1129
|
this.getWorldPosition(worldPositionScratch);
|
|
424
1130
|
const movedX = Math.abs(worldPositionScratch.x - this.shoalingBakedX);
|
|
1131
|
+
const phaseUsesDepth = this.nearshorePhaseEnabled && !shouldUseDedicatedBreakerShell(
|
|
1132
|
+
this.settings,
|
|
1133
|
+
Boolean(this.bedHeightSampler),
|
|
1134
|
+
);
|
|
1135
|
+
const movedY = phaseUsesDepth
|
|
1136
|
+
? Math.abs(worldPositionScratch.y - this.shoalingBakedY)
|
|
1137
|
+
: 0;
|
|
425
1138
|
const movedZ = Math.abs(worldPositionScratch.z - this.shoalingBakedZ);
|
|
426
|
-
|
|
1139
|
+
const phaseChanged = this.nearshorePhaseBakedSignature !== this.nearshorePhaseSignature();
|
|
1140
|
+
if (!(movedX <= 0.05 && movedY <= 0.02 && movedZ <= 0.05) || phaseChanged) {
|
|
1141
|
+
this.bakeShoalingDepths();
|
|
1142
|
+
}
|
|
427
1143
|
}
|
|
428
1144
|
this.updateBreakers();
|
|
429
1145
|
|
|
@@ -460,7 +1176,37 @@ export class WaterSurface extends THREE.Mesh {
|
|
|
460
1176
|
}
|
|
461
1177
|
|
|
462
1178
|
const uniforms = this.material.uniforms;
|
|
1179
|
+
const swashFrame = this.sampleSwashFrame(this.time, this.settings.runupDistance);
|
|
1180
|
+
uniforms.uSwashCycle.value = swashFrame.cycle;
|
|
1181
|
+
uniforms.uSwashProgress.value = swashFrame.progress;
|
|
1182
|
+
uniforms.uSwashIncidenceX.value = swashFrame.primaryDirectionX;
|
|
1183
|
+
uniforms.uSwashEdgeShape.value.set(
|
|
1184
|
+
swashFrame.edgeShape.phase,
|
|
1185
|
+
swashFrame.edgeShape.frequency,
|
|
1186
|
+
swashFrame.edgeShape.amplitude,
|
|
1187
|
+
);
|
|
1188
|
+
uniforms.uSwashRunupScale.value = (this.settings.runupDistance ?? 0) > 0.01
|
|
1189
|
+
? swashFrame.runupScale
|
|
1190
|
+
: 1;
|
|
1191
|
+
uniforms.uSwashStartOffset.value = (this.settings.runupDistance ?? 0) > 0.01
|
|
1192
|
+
? swashFrame.startOffset
|
|
1193
|
+
: 0;
|
|
1194
|
+
uniforms.uSwashEndOffset.value = (this.settings.runupDistance ?? 0) > 0.01
|
|
1195
|
+
? swashFrame.endOffset
|
|
1196
|
+
: 0;
|
|
463
1197
|
uniforms.uTime.value = this.time;
|
|
1198
|
+
if (this.shoreState) {
|
|
1199
|
+
this.getWorldPosition(worldPositionScratch);
|
|
1200
|
+
this.shoreState.update(renderer, {
|
|
1201
|
+
time: this.time,
|
|
1202
|
+
waterLevel: worldPositionScratch.y,
|
|
1203
|
+
waves: this.gerstnerWaves,
|
|
1204
|
+
settings: this.settings,
|
|
1205
|
+
waveEnergy: this.waveEnergy,
|
|
1206
|
+
swashFrame,
|
|
1207
|
+
}, clampedDelta);
|
|
1208
|
+
this.bindShoreState();
|
|
1209
|
+
}
|
|
464
1210
|
// Mirror scene.fog so distant water hazes with the rest of the world
|
|
465
1211
|
// (the material handles fog manually — see uSceneFog* in the shader;
|
|
466
1212
|
// setDistanceFog adds the environment-matching exponential layer).
|
|
@@ -473,8 +1219,28 @@ export class WaterSurface extends THREE.Mesh {
|
|
|
473
1219
|
}
|
|
474
1220
|
updateWaterMaterialCamera(this.material, renderer, camera);
|
|
475
1221
|
this.getWorldPosition(worldPositionScratch);
|
|
476
|
-
|
|
477
|
-
|
|
1222
|
+
const cameraBelow = camera.getWorldPosition(followScratch).y < worldPositionScratch.y;
|
|
1223
|
+
uniforms.uCameraBelow.value = cameraBelow ? 1 : 0;
|
|
1224
|
+
const renderedSettings = this.renderedSettings ?? this.settings;
|
|
1225
|
+
updateProjectedWaterCaustics({
|
|
1226
|
+
enabled: cameraBelow && renderedSettings.causticsStrength > 0.001,
|
|
1227
|
+
time: this.time,
|
|
1228
|
+
waterLevel: worldPositionScratch.y,
|
|
1229
|
+
centerX: worldPositionScratch.x,
|
|
1230
|
+
centerZ: worldPositionScratch.z,
|
|
1231
|
+
halfWidth: this.width * 0.5,
|
|
1232
|
+
halfDepth: this.depth * 0.5,
|
|
1233
|
+
color: renderedSettings.sunColor,
|
|
1234
|
+
intensity: renderedSettings.causticsStrength * 0.65,
|
|
1235
|
+
scale: renderedSettings.causticsScale,
|
|
1236
|
+
speed: renderedSettings.causticsSpeed,
|
|
1237
|
+
flowDirection: renderedSettings.flowDirection,
|
|
1238
|
+
waveDistortion: 0.035 + renderedSettings.detailNormalStrength * 0.08,
|
|
1239
|
+
depthAttenuation: 1 / Math.max(
|
|
1240
|
+
renderedSettings.depthFadeDistance + renderedSettings.deepFadeDistance,
|
|
1241
|
+
0.25,
|
|
1242
|
+
),
|
|
1243
|
+
});
|
|
478
1244
|
if (this.ripples) {
|
|
479
1245
|
uniforms.uRippleMap.value = this.ripples.texture;
|
|
480
1246
|
uniforms.uUseRippleMap.value = 1;
|
|
@@ -495,6 +1261,9 @@ export class WaterSurface extends THREE.Mesh {
|
|
|
495
1261
|
this.geometry.dispose();
|
|
496
1262
|
this.material.dispose();
|
|
497
1263
|
this.ripples?.dispose();
|
|
1264
|
+
this.shoreState?.dispose();
|
|
1265
|
+
if (this.ownsCurrentField) this.currentField?.dispose();
|
|
1266
|
+
this.shoreStateMaterials.clear();
|
|
498
1267
|
this.splashSystem?.dispose();
|
|
499
1268
|
this.breakers?.dispose();
|
|
500
1269
|
this.passes?.dispose();
|