@call-me-sensei/toonlab 0.1.1
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/ATTRIBUTION.md +40 -0
- package/LICENSE +21 -0
- package/README.md +125 -0
- package/package.json +97 -0
- package/src/character/characterRig.js +353 -0
- package/src/character/freestyleSwimClip.js +541 -0
- package/src/character/index.js +4 -0
- package/src/character/modelLoader.js +433 -0
- package/src/core/materialRoles.js +438 -0
- package/src/core/presetDocuments.js +110 -0
- package/src/core/shaderBackend.js +24 -0
- package/src/debrisgen/debrisFields.js +544 -0
- package/src/debrisgen/debrisGenerator.js +1929 -0
- package/src/debrisgen/debrisPalettes.js +71 -0
- package/src/debrisgen/debrisPhysics.js +198 -0
- package/src/debrisgen/debrisPresets.js +155 -0
- package/src/debrisgen/debrisSettings.js +333 -0
- package/src/debrisgen/debrisTextures.js +380 -0
- package/src/debrisgen/index.js +5 -0
- package/src/debug/fieldValues.js +95 -0
- package/src/debug/index.js +7 -0
- package/src/debug/settingsPanel.js +170 -0
- package/src/environment/environmentAmbientProbe.js +160 -0
- package/src/environment/environmentMaterialAdapter.js +336 -0
- package/src/environment/environmentMaterialClassifier.js +178 -0
- package/src/environment/environmentPlanarReflection.js +201 -0
- package/src/environment/environmentPresets.js +443 -0
- package/src/environment/environmentRigs.js +684 -0
- package/src/environment/environmentSettings.js +504 -0
- package/src/environment/environmentShaderMaterials.js +318 -0
- package/src/environment/environmentSunShadowPass.js +268 -0
- package/src/environment/environmentTextureResolver.js +161 -0
- package/src/environment/environmentTimeOfDay.js +167 -0
- package/src/environment/environmentVertexAo.js +188 -0
- package/src/environment/index.js +8 -0
- package/src/environment/scanAssetStylize.js +103 -0
- package/src/index.js +21 -0
- package/src/loaders/index.js +1 -0
- package/src/post/index.js +2 -0
- package/src/post/postProcessing.js +1087 -0
- package/src/rockgen/export/glbExport.js +177 -0
- package/src/rockgen/heightfield/heightfieldErosion.js +7 -0
- package/src/rockgen/heightfield/heightfieldPatch.js +183 -0
- package/src/rockgen/heightfield/stylizedErosionSim.js +356 -0
- package/src/rockgen/index.js +14 -0
- package/src/rockgen/mesh/meshAttributes.js +374 -0
- package/src/rockgen/mesh/meshDocument.js +158 -0
- package/src/rockgen/mesh/surfaceNets.js +329 -0
- package/src/rockgen/noise/cellularNoise3.js +92 -0
- package/src/rockgen/noise/prng.js +62 -0
- package/src/rockgen/noise/simplexNoise3.js +89 -0
- package/src/rockgen/noise/valueNoise3.js +87 -0
- package/src/rockgen/rockDocument.js +255 -0
- package/src/rockgen/rockHelpers.js +8 -0
- package/src/rockgen/rockgenPresets.js +559 -0
- package/src/rockgen/rockgenSettings.js +756 -0
- package/src/rockgen/sdf/fieldCompiler.js +544 -0
- package/src/rockgen/sdf/sculptEdits.js +66 -0
- package/src/rockgen/sdf/sdfModifiers.js +45 -0
- package/src/rockgen/sdf/sdfOps.js +37 -0
- package/src/rockgen/sdf/sdfPrimitives.js +88 -0
- package/src/shaders-tsl/anime.js +963 -0
- package/src/shaders-tsl/chunks/character-color.js +64 -0
- package/src/shaders-tsl/chunks/character-highlights.js +174 -0
- package/src/shaders-tsl/chunks/character-lighting.js +309 -0
- package/src/shaders-tsl/chunks/character-material-maps.js +146 -0
- package/src/shaders-tsl/chunks/character-roles.js +52 -0
- package/src/shaders-tsl/chunks/character-scene-lights.js +270 -0
- package/src/shaders-tsl/chunks/character-shadow-color.js +61 -0
- package/src/shaders-tsl/chunks/character-skinning.js +140 -0
- package/src/shaders-tsl/chunks/environment-color.js +47 -0
- package/src/shaders-tsl/chunks/environment-debug.js +79 -0
- package/src/shaders-tsl/chunks/environment-lighting.js +260 -0
- package/src/shaders-tsl/chunks/environment-sun-shadow.js +87 -0
- package/src/shaders-tsl/chunks/foliage-fog.js +60 -0
- package/src/shaders-tsl/chunks/pass-depth-color.js +89 -0
- package/src/shaders-tsl/chunks/stylized-cloud-shadow.js +70 -0
- package/src/shaders-tsl/chunks/water-color.js +190 -0
- package/src/shaders-tsl/chunks/water-common.js +124 -0
- package/src/shaders-tsl/chunks/water-foam.js +93 -0
- package/src/shaders-tsl/chunks/water-lighting.js +116 -0
- package/src/shaders-tsl/chunks/water-ripple.js +64 -0
- package/src/shaders-tsl/chunks/water-waves.js +100 -0
- package/src/shaders-tsl/environment-ao-overlay.js +60 -0
- package/src/shaders-tsl/environment.js +680 -0
- package/src/shaders-tsl/flower.js +267 -0
- package/src/shaders-tsl/grass.js +193 -0
- package/src/shaders-tsl/post-composite.js +461 -0
- package/src/shaders-tsl/sky.js +175 -0
- package/src/shaders-tsl/tree-leaf.js +264 -0
- package/src/shaders-tsl/water-breaker.js +423 -0
- package/src/shaders-tsl/water-kelp.js +171 -0
- package/src/shaders-tsl/water-rain.js +107 -0
- package/src/shaders-tsl/water-simulation.js +140 -0
- package/src/shaders-tsl/water-splash.js +298 -0
- package/src/shaders-tsl/water.js +509 -0
- package/src/sky/index.js +2 -0
- package/src/sky/stylizedSky.js +341 -0
- package/src/toon/characterRenderPasses.js +646 -0
- package/src/toon/index.js +4 -0
- package/src/toon/settings/alphaSettings.js +170 -0
- package/src/toon/settings/averageShadowSettings.js +181 -0
- package/src/toon/settings/baseTextureSettings.js +133 -0
- package/src/toon/settings/celShadeSettings.js +96 -0
- package/src/toon/settings/contactShadowSettings.js +114 -0
- package/src/toon/settings/eyeHighlightSettings.js +187 -0
- package/src/toon/settings/faceLightingSettings.js +135 -0
- package/src/toon/settings/furSettings.js +112 -0
- package/src/toon/settings/glitterSettings.js +102 -0
- package/src/toon/settings/hairHighlightSettings.js +325 -0
- package/src/toon/settings/indirectLightSettings.js +244 -0
- package/src/toon/settings/localLightSettings.js +176 -0
- package/src/toon/settings/materialMapSettings.js +299 -0
- package/src/toon/settings/outlineSettings.js +263 -0
- package/src/toon/settings/perspectiveRemovalSettings.js +60 -0
- package/src/toon/settings/rimLightSettings.js +268 -0
- package/src/toon/settings/sceneShadowSettings.js +147 -0
- package/src/toon/settings/selfShadowSettings.js +221 -0
- package/src/toon/settings/shadowColorSettings.js +238 -0
- package/src/toon/settings/skinToneSettings.js +150 -0
- package/src/toon/settings/specularSettings.js +314 -0
- package/src/toon/settings/stickerSettings.js +102 -0
- package/src/toon/toonMaterialAdapter.js +2235 -0
- package/src/toon/toonSettings.js +860 -0
- package/src/vegetation/flowerSpecies.js +260 -0
- package/src/vegetation/index.js +10 -0
- package/src/vegetation/stylizedBush.js +136 -0
- package/src/vegetation/stylizedFlower.js +213 -0
- package/src/vegetation/stylizedFlowers.js +297 -0
- package/src/vegetation/stylizedGrass.js +491 -0
- package/src/vegetation/stylizedTree.js +2449 -0
- package/src/vegetation/stylizedTreeFoliage.js +691 -0
- package/src/vegetation/treeExport.js +300 -0
- package/src/vegetation/treeRecipe.js +647 -0
- package/src/water/index.js +2 -0
- package/src/water/water.js +20 -0
- package/src/water/waterBreakerSystem.js +591 -0
- package/src/water/waterInteraction.js +193 -0
- package/src/water/waterMaterial.js +263 -0
- package/src/water/waterRain.js +136 -0
- package/src/water/waterRippleSimulation.js +227 -0
- package/src/water/waterScenePasses.js +415 -0
- package/src/water/waterSettings.js +1138 -0
- package/src/water/waterSplashSystem.js +251 -0
- package/src/water/waterSurface.js +475 -0
- package/src/water/waterVegetation.js +83 -0
|
@@ -0,0 +1,591 @@
|
|
|
1
|
+
import * as THREE from 'three';
|
|
2
|
+
|
|
3
|
+
import { createWaterBreakerNodeMaterial } from '../shaders-tsl/water-breaker.js';
|
|
4
|
+
import { WATER_GERSTNER_WAVE_COUNT } from './waterSettings.js';
|
|
5
|
+
|
|
6
|
+
// Dedicated plunging-breaker geometry. The water heightfield steepens and
|
|
7
|
+
// trims waves in the shallows, but a heightfield can never overhang — so
|
|
8
|
+
// surfable curling waves need their own mesh. This system:
|
|
9
|
+
//
|
|
10
|
+
// 1. Finds the break line: the seabed contour at the depth where the swell
|
|
11
|
+
// reaches its collapse criterion (crest = 0.72 x column depth), via
|
|
12
|
+
// marching squares over the bed sampler.
|
|
13
|
+
// 2. Keeps only wave-facing stretches (where the swell actually travels
|
|
14
|
+
// toward the shallows) and sweeps a ribbon of profile columns along them.
|
|
15
|
+
// 3. Shapes the ribbon in the vertex shader into a curling wave shell,
|
|
16
|
+
// phase-locked to the dominant Gerstner component so barrels arrive with
|
|
17
|
+
// the rendered swell and peel down the line.
|
|
18
|
+
//
|
|
19
|
+
// The shell is purely visual — buoyancy/height queries still come from the
|
|
20
|
+
// heightfield mirror, which the shell's skirt is glued to.
|
|
21
|
+
|
|
22
|
+
const PROFILE_ROWS = 15;
|
|
23
|
+
const COLUMN_SPACING = 0.8;
|
|
24
|
+
const END_FADE_DISTANCE = 2.4;
|
|
25
|
+
const MIN_CHAIN_LENGTH = 5;
|
|
26
|
+
const GRID_RESOLUTION = 110;
|
|
27
|
+
const GRADIENT_EPSILON = 0.35;
|
|
28
|
+
// A column only breaks where the swell travels toward the shallows.
|
|
29
|
+
const MIN_WAVE_FACING = 0.25;
|
|
30
|
+
|
|
31
|
+
function setSrgbColor(color, rgb) {
|
|
32
|
+
color.setRGB(rgb[0], rgb[1], rgb[2], THREE.SRGBColorSpace);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Depth at which the swell collapses: solves 0.72 * d = energy * shoal(d) by
|
|
36
|
+
// fixed-point iteration (shoal depends on d through the same rear-up curve
|
|
37
|
+
// the shaders use). Exported for tests and debug overlays.
|
|
38
|
+
export function computeBreakingDepth(settings, waveEnergy) {
|
|
39
|
+
// Same energy-scaled shoal range as the water vertex shader.
|
|
40
|
+
const range = Math.max(settings.shoalingDepth ?? 1.4, waveEnergy * 2.2, 1e-3);
|
|
41
|
+
const shorelineWaves = settings.shorelineWaves ?? 0.35;
|
|
42
|
+
let depth = Math.max((waveEnergy * 1.4) / 0.72, 0.12);
|
|
43
|
+
for (let i = 0; i < 5; i += 1) {
|
|
44
|
+
const deepFactor = THREE.MathUtils.smoothstep(depth, 0, range);
|
|
45
|
+
const rearUp = (1 - THREE.MathUtils.smoothstep(depth, range * 0.45, range * 1.5)) *
|
|
46
|
+
THREE.MathUtils.smoothstep(depth, 0.05, 0.35);
|
|
47
|
+
const shoal = THREE.MathUtils.lerp(shorelineWaves, 1, deepFactor) * (1 + 0.3 * rearUp);
|
|
48
|
+
// Big swells break far outside the shoaling band, so the ceiling must
|
|
49
|
+
// scale with energy, not just the shoaling range.
|
|
50
|
+
depth = Math.min(
|
|
51
|
+
Math.max((waveEnergy * shoal) / 0.72, 0.12),
|
|
52
|
+
Math.max(range * 2, waveEnergy * 2));
|
|
53
|
+
}
|
|
54
|
+
return depth;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function pointKey(x, z) {
|
|
58
|
+
return `${Math.round(x * 512)},${Math.round(z * 512)}`;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Marching squares over the rest-depth field: returns raw contour segments
|
|
62
|
+
// [x0, z0, x1, z1] (world space) where restDepth crosses breakDepth.
|
|
63
|
+
function marchBreakContour({ bedSampler, originX, originZ, surfaceY, width, depth, breakDepth }) {
|
|
64
|
+
const nx = GRID_RESOLUTION;
|
|
65
|
+
const nz = GRID_RESOLUTION;
|
|
66
|
+
const x0 = originX - width * 0.5;
|
|
67
|
+
const z0 = originZ - depth * 0.5;
|
|
68
|
+
const dx = width / nx;
|
|
69
|
+
const dz = depth / nz;
|
|
70
|
+
|
|
71
|
+
const field = new Float32Array((nx + 1) * (nz + 1));
|
|
72
|
+
for (let j = 0; j <= nz; j += 1) {
|
|
73
|
+
for (let i = 0; i <= nx; i += 1) {
|
|
74
|
+
const x = x0 + i * dx;
|
|
75
|
+
const z = z0 + j * dz;
|
|
76
|
+
field[j * (nx + 1) + i] = (surfaceY - bedSampler(x, z)) - breakDepth;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const segments = [];
|
|
81
|
+
const lerpEdge = (xa, za, va, xb, zb, vb) => {
|
|
82
|
+
const t = va / (va - vb);
|
|
83
|
+
return [xa + (xb - xa) * t, za + (zb - za) * t];
|
|
84
|
+
};
|
|
85
|
+
for (let j = 0; j < nz; j += 1) {
|
|
86
|
+
for (let i = 0; i < nx; i += 1) {
|
|
87
|
+
const v00 = field[j * (nx + 1) + i];
|
|
88
|
+
const v10 = field[j * (nx + 1) + i + 1];
|
|
89
|
+
const v11 = field[(j + 1) * (nx + 1) + i + 1];
|
|
90
|
+
const v01 = field[(j + 1) * (nx + 1) + i];
|
|
91
|
+
const caseIndex = (v00 > 0 ? 1 : 0) | (v10 > 0 ? 2 : 0) |
|
|
92
|
+
(v11 > 0 ? 4 : 0) | (v01 > 0 ? 8 : 0);
|
|
93
|
+
if (caseIndex === 0 || caseIndex === 15) continue;
|
|
94
|
+
const xa = x0 + i * dx;
|
|
95
|
+
const za = z0 + j * dz;
|
|
96
|
+
const xb = xa + dx;
|
|
97
|
+
const zb = za + dz;
|
|
98
|
+
// Edge crossings: bottom (00-10), right (10-11), top (01-11), left (00-01).
|
|
99
|
+
const bottom = (v00 > 0) !== (v10 > 0) ? lerpEdge(xa, za, v00, xb, za, v10) : null;
|
|
100
|
+
const right = (v10 > 0) !== (v11 > 0) ? lerpEdge(xb, za, v10, xb, zb, v11) : null;
|
|
101
|
+
const top = (v01 > 0) !== (v11 > 0) ? lerpEdge(xa, zb, v01, xb, zb, v11) : null;
|
|
102
|
+
const left = (v00 > 0) !== (v01 > 0) ? lerpEdge(xa, za, v00, xa, zb, v01) : null;
|
|
103
|
+
const crossings = [bottom, right, top, left].filter(Boolean);
|
|
104
|
+
if (crossings.length === 2) {
|
|
105
|
+
segments.push([...crossings[0], ...crossings[1]]);
|
|
106
|
+
} else if (crossings.length === 4) {
|
|
107
|
+
// Ambiguous saddle: pair bottom-right and top-left (arbitrary but
|
|
108
|
+
// consistent; the wave-facing filter drops bad joins anyway).
|
|
109
|
+
segments.push([...bottom, ...right], [...top, ...left]);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return segments;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Joins raw segments into ordered polylines by matching endpoints.
|
|
117
|
+
function chainSegments(segments) {
|
|
118
|
+
const adjacency = new Map();
|
|
119
|
+
const addEnd = (key, entry) => {
|
|
120
|
+
const list = adjacency.get(key);
|
|
121
|
+
if (list) list.push(entry);
|
|
122
|
+
else adjacency.set(key, [entry]);
|
|
123
|
+
};
|
|
124
|
+
segments.forEach((segment, index) => {
|
|
125
|
+
addEnd(pointKey(segment[0], segment[1]), { index, end: 0 });
|
|
126
|
+
addEnd(pointKey(segment[2], segment[3]), { index, end: 1 });
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
const used = new Array(segments.length).fill(false);
|
|
130
|
+
const chains = [];
|
|
131
|
+
const walk = (startIndex, startEnd) => {
|
|
132
|
+
const chain = [];
|
|
133
|
+
let index = startIndex;
|
|
134
|
+
let end = startEnd;
|
|
135
|
+
while (index !== -1 && !used[index]) {
|
|
136
|
+
used[index] = true;
|
|
137
|
+
const segment = segments[index];
|
|
138
|
+
const from = end === 0
|
|
139
|
+
? [segment[0], segment[1]]
|
|
140
|
+
: [segment[2], segment[3]];
|
|
141
|
+
const to = end === 0
|
|
142
|
+
? [segment[2], segment[3]]
|
|
143
|
+
: [segment[0], segment[1]];
|
|
144
|
+
if (chain.length === 0) chain.push(from);
|
|
145
|
+
chain.push(to);
|
|
146
|
+
const nextList = adjacency.get(pointKey(to[0], to[1])) ?? [];
|
|
147
|
+
const next = nextList.find((entry) => !used[entry.index]);
|
|
148
|
+
if (!next) break;
|
|
149
|
+
index = next.index;
|
|
150
|
+
end = next.end;
|
|
151
|
+
}
|
|
152
|
+
return chain;
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
// Open chains first (endpoints with a single incident segment)...
|
|
156
|
+
for (const entries of adjacency.values()) {
|
|
157
|
+
if (entries.length !== 1 || used[entries[0].index]) continue;
|
|
158
|
+
const chain = walk(entries[0].index, entries[0].end);
|
|
159
|
+
if (chain.length > 1) chains.push(chain);
|
|
160
|
+
}
|
|
161
|
+
// ...then whatever remains (closed loops around islands).
|
|
162
|
+
segments.forEach((_, index) => {
|
|
163
|
+
if (used[index]) return;
|
|
164
|
+
const chain = walk(index, 0);
|
|
165
|
+
if (chain.length > 1) chains.push(chain);
|
|
166
|
+
});
|
|
167
|
+
return chains;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function smoothChain(points, iterations = 2) {
|
|
171
|
+
let current = points;
|
|
172
|
+
for (let pass = 0; pass < iterations; pass += 1) {
|
|
173
|
+
const next = current.map((point, i) => {
|
|
174
|
+
if (i === 0 || i === current.length - 1) return point;
|
|
175
|
+
return [
|
|
176
|
+
(current[i - 1][0] + point[0] * 2 + current[i + 1][0]) * 0.25,
|
|
177
|
+
(current[i - 1][1] + point[1] * 2 + current[i + 1][1]) * 0.25,
|
|
178
|
+
];
|
|
179
|
+
});
|
|
180
|
+
current = next;
|
|
181
|
+
}
|
|
182
|
+
return current;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function resampleChain(points, spacing) {
|
|
186
|
+
const resampled = [points[0]];
|
|
187
|
+
let carried = 0;
|
|
188
|
+
for (let i = 1; i < points.length; i += 1) {
|
|
189
|
+
let [px, pz] = points[i - 1];
|
|
190
|
+
const [qx, qz] = points[i];
|
|
191
|
+
let segmentLength = Math.hypot(qx - px, qz - pz);
|
|
192
|
+
while (carried + segmentLength >= spacing) {
|
|
193
|
+
const t = (spacing - carried) / segmentLength;
|
|
194
|
+
px += (qx - px) * t;
|
|
195
|
+
pz += (qz - pz) * t;
|
|
196
|
+
resampled.push([px, pz]);
|
|
197
|
+
segmentLength = Math.hypot(qx - px, qz - pz);
|
|
198
|
+
carried = 0;
|
|
199
|
+
}
|
|
200
|
+
carried += segmentLength;
|
|
201
|
+
}
|
|
202
|
+
return resampled;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Full break-line extraction: contour, smoothing, resampling, then per-point
|
|
206
|
+
// travel direction with the wave-facing split. Returns chains of
|
|
207
|
+
// { x, z, dirX, dirZ, depth } in world space.
|
|
208
|
+
export function extractBreakLineChains({
|
|
209
|
+
bedSampler,
|
|
210
|
+
originX,
|
|
211
|
+
originZ,
|
|
212
|
+
surfaceY,
|
|
213
|
+
width,
|
|
214
|
+
depth,
|
|
215
|
+
breakDepth,
|
|
216
|
+
waveDirX,
|
|
217
|
+
waveDirZ,
|
|
218
|
+
}) {
|
|
219
|
+
const segments = marchBreakContour({
|
|
220
|
+
bedSampler, originX, originZ, surfaceY, width, depth, breakDepth,
|
|
221
|
+
});
|
|
222
|
+
const chains = [];
|
|
223
|
+
for (const rawChain of chainSegments(segments)) {
|
|
224
|
+
const resampled = resampleChain(smoothChain(rawChain), COLUMN_SPACING);
|
|
225
|
+
if (resampled.length < 3) continue;
|
|
226
|
+
|
|
227
|
+
// Travel direction: uphill bed gradient (toward the shallows), blended
|
|
228
|
+
// with the global swell direction so noisy contours can't twist columns.
|
|
229
|
+
const points = resampled.map(([x, z]) => {
|
|
230
|
+
const gradientX = bedSampler(x + GRADIENT_EPSILON, z) - bedSampler(x - GRADIENT_EPSILON, z);
|
|
231
|
+
const gradientZ = bedSampler(x, z + GRADIENT_EPSILON) - bedSampler(x, z - GRADIENT_EPSILON);
|
|
232
|
+
const gradientLength = Math.hypot(gradientX, gradientZ) || 1;
|
|
233
|
+
const shoreX = gradientX / gradientLength;
|
|
234
|
+
const shoreZ = gradientZ / gradientLength;
|
|
235
|
+
const facing = shoreX * waveDirX + shoreZ * waveDirZ;
|
|
236
|
+
let dirX = shoreX * 0.6 + waveDirX * 0.4;
|
|
237
|
+
let dirZ = shoreZ * 0.6 + waveDirZ * 0.4;
|
|
238
|
+
const dirLength = Math.hypot(dirX, dirZ) || 1;
|
|
239
|
+
dirX /= dirLength;
|
|
240
|
+
dirZ /= dirLength;
|
|
241
|
+
// Bed rise per meter of shoreward travel: lets the vertex shader
|
|
242
|
+
// estimate the water depth under the crest as it rides through the
|
|
243
|
+
// breaking zone (deeper offshore, shoaling out toward the beach).
|
|
244
|
+
const slope = Math.max(
|
|
245
|
+
(gradientX * dirX + gradientZ * dirZ) / (2 * GRADIENT_EPSILON), 0);
|
|
246
|
+
return { x, z, dirX, dirZ, depth: surfaceY - bedSampler(x, z), facing, slope };
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
// Orient the chain so the peel travels the way the swell sweeps along
|
|
250
|
+
// the shore — marching squares emits arbitrary winding, and a peel
|
|
251
|
+
// running against the swell's alongshore drift reads backwards.
|
|
252
|
+
let shoreX = 0;
|
|
253
|
+
let shoreZ = 0;
|
|
254
|
+
for (const point of points) {
|
|
255
|
+
shoreX += point.dirX;
|
|
256
|
+
shoreZ += point.dirZ;
|
|
257
|
+
}
|
|
258
|
+
const shoreLength = Math.hypot(shoreX, shoreZ) || 1;
|
|
259
|
+
shoreX /= shoreLength;
|
|
260
|
+
shoreZ /= shoreLength;
|
|
261
|
+
const onshore = waveDirX * shoreX + waveDirZ * shoreZ;
|
|
262
|
+
const alongX = waveDirX - onshore * shoreX;
|
|
263
|
+
const alongZ = waveDirZ - onshore * shoreZ;
|
|
264
|
+
const first = points[0];
|
|
265
|
+
const last = points[points.length - 1];
|
|
266
|
+
if ((last.x - first.x) * alongX + (last.z - first.z) * alongZ < 0) {
|
|
267
|
+
points.reverse();
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// Split into wave-facing runs and keep the ones long enough to surf.
|
|
271
|
+
let run = [];
|
|
272
|
+
const flushRun = () => {
|
|
273
|
+
if ((run.length - 1) * COLUMN_SPACING >= MIN_CHAIN_LENGTH) chains.push(run);
|
|
274
|
+
run = [];
|
|
275
|
+
};
|
|
276
|
+
for (const point of points) {
|
|
277
|
+
if (point.facing > MIN_WAVE_FACING) run.push(point);
|
|
278
|
+
else flushRun();
|
|
279
|
+
}
|
|
280
|
+
flushRun();
|
|
281
|
+
}
|
|
282
|
+
return chains;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// Stamps each chain point with its arc length and end fade. The geometry
|
|
286
|
+
// builder reads them into vertex attributes, and the CPU shell mirror
|
|
287
|
+
// (sampleAt) needs the same values for physics queries.
|
|
288
|
+
function annotateChains(chains) {
|
|
289
|
+
for (const chain of chains) {
|
|
290
|
+
let arc = 0;
|
|
291
|
+
for (let i = 0; i < chain.length; i += 1) {
|
|
292
|
+
if (i > 0) {
|
|
293
|
+
arc += Math.hypot(chain[i].x - chain[i - 1].x, chain[i].z - chain[i - 1].z);
|
|
294
|
+
}
|
|
295
|
+
chain[i].along = arc;
|
|
296
|
+
}
|
|
297
|
+
for (const point of chain) {
|
|
298
|
+
point.endFade = THREE.MathUtils.smoothstep(
|
|
299
|
+
Math.min(point.along, arc - point.along), 0, END_FADE_DISTANCE);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
return chains;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// Ribbon geometry: PROFILE_ROWS rows per column, all rows anchored at the
|
|
306
|
+
// column's break-line point — the vertex shader spreads them into the shell.
|
|
307
|
+
function buildBreakerGeometry(chains, originX, originZ) {
|
|
308
|
+
let vertexCount = 0;
|
|
309
|
+
let quadCount = 0;
|
|
310
|
+
for (const chain of chains) {
|
|
311
|
+
vertexCount += chain.length * PROFILE_ROWS;
|
|
312
|
+
quadCount += (chain.length - 1) * (PROFILE_ROWS - 1);
|
|
313
|
+
}
|
|
314
|
+
const positions = new Float32Array(vertexCount * 3);
|
|
315
|
+
const dirs = new Float32Array(vertexCount * 2);
|
|
316
|
+
const infos = new Float32Array(vertexCount * 4);
|
|
317
|
+
const slopes = new Float32Array(vertexCount);
|
|
318
|
+
const indices = new (vertexCount > 65535 ? Uint32Array : Uint16Array)(quadCount * 6);
|
|
319
|
+
|
|
320
|
+
let vertex = 0;
|
|
321
|
+
let index = 0;
|
|
322
|
+
for (const chain of chains) {
|
|
323
|
+
const columnStart = vertex;
|
|
324
|
+
for (let i = 0; i < chain.length; i += 1) {
|
|
325
|
+
const point = chain[i];
|
|
326
|
+
for (let row = 0; row < PROFILE_ROWS; row += 1) {
|
|
327
|
+
positions[vertex * 3] = point.x - originX;
|
|
328
|
+
positions[vertex * 3 + 1] = 0;
|
|
329
|
+
positions[vertex * 3 + 2] = point.z - originZ;
|
|
330
|
+
dirs[vertex * 2] = point.dirX;
|
|
331
|
+
dirs[vertex * 2 + 1] = point.dirZ;
|
|
332
|
+
infos[vertex * 4] = point.along;
|
|
333
|
+
infos[vertex * 4 + 1] = row / (PROFILE_ROWS - 1);
|
|
334
|
+
infos[vertex * 4 + 2] = point.depth;
|
|
335
|
+
infos[vertex * 4 + 3] = point.endFade;
|
|
336
|
+
slopes[vertex] = point.slope ?? 0;
|
|
337
|
+
vertex += 1;
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
for (let i = 0; i < chain.length - 1; i += 1) {
|
|
341
|
+
for (let row = 0; row < PROFILE_ROWS - 1; row += 1) {
|
|
342
|
+
const a = columnStart + i * PROFILE_ROWS + row;
|
|
343
|
+
const b = a + PROFILE_ROWS;
|
|
344
|
+
indices[index] = a;
|
|
345
|
+
indices[index + 1] = b;
|
|
346
|
+
indices[index + 2] = a + 1;
|
|
347
|
+
indices[index + 3] = a + 1;
|
|
348
|
+
indices[index + 4] = b;
|
|
349
|
+
indices[index + 5] = b + 1;
|
|
350
|
+
index += 6;
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
const geometry = new THREE.BufferGeometry();
|
|
356
|
+
geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));
|
|
357
|
+
geometry.setAttribute('aDir', new THREE.BufferAttribute(dirs, 2));
|
|
358
|
+
geometry.setAttribute('aInfo', new THREE.BufferAttribute(infos, 4));
|
|
359
|
+
geometry.setAttribute('aSlope', new THREE.BufferAttribute(slopes, 1));
|
|
360
|
+
geometry.setIndex(new THREE.BufferAttribute(indices, 1));
|
|
361
|
+
// Shell displacement happens in the vertex shader; skip culling.
|
|
362
|
+
geometry.boundingSphere = new THREE.Sphere(new THREE.Vector3(), 1e5);
|
|
363
|
+
return geometry;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
export class WaterBreakerSystem extends THREE.Group {
|
|
367
|
+
constructor() {
|
|
368
|
+
super();
|
|
369
|
+
this.name = 'WaterBreakerSystem';
|
|
370
|
+
this.chains = [];
|
|
371
|
+
// Flattened chain points for CPU physics queries (sampleAt).
|
|
372
|
+
this.columns = [];
|
|
373
|
+
this.waves = null;
|
|
374
|
+
this.params = null;
|
|
375
|
+
this.time = 0;
|
|
376
|
+
// attachWaveUniforms rebuilds this with the owning surface's uniform nodes
|
|
377
|
+
// adopted into the graph (sharing after the fact cannot rewire a built
|
|
378
|
+
// node graph; see water-breaker.js).
|
|
379
|
+
this.material = createWaterBreakerNodeMaterial({
|
|
380
|
+
waveCount: WATER_GERSTNER_WAVE_COUNT,
|
|
381
|
+
foamOctaves: 3,
|
|
382
|
+
});
|
|
383
|
+
this.mesh = null;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// Shares the owning water material's Gerstner AND lighting uniforms by
|
|
387
|
+
// reference, so the shell rides exactly the rendered swell and is shaded as
|
|
388
|
+
// exactly the same material — colors, sun, fresnel, sparkles, and the live
|
|
389
|
+
// planar reflection target all stay in lockstep with the surface.
|
|
390
|
+
attachWaveUniforms(sourceMaterial) {
|
|
391
|
+
if (!sourceMaterial?.uniforms?.uWavesA) return this;
|
|
392
|
+
const waveCount = sourceMaterial.defines?.WATER_WAVE_COUNT;
|
|
393
|
+
// Swapping `.uniforms` entries cannot rewire a built node graph, so
|
|
394
|
+
// rebuild the material with the surface's uniform nodes adopted by
|
|
395
|
+
// reference. Own uniform nodes are carried over so configure() writes
|
|
396
|
+
// persist.
|
|
397
|
+
this.material = createWaterBreakerNodeMaterial({
|
|
398
|
+
waveCount: waveCount ?? this.material.defines.WATER_WAVE_COUNT,
|
|
399
|
+
foamOctaves: this.material.defines.WATER_FOAM_OCTAVES ?? 3,
|
|
400
|
+
previous: this.material.uniforms,
|
|
401
|
+
shared: sourceMaterial.uniforms,
|
|
402
|
+
});
|
|
403
|
+
if (this.mesh) this.mesh.material = this.material;
|
|
404
|
+
return this;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
// Cheap per-frame uniform sync from the owning surface's settings. `waves`
|
|
408
|
+
// is the CPU wave list matching uWavesA/B — sampleAt mirrors the shader
|
|
409
|
+
// with it, so pass the live array (it is swapped on wave setting changes).
|
|
410
|
+
configure(settings, waveEnergy, waves = this.waves) {
|
|
411
|
+
this.waves = waves;
|
|
412
|
+
this.params = {
|
|
413
|
+
amount: THREE.MathUtils.clamp(settings.breakerAmount ?? 0, 0, 1),
|
|
414
|
+
curl: settings.breakerCurl ?? 0.8,
|
|
415
|
+
scale: settings.breakerScale ?? 1,
|
|
416
|
+
peel: settings.breakerPeel ?? 1,
|
|
417
|
+
setPair: (settings.waveSetStrength ?? 0) > 0.001,
|
|
418
|
+
};
|
|
419
|
+
const uniforms = this.material.uniforms;
|
|
420
|
+
uniforms.uBreakerAmount.value = settings.breakerAmount ?? 0;
|
|
421
|
+
uniforms.uBreakerCurl.value = settings.breakerCurl ?? 0.8;
|
|
422
|
+
uniforms.uBreakerScale.value = settings.breakerScale ?? 1;
|
|
423
|
+
uniforms.uBreakerPeel.value = settings.breakerPeel ?? 1;
|
|
424
|
+
uniforms.uShoalingDepth.value = settings.shoalingDepth ?? 1.4;
|
|
425
|
+
uniforms.uShorelineWaves.value = settings.shorelineWaves ?? 0.35;
|
|
426
|
+
uniforms.uWaveEnergy.value = Math.max(waveEnergy ?? 0.3, 1e-3);
|
|
427
|
+
uniforms.uSetPair.value = (settings.waveSetStrength ?? 0) > 0.001 ? 1 : 0;
|
|
428
|
+
uniforms.uFoamNoiseScale.value = settings.foamNoiseScale ?? 0.6;
|
|
429
|
+
setSrgbColor(uniforms.uShallowColor.value, settings.shallowColor);
|
|
430
|
+
setSrgbColor(uniforms.uMidColor.value, settings.midColor);
|
|
431
|
+
setSrgbColor(uniforms.uDeepColor.value, settings.deepColor);
|
|
432
|
+
setSrgbColor(uniforms.uFoamColor.value, settings.foamColor);
|
|
433
|
+
setSrgbColor(uniforms.uSunColor.value, settings.sunColor);
|
|
434
|
+
uniforms.uSunDirection.value
|
|
435
|
+
.set(settings.sunDirection[0], settings.sunDirection[1], settings.sunDirection[2])
|
|
436
|
+
.normalize();
|
|
437
|
+
return this;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
// Re-extracts the break line and rebuilds the ribbon. Call when the water
|
|
441
|
+
// level, surface position, or wave energy changes; the bed is assumed
|
|
442
|
+
// static. Geometry positions are local to the owning surface.
|
|
443
|
+
rebuild({ bedSampler, originX, originZ, surfaceY, width, depth, settings, waveEnergy, waves }) {
|
|
444
|
+
let breakDepth = computeBreakingDepth(settings, waveEnergy);
|
|
445
|
+
const dominant = waves?.[0] ?? { dirX: 1, dirZ: 0 };
|
|
446
|
+
// A swell too big for the local bathymetry has its true break line out
|
|
447
|
+
// past the deepest water here — walk the contour depth in until it
|
|
448
|
+
// exists, so monster sets break at the deepest line the scene offers.
|
|
449
|
+
this.chains = [];
|
|
450
|
+
for (let attempt = 0; attempt < 6 && breakDepth > 0.12; attempt += 1) {
|
|
451
|
+
this.chains = extractBreakLineChains({
|
|
452
|
+
bedSampler,
|
|
453
|
+
originX,
|
|
454
|
+
originZ,
|
|
455
|
+
surfaceY,
|
|
456
|
+
width,
|
|
457
|
+
depth,
|
|
458
|
+
breakDepth,
|
|
459
|
+
waveDirX: dominant.dirX,
|
|
460
|
+
waveDirZ: dominant.dirZ,
|
|
461
|
+
});
|
|
462
|
+
if (this.chains.length > 0) break;
|
|
463
|
+
breakDepth *= 0.72;
|
|
464
|
+
}
|
|
465
|
+
annotateChains(this.chains);
|
|
466
|
+
this.columns = this.chains.flat();
|
|
467
|
+
this.waves = waves ?? this.waves;
|
|
468
|
+
const geometry = buildBreakerGeometry(this.chains, originX, originZ);
|
|
469
|
+
if (this.mesh) {
|
|
470
|
+
this.mesh.geometry.dispose();
|
|
471
|
+
this.mesh.geometry = geometry;
|
|
472
|
+
} else {
|
|
473
|
+
this.mesh = new THREE.Mesh(geometry, this.material);
|
|
474
|
+
this.mesh.frustumCulled = false;
|
|
475
|
+
// Early in the transparent pass like the water surface (-0.5): just
|
|
476
|
+
// above it so the sheet blends over the water, but before character
|
|
477
|
+
// materials (0+). The material writes depth, so alpha-blended hair
|
|
478
|
+
// drawn later still wins where it is closer — otherwise the shore foam
|
|
479
|
+
// sheet paints over heads that overlap the waterline on screen.
|
|
480
|
+
this.mesh.renderOrder = -0.4;
|
|
481
|
+
this.add(this.mesh);
|
|
482
|
+
}
|
|
483
|
+
return this;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
update(time) {
|
|
487
|
+
this.time = time;
|
|
488
|
+
this.material.uniforms.uTime.value = time;
|
|
489
|
+
return this;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
// CPU mirror of the vertex-shader shell so the breakers are PHYSICAL:
|
|
493
|
+
// height queries ride the traveling wave face and flow queries return the
|
|
494
|
+
// bore's shoreward push. Keep every constant in sync with
|
|
495
|
+
// waterBreaker.vert.glsl. Writes into `result`:
|
|
496
|
+
// weight 0..1 blend of the shell over the base water surface
|
|
497
|
+
// crestY shell crest height relative to the rest water level
|
|
498
|
+
// flowX/Z horizontal push velocity (m/s, world space)
|
|
499
|
+
// Only the wave envelope is mirrored (no overhanging lip): buoyancy needs
|
|
500
|
+
// the single-valued surface the object actually rests on.
|
|
501
|
+
sampleAt(x, z, result) {
|
|
502
|
+
result.weight = 0;
|
|
503
|
+
result.crestY = 0;
|
|
504
|
+
result.flowX = 0;
|
|
505
|
+
result.flowZ = 0;
|
|
506
|
+
const waves = this.waves;
|
|
507
|
+
const p = this.params;
|
|
508
|
+
if (!waves?.length || !p || p.amount <= 0.001 || this.columns.length === 0) {
|
|
509
|
+
return result;
|
|
510
|
+
}
|
|
511
|
+
const smoothstep = THREE.MathUtils.smoothstep;
|
|
512
|
+
const w0 = waves[0];
|
|
513
|
+
const w1 = waves[1] ?? w0;
|
|
514
|
+
const k0 = Math.max(w0.waveNumber, 1e-4);
|
|
515
|
+
const waveLen = (2 * Math.PI) / k0;
|
|
516
|
+
const time = this.time;
|
|
517
|
+
|
|
518
|
+
for (const col of this.columns) {
|
|
519
|
+
const px = x - col.x;
|
|
520
|
+
const pz = z - col.z;
|
|
521
|
+
// Position along this column's travel line; skip columns whose line
|
|
522
|
+
// does not pass near the query point (neighbors 0.8 m apart tile the
|
|
523
|
+
// surf zone, so a ~1.2 m window always keeps the nearest few).
|
|
524
|
+
const s = px * col.dirX + pz * col.dirZ;
|
|
525
|
+
const latSq = px * px + pz * pz - s * s;
|
|
526
|
+
if (latSq > 1.44) continue;
|
|
527
|
+
|
|
528
|
+
const theta = k0 * (w0.dirX * col.x + w0.dirZ * col.z) - w0.omega * time +
|
|
529
|
+
w0.phase - p.peel * 0.4 * k0 * col.along;
|
|
530
|
+
const wrapped = (Math.PI * 0.5 - theta) / (2 * Math.PI) + 0.5;
|
|
531
|
+
const delta = wrapped - Math.floor(wrapped) - 0.5;
|
|
532
|
+
const facing = THREE.MathUtils.clamp(
|
|
533
|
+
w0.dirX * col.dirX + w0.dirZ * col.dirZ, 0.3, 1);
|
|
534
|
+
const ride = (delta * waveLen) / facing;
|
|
535
|
+
const localDepth = THREE.MathUtils.clamp(
|
|
536
|
+
col.depth - (col.slope ?? 0) * ride, 0.05, col.depth * 3);
|
|
537
|
+
|
|
538
|
+
const crestX = col.x + col.dirX * ride;
|
|
539
|
+
const crestZ = col.z + col.dirZ * ride;
|
|
540
|
+
const thetaBeat = w1.waveNumber * (w1.dirX * crestX + w1.dirZ * crestZ) -
|
|
541
|
+
w1.omega * time + w1.phase;
|
|
542
|
+
const a0 = w0.amplitude;
|
|
543
|
+
const a1 = w1.amplitude;
|
|
544
|
+
const interference = Math.sqrt(Math.max(
|
|
545
|
+
a0 * a0 + a1 * a1 + 2 * a0 * a1 * Math.cos(Math.PI * 0.5 - thetaBeat), 0)) /
|
|
546
|
+
Math.max(a0 + a1, 1e-4);
|
|
547
|
+
const setEnvelope = p.setPair ? interference : 1;
|
|
548
|
+
const setGate = smoothstep(setEnvelope, 0.5, 0.78);
|
|
549
|
+
if (setGate < 1e-3) continue;
|
|
550
|
+
|
|
551
|
+
const approach = smoothstep(delta, -0.3, -0.03);
|
|
552
|
+
const pulse = approach * (1 - smoothstep(delta, 0.03, 0.3)) * setGate;
|
|
553
|
+
const post = smoothstep(delta, 0.02, 0.12) *
|
|
554
|
+
(1 - smoothstep(delta, 0.28, 0.46)) * setGate;
|
|
555
|
+
const travelFade = smoothstep(delta, -0.48, -0.4) *
|
|
556
|
+
(1 - smoothstep(delta, 0.38, 0.47));
|
|
557
|
+
|
|
558
|
+
const capDepth = Math.min(col.depth, localDepth);
|
|
559
|
+
const breakHeight = 0.9 * capDepth * p.scale * p.amount * col.endFade * setEnvelope;
|
|
560
|
+
const H = breakHeight * THREE.MathUtils.lerp(0.12, 1, pulse) * (1 - 0.55 * post);
|
|
561
|
+
const fade = p.amount * col.endFade * travelFade * setGate *
|
|
562
|
+
smoothstep(pulse + post, 0.12, 0.35) * smoothstep(H, 0.05, 0.15);
|
|
563
|
+
if (fade < 0.02 || H < 0.03) continue;
|
|
564
|
+
|
|
565
|
+
// Envelope profile along the travel axis, peaking at the leaned crest:
|
|
566
|
+
// the long back face rises over ~2.1 H, the front drops over ~0.9 H.
|
|
567
|
+
const lean = H * (0.3 * pulse + 0.45 * post);
|
|
568
|
+
const u = s - ride - lean;
|
|
569
|
+
const profile = u < 0
|
|
570
|
+
? smoothstep(u, -(2.1 * H + 0.35), 0)
|
|
571
|
+
: 1 - smoothstep(u, 0, 0.9 * H + 0.2);
|
|
572
|
+
const lateralFade = 1 - smoothstep(Math.sqrt(Math.max(latSq, 0)), 0.6, 1.2);
|
|
573
|
+
const weight = profile * fade * lateralFade;
|
|
574
|
+
if (weight <= result.weight) continue;
|
|
575
|
+
|
|
576
|
+
result.weight = weight;
|
|
577
|
+
result.crestY = H;
|
|
578
|
+
// Broken whitewater carries near the crest phase speed; the unbroken
|
|
579
|
+
// face mostly lifts, with a modest surface drift.
|
|
580
|
+
const push = (w0.omega / k0 / facing) * (0.25 * pulse + 0.9 * post) * weight;
|
|
581
|
+
result.flowX = col.dirX * push;
|
|
582
|
+
result.flowZ = col.dirZ * push;
|
|
583
|
+
}
|
|
584
|
+
return result;
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
dispose() {
|
|
588
|
+
this.mesh?.geometry.dispose();
|
|
589
|
+
this.material.dispose();
|
|
590
|
+
}
|
|
591
|
+
}
|