@call-me-sensei/toonlab 0.4.21 → 0.4.23
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/NPM-LIBRARY.md +87 -27
- package/README.md +11 -10
- package/agents/PROMPTS.md +17 -3
- package/agents/README.md +13 -3
- package/agents/claude/CLAUDE.md +3 -0
- package/agents/codex/AGENTS.md +112 -159
- package/agents/cursor/toonlab.mdc +7 -3
- package/agents/examples/game-foundation.mjs +104 -0
- package/agents/references/game-lifecycle.md +57 -0
- package/agents/references/runtime-entry-points.md +26 -0
- package/agents/references/style-bundles.md +4 -1
- package/agents/skills/claude/asset-sourcing/SKILL.md +7 -4
- package/agents/skills/claude/environment/SKILL.md +4 -0
- package/agents/skills/claude/game-dev/SKILL.md +63 -87
- package/agents/skills/claude/lighting/SKILL.md +36 -0
- package/agents/skills/claude/post-processing/SKILL.md +8 -3
- package/agents/skills/claude/rock-ground-shaders/SKILL.md +10 -15
- package/agents/skills/claude/rockgen/SKILL.md +52 -21
- package/agents/skills/claude/scene-style-application/SKILL.md +4 -0
- package/agents/skills/claude/style-presets/SKILL.md +11 -8
- package/agents/skills/claude/toon-shading/SKILL.md +4 -0
- package/agents/skills/claude/vegetation-sky/SKILL.md +13 -7
- package/agents/skills/claude/visual-verification/SKILL.md +45 -119
- package/agents/skills/claude/water/SKILL.md +6 -2
- package/agents/skills/codex/asset-sourcing/SKILL.md +7 -4
- package/agents/skills/codex/environment/SKILL.md +4 -0
- package/agents/skills/codex/game-dev/SKILL.md +63 -87
- package/agents/skills/codex/lighting/SKILL.md +36 -0
- package/agents/skills/codex/post-processing/SKILL.md +8 -3
- package/agents/skills/codex/rock-ground-shaders/SKILL.md +10 -15
- package/agents/skills/codex/rockgen/SKILL.md +52 -21
- package/agents/skills/codex/scene-style-application/SKILL.md +4 -0
- package/agents/skills/codex/style-presets/SKILL.md +11 -8
- package/agents/skills/codex/toon-shading/SKILL.md +4 -0
- package/agents/skills/codex/vegetation-sky/SKILL.md +13 -7
- package/agents/skills/codex/visual-verification/SKILL.md +45 -119
- package/agents/skills/codex/water/SKILL.md +6 -2
- package/database/apply-one-catalog-seed.mjs +63 -0
- package/database/providers.mjs +1 -0
- package/database/seeds/catalog/0005_2026-08-c8-first12.sql +1101 -0
- package/database/seeds/catalog/0006_2026-09-c8-first100.sql +14245 -0
- package/database/seeds/catalog/0007_2026-09-c8-first100-primary-model.sql +14845 -0
- package/mcp/lab-management.mjs +11 -8
- package/mcp/public-catalog.mjs +0 -16
- package/mcp/tree-lab-contract.mjs +2381 -0
- package/mcp/vite-plugin.mjs +5 -1
- package/package.json +29 -18
- package/src/catalog/officialCatalog.js +1 -0
- package/src/catalog/officialCatalogAssetRuntime.js +98 -1
- package/src/catalog/officialCatalogLod.js +160 -16
- package/src/catalog/officialCatalogPlacement.js +52 -12
- package/src/catalog/officialCatalogProvider.js +79 -8
- package/src/catalog/officialCatalogRockPackage.js +172 -0
- package/src/cloud/cloudReprojection.js +40 -7
- package/src/cloud/cloudVolume.js +44 -7
- package/src/cloud/noise/baseShapeVolume.js +8 -6
- package/src/cloud/noise/cirrusMap.js +2 -1
- package/src/cloud/noise/curlNoise.js +2 -1
- package/src/cloud/noise/erosionVolume.js +2 -1
- package/src/cloud/noise/textureCache.js +77 -0
- package/src/cloud/noise/weatherMap.js +2 -1
- package/src/core/sha256.js +83 -0
- package/src/ground-shader/groundShaderMaterial.js +12 -3
- package/src/rock-shader/rockMaterial.js +205 -28
- package/src/rock-shader/rockShaderRuntime.js +17 -1
- package/src/rock-shader/rockShaderSettings.js +16 -0
- package/src/rockgen/index.js +9 -0
- package/src/rockgen/lod/index.js +1 -0
- package/src/rockgen/lod/rockDenseFieldPolicy.js +143 -0
- package/src/rockgen/rockDocument.js +899 -35
- package/src/rockgen/surface/c7GeologySurface.js +39 -9
- package/src/rockgen/surface/naturalRockSurface.js +1948 -0
- package/src/shaders-tsl/chunks/projected-water-caustics.js +14 -2
- package/src/shaders-tsl/chunks/water-foam.js +6 -6
- package/src/shaders-tsl/chunks/water-lighting.js +28 -5
- package/src/shaders-tsl/chunks/water-shore-state.js +5 -2
- package/src/shaders-tsl/chunks/water-waves.js +10 -5
- package/src/shaders-tsl/flower.js +7 -4
- package/src/shaders-tsl/grass.js +4 -3
- package/src/shaders-tsl/water-shore-state-simulation.js +3 -2
- package/src/shaders-tsl/water.js +209 -41
- package/src/sky/skySystem.js +30 -7
- package/src/toon/settings/perspectiveRemovalSettings.js +1 -1
- package/src/toon/toonSettings.js +74 -0
- package/src/vegetation/stylizedFlower.js +6 -0
- package/src/version.js +1 -1
- package/src/water/waterDetailSpectrum.js +91 -0
- package/src/water/waterDynamics.js +422 -0
- package/src/water/waterFoamParticles.js +196 -0
- package/src/water/waterFoamTexture.js +57 -0
- package/src/water/waterHydrodynamics.js +209 -0
- package/src/water/waterMaterial.js +1 -0
- package/src/water/waterRenderExtension.js +107 -0
- package/src/water/waterScenePasses.js +14 -6
- package/src/water/waterSettings.js +53 -26
- package/src/water/waterShoreMaterial.js +38 -14
- package/src/water/waterSpectralOcean.js +137 -0
- package/src/water/waterStageSettings.js +19 -1
- package/src/water/waterSurface.js +62 -18
- package/src/water/waterUnderwaterAtmosphere.js +30 -3
- package/types/catalog/officialCatalog.d.ts +1 -0
- package/types/catalog/officialCatalogPlacement.d.ts +4 -0
- package/types/catalog/officialCatalogRockPackage.d.ts +68 -0
- package/types/cloud/noise/baseShapeVolume.d.ts +1 -1
- package/types/cloud/noise/textureCache.d.ts +12 -0
- package/types/core/sha256.d.ts +2 -0
- package/types/index.d.ts +30 -0
- package/types/rock-shader/rockMaterial.d.ts +3 -0
- package/types/rock-shader/rockShaderSettings.d.ts +2 -0
- package/types/rockgen/index.d.ts +1 -0
- package/types/rockgen/lod/index.d.ts +1 -0
- package/types/rockgen/lod/rockDenseFieldPolicy.d.ts +181 -0
- package/types/rockgen/rockDocument.d.ts +654 -71
- package/types/rockgen/surface/naturalRockSurface.d.ts +100 -0
- package/types/shaders-tsl/chunks/projected-water-caustics.d.ts +7 -1
- package/types/shaders-tsl/chunks/water-lighting.d.ts +4 -3
- package/types/shaders-tsl/water.d.ts +2 -1
- package/types/version.d.ts +1 -1
- package/types/water/waterDetailSpectrum.d.ts +13 -0
- package/types/water/waterDynamics.d.ts +89 -0
- package/types/water/waterFoamParticles.d.ts +32 -0
- package/types/water/waterFoamTexture.d.ts +1 -0
- package/types/water/waterHydrodynamics.d.ts +49 -0
- package/types/water/waterRenderExtension.d.ts +16 -0
- package/types/water/waterScenePasses.d.ts +1 -1
- package/types/water/waterSettings.d.ts +1 -0
- package/types/water/waterSpectralOcean.d.ts +17 -0
- package/types/water/waterStageSettings.d.ts +1 -1
- package/types/water/waterUnderwaterAtmosphere.d.ts +19 -4
package/src/toon/toonSettings.js
CHANGED
|
@@ -476,6 +476,78 @@ function descriptionForField(group, key) {
|
|
|
476
476
|
// whenever a new numeric setting would otherwise get a nonsense slider —
|
|
477
477
|
// name-pattern inference is a fallback, not a contract.
|
|
478
478
|
const FIELD_RANGE_OVERRIDES = Object.freeze({
|
|
479
|
+
'averageShadow.defaultStrength': { max: 1, min: 0, step: 0.01 },
|
|
480
|
+
'averageShadow.eyeStrength': { max: 1, min: 0, step: 0.01 },
|
|
481
|
+
'averageShadow.faceStrength': { max: 1, min: 0, step: 0.01 },
|
|
482
|
+
'averageShadow.hairStrength': { max: 1, min: 0, step: 0.01 },
|
|
483
|
+
'averageShadow.skinStrength': { max: 1, min: 0, step: 0.01 },
|
|
484
|
+
'celShade.bodyCelSoftness': { max: 1, min: 0.001, step: 0.001 },
|
|
485
|
+
'celShade.edgeAntiAliasStrength': { max: 4, min: 0, step: 0.01 },
|
|
486
|
+
'contactShadow.faceStrength': { max: 1, min: 0, step: 0.01 },
|
|
487
|
+
'contactShadow.fadeRange': { max: 1, min: 0.01, step: 0.001 },
|
|
488
|
+
'contactShadow.thresholdOffset': { max: 1, min: -0.05, step: 0.005 },
|
|
489
|
+
'eyeHighlight.maskStrength': { max: 1, min: 0, step: 0.01 },
|
|
490
|
+
'eyeHighlight.power': { max: 44, min: 1, step: 0.01 },
|
|
491
|
+
'faceLighting.faceCelSoftness': { max: 1, min: 0.001, step: 0.001 },
|
|
492
|
+
'faceLighting.faceSceneShadowStrength': { max: 1, min: 0, step: 0.01 },
|
|
493
|
+
'fur.density': { max: 6, min: 0.1, step: 0.01 },
|
|
494
|
+
'fur.rootOffset': { max: 0, min: -1, step: 0.005 },
|
|
495
|
+
'fur.shellCount': { max: 16, min: 1, step: 0.01 },
|
|
496
|
+
'glitter.density': { max: 1, min: 0.01, step: 0.01 },
|
|
497
|
+
'glitter.randomNormalStrength': { max: 1, min: 0, step: 0.01 },
|
|
498
|
+
'glitter.size': { max: 1, min: 0.05, step: 0.01 },
|
|
499
|
+
'hairHighlight.maskStrength': { max: 1, min: 0, step: 0.01 },
|
|
500
|
+
'hairHighlight.uvBandHalfWidth': { max: 0.08, min: 0.001, step: 0.0005 },
|
|
501
|
+
'materialMaps.aoStrength': { max: 1, min: 0, step: 0.01 },
|
|
502
|
+
'materialMaps.detailStrength': { max: 1, min: 0, step: 0.01 },
|
|
503
|
+
'materialMaps.matcapStrength': { max: 1, min: 0, step: 0.01 },
|
|
504
|
+
'materialMaps.metalnessStrength': { max: 1, min: 0, step: 0.01 },
|
|
505
|
+
'materialMaps.normalStrength': { max: 1, min: 0, step: 0.01 },
|
|
506
|
+
'materialMaps.rampStrength': { max: 1, min: 0, step: 0.01 },
|
|
507
|
+
'materialMaps.roughnessStrength': { max: 1, min: 0, step: 0.01 },
|
|
508
|
+
'materialMaps.specularColorStrength': { max: 1, min: 0, step: 0.01 },
|
|
509
|
+
'outline.defaultWidth': { max: 0.05, min: 0, step: 0.0005 },
|
|
510
|
+
'outline.depthOffset': { max: 0.05, min: 0, step: 0.005 },
|
|
511
|
+
'outline.eyeWidth': { max: 0.05, min: 0, step: 0.0005 },
|
|
512
|
+
'outline.faceWidth': { max: 0.05, min: 0, step: 0.0005 },
|
|
513
|
+
'outline.hairCutoutWidth': { max: 0.05, min: 0, step: 0.0005 },
|
|
514
|
+
'outline.hairWidth': { max: 0.05, min: 0, step: 0.0005 },
|
|
515
|
+
'outline.maxWidth': { max: 0.05, min: 0, step: 0.0005 },
|
|
516
|
+
'outline.metalWidth': { max: 0.05, min: 0, step: 0.0005 },
|
|
517
|
+
'outline.skinWidth': { max: 0.05, min: 0, step: 0.0005 },
|
|
518
|
+
'outline.transparentOverlayWidth': { max: 0.05, min: 0, step: 0.0005 },
|
|
519
|
+
'perspectiveRemoval.radius': { max: 2.8, min: 0.05, step: 0.01 },
|
|
520
|
+
'rimLight.depthFadeRange': { max: 1, min: 0.01, step: 0.001 },
|
|
521
|
+
'rimLight.depthSafeDistance': { max: 1, min: 0.05, step: 0.01 },
|
|
522
|
+
'rimLight.depthThresholdOffset': { max: 1, min: -0.05, step: 0.005 },
|
|
523
|
+
'rimLight.softness': { max: 1, min: 0.001, step: 0.01 },
|
|
524
|
+
'sceneShadow.defaultStrength': { max: 1, min: 0, step: 0.01 },
|
|
525
|
+
'sceneShadow.eyeStrength': { max: 1, min: 0, step: 0.01 },
|
|
526
|
+
'sceneShadow.faceStrength': { max: 1, min: 0, step: 0.01 },
|
|
527
|
+
'sceneShadow.shadowAreaStrength': { max: 1, min: 0, step: 0.01 },
|
|
528
|
+
'sceneShadow.skinStrength': { max: 1, min: 0, step: 0.01 },
|
|
529
|
+
'selfShadow.defaultStrength': { max: 1, min: 0, step: 0.01 },
|
|
530
|
+
'selfShadow.eyeStrength': { max: 1, min: 0, step: 0.01 },
|
|
531
|
+
'selfShadow.faceStrength': { max: 1, min: 0, step: 0.01 },
|
|
532
|
+
'selfShadow.hairStrength': { max: 1, min: 0, step: 0.01 },
|
|
533
|
+
'selfShadow.shadowAreaStrength': { max: 1, min: 0, step: 0.01 },
|
|
534
|
+
'selfShadow.skinStrength': { max: 1, min: 0, step: 0.01 },
|
|
535
|
+
'shadowColor.selfShadowAlbedoMulStrength': { max: 1, min: 0, step: 0.01 },
|
|
536
|
+
'shadowColor.selfShadowAreaHSVStrength': { max: 1, min: 0, step: 0.01 },
|
|
537
|
+
'shadowColor.selfShadowAreaSaturationBoost': { max: 1, min: 0, step: 0.01 },
|
|
538
|
+
'shadowColor.selfShadowAreaValueMul': { max: 1, min: 0, step: 0.01 },
|
|
539
|
+
'shadowColor.transitionAreaSaturationBoost': { max: 1, min: 0, step: 0.01 },
|
|
540
|
+
'shadowColor.transitionAreaValueMul': { max: 1, min: 0, step: 0.01 },
|
|
541
|
+
'skinTone.faceMaxDirectLight': { max: 8, min: 0.05, step: 0.05 },
|
|
542
|
+
'skinTone.faceShadowTintStrength': { max: 1, min: 0, step: 0.01 },
|
|
543
|
+
'skinTone.skinMaxDirectLight': { max: 8, min: 0.05, step: 0.05 },
|
|
544
|
+
'skinTone.skinShadowTintStrength': { max: 1, min: 0, step: 0.01 },
|
|
545
|
+
'specular.defaultMidPoint': { max: 1, min: 0, step: 0.005 },
|
|
546
|
+
'specular.defaultRange': { max: 1, min: 0.001, step: 0.001 },
|
|
547
|
+
'specular.eyeMidPoint': { max: 1, min: 0, step: 0.005 },
|
|
548
|
+
'specular.eyeRange': { max: 1, min: 0.001, step: 0.001 },
|
|
549
|
+
'specular.maskStrength': { max: 1, min: 0, step: 0.01 },
|
|
550
|
+
|
|
479
551
|
'outline.referenceDistance': { max: 20, min: 0.5, step: 0.1 },
|
|
480
552
|
'outline.referenceFov': { max: 120, min: 10, step: 1 },
|
|
481
553
|
'outline.widthFadeDistance': { max: 100, min: 1, step: 0.5 },
|
|
@@ -509,6 +581,7 @@ function isColorField(key, value) {
|
|
|
509
581
|
|
|
510
582
|
function fieldTypeFor(groupId, key, value) {
|
|
511
583
|
const fieldId = `${groupId}.${key}`;
|
|
584
|
+
if (groupId === 'indirectLight' && ['faceMinimumIndirectLight', 'skinMinimumIndirectLight'].includes(key)) return 'number';
|
|
512
585
|
if (FIELD_TYPE_OVERRIDES[fieldId]) return FIELD_TYPE_OVERRIDES[fieldId];
|
|
513
586
|
if (SELECT_FIELD_OPTIONS[fieldId]) return 'select';
|
|
514
587
|
if (typeof value === 'boolean') return 'boolean';
|
|
@@ -777,6 +850,7 @@ function coerceFieldValue(value, field) {
|
|
|
777
850
|
case 'boolean':
|
|
778
851
|
return booleanFromValue(value, fallback);
|
|
779
852
|
case 'number':
|
|
853
|
+
if (value === null && fallback === null) return null;
|
|
780
854
|
return numberFromValue(value, fallback);
|
|
781
855
|
case 'select':
|
|
782
856
|
return selectFromValue(value, fallback, field.options);
|
|
@@ -277,6 +277,12 @@ export class StylizedFlower extends StylizedTree {
|
|
|
277
277
|
: [options.sky.r, options.sky.g, options.sky.b];
|
|
278
278
|
uniforms.uSkyColor.value.setRGB(...sky, THREE.SRGBColorSpace);
|
|
279
279
|
}
|
|
280
|
+
if (Number.isFinite(options.intensity) && uniforms.uSunIntensity) {
|
|
281
|
+
uniforms.uSunIntensity.value = Math.max(options.intensity, 0);
|
|
282
|
+
}
|
|
283
|
+
if (Number.isFinite(options.skyIntensity) && uniforms.uSkyIntensity) {
|
|
284
|
+
uniforms.uSkyIntensity.value = Math.max(options.skyIntensity, 0);
|
|
285
|
+
}
|
|
280
286
|
return this;
|
|
281
287
|
}
|
|
282
288
|
|
package/src/version.js
CHANGED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import * as THREE from 'three';
|
|
2
|
+
import { inverseFFT2D } from './waterSpectralOcean.js';
|
|
3
|
+
|
|
4
|
+
// Short wind waves: many independently seeded Fourier modes, rather than
|
|
5
|
+
// a handful of full-strength plane waves. Both optics paths sample this field.
|
|
6
|
+
export class WaterDetailSpectrum {
|
|
7
|
+
constructor() {
|
|
8
|
+
this.size = 64;
|
|
9
|
+
this.signature = '';
|
|
10
|
+
this.time = NaN;
|
|
11
|
+
this.bands = [17, 7.3].map((length) => {
|
|
12
|
+
const n = this.size ** 2;
|
|
13
|
+
const band = { length, baseLength: length };
|
|
14
|
+
for (const key of ['r', 'i', 'kx', 'kz', 'omega', 'xr', 'xi', 'zr', 'zi']) band[key] = new Float64Array(n);
|
|
15
|
+
band.data = new Float32Array(n * 4);
|
|
16
|
+
band.texture = new THREE.DataTexture(band.data, this.size, this.size, THREE.RGBAFormat, THREE.FloatType);
|
|
17
|
+
band.texture.wrapS = band.texture.wrapT = THREE.RepeatWrapping;
|
|
18
|
+
band.texture.minFilter = THREE.LinearMipmapLinearFilter;
|
|
19
|
+
band.texture.magFilter = THREE.LinearFilter;
|
|
20
|
+
band.texture.generateMipmaps = true;
|
|
21
|
+
return band;
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
configure(settings) {
|
|
25
|
+
const direction = Math.atan2(settings.waveDirection[1], settings.waveDirection[0]);
|
|
26
|
+
const signature = JSON.stringify([direction, settings.detailScale]);
|
|
27
|
+
if (signature === this.signature) return;
|
|
28
|
+
this.signature = signature;
|
|
29
|
+
this.time = NaN;
|
|
30
|
+
const n = this.size, tau = 2 * Math.PI;
|
|
31
|
+
this.bands.forEach((band, index) => {
|
|
32
|
+
let seed = 9731 + index * 1237;
|
|
33
|
+
const random = () => ((seed = (Math.imul(seed, 1664525) + 1013904223) >>> 0) + 0.5) / 4294967296;
|
|
34
|
+
band.length = band.baseLength / settings.detailScale;
|
|
35
|
+
let variance = 0;
|
|
36
|
+
for (let z = 0; z < n; z++) for (let x = 0; x < n; x++) {
|
|
37
|
+
const i = z * n + x;
|
|
38
|
+
const fx = x < n / 2 ? x : x - n, fz = z < n / 2 ? z : z - n;
|
|
39
|
+
const baseK = Math.hypot(fx, fz) * tau / band.baseLength;
|
|
40
|
+
const wavelength = tau / Math.max(baseK, 1e-8);
|
|
41
|
+
const valid = index === 0 ? wavelength >= 0.95 && wavelength < 3.6 : wavelength >= 0.34 && wavelength < 0.95;
|
|
42
|
+
const kx = fx * tau / band.length, kz = fz * tau / band.length, k = Math.hypot(kx, kz);
|
|
43
|
+
band.kx[i] = kx; band.kz[i] = kz;
|
|
44
|
+
band.omega[i] = Math.sqrt(9.81 * k + 0.000074 * k ** 3);
|
|
45
|
+
// Broad directional spread and a broad frequency envelope keep one
|
|
46
|
+
// orientation or wavelength from becoming visible as a repeated grid.
|
|
47
|
+
const wind = Math.max(0, Math.cos(Math.atan2(kz, kx) - direction));
|
|
48
|
+
const envelope = Math.exp(-0.5 * (Math.log(wavelength / (index ? 0.6 : 1.7)) / 0.6) ** 2);
|
|
49
|
+
const amplitude = valid ? Math.sqrt(envelope * (0.12 + wind ** 4)) / Math.max(k * k, 1e-8) : 0;
|
|
50
|
+
const gaussian = Math.sqrt(-2 * Math.log(random())), phase = tau * random();
|
|
51
|
+
band.r[i] = amplitude * gaussian * Math.cos(phase);
|
|
52
|
+
band.i[i] = amplitude * gaussian * Math.sin(phase);
|
|
53
|
+
variance += 2 * (band.r[i] ** 2 + band.i[i] ** 2) * k * k / n ** 4;
|
|
54
|
+
}
|
|
55
|
+
const gain = (index ? 0.045 : 0.075) / Math.sqrt(Math.max(variance, 1e-20));
|
|
56
|
+
for (let i = 0; i < n * n; i++) { band.r[i] *= gain; band.i[i] *= gain; }
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
update(time) {
|
|
60
|
+
if (time === this.time) return;
|
|
61
|
+
this.time = time;
|
|
62
|
+
const n = this.size;
|
|
63
|
+
for (const b of this.bands) {
|
|
64
|
+
for (let z = 0; z < n; z++) for (let x = 0; x < n; x++) {
|
|
65
|
+
const i = z * n + x, j = ((n - z) % n) * n + (n - x) % n;
|
|
66
|
+
const phase = b.omega[i] * time, c = Math.cos(phase), s = Math.sin(phase);
|
|
67
|
+
const r = b.r[i] * c + b.i[i] * s + b.r[j] * c + b.i[j] * s;
|
|
68
|
+
const im = b.i[i] * c - b.r[i] * s - b.i[j] * c + b.r[j] * s;
|
|
69
|
+
b.xr[i] = -im * b.kx[i]; b.xi[i] = r * b.kx[i];
|
|
70
|
+
b.zr[i] = -im * b.kz[i]; b.zi[i] = r * b.kz[i];
|
|
71
|
+
}
|
|
72
|
+
inverseFFT2D(b.xr, b.xi, n); inverseFFT2D(b.zr, b.zi, n);
|
|
73
|
+
for (let i = 0; i < n * n; i++) { b.data[i * 4] = b.xr[i]; b.data[i * 4 + 1] = b.zr[i]; }
|
|
74
|
+
b.texture.needsUpdate = true;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
sample(x, z, out) {
|
|
78
|
+
out.x = out.z = 0;
|
|
79
|
+
const n = this.size;
|
|
80
|
+
for (const b of this.bands) {
|
|
81
|
+
const gx = ((x / b.length % 1) + 1) % 1 * n, gz = ((z / b.length % 1) + 1) % 1 * n;
|
|
82
|
+
const ix = Math.floor(gx), iz = Math.floor(gz), tx = gx - ix, tz = gz - iz;
|
|
83
|
+
const a = (iz * n + ix) * 4, c = (((iz + 1) % n) * n + ix) * 4;
|
|
84
|
+
const bb = (iz * n + (ix + 1) % n) * 4, d = (((iz + 1) % n) * n + (ix + 1) % n) * 4;
|
|
85
|
+
out.x += (b.data[a] * (1 - tx) + b.data[bb] * tx) * (1 - tz) + (b.data[c] * (1 - tx) + b.data[d] * tx) * tz;
|
|
86
|
+
out.z += (b.data[a + 1] * (1 - tx) + b.data[bb + 1] * tx) * (1 - tz) + (b.data[c + 1] * (1 - tx) + b.data[d + 1] * tx) * tz;
|
|
87
|
+
}
|
|
88
|
+
return out;
|
|
89
|
+
}
|
|
90
|
+
dispose() { for (const b of this.bands) b.texture.dispose(); }
|
|
91
|
+
}
|
|
@@ -0,0 +1,422 @@
|
|
|
1
|
+
import * as THREE from 'three';
|
|
2
|
+
import { WaterHydrodynamics } from './waterHydrodynamics.js';
|
|
3
|
+
import { WaterSpectralOcean } from './waterSpectralOcean.js';
|
|
4
|
+
import { WaterDetailSpectrum } from './waterDetailSpectrum.js';
|
|
5
|
+
import { WaterRenderExtension } from './waterRenderExtension.js';
|
|
6
|
+
import { WaterFoamParticles } from './waterFoamParticles.js';
|
|
7
|
+
import { buildGerstnerWaves } from './waterSettings.js';
|
|
8
|
+
// One authoritative, sampled water surface. The renderer and CPU contacts
|
|
9
|
+
// interpolate the same triangles. Style never changes the solved geometry.
|
|
10
|
+
export class WaterDynamics {
|
|
11
|
+
constructor(surface, options = {}) {
|
|
12
|
+
this.surface = surface;
|
|
13
|
+
this.isWaterDynamics = true;
|
|
14
|
+
this.options = options;
|
|
15
|
+
this.mode = options.mode ?? 'spectral';
|
|
16
|
+
this.time = 0;
|
|
17
|
+
this.columns = surface.geometry.parameters.widthSegments + 1;
|
|
18
|
+
this.rows = surface.geometry.parameters.heightSegments + 1;
|
|
19
|
+
this.state = new Float32Array(this.columns * this.rows * 4);
|
|
20
|
+
this.normals = new Float32Array(this.columns * this.rows * 3);
|
|
21
|
+
this.history = new Float32Array(this.columns * this.rows * 4);
|
|
22
|
+
this.foamCoordinates = new Float32Array(this.history.length);
|
|
23
|
+
this.previousFoamCoordinates = new Float32Array(this.history.length);
|
|
24
|
+
this.shoreDistances = new Float32Array(this.columns * this.rows);
|
|
25
|
+
this.shoreContour = new Float32Array(this.columns);
|
|
26
|
+
this.causticResolution = 256;
|
|
27
|
+
this.light = new Float32Array(this.causticResolution ** 2 * 4);
|
|
28
|
+
this.causticWork = new Float64Array(this.causticResolution ** 2);
|
|
29
|
+
this.causticRegion = new THREE.Vector4();
|
|
30
|
+
this.previousFoam = new Float32Array(this.columns * this.rows);
|
|
31
|
+
this.stateAttribute = new THREE.BufferAttribute(this.state, 4).setUsage(THREE.DynamicDrawUsage);
|
|
32
|
+
this.normalAttribute = new THREE.BufferAttribute(this.normals, 3).setUsage(THREE.DynamicDrawUsage);
|
|
33
|
+
surface.geometry.setAttribute('aWaterState', this.stateAttribute);
|
|
34
|
+
surface.geometry.setAttribute('aWaterNormal', this.normalAttribute);
|
|
35
|
+
surface.geometry.setAttribute('aWaterWetDistance',new THREE.BufferAttribute(this.shoreDistances,1).setUsage(THREE.DynamicDrawUsage));
|
|
36
|
+
const makeTexture = (array) => { const t = new THREE.DataTexture(array, this.columns, this.rows, THREE.RGBAFormat, THREE.FloatType); t.minFilter = t.magFilter = THREE.LinearFilter; t.generateMipmaps = false; return t; };
|
|
37
|
+
this.texture = makeTexture(this.history);
|
|
38
|
+
this.foamCoordinateTexture = makeTexture(this.foamCoordinates);
|
|
39
|
+
this.causticTexture = new THREE.DataTexture(this.light, this.causticResolution, this.causticResolution, THREE.RGBAFormat, THREE.FloatType);
|
|
40
|
+
this.causticTexture.minFilter = this.causticTexture.magFilter = THREE.LinearFilter;
|
|
41
|
+
this.causticTexture.generateMipmaps = false;
|
|
42
|
+
this.spectrum = new WaterSpectralOcean({ resolution: options.spectralResolution ?? 32 });
|
|
43
|
+
this.normalSpectrum = new WaterSpectralOcean({ resolution: options.spectralResolution ?? 32 });
|
|
44
|
+
this.detailSpectrum = new WaterDetailSpectrum();
|
|
45
|
+
const resolution = this.normalSpectrum.resolution;
|
|
46
|
+
this.normalTextures = Array.from({ length: 3 }, () => {
|
|
47
|
+
const texture = new THREE.DataTexture(new Float32Array(resolution * resolution * 4), resolution, resolution, THREE.RGBAFormat, THREE.FloatType);
|
|
48
|
+
texture.wrapS = texture.wrapT = THREE.RepeatWrapping;
|
|
49
|
+
texture.minFilter = texture.magFilter = THREE.LinearFilter;
|
|
50
|
+
texture.generateMipmaps = false;
|
|
51
|
+
return texture;
|
|
52
|
+
});
|
|
53
|
+
this.gridSignature = '';
|
|
54
|
+
this.reference = false;
|
|
55
|
+
this.paused = false;
|
|
56
|
+
this.pendingStep = 0;
|
|
57
|
+
this.sampleScratch = {};
|
|
58
|
+
this.settingsSignature = '';
|
|
59
|
+
}
|
|
60
|
+
getRegion(out = new THREE.Vector4()) { return out.set(this.x ?? 0, this.z ?? 0, (this.surface.width + (this.dx ?? 0)) / 2, (this.surface.depth + (this.dz ?? 0)) / 2); }
|
|
61
|
+
rebuild() {
|
|
62
|
+
this.foamParticles?.dispose();this.foamParticles=null;
|
|
63
|
+
this.renderExtension?.dispose();
|
|
64
|
+
this.renderExtension = null;
|
|
65
|
+
const s = this.surface, p = s.getWorldPosition(new THREE.Vector3());
|
|
66
|
+
this.x = p.x;
|
|
67
|
+
this.z = p.z;
|
|
68
|
+
this.level = p.y;
|
|
69
|
+
this.dx = s.width / (this.columns - 1);
|
|
70
|
+
this.dz = s.depth / (this.rows - 1);
|
|
71
|
+
this.minX = this.x - s.width / 2;
|
|
72
|
+
this.minZ = this.z - s.depth / 2;
|
|
73
|
+
this.bed = s.bedHeightSampler ?? (() => this.level - 20);
|
|
74
|
+
if (this.mode !== 'spectral') {
|
|
75
|
+
this.hydro = new WaterHydrodynamics({ columns: this.columns, rows: this.rows, width: s.width, depth: s.depth, centerX: this.x, centerZ: this.z, waterLevel: this.level, bedHeight: this.bed,
|
|
76
|
+
boundary: (side, x, z, time, ghost) => this.boundary(side, x, z, time, ghost), friction: this.mode === 'tank' ? 0 : 0.018 });
|
|
77
|
+
if (this.mode === 'river') {
|
|
78
|
+
for (let i = 0; i < this.hydro.length; i++)
|
|
79
|
+
this.hydro.qx[i] = this.hydro.h[i] * (s.settings.flowSpeed ?? 0);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
else
|
|
83
|
+
this.hydro = null;
|
|
84
|
+
this.interactions = null;
|
|
85
|
+
this.history.fill(0);
|
|
86
|
+
for(let z=0;z<this.rows;z++)for(let x=0;x<this.columns;x++) {
|
|
87
|
+
const i=(z*this.columns+x)*4;
|
|
88
|
+
this.foamCoordinates[i]=this.minX+x*this.dx;
|
|
89
|
+
this.foamCoordinates[i+1]=this.minZ+z*this.dz;
|
|
90
|
+
}
|
|
91
|
+
if (this.hydro) this.time = 0;
|
|
92
|
+
this.warmupRemaining = this.hydro ? Math.max(0, this.options.warmupSeconds ?? 0) : 0;
|
|
93
|
+
this.spectralTime ??= 0;
|
|
94
|
+
}
|
|
95
|
+
boundary(side, x, z, time, ghost) {
|
|
96
|
+
const settings = this.surface.renderedSettings ?? this.surface.settings;
|
|
97
|
+
if (this.mode === 'river' && (side === 'west' || side === 'east')) {
|
|
98
|
+
if (side === 'west') {
|
|
99
|
+
ghost.h = Math.max(0, this.level - ghost.bed);
|
|
100
|
+
ghost.qx = ghost.h * settings.flowSpeed;
|
|
101
|
+
ghost.qz = 0;
|
|
102
|
+
}
|
|
103
|
+
return true; // east is a zero-gradient outflow; north/south remain walls
|
|
104
|
+
}
|
|
105
|
+
if (this.mode === 'coast' && side === 'south') {
|
|
106
|
+
const h0 = Math.max(this.level - ghost.bed, 0), c = Math.sqrt(9.81 * Math.max(h0, 0.01));
|
|
107
|
+
if (h0 <= 1e-5) {
|
|
108
|
+
ghost.h = ghost.qx = ghost.qz = 0;
|
|
109
|
+
return true;
|
|
110
|
+
}
|
|
111
|
+
// Prescribe only the incoming characteristic. Retain the outgoing one
|
|
112
|
+
// from the interior so reflected waves can leave through this boundary.
|
|
113
|
+
let elevation = 0;
|
|
114
|
+
for (const w of this.incidentWaves ?? [])
|
|
115
|
+
elevation += w.amplitude * Math.sin(w.waveNumber * (w.dirX * x + w.dirZ * z) - w.omega * time + w.phase);
|
|
116
|
+
// Bound incident height by the available column. Lowering the
|
|
117
|
+
// water level must not inject a singular shallow-boundary jet.
|
|
118
|
+
elevation = h0 * 0.35 * Math.tanh(elevation / (h0 * 0.35));
|
|
119
|
+
elevation *= Math.min(time / 3, 1); // smooth startup avoids an impulsive boundary step
|
|
120
|
+
const incoming = 2 * c + 2 * Math.sqrt(9.81 / h0) * elevation;
|
|
121
|
+
const outgoing = (ghost.h > 1e-5 ? ghost.qz / ghost.h : 0) - 2 * Math.sqrt(9.81 * ghost.h);
|
|
122
|
+
const v = (incoming + outgoing) / 2, cb = Math.max((incoming - outgoing) / 4, 0);
|
|
123
|
+
ghost.h = cb * cb / 9.81;
|
|
124
|
+
ghost.qz = ghost.h * v;
|
|
125
|
+
ghost.qx = 0;
|
|
126
|
+
return true;
|
|
127
|
+
}
|
|
128
|
+
return false;
|
|
129
|
+
}
|
|
130
|
+
update(delta) {
|
|
131
|
+
const s = this.surface, p = s.getWorldPosition(new THREE.Vector3());
|
|
132
|
+
const signature = JSON.stringify([p.x, p.y, p.z, this.mode]);
|
|
133
|
+
if (signature !== this.gridSignature || this.bed !== s.bedHeightSampler && s.bedHeightSampler) {
|
|
134
|
+
this.gridSignature = signature;
|
|
135
|
+
this.rebuild();
|
|
136
|
+
}
|
|
137
|
+
const dt = this.paused ? this.pendingStep : Math.max(0, Math.min(delta, 0.1));
|
|
138
|
+
let transportDelta = dt;
|
|
139
|
+
this.pendingStep = 0;
|
|
140
|
+
this.time += dt;
|
|
141
|
+
const settings = s.renderedSettings ?? s.settings;
|
|
142
|
+
const sig = JSON.stringify([settings.waveAmplitude, settings.waveIntensity, settings.waveLength, settings.waveDirection, settings.waveDirectionSpread, settings.waveSpeed, settings.waveSetPeriod, settings.waveSetStrength]);
|
|
143
|
+
if (sig !== this.settingsSignature) {
|
|
144
|
+
this.settingsSignature = sig;
|
|
145
|
+
this.incidentWaves = buildGerstnerWaves(settings);
|
|
146
|
+
}
|
|
147
|
+
if (this.hydro) {
|
|
148
|
+
this.hydro.foamLifetime = settings.swashFoamLifetime;
|
|
149
|
+
this.hydro.foamResidueLifetime = settings.swashFoamResidueLifetime;
|
|
150
|
+
// Amortize the physical pre-roll over frames; never fake the
|
|
151
|
+
// shoreline position or add an unaccounted water source.
|
|
152
|
+
const preroll = dt > 0 ? Math.min(this.warmupRemaining, 0.25) : 0;
|
|
153
|
+
transportDelta += preroll;
|
|
154
|
+
this.hydro.advance(dt + preroll);
|
|
155
|
+
this.warmupRemaining -= preroll;
|
|
156
|
+
this.time = this.hydro.time;
|
|
157
|
+
}
|
|
158
|
+
else {
|
|
159
|
+
this.spectrum.configure(settings, Math.max(1, this.level - this.bed(this.x, this.z)), Math.max(this.dx, this.dz) * 2.5);
|
|
160
|
+
this.spectralTime += (dt * settings.waveSpeed);
|
|
161
|
+
this.spectrum.update(this.spectralTime);
|
|
162
|
+
this.normalSpectrum.configure(settings, Math.max(1, this.level - this.bed(this.x, this.z)));
|
|
163
|
+
this.normalSpectrum.update(this.spectralTime);
|
|
164
|
+
const lengths = [];
|
|
165
|
+
this.normalSpectrum.bands.forEach((band, index) => {
|
|
166
|
+
const n = band.size, step = band.length / n, data = this.normalTextures[index].image.data;
|
|
167
|
+
for (let z = 0; z < n; z++)
|
|
168
|
+
for (let x = 0; x < n; x++) {
|
|
169
|
+
const i = z * n + x;
|
|
170
|
+
data[i * 4] = (band.real[z * n + (x + 1) % n] - band.real[z * n + (x + n - 1) % n]) / (2 * step);
|
|
171
|
+
data[i * 4 + 1] = (band.real[((z + 1) % n) * n + x] - band.real[((z + n - 1) % n) * n + x]) / (2 * step);
|
|
172
|
+
}
|
|
173
|
+
this.normalTextures[index].needsUpdate = true;
|
|
174
|
+
this.surface.material.uniforms['uSpectralNormal' + index].value = this.normalTextures[index];
|
|
175
|
+
lengths.push(band.length);
|
|
176
|
+
});
|
|
177
|
+
this.surface.material.uniforms.uSpectralLengths.value.set(...lengths);
|
|
178
|
+
this.interactions?.advance(dt);
|
|
179
|
+
}
|
|
180
|
+
const nx = this.columns, nz = this.rows;
|
|
181
|
+
let minimumY = Infinity, maximumY = -Infinity;
|
|
182
|
+
for (let i=0;i<this.previousFoam.length;i++) this.previousFoam[i]=this.history[i*4+2];
|
|
183
|
+
for (let iz = 0; iz < nz; iz++)
|
|
184
|
+
for (let ix = 0; ix < nx; ix++) {
|
|
185
|
+
const i = iz * nx + ix, j = i * 4, x = this.minX + ix * this.dx, z = this.minZ + iz * this.dz, bed = this.bed(x, z);
|
|
186
|
+
let height, depth, u = 0, v = 0, foam = 0;
|
|
187
|
+
if (this.hydro) {
|
|
188
|
+
depth = this.hydro.h[i];
|
|
189
|
+
height = bed + depth;
|
|
190
|
+
u = depth > 1e-5 ? this.hydro.qx[i] / depth : 0;
|
|
191
|
+
v = depth > 1e-5 ? this.hydro.qz[i] / depth : 0;
|
|
192
|
+
foam = this.hydro.foam[i];
|
|
193
|
+
}
|
|
194
|
+
else {
|
|
195
|
+
const restDepth = Math.max(this.level - bed, 0);
|
|
196
|
+
const wave = this.spectrum.sample(x - settings.flowDirection[0] * settings.flowSpeed * this.time, z - settings.flowDirection[1] * settings.flowSpeed * this.time)
|
|
197
|
+
+ (this.interactions ? this.interactions.h[i] + this.interactions.bed[i] - this.level : 0);
|
|
198
|
+
// Offshore linear waves taper smoothly as their depth-limited height
|
|
199
|
+
// is approached; no clipped plateaus or disconnected curling shells.
|
|
200
|
+
const allowance = 0.35 * restDepth;
|
|
201
|
+
const displacement = allowance > 1e-5 ? allowance * Math.tanh(wave / allowance) : 0;
|
|
202
|
+
height = this.level + displacement;
|
|
203
|
+
depth = Math.max(height - bed, 0);
|
|
204
|
+
u = settings.flowDirection[0] * settings.flowSpeed;
|
|
205
|
+
v = settings.flowDirection[1] * settings.flowSpeed;
|
|
206
|
+
if (this.interactions && this.interactions.h[i] > 1e-5) {
|
|
207
|
+
u += this.interactions.qx[i] / this.interactions.h[i];
|
|
208
|
+
v += this.interactions.qz[i] / this.interactions.h[i];
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
minimumY = Math.min(minimumY, height);
|
|
212
|
+
maximumY = Math.max(maximumY, height);
|
|
213
|
+
this.state[j] = height;
|
|
214
|
+
this.state[j + 1] = depth;
|
|
215
|
+
this.state[j + 2] = u;
|
|
216
|
+
this.state[j + 3] = v;
|
|
217
|
+
this.history[j] = depth > 0.002 ? 1 : this.history[j] * Math.exp(-dt / Math.max(settings.wetSandDryTime, 1));
|
|
218
|
+
this.history[j + 1] = depth; // solved field stores metres, not a binary wet-film flag
|
|
219
|
+
if (this.hydro)
|
|
220
|
+
this.history[j + 2] = foam;
|
|
221
|
+
this.history[j + 3] = foam * (depth < 0.01 ? 1 : 0);
|
|
222
|
+
}
|
|
223
|
+
// Frame and optical focusing derive from this exact surface, including
|
|
224
|
+
// wet/dry cells. No unrelated caustic animation clock or noise texture.
|
|
225
|
+
for (let iz = 0; iz < nz; iz++)
|
|
226
|
+
for (let ix = 0; ix < nx; ix++) {
|
|
227
|
+
const i = iz * nx + ix, j = i * 4, left = (iz * nx + Math.max(0, ix - 1)) * 4, right = (iz * nx + Math.min(nx - 1, ix + 1)) * 4;
|
|
228
|
+
const down = (Math.max(0, iz - 1) * nx + ix) * 4, up = (Math.min(nz - 1, iz + 1) * nx + ix) * 4;
|
|
229
|
+
const gx = (this.state[right] - this.state[left]) / (this.dx * (ix > 0 && ix < nx - 1 ? 2 : 1));
|
|
230
|
+
const gz = (this.state[up] - this.state[down]) / (this.dz * (iz > 0 && iz < nz - 1 ? 2 : 1));
|
|
231
|
+
const inv = 1 / Math.hypot(gx, 1, gz);
|
|
232
|
+
this.normals[i * 3] = -gx * inv;
|
|
233
|
+
this.normals[i * 3 + 1] = inv;
|
|
234
|
+
this.normals[i * 3 + 2] = -gz * inv;
|
|
235
|
+
if (!this.hydro) {
|
|
236
|
+
const source = Math.max(0, Math.hypot(gx, gz) - 0.24) * 3;
|
|
237
|
+
const tracedX = Math.max(0, Math.min(nx-1, ix-this.state[j+2]*dt/this.dx));
|
|
238
|
+
const tracedZ = Math.max(0, Math.min(nz-1, iz-this.state[j+3]*dt/this.dz));
|
|
239
|
+
const x0 = Math.min(nx-2, Math.floor(tracedX)), z0 = Math.min(nz-2, Math.floor(tracedZ));
|
|
240
|
+
const tx = tracedX-x0, tz = tracedZ-z0, fi=z0*nx+x0, old=this.previousFoam;
|
|
241
|
+
const advected = (old[fi]*(1-tx)+old[fi+1]*tx)*(1-tz)+(old[fi+nx]*(1-tx)+old[fi+nx+1]*tx)*tz;
|
|
242
|
+
this.history[j + 2] = Math.min(1, advected * Math.exp(-dt / 3) + source * dt);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
this.updateShoreContour();
|
|
246
|
+
this.updateFoamCoordinates(transportDelta);
|
|
247
|
+
if(this.options.foamParticles&&this.hydro) {
|
|
248
|
+
this.foamParticles ??= new WaterFoamParticles(this);
|
|
249
|
+
this.foamParticles.update(transportDelta);
|
|
250
|
+
}
|
|
251
|
+
s.geometry.boundingBox ??= new THREE.Box3();
|
|
252
|
+
s.geometry.boundingBox.min.set(-s.width / 2, minimumY - this.level, -s.depth / 2);
|
|
253
|
+
s.geometry.boundingBox.max.set(s.width / 2, maximumY - this.level, s.depth / 2);
|
|
254
|
+
s.geometry.boundingSphere ??= new THREE.Sphere();
|
|
255
|
+
s.geometry.boundingBox.getBoundingSphere(s.geometry.boundingSphere);
|
|
256
|
+
if (this.options.renderExtent && this.mode === 'coast') {
|
|
257
|
+
this.renderExtension ??= new WaterRenderExtension(this, this.options.renderExtent);
|
|
258
|
+
this.renderExtension.update();
|
|
259
|
+
}
|
|
260
|
+
this.detailSpectrum.configure(settings);
|
|
261
|
+
this.detailSpectrum.update(this.time);
|
|
262
|
+
this.detailSpectrum.bands.forEach((band, i) => {
|
|
263
|
+
this.surface.material.uniforms['uDetailNormal' + i].value = band.texture;
|
|
264
|
+
});
|
|
265
|
+
this.surface.material.uniforms.uDetailLengths.value.set(...this.detailSpectrum.bands.map(b => b.length));
|
|
266
|
+
this.updateCaustics();
|
|
267
|
+
this.stateAttribute.needsUpdate = true;
|
|
268
|
+
this.normalAttribute.needsUpdate = true;
|
|
269
|
+
this.texture.needsUpdate = true;
|
|
270
|
+
this.causticTexture.needsUpdate = true;
|
|
271
|
+
this.surface.material.uniforms.uReferenceView.value = this.reference ? 1 : 0;
|
|
272
|
+
this.surface.material.uniforms.uUseShoreContour.value = this.options.shorelineAxis === 'z' ? 1 : 0;
|
|
273
|
+
this.surface.material.uniforms.uFoamCoordinates.value = this.foamCoordinateTexture;
|
|
274
|
+
this.surface.material.uniforms.uContinueShoreAlongX.value = this.renderExtension ? 1 : 0;
|
|
275
|
+
this.surface.material.uniforms.uUseSpectralNormals.value = this.hydro ? 0 : 1;
|
|
276
|
+
if (this.mode === 'tank')
|
|
277
|
+
this.surface.material.uniforms.uWaveEnergy.value = 0;
|
|
278
|
+
}
|
|
279
|
+
updateShoreContour() {
|
|
280
|
+
if(this.options.shorelineAxis !== 'z') return;
|
|
281
|
+
for(let x=0;x<this.columns;x++) {
|
|
282
|
+
let edge=this.state[x*4+1]>.002 ? this.minZ+this.surface.depth+this.dz : this.minZ-this.dz;
|
|
283
|
+
for(let z=0;z<this.rows-1;z++) {
|
|
284
|
+
const a=this.state[(z*this.columns+x)*4+1],b=this.state[((z+1)*this.columns+x)*4+1];
|
|
285
|
+
if(a>=.002&&b<.002){
|
|
286
|
+
const i=z*this.columns+x,next=i+this.columns,previous=Math.max(0,z-1)*this.columns+x;
|
|
287
|
+
const eta=this.state[i*4],etaSlope=z>0?eta-this.state[previous*4]:0;
|
|
288
|
+
const nextBed=this.state[next*4]-b;
|
|
289
|
+
// Dry cells store h=0, not the negative signed depth of
|
|
290
|
+
// the water/bed intersection. Extrapolate the adjacent
|
|
291
|
+
// wet free surface before finding that intersection.
|
|
292
|
+
const signedNext=Math.min(b,eta+etaSlope-nextBed);
|
|
293
|
+
edge=this.minZ+(z+(a-.002)/Math.max(a-signedNext,1e-8))*this.dz;
|
|
294
|
+
break;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
this.shoreContour[x]=edge;
|
|
298
|
+
for(let z=0;z<this.rows;z++)this.shoreDistances[z*this.columns+x]=edge-(this.minZ+z*this.dz);
|
|
299
|
+
}
|
|
300
|
+
this.surface.geometry.attributes.aWaterWetDistance.needsUpdate=true;
|
|
301
|
+
}
|
|
302
|
+
updateFoamCoordinates(dt) {
|
|
303
|
+
const old=this.previousFoamCoordinates,nx=this.columns,nz=this.rows;
|
|
304
|
+
old.set(this.foamCoordinates);
|
|
305
|
+
for(let z=0;z<nz;z++)for(let x=0;x<nx;x++) {
|
|
306
|
+
const i=(z*nx+x)*4;
|
|
307
|
+
if(this.history[i+2]<.002) {
|
|
308
|
+
this.foamCoordinates[i]=this.minX+x*this.dx;this.foamCoordinates[i+1]=this.minZ+z*this.dz;
|
|
309
|
+
continue;
|
|
310
|
+
}
|
|
311
|
+
const gx=Math.max(0,Math.min(nx-1,x-this.state[i+2]*dt/this.dx));
|
|
312
|
+
const gz=Math.max(0,Math.min(nz-1,z-this.state[i+3]*dt/this.dz));
|
|
313
|
+
const a=Math.min(nx-2,Math.floor(gx)),b=Math.min(nz-2,Math.floor(gz)),tx=gx-a,tz=gz-b,j=(b*nx+a)*4;
|
|
314
|
+
for(let c=0;c<2;c++)this.foamCoordinates[i+c]=(old[j+c]*(1-tx)+old[j+4+c]*tx)*(1-tz)+(old[j+nx*4+c]*(1-tx)+old[j+(nx+1)*4+c]*tx)*tz;
|
|
315
|
+
}
|
|
316
|
+
this.foamCoordinateTexture.needsUpdate=true;
|
|
317
|
+
}
|
|
318
|
+
updateCaustics() {
|
|
319
|
+
// Resolve focusing at a finer scale than the water mesh. A metre-wide
|
|
320
|
+
// ray grid cannot see the sub-metre ripples responsible for caustics.
|
|
321
|
+
const n=this.causticResolution, size=32, step=size/n;
|
|
322
|
+
const center=this.options.causticsCenter ?? [this.x,this.z];
|
|
323
|
+
this.causticRegion.set(center[0],center[1],size/2,size/2);
|
|
324
|
+
this.causticWork.fill(0);
|
|
325
|
+
const settings=this.surface.renderedSettings ?? this.surface.settings;
|
|
326
|
+
const sun=settings.sunDirection,l=Math.hypot(...sun)||1;
|
|
327
|
+
const ix=-sun[0]/l,iy=-sun[1]/l,iz=-sun[2]/l,eta=1/settings.indexOfRefraction;
|
|
328
|
+
if(iy>-0.02 || settings.causticsStrength<=0) {this.light.fill(0);return;}
|
|
329
|
+
const activity=this.mode==='tank'?0:Math.min(this.surface.waveEnergy/0.35,1);
|
|
330
|
+
const styleGain=this.reference?1:settings.detailNormalStrength*0.45+1;
|
|
331
|
+
const detailSlope = {x:0,z:0};
|
|
332
|
+
for(let z=0;z<n;z++)for(let x=0;x<n;x++) {
|
|
333
|
+
const wx=center[0]-size/2+(x+0.5)*step,wz=center[1]-size/2+(z+0.5)*step;
|
|
334
|
+
const gx=Math.max(0,Math.min(this.columns-1,(wx-this.minX)/this.dx));
|
|
335
|
+
const gz=Math.max(0,Math.min(this.rows-1,(wz-this.minZ)/this.dz));
|
|
336
|
+
const a=Math.min(this.columns-2,Math.floor(gx)),b=Math.min(this.rows-2,Math.floor(gz)),tx=gx-a,tz=gz-b;
|
|
337
|
+
const i=b*this.columns+a;
|
|
338
|
+
const sample=(array,stride,component)=>
|
|
339
|
+
(array[i*stride+component]*(1-tx)+array[(i+1)*stride+component]*tx)*(1-tz)+
|
|
340
|
+
(array[(i+this.columns)*stride+component]*(1-tx)+array[(i+this.columns+1)*stride+component]*tx)*tz;
|
|
341
|
+
const h=sample(this.state,4,1);if(h<0.02)continue;
|
|
342
|
+
let nx=sample(this.normals,3,0),ny=sample(this.normals,3,1),nz=sample(this.normals,3,2);
|
|
343
|
+
const ax=wx-settings.flowDirection[0]*settings.flowSpeed*this.time,az=wz-settings.flowDirection[1]*settings.flowSpeed*this.time;
|
|
344
|
+
const wetT=Math.max(0,Math.min(1,(h-0.025)/0.195)), wetRipple=wetT*wetT*(3-2*wetT);
|
|
345
|
+
this.detailSpectrum.sample(ax,az,detailSlope);
|
|
346
|
+
nx-=detailSlope.x*activity*styleGain*wetRipple;
|
|
347
|
+
nz-=detailSlope.z*activity*styleGain*wetRipple;
|
|
348
|
+
const norm=Math.hypot(nx,ny,nz);nx/=norm;ny/=norm;nz/=norm;
|
|
349
|
+
const dot=nx*ix+ny*iy+nz*iz,k=1-eta*eta*(1-dot*dot);if(k<=0)continue;
|
|
350
|
+
const q=eta*dot+Math.sqrt(k),rx=eta*ix-q*nx,ry=eta*iy-q*ny,rz=eta*iz-q*nz;if(ry>=-0.02)continue;
|
|
351
|
+
const surfaceY=sample(this.state,4,0);
|
|
352
|
+
let distance=-h/ry;
|
|
353
|
+
// Refine the intersection against the receiving bed, rather
|
|
354
|
+
// than stopping on a horizontal plane beneath the source ray.
|
|
355
|
+
for(let refine=0;refine<2;refine++) distance=Math.max(0,(this.bed(wx+rx*distance,wz+rz*distance)-surfaceY)/ry);
|
|
356
|
+
const fx=x+rx*distance/step,fz=z+rz*distance/step;
|
|
357
|
+
const x0=Math.floor(fx),z0=Math.floor(fz),u=fx-x0,v=fz-z0;
|
|
358
|
+
const energy=Math.max(0,-dot)*Math.exp(-distance*0.08);
|
|
359
|
+
for(let dz=0;dz<2;dz++)for(let dx=0;dx<2;dx++){
|
|
360
|
+
const xx=x0+dx,zz=z0+dz;if(xx>=0&&xx<n&&zz>=0&&zz<n)this.causticWork[zz*n+xx]+=energy*(dx?u:1-u)*(dz?v:1-v);
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
const flatRayY=Math.sqrt(1-eta*eta*(1-iy*iy));
|
|
364
|
+
for(let i=0;i<n*n;i++){
|
|
365
|
+
const wx=center[0]-size/2+(i%n+0.5)*step,wz=center[1]-size/2+(Math.floor(i/n)+0.5)*step;
|
|
366
|
+
const depth=Math.max(0,this.level-this.bed(wx,wz));
|
|
367
|
+
// Display the focusing excess over an unperturbed interface.
|
|
368
|
+
// A perfectly flat tank must not create a glowing caustic wash.
|
|
369
|
+
const unfocused=Math.abs(iy)*Math.exp(-depth/flatRayY*0.08);
|
|
370
|
+
const focused=Math.max(0,this.causticWork[i]-unfocused);
|
|
371
|
+
this.light[i*4]=this.light[i*4+1]=this.light[i*4+2]=focused*3;
|
|
372
|
+
this.light[i*4+3]=1;
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
// PlaneGeometry's diagonal is (x+1,z)--(x,z+1). Matching this, rather than
|
|
376
|
+
// bilinear sampling, makes contact height equal the rendered triangles.
|
|
377
|
+
sample(x, z, out = {}) {
|
|
378
|
+
const gx = Math.max(0, Math.min(this.columns - 1, (x - this.minX) / this.dx)), gz = Math.max(0, Math.min(this.rows - 1, (z - this.minZ) / this.dz));
|
|
379
|
+
const ix = Math.min(this.columns - 2, Math.floor(gx)), iz = Math.min(this.rows - 2, Math.floor(gz)), tx = gx - ix, tz = gz - iz;
|
|
380
|
+
const i = iz * this.columns + ix, ids = tx + tz <= 1 ? [i, i + 1, i + this.columns] : [i + this.columns + 1, i + this.columns, i + 1];
|
|
381
|
+
const weights = tx + tz <= 1 ? [1 - tx - tz, tx, tz] : [tx + tz - 1, 1 - tx, 1 - tz];
|
|
382
|
+
for (const [key, offset] of [['height', 0], ['depth', 1], ['velocityX', 2], ['velocityZ', 3]])
|
|
383
|
+
out[key] = ids.reduce((sum, id, k) => sum + this.state[id * 4 + offset] * weights[k], 0);
|
|
384
|
+
out.wet = out.depth > 0.002;
|
|
385
|
+
if(this.options.shorelineAxis === 'z') {
|
|
386
|
+
const edge=this.shoreContour[ix]*(1-tx)+this.shoreContour[ix+1]*tx;
|
|
387
|
+
out.wet=out.depth>0&&z<edge;
|
|
388
|
+
}
|
|
389
|
+
return out;
|
|
390
|
+
}
|
|
391
|
+
impulse(x, z, strength, radius) {
|
|
392
|
+
if (!this.gridSignature)
|
|
393
|
+
this.update(0);
|
|
394
|
+
if (this.hydro)
|
|
395
|
+
this.hydro.impulse(x, z, strength, radius);
|
|
396
|
+
else {
|
|
397
|
+
if (!this.interactions)
|
|
398
|
+
this.interactions = new WaterHydrodynamics({ columns: this.columns, rows: this.rows,
|
|
399
|
+
width: this.surface.width, depth: this.surface.depth, centerX: this.x, centerZ: this.z, waterLevel: this.level, bedHeight: this.bed,
|
|
400
|
+
boundary: () => true, friction: 0.02 });
|
|
401
|
+
this.interactions.impulse(x, z, strength, radius);
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
shoreEdgeAt(x = 0, minimumDepth = 0.005) {
|
|
405
|
+
if (!this.hydro) return null;
|
|
406
|
+
const ix = Math.max(0, Math.min(this.columns-1, Math.round((x-this.minX)/this.dx)));
|
|
407
|
+
let edge = null;
|
|
408
|
+
// Follow the connected water from the incoming boundary; ignore any
|
|
409
|
+
// isolated residual puddle when reporting uprush/backwash position.
|
|
410
|
+
for(let z=0;z<this.rows-1;z++) {
|
|
411
|
+
const a=this.state[(z*this.columns+ix)*4+1], b=this.state[((z+1)*this.columns+ix)*4+1];
|
|
412
|
+
if(a>=minimumDepth && b<minimumDepth) {
|
|
413
|
+
edge=this.minZ+(z+(a-minimumDepth)/Math.max(a-b,1e-8))*this.dz;
|
|
414
|
+
break;
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
return edge;
|
|
418
|
+
}
|
|
419
|
+
diagnostics() { return this.hydro?.diagnostics() ?? { time: this.time, model: 'spectral', bands: 3, peakLength: this.spectrum.peakLength }; }
|
|
420
|
+
dispose() { this.foamParticles?.dispose(); this.foamCoordinateTexture.dispose(); this.renderExtension?.dispose(); this.detailSpectrum.dispose(); this.texture.dispose(); this.causticTexture.dispose(); for (const texture of this.normalTextures)
|
|
421
|
+
texture.dispose(); }
|
|
422
|
+
}
|