@wave3d/core 0.10.0 → 0.11.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/dist/config/model.d.ts +209 -6
- package/dist/config/model.js +85 -3
- package/dist/config/model.js.map +1 -1
- package/dist/index.d.ts +3 -2
- package/dist/index.js +3 -2
- package/dist/presets.js +295 -0
- package/dist/presets.js.map +1 -1
- package/dist/renderer/WaveGeometry.js +21 -0
- package/dist/renderer/WaveGeometry.js.map +1 -1
- package/dist/renderer/WaveRenderer.d.ts +62 -0
- package/dist/renderer/WaveRenderer.js +349 -10
- package/dist/renderer/WaveRenderer.js.map +1 -1
- package/dist/renderer/WaveRendererGPU.js +32 -2
- package/dist/renderer/WaveRendererGPU.js.map +1 -1
- package/dist/renderer/interaction.js +12 -0
- package/dist/renderer/interaction.js.map +1 -1
- package/dist/renderer/particleField.js +26 -2
- package/dist/renderer/particleField.js.map +1 -1
- package/dist/renderer/particleFieldGPU.js +6 -0
- package/dist/renderer/particleFieldGPU.js.map +1 -1
- package/dist/renderer/shaders.js +748 -13
- package/dist/renderer/shaders.js.map +1 -1
- package/dist/renderer/tsl/dissolve.js +56 -0
- package/dist/renderer/tsl/dissolve.js.map +1 -0
- package/dist/renderer/tsl/particleMaterial.js +46 -8
- package/dist/renderer/tsl/particleMaterial.js.map +1 -1
- package/dist/renderer/tsl/uniforms.js +40 -1
- package/dist/renderer/tsl/uniforms.js.map +1 -1
- package/dist/renderer/tsl/waveMaterial.js +275 -26
- package/dist/renderer/tsl/waveMaterial.js.map +1 -1
- package/dist/renderer/tsl/waveShape.js +31 -10
- package/dist/renderer/tsl/waveShape.js.map +1 -1
- package/dist/renderer/wavePath.js +189 -0
- package/dist/renderer/wavePath.js.map +1 -0
- package/dist/shell/createWave.d.ts +23 -3
- package/dist/shell/createWave.js +5 -4
- package/dist/shell/createWave.js.map +1 -1
- package/dist/shell/probe.d.ts +28 -0
- package/dist/shell/probe.js +58 -11
- package/dist/shell/probe.js.map +1 -1
- package/dist/standalone/wave3d.standalone.js +3401 -2108
- package/dist/standalone/wave3d.standalone.webgpu.js +7372 -5848
- package/dist/standalone.d.ts +2 -2
- package/dist/standalone.js +2 -2
- package/dist/studio/StudioWaveRenderer.d.ts +115 -8
- package/dist/studio/StudioWaveRenderer.js +588 -14
- package/dist/studio/StudioWaveRenderer.js.map +1 -1
- package/dist/studio/index.d.ts +2 -2
- package/dist/studio/index.js.map +1 -1
- package/dist/studio/randomize.js +0 -1
- package/dist/studio/randomize.js.map +1 -1
- package/package.json +1 -1
- package/skills/wave3d/SKILL.md +142 -5
|
@@ -108,6 +108,14 @@ type Wave = {
|
|
|
108
108
|
/** This wave's own particle / dust field — created when its `particles.count` first goes >0,
|
|
109
109
|
* disposed at 0 / absent (the WavePalette lifecycle pattern). Undefined = no dust for this wave. */
|
|
110
110
|
particleField?: WaveParticleField;
|
|
111
|
+
/** The baked centreline LUT for this wave's `path` (absent when it has none). */
|
|
112
|
+
pathTexture?: THREE.DataTexture;
|
|
113
|
+
/** Signature of the points the LUT was baked from, so a drag only rewrites it when it changed. */
|
|
114
|
+
pathSig?: string;
|
|
115
|
+
/** Glass only: the layer-count companion (this wave's vertex stage + a coverage-only fragment),
|
|
116
|
+
* built lazily by renderGlassPasses() and rebuilt when `layerKey` no longer matches. */
|
|
117
|
+
layerMaterial?: THREE.Material;
|
|
118
|
+
layerKey?: string;
|
|
111
119
|
};
|
|
112
120
|
/** Convert an sRGB hex string to a linear-space RGB vector (three's ColorManagement does the
|
|
113
121
|
* sRGB→linear conversion on parse). Exported for the studio subclass's live light-uniform push. */
|
|
@@ -147,6 +155,12 @@ declare class WaveRenderer {
|
|
|
147
155
|
protected config: StudioConfig;
|
|
148
156
|
protected waves: Wave[];
|
|
149
157
|
private backgroundTexture?;
|
|
158
|
+
/** The frame behind the glass waves — allocated only once a wave asks for the glass theme. */
|
|
159
|
+
private backdropTarget?;
|
|
160
|
+
/** How many glass layers cover each pixel. A sheet that folds over itself is thicker there, and
|
|
161
|
+
* thickness is the cue that reads as VOLUME rather than as a tinted film — but glass draws
|
|
162
|
+
* opaque, so the nearest layer wins and the fold behind it is invisible without counting. */
|
|
163
|
+
private layerTarget?;
|
|
150
164
|
private backgroundSig;
|
|
151
165
|
private backgroundImage?;
|
|
152
166
|
private backgroundImageUrl;
|
|
@@ -186,6 +200,11 @@ declare class WaveRenderer {
|
|
|
186
200
|
protected running: boolean;
|
|
187
201
|
private started;
|
|
188
202
|
private visible;
|
|
203
|
+
/** Read from the document, NOT assumed: the shell builds the renderer whenever the engine chunk
|
|
204
|
+
* lands, which can be after the reader has left the tab. Assumed visible, that renderer marked
|
|
205
|
+
* itself running with no frame to run on, and the `visibilitychange` that brought the reader back
|
|
206
|
+
* found nothing to restart — so the delta baseline was never reset and the first frame was handed
|
|
207
|
+
* the whole absence at once (see MAX_FRAME_SECONDS for the same stall without the event). */
|
|
189
208
|
private pageVisible;
|
|
190
209
|
private reducedMotion;
|
|
191
210
|
/** Intro ramp: eases animation time 0→1 over ~1s on load (when config.introRamp). */
|
|
@@ -216,6 +235,10 @@ declare class WaveRenderer {
|
|
|
216
235
|
init(): Promise<void>;
|
|
217
236
|
private get segments();
|
|
218
237
|
private makeUniforms;
|
|
238
|
+
/** Reconcile a wave's baked path LUT with its config. The texture is rewritten in place when the
|
|
239
|
+
* points change — which is what makes dragging a control point cheap, since the alternative is
|
|
240
|
+
* rebuilding 80k vertices per frame — and disposed when a wave loses its path. */
|
|
241
|
+
private syncPathTexture;
|
|
219
242
|
/** Vertex-shader #defines for a wave: TWIST_MOTION (per-wave animated twist wobble) and
|
|
220
243
|
* LOOP_MOTION (scene-level seamless loop). Both select #ifdef-gated code paths; an empty
|
|
221
244
|
* object compiles the default (linear-time) program. */
|
|
@@ -227,6 +250,20 @@ declare class WaveRenderer {
|
|
|
227
250
|
* surface, so everything downstream in `refresh()` is shared.
|
|
228
251
|
*/
|
|
229
252
|
protected createWaveMaterial(sc: WaveConfig | undefined): WaveMaterial;
|
|
253
|
+
/**
|
|
254
|
+
* The glass LAYER-COUNT companion for one wave: the same vertex program on the same uniforms
|
|
255
|
+
* object (shared by reference, so every per-frame write reaches it), with a fragment that only
|
|
256
|
+
* marks coverage. Drawn additively with depth off, so every fold over a pixel adds up.
|
|
257
|
+
*
|
|
258
|
+
* A stock override material could not stand in for this: its vertex stage knows nothing of the
|
|
259
|
+
* deformation and it culls back faces, so the count came out for the rest-pose plane — measured
|
|
260
|
+
* at a tenth of the real silhouette on the Liquid Glass preset. The WebGPU subclass builds the
|
|
261
|
+
* node twin from the same flags.
|
|
262
|
+
*/
|
|
263
|
+
protected createLayerMaterial(sc: WaveConfig, material: WaveMaterial): THREE.Material;
|
|
264
|
+
/** What the layer companion was built against; when this changes, it is rebuilt. */
|
|
265
|
+
protected layerVariantKey(material: WaveMaterial): string;
|
|
266
|
+
private dropLayerMaterial;
|
|
230
267
|
/**
|
|
231
268
|
* Re-select this wave's shader variant when its config changes shape (theme, twist motion, helix,
|
|
232
269
|
* …). Returns true if the material needs recompiling. The GLSL backend swaps `defines` and the
|
|
@@ -360,6 +397,31 @@ declare class WaveRenderer {
|
|
|
360
397
|
private updateTime;
|
|
361
398
|
/** Draw the composed frame. WebGL runs the EffectComposer; WebGPU runs a node post chain. */
|
|
362
399
|
protected renderComposed(): void;
|
|
400
|
+
/** Capture what sits BEHIND the glass waves, so they have something to bend.
|
|
401
|
+
*
|
|
402
|
+
* Glass is drawn opaque and fakes its see-through by sampling this texture, which is why
|
|
403
|
+
* overlapping sheets need no sorting. The pass is skipped entirely when no wave asks for it, so
|
|
404
|
+
* a scene without glass renders exactly as before.
|
|
405
|
+
*
|
|
406
|
+
* The texture MUST be unbound while we render into it: binding a target as a texture while it is
|
|
407
|
+
* the render target is a framebuffer feedback loop, and the frame is undefined. */
|
|
408
|
+
/** The world axis from a surface toward the camera. Constant under an orthographic projection,
|
|
409
|
+
* which is exactly why it is a uniform rather than something the shader derives per fragment. */
|
|
410
|
+
private pushViewAxis;
|
|
411
|
+
private viewAxisTmp;
|
|
412
|
+
private empty?;
|
|
413
|
+
private backdropClear;
|
|
414
|
+
private prevClear;
|
|
415
|
+
private depthTmp;
|
|
416
|
+
private sizeTmp;
|
|
417
|
+
/** Pin a capture's sampling so both backends read it the same way. Left to their defaults the
|
|
418
|
+
* two disagree about filtering and edge behaviour, and a glass sheet samples FAR from the
|
|
419
|
+
* fragment — one offset per channel — so the disagreement shows up as a red/blue bias with green
|
|
420
|
+
* untouched, which looks like a dispersion bug and is not one. */
|
|
421
|
+
private pinSampling;
|
|
422
|
+
/** A 1x1 transparent texture, used wherever a sampler must be BOUND but must contribute nothing. */
|
|
423
|
+
private emptyTexture;
|
|
424
|
+
protected renderGlassPasses(): void;
|
|
363
425
|
/** Resize the post chain's render targets. */
|
|
364
426
|
protected resizePost(w: number, h: number, dpr: number): void;
|
|
365
427
|
/** Release the post chain's GPU resources. */
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { ensureStudioConfig } from "../config/model.js";
|
|
2
2
|
import { WaveGeometry } from "./WaveGeometry.js";
|
|
3
|
-
import {
|
|
3
|
+
import { bakePathTexture, writePathTexture } from "./wavePath.js";
|
|
4
|
+
import { ditherFragmentShader, fragmentShader, glassFragmentShader, glassLayerFragmentShader, halftoneCmykFragmentShader, halftoneFragmentShader, heatmapFragmentShader, innerLightFragmentShader, lineFragmentShader, paperTextureFragmentShader, postFragmentShader, postVertexShader, vertexShader } from "./shaders.js";
|
|
4
5
|
import { anyPointerFxActive, interactionActive, wavePointerFxActive, waveRipplesActive } from "./interactionGates.js";
|
|
5
6
|
import { ParticleField } from "./particleField.js";
|
|
6
7
|
import { PALETTE_MAPS, buildBackgroundGradientCanvas, buildBackgroundImageCanvas, buildBackgroundMeshCanvas, buildPaletteTexture, canvasToTexture, configurePaletteTexture, drawBackgroundMediaFrame, loadPaletteImage, paletteMapCanvas, paletteSignature } from "./palette.js";
|
|
@@ -12,7 +13,40 @@ import { OutputPass } from "three/addons/postprocessing/OutputPass.js";
|
|
|
12
13
|
import { ShaderPass } from "three/addons/postprocessing/ShaderPass.js";
|
|
13
14
|
import { UnrealBloomPass } from "three/addons/postprocessing/UnrealBloomPass.js";
|
|
14
15
|
//#region src/renderer/WaveRenderer.ts
|
|
16
|
+
/** Which fragment program a wave's theme wants. */
|
|
17
|
+
function fragmentFor(sc) {
|
|
18
|
+
if (sc?.theme === "wireframe") return lineFragmentShader;
|
|
19
|
+
if (sc?.theme === "glass") return glassFragmentShader;
|
|
20
|
+
return fragmentShader;
|
|
21
|
+
}
|
|
22
|
+
/** How opaque a wave's between-strand colour is. Absent is the page background at full strength (the
|
|
23
|
+
* theme as it always drew); `"transparent"` or an 8-digit hex carries its own alpha. */
|
|
24
|
+
function gapAlpha(color) {
|
|
25
|
+
if (!color) return 1;
|
|
26
|
+
if (color === "transparent") return 0;
|
|
27
|
+
if (/^#[0-9a-f]{8}$/i.test(color)) return parseInt(color.slice(7, 9), 16) / 255;
|
|
28
|
+
return 1;
|
|
29
|
+
}
|
|
30
|
+
/** The colour half of the same field — the alpha is stripped, and `"transparent"` has no colour of
|
|
31
|
+
* its own, so it falls back to the page background it is standing in for. */
|
|
32
|
+
function gapColorHex(color) {
|
|
33
|
+
if (!color || color === "transparent") return void 0;
|
|
34
|
+
return /^#[0-9a-f]{8}$/i.test(color) ? color.slice(0, 7) : color;
|
|
35
|
+
}
|
|
36
|
+
/** Dissolve axis name → the float the shaders branch on (see dissolveChunk's dissolveCoord). */
|
|
37
|
+
const DISSOLVE_AXIS_INDEX = {
|
|
38
|
+
length: 0,
|
|
39
|
+
width: 1,
|
|
40
|
+
screenX: 2,
|
|
41
|
+
screenY: 3
|
|
42
|
+
};
|
|
15
43
|
const BASE_SEGMENTS = 220;
|
|
44
|
+
/** The longest frame the clock will believe, in seconds. Anything past it is a STALL — frames
|
|
45
|
+
* suspended with no `visibilitychange` to say so (an embedding webview that parks rAF, a debugger
|
|
46
|
+
* pause, the machine asleep) — and replaying a stall is a fast-forward, not motion. A ceiling, not
|
|
47
|
+
* a jitter clamp: recordings run on this same wall-clock loop, so a merely slow frame (a 4K export
|
|
48
|
+
* at 10fps) must still advance by its real length or a `loopSeconds` export stops closing. */
|
|
49
|
+
const MAX_FRAME_SECONDS = 1;
|
|
16
50
|
/** Reference frame (world units) the orthographic camera fills at cameraZoom 1. The wave is
|
|
17
51
|
* framed by mapping this FRAME_W × FRAME_H rectangle (centred on cameraTarget) onto the canvas,
|
|
18
52
|
* so a given cameraZoom / cameraTarget frames the wave the SAME at any canvas size or aspect
|
|
@@ -202,6 +236,12 @@ var WaveRenderer = class {
|
|
|
202
236
|
config;
|
|
203
237
|
waves = [];
|
|
204
238
|
backgroundTexture;
|
|
239
|
+
/** The frame behind the glass waves — allocated only once a wave asks for the glass theme. */
|
|
240
|
+
backdropTarget;
|
|
241
|
+
/** How many glass layers cover each pixel. A sheet that folds over itself is thicker there, and
|
|
242
|
+
* thickness is the cue that reads as VOLUME rather than as a tinted film — but glass draws
|
|
243
|
+
* opaque, so the nearest layer wins and the fold behind it is invisible without counting. */
|
|
244
|
+
layerTarget;
|
|
205
245
|
backgroundSig = "";
|
|
206
246
|
backgroundImage;
|
|
207
247
|
backgroundImageUrl = "";
|
|
@@ -244,7 +284,12 @@ var WaveRenderer = class {
|
|
|
244
284
|
running = false;
|
|
245
285
|
started = false;
|
|
246
286
|
visible = true;
|
|
247
|
-
|
|
287
|
+
/** Read from the document, NOT assumed: the shell builds the renderer whenever the engine chunk
|
|
288
|
+
* lands, which can be after the reader has left the tab. Assumed visible, that renderer marked
|
|
289
|
+
* itself running with no frame to run on, and the `visibilitychange` that brought the reader back
|
|
290
|
+
* found nothing to restart — so the delta baseline was never reset and the first frame was handed
|
|
291
|
+
* the whole absence at once (see MAX_FRAME_SECONDS for the same stall without the event). */
|
|
292
|
+
pageVisible = document.visibilityState === "visible";
|
|
248
293
|
reducedMotion = false;
|
|
249
294
|
/** Intro ramp: eases animation time 0→1 over ~1s on load (when config.introRamp). */
|
|
250
295
|
introTimeRamp = 0;
|
|
@@ -422,6 +467,28 @@ var WaveRenderer = class {
|
|
|
422
467
|
uCreaseSoftness: { value: 1 },
|
|
423
468
|
uEdgeFade: { value: .06 },
|
|
424
469
|
uEdgeFeather: { value: .1 },
|
|
470
|
+
uBackdrop: { value: null },
|
|
471
|
+
uGlassStrength: { value: 90 },
|
|
472
|
+
uGlassChroma: { value: .7 },
|
|
473
|
+
uGlassFrost: { value: .08 },
|
|
474
|
+
uGlassSpec: { value: 1.2 },
|
|
475
|
+
uGlassVibrancy: { value: .05 },
|
|
476
|
+
uGlassTint: { value: .12 },
|
|
477
|
+
uGlassRimPower: { value: 1.2 },
|
|
478
|
+
uGlassRipple: { value: 0 },
|
|
479
|
+
uGlassRippleScale: { value: .012 },
|
|
480
|
+
uGlassFlow: { value: .9 },
|
|
481
|
+
uGlassPath: { value: .45 },
|
|
482
|
+
uGlassDensity: { value: 1.2 },
|
|
483
|
+
uGlassRim: { value: .5 },
|
|
484
|
+
uGlassIrid: { value: 0 },
|
|
485
|
+
uGlassFilmNm: { value: 380 },
|
|
486
|
+
uGlassIor: { value: 1.45 },
|
|
487
|
+
uLayers: { value: null },
|
|
488
|
+
uGlassLayerGain: { value: .6 },
|
|
489
|
+
uGlassFusion: { value: 0 },
|
|
490
|
+
uGlassCaustic: { value: .4 },
|
|
491
|
+
uViewAxis: { value: new THREE.Vector3(0, 0, 1) },
|
|
425
492
|
uOpacity: { value: 1 },
|
|
426
493
|
uSquared: { value: 1 },
|
|
427
494
|
uResolution: { value: this.renderer.getDrawingBufferSize(new THREE.Vector2()) },
|
|
@@ -437,19 +504,32 @@ var WaveRenderer = class {
|
|
|
437
504
|
uLineAmount: { value: 425 },
|
|
438
505
|
uLineThickness: { value: 1 },
|
|
439
506
|
uLineDerivativePower: { value: .95 },
|
|
440
|
-
|
|
507
|
+
uLineDepthFade: { value: 1 },
|
|
508
|
+
uLineSharpness: { value: 0 },
|
|
509
|
+
uLineGapOpacity: { value: 1 },
|
|
510
|
+
uLineLight: { value: 0 },
|
|
441
511
|
uClearColor: { value: new THREE.Vector3(1, 1, 1) },
|
|
442
512
|
uHelixTurns: { value: 0 },
|
|
443
513
|
uHelixRadius: { value: 0 },
|
|
444
514
|
uHelixRoll: { value: 0 },
|
|
445
515
|
uHelixPhase: { value: 0 },
|
|
516
|
+
uPathTex: { value: null },
|
|
446
517
|
uRadialAmount: { value: 0 },
|
|
447
518
|
uRadialArc: { value: 160 },
|
|
448
519
|
uRadialSpread: { value: 1 },
|
|
449
520
|
uRadialRadius: { value: 40 },
|
|
450
521
|
uRadialCenter: { value: 0 },
|
|
522
|
+
uRadialCone: { value: 0 },
|
|
523
|
+
uRadialSwirl: { value: 0 },
|
|
451
524
|
uRungAmount: { value: 0 },
|
|
452
525
|
uRungThickness: { value: 1 },
|
|
526
|
+
uDissolveAmount: { value: 0 },
|
|
527
|
+
uDissolveBand: { value: .35 },
|
|
528
|
+
uDissolveScale: { value: 90 },
|
|
529
|
+
uDissolveBlocky: { value: .6 },
|
|
530
|
+
uDissolveAxis: { value: 0 },
|
|
531
|
+
uDissolveReverse: { value: 0 },
|
|
532
|
+
uDissolveDust: { value: 1 },
|
|
453
533
|
uPointer: { value: new THREE.Vector2(0, 0) },
|
|
454
534
|
uPointerActive: { value: 0 },
|
|
455
535
|
uPointerRadius: { value: .6 },
|
|
@@ -468,6 +548,30 @@ var WaveRenderer = class {
|
|
|
468
548
|
uRippleAmp: { value: rippleAmp }
|
|
469
549
|
};
|
|
470
550
|
}
|
|
551
|
+
/** Reconcile a wave's baked path LUT with its config. The texture is rewritten in place when the
|
|
552
|
+
* points change — which is what makes dragging a control point cheap, since the alternative is
|
|
553
|
+
* rebuilding 80k vertices per frame — and disposed when a wave loses its path. */
|
|
554
|
+
syncPathTexture(wave, sc) {
|
|
555
|
+
const pts = sc.path;
|
|
556
|
+
if (!pts || pts.length < 2) {
|
|
557
|
+
if (wave.pathTexture) {
|
|
558
|
+
wave.pathTexture.dispose();
|
|
559
|
+
wave.pathTexture = void 0;
|
|
560
|
+
wave.pathSig = void 0;
|
|
561
|
+
}
|
|
562
|
+
return;
|
|
563
|
+
}
|
|
564
|
+
const sig = JSON.stringify(pts);
|
|
565
|
+
if (!wave.pathTexture) {
|
|
566
|
+
wave.pathTexture = bakePathTexture(pts);
|
|
567
|
+
wave.pathSig = sig;
|
|
568
|
+
} else if (sig !== wave.pathSig) {
|
|
569
|
+
writePathTexture(wave.pathTexture.image.data, pts);
|
|
570
|
+
wave.pathTexture.needsUpdate = true;
|
|
571
|
+
wave.pathSig = sig;
|
|
572
|
+
}
|
|
573
|
+
wave.material.uniforms.uPathTex.value = wave.pathTexture;
|
|
574
|
+
}
|
|
471
575
|
/** Vertex-shader #defines for a wave: TWIST_MOTION (per-wave animated twist wobble) and
|
|
472
576
|
* LOOP_MOTION (scene-level seamless loop). Both select #ifdef-gated code paths; an empty
|
|
473
577
|
* object compiles the default (linear-time) program. */
|
|
@@ -481,8 +585,15 @@ var WaveRenderer = class {
|
|
|
481
585
|
if ((sc?.edgeFeather ?? .1) !== .1) defines.EDGE_FEATHER = "";
|
|
482
586
|
const bindsHelix = sc?.interaction?.bindings?.some((b) => b.target.startsWith("helix")) ?? false;
|
|
483
587
|
if ((sc?.helixRadius ?? 0) !== 0 || (sc?.helixRoll ?? 0) !== 0 || bindsHelix) defines.HELIX = "";
|
|
588
|
+
if (sc?.path && sc.path.length >= 2) defines.PATH = "";
|
|
589
|
+
if (sc?.theme === "glass") defines.VERTEX_NORMAL = "";
|
|
484
590
|
if ((sc?.radialAmount ?? 0) !== 0) defines.RADIAL = "";
|
|
485
591
|
if (sc?.theme === "wireframe" && (sc.rungAmount ?? 0) > 0) defines.RUNGS = "";
|
|
592
|
+
if (sc?.theme === "wireframe" && (sc.lineSharpness ?? 0) > 0) defines.LINE_SHARP = "";
|
|
593
|
+
if (sc?.theme === "wireframe" && gapAlpha(sc.lineGapColor) < 1) defines.LINE_CLEAR_GAPS = "";
|
|
594
|
+
if (sc?.theme === "wireframe" && (sc.lineLight ?? 0) > 0) defines.LINE_LIGHT = "";
|
|
595
|
+
const bindsDissolve = sc?.interaction?.bindings?.some((b) => b.target === "dissolveAmount") ?? false;
|
|
596
|
+
if (sc?.dissolve && ((sc.dissolve.amount ?? 0) > 0 || bindsDissolve)) defines.DISSOLVE = "";
|
|
486
597
|
if (sc && wavePointerFxActive(this.config, sc)) {
|
|
487
598
|
defines.POINTER_FX = "";
|
|
488
599
|
if (waveRipplesActive(this.config, sc)) defines.POINTER_RIPPLES = "";
|
|
@@ -519,14 +630,51 @@ var WaveRenderer = class {
|
|
|
519
630
|
uniforms: this.makeUniforms(),
|
|
520
631
|
defines: this.waveDefines(sc),
|
|
521
632
|
vertexShader,
|
|
522
|
-
fragmentShader: sc
|
|
523
|
-
transparent:
|
|
633
|
+
fragmentShader: fragmentFor(sc),
|
|
634
|
+
transparent: sc?.theme !== "glass",
|
|
524
635
|
depthTest: true,
|
|
525
636
|
depthWrite: true,
|
|
526
637
|
side: THREE.DoubleSide
|
|
527
638
|
});
|
|
528
639
|
}
|
|
529
640
|
/**
|
|
641
|
+
* The glass LAYER-COUNT companion for one wave: the same vertex program on the same uniforms
|
|
642
|
+
* object (shared by reference, so every per-frame write reaches it), with a fragment that only
|
|
643
|
+
* marks coverage. Drawn additively with depth off, so every fold over a pixel adds up.
|
|
644
|
+
*
|
|
645
|
+
* A stock override material could not stand in for this: its vertex stage knows nothing of the
|
|
646
|
+
* deformation and it culls back faces, so the count came out for the rest-pose plane — measured
|
|
647
|
+
* at a tenth of the real silhouette on the Liquid Glass preset. The WebGPU subclass builds the
|
|
648
|
+
* node twin from the same flags.
|
|
649
|
+
*/
|
|
650
|
+
createLayerMaterial(sc, material) {
|
|
651
|
+
const main = material;
|
|
652
|
+
const defines = { ...main.defines ?? this.waveDefines(sc) };
|
|
653
|
+
delete defines.VERTEX_NORMAL;
|
|
654
|
+
return new THREE.ShaderMaterial({
|
|
655
|
+
uniforms: main.uniforms,
|
|
656
|
+
defines,
|
|
657
|
+
vertexShader,
|
|
658
|
+
fragmentShader: glassLayerFragmentShader,
|
|
659
|
+
transparent: true,
|
|
660
|
+
blending: THREE.AdditiveBlending,
|
|
661
|
+
depthTest: false,
|
|
662
|
+
depthWrite: false,
|
|
663
|
+
side: THREE.DoubleSide
|
|
664
|
+
});
|
|
665
|
+
}
|
|
666
|
+
/** What the layer companion was built against; when this changes, it is rebuilt. */
|
|
667
|
+
layerVariantKey(material) {
|
|
668
|
+
const defines = material.defines ?? {};
|
|
669
|
+
return Object.keys(defines).sort().join(",");
|
|
670
|
+
}
|
|
671
|
+
dropLayerMaterial(wave) {
|
|
672
|
+
if (!wave.layerMaterial) return;
|
|
673
|
+
wave.layerMaterial.dispose();
|
|
674
|
+
wave.layerMaterial = void 0;
|
|
675
|
+
wave.layerKey = void 0;
|
|
676
|
+
}
|
|
677
|
+
/**
|
|
530
678
|
* Re-select this wave's shader variant when its config changes shape (theme, twist motion, helix,
|
|
531
679
|
* …). Returns true if the material needs recompiling. The GLSL backend swaps `defines` and the
|
|
532
680
|
* fragment source; the WebGPU subclass rebuilds the node graph, since a TSL variant is a
|
|
@@ -541,11 +689,12 @@ var WaveRenderer = class {
|
|
|
541
689
|
material.defines = wantDefines;
|
|
542
690
|
changed = true;
|
|
543
691
|
}
|
|
544
|
-
const wantFrag = sc
|
|
692
|
+
const wantFrag = fragmentFor(sc);
|
|
545
693
|
if (material.fragmentShader !== wantFrag) {
|
|
546
694
|
material.fragmentShader = wantFrag;
|
|
547
695
|
changed = true;
|
|
548
696
|
}
|
|
697
|
+
material.transparent = sc.theme !== "glass";
|
|
549
698
|
return changed;
|
|
550
699
|
}
|
|
551
700
|
/**
|
|
@@ -571,6 +720,7 @@ var WaveRenderer = class {
|
|
|
571
720
|
for (const s of this.waves) {
|
|
572
721
|
this.group.remove(s.mesh);
|
|
573
722
|
s.material.dispose();
|
|
723
|
+
s.layerMaterial?.dispose();
|
|
574
724
|
s.geometry.dispose();
|
|
575
725
|
s.palette.dispose();
|
|
576
726
|
if (s.particleField) {
|
|
@@ -593,6 +743,7 @@ var WaveRenderer = class {
|
|
|
593
743
|
if (!s) break;
|
|
594
744
|
this.group.remove(s.mesh);
|
|
595
745
|
s.material.dispose();
|
|
746
|
+
s.layerMaterial?.dispose();
|
|
596
747
|
s.geometry.dispose();
|
|
597
748
|
s.palette.dispose();
|
|
598
749
|
if (s.particleField) {
|
|
@@ -663,10 +814,15 @@ var WaveRenderer = class {
|
|
|
663
814
|
u.uLineAmount.value = sc.lineAmount ?? 425;
|
|
664
815
|
u.uLineThickness.value = sc.lineThickness ?? 1;
|
|
665
816
|
u.uLineDerivativePower.value = sc.lineDerivativePower ?? .95;
|
|
817
|
+
u.uLineDepthFade.value = sc.lineDepthFade ?? 1;
|
|
818
|
+
u.uLineSharpness.value = sc.lineSharpness ?? 0;
|
|
819
|
+
u.uLineGapOpacity.value = gapAlpha(sc.lineGapColor);
|
|
820
|
+
u.uLineLight.value = sc.lineLight ?? 0;
|
|
821
|
+
const wantDepthWrite = !(sc.theme === "wireframe" && gapAlpha(sc.lineGapColor) < 1);
|
|
822
|
+
if (wave.material.depthWrite !== wantDepthWrite) wave.material.depthWrite = wantDepthWrite;
|
|
666
823
|
u.uRungAmount.value = sc.rungAmount ?? 0;
|
|
667
824
|
u.uRungThickness.value = sc.rungThickness ?? 1;
|
|
668
|
-
|
|
669
|
-
hexToLinearVec3(this.config.background, u.uClearColor.value);
|
|
825
|
+
hexToLinearVec3(gapColorHex(sc.lineGapColor) ?? this.config.background, u.uClearColor.value);
|
|
670
826
|
u.uFiberCount.value = sc.fiberCount;
|
|
671
827
|
u.uFiberStrength.value = sc.fiberStrength;
|
|
672
828
|
u.uTexture.value = sc.texture;
|
|
@@ -680,6 +836,32 @@ var WaveRenderer = class {
|
|
|
680
836
|
hexToLinearVec3(sc.depthTintColor ?? "#0a2540", u.uDepthTintColor.value);
|
|
681
837
|
u.uEdgeFade.value = sc.edgeFade;
|
|
682
838
|
u.uEdgeFeather.value = sc.edgeFeather ?? .1;
|
|
839
|
+
if (sc.theme === "glass") {
|
|
840
|
+
hexToLinearVec3(this.config.background, u.uClearColor.value);
|
|
841
|
+
u.uGlassStrength.value = sc.glassStrength ?? 90;
|
|
842
|
+
u.uGlassChroma.value = sc.glassChroma ?? .7;
|
|
843
|
+
u.uGlassFrost.value = sc.glassFrost ?? .08;
|
|
844
|
+
u.uGlassSpec.value = sc.glassSpec ?? 1.2;
|
|
845
|
+
u.uGlassVibrancy.value = sc.glassVibrancy ?? .05;
|
|
846
|
+
u.uGlassTint.value = sc.glassTint ?? .12;
|
|
847
|
+
u.uGlassRimPower.value = sc.glassRimPower ?? 1.2;
|
|
848
|
+
u.uGlassRipple.value = sc.glassRipple ?? 0;
|
|
849
|
+
u.uGlassRippleScale.value = sc.glassRippleScale ?? .012;
|
|
850
|
+
{
|
|
851
|
+
const flow = sc.glassFlow ?? .9;
|
|
852
|
+
const loop = this.config.loopSeconds ?? 0;
|
|
853
|
+
u.uGlassFlow.value = loop > 0 && flow !== 0 ? Math.max(1, Math.round(Math.abs(flow) * loop / (Math.PI * 2))) * (Math.PI * 2) / loop / (flow < 0 ? -1 : 1) : flow;
|
|
854
|
+
}
|
|
855
|
+
u.uGlassPath.value = sc.glassPath ?? .45;
|
|
856
|
+
u.uGlassDensity.value = sc.glassDensity ?? 1.2;
|
|
857
|
+
u.uGlassRim.value = sc.glassRim ?? .5;
|
|
858
|
+
u.uGlassIrid.value = sc.glassIrid ?? 0;
|
|
859
|
+
u.uGlassFilmNm.value = sc.glassFilmNm ?? 380;
|
|
860
|
+
u.uGlassIor.value = sc.glassIor ?? 1.45;
|
|
861
|
+
u.uGlassLayerGain.value = sc.glassLayerGain ?? .6;
|
|
862
|
+
u.uGlassFusion.value = sc.glassFusion ?? 0;
|
|
863
|
+
u.uGlassCaustic.value = sc.glassCaustic ?? .4;
|
|
864
|
+
}
|
|
683
865
|
const lights = this.config.lights ?? [];
|
|
684
866
|
u.uAmbient.value = this.config.ambient ?? .45;
|
|
685
867
|
u.uNumLights.value = Math.min(lights.length, 8);
|
|
@@ -724,12 +906,23 @@ var WaveRenderer = class {
|
|
|
724
906
|
u.uHelixTurns.value = sc.helixTurns ?? 0;
|
|
725
907
|
u.uHelixRadius.value = sc.helixRadius ?? 0;
|
|
726
908
|
u.uHelixRoll.value = sc.helixRoll ?? 0;
|
|
909
|
+
this.syncPathTexture(wave, sc);
|
|
727
910
|
u.uHelixPhase.value = sc.helixPhase ?? 0;
|
|
728
911
|
u.uRadialAmount.value = sc.radialAmount ?? 0;
|
|
729
912
|
u.uRadialArc.value = sc.radialArc ?? 160;
|
|
730
913
|
u.uRadialSpread.value = sc.radialSpread ?? 1;
|
|
731
914
|
u.uRadialRadius.value = sc.radialRadius ?? 40;
|
|
732
915
|
u.uRadialCenter.value = sc.radialCenter ?? 0;
|
|
916
|
+
u.uRadialCone.value = sc.radialCone ?? 0;
|
|
917
|
+
u.uRadialSwirl.value = sc.radialSwirl ?? 0;
|
|
918
|
+
const dis = sc.dissolve;
|
|
919
|
+
u.uDissolveAmount.value = dis?.amount ?? 0;
|
|
920
|
+
u.uDissolveBand.value = dis?.band ?? .35;
|
|
921
|
+
u.uDissolveScale.value = dis?.scale ?? 90;
|
|
922
|
+
u.uDissolveBlocky.value = dis?.blocky ?? .6;
|
|
923
|
+
u.uDissolveAxis.value = DISSOLVE_AXIS_INDEX[dis?.axis ?? "length"] ?? 0;
|
|
924
|
+
u.uDissolveReverse.value = dis?.reverse ? 1 : 0;
|
|
925
|
+
u.uDissolveDust.value = dis?.dust ?? 1;
|
|
733
926
|
wave.mesh.scale.set(sc.scale.x, sc.scale.y, sc.scale.z);
|
|
734
927
|
wave.mesh.rotation.set(THREE.MathUtils.degToRad(sc.rotation.x), THREE.MathUtils.degToRad(sc.rotation.y), THREE.MathUtils.degToRad(sc.rotation.z));
|
|
735
928
|
wave.mesh.position.set(sc.position.x, sc.position.y, sc.position.z);
|
|
@@ -1378,7 +1571,7 @@ var WaveRenderer = class {
|
|
|
1378
1571
|
loop = () => {
|
|
1379
1572
|
if (!this.running) return;
|
|
1380
1573
|
this.timer.update();
|
|
1381
|
-
const dt = this.timer.getDelta();
|
|
1574
|
+
const dt = Math.min(this.timer.getDelta(), MAX_FRAME_SECONDS);
|
|
1382
1575
|
this.time += dt;
|
|
1383
1576
|
this.interaction?.update(dt);
|
|
1384
1577
|
if (this.introTimeRamp < 1) this.introTimeRamp = Math.min(1, this.introTimeRamp + .016);
|
|
@@ -1406,8 +1599,144 @@ var WaveRenderer = class {
|
|
|
1406
1599
|
}
|
|
1407
1600
|
/** Draw the composed frame. WebGL runs the EffectComposer; WebGPU runs a node post chain. */
|
|
1408
1601
|
renderComposed() {
|
|
1602
|
+
this.renderGlassPasses();
|
|
1409
1603
|
this.composer.render();
|
|
1410
1604
|
}
|
|
1605
|
+
/** Capture what sits BEHIND the glass waves, so they have something to bend.
|
|
1606
|
+
*
|
|
1607
|
+
* Glass is drawn opaque and fakes its see-through by sampling this texture, which is why
|
|
1608
|
+
* overlapping sheets need no sorting. The pass is skipped entirely when no wave asks for it, so
|
|
1609
|
+
* a scene without glass renders exactly as before.
|
|
1610
|
+
*
|
|
1611
|
+
* The texture MUST be unbound while we render into it: binding a target as a texture while it is
|
|
1612
|
+
* the render target is a framebuffer feedback loop, and the frame is undefined. */
|
|
1613
|
+
/** The world axis from a surface toward the camera. Constant under an orthographic projection,
|
|
1614
|
+
* which is exactly why it is a uniform rather than something the shader derives per fragment. */
|
|
1615
|
+
pushViewAxis() {
|
|
1616
|
+
const fwd = this.camera.getWorldDirection(this.viewAxisTmp).multiplyScalar(-1);
|
|
1617
|
+
for (const w of this.waves) w.material.uniforms.uViewAxis.value.copy(fwd);
|
|
1618
|
+
}
|
|
1619
|
+
viewAxisTmp = new THREE.Vector3();
|
|
1620
|
+
empty;
|
|
1621
|
+
backdropClear = new THREE.Color();
|
|
1622
|
+
prevClear = new THREE.Color();
|
|
1623
|
+
depthTmp = new THREE.Vector3();
|
|
1624
|
+
sizeTmp = new THREE.Vector2();
|
|
1625
|
+
/** Pin a capture's sampling so both backends read it the same way. Left to their defaults the
|
|
1626
|
+
* two disagree about filtering and edge behaviour, and a glass sheet samples FAR from the
|
|
1627
|
+
* fragment — one offset per channel — so the disagreement shows up as a red/blue bias with green
|
|
1628
|
+
* untouched, which looks like a dispersion bug and is not one. */
|
|
1629
|
+
pinSampling(t) {
|
|
1630
|
+
t.texture.minFilter = THREE.LinearFilter;
|
|
1631
|
+
t.texture.magFilter = THREE.LinearFilter;
|
|
1632
|
+
t.texture.wrapS = THREE.ClampToEdgeWrapping;
|
|
1633
|
+
t.texture.wrapT = THREE.ClampToEdgeWrapping;
|
|
1634
|
+
t.texture.generateMipmaps = false;
|
|
1635
|
+
return t;
|
|
1636
|
+
}
|
|
1637
|
+
/** A 1x1 transparent texture, used wherever a sampler must be BOUND but must contribute nothing. */
|
|
1638
|
+
emptyTexture() {
|
|
1639
|
+
if (!this.empty) {
|
|
1640
|
+
this.empty = new THREE.DataTexture(new Uint8Array([
|
|
1641
|
+
0,
|
|
1642
|
+
0,
|
|
1643
|
+
0,
|
|
1644
|
+
0
|
|
1645
|
+
]), 1, 1);
|
|
1646
|
+
this.empty.needsUpdate = true;
|
|
1647
|
+
}
|
|
1648
|
+
return this.empty;
|
|
1649
|
+
}
|
|
1650
|
+
renderGlassPasses() {
|
|
1651
|
+
this.pushViewAxis();
|
|
1652
|
+
const glassIdx = [];
|
|
1653
|
+
for (let i = 0; i < this.waves.length; i++) if ((this.config.waves[i] ?? this.config.waves[this.config.waves.length - 1])?.theme === "glass") glassIdx.push(i);
|
|
1654
|
+
if (glassIdx.length === 0) {
|
|
1655
|
+
if (this.backdropTarget) for (const w of this.waves) w.material.uniforms.uBackdrop.value = null;
|
|
1656
|
+
for (const w of this.waves) this.dropLayerMaterial(w);
|
|
1657
|
+
return;
|
|
1658
|
+
}
|
|
1659
|
+
this.scene.updateMatrixWorld();
|
|
1660
|
+
const axis = this.viewAxisTmp;
|
|
1661
|
+
const depthOf = (i) => this.waves[i].mesh.getWorldPosition(this.depthTmp).dot(axis);
|
|
1662
|
+
const glassDepth = glassIdx.map(depthOf);
|
|
1663
|
+
const excluded = [];
|
|
1664
|
+
for (let i = 0; i < this.waves.length; i++) {
|
|
1665
|
+
const d = depthOf(i);
|
|
1666
|
+
const inFront = glassIdx.some((g, k) => {
|
|
1667
|
+
const dd = d - glassDepth[k];
|
|
1668
|
+
return dd > .001 || Math.abs(dd) <= .001 && i > g;
|
|
1669
|
+
});
|
|
1670
|
+
if (glassIdx.includes(i) || inFront) excluded.push(this.waves[i].mesh);
|
|
1671
|
+
}
|
|
1672
|
+
const size = this.renderer.getDrawingBufferSize(this.sizeTmp);
|
|
1673
|
+
if (!this.backdropTarget) this.backdropTarget = this.pinSampling(new THREE.WebGLRenderTarget(size.x, size.y, {
|
|
1674
|
+
depthBuffer: true,
|
|
1675
|
+
stencilBuffer: false
|
|
1676
|
+
}));
|
|
1677
|
+
else if (this.backdropTarget.width !== size.x || this.backdropTarget.height !== size.y) this.backdropTarget.setSize(size.x, size.y);
|
|
1678
|
+
const prevTarget = this.renderer.getRenderTarget();
|
|
1679
|
+
const prevClearAlpha = this.renderer.getClearAlpha();
|
|
1680
|
+
this.renderer.getClearColor(this.prevClear);
|
|
1681
|
+
const prevBg = this.scene.background;
|
|
1682
|
+
const restore = () => {
|
|
1683
|
+
this.renderer.setRenderTarget(prevTarget);
|
|
1684
|
+
this.renderer.setClearColor(this.prevClear, prevClearAlpha);
|
|
1685
|
+
this.scene.background = prevBg;
|
|
1686
|
+
};
|
|
1687
|
+
for (const w of this.waves) w.material.uniforms.uBackdrop.value = this.emptyTexture();
|
|
1688
|
+
for (const m of excluded) m.visible = false;
|
|
1689
|
+
this.renderer.setRenderTarget(this.backdropTarget);
|
|
1690
|
+
this.backdropClear.set(this.config.background || "#ffffff");
|
|
1691
|
+
this.renderer.setClearColor(this.backdropClear, 1);
|
|
1692
|
+
this.renderer.clear(true, true, false);
|
|
1693
|
+
this.renderer.render(this.scene, this.camera);
|
|
1694
|
+
for (const m of excluded) m.visible = true;
|
|
1695
|
+
for (const w of this.waves) w.material.uniforms.uBackdrop.value = this.backdropTarget.texture;
|
|
1696
|
+
const hidden = [];
|
|
1697
|
+
const hide = (o) => {
|
|
1698
|
+
if (o && o.visible) {
|
|
1699
|
+
o.visible = false;
|
|
1700
|
+
hidden.push(o);
|
|
1701
|
+
}
|
|
1702
|
+
};
|
|
1703
|
+
for (let i = 0; i < this.waves.length; i++) {
|
|
1704
|
+
if (!glassIdx.includes(i)) hide(this.waves[i].mesh);
|
|
1705
|
+
hide(this.waves[i].particleField?.object);
|
|
1706
|
+
}
|
|
1707
|
+
this.scene.background = null;
|
|
1708
|
+
if (!this.layerTarget) this.layerTarget = this.pinSampling(new THREE.WebGLRenderTarget(size.x, size.y, {
|
|
1709
|
+
depthBuffer: false,
|
|
1710
|
+
stencilBuffer: false
|
|
1711
|
+
}));
|
|
1712
|
+
else if (this.layerTarget.width !== size.x || this.layerTarget.height !== size.y) this.layerTarget.setSize(size.x, size.y);
|
|
1713
|
+
for (const w of this.waves) w.material.uniforms.uLayers.value = this.emptyTexture();
|
|
1714
|
+
const swapped = [];
|
|
1715
|
+
for (let i = 0; i < this.waves.length; i++) {
|
|
1716
|
+
const w = this.waves[i];
|
|
1717
|
+
if (!glassIdx.includes(i)) {
|
|
1718
|
+
this.dropLayerMaterial(w);
|
|
1719
|
+
continue;
|
|
1720
|
+
}
|
|
1721
|
+
const sc = this.config.waves[i] ?? this.config.waves[this.config.waves.length - 1];
|
|
1722
|
+
const key = this.layerVariantKey(w.material);
|
|
1723
|
+
if (w.layerMaterial && w.layerKey !== key) this.dropLayerMaterial(w);
|
|
1724
|
+
if (!w.layerMaterial) {
|
|
1725
|
+
w.layerMaterial = this.createLayerMaterial(sc, w.material);
|
|
1726
|
+
w.layerKey = key;
|
|
1727
|
+
}
|
|
1728
|
+
w.mesh.material = w.layerMaterial;
|
|
1729
|
+
swapped.push(w);
|
|
1730
|
+
}
|
|
1731
|
+
this.renderer.setRenderTarget(this.layerTarget);
|
|
1732
|
+
this.renderer.setClearColor(0, 0);
|
|
1733
|
+
this.renderer.clear(true, false, false);
|
|
1734
|
+
this.renderer.render(this.scene, this.camera);
|
|
1735
|
+
for (const w of swapped) w.mesh.material = w.material;
|
|
1736
|
+
for (const w of this.waves) w.material.uniforms.uLayers.value = this.layerTarget.texture;
|
|
1737
|
+
for (const o of hidden) o.visible = true;
|
|
1738
|
+
restore();
|
|
1739
|
+
}
|
|
1411
1740
|
/** Resize the post chain's render targets. */
|
|
1412
1741
|
resizePost(w, h, dpr) {
|
|
1413
1742
|
this.composer.setPixelRatio(dpr);
|
|
@@ -1416,6 +1745,12 @@ var WaveRenderer = class {
|
|
|
1416
1745
|
/** Release the post chain's GPU resources. */
|
|
1417
1746
|
disposePost() {
|
|
1418
1747
|
this.composer.dispose();
|
|
1748
|
+
this.backdropTarget?.dispose();
|
|
1749
|
+
this.backdropTarget = void 0;
|
|
1750
|
+
this.layerTarget?.dispose();
|
|
1751
|
+
this.layerTarget = void 0;
|
|
1752
|
+
this.empty?.dispose();
|
|
1753
|
+
this.empty = void 0;
|
|
1419
1754
|
}
|
|
1420
1755
|
/** Render exactly one frame at the current time. */
|
|
1421
1756
|
renderOnce() {
|
|
@@ -1501,8 +1836,10 @@ var WaveRenderer = class {
|
|
|
1501
1836
|
"HELIX",
|
|
1502
1837
|
"TWIST_MOTION",
|
|
1503
1838
|
"RADIAL",
|
|
1839
|
+
"PATH",
|
|
1504
1840
|
"POINTER_FX",
|
|
1505
|
-
"POINTER_RIPPLES"
|
|
1841
|
+
"POINTER_RIPPLES",
|
|
1842
|
+
"DISSOLVE"
|
|
1506
1843
|
]) if (k in all) out[k] = "";
|
|
1507
1844
|
return out;
|
|
1508
1845
|
}
|
|
@@ -1892,9 +2229,11 @@ var WaveRenderer = class {
|
|
|
1892
2229
|
this.backgroundTexture?.dispose();
|
|
1893
2230
|
for (const s of this.waves) {
|
|
1894
2231
|
s.material.dispose();
|
|
2232
|
+
s.layerMaterial?.dispose();
|
|
1895
2233
|
s.geometry.dispose();
|
|
1896
2234
|
s.palette.dispose();
|
|
1897
2235
|
s.particleField?.dispose();
|
|
2236
|
+
s.pathTexture?.dispose();
|
|
1898
2237
|
}
|
|
1899
2238
|
this.bloomPass?.dispose();
|
|
1900
2239
|
this.ditherPass?.dispose();
|