@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,320 @@
|
|
|
1
|
+
// CPU-baked nearshore phase field for the primary Gerstner swell and its
|
|
2
|
+
// optional same-direction set-beat partner.
|
|
3
|
+
//
|
|
4
|
+
// A spatial phase coordinate q(x,z) replaces the deep-water dot(direction,
|
|
5
|
+
// position) term close to bathymetry. Its gradient is the local wave vector
|
|
6
|
+
// expressed as a multiple of the offshore wave number:
|
|
7
|
+
//
|
|
8
|
+
// theta_i = k_i * q(x,z) - omega_i * time + phase_i
|
|
9
|
+
// grad(theta_i) = k_i * grad(q)
|
|
10
|
+
//
|
|
11
|
+
// Keeping omega fixed preserves the incident period. Increasing |grad(q)| in
|
|
12
|
+
// shallow water therefore lowers phase speed and shortens wavelength, while
|
|
13
|
+
// the direction of grad(q) bends crests around depth contours. The field is
|
|
14
|
+
// baked once when the static bed atlas is baked; WaterSurface packs it into
|
|
15
|
+
// one vec4 attribute (q, dq/dx, dq/dz, slot mask), avoiding another texture
|
|
16
|
+
// pass or per-wave root solve in the already-large WebGPU material.
|
|
17
|
+
|
|
18
|
+
const EPSILON = 1e-8;
|
|
19
|
+
|
|
20
|
+
function finitePositive(value, fallback) {
|
|
21
|
+
const number = Number(value);
|
|
22
|
+
return Number.isFinite(number) && number > 0 ? number : fallback;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function normalizedDirection(x, z) {
|
|
26
|
+
const length = Math.hypot(Number(x) || 0, Number(z) || 0);
|
|
27
|
+
return length > EPSILON
|
|
28
|
+
? { x: x / length, z: z / length }
|
|
29
|
+
: { x: 0, z: 1 };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Solves the finite-depth dispersion relation for a fixed incident period.
|
|
34
|
+
*
|
|
35
|
+
* buildGerstnerWaves authors omega from the deep-water k0 and then applies a
|
|
36
|
+
* user speed multiplier. Treating that multiplier as an effective gravity
|
|
37
|
+
* reduces the usual omega^2 = g*k*tanh(k*h) equation to:
|
|
38
|
+
*
|
|
39
|
+
* k * tanh(k*h) = k0
|
|
40
|
+
*
|
|
41
|
+
* This keeps the speed dial a uniform multiplier while retaining the correct
|
|
42
|
+
* deep- and shallow-water wavelength ratios.
|
|
43
|
+
*/
|
|
44
|
+
export function solveFiniteDepthWaveNumber(deepWaveNumber, waterDepth, iterations = 6) {
|
|
45
|
+
const k0 = finitePositive(deepWaveNumber, 1);
|
|
46
|
+
if (waterDepth === Infinity) return k0;
|
|
47
|
+
const h = finitePositive(waterDepth, 1e-4);
|
|
48
|
+
let k = Math.max(k0, Math.sqrt(k0 / h));
|
|
49
|
+
const count = Math.max(1, Math.min(Math.trunc(iterations) || 6, 12));
|
|
50
|
+
|
|
51
|
+
for (let i = 0; i < count; i += 1) {
|
|
52
|
+
const kh = k * h;
|
|
53
|
+
const tanhKh = Math.tanh(kh);
|
|
54
|
+
const derivative = tanhKh + kh * (1 - tanhKh * tanhKh);
|
|
55
|
+
if (!(derivative > EPSILON)) break;
|
|
56
|
+
k -= (k * tanhKh - k0) / derivative;
|
|
57
|
+
k = Math.max(k, k0);
|
|
58
|
+
}
|
|
59
|
+
return k;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function finiteDepthWaveNumberRatio(
|
|
63
|
+
deepWaveNumber,
|
|
64
|
+
waterDepth,
|
|
65
|
+
{ minDepth = 0.05, maxRatio = 4 } = {},
|
|
66
|
+
) {
|
|
67
|
+
const k0 = finitePositive(deepWaveNumber, 1);
|
|
68
|
+
const h = Math.max(Number(waterDepth) || 0, finitePositive(minDepth, 0.05));
|
|
69
|
+
const cap = Math.max(Number(maxRatio) || 1, 1);
|
|
70
|
+
return Math.min(solveFiniteDepthWaveNumber(k0, h) / k0, cap);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function assertGrid(restDepths, columns, rows) {
|
|
74
|
+
const nx = Math.trunc(columns);
|
|
75
|
+
const nz = Math.trunc(rows);
|
|
76
|
+
if (nx < 2 || nz < 2) {
|
|
77
|
+
throw new RangeError('Nearshore phase field requires at least a 2 x 2 grid.');
|
|
78
|
+
}
|
|
79
|
+
if (!restDepths || restDepths.length !== nx * nz) {
|
|
80
|
+
throw new RangeError(`Nearshore phase depth grid must contain ${nx * nz} samples.`);
|
|
81
|
+
}
|
|
82
|
+
return { nx, nz };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Builds a one-way upwind eikonal field on a regular world-space XZ grid. The
|
|
87
|
+
* incident edge retains the offshore plane-wave phase, and each successive
|
|
88
|
+
* slice solves the propagation component of |grad(q)| = k/k0 from the prior
|
|
89
|
+
* slice's transverse derivative. This is a mild-slope/ray approximation: it
|
|
90
|
+
* handles refraction but deliberately does not model diffraction or a ray
|
|
91
|
+
* turning back toward the incident boundary.
|
|
92
|
+
*
|
|
93
|
+
* Grid storage is row-major: index = zIndex * columns + xIndex.
|
|
94
|
+
*/
|
|
95
|
+
export function buildNearshorePhaseField({
|
|
96
|
+
restDepths,
|
|
97
|
+
columns,
|
|
98
|
+
rows,
|
|
99
|
+
originX,
|
|
100
|
+
originZ,
|
|
101
|
+
stepX,
|
|
102
|
+
stepZ,
|
|
103
|
+
directionX,
|
|
104
|
+
directionZ,
|
|
105
|
+
deepWaveNumber,
|
|
106
|
+
incidentAxis = null,
|
|
107
|
+
minDepth = 0.05,
|
|
108
|
+
maxWavenumberRatio = 4,
|
|
109
|
+
} = {}) {
|
|
110
|
+
const { nx, nz } = assertGrid(restDepths, columns, rows);
|
|
111
|
+
const x0 = Number(originX) || 0;
|
|
112
|
+
const z0 = Number(originZ) || 0;
|
|
113
|
+
const dxGrid = finitePositive(stepX, 1);
|
|
114
|
+
const dzGrid = finitePositive(stepZ, 1);
|
|
115
|
+
const direction = normalizedDirection(directionX, directionZ);
|
|
116
|
+
const k0 = finitePositive(deepWaveNumber, 1);
|
|
117
|
+
const ratioCap = Math.max(Number(maxWavenumberRatio) || 1, 1);
|
|
118
|
+
|
|
119
|
+
const ratios = new Float32Array(nx * nz);
|
|
120
|
+
for (let index = 0; index < ratios.length; index += 1) {
|
|
121
|
+
ratios[index] = finiteDepthWaveNumberRatio(k0, restDepths[index], {
|
|
122
|
+
minDepth,
|
|
123
|
+
maxRatio: ratioCap,
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Keep the integration in Float64. On a 100m+ water body, Float32 phase
|
|
128
|
+
// accumulation otherwise leaves small grid-aligned velocity changes.
|
|
129
|
+
const phase64 = new Float64Array(nx * nz);
|
|
130
|
+
const basePhase = (x, z) => direction.x * x + direction.z * z;
|
|
131
|
+
const indexOf = (ix, iz) => iz * nx + ix;
|
|
132
|
+
|
|
133
|
+
// Pick one incident boundary. A raw plane-phase condition on *both* inflow
|
|
134
|
+
// edges is wrong when a side edge already crosses changing bathymetry: that
|
|
135
|
+
// edge would inject undelayed shallow phase into the interior. Prefer the
|
|
136
|
+
// deepest plausible incoming edge. This choice deliberately does not vary
|
|
137
|
+
// with incidence angle: an angle-weighted score moved the old 45-degree pop
|
|
138
|
+
// to a different threshold. Callers that know their offshore axis should
|
|
139
|
+
// pass incidentAxis ('x' or 'z') and keep it stable across direction edits.
|
|
140
|
+
const setBoundary = (ix, iz) => {
|
|
141
|
+
const index = indexOf(ix, iz);
|
|
142
|
+
phase64[index] = basePhase(x0 + ix * dxGrid, z0 + iz * dzGrid);
|
|
143
|
+
};
|
|
144
|
+
const candidates = [];
|
|
145
|
+
if (Math.abs(direction.x) > EPSILON) {
|
|
146
|
+
const ix = direction.x > 0 ? 0 : nx - 1;
|
|
147
|
+
let averageRatio = 0;
|
|
148
|
+
for (let iz = 0; iz < nz; iz += 1) averageRatio += ratios[indexOf(ix, iz)];
|
|
149
|
+
candidates.push({ axis: 'x', index: ix, score: averageRatio / nz });
|
|
150
|
+
}
|
|
151
|
+
if (Math.abs(direction.z) > EPSILON) {
|
|
152
|
+
const iz = direction.z > 0 ? 0 : nz - 1;
|
|
153
|
+
let averageRatio = 0;
|
|
154
|
+
for (let ix = 0; ix < nx; ix += 1) averageRatio += ratios[indexOf(ix, iz)];
|
|
155
|
+
candidates.push({ axis: 'z', index: iz, score: averageRatio / nx });
|
|
156
|
+
}
|
|
157
|
+
candidates.sort((a, b) => a.score - b.score ||
|
|
158
|
+
Math.abs(direction[b.axis]) - Math.abs(direction[a.axis]) ||
|
|
159
|
+
a.axis.localeCompare(b.axis));
|
|
160
|
+
const requestedAxis = incidentAxis === 'x' || incidentAxis === 'z' ? incidentAxis : null;
|
|
161
|
+
const incident = candidates.find((candidate) => candidate.axis === requestedAxis) ?? candidates[0];
|
|
162
|
+
const valid = new Uint8Array(nx * nz);
|
|
163
|
+
valid.fill(1);
|
|
164
|
+
let invalidCount = 0;
|
|
165
|
+
const invalidate = (index) => {
|
|
166
|
+
if (valid[index]) {
|
|
167
|
+
valid[index] = 0;
|
|
168
|
+
invalidCount += 1;
|
|
169
|
+
}
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
if (incident.axis === 'x') {
|
|
173
|
+
for (let iz = 0; iz < nz; iz += 1) setBoundary(incident.index, iz);
|
|
174
|
+
} else {
|
|
175
|
+
for (let ix = 0; ix < nx; ix += 1) setBoundary(ix, incident.index);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (incident.axis === 'x') {
|
|
179
|
+
const stepSign = incident.index === 0 ? 1 : -1;
|
|
180
|
+
for (let offset = 1; offset < nx; offset += 1) {
|
|
181
|
+
const ix = incident.index + offset * stepSign;
|
|
182
|
+
const previousX = ix - stepSign;
|
|
183
|
+
for (let iz = 0; iz < nz; iz += 1) {
|
|
184
|
+
const zBefore = Math.max(iz - 1, 0);
|
|
185
|
+
const zAfter = Math.min(iz + 1, nz - 1);
|
|
186
|
+
const transverse = (
|
|
187
|
+
phase64[indexOf(previousX, zAfter)] - phase64[indexOf(previousX, zBefore)]
|
|
188
|
+
) / Math.max((zAfter - zBefore) * dzGrid, EPSILON);
|
|
189
|
+
const previousIndex = indexOf(previousX, iz);
|
|
190
|
+
const index = indexOf(ix, iz);
|
|
191
|
+
if (!valid[previousIndex]) invalidate(index);
|
|
192
|
+
const previousAxisSquared = ratios[previousIndex] * ratios[previousIndex] -
|
|
193
|
+
transverse * transverse;
|
|
194
|
+
const currentAxisSquared = ratios[index] * ratios[index] - transverse * transverse;
|
|
195
|
+
if (previousAxisSquared < 0 || currentAxisSquared < 0) invalidate(index);
|
|
196
|
+
const previousAxis = stepSign * Math.sqrt(Math.max(previousAxisSquared, EPSILON));
|
|
197
|
+
const currentAxis = stepSign * Math.sqrt(Math.max(currentAxisSquared, EPSILON));
|
|
198
|
+
phase64[index] = phase64[previousIndex] +
|
|
199
|
+
0.5 * (previousAxis + currentAxis) * stepSign * dxGrid;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
} else {
|
|
203
|
+
const stepSign = incident.index === 0 ? 1 : -1;
|
|
204
|
+
for (let offset = 1; offset < nz; offset += 1) {
|
|
205
|
+
const iz = incident.index + offset * stepSign;
|
|
206
|
+
const previousZ = iz - stepSign;
|
|
207
|
+
for (let ix = 0; ix < nx; ix += 1) {
|
|
208
|
+
const xBefore = Math.max(ix - 1, 0);
|
|
209
|
+
const xAfter = Math.min(ix + 1, nx - 1);
|
|
210
|
+
const transverse = (
|
|
211
|
+
phase64[indexOf(xAfter, previousZ)] - phase64[indexOf(xBefore, previousZ)]
|
|
212
|
+
) / Math.max((xAfter - xBefore) * dxGrid, EPSILON);
|
|
213
|
+
const previousIndex = indexOf(ix, previousZ);
|
|
214
|
+
const index = indexOf(ix, iz);
|
|
215
|
+
if (!valid[previousIndex]) invalidate(index);
|
|
216
|
+
const previousAxisSquared = ratios[previousIndex] * ratios[previousIndex] -
|
|
217
|
+
transverse * transverse;
|
|
218
|
+
const currentAxisSquared = ratios[index] * ratios[index] - transverse * transverse;
|
|
219
|
+
if (previousAxisSquared < 0 || currentAxisSquared < 0) invalidate(index);
|
|
220
|
+
const previousAxis = stepSign * Math.sqrt(Math.max(previousAxisSquared, EPSILON));
|
|
221
|
+
const currentAxis = stepSign * Math.sqrt(Math.max(currentAxisSquared, EPSILON));
|
|
222
|
+
phase64[index] = phase64[previousIndex] +
|
|
223
|
+
0.5 * (previousAxis + currentAxis) * stepSign * dzGrid;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const phaseCoordinate = new Float32Array(phase64);
|
|
229
|
+
const waveVector = new Float32Array(nx * nz * 2);
|
|
230
|
+
for (let iz = 0; iz < nz; iz += 1) {
|
|
231
|
+
const zBefore = Math.max(iz - 1, 0);
|
|
232
|
+
const zAfter = Math.min(iz + 1, nz - 1);
|
|
233
|
+
const dz = Math.max((zAfter - zBefore) * dzGrid, EPSILON);
|
|
234
|
+
for (let ix = 0; ix < nx; ix += 1) {
|
|
235
|
+
const xBefore = Math.max(ix - 1, 0);
|
|
236
|
+
const xAfter = Math.min(ix + 1, nx - 1);
|
|
237
|
+
const dx = Math.max((xAfter - xBefore) * dxGrid, EPSILON);
|
|
238
|
+
const index = indexOf(ix, iz);
|
|
239
|
+
let gx = (phase64[indexOf(xAfter, iz)] - phase64[indexOf(xBefore, iz)]) / dx;
|
|
240
|
+
let gz = (phase64[indexOf(ix, zAfter)] - phase64[indexOf(ix, zBefore)]) / dz;
|
|
241
|
+
let magnitude = Math.hypot(gx, gz);
|
|
242
|
+
if (!(magnitude > EPSILON) || !Number.isFinite(magnitude)) {
|
|
243
|
+
invalidate(index);
|
|
244
|
+
magnitude = ratios[index];
|
|
245
|
+
gx = direction.x * magnitude;
|
|
246
|
+
gz = direction.z * magnitude;
|
|
247
|
+
} else if (gx * direction.x + gz * direction.z <= 0) {
|
|
248
|
+
// Preserve the actual phase gradient for analytic Gerstner normals;
|
|
249
|
+
// replacing it independently would make q and grad(q) disagree.
|
|
250
|
+
invalidate(index);
|
|
251
|
+
}
|
|
252
|
+
waveVector[index * 2] = gx;
|
|
253
|
+
waveVector[index * 2 + 1] = gz;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
return {
|
|
258
|
+
columns: nx,
|
|
259
|
+
rows: nz,
|
|
260
|
+
originX: x0,
|
|
261
|
+
originZ: z0,
|
|
262
|
+
stepX: dxGrid,
|
|
263
|
+
stepZ: dzGrid,
|
|
264
|
+
directionX: direction.x,
|
|
265
|
+
directionZ: direction.z,
|
|
266
|
+
deepWaveNumber: k0,
|
|
267
|
+
incidentAxis: incident.axis,
|
|
268
|
+
invalidCount,
|
|
269
|
+
invalidFraction: invalidCount / (nx * nz),
|
|
270
|
+
phaseCoordinate,
|
|
271
|
+
validity: valid,
|
|
272
|
+
waveVector,
|
|
273
|
+
waveNumberRatio: ratios,
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/** Bilinearly samples the baked field for the CPU water-height mirror. */
|
|
278
|
+
export function sampleNearshorePhaseField(field, x, z, out = {}) {
|
|
279
|
+
const nx = field?.columns ?? 0;
|
|
280
|
+
const nz = field?.rows ?? 0;
|
|
281
|
+
if (nx < 2 || nz < 2) {
|
|
282
|
+
const direction = normalizedDirection(field?.directionX ?? 1, field?.directionZ ?? 0);
|
|
283
|
+
const worldX = Number(x) || 0;
|
|
284
|
+
const worldZ = Number(z) || 0;
|
|
285
|
+
out.phaseCoordinate = direction.x * worldX + direction.z * worldZ;
|
|
286
|
+
out.waveVectorX = direction.x;
|
|
287
|
+
out.waveVectorZ = direction.z;
|
|
288
|
+
return out;
|
|
289
|
+
}
|
|
290
|
+
const worldX = Number(x) || 0;
|
|
291
|
+
const worldZ = Number(z) || 0;
|
|
292
|
+
const unclampedX = (worldX - field.originX) / field.stepX;
|
|
293
|
+
const unclampedZ = (worldZ - field.originZ) / field.stepZ;
|
|
294
|
+
const fx = Math.min(Math.max(unclampedX, 0), nx - 1);
|
|
295
|
+
const fz = Math.min(Math.max(unclampedZ, 0), nz - 1);
|
|
296
|
+
const ix = Math.min(Math.floor(fx), nx - 2);
|
|
297
|
+
const iz = Math.min(Math.floor(fz), nz - 2);
|
|
298
|
+
const tx = fx - ix;
|
|
299
|
+
const tz = fz - iz;
|
|
300
|
+
const indices = [iz * nx + ix, iz * nx + ix + 1, (iz + 1) * nx + ix, (iz + 1) * nx + ix + 1];
|
|
301
|
+
const weights = [(1 - tx) * (1 - tz), tx * (1 - tz), (1 - tx) * tz, tx * tz];
|
|
302
|
+
let phase = 0;
|
|
303
|
+
let vectorX = 0;
|
|
304
|
+
let vectorZ = 0;
|
|
305
|
+
for (let i = 0; i < 4; i += 1) {
|
|
306
|
+
phase += field.phaseCoordinate[indices[i]] * weights[i];
|
|
307
|
+
vectorX += field.waveVector[indices[i] * 2] * weights[i];
|
|
308
|
+
vectorZ += field.waveVector[indices[i] * 2 + 1] * weights[i];
|
|
309
|
+
}
|
|
310
|
+
const clampedWorldX = field.originX + fx * field.stepX;
|
|
311
|
+
const clampedWorldZ = field.originZ + fz * field.stepZ;
|
|
312
|
+
// CPU callers can query just outside the finite mesh (wide splash rings,
|
|
313
|
+
// interaction margins). Continue the edge phase linearly instead of
|
|
314
|
+
// clamping q to a spatially constant value and stopping the wave there.
|
|
315
|
+
out.phaseCoordinate = phase + vectorX * (worldX - clampedWorldX) +
|
|
316
|
+
vectorZ * (worldZ - clampedWorldZ);
|
|
317
|
+
out.waveVectorX = vectorX;
|
|
318
|
+
out.waveVectorZ = vectorZ;
|
|
319
|
+
return out;
|
|
320
|
+
}
|
|
@@ -9,6 +9,7 @@ import { createPassDepthColorMaterial } from '../shaders-tsl/chunks/pass-depth-c
|
|
|
9
9
|
//
|
|
10
10
|
// Exclusion flags on object.userData:
|
|
11
11
|
// waterExclude — hidden from both passes
|
|
12
|
+
// waterGrabExclude — hidden from the above-water grab only
|
|
12
13
|
// waterReflectionExclude — hidden from the reflection pass only
|
|
13
14
|
// skipWaterReflection — legacy alias of waterReflectionExclude
|
|
14
15
|
//
|
|
@@ -70,10 +71,12 @@ export class WaterScenePasses {
|
|
|
70
71
|
|
|
71
72
|
this.grabTarget = null;
|
|
72
73
|
this.reflectionTarget = null;
|
|
74
|
+
this.transmissionCamera = new THREE.PerspectiveCamera();
|
|
73
75
|
this.reflectionCamera = new THREE.PerspectiveCamera();
|
|
74
76
|
this.reflectionMatrix = new THREE.Matrix4();
|
|
75
77
|
this.reflectionValid = false;
|
|
76
78
|
this.grabValid = false;
|
|
79
|
+
this.depthValid = false;
|
|
77
80
|
|
|
78
81
|
// Scene-depth pass state for the TSL renderer path.
|
|
79
82
|
this.depthTarget = null;
|
|
@@ -89,6 +92,7 @@ export class WaterScenePasses {
|
|
|
89
92
|
drawingBufferSize: new THREE.Vector2(),
|
|
90
93
|
cameraPosition: new THREE.Vector3(),
|
|
91
94
|
cameraDirection: new THREE.Vector3(),
|
|
95
|
+
cameraQuaternion: new THREE.Quaternion(),
|
|
92
96
|
target: new THREE.Vector3(),
|
|
93
97
|
plane: new THREE.Plane(),
|
|
94
98
|
clipPlane: new THREE.Vector4(),
|
|
@@ -265,11 +269,25 @@ export class WaterScenePasses {
|
|
|
265
269
|
|
|
266
270
|
renderGrabPass(renderer, scene, camera, waterMesh) {
|
|
267
271
|
this.ensureGrabTarget(renderer);
|
|
272
|
+
this.scratch.waterWorldPosition.setFromMatrixPosition(waterMesh.matrixWorld);
|
|
273
|
+
camera.getWorldPosition(this.scratch.cameraPosition);
|
|
274
|
+
const cameraBelow = this.scratch.cameraPosition.y < this.scratch.waterWorldPosition.y;
|
|
275
|
+
// Above water the grab sees the submerged scene. Below water it becomes a
|
|
276
|
+
// same-pose transmission camera clipped to the air side of the surface,
|
|
277
|
+
// so clouds and above-water objects are genuinely visible through it.
|
|
278
|
+
const grabCamera = cameraBelow
|
|
279
|
+
? this.updateTransmissionCamera(
|
|
280
|
+
renderer,
|
|
281
|
+
camera,
|
|
282
|
+
this.scratch.waterWorldPosition.y,
|
|
283
|
+
)
|
|
284
|
+
: camera;
|
|
268
285
|
// waterGrabExclude skips the refraction grab/depth renders only — for
|
|
269
286
|
// above-water set dressing (cliffs, large rocks) whose reflection
|
|
270
|
-
// matters but whose refracted contribution is invisible.
|
|
287
|
+
// matters but whose refracted contribution is invisible. From below,
|
|
288
|
+
// that set dressing belongs in the transmitted above-water view.
|
|
271
289
|
const restoreFlagged = hideFlagged(scene, (object) => object.userData?.waterExclude
|
|
272
|
-
|| object.userData?.waterGrabExclude);
|
|
290
|
+
|| (!cameraBelow && object.userData?.waterGrabExclude));
|
|
273
291
|
const waterWasVisible = waterMesh.visible;
|
|
274
292
|
waterMesh.visible = false;
|
|
275
293
|
const previousTarget = renderer.getRenderTarget();
|
|
@@ -280,8 +298,13 @@ export class WaterScenePasses {
|
|
|
280
298
|
renderer.shadowMap.autoUpdate = false;
|
|
281
299
|
renderer.setRenderTarget(this.grabTarget);
|
|
282
300
|
renderer.clear();
|
|
283
|
-
renderer.render(scene,
|
|
284
|
-
|
|
301
|
+
renderer.render(scene, grabCamera);
|
|
302
|
+
if (cameraBelow) {
|
|
303
|
+
this.depthValid = false;
|
|
304
|
+
} else {
|
|
305
|
+
this.renderDepthPass(renderer, scene, grabCamera);
|
|
306
|
+
this.depthValid = true;
|
|
307
|
+
}
|
|
285
308
|
this.grabValid = true;
|
|
286
309
|
} finally {
|
|
287
310
|
renderer.setRenderTarget(previousTarget);
|
|
@@ -292,6 +315,52 @@ export class WaterScenePasses {
|
|
|
292
315
|
}
|
|
293
316
|
}
|
|
294
317
|
|
|
318
|
+
applyWaterClipPlane(renderer, camera, waterY) {
|
|
319
|
+
// Oblique near-plane clipping (Lengyel), matching THREE.Reflector's
|
|
320
|
+
// coordinate-system-aware construction. The positive side of this plane
|
|
321
|
+
// is the air volume, even when the virtual camera itself is underwater.
|
|
322
|
+
const { plane, clipPlane, q } = this.scratch;
|
|
323
|
+
plane.normal.set(0, 1, 0);
|
|
324
|
+
plane.constant = -waterY;
|
|
325
|
+
plane.applyMatrix4(camera.matrixWorldInverse);
|
|
326
|
+
clipPlane.set(plane.normal.x, plane.normal.y, plane.normal.z, plane.constant);
|
|
327
|
+
|
|
328
|
+
const projectionMatrix = camera.projectionMatrix;
|
|
329
|
+
q.x = (Math.sign(clipPlane.x) + projectionMatrix.elements[8]) / projectionMatrix.elements[0];
|
|
330
|
+
q.y = (Math.sign(clipPlane.y) + projectionMatrix.elements[9]) / projectionMatrix.elements[5];
|
|
331
|
+
q.z = -1.0;
|
|
332
|
+
q.w = (1.0 + projectionMatrix.elements[10]) / projectionMatrix.elements[14];
|
|
333
|
+
clipPlane.multiplyScalar(1.0 / clipPlane.dot(q));
|
|
334
|
+
projectionMatrix.elements[2] = clipPlane.x;
|
|
335
|
+
projectionMatrix.elements[6] = clipPlane.y;
|
|
336
|
+
projectionMatrix.elements[10] = renderer.coordinateSystem === THREE.WebGPUCoordinateSystem
|
|
337
|
+
? clipPlane.z - this.clipBias
|
|
338
|
+
: clipPlane.z + 1.0 - this.clipBias;
|
|
339
|
+
projectionMatrix.elements[14] = clipPlane.w;
|
|
340
|
+
camera.projectionMatrixInverse.copy(projectionMatrix).invert();
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
updateTransmissionCamera(renderer, camera, waterY) {
|
|
344
|
+
const { cameraPosition, cameraQuaternion } = this.scratch;
|
|
345
|
+
camera.getWorldPosition(cameraPosition);
|
|
346
|
+
camera.getWorldQuaternion(cameraQuaternion);
|
|
347
|
+
|
|
348
|
+
const transmissionCamera = this.transmissionCamera;
|
|
349
|
+
transmissionCamera.coordinateSystem = renderer.coordinateSystem;
|
|
350
|
+
transmissionCamera.position.copy(cameraPosition);
|
|
351
|
+
transmissionCamera.quaternion.copy(cameraQuaternion);
|
|
352
|
+
transmissionCamera.scale.set(1, 1, 1);
|
|
353
|
+
transmissionCamera.near = camera.near;
|
|
354
|
+
transmissionCamera.far = camera.far;
|
|
355
|
+
transmissionCamera.layers.mask = camera.layers.mask;
|
|
356
|
+
transmissionCamera.projectionMatrix.copy(camera.projectionMatrix);
|
|
357
|
+
transmissionCamera.projectionMatrixInverse.copy(camera.projectionMatrixInverse);
|
|
358
|
+
transmissionCamera.updateMatrixWorld();
|
|
359
|
+
transmissionCamera.matrixWorldInverse.copy(transmissionCamera.matrixWorld).invert();
|
|
360
|
+
this.applyWaterClipPlane(renderer, transmissionCamera, waterY);
|
|
361
|
+
return transmissionCamera;
|
|
362
|
+
}
|
|
363
|
+
|
|
295
364
|
updateReflectionCamera(renderer, camera, waterY) {
|
|
296
365
|
const { cameraPosition, cameraDirection, target } = this.scratch;
|
|
297
366
|
camera.getWorldPosition(cameraPosition);
|
|
@@ -315,29 +384,7 @@ export class WaterScenePasses {
|
|
|
315
384
|
reflectionCamera.updateMatrixWorld();
|
|
316
385
|
reflectionCamera.matrixWorldInverse.copy(reflectionCamera.matrixWorld).invert();
|
|
317
386
|
|
|
318
|
-
|
|
319
|
-
// THREE.Reflector: geometry below the water plane never leaks into the
|
|
320
|
-
// mirrored image. The node backends use ReflectorNode's coordinate-
|
|
321
|
-
// system-aware variant of the same construction.
|
|
322
|
-
const { plane, clipPlane, q } = this.scratch;
|
|
323
|
-
plane.setFromNormalAndCoplanarPoint(
|
|
324
|
-
new THREE.Vector3(0, 1, 0),
|
|
325
|
-
new THREE.Vector3(0, waterY, 0));
|
|
326
|
-
plane.applyMatrix4(reflectionCamera.matrixWorldInverse);
|
|
327
|
-
clipPlane.set(plane.normal.x, plane.normal.y, plane.normal.z, plane.constant);
|
|
328
|
-
|
|
329
|
-
const projectionMatrix = reflectionCamera.projectionMatrix;
|
|
330
|
-
q.x = (Math.sign(clipPlane.x) + projectionMatrix.elements[8]) / projectionMatrix.elements[0];
|
|
331
|
-
q.y = (Math.sign(clipPlane.y) + projectionMatrix.elements[9]) / projectionMatrix.elements[5];
|
|
332
|
-
q.z = -1.0;
|
|
333
|
-
q.w = (1.0 + projectionMatrix.elements[10]) / projectionMatrix.elements[14];
|
|
334
|
-
clipPlane.multiplyScalar(1.0 / clipPlane.dot(q));
|
|
335
|
-
projectionMatrix.elements[2] = clipPlane.x;
|
|
336
|
-
projectionMatrix.elements[6] = clipPlane.y;
|
|
337
|
-
projectionMatrix.elements[10] = renderer.coordinateSystem === THREE.WebGPUCoordinateSystem
|
|
338
|
-
? clipPlane.z - this.clipBias
|
|
339
|
-
: clipPlane.z + 1.0 - this.clipBias;
|
|
340
|
-
projectionMatrix.elements[14] = clipPlane.w;
|
|
387
|
+
this.applyWaterClipPlane(renderer, reflectionCamera, waterY);
|
|
341
388
|
|
|
342
389
|
this.reflectionMatrix
|
|
343
390
|
.copy(BIAS_MATRIX)
|
|
@@ -382,7 +429,10 @@ export class WaterScenePasses {
|
|
|
382
429
|
|
|
383
430
|
render(renderer, scene, camera, waterMesh) {
|
|
384
431
|
if (this.sceneColorEnabled) this.renderGrabPass(renderer, scene, camera, waterMesh);
|
|
385
|
-
else
|
|
432
|
+
else {
|
|
433
|
+
this.grabValid = false;
|
|
434
|
+
this.depthValid = false;
|
|
435
|
+
}
|
|
386
436
|
if (this.reflectionEnabled) this.renderReflectionPass(renderer, scene, camera, waterMesh);
|
|
387
437
|
else this.reflectionValid = false;
|
|
388
438
|
}
|
|
@@ -392,11 +442,14 @@ export class WaterScenePasses {
|
|
|
392
442
|
if (!uniforms) return;
|
|
393
443
|
if (this.grabValid && this.grabTarget) {
|
|
394
444
|
uniforms.uSceneColor.value = this.grabTarget.texture;
|
|
395
|
-
uniforms.uSceneDepth.value = this.depthTarget.texture;
|
|
396
445
|
uniforms.uUseSceneColor.value = 1;
|
|
397
|
-
uniforms.uUseSceneDepth.value = 1;
|
|
398
446
|
} else {
|
|
399
447
|
uniforms.uUseSceneColor.value = 0;
|
|
448
|
+
}
|
|
449
|
+
if (this.depthValid && this.depthTarget) {
|
|
450
|
+
uniforms.uSceneDepth.value = this.depthTarget.texture;
|
|
451
|
+
uniforms.uUseSceneDepth.value = 1;
|
|
452
|
+
} else {
|
|
400
453
|
uniforms.uUseSceneDepth.value = 0;
|
|
401
454
|
}
|
|
402
455
|
if (this.reflectionValid && this.reflectionTarget) {
|