@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
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
// POI site selection: score the terrain per archetype (flatness, dryness,
|
|
2
|
+
// shore/hilltop affinities from the archetype table), then greedy-pick
|
|
3
|
+
// mutually distant winners. Deterministic per seed.
|
|
4
|
+
|
|
5
|
+
import { POI_ARCHETYPES } from './villageArchetypes.js';
|
|
6
|
+
|
|
7
|
+
function mulberry32(seed) {
|
|
8
|
+
let state = (Math.trunc(seed) || 1) >>> 0;
|
|
9
|
+
return () => {
|
|
10
|
+
state = (state + 0x6d2b79f5) >>> 0;
|
|
11
|
+
let t = state;
|
|
12
|
+
t = Math.imul(t ^ (t >>> 15), t | 1);
|
|
13
|
+
t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
|
|
14
|
+
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* @param {Object} options
|
|
20
|
+
* @param {Function} options.heightAt
|
|
21
|
+
* @param {number} [options.waterLevel]
|
|
22
|
+
* @param {number|{x,z}} [options.size]
|
|
23
|
+
* @param {number} [options.seed]
|
|
24
|
+
* @param {Array<{archetype: string, count: number, radius?: number}>} options.requests
|
|
25
|
+
* @returns {Array<{archetype, seed, x, z, radius}>}
|
|
26
|
+
*/
|
|
27
|
+
export function pickPoiSites({
|
|
28
|
+
heightAt,
|
|
29
|
+
waterLevel = 0,
|
|
30
|
+
size = 1000,
|
|
31
|
+
seed = 1,
|
|
32
|
+
requests = [],
|
|
33
|
+
} = {}) {
|
|
34
|
+
const width = Number(size?.width ?? size?.x ?? size) || 1000;
|
|
35
|
+
const depth = Number(size?.depth ?? size?.z ?? size) || 1000;
|
|
36
|
+
const random = mulberry32(seed * 22695477 + 3);
|
|
37
|
+
const innerX = width * 0.4;
|
|
38
|
+
const innerZ = depth * 0.4;
|
|
39
|
+
|
|
40
|
+
const slopeAt = (x, z) => {
|
|
41
|
+
const step = 3;
|
|
42
|
+
const dx = (heightAt(x + step, z) - heightAt(x - step, z)) / (2 * step);
|
|
43
|
+
const dz = (heightAt(x, z + step) - heightAt(x, z - step)) / (2 * step);
|
|
44
|
+
return Math.hypot(dx, dz);
|
|
45
|
+
};
|
|
46
|
+
const shoreDistance = (x, z) => {
|
|
47
|
+
for (const reach of [6, 12, 20, 28]) {
|
|
48
|
+
for (let probe = 0; probe < 8; probe += 1) {
|
|
49
|
+
const angle = (probe / 8) * Math.PI * 2;
|
|
50
|
+
if (heightAt(x + Math.cos(angle) * reach, z + Math.sin(angle) * reach) <= waterLevel) {
|
|
51
|
+
return reach;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return 99;
|
|
56
|
+
};
|
|
57
|
+
const context = { heightAt, shoreDistance, slopeAt, waterLevel };
|
|
58
|
+
|
|
59
|
+
// Site flatness must hold across the whole radius, not just the center —
|
|
60
|
+
// sample a ring too.
|
|
61
|
+
const areaScore = (archetypeSpec, x, z, radius) => {
|
|
62
|
+
let score = archetypeSpec.siteScore(x, z, context);
|
|
63
|
+
if (score <= 0) return 0;
|
|
64
|
+
for (let probe = 0; probe < 4; probe += 1) {
|
|
65
|
+
const angle = (probe / 4) * Math.PI * 2 + 0.5;
|
|
66
|
+
const rx = x + Math.cos(angle) * radius * 0.6;
|
|
67
|
+
const rz = z + Math.sin(angle) * radius * 0.6;
|
|
68
|
+
const ringScore = archetypeSpec.siteScore(rx, rz, context);
|
|
69
|
+
if (ringScore <= 0) return 0;
|
|
70
|
+
score += ringScore * 0.25;
|
|
71
|
+
}
|
|
72
|
+
return score;
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
const picked = [];
|
|
76
|
+
for (const request of requests) {
|
|
77
|
+
const archetypeSpec = POI_ARCHETYPES[request.archetype];
|
|
78
|
+
if (!archetypeSpec) continue;
|
|
79
|
+
const radius = request.radius ?? Math.max(archetypeSpec.minRadius, 30);
|
|
80
|
+
for (let instance = 0; instance < (request.count ?? 1); instance += 1) {
|
|
81
|
+
let best = null;
|
|
82
|
+
for (let attempt = 0; attempt < 220; attempt += 1) {
|
|
83
|
+
const x = (random() * 2 - 1) * innerX;
|
|
84
|
+
const z = (random() * 2 - 1) * innerZ;
|
|
85
|
+
let clear = true;
|
|
86
|
+
for (const site of picked) {
|
|
87
|
+
const minDistance = site.radius + radius + 40;
|
|
88
|
+
if ((site.x - x) ** 2 + (site.z - z) ** 2 < minDistance ** 2) { clear = false; break; }
|
|
89
|
+
}
|
|
90
|
+
if (!clear) continue;
|
|
91
|
+
const score = areaScore(archetypeSpec, x, z, radius);
|
|
92
|
+
if (score > 0 && (!best || score > best.score)) best = { score, x, z };
|
|
93
|
+
}
|
|
94
|
+
if (!best) continue; // terrain had no room — honest miss, not a crash
|
|
95
|
+
picked.push({
|
|
96
|
+
archetype: request.archetype,
|
|
97
|
+
radius,
|
|
98
|
+
seed: Math.floor(random() * 0xffffffff),
|
|
99
|
+
x: best.x,
|
|
100
|
+
z: best.z,
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return picked;
|
|
105
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ordered transient layers for active water. Lower priorities establish a
|
|
3
|
+
* base that higher priorities can derive from or replace. Runtime policy is
|
|
4
|
+
* never serialized into a portable water preset.
|
|
5
|
+
*/
|
|
6
|
+
export const WATER_SCENE_OVERRIDE_PRIORITIES = Object.freeze({
|
|
7
|
+
lighting: 100,
|
|
8
|
+
weather: 200,
|
|
9
|
+
scene: 300,
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Authored fallback fields that a live scene may replace without editing the
|
|
14
|
+
* portable water asset. Palette, wave structure, foam, and interaction
|
|
15
|
+
* response remain authored settings.
|
|
16
|
+
*/
|
|
17
|
+
export const WATER_SCENE_OVERRIDE_KEYS = Object.freeze([
|
|
18
|
+
'waveIntensity',
|
|
19
|
+
'sunDirection',
|
|
20
|
+
'sunColor',
|
|
21
|
+
'skyZenithColor',
|
|
22
|
+
'skyHorizonColor',
|
|
23
|
+
]);
|
package/src/water/water.js
CHANGED
|
@@ -9,12 +9,17 @@
|
|
|
9
9
|
// water.update(renderer, scene, camera, delta);
|
|
10
10
|
|
|
11
11
|
export * from './waterSettings.js';
|
|
12
|
+
export * from './sceneOverrideLayers.js';
|
|
12
13
|
export * from './waterMaterial.js';
|
|
14
|
+
export * from './waterCurrentField.js';
|
|
15
|
+
export * from './waterNearshorePhase.js';
|
|
13
16
|
export * from './waterRippleSimulation.js';
|
|
14
17
|
export * from './waterSplashSystem.js';
|
|
15
18
|
export * from './waterBreakerSystem.js';
|
|
16
19
|
export * from './waterScenePasses.js';
|
|
17
20
|
export * from './waterInteraction.js';
|
|
18
21
|
export * from './waterSurface.js';
|
|
22
|
+
export * from './waterShoreStateField.js';
|
|
23
|
+
export * from './waterShoreMaterial.js';
|
|
19
24
|
export * from './waterRain.js';
|
|
20
25
|
export * from './waterVegetation.js';
|
|
@@ -54,6 +54,20 @@ export function computeBreakingDepth(settings, waveEnergy) {
|
|
|
54
54
|
return depth;
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
+
// The overhanging shell is a second water surface. It works in a substantial
|
|
58
|
+
// shore/open-water column, but cannot be welded convincingly to the few-cm
|
|
59
|
+
// film used by an explicit calibration swash; there it produced detached
|
|
60
|
+
// foam ribbons. The main heightfield still performs depth capping and breaker
|
|
61
|
+
// foam, controlled by breakerEnabled/breakerAmount.
|
|
62
|
+
export function shouldUseDedicatedBreakerShell(settings, hasBedSampler = true) {
|
|
63
|
+
return Boolean(
|
|
64
|
+
hasBedSampler
|
|
65
|
+
&& settings?.breakerEnabled !== false
|
|
66
|
+
&& (settings?.breakerAmount ?? 0) > 0.001
|
|
67
|
+
&& (settings?.runupDistance ?? 0) <= 0.01,
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
|
|
57
71
|
function pointKey(x, z) {
|
|
58
72
|
return `${Math.round(x * 512)},${Math.round(z * 512)}`;
|
|
59
73
|
}
|
|
@@ -556,7 +570,7 @@ export class WaterBreakerSystem extends THREE.Group {
|
|
|
556
570
|
(1 - smoothstep(delta, 0.38, 0.47));
|
|
557
571
|
|
|
558
572
|
const capDepth = Math.min(col.depth, localDepth);
|
|
559
|
-
const breakHeight = 0.
|
|
573
|
+
const breakHeight = 0.48 * capDepth * p.scale * p.amount * col.endFade * setEnvelope;
|
|
560
574
|
const H = breakHeight * THREE.MathUtils.lerp(0.12, 1, pulse) * (1 - 0.55 * post);
|
|
561
575
|
const fade = p.amount * col.endFade * travelFade * setGate *
|
|
562
576
|
smoothstep(pulse + post, 0.12, 0.35) * smoothstep(H, 0.05, 0.15);
|
|
@@ -0,0 +1,447 @@
|
|
|
1
|
+
import * as THREE from 'three';
|
|
2
|
+
|
|
3
|
+
const DEFAULT_MAX_SPEED = 8;
|
|
4
|
+
const sampleScratch = new THREE.Vector2();
|
|
5
|
+
|
|
6
|
+
function finiteOr(value, fallback) {
|
|
7
|
+
const number = Number(value);
|
|
8
|
+
return Number.isFinite(number) ? number : fallback;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function clamp01(value) {
|
|
12
|
+
return THREE.MathUtils.clamp(finiteOr(value, 0), 0, 1);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function smoothstep01(value) {
|
|
16
|
+
const x = clamp01(value);
|
|
17
|
+
return x * x * (3 - 2 * x);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function vector2Components(value, fallbackX = 0, fallbackZ = 0) {
|
|
21
|
+
if (Array.isArray(value)) {
|
|
22
|
+
return [finiteOr(value[0], fallbackX), finiteOr(value[1], fallbackZ)];
|
|
23
|
+
}
|
|
24
|
+
return [
|
|
25
|
+
finiteOr(value?.x ?? value?.[0], fallbackX),
|
|
26
|
+
finiteOr(value?.z ?? value?.y ?? value?.[1], fallbackZ),
|
|
27
|
+
];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Constructor regions use full width/depth. Vector4 uses the shader-facing
|
|
31
|
+
// (centerX, centerZ, halfWidth, halfDepth) convention shared by ripple and
|
|
32
|
+
// shoreline atlases.
|
|
33
|
+
function normalizeRegion(region) {
|
|
34
|
+
if (region?.isVector4) {
|
|
35
|
+
return {
|
|
36
|
+
centerX: finiteOr(region.x, 0),
|
|
37
|
+
centerZ: finiteOr(region.y, 0),
|
|
38
|
+
width: Math.max(finiteOr(region.z, 16) * 2, 0.1),
|
|
39
|
+
depth: Math.max(finiteOr(region.w, 16) * 2, 0.1),
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
if (Array.isArray(region)) {
|
|
43
|
+
return {
|
|
44
|
+
centerX: finiteOr(region[0], 0),
|
|
45
|
+
centerZ: finiteOr(region[1], 0),
|
|
46
|
+
width: Math.max(finiteOr(region[2], 32), 0.1),
|
|
47
|
+
depth: Math.max(finiteOr(region[3], 32), 0.1),
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
if (region?.min && region?.max) {
|
|
51
|
+
const minX = finiteOr(region.min.x, -16);
|
|
52
|
+
const minZ = finiteOr(region.min.z ?? region.min.y, -16);
|
|
53
|
+
const maxX = finiteOr(region.max.x, 16);
|
|
54
|
+
const maxZ = finiteOr(region.max.z ?? region.max.y, 16);
|
|
55
|
+
return {
|
|
56
|
+
centerX: (minX + maxX) * 0.5,
|
|
57
|
+
centerZ: (minZ + maxZ) * 0.5,
|
|
58
|
+
width: Math.max(maxX - minX, 0.1),
|
|
59
|
+
depth: Math.max(maxZ - minZ, 0.1),
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
const center = region?.center;
|
|
63
|
+
const size = region?.size;
|
|
64
|
+
return {
|
|
65
|
+
centerX: finiteOr(region?.centerX ?? center?.x ?? region?.x, 0),
|
|
66
|
+
centerZ: finiteOr(region?.centerZ ?? center?.z ?? center?.y ?? region?.z, 0),
|
|
67
|
+
width: Math.max(finiteOr(
|
|
68
|
+
region?.width ?? size?.x ??
|
|
69
|
+
(Number.isFinite(region?.halfWidth) ? region.halfWidth * 2 : undefined),
|
|
70
|
+
32,
|
|
71
|
+
), 0.1),
|
|
72
|
+
depth: Math.max(finiteOr(
|
|
73
|
+
region?.depth ?? size?.z ?? size?.y ??
|
|
74
|
+
(Number.isFinite(region?.halfDepth) ? region.halfDepth * 2 : undefined),
|
|
75
|
+
32,
|
|
76
|
+
), 0.1),
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function normalizeResolution(resolution) {
|
|
81
|
+
let x;
|
|
82
|
+
let y;
|
|
83
|
+
if (Number.isFinite(resolution)) {
|
|
84
|
+
x = resolution;
|
|
85
|
+
y = resolution;
|
|
86
|
+
} else if (Array.isArray(resolution)) {
|
|
87
|
+
[x, y] = resolution;
|
|
88
|
+
} else {
|
|
89
|
+
x = resolution?.x ?? resolution?.width;
|
|
90
|
+
y = resolution?.y ?? resolution?.height;
|
|
91
|
+
}
|
|
92
|
+
return {
|
|
93
|
+
x: THREE.MathUtils.clamp(Math.round(finiteOr(x, 128)), 2, 2048),
|
|
94
|
+
y: THREE.MathUtils.clamp(Math.round(finiteOr(y, 128)), 2, 2048),
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function resolveVelocity(result, out) {
|
|
99
|
+
if (result === false || result === null) return false;
|
|
100
|
+
if (Array.isArray(result)) {
|
|
101
|
+
out.set(finiteOr(result[0], NaN), finiteOr(result[1], NaN));
|
|
102
|
+
} else if (result && result !== out) {
|
|
103
|
+
out.set(
|
|
104
|
+
finiteOr(result.x ?? result[0], NaN),
|
|
105
|
+
finiteOr(result.z ?? result.y ?? result[1], NaN),
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
return Number.isFinite(out.x) && Number.isFinite(out.y);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* CPU-authored, world-space horizontal current field.
|
|
113
|
+
*
|
|
114
|
+
* The CPU retains full-precision velocity/mask arrays for gameplay queries.
|
|
115
|
+
* A compact linearly-filtered RGBA8 texture mirrors the field for lightweight
|
|
116
|
+
* GPU consumers such as shoreline-foam advection:
|
|
117
|
+
*
|
|
118
|
+
* R/G = signed X/Z velocity encoded around 0.5
|
|
119
|
+
* B = fluid/obstacle weight
|
|
120
|
+
* A = valid authored domain
|
|
121
|
+
*
|
|
122
|
+
* This is deliberately not a shallow-water or Navier-Stokes solver. The
|
|
123
|
+
* caller authors the large-scale velocity (`velocitySampler`) and optional
|
|
124
|
+
* water-domain mask. A signed-distance sampler adds a deterministic
|
|
125
|
+
* no-penetration projection near banks/rocks, but it cannot infer circulation,
|
|
126
|
+
* pressure, wakes, separation, or downstream turbulence. Those need authored
|
|
127
|
+
* vectors, a flow-map bake, or a real fluid solver upstream.
|
|
128
|
+
*
|
|
129
|
+
* `velocitySampler(x, z, out, context)` may mutate `out` or return a Vector2,
|
|
130
|
+
* `[vx, vz]`, `{ x, z }`, `false`, or `null`. `context.time` lets a caller bake
|
|
131
|
+
* a new tidal phase. For cheap whole-field tidal reversal between bakes, use
|
|
132
|
+
* `setStrength(-1)`; CPU and GPU consumers receive the same multiplier.
|
|
133
|
+
*/
|
|
134
|
+
export class WaterCurrentField {
|
|
135
|
+
constructor({
|
|
136
|
+
region = {},
|
|
137
|
+
resolution = { x: 128, y: 128 },
|
|
138
|
+
velocity = [0, 0],
|
|
139
|
+
velocitySampler = null,
|
|
140
|
+
sampler = velocitySampler,
|
|
141
|
+
maskSampler = null,
|
|
142
|
+
domainMaskSampler = maskSampler,
|
|
143
|
+
signedDistanceSampler = null,
|
|
144
|
+
obstacleDistanceSampler = signedDistanceSampler,
|
|
145
|
+
obstacleInfluence = 1,
|
|
146
|
+
obstacleDeflection = 1,
|
|
147
|
+
preserveTangentialSpeed = 0.7,
|
|
148
|
+
obstacleGradientStep = null,
|
|
149
|
+
maxSpeed = DEFAULT_MAX_SPEED,
|
|
150
|
+
strength = 1,
|
|
151
|
+
time = 0,
|
|
152
|
+
} = {}) {
|
|
153
|
+
const normalizedRegion = normalizeRegion(region);
|
|
154
|
+
const normalizedResolution = normalizeResolution(resolution);
|
|
155
|
+
this.isWaterCurrentField = true;
|
|
156
|
+
this.centerX = normalizedRegion.centerX;
|
|
157
|
+
this.centerZ = normalizedRegion.centerZ;
|
|
158
|
+
this.worldWidth = normalizedRegion.width;
|
|
159
|
+
this.worldDepth = normalizedRegion.depth;
|
|
160
|
+
this.resolutionX = normalizedResolution.x;
|
|
161
|
+
this.resolutionY = normalizedResolution.y;
|
|
162
|
+
this.region = new THREE.Vector4(
|
|
163
|
+
this.centerX,
|
|
164
|
+
this.centerZ,
|
|
165
|
+
this.worldWidth * 0.5,
|
|
166
|
+
this.worldDepth * 0.5,
|
|
167
|
+
);
|
|
168
|
+
this.minX = this.centerX - this.worldWidth * 0.5;
|
|
169
|
+
this.minZ = this.centerZ - this.worldDepth * 0.5;
|
|
170
|
+
this.cellSizeX = this.worldWidth / this.resolutionX;
|
|
171
|
+
this.cellSizeZ = this.worldDepth / this.resolutionY;
|
|
172
|
+
|
|
173
|
+
const [constantX, constantZ] = vector2Components(velocity);
|
|
174
|
+
this.constantVelocity = new THREE.Vector2(constantX, constantZ);
|
|
175
|
+
this.velocitySampler = typeof sampler === 'function' ? sampler : null;
|
|
176
|
+
this.maskSampler = typeof domainMaskSampler === 'function' ? domainMaskSampler : null;
|
|
177
|
+
this.obstacleDistanceSampler = typeof obstacleDistanceSampler === 'function'
|
|
178
|
+
? obstacleDistanceSampler
|
|
179
|
+
: null;
|
|
180
|
+
this.obstacleInfluence = Math.max(finiteOr(obstacleInfluence, 1), 0);
|
|
181
|
+
this.obstacleDeflection = clamp01(obstacleDeflection);
|
|
182
|
+
this.preserveTangentialSpeed = clamp01(preserveTangentialSpeed);
|
|
183
|
+
this.obstacleGradientStep = Math.max(
|
|
184
|
+
finiteOr(obstacleGradientStep, Math.min(this.cellSizeX, this.cellSizeZ) * 0.75),
|
|
185
|
+
1e-3,
|
|
186
|
+
);
|
|
187
|
+
this.maxSpeed = Math.max(finiteOr(maxSpeed, DEFAULT_MAX_SPEED), 1e-3);
|
|
188
|
+
this.strength = finiteOr(strength, 1);
|
|
189
|
+
this.time = finiteOr(time, 0);
|
|
190
|
+
this.revision = 0;
|
|
191
|
+
this.disposed = false;
|
|
192
|
+
this.sampleContext = { time: this.time, field: this };
|
|
193
|
+
|
|
194
|
+
const count = this.resolutionX * this.resolutionY;
|
|
195
|
+
this.velocities = new Float32Array(count * 2);
|
|
196
|
+
this.weights = new Float32Array(count);
|
|
197
|
+
this.validity = new Float32Array(count);
|
|
198
|
+
this.encodedData = new Uint8Array(count * 4);
|
|
199
|
+
this.texture = new THREE.DataTexture(
|
|
200
|
+
this.encodedData,
|
|
201
|
+
this.resolutionX,
|
|
202
|
+
this.resolutionY,
|
|
203
|
+
THREE.RGBAFormat,
|
|
204
|
+
THREE.UnsignedByteType,
|
|
205
|
+
);
|
|
206
|
+
this.texture.name = 'waterCurrentField';
|
|
207
|
+
this.texture.wrapS = THREE.ClampToEdgeWrapping;
|
|
208
|
+
this.texture.wrapT = THREE.ClampToEdgeWrapping;
|
|
209
|
+
this.texture.minFilter = THREE.LinearFilter;
|
|
210
|
+
this.texture.magFilter = THREE.LinearFilter;
|
|
211
|
+
this.texture.generateMipmaps = false;
|
|
212
|
+
this.texture.flipY = false;
|
|
213
|
+
this.texture.colorSpace = THREE.NoColorSpace;
|
|
214
|
+
this.texture.unpackAlignment = 1;
|
|
215
|
+
|
|
216
|
+
this.rebuild({ time: this.time });
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
getRegion(out = new THREE.Vector4()) {
|
|
220
|
+
return out.copy(this.region);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
containsPoint(x, z) {
|
|
224
|
+
return Number.isFinite(x) && Number.isFinite(z) &&
|
|
225
|
+
x >= this.minX && x <= this.minX + this.worldWidth &&
|
|
226
|
+
z >= this.minZ && z <= this.minZ + this.worldDepth;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
setStrength(strength) {
|
|
230
|
+
this.strength = finiteOr(strength, this.strength);
|
|
231
|
+
return this;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
setTime(time, { rebuild = true } = {}) {
|
|
235
|
+
this.time = finiteOr(time, this.time);
|
|
236
|
+
if (rebuild) this.rebuild({ time: this.time });
|
|
237
|
+
return this;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
setVelocitySampler(sampler, { rebuild = true } = {}) {
|
|
241
|
+
if (sampler != null && typeof sampler !== 'function') {
|
|
242
|
+
throw new TypeError('WaterCurrentField velocitySampler must be a function or null.');
|
|
243
|
+
}
|
|
244
|
+
this.velocitySampler = sampler ?? null;
|
|
245
|
+
if (rebuild) this.rebuild();
|
|
246
|
+
return this;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
setMaskSampler(sampler, { rebuild = true } = {}) {
|
|
250
|
+
if (sampler != null && typeof sampler !== 'function') {
|
|
251
|
+
throw new TypeError('WaterCurrentField maskSampler must be a function or null.');
|
|
252
|
+
}
|
|
253
|
+
this.maskSampler = sampler ?? null;
|
|
254
|
+
if (rebuild) this.rebuild();
|
|
255
|
+
return this;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
setObstacleDistanceSampler(sampler, { rebuild = true } = {}) {
|
|
259
|
+
if (sampler != null && typeof sampler !== 'function') {
|
|
260
|
+
throw new TypeError(
|
|
261
|
+
'WaterCurrentField obstacleDistanceSampler must be a function or null.',
|
|
262
|
+
);
|
|
263
|
+
}
|
|
264
|
+
this.obstacleDistanceSampler = sampler ?? null;
|
|
265
|
+
if (rebuild) this.rebuild();
|
|
266
|
+
return this;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// Applies the local solid-boundary approximation in-place. Positive signed
|
|
270
|
+
// distance is fluid; <= 0 is solid. The gradient points out of the solid,
|
|
271
|
+
// so only velocity aimed into it is removed.
|
|
272
|
+
projectAtObstacle(x, z, velocity, distance) {
|
|
273
|
+
if (!this.obstacleDistanceSampler || !Number.isFinite(distance)) return 1;
|
|
274
|
+
if (distance <= 0) {
|
|
275
|
+
velocity.set(0, 0);
|
|
276
|
+
return 0;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
const influence = this.obstacleInfluence;
|
|
280
|
+
if (influence <= 0 || distance >= influence) return 1;
|
|
281
|
+
const h = this.obstacleGradientStep;
|
|
282
|
+
const left = Number(this.obstacleDistanceSampler(x - h, z, this.sampleContext));
|
|
283
|
+
const right = Number(this.obstacleDistanceSampler(x + h, z, this.sampleContext));
|
|
284
|
+
const down = Number(this.obstacleDistanceSampler(x, z - h, this.sampleContext));
|
|
285
|
+
const up = Number(this.obstacleDistanceSampler(x, z + h, this.sampleContext));
|
|
286
|
+
const gradientX = right - left;
|
|
287
|
+
const gradientZ = up - down;
|
|
288
|
+
const gradientLength = Math.hypot(gradientX, gradientZ);
|
|
289
|
+
const avoidance = (1 - smoothstep01(distance / influence)) * this.obstacleDeflection;
|
|
290
|
+
if (gradientLength > 1e-6 && avoidance > 0) {
|
|
291
|
+
const normalX = gradientX / gradientLength;
|
|
292
|
+
const normalZ = gradientZ / gradientLength;
|
|
293
|
+
const inward = velocity.x * normalX + velocity.y * normalZ;
|
|
294
|
+
if (inward < 0) {
|
|
295
|
+
const originalSpeed = velocity.length();
|
|
296
|
+
velocity.x -= normalX * inward * avoidance;
|
|
297
|
+
velocity.y -= normalZ * inward * avoidance;
|
|
298
|
+
const projectedSpeed = velocity.length();
|
|
299
|
+
if (projectedSpeed > 1e-5 && originalSpeed > projectedSpeed) {
|
|
300
|
+
const preservedSpeed = THREE.MathUtils.lerp(
|
|
301
|
+
projectedSpeed,
|
|
302
|
+
originalSpeed,
|
|
303
|
+
avoidance * this.preserveTangentialSpeed,
|
|
304
|
+
);
|
|
305
|
+
velocity.multiplyScalar(preservedSpeed / projectedSpeed);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
// Feather the last fraction of a cell so linear GPU sampling does not
|
|
310
|
+
// carry a full-speed texel across a solid boundary.
|
|
311
|
+
const feather = Math.max(Math.min(influence * 0.25, Math.hypot(
|
|
312
|
+
this.cellSizeX,
|
|
313
|
+
this.cellSizeZ,
|
|
314
|
+
)), 1e-3);
|
|
315
|
+
return smoothstep01(distance / feather);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
rebuild({ time = this.time } = {}) {
|
|
319
|
+
if (this.disposed) return this;
|
|
320
|
+
this.time = finiteOr(time, this.time);
|
|
321
|
+
this.sampleContext.time = this.time;
|
|
322
|
+
const width = this.resolutionX;
|
|
323
|
+
const height = this.resolutionY;
|
|
324
|
+
const maximum = this.maxSpeed;
|
|
325
|
+
|
|
326
|
+
for (let y = 0; y < height; y += 1) {
|
|
327
|
+
const worldZ = this.minZ + (y + 0.5) * this.cellSizeZ;
|
|
328
|
+
for (let x = 0; x < width; x += 1) {
|
|
329
|
+
const worldX = this.minX + (x + 0.5) * this.cellSizeX;
|
|
330
|
+
const index = y * width + x;
|
|
331
|
+
const velocityIndex = index * 2;
|
|
332
|
+
const textureIndex = index * 4;
|
|
333
|
+
sampleScratch.copy(this.constantVelocity);
|
|
334
|
+
let valid = true;
|
|
335
|
+
if (this.velocitySampler) {
|
|
336
|
+
const result = this.velocitySampler(
|
|
337
|
+
worldX,
|
|
338
|
+
worldZ,
|
|
339
|
+
sampleScratch,
|
|
340
|
+
this.sampleContext,
|
|
341
|
+
);
|
|
342
|
+
valid = resolveVelocity(result, sampleScratch);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
let weight = valid ? 1 : 0;
|
|
346
|
+
if (valid && this.maskSampler) {
|
|
347
|
+
weight *= clamp01(this.maskSampler(worldX, worldZ, this.sampleContext));
|
|
348
|
+
}
|
|
349
|
+
if (valid && this.obstacleDistanceSampler) {
|
|
350
|
+
const distance = Number(
|
|
351
|
+
this.obstacleDistanceSampler(worldX, worldZ, this.sampleContext),
|
|
352
|
+
);
|
|
353
|
+
if (Number.isFinite(distance)) {
|
|
354
|
+
weight *= this.projectAtObstacle(worldX, worldZ, sampleScratch, distance);
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
if (!valid || weight <= 0) sampleScratch.set(0, 0);
|
|
359
|
+
const speed = sampleScratch.length();
|
|
360
|
+
if (speed > maximum) sampleScratch.multiplyScalar(maximum / speed);
|
|
361
|
+
this.velocities[velocityIndex] = sampleScratch.x;
|
|
362
|
+
this.velocities[velocityIndex + 1] = sampleScratch.y;
|
|
363
|
+
this.weights[index] = clamp01(weight);
|
|
364
|
+
this.validity[index] = valid ? 1 : 0;
|
|
365
|
+
// Reserve byte 128 as exact zero. A conventional UNORM 0.5 bias
|
|
366
|
+
// decodes 128/255 to +1/255, which becomes a persistent 3.1 cm/s
|
|
367
|
+
// diagonal drift at the default 8 m/s range even in authored still
|
|
368
|
+
// water. Codes 1..255 provide a symmetric signed 127-step range;
|
|
369
|
+
// code 0 remains unused by valid velocity samples.
|
|
370
|
+
this.encodedData[textureIndex] = Math.round(
|
|
371
|
+
128 + THREE.MathUtils.clamp(sampleScratch.x / maximum, -1, 1) * 127,
|
|
372
|
+
);
|
|
373
|
+
this.encodedData[textureIndex + 1] = Math.round(
|
|
374
|
+
128 + THREE.MathUtils.clamp(sampleScratch.y / maximum, -1, 1) * 127,
|
|
375
|
+
);
|
|
376
|
+
this.encodedData[textureIndex + 2] = Math.round(this.weights[index] * 255);
|
|
377
|
+
this.encodedData[textureIndex + 3] = valid ? 255 : 0;
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
this.texture.needsUpdate = true;
|
|
381
|
+
this.revision += 1;
|
|
382
|
+
return this;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
sampleChannelsAt(x, z, out) {
|
|
386
|
+
if (!this.containsPoint(x, z)) {
|
|
387
|
+
out.vx = 0;
|
|
388
|
+
out.vz = 0;
|
|
389
|
+
out.weight = 0;
|
|
390
|
+
out.valid = 0;
|
|
391
|
+
return out;
|
|
392
|
+
}
|
|
393
|
+
const pixelX = (x - this.minX) / this.worldWidth * this.resolutionX - 0.5;
|
|
394
|
+
const pixelY = (z - this.minZ) / this.worldDepth * this.resolutionY - 0.5;
|
|
395
|
+
const baseX = Math.floor(pixelX);
|
|
396
|
+
const baseY = Math.floor(pixelY);
|
|
397
|
+
const x0 = THREE.MathUtils.clamp(baseX, 0, this.resolutionX - 1);
|
|
398
|
+
const y0 = THREE.MathUtils.clamp(baseY, 0, this.resolutionY - 1);
|
|
399
|
+
const x1 = THREE.MathUtils.clamp(baseX + 1, 0, this.resolutionX - 1);
|
|
400
|
+
const y1 = THREE.MathUtils.clamp(baseY + 1, 0, this.resolutionY - 1);
|
|
401
|
+
const tx = THREE.MathUtils.clamp(pixelX - baseX, 0, 1);
|
|
402
|
+
const ty = THREE.MathUtils.clamp(pixelY - baseY, 0, 1);
|
|
403
|
+
const i00 = y0 * this.resolutionX + x0;
|
|
404
|
+
const i10 = y0 * this.resolutionX + x1;
|
|
405
|
+
const i01 = y1 * this.resolutionX + x0;
|
|
406
|
+
const i11 = y1 * this.resolutionX + x1;
|
|
407
|
+
const bilerp = (array, stride = 1, offset = 0) => {
|
|
408
|
+
const a = THREE.MathUtils.lerp(
|
|
409
|
+
array[i00 * stride + offset],
|
|
410
|
+
array[i10 * stride + offset],
|
|
411
|
+
tx,
|
|
412
|
+
);
|
|
413
|
+
const b = THREE.MathUtils.lerp(
|
|
414
|
+
array[i01 * stride + offset],
|
|
415
|
+
array[i11 * stride + offset],
|
|
416
|
+
tx,
|
|
417
|
+
);
|
|
418
|
+
return THREE.MathUtils.lerp(a, b, ty);
|
|
419
|
+
};
|
|
420
|
+
out.vx = bilerp(this.velocities, 2, 0);
|
|
421
|
+
out.vz = bilerp(this.velocities, 2, 1);
|
|
422
|
+
out.weight = bilerp(this.weights);
|
|
423
|
+
out.valid = bilerp(this.validity);
|
|
424
|
+
return out;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
/** Bilinear CPU sample in metres/second. Outside/solid/invalid = (0, 0). */
|
|
428
|
+
sampleAt(x, z, out = new THREE.Vector2()) {
|
|
429
|
+
const channels = this.sampleChannelsAt(x, z, WaterCurrentField._sampleChannels);
|
|
430
|
+
const scale = channels.weight * channels.valid * this.strength;
|
|
431
|
+
return out.set(channels.vx * scale, channels.vz * scale);
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
/** Fluid-domain weight after obstacle and validity feathering, 0..1. */
|
|
435
|
+
sampleWeightAt(x, z) {
|
|
436
|
+
const channels = this.sampleChannelsAt(x, z, WaterCurrentField._sampleChannels);
|
|
437
|
+
return clamp01(channels.weight * channels.valid);
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
dispose() {
|
|
441
|
+
if (this.disposed) return;
|
|
442
|
+
this.disposed = true;
|
|
443
|
+
this.texture.dispose();
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
WaterCurrentField._sampleChannels = { vx: 0, vz: 0, weight: 0, valid: 0 };
|
|
@@ -133,6 +133,9 @@ export function applyWaterSettingsToMaterial(material, options = {}) {
|
|
|
133
133
|
const waves = buildGerstnerWaves(settings);
|
|
134
134
|
|
|
135
135
|
writeWaveUniforms(material, waves);
|
|
136
|
+
if (uniforms.uSwashIncidenceX) {
|
|
137
|
+
uniforms.uSwashIncidenceX.value = waves[0]?.dirX ?? 0;
|
|
138
|
+
}
|
|
136
139
|
|
|
137
140
|
uniforms.uDetailNormalStrength.value = settings.detailNormalStrength;
|
|
138
141
|
uniforms.uDetailScale.value = settings.detailScale;
|
|
@@ -146,12 +149,16 @@ export function applyWaterSettingsToMaterial(material, options = {}) {
|
|
|
146
149
|
uniforms.uDeepFadeDistance.value = settings.deepFadeDistance;
|
|
147
150
|
uniforms.uOpacity.value = settings.opacity;
|
|
148
151
|
uniforms.uRefractionStrength.value = settings.refractionStrength;
|
|
152
|
+
uniforms.uIndexOfRefraction.value = settings.indexOfRefraction;
|
|
153
|
+
uniforms.uUnderwaterTransmission.value = settings.underwaterTransmission;
|
|
154
|
+
uniforms.uUnderwaterTintStrength.value = settings.underwaterTintStrength;
|
|
149
155
|
uniforms.uCausticsStrength.value = settings.causticsStrength;
|
|
150
156
|
uniforms.uCausticsScale.value = settings.causticsScale;
|
|
151
157
|
uniforms.uCausticsSpeed.value = settings.causticsSpeed;
|
|
152
158
|
|
|
153
159
|
setSrgbColorUniform(uniforms.uFoamColor, settings.foamColor);
|
|
154
160
|
uniforms.uFoamAmount.value = settings.foamAmount;
|
|
161
|
+
uniforms.uSwashFoamAmount.value = settings.swashFoamAmount;
|
|
155
162
|
uniforms.uFoamContactDistance.value = settings.foamContactDistance;
|
|
156
163
|
uniforms.uFoamLineSpacing.value = settings.foamLineSpacing;
|
|
157
164
|
uniforms.uFoamNoiseScale.value = settings.foamNoiseScale;
|
|
@@ -182,6 +189,11 @@ export function applyWaterSettingsToMaterial(material, options = {}) {
|
|
|
182
189
|
uniforms.uShoalingDepth.value = settings.shoalingDepth;
|
|
183
190
|
uniforms.uShorelineWaves.value = settings.shorelineWaves;
|
|
184
191
|
uniforms.uShorelineRunup.value = settings.shorelineRunup;
|
|
192
|
+
uniforms.uRunupDistance.value = settings.runupDistance;
|
|
193
|
+
if (uniforms.uBreakerEnabled) {
|
|
194
|
+
uniforms.uBreakerEnabled.value = settings.breakerEnabled === false ? 0 : 1;
|
|
195
|
+
}
|
|
196
|
+
if (uniforms.uBreakerAmount) uniforms.uBreakerAmount.value = settings.breakerAmount;
|
|
185
197
|
// Total swell energy — normalizes breaker foam and scales swash run-up.
|
|
186
198
|
const waveEnergy = waves.reduce((sum, wave) => sum + Math.abs(wave.amplitude), 0);
|
|
187
199
|
uniforms.uWaveEnergy.value = waveEnergy;
|