@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
package/src/shaders-tsl/water.js
CHANGED
|
@@ -45,9 +45,12 @@ import {
|
|
|
45
45
|
mix,
|
|
46
46
|
modelWorldMatrix,
|
|
47
47
|
normalize,
|
|
48
|
+
pow,
|
|
48
49
|
positionGeometry,
|
|
50
|
+
refract,
|
|
49
51
|
screenUV,
|
|
50
52
|
select,
|
|
53
|
+
sin,
|
|
51
54
|
smoothstep,
|
|
52
55
|
texture,
|
|
53
56
|
uniform,
|
|
@@ -61,11 +64,16 @@ import { NodeMaterial } from 'three/webgpu';
|
|
|
61
64
|
|
|
62
65
|
import { sampleEnvironmentSunShadow } from './chunks/environment-sun-shadow.js';
|
|
63
66
|
import { stylizedCloudShadow } from './chunks/stylized-cloud-shadow.js';
|
|
64
|
-
import {
|
|
67
|
+
import {
|
|
68
|
+
waterCombineNormal,
|
|
69
|
+
waterFbm,
|
|
70
|
+
waterRotate2d,
|
|
71
|
+
} from './chunks/water-common.js';
|
|
65
72
|
import { createWaterColorChunk } from './chunks/water-color.js';
|
|
66
73
|
import { createWaterFoamChunk } from './chunks/water-foam.js';
|
|
67
74
|
import { createWaterLightingChunk } from './chunks/water-lighting.js';
|
|
68
75
|
import { createWaterRippleChunk } from './chunks/water-ripple.js';
|
|
76
|
+
import { createWaterShoreStateChunk } from './chunks/water-shore-state.js';
|
|
69
77
|
import { createWaterWavesChunk } from './chunks/water-waves.js';
|
|
70
78
|
|
|
71
79
|
// Node-backend placeholders: same roles as waterMaterial.js's placeholders,
|
|
@@ -86,6 +94,7 @@ function getPlaceholders() {
|
|
|
86
94
|
reflection: createLinearPlaceholder([126, 190, 218, 255]),
|
|
87
95
|
ripple: createLinearPlaceholder([128, 128, 128, 255]),
|
|
88
96
|
sceneColor: createLinearPlaceholder([80, 150, 190, 255]),
|
|
97
|
+
shoreState: createLinearPlaceholder([0, 0, 0, 0]),
|
|
89
98
|
};
|
|
90
99
|
}
|
|
91
100
|
return placeholders;
|
|
@@ -179,9 +188,26 @@ export function createWaterNodeMaterial({
|
|
|
179
188
|
uRippleTexel: uniform(new THREE.Vector2(1 / 256, 1 / 256)),
|
|
180
189
|
uRippleHeightScale: uniform(1),
|
|
181
190
|
|
|
191
|
+
uShoreStateMap: texture(holders.shoreState),
|
|
192
|
+
uUseShoreState: uniform(0),
|
|
193
|
+
uShoreStateRegion: uniform(new THREE.Vector4(0, 0, 1, 1)),
|
|
194
|
+
|
|
182
195
|
uShoalingDepth: uniform(1.4),
|
|
183
196
|
uShorelineWaves: uniform(0.35),
|
|
184
197
|
uShorelineRunup: uniform(0.6),
|
|
198
|
+
uRunupDistance: uniform(0),
|
|
199
|
+
// Runtime event envelope supplied by WaterSurface. Keeping the irregular
|
|
200
|
+
// sequence on the CPU avoids expanding the already-dense WebGPU graph.
|
|
201
|
+
uSwashRunupScale: uniform(1),
|
|
202
|
+
uSwashStartOffset: uniform(0),
|
|
203
|
+
uSwashEndOffset: uniform(0),
|
|
204
|
+
uSwashCycle: uniform(0),
|
|
205
|
+
uSwashProgress: uniform(0),
|
|
206
|
+
uSwashIncidenceX: uniform(0),
|
|
207
|
+
// (phase radians, alongshore frequency rad/m, amplitude metres).
|
|
208
|
+
uSwashEdgeShape: uniform(new THREE.Vector3(0, 0.1, 0)),
|
|
209
|
+
uBreakerEnabled: uniform(1),
|
|
210
|
+
uBreakerAmount: uniform(0.5),
|
|
185
211
|
uWaveEnergy: uniform(0.3),
|
|
186
212
|
|
|
187
213
|
uWavesA: waterArrayUniformEntry(wavesANode),
|
|
@@ -199,12 +225,16 @@ export function createWaterNodeMaterial({
|
|
|
199
225
|
uDeepFadeDistance: uniform(3),
|
|
200
226
|
uOpacity: uniform(0.9),
|
|
201
227
|
uRefractionStrength: uniform(0.6),
|
|
228
|
+
uIndexOfRefraction: uniform(1.333),
|
|
229
|
+
uUnderwaterTransmission: uniform(1.0),
|
|
230
|
+
uUnderwaterTintStrength: uniform(0.35),
|
|
202
231
|
uCausticsStrength: uniform(0.6),
|
|
203
232
|
uCausticsScale: uniform(1),
|
|
204
233
|
uCausticsSpeed: uniform(1),
|
|
205
234
|
|
|
206
235
|
uFoamColor: uniform(new THREE.Color()),
|
|
207
236
|
uFoamAmount: uniform(1),
|
|
237
|
+
uSwashFoamAmount: uniform(1.15),
|
|
208
238
|
uFoamContactDistance: uniform(1),
|
|
209
239
|
uFoamLineSpacing: uniform(1.4),
|
|
210
240
|
uFoamNoiseScale: uniform(0.6),
|
|
@@ -240,6 +270,7 @@ export function createWaterNodeMaterial({
|
|
|
240
270
|
|
|
241
271
|
const waves = createWaterWavesChunk({ wavesA: wavesANode, wavesB: wavesBNode, waveCount });
|
|
242
272
|
const ripple = createWaterRippleChunk({ u });
|
|
273
|
+
const shoreState = createWaterShoreStateChunk({ u });
|
|
243
274
|
const color = createWaterColorChunk({ u, flags });
|
|
244
275
|
const foam = createWaterFoamChunk({ u, foamOctaves });
|
|
245
276
|
const lighting = createWaterLightingChunk({ u });
|
|
@@ -257,10 +288,17 @@ export function createWaterNodeMaterial({
|
|
|
257
288
|
const vWorldPosition = varying(vec3(), 'vWaterWorldPosition');
|
|
258
289
|
const vRestWorldXZ = varying(vec2(), 'vWaterRestWorldXZ');
|
|
259
290
|
const vCrest = varying(float(), 'vWaterCrest');
|
|
291
|
+
const vGerstnerNormal = varying(vec3(), 'vWaterGerstnerNormal');
|
|
260
292
|
const vShoal = varying(float(), 'vWaterShoal');
|
|
261
293
|
const vBreaker = varying(float(), 'vWaterBreaker');
|
|
262
|
-
|
|
263
|
-
|
|
294
|
+
// The connected wet/dry geometry is evaluated per vertex, but foam noise
|
|
295
|
+
// is evaluated per fragment. Passing only these smooth physical inputs
|
|
296
|
+
// avoids exposing the water grid as large triangular white patches.
|
|
297
|
+
const vSwashFoamData = varying(vec2(), 'vWaterSwashFoamData');
|
|
298
|
+
// Small shore-normal foam drift. This never displaces geometry: amplifying
|
|
299
|
+
// Gerstner's horizontal orbit folded the near-shore grid into white wedges.
|
|
300
|
+
const vSwashSurge = varying(vec2(), 'vWaterSwashSurge');
|
|
301
|
+
const vSwashZone = varying(float(), 'vWaterSwashZone');
|
|
264
302
|
// Rest-pose column depth (waterline − bed). Deeply negative = the surface
|
|
265
303
|
// is under dry land; the fragment shader discards those fragments so
|
|
266
304
|
// triangles spanning a cliff face can't poke through the terrain.
|
|
@@ -277,9 +315,9 @@ export function createWaterNodeMaterial({
|
|
|
277
315
|
const shoal = float(1.0).toVar();
|
|
278
316
|
const rippleShoal = float(1.0).toVar();
|
|
279
317
|
const breakFoam = float(0.0).toVar();
|
|
280
|
-
const swash = float(0.0).toVar();
|
|
281
318
|
|
|
282
319
|
let rawWave;
|
|
320
|
+
let nearshore = null;
|
|
283
321
|
const waveDisplacement = vec3(0.0).toVar();
|
|
284
322
|
if (flags.shoaling) {
|
|
285
323
|
const aBedHeight = attribute('aBedHeight', 'float');
|
|
@@ -289,7 +327,30 @@ export function createWaterNodeMaterial({
|
|
|
289
327
|
const shoalRange = max(max(u.uShoalingDepth, u.uWaveEnergy.mul(2.2)), 1e-3).toVar();
|
|
290
328
|
chopWeight.assign(mix(0.15, 1.0, smoothstep(shoalRange.mul(0.3), shoalRange.mul(1.4), restDepth)));
|
|
291
329
|
|
|
292
|
-
|
|
330
|
+
// Packed CPU-baked mild-slope field: q, dq/dx, dq/dz, slot mask. It is fully
|
|
331
|
+
// active through the breaker/swash handoff, fades back to the original
|
|
332
|
+
// plane phase over a broad deep-water band, and only affects the two
|
|
333
|
+
// dominant swell slots. The attribute is a plane-wave fallback on
|
|
334
|
+
// stages where the nearshore solve is disabled or invalid.
|
|
335
|
+
const aNearshorePhase = attribute('aNearshorePhase', 'vec4').toVar();
|
|
336
|
+
const nearshoreBlend = smoothstep(
|
|
337
|
+
shoalRange.mul(0.8),
|
|
338
|
+
shoalRange.mul(1.8),
|
|
339
|
+
restDepth,
|
|
340
|
+
).oneMinus().toVar();
|
|
341
|
+
nearshore = {
|
|
342
|
+
phaseCoordinate: aNearshorePhase.x,
|
|
343
|
+
waveVector: aNearshorePhase.yz,
|
|
344
|
+
blend: nearshoreBlend,
|
|
345
|
+
slotMask: aNearshorePhase.w,
|
|
346
|
+
};
|
|
347
|
+
|
|
348
|
+
rawWave = waves.gerstnerDisplacementFiltered(
|
|
349
|
+
restXZ,
|
|
350
|
+
u.uTime,
|
|
351
|
+
chopWeight,
|
|
352
|
+
nearshore,
|
|
353
|
+
).toVar();
|
|
293
354
|
|
|
294
355
|
// Stylized breaking-wave shoaling (see water.vert.glsl for the model).
|
|
295
356
|
const deepFactor = smoothstep(0.0, shoalRange, restDepth);
|
|
@@ -305,12 +366,99 @@ export function createWaterNodeMaterial({
|
|
|
305
366
|
const brokenY = min(targetY, capY).add(capExcess.mul(0.1)).toVar();
|
|
306
367
|
// A trough can never dip below the seabed.
|
|
307
368
|
brokenY.assign(max(brokenY, max(restDepth, 0.0).negate().add(0.04)));
|
|
308
|
-
breakFoam.assign(
|
|
369
|
+
breakFoam.assign(
|
|
370
|
+
clamp(capExcess.mul(0.9).div(max(u.uWaveEnergy.mul(0.3), 1e-3)), 0.0, 1.0)
|
|
371
|
+
.mul(u.uBreakerAmount)
|
|
372
|
+
.mul(u.uBreakerEnabled),
|
|
373
|
+
);
|
|
309
374
|
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
//
|
|
313
|
-
|
|
375
|
+
// The surf hands into one connected swash event. The dominant crest's
|
|
376
|
+
// shoreline arrival starts a quick uprush; gravity then owns the longer
|
|
377
|
+
// drain. Offshore Gerstner phase still sets the visible incidence angle,
|
|
378
|
+
// while the wet/dry edge remains one inequality instead of many islands.
|
|
379
|
+
const aBedSlope = attribute('aBedSlope', 'float');
|
|
380
|
+
const aBedGradient = attribute('aBedGradient', 'vec2').toVar();
|
|
381
|
+
const bedSlope = max(aBedSlope, 0.005).toVar();
|
|
382
|
+
const shoreDirection = aBedGradient.div(max(length(aBedGradient), 0.005)).toVar();
|
|
383
|
+
const alongDirection = vec2(shoreDirection.y, shoreDirection.x.negate()).toVar();
|
|
384
|
+
const alongCoordinate = dot(restXZ, alongDirection).toVar();
|
|
385
|
+
// WaterSurface computes this frame once on the CPU and shares it with
|
|
386
|
+
// the persistent foam/wetness pass. That keeps the visible lip, foam
|
|
387
|
+
// injection, and wet-sand history on the exact same event and removes a
|
|
388
|
+
// duplicate phase/easing stack from this already-large shader.
|
|
389
|
+
const swashCycle = u.uSwashCycle.toVar();
|
|
390
|
+
const swashProgress = u.uSwashProgress.toVar();
|
|
391
|
+
|
|
392
|
+
// The user setting is the maximum horizontal reach. Runtime event
|
|
393
|
+
// uniforms vary ordinary peaks over 80–100%, carry the previous rundown
|
|
394
|
+
// endpoint into the next uprush, and let each backwash stop at a slightly
|
|
395
|
+
// different point around the still-water shoreline.
|
|
396
|
+
const explicitRunup = u.uRunupDistance.greaterThan(0.01);
|
|
397
|
+
const maximumHorizontalReach = select(
|
|
398
|
+
explicitRunup,
|
|
399
|
+
u.uRunupDistance,
|
|
400
|
+
u.uShorelineRunup.mul(u.uWaveEnergy).div(bedSlope),
|
|
401
|
+
).toVar();
|
|
402
|
+
const runupDistance = maximumHorizontalReach
|
|
403
|
+
.mul(select(explicitRunup, u.uSwashRunupScale, 1.0)).toVar();
|
|
404
|
+
const startOffset = select(explicitRunup, u.uSwashStartOffset, 0.0).toVar();
|
|
405
|
+
const endOffset = select(explicitRunup, u.uSwashEndOffset, 0.0).toVar();
|
|
406
|
+
const edgeDistance = select(
|
|
407
|
+
swashCycle.lessThan(0.34),
|
|
408
|
+
mix(startOffset, runupDistance, swashProgress),
|
|
409
|
+
mix(endOffset, runupDistance, swashProgress),
|
|
410
|
+
).toVar();
|
|
411
|
+
const maximumSwashReach = maximumHorizontalReach.mul(bedSlope).toVar();
|
|
412
|
+
// Match the incoming oblique crest instead of marching the whole beach
|
|
413
|
+
// forward like a ruler. A broad tilt supplies the incidence angle; two
|
|
414
|
+
// small traveling scallops break up the lip without splitting it.
|
|
415
|
+
// Retain a modest angle even at nominal rest/full reach: an oblique
|
|
416
|
+
// crest reaches one end of a real beach first. Forcing every X column
|
|
417
|
+
// onto the same endpoint created the conspicuous horizontal ruler line.
|
|
418
|
+
const edgeEnvelope = mix(0.18, 1.0, sin(swashProgress.mul(Math.PI))).toVar();
|
|
419
|
+
const incidenceSlope = clamp(u.uSwashIncidenceX.mul(0.52), -0.2, 0.2).toVar();
|
|
420
|
+
const rawEdgeTilt = alongCoordinate.mul(incidenceSlope).negate().toVar();
|
|
421
|
+
// Bounded soft-sign: preserves the local crest angle without ever
|
|
422
|
+
// reaching a flat clamp. The former unbounded x*slope term reached
|
|
423
|
+
// ±22 m across this lab and pinned whole shoreline blocks to one cap.
|
|
424
|
+
const edgeTilt = rawEdgeTilt.div(abs(rawEdgeTilt).div(2.0).add(1.0)).toVar();
|
|
425
|
+
const edgeScallop = sin(alongCoordinate.mul(0.32).sub(u.uTime.mul(0.35)))
|
|
426
|
+
.sub(sin(u.uTime.mul(-0.35)))
|
|
427
|
+
.add(
|
|
428
|
+
sin(alongCoordinate.mul(0.91).add(u.uTime.mul(0.18)))
|
|
429
|
+
.sub(sin(u.uTime.mul(0.18)))
|
|
430
|
+
.mul(0.35),
|
|
431
|
+
)
|
|
432
|
+
.mul(0.4);
|
|
433
|
+
// Broad per-event tongues make the actual water silhouette irregular,
|
|
434
|
+
// not merely the foam painted behind it. The squared cycle envelope is
|
|
435
|
+
// zero with zero velocity at event boundaries, so shape seeds may vary
|
|
436
|
+
// each wave without a shoreline pop. Subtracting the x=0 samples keeps
|
|
437
|
+
// the calibrated 8-10 m centerline reach unchanged.
|
|
438
|
+
const macroWave = sin(swashCycle.mul(Math.PI)).toVar();
|
|
439
|
+
const secondaryPhase = u.uSwashEdgeShape.x.mul(-0.71).toVar();
|
|
440
|
+
const edgeMacro = sin(
|
|
441
|
+
alongCoordinate.mul(u.uSwashEdgeShape.y).add(u.uSwashEdgeShape.x),
|
|
442
|
+
).sub(sin(u.uSwashEdgeShape.x))
|
|
443
|
+
.add(
|
|
444
|
+
sin(
|
|
445
|
+
alongCoordinate.mul(u.uSwashEdgeShape.y).mul(2.35).add(secondaryPhase),
|
|
446
|
+
).sub(sin(secondaryPhase)).mul(0.42),
|
|
447
|
+
)
|
|
448
|
+
.mul(u.uSwashEdgeShape.z)
|
|
449
|
+
.mul(macroWave.mul(macroWave))
|
|
450
|
+
.toVar();
|
|
451
|
+
const edgeHead = edgeDistance.mul(bedSlope)
|
|
452
|
+
.add(
|
|
453
|
+
edgeTilt.add(edgeScallop).mul(edgeEnvelope)
|
|
454
|
+
.add(edgeMacro)
|
|
455
|
+
.mul(bedSlope),
|
|
456
|
+
).toVar();
|
|
457
|
+
const filmHead = restDepth.add(edgeHead).toVar();
|
|
458
|
+
// Behind the edge the sheet is centimetres deep, not a second full-body
|
|
459
|
+
// water plane. Dry vertices remain conformal just above the bed; the
|
|
460
|
+
// fragment-space signed head owns visibility, avoiding lifting rows.
|
|
461
|
+
const film = clamp(filmHead.mul(0.45).add(0.008), 0.003, 0.045).toVar();
|
|
314
462
|
// Cap the drape: the film follows the bed up the beach, but a steep
|
|
315
463
|
// bank (terraced shore, cliff base) puts a whole cliff step inside one
|
|
316
464
|
// mesh cell — the dry vertex would ride 10m+ up the slope and drag
|
|
@@ -318,18 +466,37 @@ export function createWaterNodeMaterial({
|
|
|
318
466
|
// wedges at distant shorelines). Real swash never climbs above ~½m
|
|
319
467
|
// over the rest waterline, so clamp the geometry there and let the
|
|
320
468
|
// vRestDepth fragment discard own the rest.
|
|
321
|
-
const filmY = min(
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
469
|
+
const filmY = min(
|
|
470
|
+
aBedHeight.sub(worldPosition.y).add(film),
|
|
471
|
+
max(0.5, maximumSwashReach.mul(1.1)),
|
|
472
|
+
);
|
|
473
|
+
// Hand off over the last ~4 m of the 1:20 profile. Deeper water stays
|
|
474
|
+
// the Gerstner surface, preventing the visible "three stacked systems"
|
|
475
|
+
// boundary that a reach-sized blend band created.
|
|
476
|
+
const beach = smoothstep(0.06, 0.22, restDepth).oneMinus().toVar();
|
|
477
|
+
vSwashZone.assign(beach);
|
|
478
|
+
|
|
479
|
+
rippleShoal.assign(smoothstep(0.0, 0.18, restDepth));
|
|
480
|
+
const foamWidth = max(bedSlope.mul(0.68), 0.018).toVar();
|
|
481
|
+
vSwashFoamData.assign(vec2(filmHead, foamWidth));
|
|
482
|
+
breakFoam.mulAssign(beach.oneMinus());
|
|
326
483
|
shoal.mulAssign(beach.oneMinus());
|
|
484
|
+
// Foam moves metres with the sheet, not a token 24 cm texture wobble.
|
|
485
|
+
// Continuous alongshore drift plus the non-resetting animation clock
|
|
486
|
+
// prevents successive drains retracing the exact same texture path.
|
|
487
|
+
const surgeDistance = min(runupDistance.mul(0.32), 3.2).toVar();
|
|
488
|
+
const swashDrift = shoreDirection.mul(swashProgress).mul(surgeDistance)
|
|
489
|
+
.add(alongDirection.mul(sin(u.uTime.mul(0.37))).mul(swashProgress).mul(0.55));
|
|
490
|
+
vSwashSurge.assign(swashDrift.mul(beach));
|
|
327
491
|
waveDisplacement.assign(vec3(
|
|
328
492
|
rawWave.x.mul(shoal),
|
|
329
493
|
mix(brokenY, filmY, beach),
|
|
330
494
|
rawWave.z.mul(shoal),
|
|
331
495
|
));
|
|
332
496
|
} else {
|
|
497
|
+
vSwashSurge.assign(vec2(0.0));
|
|
498
|
+
vSwashZone.assign(float(0.0));
|
|
499
|
+
vSwashFoamData.assign(vec2(-1.0, 0.018));
|
|
333
500
|
vRestDepth.assign(float(1000.0));
|
|
334
501
|
rawWave = waves.gerstnerDisplacementFiltered(restXZ, u.uTime, chopWeight).toVar();
|
|
335
502
|
waveDisplacement.assign(rawWave);
|
|
@@ -341,12 +508,21 @@ export function createWaterNodeMaterial({
|
|
|
341
508
|
worldPosition.xyz.addAssign(waveDisplacement);
|
|
342
509
|
worldPosition.y.addAssign(rippleHeight);
|
|
343
510
|
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
511
|
+
// Evaluate the analytic long-wave frame once per vertex. The former
|
|
512
|
+
// per-fragment eight-wave loop was the largest source of WebGPU private
|
|
513
|
+
// address-space pressure; interpolating this macro normal leaves the
|
|
514
|
+
// fragment stage's high-frequency detail and ripple normals intact.
|
|
515
|
+
const gerstner = waves.gerstnerNormalFiltered(
|
|
516
|
+
restXZ,
|
|
517
|
+
u.uTime,
|
|
518
|
+
chopWeight,
|
|
519
|
+
nearshore,
|
|
520
|
+
);
|
|
521
|
+
const shoalBlend = clamp(shoal, 0.0, 1.0).toVar();
|
|
522
|
+
vGerstnerNormal.assign(normalize(mix(vec3(0.0, 1.0, 0.0), gerstner.normal, shoalBlend)));
|
|
523
|
+
vCrest.assign(gerstner.crest);
|
|
347
524
|
vShoal.assign(shoal);
|
|
348
525
|
vBreaker.assign(breakFoam);
|
|
349
|
-
vSwash.assign(swash);
|
|
350
526
|
vWorldPosition.assign(worldPosition.xyz);
|
|
351
527
|
|
|
352
528
|
return cameraProjectionMatrix.mul(cameraViewMatrix).mul(vec4(worldPosition.xyz, 1.0));
|
|
@@ -354,28 +530,37 @@ export function createWaterNodeMaterial({
|
|
|
354
530
|
|
|
355
531
|
// ---- Fragment stage (water.frag.glsl) ----
|
|
356
532
|
material.fragmentNode = Fn(() => {
|
|
357
|
-
//
|
|
358
|
-
//
|
|
359
|
-
//
|
|
533
|
+
// Apply the nonlinear wet threshold after the signed film head has been
|
|
534
|
+
// interpolated. Evaluating smoothstep at sparse vertices first exposed
|
|
535
|
+
// individual triangle rows as the water advanced at a grazing angle.
|
|
360
536
|
if (flags.shoaling) {
|
|
361
|
-
|
|
537
|
+
const fragmentWet = mix(
|
|
538
|
+
1.0,
|
|
539
|
+
smoothstep(-0.004, 0.012, vSwashFoamData.x),
|
|
540
|
+
clamp(vSwashZone, 0.0, 1.0),
|
|
541
|
+
);
|
|
542
|
+
Discard(fragmentWet.lessThan(0.015));
|
|
362
543
|
}
|
|
363
544
|
const screenUv = screenUV.toVar();
|
|
364
545
|
const toCamera = cameraPosition.sub(vWorldPosition).toVar();
|
|
365
546
|
const viewDir = normalize(toCamera).toVar();
|
|
366
547
|
const viewDistance = length(toCamera).toVar();
|
|
367
548
|
|
|
368
|
-
// --- surface normal: Gerstner
|
|
369
|
-
const
|
|
370
|
-
const
|
|
371
|
-
const crest = float(gerstner.crest).toVar();
|
|
549
|
+
// --- surface normal: interpolated Gerstner frame + fbm detail + ripples ---
|
|
550
|
+
const gerstnerNormal = normalize(vGerstnerNormal).toVar();
|
|
551
|
+
const crest = float(vCrest).toVar();
|
|
372
552
|
const shoalBlend = clamp(vShoal, 0.0, 1.0).toVar();
|
|
373
|
-
gerstnerNormal.assign(normalize(mix(vec3(0.0, 1.0, 0.0), gerstnerNormal, shoalBlend)));
|
|
374
553
|
crest.assign(clamp(crest.mul(shoalBlend).add(vBreaker.mul(crest.mul(0.55).add(0.45))), 0.0, 1.0));
|
|
375
554
|
const gerstnerNormalY = gerstnerNormal.y.toVar();
|
|
376
555
|
|
|
377
556
|
const detailFade = smoothstep(16.0, 60.0, viewDistance).oneMinus();
|
|
378
|
-
const
|
|
557
|
+
const swashOptics = clamp(vSwashZone, 0.0, 1.0).toVar();
|
|
558
|
+
// A centimetres-thin film is optically smooth at this camera scale. Full
|
|
559
|
+
// open-water normal detail turned every texel into a bright grazing-angle
|
|
560
|
+
// mirror and made the swash look like an opaque sheet of snow.
|
|
561
|
+
const detailStrength = u.uDetailNormalStrength
|
|
562
|
+
.mul(mix(0.2, 1.0, detailFade))
|
|
563
|
+
.mul(mix(1.0, 0.18, swashOptics)).toVar();
|
|
379
564
|
const flowOffset = u.uFlowDirection.mul(u.uTime).mul(u.uFlowSpeed).toVar();
|
|
380
565
|
const detailUv1 = vRestWorldXZ.mul(u.uDetailScale).add(flowOffset.mul(0.55));
|
|
381
566
|
const detailUv2 = waterRotate2d(1.9).mul(vRestWorldXZ).mul(u.uDetailScale.mul(1.63)).sub(flowOffset.mul(0.34));
|
|
@@ -398,18 +583,122 @@ export function createWaterNodeMaterial({
|
|
|
398
583
|
const output = vec4(0.0).toVar();
|
|
399
584
|
If(u.uCameraBelow.greaterThan(0.5), () => {
|
|
400
585
|
// --- underside: camera below the surface looking up ---
|
|
401
|
-
//
|
|
402
|
-
//
|
|
403
|
-
|
|
404
|
-
|
|
586
|
+
// Trace the view ray from water into air. The grab pass uses a clipped
|
|
587
|
+
// same-pose camera while submerged, so this lookup contains the actual
|
|
588
|
+
// sky, clouds, and above-water silhouettes rather than a sky-color
|
|
589
|
+
// approximation. The physical critical angle anchors the composition;
|
|
590
|
+
// color and edge shaping stay deliberately stylized.
|
|
591
|
+
const incident = viewDir.negate().toVar();
|
|
592
|
+
const eta = clamp(u.uIndexOfRefraction, 1.0001, 1.8).toVar();
|
|
593
|
+
const cosIncident = clamp(dot(incident, surfaceNormal), 0.0, 1.0).toVar();
|
|
594
|
+
const criticalCos = max(
|
|
595
|
+
float(1.0).sub(float(1.0).div(eta.mul(eta))),
|
|
596
|
+
0.0,
|
|
597
|
+
).sqrt().toVar();
|
|
598
|
+
const snellWindow = smoothstep(
|
|
599
|
+
criticalCos.sub(0.04),
|
|
600
|
+
criticalCos.add(0.045),
|
|
601
|
+
cosIncident,
|
|
602
|
+
).toVar();
|
|
603
|
+
const refractedDirection = refract(
|
|
604
|
+
incident,
|
|
605
|
+
surfaceNormal.negate(),
|
|
606
|
+
eta,
|
|
607
|
+
).toVar();
|
|
608
|
+
// refract() returns zero under total internal reflection. Blend toward
|
|
609
|
+
// the incident ray there to keep the projection numerically stable;
|
|
610
|
+
// snellWindow prevents that fallback ray from becoming transmission.
|
|
611
|
+
const transmissionDirection = normalize(mix(
|
|
612
|
+
incident,
|
|
613
|
+
refractedDirection,
|
|
614
|
+
max(snellWindow, 0.001),
|
|
615
|
+
)).toVar();
|
|
616
|
+
const transmissionWorld = vWorldPosition.add(
|
|
617
|
+
transmissionDirection.mul(min(u.uCameraFar.mul(0.75), 400.0)),
|
|
618
|
+
);
|
|
619
|
+
const transmissionClip = cameraProjectionMatrix
|
|
620
|
+
.mul(cameraViewMatrix)
|
|
621
|
+
.mul(vec4(transmissionWorld, 1.0)).toVar();
|
|
622
|
+
const transmissionUv = transmissionClip.xy
|
|
623
|
+
.div(max(abs(transmissionClip.w), 1e-5))
|
|
624
|
+
.mul(vec2(0.5, -0.5))
|
|
625
|
+
.add(0.5)
|
|
626
|
+
.add(refractionNormal.xz.mul(u.uRefractionStrength).mul(0.012))
|
|
627
|
+
.toVar();
|
|
628
|
+
const insideCapture = smoothstep(-0.01, 0.01, transmissionUv.x)
|
|
629
|
+
.mul(smoothstep(0.99, 1.01, transmissionUv.x).oneMinus())
|
|
630
|
+
.mul(smoothstep(-0.01, 0.01, transmissionUv.y))
|
|
631
|
+
.mul(smoothstep(0.99, 1.01, transmissionUv.y).oneMinus())
|
|
632
|
+
.mul(smoothstep(0.001, 0.03, transmissionClip.w));
|
|
633
|
+
const capturedAir = u.uSceneColor
|
|
634
|
+
.sample(clamp(transmissionUv, vec2(0.0), vec2(1.0)))
|
|
635
|
+
.level(0).rgb;
|
|
636
|
+
const transmitted = mix(
|
|
637
|
+
lighting.proceduralSky(transmissionDirection),
|
|
638
|
+
capturedAir,
|
|
639
|
+
clamp(insideCapture.mul(u.uUseSceneColor), 0.0, 1.0),
|
|
640
|
+
).toVar();
|
|
641
|
+
|
|
642
|
+
// Compact Beer-Lambert-inspired attenuation through the camera's water
|
|
643
|
+
// column. This uses ToonLab's authored palette, keeping the result
|
|
644
|
+
// graphic and readable instead of pursuing the reference's realism.
|
|
645
|
+
const cameraDepth = max(vWorldPosition.y.sub(cameraPosition.y), 0.0).toVar();
|
|
646
|
+
const volumeAbsorb = exp(
|
|
647
|
+
cameraDepth.div(max(
|
|
648
|
+
u.uDepthFadeDistance.add(u.uDeepFadeDistance.mul(0.5)),
|
|
649
|
+
0.1,
|
|
650
|
+
)).negate(),
|
|
651
|
+
).oneMinus().toVar();
|
|
652
|
+
const waterTint = mix(
|
|
653
|
+
u.uShallowColor,
|
|
654
|
+
u.uMidColor,
|
|
655
|
+
clamp(volumeAbsorb.mul(1.35), 0.0, 1.0),
|
|
656
|
+
).toVar();
|
|
657
|
+
transmitted.mulAssign(mix(
|
|
658
|
+
vec3(1.0),
|
|
659
|
+
waterTint.mul(1.12),
|
|
660
|
+
volumeAbsorb.mul(0.3),
|
|
661
|
+
));
|
|
662
|
+
transmitted.assign(mix(
|
|
663
|
+
transmitted,
|
|
664
|
+
waterTint,
|
|
665
|
+
volumeAbsorb.mul(u.uUnderwaterTintStrength),
|
|
666
|
+
));
|
|
667
|
+
|
|
668
|
+
const f0 = eta.sub(1.0).div(eta.add(1.0)).pow2().toVar();
|
|
669
|
+
const fresnel = f0.add(
|
|
670
|
+
f0.oneMinus().mul(pow(cosIncident.oneMinus(), 5.0)),
|
|
671
|
+
).toVar();
|
|
672
|
+
const transmissionWeight = fresnel.oneMinus()
|
|
673
|
+
.mul(snellWindow)
|
|
674
|
+
.mul(u.uUnderwaterTransmission)
|
|
675
|
+
.toVar();
|
|
676
|
+
const tirColor = mix(
|
|
677
|
+
u.uDeepColor.mul(0.48),
|
|
678
|
+
u.uMidColor.mul(0.72),
|
|
679
|
+
cosIncident.mul(0.3),
|
|
680
|
+
);
|
|
681
|
+
const underColor = mix(
|
|
682
|
+
tirColor,
|
|
683
|
+
transmitted,
|
|
684
|
+
clamp(transmissionWeight, 0.0, 1.0),
|
|
405
685
|
).toVar();
|
|
406
|
-
const
|
|
407
|
-
|
|
408
|
-
|
|
686
|
+
const criticalRim = smoothstep(
|
|
687
|
+
criticalCos.sub(0.085),
|
|
688
|
+
criticalCos,
|
|
689
|
+
cosIncident,
|
|
690
|
+
).mul(smoothstep(
|
|
691
|
+
criticalCos,
|
|
692
|
+
criticalCos.add(0.1),
|
|
693
|
+
cosIncident,
|
|
694
|
+
).oneMinus());
|
|
695
|
+
underColor.addAssign(u.uFresnelColor.mul(criticalRim).mul(0.12));
|
|
409
696
|
underColor.addAssign(
|
|
410
697
|
lighting.sparkles(vRestWorldXZ, surfaceNormal, viewDir, viewDistance, u.uTime).mul(0.5),
|
|
411
698
|
);
|
|
412
|
-
|
|
699
|
+
// The captured air-side scene is already composited; render the
|
|
700
|
+
// underside opaque to avoid double-blending the main scene behind it.
|
|
701
|
+
output.assign(vec4(underColor, 1.0));
|
|
413
702
|
}).Else(() => {
|
|
414
703
|
// --- scene depth, refraction, and the water body color ---
|
|
415
704
|
const waterViewDistance = cameraViewMatrix.mul(vec4(vWorldPosition, 1.0)).z.negate().toVar();
|
|
@@ -440,6 +729,14 @@ export function createWaterNodeMaterial({
|
|
|
440
729
|
bodyColor.assign(absorptionTint);
|
|
441
730
|
alpha.assign(clamp(u.uOpacity.add(absorb.mul(0.3)), 0.0, 1.0));
|
|
442
731
|
});
|
|
732
|
+
// A thin film still has a readable water body. Preserve enough of the
|
|
733
|
+
// sand for a wet-ground cue, but keep the shallow tint and sky sheen so
|
|
734
|
+
// the run-up remains visibly connected to the sea.
|
|
735
|
+
const clearScene = u.uSceneColor.sample(screenUv).level(0).rgb;
|
|
736
|
+
const wetScene = clearScene.mul(0.85);
|
|
737
|
+
const clearSceneWeight = select(u.uUseSceneColor.greaterThan(0.5), 0.48, 0.0);
|
|
738
|
+
bodyColor.assign(mix(bodyColor, wetScene, swashOptics.mul(clearSceneWeight)));
|
|
739
|
+
bodyColor.assign(mix(bodyColor, u.uShallowColor, swashOptics.mul(0.18)));
|
|
443
740
|
|
|
444
741
|
// Steep wave faces act like windows, not lenses: drop caustics there,
|
|
445
742
|
// and fade the dappling with view distance (WATER_QUALITY >= 1).
|
|
@@ -451,17 +748,77 @@ export function createWaterNodeMaterial({
|
|
|
451
748
|
: vec3(0.0);
|
|
452
749
|
|
|
453
750
|
// --- foam ---
|
|
751
|
+
// Foam pattern follows the swash sheet's orbital surge (zero offshore),
|
|
752
|
+
// so the drain visibly pulls the foam seaward instead of the pattern
|
|
753
|
+
// staying glued to rest space while only the envelope moves.
|
|
754
|
+
const foamXZ = vRestWorldXZ.sub(vSwashSurge).toVar();
|
|
454
755
|
const pierce = color.pierceProximity(screenUv, waterViewDistance, vWorldPosition.y).toVar();
|
|
455
|
-
const shoreFoam = foam.shoreFoam(columnDepth, viewDepthDiff,
|
|
456
|
-
|
|
756
|
+
const shoreFoam = foam.shoreFoam(columnDepth, viewDepthDiff, foamXZ, u.uTime, pierce)
|
|
757
|
+
// The legacy contact mask assumes a water body deeper than the foam
|
|
758
|
+
// distance. On a centimetre-thin swash film it is almost 1 everywhere,
|
|
759
|
+
// so keep its familiar offshore/rock character but let the dedicated
|
|
760
|
+
// turbulent edge above own the beach foam.
|
|
761
|
+
.mul(mix(1.0, 0.05, clamp(vSwashZone, 0.0, 1.0))).toVar();
|
|
762
|
+
const whitecaps = foam.whitecaps(crest, gerstnerNormalY, foamXZ, u.uTime).toVar();
|
|
457
763
|
const rippleFoamRaw = clamp(
|
|
458
764
|
rippleState.b.mul(u.uRippleFoamStrength).add(abs(rippleState.r).mul(u.uRippleFoamStrength).mul(4.0)),
|
|
459
765
|
0.0,
|
|
460
766
|
1.0,
|
|
461
767
|
);
|
|
462
|
-
const
|
|
463
|
-
|
|
464
|
-
|
|
768
|
+
const swashFoamRaw = float(0.0).toVar();
|
|
769
|
+
const persistentShoreState = flags.shoaling
|
|
770
|
+
? shoreState.shoreStateSample(vRestWorldXZ)
|
|
771
|
+
: vec4(0.0);
|
|
772
|
+
if (flags.shoaling) {
|
|
773
|
+
// The temporal shore field owns transport, decay, breakup, and
|
|
774
|
+
// event-to-event variation. Keep this main material deliberately
|
|
775
|
+
// small: duplicating procedural noise here can exceed WebGPU's hard
|
|
776
|
+
// 8 KiB private-variable budget for StylizedWater.
|
|
777
|
+
If(vSwashZone.greaterThan(0.001), () => {
|
|
778
|
+
const filmHead = vSwashFoamData.x.toVar();
|
|
779
|
+
const foamWidth = max(vSwashFoamData.y, 0.018).toVar();
|
|
780
|
+
const fallbackBand = smoothstep(-0.006, 0.004, filmHead)
|
|
781
|
+
.mul(smoothstep(foamWidth.mul(0.7), foamWidth.mul(1.25), filmHead).oneMinus());
|
|
782
|
+
const storedFoam = clamp(
|
|
783
|
+
// The state pass deliberately stores low-energy values so its
|
|
784
|
+
// advection remains stable. Remap those values into the original
|
|
785
|
+
// foamShape range here: the former 0.16..0.58 / 0.82 mapping
|
|
786
|
+
// required active state B ~= 0.34 before a single solid pixel
|
|
787
|
+
// could appear, and residue A could never reach the shape's
|
|
788
|
+
// onset at all. Active foam gets the lower presentation onset;
|
|
789
|
+
// residue stays a quiet modifier so old events cannot rebuild a
|
|
790
|
+
// continuous bright shoreline. This does not add a coverage
|
|
791
|
+
// floor or refill the source's true gaps.
|
|
792
|
+
smoothstep(0.12, 0.48, persistentShoreState.b).mul(0.9)
|
|
793
|
+
.add(smoothstep(0.25, 0.7, persistentShoreState.a).mul(0.18)),
|
|
794
|
+
0.0,
|
|
795
|
+
1.0,
|
|
796
|
+
);
|
|
797
|
+
swashFoamRaw.assign(
|
|
798
|
+
max(
|
|
799
|
+
storedFoam,
|
|
800
|
+
fallbackBand.mul(u.uUseShoreState.oneMinus()),
|
|
801
|
+
).mul(vSwashZone),
|
|
802
|
+
);
|
|
803
|
+
});
|
|
804
|
+
}
|
|
805
|
+
const foamAmount = clamp(u.uFoamAmount, 0.0, 2.0).toVar();
|
|
806
|
+
const baseFoamRaw = clamp(
|
|
807
|
+
shoreFoam.add(whitecaps).add(vBreaker.mul(0.85)).add(rippleFoamRaw),
|
|
808
|
+
0.0,
|
|
809
|
+
1.0,
|
|
810
|
+
).mul(foamAmount).toVar();
|
|
811
|
+
const swashFoamValue = clamp(
|
|
812
|
+
swashFoamRaw.mul(clamp(u.uSwashFoamAmount, 0.0, 2.0)),
|
|
813
|
+
0.0,
|
|
814
|
+
1.0,
|
|
815
|
+
).toVar();
|
|
816
|
+
// The pre-session foam looked better because every source shared this
|
|
817
|
+
// crisp solid-core + dissolve-fringe shaping. Shape stored swash foam
|
|
818
|
+
// together with offshore/contact foam; never max an unshaped milky mask
|
|
819
|
+
// over the final result.
|
|
820
|
+
const foamRaw = max(baseFoamRaw, swashFoamValue).toVar();
|
|
821
|
+
const foamValue = foam.foamShape(foamRaw, foamXZ, u.uTime).toVar();
|
|
465
822
|
|
|
466
823
|
// Drifting cloud shadows plus scene shadows (shared sun-shadow pass).
|
|
467
824
|
const cloudShadow = stylizedCloudShadow(
|
|
@@ -470,19 +827,35 @@ export function createWaterNodeMaterial({
|
|
|
470
827
|
);
|
|
471
828
|
const sceneShadow = mix(1.0, sampleEnvironmentSunShadow(vWorldPosition), u.uSceneShadowStrength);
|
|
472
829
|
const sunVisibility = cloudShadow.mul(sceneShadow).toVar();
|
|
830
|
+
// Keep one caustic field across the offshore body and the running sheet.
|
|
831
|
+
// The caustics chunk already fades from the sampled scene depth; applying
|
|
832
|
+
// an additional swash-zone multiplier here exposed the implementation
|
|
833
|
+
// handoff as a hard, visibly separate rendering system.
|
|
473
834
|
bodyColor.addAssign(caustics.mul(foamValue.oneMinus()).mul(sunVisibility));
|
|
474
835
|
|
|
475
836
|
// --- reflection, fresnel, glints ---
|
|
476
837
|
const fresnel = lighting.fresnelFactor(viewDir, surfaceNormal).toVar();
|
|
477
838
|
const reflection = lighting.reflectionColor(vWorldPosition, surfaceNormal, viewDir).toVar();
|
|
478
|
-
|
|
839
|
+
// Thin swash still catches the sky at its lip, but the interior should
|
|
840
|
+
// mostly reveal wet sand. Attenuate (rather than remove) its Fresnel,
|
|
841
|
+
// reflection and glints while keeping offshore water unchanged.
|
|
842
|
+
const swashLight = mix(1.0, 0.36, swashOptics).toVar();
|
|
843
|
+
const surfaceFresnel = fresnel.mul(swashLight).toVar();
|
|
844
|
+
const reflectionMix = clamp(
|
|
845
|
+
surfaceFresnel.mul(u.uReflectionStrength), 0.0, 0.92,
|
|
846
|
+
).toVar();
|
|
479
847
|
const litColor = mix(bodyColor, reflection, reflectionMix).toVar();
|
|
480
|
-
litColor.addAssign(
|
|
481
|
-
|
|
848
|
+
litColor.addAssign(
|
|
849
|
+
u.uFresnelColor.mul(surfaceFresnel).mul(surfaceFresnel).mul(0.35),
|
|
850
|
+
);
|
|
851
|
+
litColor.addAssign(
|
|
852
|
+
lighting.specular(viewDir, surfaceNormal, foamValue.mul(0.75).oneMinus())
|
|
853
|
+
.mul(sunVisibility).mul(swashLight),
|
|
854
|
+
);
|
|
482
855
|
if (flags.sparkles) {
|
|
483
856
|
litColor.addAssign(
|
|
484
857
|
lighting.sparkles(vRestWorldXZ, surfaceNormal, viewDir, viewDistance, u.uTime)
|
|
485
|
-
.mul(foamValue.oneMinus()).mul(sunVisibility),
|
|
858
|
+
.mul(foamValue.oneMinus()).mul(sunVisibility).mul(swashLight),
|
|
486
859
|
);
|
|
487
860
|
}
|
|
488
861
|
litColor.mulAssign(mix(vec3(0.8, 0.85, 0.94), vec3(1.0), sunVisibility));
|
|
@@ -490,11 +863,22 @@ export function createWaterNodeMaterial({
|
|
|
490
863
|
// --- foam overlay, slightly shaded by the sun for a two-tone toon read ---
|
|
491
864
|
const foamLight = clamp(dot(surfaceNormal, u.uSunDirection), 0.0, 1.0).mul(0.18).add(0.82)
|
|
492
865
|
.mul(sunVisibility.mul(0.18).add(0.82));
|
|
493
|
-
|
|
866
|
+
// Preserve the original bright foam, with just enough shallow-water
|
|
867
|
+
// shade at its porous fringe to show volume instead of flat white paint.
|
|
868
|
+
const foamShade = mix(u.uShallowColor, u.uFoamColor, 0.8).toVar();
|
|
869
|
+
const foamTint = mix(
|
|
870
|
+
foamShade,
|
|
871
|
+
u.uFoamColor.mul(foamLight),
|
|
872
|
+
smoothstep(0.5, 0.82, foamRaw),
|
|
873
|
+
).toVar();
|
|
874
|
+
litColor.assign(mix(litColor, foamTint, foamValue));
|
|
494
875
|
alpha.assign(max(alpha, foamValue.mul(0.92)));
|
|
495
876
|
// The swash film is only centimeters deep; give it body.
|
|
496
|
-
alpha.assign(max(alpha,
|
|
497
|
-
alpha.assign(max(
|
|
877
|
+
alpha.assign(max(alpha, swashFoamValue.mul(0.55)));
|
|
878
|
+
alpha.assign(max(
|
|
879
|
+
alpha,
|
|
880
|
+
clamp(reflectionMix.add(surfaceFresnel.mul(0.4)), 0.0, 1.0).mul(0.85),
|
|
881
|
+
));
|
|
498
882
|
|
|
499
883
|
// Fog last: foam, sparkles, and shorelines haze out with the rest of
|
|
500
884
|
// the world. Exponential term mirrors the environment height fog;
|
|
@@ -530,6 +914,12 @@ export function createWaterNodeMaterial({
|
|
|
530
914
|
)],
|
|
531
915
|
[8, vec4(vec3(fresnel), 1.0)],
|
|
532
916
|
[9, vec4(crest, vCrest, whitecaps, 1.0)],
|
|
917
|
+
[10, vec4(
|
|
918
|
+
persistentShoreState.r,
|
|
919
|
+
persistentShoreState.g,
|
|
920
|
+
max(persistentShoreState.b, persistentShoreState.a),
|
|
921
|
+
1.0,
|
|
922
|
+
)],
|
|
533
923
|
];
|
|
534
924
|
let debugMask = float(0.0);
|
|
535
925
|
let debugSum = vec4(0.0);
|