@3driseai/3drise-viewer 0.1.17 → 0.1.18

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.
Files changed (36) hide show
  1. package/dist/components/Galaxy.d.ts.map +1 -1
  2. package/dist/components/Galaxy.js +2 -0
  3. package/dist/components/ObjectNode.d.ts.map +1 -1
  4. package/dist/components/ObjectNode.js +9 -2
  5. package/dist/components/ParticlesGenerator.d.ts +10 -0
  6. package/dist/components/ParticlesGenerator.d.ts.map +1 -0
  7. package/dist/components/ParticlesGenerator.js +23 -0
  8. package/dist/components/effects/ShockwaveEffect.js +2 -2
  9. package/dist/components/index.d.ts +3 -0
  10. package/dist/components/index.d.ts.map +1 -1
  11. package/dist/components/index.js +2 -0
  12. package/dist/components/particles/BioluminescentWakeParticles.d.ts +12 -0
  13. package/dist/components/particles/BioluminescentWakeParticles.d.ts.map +1 -0
  14. package/dist/components/particles/BioluminescentWakeParticles.js +1123 -0
  15. package/dist/components/particles/BrownianDustParticles.d.ts +12 -0
  16. package/dist/components/particles/BrownianDustParticles.d.ts.map +1 -0
  17. package/dist/components/particles/BrownianDustParticles.js +757 -0
  18. package/dist/components/particles/ElectronOrbitalsParticles.d.ts +12 -0
  19. package/dist/components/particles/ElectronOrbitalsParticles.d.ts.map +1 -0
  20. package/dist/components/particles/ElectronOrbitalsParticles.js +944 -0
  21. package/dist/components/particles/GrinderSparksParticles.d.ts +12 -0
  22. package/dist/components/particles/GrinderSparksParticles.d.ts.map +1 -0
  23. package/dist/components/particles/GrinderSparksParticles.js +744 -0
  24. package/dist/components/particles/TidalStreamParticles.d.ts +12 -0
  25. package/dist/components/particles/TidalStreamParticles.d.ts.map +1 -0
  26. package/dist/components/particles/TidalStreamParticles.js +788 -0
  27. package/dist/components/particles/index.d.ts +11 -0
  28. package/dist/components/particles/index.d.ts.map +1 -0
  29. package/dist/components/particles/index.js +6 -0
  30. package/dist/utils/particleTrails.d.ts +70 -0
  31. package/dist/utils/particleTrails.d.ts.map +1 -0
  32. package/dist/utils/particleTrails.js +369 -0
  33. package/dist/utils/pointerField.d.ts +28 -0
  34. package/dist/utils/pointerField.d.ts.map +1 -0
  35. package/dist/utils/pointerField.js +103 -0
  36. package/package.json +2 -2
@@ -0,0 +1,1123 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { useEffect, useLayoutEffect, useMemo, useRef } from 'react';
3
+ import * as THREE from 'three';
4
+ import { useFrame } from '@react-three/fiber';
5
+ import { applyGenerativeAnimations } from '../../utils/handleGenerativeEffectsAnimations.js';
6
+ import { createPointerUniforms, stepPointerField, usePointerField } from '../../utils/pointerField.js';
7
+ import { createTrailUniforms, applyTrailConfig, createCpuTrail } from '../../utils/particleTrails.js';
8
+ import { useObjectReady } from '../../hooks/useObjectReady.js';
9
+ /**
10
+ * Sea Sparkle — Bioluminescent plankton that flash when the water shears: swell orbits, excitable cells that tire and recover, moon glitter; the cursor is a hand, falling pebbles or a hunting fish. Use for night sea and fantasy scenes.
11
+ */
12
+ // Sea Sparkle — bioluminescent plankton in a moonlit sea.
13
+ // Movement: plankton are passive tracers. Linear deep-water swell (two Gerstner waves, deep-water
14
+ // dispersion w = sqrt(g k)) moves each cell on its closed orbital ellipse, amplitude
15
+ // A e^{k y}; the cloud drifts with a slow current. Noctiluca are buoyant, so cells sit in an
16
+ // exponential layer under the surface.
17
+ // Colour: mechanically stimulated luminescence. Every cell is an excitable unit: when the local
18
+ // shear rate of the water exceeds its threshold (lognormal across cells) it flashes
19
+ // (luciferin blue, ~0.2 s decay), spends part of its reserve and needs `recovery` seconds
20
+ // to recharge — so wakes glow bright once and fade as the cells tire. Light leaving deeper
21
+ // cells is absorbed (exp(-c depth)), shifted toward `deepColor` and widened by forward
22
+ // scattering. The surface is a moon-glitter layer: Cox–Munk style micro-facets on the
23
+ // same swell, glinting where a facet's normal bisects moon and eye.
24
+ // Mouse (pointerMode; the cursor lives on the water plane):
25
+ // hand — a swimmer's hand: moving-sphere potential flow (dipole) + wake; shear ~ 3 U R^3 / r^4
26
+ // lights a sheath around the stroke and a glowing wake behind it. A still hand is dark.
27
+ // pebble — pebbles dropped along the cursor path: a splash burst, a sinking pebble trailing a wake,
28
+ // and capillary–gravity ripple packets whose crests light expanding rings.
29
+ // fish — a fish that hunts the cursor (cruises in a circle when the cursor rests): the shear in
30
+ // its boundary layer outlines the body in light and its tail sheds a reverse Karman street
31
+ // of vortices that stir a glowing, swirling wake.
32
+ // Trails (trailMode): streak = long exposure, comet = glow threads, echo = afterglow — only
33
+ // glowing cells leave light (TRAIL_K = emission).
34
+ //
35
+ // Generated from the approved lab module (particle lab, batch 2). Sim units = object-local / size.
36
+ /** mulberry32 — seeded PRNG, returns a function giving floats in [0,1). */
37
+ function mulberry32(seed) {
38
+ let a = seed >>> 0;
39
+ return function () {
40
+ a = (a + 0x6d2b79f5) >>> 0;
41
+ let t = a;
42
+ t = Math.imul(t ^ (t >>> 15), t | 1);
43
+ t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
44
+ return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
45
+ };
46
+ }
47
+ /** Standard normal from a uniform PRNG (Box–Muller, one value per call). */
48
+ function gauss(rng) {
49
+ const u = Math.max(rng(), 1e-9);
50
+ const v = rng();
51
+ return Math.sqrt(-2 * Math.log(u)) * Math.cos(6.283185307 * v);
52
+ }
53
+ /** Viewport-height hook so point sizes are pixel-correct at any canvas size. */
54
+ function attachViewH(obj, u) {
55
+ const scratch = new THREE.Vector2();
56
+ obj.onBeforeRender = (renderer) => {
57
+ renderer.getDrawingBufferSize(scratch);
58
+ u.uViewH.value = scratch.y;
59
+ };
60
+ }
61
+ function colorUniform(hex) {
62
+ return { value: new THREE.Color(hex) };
63
+ }
64
+ /** Quality levels scale counts, raymarch samples and trail caps. Structural. */
65
+ const QUALITY_SCALE = { low: 0.5, medium: 1, high: 1.6 };
66
+ function qualityScale(q) { return QUALITY_SCALE[q] || 1; }
67
+ /** Mix the object's own base seed with the `seed` knob into a mulberry32 seed. */
68
+ function seedMix(base, seed) {
69
+ let h = (base ^ Math.imul((Math.round(seed) | 0) + 0x9e37, 0x85ebca6b)) >>> 0;
70
+ h = Math.imul(h ^ (h >>> 13), 0xc2b2ae35) >>> 0;
71
+ return (h ^ (h >>> 16)) >>> 0;
72
+ }
73
+ /** Fixed-step clock: the same motion at 30, 60 or 144 Hz. `run(d, step)` advances d sim-seconds
74
+ * in steps of exactly H (at most maxSteps per call; a longer backlog is dropped, not caught up). */
75
+ function createFixedStep(H, maxSteps) {
76
+ let acc = 0;
77
+ return {
78
+ H,
79
+ run(d, step) {
80
+ acc += Math.max(d, 0);
81
+ let k = 0;
82
+ while (acc >= H && k < maxSteps) {
83
+ step(H);
84
+ acc -= H;
85
+ k++;
86
+ }
87
+ if (k === maxSteps)
88
+ acc = Math.min(acc, H);
89
+ return acc / H; // leftover fraction, for render interpolation
90
+ },
91
+ reset() { acc = 0; },
92
+ };
93
+ }
94
+ /** Selection bounds: an invisible sphere the editor can pick, frame and size (never rendered,
95
+ * still raycastable and counted by Box3.setFromObject). */
96
+ function createPickProxy(center, radius) {
97
+ const geo = new THREE.SphereGeometry(1, 16, 10);
98
+ const mat = new THREE.MeshBasicMaterial({ visible: false });
99
+ const mesh = new THREE.Mesh(geo, mat);
100
+ mesh.name = 'pickProxy';
101
+ mesh.position.set(center[0], center[1], center[2]);
102
+ mesh.scale.setScalar(radius);
103
+ return { mesh, dispose() { geo.dispose(); mat.dispose(); } };
104
+ }
105
+ const _frustum = new THREE.Frustum();
106
+ const _pm = new THREE.Matrix4();
107
+ const _sphere = new THREE.Sphere();
108
+ /** Off-screen test: false when the object's bounding sphere is outside the camera frustum. */
109
+ function inView(object, camera, center, radius, size) {
110
+ if (!camera || !camera.projectionMatrix)
111
+ return true;
112
+ object.updateWorldMatrix(true, false);
113
+ _sphere.center.set(center[0], center[1], center[2]).multiplyScalar(size).applyMatrix4(object.matrixWorld);
114
+ _sphere.radius = radius * size * object.matrixWorld.getMaxScaleOnAxis();
115
+ _pm.multiplyMatrices(camera.projectionMatrix, camera.matrixWorldInverse);
116
+ _frustum.setFromProjectionMatrix(_pm);
117
+ return _frustum.intersectsSphere(_sphere);
118
+ }
119
+ const POINTER_MODES = ['hand', 'pebble', 'fish'];
120
+ const HX = 1.0; // half width of the sea patch (sim units)
121
+ const D = 0.3; // depth of the plankton layer
122
+ const LAYER = 0.06; // e-folding depth of the buoyant cell layer
123
+ const G = 0.981; // sim gravity: 1 sim unit ~ 10 m of sea, so the swell is slow and calm
124
+ const H_STEP = 1 / 30;
125
+ const TH0 = 1.5; // median shear threshold (1/s) at sensitivity 1
126
+ const MAX_PEBBLES = 6;
127
+ const MAX_VORT = 16;
128
+ const RIP_C = 0.3; // ripple packet speed
129
+ const RIP_L = 0.05; // ripple crest spacing
130
+ const ANCHOR = [0, -0.035, 0];
131
+ // seconds of water motion per trail second, per trail type (cells drift a few cm/s; a flash lasts 0.2 s)
132
+ const TRAIL_SCALE = { streak: 3.5, comet: 2.5, echo: 1 };
133
+ const TRAIL_GAIN = { streak: 0.55, comet: 1, echo: 3.5 }; // even out the three looks
134
+ const BOUNDS = { center: [0, -0.1, 0], radius: 1.5 };
135
+ function createUniforms() {
136
+ const pebbles = [];
137
+ for (let i = 0; i < MAX_PEBBLES; i++)
138
+ pebbles.push(new THREE.Vector4(0, 0, 99, 0));
139
+ return {
140
+ uTime: { value: 0 },
141
+ uSize: { value: 1 },
142
+ uViewH: { value: 800 },
143
+ uIntensity: { value: 1 },
144
+ uSpeed: { value: 1 },
145
+ uOpacity: { value: 0.9 },
146
+ uColor: colorUniform('#ffffff'),
147
+ uParticleCount: { value: 0 },
148
+ uSwellHeight: { value: 1 },
149
+ uCurrent: { value: 0.3 },
150
+ uSensitivity: { value: 1 },
151
+ uRecovery: { value: 6 },
152
+ uGlowTime: { value: 1 },
153
+ uWaterClarity: { value: 1 },
154
+ uDeepColor: colorUniform('#ffffff'),
155
+ uMoonlight: { value: 0.6 },
156
+ uPointerMode: { value: 0 },
157
+ uPointerStrength: { value: 1 },
158
+ uPointerRadius: { value: 1 },
159
+ uPointerColor: colorUniform('#ffffff'),
160
+ uWave0: { value: new THREE.Vector4() }, // dir.x, dir.z, k, A
161
+ uWave1: { value: new THREE.Vector4() },
162
+ uWavePh: { value: new THREE.Vector2() }, // current phase of each wave (w t - phi)
163
+ uCurOff: { value: new THREE.Vector2() }, // current offset (x, z)
164
+ uPebbles: { value: pebbles }, // x, z, age, strength
165
+ uCamLocal: { value: new THREE.Vector3(0, 1, 2) },
166
+ uTrailGain: { value: 1 },
167
+ ...createPointerUniforms(),
168
+ ...createTrailUniforms(),
169
+ };
170
+ }
171
+ function applyConfig(u, c, size) {
172
+ u.uSize.value = size;
173
+ u.uIntensity.value = c.intensity;
174
+ u.uSpeed.value = c.speed;
175
+ u.uOpacity.value = c.opacity;
176
+ u.uColor.value.set(c.color);
177
+ u.uParticleCount.value = c.particleCount;
178
+ u.uSwellHeight.value = c.swellHeight;
179
+ u.uCurrent.value = c.current;
180
+ u.uSensitivity.value = c.sensitivity;
181
+ u.uRecovery.value = c.recovery;
182
+ u.uGlowTime.value = c.glowTime;
183
+ u.uWaterClarity.value = c.waterClarity;
184
+ u.uDeepColor.value.set(c.deepColor);
185
+ u.uMoonlight.value = c.moonlight;
186
+ u.uPointerMode.value = POINTER_MODES.indexOf(c.pointerMode); // -1 = 'none' (off)
187
+ u.uPointerStrength.value = c.pointerStrength;
188
+ u.uPointerRadius.value = c.pointerRadius;
189
+ u.uPointerColor.value.set(c.pointerColor);
190
+ applyTrailConfig(u, c);
191
+ }
192
+ const BASE_U = 'uniform float uSize, uViewH, uIntensity, uOpacity;';
193
+ // Shared water optics: absorption, depth tint, edge fade.
194
+ const WATER_GLSL = /* glsl */ `
195
+ uniform vec3 uColor, uDeepColor;
196
+ uniform float uWaterClarity;
197
+ float edgeFade(vec3 p) {
198
+ vec2 e = ${HX.toFixed(2)} - abs(p.xz);
199
+ return smoothstep(0.0, 0.18, min(e.x, e.y));
200
+ }
201
+ float absorb(float depth) { return exp(-depth * 7.0 / max(uWaterClarity, 0.05)); }
202
+ vec3 depthTint(float depth) { return mix(uColor, uDeepColor, 1.0 - exp(-depth * 6.0 / max(uWaterClarity, 0.05))); }
203
+ `;
204
+ const cellVertex = /* glsl */ `
205
+ ${BASE_U}
206
+ ${WATER_GLSL}
207
+ uniform float uMoonlight;
208
+ attribute float aL; // emitted light (0..~1.4)
209
+ attribute float aSeed;
210
+ varying vec3 vCol;
211
+ varying float vA;
212
+ void main() {
213
+ float depth = max(-position.y, 0.0);
214
+ float L = aL * edgeFade(position);
215
+ float att = absorb(depth);
216
+ // forward scattering in the water widens deeper flashes into soft discs (energy conserved)
217
+ float blur = 1.0 + depth * 10.0;
218
+ vec4 mv = modelViewMatrix * vec4(position * uSize, 1.0);
219
+ float Lc = min(L, 1.4);
220
+ float px = uSize * uViewH * (0.0035 + 0.013 * sqrt(Lc)) * (0.8 + 0.4 * aSeed) * blur / -mv.z;
221
+ vec3 glow = depthTint(depth) * L * att * 4.0;
222
+ // unlit cells are specks catching a little moonlight near the surface
223
+ vec3 speck = vec3(0.45, 0.55, 0.7) * 0.07 * uMoonlight * exp(-depth * 18.0) * edgeFade(position);
224
+ vCol = (glow + speck) * uIntensity;
225
+ vA = uOpacity / (blur * blur);
226
+ gl_PointSize = clamp(px, 1.0, 64.0);
227
+ gl_Position = projectionMatrix * mv;
228
+ }
229
+ `;
230
+ const cellFragment = /* glsl */ `
231
+ varying vec3 vCol;
232
+ varying float vA;
233
+ void main() {
234
+ vec2 d = gl_PointCoord * 2.0 - 1.0;
235
+ float r2 = dot(d, d);
236
+ if (r2 > 1.0) discard;
237
+ float a = (exp(-r2 * 10.0) + 0.22 * exp(-r2 * 2.5)) * (1.0 - r2) * vA;
238
+ vec3 c = vCol * a;
239
+ if (max(c.r, max(c.g, c.b)) < 0.0015) discard;
240
+ gl_FragColor = vec4(vCol, a);
241
+ }
242
+ `;
243
+ // In-water scattering: every lit cell also lights the water around it — a wide, faint disc
244
+ // (radius ~ a few scattering lengths). Where many cells flash together (a fish, a wake) the
245
+ // discs merge into a continuous glow; lone flashes stay points. Cells below a small emission
246
+ // are collapsed to zero size so the pass costs almost nothing when the sea is dark.
247
+ const haloVertex = /* glsl */ `
248
+ ${BASE_U}
249
+ ${WATER_GLSL}
250
+ attribute float aL;
251
+ varying vec3 vCol;
252
+ varying float vA;
253
+ void main() {
254
+ float depth = max(-position.y, 0.0);
255
+ float L = aL * edgeFade(position);
256
+ vec4 mv = modelViewMatrix * vec4(position * uSize, 1.0);
257
+ float on = step(0.12, L);
258
+ gl_PointSize = on * clamp(uSize * uViewH * (0.065 + 0.2 * depth) / -mv.z, 0.0, 180.0);
259
+ vCol = depthTint(depth + 0.05) * uIntensity;
260
+ vA = on * min(L, 1.4) * absorb(depth) * 0.12 * uOpacity;
261
+ gl_Position = projectionMatrix * mv;
262
+ }
263
+ `;
264
+ const haloFragment = /* glsl */ `
265
+ varying vec3 vCol;
266
+ varying float vA;
267
+ void main() {
268
+ vec2 d = gl_PointCoord * 2.0 - 1.0;
269
+ float r2 = dot(d, d);
270
+ if (r2 > 1.0) discard;
271
+ float a = exp(-r2 * 3.0) * (1.0 - r2) * vA;
272
+ gl_FragColor = vec4(vCol, a);
273
+ }
274
+ `;
275
+ // Moon glitter: facets on the swell (same waves as the plankton) plus pebble ripples. Each facet
276
+ // rocks slowly around the mean normal (Cox–Munk slope spread), so the glitter path twinkles calmly.
277
+ const surfVertex = /* glsl */ `
278
+ ${BASE_U}
279
+ uniform float uTime, uMoonlight, uSwellHeight;
280
+ uniform vec4 uWave0, uWave1;
281
+ uniform vec2 uWavePh, uCurOff;
282
+ uniform vec4 uPebbles[${MAX_PEBBLES}];
283
+ uniform vec3 uCamLocal;
284
+ attribute vec2 aRnd;
285
+ varying vec3 vCol;
286
+ varying float vA;
287
+ void wave(vec4 w, float ph, vec2 xw, inout vec3 P, inout vec2 slope) {
288
+ float th = w.z * dot(w.xy, xw) - ph;
289
+ float A = w.w * uSwellHeight;
290
+ P.xz -= w.xy * A * sin(th);
291
+ P.y += A * cos(th);
292
+ slope -= w.xy * (w.z * A * sin(th));
293
+ }
294
+ void main() {
295
+ vec3 P = position;
296
+ vec2 xw = position.xz - uCurOff;
297
+ vec2 slope = vec2(0.0);
298
+ wave(uWave0, uWavePh.x, xw, P, slope);
299
+ wave(uWave1, uWavePh.y, xw, P, slope);
300
+ for (int i = 0; i < ${MAX_PEBBLES}; i++) {
301
+ vec4 pb = uPebbles[i];
302
+ if (pb.w <= 0.0 || pb.z > 4.5) continue;
303
+ vec2 dd = position.xz - pb.xy;
304
+ float rho = length(dd) + 1e-4;
305
+ float rf = ${RIP_C.toFixed(3)} * pb.z;
306
+ float w = 0.03 + 0.016 * pb.z;
307
+ float x = rho - rf;
308
+ float env = pb.w * 0.004 * exp(-pb.z / 1.6) * exp(-(x * x) / (w * w)) / sqrt(1.0 + rf / 0.08);
309
+ float kk = 6.2831853 / ${RIP_L.toFixed(3)};
310
+ P.y += env * cos(kk * x);
311
+ slope -= (dd / rho) * env * kk * sin(kk * x);
312
+ }
313
+ vec2 rock = vec2(sin(aRnd.x * 6.2831 + uTime * (0.35 + 0.5 * aRnd.y)), cos(aRnd.y * 6.2831 + uTime * (0.3 + 0.45 * aRnd.x)));
314
+ vec3 n = normalize(vec3(-slope.x + 0.09 * rock.x, 1.0, -slope.y + 0.09 * rock.y));
315
+ vec3 V = normalize(uCamLocal - P);
316
+ vec3 Lm = normalize(vec3(0.0, 0.42, -0.9)); // low moon behind the patch
317
+ vec3 Hh = normalize(Lm + V);
318
+ float ndv = max(dot(n, V), 0.0);
319
+ float F = 0.02 + 0.98 * pow(1.0 - ndv, 5.0);
320
+ float spec = pow(max(dot(n, Hh), 0.0), 1400.0) * 90.0 * F;
321
+ vec2 e = ${HX.toFixed(2)} - abs(position.xz);
322
+ float edge = smoothstep(0.0, 0.3, min(e.x, e.y));
323
+ vec3 sky = vec3(0.10, 0.14, 0.22);
324
+ vCol = (vec3(0.80, 0.86, 1.0) * spec + sky * F * 0.5) * uMoonlight * uIntensity;
325
+ vA = edge * uOpacity;
326
+ vec4 mv = modelViewMatrix * vec4(P * uSize, 1.0);
327
+ gl_PointSize = clamp(uSize * uViewH * 0.0022 * (1.0 + min(spec, 3.0) * 0.8) / -mv.z, 1.0, 16.0);
328
+ gl_Position = projectionMatrix * mv;
329
+ }
330
+ `;
331
+ const surfFragment = /* glsl */ `
332
+ varying vec3 vCol;
333
+ varying float vA;
334
+ void main() {
335
+ vec2 d = gl_PointCoord * 2.0 - 1.0;
336
+ float r2 = dot(d, d);
337
+ if (r2 > 1.0) discard;
338
+ float a = (exp(-r2 * 6.0) + 0.2 * exp(-r2 * 2.0)) * (1.0 - r2) * vA;
339
+ if (max(vCol.r, max(vCol.g, vCol.b)) * a < 0.002) discard;
340
+ gl_FragColor = vec4(vCol, a);
341
+ }
342
+ `;
343
+ // Splash glyphs for the pebble mode: a short white burst where each pebble enters.
344
+ const splashVertex = /* glsl */ `
345
+ uniform float uSize, uViewH, uPointerRadius;
346
+ attribute vec2 aPb; // age, strength
347
+ varying float vA;
348
+ void main() {
349
+ vec4 mv = modelViewMatrix * vec4(position * uSize, 1.0);
350
+ float age = aPb.x;
351
+ vA = aPb.y * exp(-age / 0.12) * step(age, 0.6);
352
+ gl_PointSize = clamp(uSize * uViewH * 0.05 * uPointerRadius * (0.4 + age * 3.0) / -mv.z, 0.0, 200.0) * step(0.01, vA);
353
+ gl_Position = projectionMatrix * mv;
354
+ }
355
+ `;
356
+ const splashFragment = /* glsl */ `
357
+ uniform vec3 uPointerColor;
358
+ uniform float uOpacity, uIntensity;
359
+ varying float vA;
360
+ void main() {
361
+ vec2 d = gl_PointCoord * 2.0 - 1.0;
362
+ float r2 = dot(d, d);
363
+ if (r2 > 1.0) discard;
364
+ float ring = exp(-pow((sqrt(r2) - 0.6) / 0.18, 2.0));
365
+ float a = (0.6 * exp(-r2 * 8.0) + 0.5 * ring) * (1.0 - r2) * vA * uOpacity;
366
+ if (a < 0.003) discard;
367
+ gl_FragColor = vec4(uPointerColor * uIntensity, a);
368
+ }
369
+ `;
370
+ function build(u, c, size) {
371
+ const q = qualityScale(c.quality);
372
+ const n = Math.max(1, Math.round(c.particleCount * q));
373
+ const rng = mulberry32(seedMix(0x5e45, c.seed));
374
+ // ---- swell: two waves, deep-water dispersion ----
375
+ const waves = [
376
+ { ang: 0.35 + (rng() - 0.5) * 0.3, lambda: 0.85, amp: 0.013, phi: rng() * 6.283, dx: 0, dz: 0, k: 0, om: 0 },
377
+ { ang: -0.55 + (rng() - 0.5) * 0.3, lambda: 0.5, amp: 0.006, phi: rng() * 6.283, dx: 0, dz: 0, k: 0, om: 0 },
378
+ ];
379
+ for (const w of waves) {
380
+ w.dx = Math.sin(w.ang);
381
+ w.dz = -Math.cos(w.ang); // travelling mostly toward -z, away from the camera
382
+ w.k = (2 * Math.PI) / w.lambda;
383
+ w.om = Math.sqrt(G * w.k);
384
+ }
385
+ u.uWave0.value.set(waves[0].dx, waves[0].dz, waves[0].k, waves[0].amp);
386
+ u.uWave1.value.set(waves[1].dx, waves[1].dz, waves[1].k, waves[1].amp);
387
+ // ---- cells: water-frame rest positions, cached wave phases, excitable state ----
388
+ const rx = new Float32Array(n), ry = new Float32Array(n), rz = new Float32Array(n);
389
+ const pc = new Float32Array(n * 2), ps = new Float32Array(n * 2), ek = new Float32Array(n * 2);
390
+ const thr = new Float32Array(n), res = new Float32Array(n), cd = new Float32Array(n);
391
+ const L = new Float32Array(n);
392
+ // exposure memory: the brightest light a cell gave during the camera's exposure window (trails use it)
393
+ const EXP = new Float32Array(n);
394
+ const pos = new Float32Array(n * 3), vel = new Float32Array(n * 3);
395
+ let trail = null, trailKey = '', trailScale = 1;
396
+ const seedA = new Float32Array(n);
397
+ const dirty = new Uint8Array(n);
398
+ for (let i = 0; i < n; i++) {
399
+ rx[i] = (rng() * 2 - 1) * HX;
400
+ rz[i] = (rng() * 2 - 1) * HX;
401
+ ry[i] = -Math.min(-Math.log(1 - rng() * 0.995) * LAYER, D);
402
+ thr[i] = TH0 * Math.exp(0.55 * gauss(rng));
403
+ res[i] = 1;
404
+ seedA[i] = rng();
405
+ dirty[i] = 1;
406
+ }
407
+ function refresh(i) {
408
+ for (let w = 0; w < 2; w++) {
409
+ const W = waves[w];
410
+ const th = W.k * (W.dx * rx[i] + W.dz * rz[i]) + W.phi;
411
+ pc[i * 2 + w] = Math.cos(th);
412
+ ps[i * 2 + w] = Math.sin(th);
413
+ ek[i * 2 + w] = Math.exp(W.k * ry[i]);
414
+ }
415
+ dirty[i] = 0;
416
+ }
417
+ // ---- pointer agents ----
418
+ const PB = new Float64Array(MAX_PEBBLES * 4); // x, z, age, strength
419
+ for (let k = 0; k < MAX_PEBBLES; k++)
420
+ PB[k * 4 + 2] = 99;
421
+ let pebHead = 0, dropClock = 0, lastDropX = 1e9, lastDropZ = 1e9;
422
+ const PK = new Float64Array(MAX_PEBBLES * 12);
423
+ const fish = { x: 0.5, z: 0, y: -0.06, psi: Math.PI / 2, v: 0.28, tail: 0, lastSin: 0, wander: rng() * 6.28 };
424
+ const VX = new Float64Array(MAX_VORT * 5); // x, z, gamma, age, alive
425
+ let vHead = 0;
426
+ // uniform grid over the patch (world coordinates) so cursor agents visit only nearby cells
427
+ const GN = 24, GS = (2 * HX) / GN;
428
+ const gStart = new Int32Array(GN * GN + 1), gCell = new Int32Array(n), gOrder = new Int32Array(n);
429
+ const stamp = new Uint32Array(n), touch = new Int32Array(n);
430
+ const SX = new Float32Array(n), UX = new Float32Array(n), UY = new Float32Array(n), UZ = new Float32Array(n);
431
+ let stampId = 0, nTouch = 0, gridAge = 0;
432
+ const PAD = 0.05;
433
+ function buildGrid() {
434
+ gStart.fill(0);
435
+ for (let i = 0; i < n; i++) {
436
+ let gx = Math.floor((rx[i] + curX + HX) / GS), gz = Math.floor((rz[i] + curZ + HX) / GS);
437
+ gx = gx < 0 ? 0 : gx >= GN ? GN - 1 : gx;
438
+ gz = gz < 0 ? 0 : gz >= GN ? GN - 1 : gz;
439
+ const g = gz * GN + gx;
440
+ gCell[i] = g;
441
+ gStart[g + 1]++;
442
+ }
443
+ for (let g = 0; g < GN * GN; g++)
444
+ gStart[g + 1] += gStart[g];
445
+ const fill = gStart.slice(0, GN * GN);
446
+ for (let i = 0; i < n; i++)
447
+ gOrder[fill[gCell[i]]++] = i;
448
+ }
449
+ // mark every cell in the buckets overlapping a square (or an annulus r0..r1 when ring = 1)
450
+ function addBox(cx, cz, r1in, r0in, ring) {
451
+ const r1 = r1in + PAD, r0 = Math.max(r0in - PAD, 0);
452
+ const gx0 = Math.max(0, Math.floor((cx - r1 + HX) / GS)), gx1 = Math.min(GN - 1, Math.floor((cx + r1 + HX) / GS));
453
+ const gz0 = Math.max(0, Math.floor((cz - r1 + HX) / GS)), gz1 = Math.min(GN - 1, Math.floor((cz + r1 + HX) / GS));
454
+ for (let gz = gz0; gz <= gz1; gz++)
455
+ for (let gx = gx0; gx <= gx1; gx++) {
456
+ if (ring) {
457
+ const bx0 = -HX + gx * GS, bz0 = -HX + gz * GS;
458
+ const nx = Math.max(bx0 - cx, 0, cx - bx0 - GS), nz = Math.max(bz0 - cz, 0, cz - bz0 - GS);
459
+ const fx = Math.max(Math.abs(bx0 - cx), Math.abs(bx0 + GS - cx)), fz = Math.max(Math.abs(bz0 - cz), Math.abs(bz0 + GS - cz));
460
+ if (nx * nx + nz * nz > r1 * r1 || fx * fx + fz * fz < r0 * r0)
461
+ continue;
462
+ }
463
+ const g = gz * GN + gx;
464
+ for (let q = gStart[g]; q < gStart[g + 1]; q++) {
465
+ const i = gOrder[q];
466
+ if (stamp[i] !== stampId) {
467
+ stamp[i] = stampId;
468
+ touch[nTouch++] = i;
469
+ }
470
+ }
471
+ }
472
+ }
473
+ let time = 0;
474
+ let curX = 0, curZ = 0;
475
+ const hp = new THREE.Vector3(), hv = new THREE.Vector3();
476
+ function step(h) {
477
+ time += h;
478
+ const sw = Math.max(u.uSwellHeight.value, 0);
479
+ const sens = Math.max(u.uSensitivity.value, 0.05);
480
+ const tauL = 0.22 * Math.max(u.uGlowTime.value, 0.05);
481
+ const decL = Math.exp(-h / tauL);
482
+ const rec = h / Math.max(u.uRecovery.value, 0.1);
483
+ // current: slow drift toward +x, slightly across
484
+ const cur = 0.035 * u.uCurrent.value;
485
+ curX += cur * 0.92 * h;
486
+ curZ += cur * 0.38 * h;
487
+ u.uCurOff.value.set(curX, curZ);
488
+ // wave clocks
489
+ const W0 = waves[0], W1 = waves[1];
490
+ const C0 = Math.cos(W0.om * time), S0 = Math.sin(W0.om * time);
491
+ const C1 = Math.cos(W1.om * time), S1 = Math.sin(W1.om * time);
492
+ const Aw0 = W0.amp * sw, Aw1 = W1.amp * sw, om0 = W0.om, om1 = W1.om;
493
+ const sk0 = W0.om * W0.k, sk1 = W1.om * W1.k; // shear per unit amplitude
494
+ const dx0 = W0.dx, dz0 = W0.dz, dx1 = W1.dx, dz1 = W1.dz;
495
+ u.uWavePh.value.set(waves[0].om * time - waves[0].phi, waves[1].om * time - waves[1].phi);
496
+ // pointer
497
+ const mode = u.uPointerMode.value;
498
+ const Sp = u.uPointerStrength.value;
499
+ const act = u.uPointerActive.value;
500
+ const Rr = u.uPointerRadius.value;
501
+ hp.copy(u.uPointer.value);
502
+ hv.copy(u.uPointerVel.value);
503
+ hp.x -= curX;
504
+ hp.z -= curZ; // into the water frame
505
+ const useHand = mode === 0 && Sp * act > 0.01;
506
+ const usePeb = mode === 1 && Sp > 0.01;
507
+ const useFish = mode === 2 && Sp > 0.01;
508
+ const RH = 0.055 * Rr;
509
+ const hs = Sp * act;
510
+ const hSpeed = Math.hypot(hv.x, hv.z);
511
+ const hY = Math.min(Math.max(hp.y, -D * 0.6), -0.012);
512
+ // pebbles: one every 0.4 s while the cursor is active, or after 0.25 of travel
513
+ if (usePeb) {
514
+ dropClock += h;
515
+ const moved = Math.hypot(hp.x - lastDropX, hp.z - lastDropZ);
516
+ if (act > 0.6 && (dropClock > 0.55 || moved > 0.25 * Rr) && dropClock > 0.15) {
517
+ const k = pebHead % MAX_PEBBLES;
518
+ pebHead++;
519
+ PB[k * 4] = hp.x;
520
+ PB[k * 4 + 1] = hp.z;
521
+ PB[k * 4 + 2] = 0;
522
+ PB[k * 4 + 3] = Sp * Rr;
523
+ dropClock = 0;
524
+ lastDropX = hp.x;
525
+ lastDropZ = hp.z;
526
+ }
527
+ }
528
+ // per-pebble constants for this step (splash, sinking wake, ripple band) so the cell loop is cheap
529
+ let nPeb = 0;
530
+ for (let k = 0; k < MAX_PEBBLES; k++) {
531
+ PB[k * 4 + 2] += h;
532
+ const age = PB[k * 4 + 2], st = PB[k * 4 + 3];
533
+ const pbv = u.uPebbles.value[k];
534
+ pbv.set(PB[k * 4] + curX, PB[k * 4 + 1] + curZ, age, age < 4.5 ? st : 0);
535
+ if (age >= 3.5)
536
+ continue;
537
+ const o = nPeb * 12;
538
+ const rf = RIP_C * age, w = 0.03 + 0.016 * age;
539
+ const r0 = Math.max(rf - 2.3 * w, 0), r1 = rf + 2.3 * w;
540
+ PK[o] = PB[k * 4];
541
+ PK[o + 1] = PB[k * 4 + 1];
542
+ PK[o + 2] = age;
543
+ PK[o + 3] = st;
544
+ PK[o + 4] = rf;
545
+ PK[o + 5] = w;
546
+ PK[o + 6] = r0 * r0;
547
+ PK[o + 7] = r1 * r1;
548
+ PK[o + 8] = st * 18 * Math.exp(-age / 1.6) / Math.sqrt(1 + rf / 0.08);
549
+ if (PK[o + 8] * sens < 0.3 * TH0) {
550
+ PK[o + 6] = 1e9;
551
+ PK[o + 7] = -1;
552
+ } // too weak to light anything
553
+ PK[o + 9] = age < 0.3 ? 60 * Math.exp(-age / 0.08) : 0; // splash amplitude
554
+ PK[o + 10] = -0.35 * age; // pebble depth
555
+ PK[o + 11] = 0.015 * st + 0.004; // pebble radius
556
+ nPeb++;
557
+ }
558
+ // fish: steer toward the cursor (or cruise a slow circle), beat the tail, shed vortices
559
+ let fcos = 0, fsin = 0, fa = 0, fb = 0, fc = 0;
560
+ if (useFish) {
561
+ const F = fish;
562
+ let tx, tz;
563
+ // targets are chosen in the world (inside the patch), then taken into the water frame
564
+ if (act > 0.3) {
565
+ tx = hp.x + curX;
566
+ tz = hp.z + curZ;
567
+ }
568
+ else {
569
+ F.wander += h * 0.16;
570
+ tx = 0.62 * Math.cos(F.wander);
571
+ tz = 0.5 * Math.sin(F.wander);
572
+ }
573
+ tx = Math.max(-0.85, Math.min(0.85, tx)) - curX;
574
+ tz = Math.max(-0.85, Math.min(0.85, tz)) - curZ;
575
+ const dx = tx - F.x, dz = tz - F.z;
576
+ const dist = Math.hypot(dx, dz);
577
+ const want = Math.atan2(dz, dx);
578
+ let dpsi = want - F.psi;
579
+ while (dpsi > Math.PI)
580
+ dpsi -= 2 * Math.PI;
581
+ while (dpsi < -Math.PI)
582
+ dpsi += 2 * Math.PI;
583
+ const turn = 2.6 * Math.min(Sp, 2);
584
+ F.psi += Math.max(-turn * h, Math.min(turn * h, dpsi));
585
+ const vDes = (act > 0.3 ? Math.min(0.3 + dist * 1.5, 1.1) : 0.34) * Math.min(Sp, 2.5);
586
+ F.v += (vDes - F.v) * Math.min(1, h * 1.8);
587
+ fcos = Math.cos(F.psi);
588
+ fsin = Math.sin(F.psi);
589
+ F.x += fcos * F.v * h;
590
+ F.z += fsin * F.v * h;
591
+ F.y = -0.04 + 0.008 * Math.sin(time * 0.7);
592
+ const freq = 1.3 + 3.2 * F.v / Math.max(Rr, 0.3);
593
+ F.tail += 6.2831853 * freq * h;
594
+ fa = 0.15 * Rr;
595
+ fb = 0.032 * Rr;
596
+ fc = 0.025 * Rr;
597
+ const sn = Math.sin(F.tail);
598
+ if ((sn >= 0) !== (F.lastSin >= 0)) {
599
+ // tail tip at the end of a stroke sheds a vortex, alternating sign: a reverse Karman street
600
+ const k = vHead % MAX_VORT;
601
+ vHead++;
602
+ const lat = 0.9 * fb * Math.cos(F.tail);
603
+ const tx2 = F.x - fcos * fa * 1.15 - fsin * lat, tz2 = F.z - fsin * fa * 1.15 + fcos * lat;
604
+ VX[k * 5] = tx2;
605
+ VX[k * 5 + 1] = tz2;
606
+ VX[k * 5 + 2] = (sn >= 0 ? 1 : -1) * 1.1 * F.v * fa;
607
+ VX[k * 5 + 3] = 0;
608
+ VX[k * 5 + 4] = 1;
609
+ }
610
+ F.lastSin = sn;
611
+ }
612
+ // vortices: decay, grow, and their bounding box
613
+ let vMinX = 1e9, vMaxX = -1e9, vMinZ = 1e9, vMaxZ = -1e9;
614
+ const liveV = [];
615
+ for (let k = 0; k < MAX_VORT; k++) {
616
+ if (!VX[k * 5 + 4])
617
+ continue;
618
+ VX[k * 5 + 3] += h;
619
+ VX[k * 5 + 2] *= Math.exp(-h / 2.4);
620
+ if (VX[k * 5 + 3] > 7 || !useFish && VX[k * 5 + 3] > 3) {
621
+ VX[k * 5 + 4] = 0;
622
+ continue;
623
+ }
624
+ const a = 0.028 + 0.018 * VX[k * 5 + 3];
625
+ liveV.push(k);
626
+ vMinX = Math.min(vMinX, VX[k * 5] - 3 * a);
627
+ vMaxX = Math.max(vMaxX, VX[k * 5] + 3 * a);
628
+ vMinZ = Math.min(vMinZ, VX[k * 5 + 1] - 3 * a);
629
+ vMaxZ = Math.max(vMaxZ, VX[k * 5 + 1] + 3 * a);
630
+ }
631
+ // vortices advect each other (2D Biot–Savart) so the street spreads and pairs
632
+ for (const k of liveV) {
633
+ let ux = 0, uz = 0;
634
+ for (const m of liveV) {
635
+ if (m === k)
636
+ continue;
637
+ const dx = VX[k * 5] - VX[m * 5], dz = VX[k * 5 + 1] - VX[m * 5 + 1];
638
+ const a = 0.028 + 0.018 * VX[m * 5 + 3];
639
+ const f = VX[m * 5 + 2] / (6.2831853 * (dx * dx + dz * dz + a * a));
640
+ ux += -dz * f;
641
+ uz += dx * f;
642
+ }
643
+ VX[k * 5] += ux * h;
644
+ VX[k * 5 + 1] += uz * h;
645
+ }
646
+ const nV = liveV.length;
647
+ const F = fish;
648
+ const hvx = hv.x, hvy = 0, hvz = hv.z;
649
+ const hvn = hSpeed > 1e-4 ? 1 / hSpeed : 0;
650
+ // ---- cursor agents: only cells inside an agent's reach are visited (grid buckets) ----
651
+ const anyAgent = useHand || nPeb > 0 || useFish || nV > 0;
652
+ if (!anyAgent)
653
+ gridAge = 0;
654
+ stampId++;
655
+ if (anyAgent) {
656
+ // cells drift slowly, so the grid is rebuilt every 6 steps and agent boxes are padded
657
+ if (gridAge <= 0) {
658
+ buildGrid();
659
+ gridAge = 6;
660
+ }
661
+ gridAge--;
662
+ nTouch = 0;
663
+ if (useHand)
664
+ addBox(hp.x + curX, hp.z + curZ, 7 * RH, 0, 0);
665
+ for (let q = 0, o = 0; q < nPeb; q++, o += 12) {
666
+ const cx = PK[o] + curX, cz = PK[o + 1] + curZ;
667
+ addBox(cx, cz, 0.12, 0, 0); // splash + sinking pebble
668
+ if (PK[o + 7] > 0)
669
+ addBox(cx, cz, Math.sqrt(PK[o + 7]), Math.sqrt(PK[o + 6]), 1); // ripple band
670
+ }
671
+ if (useFish)
672
+ addBox(F.x + curX, F.z + curZ, 2 * fa, 0, 0);
673
+ for (let q = 0; q < nV; q++) {
674
+ const k = liveV[q];
675
+ addBox(VX[k * 5] + curX, VX[k * 5 + 1] + curZ, 3 * (0.028 + 0.018 * VX[k * 5 + 3]), 0, 0);
676
+ }
677
+ for (let t = 0; t < nTouch; t++) {
678
+ const i = touch[t];
679
+ const x = rx[i], y = ry[i], z = rz[i];
680
+ let s = 0;
681
+ let ux = 0, uy = 0, uz = 0;
682
+ if (useHand) {
683
+ const qx = x - hp.x, qy = y - hY, qz = z - hp.z;
684
+ const r2 = qx * qx + qy * qy + qz * qz;
685
+ if (r2 < 49 * RH * RH) {
686
+ const r = Math.sqrt(r2) + 1e-6;
687
+ if (r > RH) {
688
+ const k3 = (RH * RH * RH) / (2 * r2 * r);
689
+ const vr = (hvx * qx + hvy * qy + hvz * qz) / r;
690
+ ux += hs * k3 * (3 * vr * qx / r - hvx);
691
+ uy += hs * k3 * (3 * vr * qy / r - hvy);
692
+ uz += hs * k3 * (3 * vr * qz / r - hvz);
693
+ s += hs * 3 * hSpeed * RH * RH * RH / (r2 * r2);
694
+ }
695
+ else {
696
+ ux += hs * hvx;
697
+ uz += hs * hvz;
698
+ s += hs * hSpeed / RH;
699
+ }
700
+ const behind = -(qx * hvx + qz * hvz) * hvn;
701
+ if (behind > 0) {
702
+ const perp2 = r2 - behind * behind;
703
+ const wk = hs * Math.exp(-perp2 / (RH * RH) - behind / (4 * RH));
704
+ ux += hvx * wk * 0.6;
705
+ uz += hvz * wk * 0.6;
706
+ s += wk * hSpeed / RH * 0.9;
707
+ }
708
+ }
709
+ }
710
+ if (nPeb && y > -D - 0.01) {
711
+ for (let q = 0, o = 0; q < nPeb; q++, o += 12) {
712
+ const dx = x - PK[o], dz = z - PK[o + 1];
713
+ const rho2 = dx * dx + dz * dz;
714
+ const st = PK[o + 3];
715
+ if (PK[o + 9] > 0 && rho2 < 0.01 * st * st + 0.0016 && y > -0.12) { // splash
716
+ // compact bump kernels (~gaussian in rho, ~e^{30 y} in depth) — no transcendentals
717
+ const b = 1 - rho2 / (0.01 * st * st + 0.0016);
718
+ const dz4 = 1 + y * 7.5;
719
+ const e = dz4 > 0 ? b * b * b * dz4 * dz4 * dz4 * dz4 : 0;
720
+ s += PK[o + 9] * e;
721
+ const rho = Math.sqrt(rho2) + 1e-4;
722
+ const push = e * PK[o + 9] / 120;
723
+ ux += dx / rho * push;
724
+ uz += dz / rho * push;
725
+ }
726
+ // sinking pebble and its short wake
727
+ if (rho2 < 0.01 && PK[o + 10] > -D - 0.05) {
728
+ const dy = y - PK[o + 10];
729
+ if (dy > -0.03 && dy < 0.14) {
730
+ const Rp = PK[o + 11];
731
+ s += (0.35 / Rp) * Math.exp(-rho2 / (Rp * Rp * 2.2) - Math.max(dy, 0) / 0.07);
732
+ }
733
+ }
734
+ // ripple packet: crests light a ring near the surface
735
+ if (y > -0.09 && rho2 > PK[o + 6] && rho2 < PK[o + 7]) {
736
+ const xr = Math.sqrt(rho2) - PK[o + 4];
737
+ const w = PK[o + 5];
738
+ const cr = Math.cos(6.2831853 * xr / RIP_L);
739
+ const b = 1 - (xr * xr) / (5.3 * w * w); // bump with the band's support (2.3 w)
740
+ const dz4 = 1 + y * 7; // ~e^{28 y}
741
+ if (b > 0 && dz4 > 0)
742
+ s += PK[o + 8] * b * b * b * dz4 * dz4 * dz4 * dz4 * cr * cr;
743
+ }
744
+ }
745
+ }
746
+ if (useFish) {
747
+ const dx = x - F.x, dz = z - F.z, dy = y - F.y;
748
+ if (dx * dx + dz * dz < 4 * fa * fa && dy * dy < 0.01) {
749
+ const ua = dx * fcos + dz * fsin, va = -dx * fsin + dz * fcos;
750
+ const tailf = Math.max(0, (fa - ua) / (2 * fa));
751
+ const wig = 0.9 * fb * tailf * tailf * Math.sin(F.tail - 5 * tailf);
752
+ const e2 = (ua / fa) * (ua / fa) + ((va - wig) / fb) * ((va - wig) / fb) + (dy / fc) * (dy / fc);
753
+ if (e2 < 5) {
754
+ const de = Math.sqrt(e2);
755
+ const ds = (de - 1) * fb; // ~distance to the body surface
756
+ // shear in the boundary layer and the displaced water around the body
757
+ const delta = 0.028 * Rr + 0.006;
758
+ if (ds > -delta)
759
+ s += (F.v / delta) * 0.5 * Math.exp(-(ds * ds) / (delta * delta));
760
+ if (de < 1.2) { // no penetration: water is pushed off the body
761
+ const push = F.v * 2.2 * (1.2 - de);
762
+ const lat = (va - wig) >= 0 ? 1 : -1;
763
+ ux += -fsin * lat * push;
764
+ uz += fcos * lat * push;
765
+ uy += (dy >= 0 ? 1 : -1) * push * 0.3;
766
+ }
767
+ }
768
+ }
769
+ }
770
+ if (nV && x > vMinX && x < vMaxX && z > vMinZ && z < vMaxZ) {
771
+ const dfac = Math.exp(-((y - F.y) * (y - F.y)) / 0.0049);
772
+ if (dfac > 0.02) {
773
+ for (let q = 0; q < nV; q++) {
774
+ const k = liveV[q];
775
+ const dx = x - VX[k * 5], dz = z - VX[k * 5 + 1];
776
+ const a = 0.028 + 0.018 * VX[k * 5 + 3];
777
+ const r2 = dx * dx + dz * dz;
778
+ if (r2 > 9 * a * a)
779
+ continue;
780
+ const G2 = VX[k * 5 + 2];
781
+ const f = (G2 / (6.2831853 * (r2 + a * a))) * dfac;
782
+ ux += -dz * f;
783
+ uz += dx * f;
784
+ s += Math.abs(G2) / (3.14159 * a * a) * Math.exp(-r2 / (a * a)) * dfac * 0.7;
785
+ }
786
+ }
787
+ }
788
+ SX[i] = s;
789
+ UX[i] = ux;
790
+ UY[i] = uy;
791
+ UZ[i] = uz;
792
+ }
793
+ }
794
+ const decE = Math.exp(-h / Math.max(0.6 * u.uTrailLength.value, 0.1));
795
+ cellLoop(h, curX, curZ, stampId, cur, sens, decL, rec, decE, C0, S0, C1, S1, Aw0, Aw1, om0, om1, sk0, sk1, dx0, dz0, dx1, dz1);
796
+ }
797
+ // the per-cell hot loop, kept in its own small function so V8 keeps its scalars in registers
798
+ function cellLoop(h, cX, cZ, sid, cur, sens, decL, rec, decE, C0, S0, C1, S1, Aw0, Aw1, om0, om1, sk0, sk1, dx0, dz0, dx1, dz1) {
799
+ const k0 = waves[0].k, k1 = waves[1].k;
800
+ const kx0 = k0 * dx0, kz0 = k0 * dz0, kx1 = k1 * dx1, kz1 = k1 * dz1;
801
+ for (let i = 0; i < n; i++) {
802
+ let x = rx[i], y = ry[i], z = rz[i];
803
+ let s = 0; // shear rate felt by the cell (1/s)
804
+ let ux = 0, uy = 0, uz = 0; // extra flow from the cursor agents
805
+ if (stamp[i] === sid) {
806
+ s = SX[i];
807
+ ux = UX[i];
808
+ uy = UY[i];
809
+ uz = UZ[i];
810
+ }
811
+ if (ux !== 0 || uy !== 0 || uz !== 0) {
812
+ const ddx = ux * h, ddz = uz * h;
813
+ let ny = y + uy * h;
814
+ if (ny > -0.003)
815
+ ny = -0.003;
816
+ else if (ny < -D)
817
+ ny = -D;
818
+ const ddy = ny - y;
819
+ x += ddx;
820
+ y = ny;
821
+ z += ddz;
822
+ rx[i] = x;
823
+ ry[i] = y;
824
+ rz[i] = z;
825
+ // small moves: rotate the cached wave phases and rescale e^{ky} instead of recomputing them
826
+ const i2 = i * 2;
827
+ const a0 = kx0 * ddx + kz0 * ddz, a1 = kx1 * ddx + kz1 * ddz;
828
+ if (a0 * a0 + a1 * a1 < 0.01) {
829
+ const q0 = 1 - 0.5 * a0 * a0, q1 = 1 - 0.5 * a1 * a1;
830
+ const p0 = pc[i2], s0 = ps[i2], p1 = pc[i2 + 1], s1 = ps[i2 + 1];
831
+ pc[i2] = p0 * q0 - s0 * a0;
832
+ ps[i2] = s0 * q0 + p0 * a0;
833
+ pc[i2 + 1] = p1 * q1 - s1 * a1;
834
+ ps[i2 + 1] = s1 * q1 + p1 * a1;
835
+ const e0 = k0 * ddy, e1 = k1 * ddy;
836
+ ek[i2] *= 1 + e0 + 0.5 * e0 * e0;
837
+ ek[i2 + 1] *= 1 + e1 + 0.5 * e1 * e1;
838
+ }
839
+ else
840
+ dirty[i] = 1;
841
+ }
842
+ // periodic patch: keep world = water frame + current inside [-HX, HX]
843
+ let wrapped = false;
844
+ if (x + cX > HX) {
845
+ x -= 2 * HX;
846
+ rx[i] = x;
847
+ dirty[i] = 1;
848
+ wrapped = true;
849
+ }
850
+ else if (x + cX < -HX) {
851
+ x += 2 * HX;
852
+ rx[i] = x;
853
+ dirty[i] = 1;
854
+ wrapped = true;
855
+ }
856
+ if (z + cZ > HX) {
857
+ z -= 2 * HX;
858
+ rz[i] = z;
859
+ dirty[i] = 1;
860
+ wrapped = true;
861
+ }
862
+ else if (z + cZ < -HX) {
863
+ z += 2 * HX;
864
+ rz[i] = z;
865
+ dirty[i] = 1;
866
+ wrapped = true;
867
+ }
868
+ if (dirty[i])
869
+ refresh(i);
870
+ // swell: orbital displacement and shear (strongest at crests, where the swell steepens)
871
+ // cos/sin(th - w t) from the cached phase by rotation (two waves, unrolled)
872
+ const i2 = i * 2;
873
+ const ct0 = pc[i2] * C0 + ps[i2] * S0, st0 = ps[i2] * C0 - pc[i2] * S0;
874
+ const ct1 = pc[i2 + 1] * C1 + ps[i2 + 1] * S1, st1 = ps[i2 + 1] * C1 - pc[i2 + 1] * S1;
875
+ const A0 = Aw0 * ek[i2], A1 = Aw1 * ek[i2 + 1];
876
+ const ox = -(dx0 * A0 * st0 + dx1 * A1 * st1), oz = -(dz0 * A0 * st0 + dz1 * A1 * st1);
877
+ const oy = A0 * ct0 + A1 * ct1;
878
+ const vx = dx0 * A0 * om0 * ct0 + dx1 * A1 * om1 * ct1;
879
+ const vz = dz0 * A0 * om0 * ct0 + dz1 * A1 * om1 * ct1;
880
+ const vy = A0 * om0 * st0 + A1 * om1 * st1;
881
+ // crest factor max(cos, 0)^6, branch-free (a data-dependent branch here costs more than the maths)
882
+ const m0 = 0.5 * (ct0 + Math.abs(ct0)), m1 = 0.5 * (ct1 + Math.abs(ct1));
883
+ const c0 = m0 * m0 * m0, c1 = m1 * m1 * m1;
884
+ s += A0 * sk0 * (1 + 3.2 * c0 * c0) + A1 * sk1 * (1 + 3.2 * c1 * c1);
885
+ // world position (current offset, periodic patch)
886
+ const wx = x + cX + ox, wz = z + cZ + oz;
887
+ const wy = Math.min(y + oy, -0.001);
888
+ const j = i * 3;
889
+ vel[j] = vx + ux + cur * 0.92;
890
+ vel[j + 1] = vy + uy;
891
+ vel[j + 2] = vz + uz + cur * 0.38;
892
+ pos[j] = wx;
893
+ pos[j + 1] = wy;
894
+ pos[j + 2] = wz;
895
+ if (wrapped && trail)
896
+ trail.reset(i);
897
+ // excitable cell: threshold, flash, fatigue, recovery
898
+ let l = L[i] * decL;
899
+ let r = res[i];
900
+ const cdi = cd[i] - h;
901
+ cd[i] = cdi;
902
+ if (cdi <= 0 && r > 0.04 && s * sens > thr[i]) {
903
+ let over = (s * sens) / thr[i] - 1;
904
+ if (over > 1.5)
905
+ over = 1.5;
906
+ const fl = r * (0.75 + 0.45 * over);
907
+ if (fl > l)
908
+ l = fl;
909
+ r *= 0.62;
910
+ cd[i] = 0.16 + 0.12 * seedA[i];
911
+ }
912
+ res[i] = r + (1 - r) * rec;
913
+ // near the patch edge, light fades (cells wrap there)
914
+ const ex = HX - (wx < 0 ? -wx : wx), ez = HX - (wz < 0 ? -wz : wz);
915
+ const em = ex < ez ? ex : ez;
916
+ const li = em < 0.12 ? l * (em > 0 ? em / 0.12 : 0) : l;
917
+ L[i] = li;
918
+ const e = EXP[i] * decE;
919
+ EXP[i] = li > e ? li : e;
920
+ }
921
+ }
922
+ const stepper = createFixedStep(H_STEP, 4);
923
+ for (let i = 0; i < n; i++)
924
+ refresh(i);
925
+ // warm up: settle recovery state and fill the swell history
926
+ for (let s = 0; s < 60; s++)
927
+ step(H_STEP);
928
+ // ---- geometry ----
929
+ const geo = new THREE.BufferGeometry();
930
+ const posAttr = new THREE.BufferAttribute(pos, 3).setUsage(THREE.DynamicDrawUsage);
931
+ const lAttr = new THREE.BufferAttribute(L, 1).setUsage(THREE.DynamicDrawUsage);
932
+ geo.setAttribute('position', posAttr);
933
+ geo.setAttribute('aL', lAttr);
934
+ geo.setAttribute('aSeed', new THREE.BufferAttribute(seedA, 1));
935
+ const mat = new THREE.ShaderMaterial({
936
+ uniforms: u, vertexShader: cellVertex, fragmentShader: cellFragment,
937
+ transparent: true, depthWrite: false, blending: THREE.AdditiveBlending,
938
+ });
939
+ const cells = new THREE.Points(geo, mat);
940
+ cells.frustumCulled = false;
941
+ attachViewH(cells, u);
942
+ const hmat = new THREE.ShaderMaterial({
943
+ uniforms: u, vertexShader: haloVertex, fragmentShader: haloFragment,
944
+ transparent: true, depthWrite: false, blending: THREE.AdditiveBlending,
945
+ });
946
+ const halos = new THREE.Points(geo, hmat);
947
+ halos.frustumCulled = false;
948
+ const NS = Math.round(170 * Math.sqrt(q));
949
+ const sp = new Float32Array(NS * NS * 3), sr = new Float32Array(NS * NS * 2);
950
+ const srng = mulberry32(seedMix(0x6a17, c.seed));
951
+ for (let a = 0, p = 0; a < NS; a++)
952
+ for (let b = 0; b < NS; b++, p++) {
953
+ sp[p * 3] = -HX + (2 * HX * (a + srng())) / NS;
954
+ sp[p * 3 + 2] = -HX + (2 * HX * (b + srng())) / NS;
955
+ sr[p * 2] = srng();
956
+ sr[p * 2 + 1] = srng();
957
+ }
958
+ const sgeo = new THREE.BufferGeometry();
959
+ sgeo.setAttribute('position', new THREE.BufferAttribute(sp, 3));
960
+ sgeo.setAttribute('aRnd', new THREE.BufferAttribute(sr, 2));
961
+ const smat = new THREE.ShaderMaterial({
962
+ uniforms: u, vertexShader: surfVertex, fragmentShader: surfFragment,
963
+ transparent: true, depthWrite: false, blending: THREE.AdditiveBlending,
964
+ });
965
+ const surface = new THREE.Points(sgeo, smat);
966
+ surface.frustumCulled = false;
967
+ const pgeo = new THREE.BufferGeometry();
968
+ const pp = new Float32Array(MAX_PEBBLES * 3), pa = new Float32Array(MAX_PEBBLES * 2);
969
+ const ppAttr = new THREE.BufferAttribute(pp, 3).setUsage(THREE.DynamicDrawUsage);
970
+ const paAttr = new THREE.BufferAttribute(pa, 2).setUsage(THREE.DynamicDrawUsage);
971
+ pgeo.setAttribute('position', ppAttr);
972
+ pgeo.setAttribute('aPb', paAttr);
973
+ const pmat = new THREE.ShaderMaterial({
974
+ uniforms: u, vertexShader: splashVertex, fragmentShader: splashFragment,
975
+ transparent: true, depthWrite: false, blending: THREE.AdditiveBlending,
976
+ });
977
+ const splashes = new THREE.Points(pgeo, pmat);
978
+ splashes.frustumCulled = false;
979
+ const object = new THREE.Group();
980
+ const proxy = createPickProxy(BOUNDS.center, BOUNDS.radius);
981
+ object.add(surface, halos, cells, splashes, proxy.mesh);
982
+ const anchor = new THREE.Vector3(...ANCHOR);
983
+ const up = new THREE.Vector3(0, 1, 0);
984
+ // ---- trails: only glowing cells leave light ----
985
+ const trailGLSL = /* glsl */ `
986
+ ${WATER_GLSL}
987
+ uniform float uTrailGain;
988
+ vec3 trailShade(vec3 p, float f) {
989
+ float depth = max(-p.y, 0.0);
990
+ return depthTint(depth) * TRAIL_K * absorb(depth) * edgeFade(p) * 3.0 * uTrailGain;
991
+ }
992
+ vec3 trailView(vec3 mv, out float mag) { mag = 1.0; return mv; }
993
+ `;
994
+ const materials = [mat, hmat, smat, pmat];
995
+ trailKey = '';
996
+ function buildTrails(mode, share) {
997
+ const key = mode + ':' + share;
998
+ if (key === trailKey)
999
+ return;
1000
+ trailKey = key;
1001
+ if (trail) {
1002
+ object.remove(trail.object);
1003
+ trail.dispose();
1004
+ trail = null;
1005
+ }
1006
+ materials.length = 4;
1007
+ trailScale = TRAIL_SCALE[mode] || 1;
1008
+ u.uTrailGain.value = TRAIL_GAIN[mode] || 1;
1009
+ trail = createCpuTrail(mode, { u, n, pos, vel, k: EXP, glsl: trailGLSL, sizePx: 0.0075, spacing: 0.0015, sub: 6 });
1010
+ if (!trail)
1011
+ return;
1012
+ object.add(trail.object);
1013
+ materials.push(...trail.materials);
1014
+ }
1015
+ buildTrails(c.trailMode, c.trailShare);
1016
+ function upload() {
1017
+ posAttr.needsUpdate = true;
1018
+ lAttr.needsUpdate = true;
1019
+ for (let k = 0; k < MAX_PEBBLES; k++) {
1020
+ const v = u.uPebbles.value[k];
1021
+ pp[k * 3] = v.x;
1022
+ pp[k * 3 + 1] = 0.004;
1023
+ pp[k * 3 + 2] = v.y;
1024
+ pa[k * 2] = v.z;
1025
+ pa[k * 2 + 1] = v.w > 0 ? Math.min(v.w, 2) : 0;
1026
+ }
1027
+ ppAttr.needsUpdate = true;
1028
+ paAttr.needsUpdate = true;
1029
+ }
1030
+ return {
1031
+ object,
1032
+ materials,
1033
+ apply(cfg) { if (cfg)
1034
+ buildTrails(cfg.trailMode, cfg.trailShare); },
1035
+ update(dt, camera, pointer) {
1036
+ const rdt = Math.min(Math.max(dt, 0), 1 / 20);
1037
+ if (camera && !inView(object, camera, BOUNDS.center, BOUNDS.radius, u.uSize.value))
1038
+ return;
1039
+ const d = rdt * u.uSpeed.value;
1040
+ u.uTime.value += d;
1041
+ if (camera) {
1042
+ object.worldToLocal(u.uCamLocal.value.copy(camera.position)).divideScalar(u.uSize.value);
1043
+ if (pointer)
1044
+ stepPointerField(u, pointer, camera, object, u.uSize.value, rdt, anchor, up);
1045
+ }
1046
+ u.uVelScale.value = trailScale; // the sea moves slowly: exposures look back further
1047
+ let simD = 0;
1048
+ stepper.run(d, (hh) => { step(hh); simD += hh; });
1049
+ if (simD > 0) {
1050
+ upload();
1051
+ if (trail)
1052
+ trail.update(simD * trailScale);
1053
+ }
1054
+ },
1055
+ seek(t) { for (let s = 0; s < Math.round(t / H_STEP); s++)
1056
+ step(H_STEP); upload(); },
1057
+ dispose() {
1058
+ geo.dispose();
1059
+ mat.dispose();
1060
+ hmat.dispose();
1061
+ sgeo.dispose();
1062
+ smat.dispose();
1063
+ pgeo.dispose();
1064
+ pmat.dispose();
1065
+ proxy.dispose();
1066
+ if (trail)
1067
+ trail.dispose();
1068
+ },
1069
+ _state: { pos, vel, L, res, EXP },
1070
+ };
1071
+ }
1072
+ /** Defaults for every knob, so an older saved config still renders (identical to the catalog entry). */
1073
+ const DEFAULTS = {
1074
+ intensity: 1,
1075
+ speed: 1,
1076
+ opacity: 0.9,
1077
+ size: 1,
1078
+ color: '#3cc3ff',
1079
+ particleCount: 36000,
1080
+ swellHeight: 1,
1081
+ current: 0.3,
1082
+ sensitivity: 1,
1083
+ recovery: 6,
1084
+ glowTime: 1,
1085
+ waterClarity: 1,
1086
+ deepColor: '#1a8f8a',
1087
+ moonlight: 0.6,
1088
+ seed: 1,
1089
+ quality: 'medium',
1090
+ pointerMode: 'fish',
1091
+ pointerStrength: 1,
1092
+ pointerRadius: 1,
1093
+ pointerColor: '#dff4ff',
1094
+ trailMode: 'streak',
1095
+ trailLength: 1.5,
1096
+ trailOpacity: 0.6,
1097
+ trailColor: '#2f7dff',
1098
+ trailShare: 1,
1099
+ };
1100
+ const BioluminescentWakeParticles = ({ size, config, animations, onObjectReady, onObjectDispose }) => {
1101
+ const rootRef = useRef(null);
1102
+ const animCache = useRef({});
1103
+ useObjectReady(rootRef, config, onObjectReady, onObjectDispose);
1104
+ const s = size > 0 ? size : 1;
1105
+ const cfg = useMemo(() => ({ ...DEFAULTS, ...config }), [config]);
1106
+ // uniforms outlive the objects, so keyframe animations survive a rebuild
1107
+ const uniforms = useMemo(() => createUniforms(), []);
1108
+ // rebuilt only when a structural knob changes (trails rebuild inside apply())
1109
+ const instance = useMemo(() => build(uniforms, cfg, s), [uniforms, cfg.particleCount, cfg.seed, cfg.quality]);
1110
+ useEffect(() => () => instance.dispose(), [instance]);
1111
+ useLayoutEffect(() => {
1112
+ applyConfig(uniforms, cfg, s);
1113
+ instance.apply(cfg);
1114
+ }, [instance, uniforms, cfg, s]);
1115
+ const pointer = usePointerField();
1116
+ useFrame((state, delta) => {
1117
+ instance.update(Math.min(delta, 0.1), state.camera, pointer);
1118
+ if (animations?.length)
1119
+ applyGenerativeAnimations(instance.materials, animations, animCache.current, Date.now(), false);
1120
+ });
1121
+ return (_jsx("group", { ref: rootRef, children: _jsx("primitive", { object: instance.object }) }));
1122
+ };
1123
+ export default BioluminescentWakeParticles;