@triforge/fluid-core 0.1.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/index.js ADDED
@@ -0,0 +1,1742 @@
1
+ import { BufferGeometry, BufferAttribute } from 'three';
2
+
3
+ function getDefaultExportFromCjs (x) {
4
+ return x && x.__esModule && Object.prototype.hasOwnProperty.call(x, 'default') ? x['default'] : x;
5
+ }
6
+
7
+ var twiddle = {};
8
+
9
+ /**
10
+ * Bit twiddling hacks for JavaScript.
11
+ *
12
+ * Author: Mikola Lysenko
13
+ *
14
+ * Ported from Stanford bit twiddling hack library:
15
+ * http://graphics.stanford.edu/~seander/bithacks.html
16
+ */
17
+
18
+ var hasRequiredTwiddle;
19
+
20
+ function requireTwiddle () {
21
+ if (hasRequiredTwiddle) return twiddle;
22
+ hasRequiredTwiddle = 1;
23
+
24
+ //Number of bits in an integer
25
+ var INT_BITS = 32;
26
+
27
+ //Constants
28
+ twiddle.INT_BITS = INT_BITS;
29
+ twiddle.INT_MAX = 0x7fffffff;
30
+ twiddle.INT_MIN = -1<<(INT_BITS-1);
31
+
32
+ //Returns -1, 0, +1 depending on sign of x
33
+ twiddle.sign = function(v) {
34
+ return (v > 0) - (v < 0);
35
+ };
36
+
37
+ //Computes absolute value of integer
38
+ twiddle.abs = function(v) {
39
+ var mask = v >> (INT_BITS-1);
40
+ return (v ^ mask) - mask;
41
+ };
42
+
43
+ //Computes minimum of integers x and y
44
+ twiddle.min = function(x, y) {
45
+ return y ^ ((x ^ y) & -(x < y));
46
+ };
47
+
48
+ //Computes maximum of integers x and y
49
+ twiddle.max = function(x, y) {
50
+ return x ^ ((x ^ y) & -(x < y));
51
+ };
52
+
53
+ //Checks if a number is a power of two
54
+ twiddle.isPow2 = function(v) {
55
+ return !(v & (v-1)) && (!!v);
56
+ };
57
+
58
+ //Computes log base 2 of v
59
+ twiddle.log2 = function(v) {
60
+ var r, shift;
61
+ r = (v > 0xFFFF) << 4; v >>>= r;
62
+ shift = (v > 0xFF ) << 3; v >>>= shift; r |= shift;
63
+ shift = (v > 0xF ) << 2; v >>>= shift; r |= shift;
64
+ shift = (v > 0x3 ) << 1; v >>>= shift; r |= shift;
65
+ return r | (v >> 1);
66
+ };
67
+
68
+ //Computes log base 10 of v
69
+ twiddle.log10 = function(v) {
70
+ return (v >= 1000000000) ? 9 : (v >= 100000000) ? 8 : (v >= 10000000) ? 7 :
71
+ (v >= 1000000) ? 6 : (v >= 100000) ? 5 : (v >= 10000) ? 4 :
72
+ (v >= 1000) ? 3 : (v >= 100) ? 2 : (v >= 10) ? 1 : 0;
73
+ };
74
+
75
+ //Counts number of bits
76
+ twiddle.popCount = function(v) {
77
+ v = v - ((v >>> 1) & 0x55555555);
78
+ v = (v & 0x33333333) + ((v >>> 2) & 0x33333333);
79
+ return ((v + (v >>> 4) & 0xF0F0F0F) * 0x1010101) >>> 24;
80
+ };
81
+
82
+ //Counts number of trailing zeros
83
+ function countTrailingZeros(v) {
84
+ var c = 32;
85
+ v &= -v;
86
+ if (v) c--;
87
+ if (v & 0x0000FFFF) c -= 16;
88
+ if (v & 0x00FF00FF) c -= 8;
89
+ if (v & 0x0F0F0F0F) c -= 4;
90
+ if (v & 0x33333333) c -= 2;
91
+ if (v & 0x55555555) c -= 1;
92
+ return c;
93
+ }
94
+ twiddle.countTrailingZeros = countTrailingZeros;
95
+
96
+ //Rounds to next power of 2
97
+ twiddle.nextPow2 = function(v) {
98
+ v += v === 0;
99
+ --v;
100
+ v |= v >>> 1;
101
+ v |= v >>> 2;
102
+ v |= v >>> 4;
103
+ v |= v >>> 8;
104
+ v |= v >>> 16;
105
+ return v + 1;
106
+ };
107
+
108
+ //Rounds down to previous power of 2
109
+ twiddle.prevPow2 = function(v) {
110
+ v |= v >>> 1;
111
+ v |= v >>> 2;
112
+ v |= v >>> 4;
113
+ v |= v >>> 8;
114
+ v |= v >>> 16;
115
+ return v - (v>>>1);
116
+ };
117
+
118
+ //Computes parity of word
119
+ twiddle.parity = function(v) {
120
+ v ^= v >>> 16;
121
+ v ^= v >>> 8;
122
+ v ^= v >>> 4;
123
+ v &= 0xf;
124
+ return (0x6996 >>> v) & 1;
125
+ };
126
+
127
+ var REVERSE_TABLE = new Array(256);
128
+
129
+ (function(tab) {
130
+ for(var i=0; i<256; ++i) {
131
+ var v = i, r = i, s = 7;
132
+ for (v >>>= 1; v; v >>>= 1) {
133
+ r <<= 1;
134
+ r |= v & 1;
135
+ --s;
136
+ }
137
+ tab[i] = (r << s) & 0xff;
138
+ }
139
+ })(REVERSE_TABLE);
140
+
141
+ //Reverse bits in a 32 bit word
142
+ twiddle.reverse = function(v) {
143
+ return (REVERSE_TABLE[ v & 0xff] << 24) |
144
+ (REVERSE_TABLE[(v >>> 8) & 0xff] << 16) |
145
+ (REVERSE_TABLE[(v >>> 16) & 0xff] << 8) |
146
+ REVERSE_TABLE[(v >>> 24) & 0xff];
147
+ };
148
+
149
+ //Interleave bits of 2 coordinates with 16 bits. Useful for fast quadtree codes
150
+ twiddle.interleave2 = function(x, y) {
151
+ x &= 0xFFFF;
152
+ x = (x | (x << 8)) & 0x00FF00FF;
153
+ x = (x | (x << 4)) & 0x0F0F0F0F;
154
+ x = (x | (x << 2)) & 0x33333333;
155
+ x = (x | (x << 1)) & 0x55555555;
156
+
157
+ y &= 0xFFFF;
158
+ y = (y | (y << 8)) & 0x00FF00FF;
159
+ y = (y | (y << 4)) & 0x0F0F0F0F;
160
+ y = (y | (y << 2)) & 0x33333333;
161
+ y = (y | (y << 1)) & 0x55555555;
162
+
163
+ return x | (y << 1);
164
+ };
165
+
166
+ //Extracts the nth interleaved component
167
+ twiddle.deinterleave2 = function(v, n) {
168
+ v = (v >>> n) & 0x55555555;
169
+ v = (v | (v >>> 1)) & 0x33333333;
170
+ v = (v | (v >>> 2)) & 0x0F0F0F0F;
171
+ v = (v | (v >>> 4)) & 0x00FF00FF;
172
+ v = (v | (v >>> 16)) & 0x000FFFF;
173
+ return (v << 16) >> 16;
174
+ };
175
+
176
+
177
+ //Interleave bits of 3 coordinates, each with 10 bits. Useful for fast octree codes
178
+ twiddle.interleave3 = function(x, y, z) {
179
+ x &= 0x3FF;
180
+ x = (x | (x<<16)) & 4278190335;
181
+ x = (x | (x<<8)) & 251719695;
182
+ x = (x | (x<<4)) & 3272356035;
183
+ x = (x | (x<<2)) & 1227133513;
184
+
185
+ y &= 0x3FF;
186
+ y = (y | (y<<16)) & 4278190335;
187
+ y = (y | (y<<8)) & 251719695;
188
+ y = (y | (y<<4)) & 3272356035;
189
+ y = (y | (y<<2)) & 1227133513;
190
+ x |= (y << 1);
191
+
192
+ z &= 0x3FF;
193
+ z = (z | (z<<16)) & 4278190335;
194
+ z = (z | (z<<8)) & 251719695;
195
+ z = (z | (z<<4)) & 3272356035;
196
+ z = (z | (z<<2)) & 1227133513;
197
+
198
+ return x | (z << 2);
199
+ };
200
+
201
+ //Extracts nth interleaved component of a 3-tuple
202
+ twiddle.deinterleave3 = function(v, n) {
203
+ v = (v >>> n) & 1227133513;
204
+ v = (v | (v>>>2)) & 3272356035;
205
+ v = (v | (v>>>4)) & 251719695;
206
+ v = (v | (v>>>8)) & 4278190335;
207
+ v = (v | (v>>>16)) & 0x3FF;
208
+ return (v<<22)>>22;
209
+ };
210
+
211
+ //Computes next combination in colexicographic order (this is mistakenly called nextPermutation on the bit twiddling hacks page)
212
+ twiddle.nextCombination = function(v) {
213
+ var t = v | (v - 1);
214
+ return (t + 1) | (((~t & -~t) - 1) >>> (countTrailingZeros(v) + 1));
215
+ };
216
+ return twiddle;
217
+ }
218
+
219
+ var almost_equal;
220
+ var hasRequiredAlmost_equal;
221
+
222
+ function requireAlmost_equal () {
223
+ if (hasRequiredAlmost_equal) return almost_equal;
224
+ hasRequiredAlmost_equal = 1;
225
+
226
+ var abs = Math.abs
227
+ , min = Math.min;
228
+
229
+ function almostEqual(a, b, absoluteError, relativeError) {
230
+ var d = abs(a - b);
231
+ if(d <= absoluteError) {
232
+ return true
233
+ }
234
+ if(d <= relativeError * min(abs(a), abs(b))) {
235
+ return true
236
+ }
237
+ return a === b
238
+ }
239
+
240
+ almostEqual.FLT_EPSILON = 1.19209290e-7;
241
+ almostEqual.DBL_EPSILON = 2.2204460492503131e-16;
242
+
243
+ almost_equal = almostEqual;
244
+ return almost_equal;
245
+ }
246
+
247
+ var pcg$1;
248
+ var hasRequiredPcg;
249
+
250
+ function requirePcg () {
251
+ if (hasRequiredPcg) return pcg$1;
252
+ hasRequiredPcg = 1;
253
+
254
+ var bits = requireTwiddle()
255
+ , almostEqual = requireAlmost_equal();
256
+
257
+ var R = new Float64Array(1024);
258
+ var P = new Float64Array(1024);
259
+ var D = new Float64Array(1024);
260
+ var Z = new Float64Array(1024);
261
+
262
+ function reserve(n) {
263
+ if(n < R.length) {
264
+ return
265
+ }
266
+ var nsize = bits.nextPow2(n);
267
+ R = new Float64Array(nsize);
268
+ P = new Float64Array(nsize);
269
+ Z = new Float64Array(nsize);
270
+ D = new Float64Array(nsize);
271
+ }
272
+
273
+ function conjugateGradient(A, b, x, tolerance, max_iter) {
274
+ var abs = Math.abs
275
+ , max = Math.max
276
+ , EPSILON = almostEqual.FLT_EPSILON
277
+ , n = A.rowCount
278
+ , i, k
279
+ , alpha_n, alpha_d, alpha, beta, rnorm, s;
280
+ if(!tolerance) {
281
+ tolerance = 1e-5;
282
+ }
283
+ if(!max_iter) {
284
+ max_iter = Math.min(n, 20);
285
+ }
286
+ if(!x) {
287
+ if(b.buffer) {
288
+ x = new b.constructor(b.buffer.slice(0));
289
+ } else {
290
+ x = b.slice(0);
291
+ }
292
+ }
293
+ reserve(n);
294
+ //Compute preconditioner
295
+ for(i=0; i<n; ++i) {
296
+ s = A.get(i, i);
297
+ if(abs(s) > EPSILON) {
298
+ D[i] = 1.0 / s;
299
+ } else {
300
+ D[i] = 1.0;
301
+ }
302
+ }
303
+ //Initialize
304
+ A.apply(x, R);
305
+ for(i=0; i<n; ++i) {
306
+ R[i] = b[i] - R[i];
307
+ Z[i] = D[i] * R[i];
308
+ P[i] = Z[i];
309
+ }
310
+ //Iterate
311
+ for(k=0; k<max_iter; ++k) {
312
+ alpha_n = 0.0;
313
+ for(i=0; i<n; ++i) {
314
+ alpha_n += R[i] * Z[i];
315
+ }
316
+ A.apply(P, Z);
317
+ alpha_d = 0.0;
318
+ for(i=0; i<n; ++i) {
319
+ alpha_d += P[i] * Z[i];
320
+ }
321
+ alpha = alpha_n / alpha_d;
322
+ beta = 0.0;
323
+ rnorm = 0.0;
324
+ for(i=0; i<n; ++i) {
325
+ x[i] += alpha * P[i];
326
+ R[i] -= alpha * Z[i];
327
+ Z[i] = D[i] * R[i];
328
+ beta += R[i] * Z[i];
329
+ rnorm = max(rnorm, abs(R[i]));
330
+ }
331
+ if(rnorm < tolerance) {
332
+ break
333
+ }
334
+ beta /= alpha_n;
335
+ for(i=0; i<n; ++i) {
336
+ P[i] = Z[i] + beta * P[i];
337
+ }
338
+ }
339
+ return x
340
+ }
341
+
342
+ pcg$1 = conjugateGradient;
343
+ return pcg$1;
344
+ }
345
+
346
+ var pcgExports = requirePcg();
347
+ var pcg = /*@__PURE__*/getDefaultExportFromCjs(pcgExports);
348
+
349
+ /**
350
+ * MACGrid — Marker-And-Cell staggered velocity grid.
351
+ *
352
+ * Velocity components are stored at face centres (staggered):
353
+ * u[i][j][k] — X-velocity at face (i-½, j, k)
354
+ * v[i][j][k] — Y-velocity at face (i, j-½, k)
355
+ * w[i][j][k] — Z-velocity at face (i, j, k-½)
356
+ * Pressure p[i][j][k] is at cell centres.
357
+ *
358
+ * Grid cell (i,j,k) covers world-space region:
359
+ * [ox + i*h, ox + (i+1)*h] × [oy + j*h, ...] × [oz + k*h, ...]
360
+ */
361
+ class MACGrid {
362
+ constructor(nx, ny, nz, h, ox = 0, oy = 0, oz = 0) {
363
+ this.nx = nx;
364
+ this.ny = ny;
365
+ this.nz = nz;
366
+ this.h = h;
367
+ this.ox = ox;
368
+ this.oy = oy;
369
+ this.oz = oz;
370
+ this.u = new Float64Array((nx + 1) * ny * nz);
371
+ this.v = new Float64Array(nx * (ny + 1) * nz);
372
+ this.w = new Float64Array(nx * ny * (nz + 1));
373
+ this.uOld = new Float64Array(this.u.length);
374
+ this.vOld = new Float64Array(this.v.length);
375
+ this.wOld = new Float64Array(this.w.length);
376
+ this.uW = new Float64Array(this.u.length);
377
+ this.vW = new Float64Array(this.v.length);
378
+ this.wW = new Float64Array(this.w.length);
379
+ this.pressure = new Float64Array(nx * ny * nz);
380
+ this.cellType = new Uint8Array(nx * ny * nz);
381
+ }
382
+ // ── Index helpers ─────────────────────────────────────────────────────────
383
+ /** Flat index for cell (i,j,k). */
384
+ idx(i, j, k) {
385
+ return i + this.nx * (j + this.ny * k);
386
+ }
387
+ /** Flat index for u-face (i,j,k) — grid is (nx+1)*ny*nz. */
388
+ uIdx(i, j, k) {
389
+ return i + (this.nx + 1) * (j + this.ny * k);
390
+ }
391
+ /** Flat index for v-face (i,j,k) — grid is nx*(ny+1)*nz. */
392
+ vIdx(i, j, k) {
393
+ return i + this.nx * (j + (this.ny + 1) * k);
394
+ }
395
+ /** Flat index for w-face (i,j,k) — grid is nx*ny*(nz+1). */
396
+ wIdx(i, j, k) {
397
+ return i + this.nx * (j + this.ny * k);
398
+ }
399
+ // ── Interpolation ─────────────────────────────────────────────────────────
400
+ /** Trilinear interpolation of u-velocity at world position (wx,wy,wz). */
401
+ interpU(wx, wy, wz) {
402
+ return this._interp(this.u, wx - this.ox, wy - this.oy - 0.5 * this.h, wz - this.oz - 0.5 * this.h, this.nx + 1, this.ny, this.nz);
403
+ }
404
+ /** Trilinear interpolation of v-velocity at world position. */
405
+ interpV(wx, wy, wz) {
406
+ return this._interp(this.v, wx - this.ox - 0.5 * this.h, wy - this.oy, wz - this.oz - 0.5 * this.h, this.nx, this.ny + 1, this.nz);
407
+ }
408
+ /** Trilinear interpolation of w-velocity at world position. */
409
+ interpW(wx, wy, wz) {
410
+ return this._interp(this.w, wx - this.ox - 0.5 * this.h, wy - this.oy - 0.5 * this.h, wz - this.oz, this.nx, this.ny, this.nz + 1);
411
+ }
412
+ _interp(arr, lx, ly, lz, Nx, Ny, Nz) {
413
+ const h = this.h;
414
+ let fi = lx / h, fj = ly / h, fk = lz / h;
415
+ fi = Math.max(0, Math.min(Nx - 1.0001, fi));
416
+ fj = Math.max(0, Math.min(Ny - 1.0001, fj));
417
+ fk = Math.max(0, Math.min(Nz - 1.0001, fk));
418
+ const i0 = Math.floor(fi), i1 = i0 + 1;
419
+ const j0 = Math.floor(fj), j1 = j0 + 1;
420
+ const k0 = Math.floor(fk), k1 = k0 + 1;
421
+ const tx = fi - i0, ty = fj - j0, tz = fk - k0;
422
+ const idx = (i, j, k) => i + Nx * (j + Ny * k);
423
+ return (1 - tz) * ((1 - ty) * ((1 - tx) * arr[idx(i0, j0, k0)] + tx * arr[idx(i1, j0, k0)]) +
424
+ ty * ((1 - tx) * arr[idx(i0, j1, k0)] + tx * arr[idx(i1, j1, k0)])) +
425
+ tz * ((1 - ty) * ((1 - tx) * arr[idx(i0, j0, k1)] + tx * arr[idx(i1, j0, k1)]) +
426
+ ty * ((1 - tx) * arr[idx(i0, j1, k1)] + tx * arr[idx(i1, j1, k1)]));
427
+ }
428
+ // ── Utilities ─────────────────────────────────────────────────────────────
429
+ clearVelocities() {
430
+ this.u.fill(0);
431
+ this.v.fill(0);
432
+ this.w.fill(0);
433
+ this.uW.fill(0);
434
+ this.vW.fill(0);
435
+ this.wW.fill(0);
436
+ }
437
+ saveVelocities() {
438
+ this.uOld.set(this.u);
439
+ this.vOld.set(this.v);
440
+ this.wOld.set(this.w);
441
+ }
442
+ normalizeWeights() {
443
+ for (let i = 0; i < this.u.length; i++)
444
+ if (this.uW[i] > 0)
445
+ this.u[i] /= this.uW[i];
446
+ for (let i = 0; i < this.v.length; i++)
447
+ if (this.vW[i] > 0)
448
+ this.v[i] /= this.vW[i];
449
+ for (let i = 0; i < this.w.length; i++)
450
+ if (this.wW[i] > 0)
451
+ this.w[i] /= this.wW[i];
452
+ }
453
+ /** Mark border cells as solid, fluid cells from a predicate. */
454
+ markCells(isFluid) {
455
+ const { nx, ny, nz } = this;
456
+ for (let k = 0; k < nz; k++)
457
+ for (let j = 0; j < ny; j++)
458
+ for (let i = 0; i < nx; i++) {
459
+ const solid = i === 0 || i === nx - 1 || j === 0 || j === ny - 1 || k === 0 || k === nz - 1;
460
+ this.cellType[this.idx(i, j, k)] = solid ? 2 : (isFluid(i, j, k) ? 1 : 0);
461
+ }
462
+ }
463
+ /** Enforce solid (no-through) boundary: zero normal velocity at solid faces. */
464
+ enforceBoundary() {
465
+ const { nx, ny, nz } = this;
466
+ // Left/right walls (i=0 and i=nx)
467
+ for (let k = 0; k < nz; k++)
468
+ for (let j = 0; j < ny; j++) {
469
+ this.u[this.uIdx(0, j, k)] = 0;
470
+ this.u[this.uIdx(nx, j, k)] = 0;
471
+ }
472
+ // Bottom/top walls
473
+ for (let k = 0; k < nz; k++)
474
+ for (let i = 0; i < nx; i++) {
475
+ this.v[this.vIdx(i, 0, k)] = 0;
476
+ this.v[this.vIdx(i, ny, k)] = 0;
477
+ }
478
+ // Front/back walls
479
+ for (let j = 0; j < ny; j++)
480
+ for (let i = 0; i < nx; i++) {
481
+ this.w[this.wIdx(i, j, 0)] = 0;
482
+ this.w[this.wIdx(i, j, nz)] = 0;
483
+ }
484
+ }
485
+ /** Apply gravity to grid v velocities over timestep dt. */
486
+ applyGravity(gx, gy, gz, dt) {
487
+ const { nx, ny, nz } = this;
488
+ for (let k = 0; k < nz; k++)
489
+ for (let j = 0; j <= ny; j++)
490
+ for (let i = 0; i < nx; i++)
491
+ this.v[this.vIdx(i, j, k)] += gy * dt;
492
+ for (let k = 0; k < nz; k++)
493
+ for (let j = 0; j < ny; j++)
494
+ for (let i = 0; i <= nx; i++)
495
+ this.u[this.uIdx(i, j, k)] += gx * dt;
496
+ for (let k = 0; k <= nz; k++)
497
+ for (let j = 0; j < ny; j++)
498
+ for (let i = 0; i < nx; i++)
499
+ this.w[this.wIdx(i, j, k)] += gz * dt;
500
+ }
501
+ }
502
+
503
+ /**
504
+ * FLIPSimulator — full FLIP (Fluid-Implicit-Particle) fluid simulation.
505
+ *
506
+ * Implements the Zhu & Bridson 2005 FLIP algorithm:
507
+ * 1. Transfer particle velocities → MAC grid (P2G, trilinear splat)
508
+ * 2. Apply body forces (gravity) on grid
509
+ * 3. Enforce solid boundary conditions
510
+ * 4. Solve pressure Poisson equation (conjugate gradient)
511
+ * 5. Subtract pressure gradient from grid velocity (projection)
512
+ * 6. Transfer grid velocity change back to particles (G2P, FLIP blend)
513
+ * 7. Advect particles through the corrected velocity field
514
+ *
515
+ * All parameters live in `parameters` for GSAP/st-keyframe animation.
516
+ *
517
+ * ```typescript
518
+ * const sim = new FLIPSimulator({ resolution: 20, particleCount: 1500 })
519
+ * sim.fillBox(-0.5, -0.5, -0.5, 0.5, 0.5, 0.5)
520
+ *
521
+ * // Each frame:
522
+ * sim.step(dt)
523
+ * const pts = sim.getPositions() // Float32Array [x,y,z, x,y,z, ...]
524
+ * ```
525
+ */
526
+ class FLIPSimulator {
527
+ constructor(opts = {}) {
528
+ const res = opts.resolution ?? 24;
529
+ const domainSize = opts.domainSize ?? 2;
530
+ const origin = opts.origin ?? [-1, -1, -1];
531
+ const h = domainSize / res;
532
+ this._nx = res;
533
+ this._ny = res;
534
+ this._nz = res;
535
+ this._grid = new MACGrid(res, res, res, h, origin[0], origin[1], origin[2]);
536
+ this.particleCount = opts.particleCount ?? 2000;
537
+ this._particles = [];
538
+ this._b = new Float64Array(res * res * res);
539
+ this._x = new Float64Array(res * res * res);
540
+ const g = opts.gravity ?? [0, -9.8, 0];
541
+ this.parameters = {
542
+ flipRatio: opts.flipRatio ?? 0.95,
543
+ gravityX: g[0],
544
+ gravityY: g[1],
545
+ gravityZ: g[2],
546
+ substeps: opts.substeps ?? 3,
547
+ solverMaxIter: opts.solverMaxIter ?? 80,
548
+ solverTolerance: opts.solverTolerance ?? 1e-4,
549
+ };
550
+ }
551
+ // ── Particle placement ────────────────────────────────────────────────────
552
+ /**
553
+ * Fill a world-space axis-aligned box with fluid particles.
554
+ * Particles are distributed using a jittered grid to avoid clumping.
555
+ * Multiple calls accumulate particles (capped at particleCount).
556
+ */
557
+ fillBox(x0, y0, z0, x1, y1, z1) {
558
+ this._particles.length = 0;
559
+ const h = this._grid.h;
560
+ const pph = 2; // particles per cell per axis (2³ = 8 per cell, standard FLIP)
561
+ const sp = h / pph;
562
+ for (let z = z0; z < z1; z += sp)
563
+ for (let y = y0; y < y1; y += sp)
564
+ for (let x = x0; x < x1; x += sp) {
565
+ if (this._particles.length >= this.particleCount)
566
+ break;
567
+ this._particles.push({
568
+ x: x + sp * Math.random(),
569
+ y: y + sp * Math.random(),
570
+ z: z + sp * Math.random(),
571
+ vx: 0, vy: 0, vz: 0,
572
+ });
573
+ }
574
+ }
575
+ // ── Main step ─────────────────────────────────────────────────────────────
576
+ /** Advance the simulation by `dt` seconds (call once per frame, e.g. dt=1/60). */
577
+ step(dt) {
578
+ const substeps = Math.max(1, Math.round(this.parameters.substeps));
579
+ const subDt = dt / substeps;
580
+ for (let s = 0; s < substeps; s++)
581
+ this._substep(subDt);
582
+ }
583
+ _substep(dt) {
584
+ const grid = this._grid;
585
+ const { nx, ny, nz, h } = grid;
586
+ // 1. Mark cells as fluid / air based on particle positions
587
+ const fluidSet = new Set();
588
+ for (const p of this._particles) {
589
+ const i = Math.floor((p.x - grid.ox) / h);
590
+ const j = Math.floor((p.y - grid.oy) / h);
591
+ const k = Math.floor((p.z - grid.oz) / h);
592
+ if (i >= 1 && i < nx - 1 && j >= 1 && j < ny - 1 && k >= 1 && k < nz - 1)
593
+ fluidSet.add(grid.idx(i, j, k));
594
+ }
595
+ grid.markCells((i, j, k) => fluidSet.has(grid.idx(i, j, k)));
596
+ // 2. P2G — splat particle velocities to grid faces
597
+ grid.clearVelocities();
598
+ this._p2g();
599
+ grid.normalizeWeights();
600
+ grid.saveVelocities();
601
+ // 3. Body forces + boundary
602
+ grid.applyGravity(this.parameters.gravityX, this.parameters.gravityY, this.parameters.gravityZ, dt);
603
+ grid.enforceBoundary();
604
+ // 4. Pressure projection
605
+ this._pressureSolve(dt);
606
+ // 5. G2P — transfer corrected grid velocities back to particles (FLIP blend)
607
+ this._g2p();
608
+ // 6. Advect particles
609
+ this._advect(dt);
610
+ // 7. Clamp particles inside domain
611
+ this._clampParticles();
612
+ }
613
+ // ── P2G (particle → grid) ─────────────────────────────────────────────────
614
+ _p2g() {
615
+ const grid = this._grid;
616
+ const { nx, ny, nz, h, ox, oy, oz } = grid;
617
+ for (const p of this._particles) {
618
+ // U faces (i-½, j, k) — offset by (0, h/2, h/2)
619
+ this._splatToFace(grid.u, grid.uW, p.vx, p.x - ox, p.y - oy - 0.5 * h, p.z - oz - 0.5 * h, nx + 1, ny, nz, h);
620
+ // V faces (i, j-½, k) — offset by (h/2, 0, h/2)
621
+ this._splatToFace(grid.v, grid.vW, p.vy, p.x - ox - 0.5 * h, p.y - oy, p.z - oz - 0.5 * h, nx, ny + 1, nz, h);
622
+ // W faces (i, j, k-½) — offset by (h/2, h/2, 0)
623
+ this._splatToFace(grid.w, grid.wW, p.vz, p.x - ox - 0.5 * h, p.y - oy - 0.5 * h, p.z - oz, nx, ny, nz + 1, h);
624
+ }
625
+ }
626
+ _splatToFace(arr, wArr, val, lx, ly, lz, Nx, Ny, Nz, h) {
627
+ let fi = lx / h, fj = ly / h, fk = lz / h;
628
+ fi = Math.max(0, Math.min(Nx - 1.0001, fi));
629
+ fj = Math.max(0, Math.min(Ny - 1.0001, fj));
630
+ fk = Math.max(0, Math.min(Nz - 1.0001, fk));
631
+ const i0 = Math.floor(fi), i1 = i0 + 1;
632
+ const j0 = Math.floor(fj), j1 = j0 + 1;
633
+ const k0 = Math.floor(fk), k1 = k0 + 1;
634
+ const tx = fi - i0, ty = fj - j0, tz = fk - k0;
635
+ const idx = (i, j, k) => i + Nx * (j + Ny * k);
636
+ const w000 = (1 - tx) * (1 - ty) * (1 - tz);
637
+ arr[idx(i0, j0, k0)] += w000 * val;
638
+ wArr[idx(i0, j0, k0)] += w000;
639
+ const w100 = tx * (1 - ty) * (1 - tz);
640
+ arr[idx(i1, j0, k0)] += w100 * val;
641
+ wArr[idx(i1, j0, k0)] += w100;
642
+ const w010 = (1 - tx) * ty * (1 - tz);
643
+ arr[idx(i0, j1, k0)] += w010 * val;
644
+ wArr[idx(i0, j1, k0)] += w010;
645
+ const w110 = tx * ty * (1 - tz);
646
+ arr[idx(i1, j1, k0)] += w110 * val;
647
+ wArr[idx(i1, j1, k0)] += w110;
648
+ const w001 = (1 - tx) * (1 - ty) * tz;
649
+ arr[idx(i0, j0, k1)] += w001 * val;
650
+ wArr[idx(i0, j0, k1)] += w001;
651
+ const w101 = tx * (1 - ty) * tz;
652
+ arr[idx(i1, j0, k1)] += w101 * val;
653
+ wArr[idx(i1, j0, k1)] += w101;
654
+ const w011 = (1 - tx) * ty * tz;
655
+ arr[idx(i0, j1, k1)] += w011 * val;
656
+ wArr[idx(i0, j1, k1)] += w011;
657
+ const w111 = tx * ty * tz;
658
+ arr[idx(i1, j1, k1)] += w111 * val;
659
+ wArr[idx(i1, j1, k1)] += w111;
660
+ }
661
+ // ── Pressure projection ───────────────────────────────────────────────────
662
+ _pressureSolve(dt) {
663
+ const grid = this._grid;
664
+ const { nx, ny, nz, h } = grid;
665
+ const N = nx * ny * nz;
666
+ const invH2 = 1.0 / (h * h);
667
+ // Build RHS: b[i] = div(u)[i] / dt (standard incompressible projection)
668
+ this._b.fill(0);
669
+ for (let k = 1; k < nz - 1; k++)
670
+ for (let j = 1; j < ny - 1; j++)
671
+ for (let i = 1; i < nx - 1; i++) {
672
+ const ci = grid.idx(i, j, k);
673
+ if (grid.cellType[ci] !== 1)
674
+ continue;
675
+ const div = (grid.u[grid.uIdx(i + 1, j, k)] - grid.u[grid.uIdx(i, j, k)]) / h
676
+ + (grid.v[grid.vIdx(i, j + 1, k)] - grid.v[grid.vIdx(i, j, k)]) / h
677
+ + (grid.w[grid.wIdx(i, j, k + 1)] - grid.w[grid.wIdx(i, j, k)]) / h;
678
+ this._b[ci] = div / dt;
679
+ }
680
+ this._x.fill(0);
681
+ const cellType = grid.cellType;
682
+ const idxFn = (i, j, k) => grid.idx(i, j, k);
683
+ // A = discrete positive-definite Laplacian (1/h² per neighbour pair)
684
+ const A = {
685
+ rowCount: N,
686
+ get: (row, _col) => {
687
+ if (cellType[row] !== 1)
688
+ return 1;
689
+ const kk = Math.floor(row / (nx * ny));
690
+ const rem = row - kk * nx * ny;
691
+ const jj = Math.floor(rem / nx);
692
+ const ii = rem - jj * nx;
693
+ let d = 0;
694
+ if (ii > 0 && cellType[idxFn(ii - 1, jj, kk)] !== 2)
695
+ d += invH2;
696
+ if (ii < nx - 1 && cellType[idxFn(ii + 1, jj, kk)] !== 2)
697
+ d += invH2;
698
+ if (jj > 0 && cellType[idxFn(ii, jj - 1, kk)] !== 2)
699
+ d += invH2;
700
+ if (jj < ny - 1 && cellType[idxFn(ii, jj + 1, kk)] !== 2)
701
+ d += invH2;
702
+ if (kk > 0 && cellType[idxFn(ii, jj, kk - 1)] !== 2)
703
+ d += invH2;
704
+ if (kk < nz - 1 && cellType[idxFn(ii, jj, kk + 1)] !== 2)
705
+ d += invH2;
706
+ return d > 0 ? d : 1;
707
+ },
708
+ apply: (xVec, out) => {
709
+ out.fill(0);
710
+ for (let k2 = 0; k2 < nz; k2++)
711
+ for (let j2 = 0; j2 < ny; j2++)
712
+ for (let i2 = 0; i2 < nx; i2++) {
713
+ const ci = idxFn(i2, j2, k2);
714
+ if (cellType[ci] !== 1) {
715
+ out[ci] = xVec[ci];
716
+ continue;
717
+ }
718
+ let sum = 0, cnt = 0;
719
+ if (i2 > 0 && cellType[idxFn(i2 - 1, j2, k2)] !== 2) {
720
+ sum += xVec[idxFn(i2 - 1, j2, k2)];
721
+ cnt++;
722
+ }
723
+ if (i2 < nx - 1 && cellType[idxFn(i2 + 1, j2, k2)] !== 2) {
724
+ sum += xVec[idxFn(i2 + 1, j2, k2)];
725
+ cnt++;
726
+ }
727
+ if (j2 > 0 && cellType[idxFn(i2, j2 - 1, k2)] !== 2) {
728
+ sum += xVec[idxFn(i2, j2 - 1, k2)];
729
+ cnt++;
730
+ }
731
+ if (j2 < ny - 1 && cellType[idxFn(i2, j2 + 1, k2)] !== 2) {
732
+ sum += xVec[idxFn(i2, j2 + 1, k2)];
733
+ cnt++;
734
+ }
735
+ if (k2 > 0 && cellType[idxFn(i2, j2, k2 - 1)] !== 2) {
736
+ sum += xVec[idxFn(i2, j2, k2 - 1)];
737
+ cnt++;
738
+ }
739
+ if (k2 < nz - 1 && cellType[idxFn(i2, j2, k2 + 1)] !== 2) {
740
+ sum += xVec[idxFn(i2, j2, k2 + 1)];
741
+ cnt++;
742
+ }
743
+ out[ci] = invH2 * (cnt * xVec[ci] - sum);
744
+ }
745
+ },
746
+ };
747
+ try {
748
+ pcg(A, this._b, this._x, this.parameters.solverTolerance, Math.round(this.parameters.solverMaxIter));
749
+ }
750
+ catch (_) { /* degenerate — keep zero pressure */ }
751
+ // Guard against NaN (degenerate geometry — no fluid cells, etc.)
752
+ for (let i = 0; i < this._x.length; i++) {
753
+ if (!isFinite(this._x[i])) {
754
+ this._x.fill(0);
755
+ break;
756
+ }
757
+ }
758
+ // Subtract pressure gradient — loop over faces (each face updated exactly once)
759
+ // U-faces: (i, j, k) for i in [1, nx-1]
760
+ for (let k = 0; k < nz; k++)
761
+ for (let j = 0; j < ny; j++)
762
+ for (let i = 1; i < nx; i++) {
763
+ const cL = grid.idx(i - 1, j, k);
764
+ const cR = grid.idx(i, j, k);
765
+ if ((cellType[cL] === 1 || cellType[cR] === 1) &&
766
+ cellType[cL] !== 2 && cellType[cR] !== 2) {
767
+ grid.u[grid.uIdx(i, j, k)] -= dt * (this._x[cR] - this._x[cL]) / h;
768
+ }
769
+ }
770
+ // V-faces: (i, j, k) for j in [1, ny-1]
771
+ for (let k = 0; k < nz; k++)
772
+ for (let j = 1; j < ny; j++)
773
+ for (let i = 0; i < nx; i++) {
774
+ const cB = grid.idx(i, j - 1, k);
775
+ const cT = grid.idx(i, j, k);
776
+ if ((cellType[cB] === 1 || cellType[cT] === 1) &&
777
+ cellType[cB] !== 2 && cellType[cT] !== 2) {
778
+ grid.v[grid.vIdx(i, j, k)] -= dt * (this._x[cT] - this._x[cB]) / h;
779
+ }
780
+ }
781
+ // W-faces: (i, j, k) for k in [1, nz-1]
782
+ for (let k = 1; k < nz; k++)
783
+ for (let j = 0; j < ny; j++)
784
+ for (let i = 0; i < nx; i++) {
785
+ const cN = grid.idx(i, j, k - 1);
786
+ const cF = grid.idx(i, j, k);
787
+ if ((cellType[cN] === 1 || cellType[cF] === 1) &&
788
+ cellType[cN] !== 2 && cellType[cF] !== 2) {
789
+ grid.w[grid.wIdx(i, j, k)] -= dt * (this._x[cF] - this._x[cN]) / h;
790
+ }
791
+ }
792
+ grid.enforceBoundary();
793
+ }
794
+ // ── G2P (grid → particle) ─────────────────────────────────────────────────
795
+ _g2p() {
796
+ const grid = this._grid;
797
+ const ratio = this.parameters.flipRatio;
798
+ for (const p of this._particles) {
799
+ // New grid velocity at particle position
800
+ const nu = grid.interpU(p.x, p.y, p.z);
801
+ const nv = grid.interpV(p.x, p.y, p.z);
802
+ const nw = grid.interpW(p.x, p.y, p.z);
803
+ // Old grid velocity at particle position (before pressure solve)
804
+ const ou = this._interpOld(grid.uOld, p.x - grid.ox, p.y - grid.oy - 0.5 * grid.h, p.z - grid.oz - 0.5 * grid.h, grid.nx + 1, grid.ny, grid.nz);
805
+ const ov = this._interpOld(grid.vOld, p.x - grid.ox - 0.5 * grid.h, p.y - grid.oy, p.z - grid.oz - 0.5 * grid.h, grid.nx, grid.ny + 1, grid.nz);
806
+ const ow = this._interpOld(grid.wOld, p.x - grid.ox - 0.5 * grid.h, p.y - grid.oy - 0.5 * grid.h, p.z - grid.oz, grid.nx, grid.ny, grid.nz + 1);
807
+ // FLIP: particle velocity += (new - old) grid velocity
808
+ // PIC: particle velocity = new grid velocity
809
+ // Blend
810
+ p.vx = ratio * (p.vx + nu - ou) + (1 - ratio) * nu;
811
+ p.vy = ratio * (p.vy + nv - ov) + (1 - ratio) * nv;
812
+ p.vz = ratio * (p.vz + nw - ow) + (1 - ratio) * nw;
813
+ }
814
+ }
815
+ _interpOld(arr, lx, ly, lz, Nx, Ny, Nz) {
816
+ const h = this._grid.h;
817
+ let fi = lx / h, fj = ly / h, fk = lz / h;
818
+ fi = Math.max(0, Math.min(Nx - 1.0001, fi));
819
+ fj = Math.max(0, Math.min(Ny - 1.0001, fj));
820
+ fk = Math.max(0, Math.min(Nz - 1.0001, fk));
821
+ const i0 = Math.floor(fi), i1 = i0 + 1;
822
+ const j0 = Math.floor(fj), j1 = j0 + 1;
823
+ const k0 = Math.floor(fk), k1 = k0 + 1;
824
+ const tx = fi - i0, ty = fj - j0, tz = fk - k0;
825
+ const idx = (i, j, k) => i + Nx * (j + Ny * k);
826
+ return (1 - tz) * ((1 - ty) * ((1 - tx) * arr[idx(i0, j0, k0)] + tx * arr[idx(i1, j0, k0)]) +
827
+ ty * ((1 - tx) * arr[idx(i0, j1, k0)] + tx * arr[idx(i1, j1, k0)])) +
828
+ tz * ((1 - ty) * ((1 - tx) * arr[idx(i0, j0, k1)] + tx * arr[idx(i1, j0, k1)]) +
829
+ ty * ((1 - tx) * arr[idx(i0, j1, k1)] + tx * arr[idx(i1, j1, k1)]));
830
+ }
831
+ // ── Advection ─────────────────────────────────────────────────────────────
832
+ _advect(dt) {
833
+ for (const p of this._particles) {
834
+ p.x += p.vx * dt;
835
+ p.y += p.vy * dt;
836
+ p.z += p.vz * dt;
837
+ }
838
+ }
839
+ _clampParticles() {
840
+ const { ox, oy, oz, nx, ny, nz, h } = this._grid;
841
+ const margin = h * 0.5;
842
+ const xMin = ox + margin, xMax = ox + nx * h - margin;
843
+ const yMin = oy + margin, yMax = oy + ny * h - margin;
844
+ const zMin = oz + margin, zMax = oz + nz * h - margin;
845
+ for (const p of this._particles) {
846
+ if (p.x < xMin) {
847
+ p.x = xMin;
848
+ if (p.vx < 0)
849
+ p.vx = 0;
850
+ }
851
+ if (p.x > xMax) {
852
+ p.x = xMax;
853
+ if (p.vx > 0)
854
+ p.vx = 0;
855
+ }
856
+ if (p.y < yMin) {
857
+ p.y = yMin;
858
+ if (p.vy < 0)
859
+ p.vy = 0;
860
+ }
861
+ if (p.y > yMax) {
862
+ p.y = yMax;
863
+ if (p.vy > 0)
864
+ p.vy = 0;
865
+ }
866
+ if (p.z < zMin) {
867
+ p.z = zMin;
868
+ if (p.vz < 0)
869
+ p.vz = 0;
870
+ }
871
+ if (p.z > zMax) {
872
+ p.z = zMax;
873
+ if (p.vz > 0)
874
+ p.vz = 0;
875
+ }
876
+ }
877
+ }
878
+ // ── Output ────────────────────────────────────────────────────────────────
879
+ /** Returns particle positions as a flat Float32Array [x,y,z, x,y,z, ...]. */
880
+ getPositions() {
881
+ const out = new Float32Array(this._particles.length * 3);
882
+ for (let i = 0; i < this._particles.length; i++) {
883
+ out[i * 3] = this._particles[i].x;
884
+ out[i * 3 + 1] = this._particles[i].y;
885
+ out[i * 3 + 2] = this._particles[i].z;
886
+ }
887
+ return out;
888
+ }
889
+ /** Returns particle velocities as a flat Float32Array [vx,vy,vz, ...]. */
890
+ getVelocities() {
891
+ const out = new Float32Array(this._particles.length * 3);
892
+ for (let i = 0; i < this._particles.length; i++) {
893
+ out[i * 3] = this._particles[i].vx;
894
+ out[i * 3 + 1] = this._particles[i].vy;
895
+ out[i * 3 + 2] = this._particles[i].vz;
896
+ }
897
+ return out;
898
+ }
899
+ /** Number of live particles. */
900
+ get liveCount() { return this._particles.length; }
901
+ /** The underlying MAC grid (for advanced use / surface reconstruction). */
902
+ get grid() { return this._grid; }
903
+ }
904
+
905
+ /**
906
+ * SPHSimulator — Smoothed Particle Hydrodynamics fluid simulation.
907
+ * Blender: Fluid modifier (FLIP/SPH domain).
908
+ *
909
+ * Implements the Müller et al. 2003 SPH formulation:
910
+ * - Poly6 kernel for density
911
+ * - Spiky kernel gradient for pressure
912
+ * - Viscosity kernel Laplacian for viscosity
913
+ *
914
+ * Workflow:
915
+ * 1. Construct with particle count + physics parameters
916
+ * 2. Call `setPositions(array)` to place particles in a volume
917
+ * 3. Call `step(dt)` each frame (dt ≈ 1/60)
918
+ * 4. Call `getGeometry()` to read back a THREE.BufferGeometry for rendering
919
+ *
920
+ * All physics parameters live in `parameters` for GSAP/st-keyframe:
921
+ * gsap.to(sim.parameters, { viscosity: 0.5, duration: 1 })
922
+ */
923
+ class SPHSimulator {
924
+ constructor(opts = {}) {
925
+ // Spatial grid for O(n) neighbour search
926
+ this._grid = new Map();
927
+ /** Simulation clock. */
928
+ this.time = 0;
929
+ // Domain bounds (AABB collider)
930
+ this._domainMin = [-2, -2, -2];
931
+ this._domainMax = [2, 2, 2];
932
+ this.particleCount = Math.max(1, Math.round(opts.particleCount ?? 500));
933
+ const h = opts.smoothingRadius ?? 0.3;
934
+ const g = opts.gravity ?? [0, -9.8, 0];
935
+ this.viscosityType = opts.viscosityType ?? 'standard';
936
+ this.parameters = {
937
+ smoothingRadius: h,
938
+ restDensity: opts.restDensity ?? 1000,
939
+ pressureK: opts.pressureK ?? 200,
940
+ viscosity: opts.viscosity ?? 0.1,
941
+ xsphFactor: 0.5,
942
+ stiffFactor: 0.5,
943
+ gravityX: g[0],
944
+ gravityY: g[1],
945
+ gravityZ: g[2],
946
+ substeps: opts.substeps ?? 4,
947
+ surfaceTension: opts.surfaceTension ?? 0.0728,
948
+ };
949
+ this._px = new Float32Array(this.particleCount);
950
+ this._py = new Float32Array(this.particleCount);
951
+ this._pz = new Float32Array(this.particleCount);
952
+ this._vx = new Float32Array(this.particleCount);
953
+ this._vy = new Float32Array(this.particleCount);
954
+ this._vz = new Float32Array(this.particleCount);
955
+ this._density = new Float32Array(this.particleCount);
956
+ this._pressure = new Float32Array(this.particleCount);
957
+ // Default: arrange in a 3D grid block
958
+ this._initGrid();
959
+ }
960
+ // ── Initialisation ────────────────────────────────────────────────────────
961
+ _initGrid() {
962
+ const n = this.particleCount;
963
+ const cbrt = Math.ceil(Math.cbrt(n));
964
+ const spacing = this.parameters.smoothingRadius * 0.9;
965
+ let i = 0;
966
+ outer: for (let z = 0; z < cbrt; z++) {
967
+ for (let y = 0; y < cbrt; y++) {
968
+ for (let x = 0; x < cbrt; x++) {
969
+ if (i >= n)
970
+ break outer;
971
+ this._px[i] = (x - cbrt / 2) * spacing;
972
+ this._py[i] = (y - cbrt / 2) * spacing;
973
+ this._pz[i] = (z - cbrt / 2) * spacing;
974
+ i++;
975
+ }
976
+ }
977
+ }
978
+ }
979
+ /**
980
+ * Set particle positions from a flat array [x0,y0,z0, x1,y1,z1, ...].
981
+ */
982
+ setPositions(arr) {
983
+ const n = Math.min(this.particleCount, Math.floor(arr.length / 3));
984
+ for (let i = 0; i < n; i++) {
985
+ this._px[i] = arr[i * 3];
986
+ this._py[i] = arr[i * 3 + 1];
987
+ this._pz[i] = arr[i * 3 + 2];
988
+ }
989
+ this._vx.fill(0);
990
+ this._vy.fill(0);
991
+ this._vz.fill(0);
992
+ }
993
+ /**
994
+ * Set simulation domain (AABB boundary collider).
995
+ * Particles bounce off the domain walls.
996
+ */
997
+ setDomain(min, max) {
998
+ this._domainMin = [...min];
999
+ this._domainMax = [...max];
1000
+ }
1001
+ // ── Simulation ────────────────────────────────────────────────────────────
1002
+ /**
1003
+ * Advance the simulation by dt seconds.
1004
+ * Recommended: pass clock.getDelta() clamped to 0.05 s.
1005
+ */
1006
+ step(dt) {
1007
+ const sub = Math.max(1, Math.round(this.parameters.substeps));
1008
+ const subDt = dt / sub;
1009
+ for (let s = 0; s < sub; s++)
1010
+ this._substep(subDt);
1011
+ this.time += dt;
1012
+ }
1013
+ /**
1014
+ * Build and return a BufferGeometry with a 'position' attribute.
1015
+ * Suitable for THREE.Points rendering.
1016
+ */
1017
+ getGeometry() {
1018
+ const positions = new Float32Array(this.particleCount * 3);
1019
+ for (let i = 0; i < this.particleCount; i++) {
1020
+ positions[i * 3] = this._px[i];
1021
+ positions[i * 3 + 1] = this._py[i];
1022
+ positions[i * 3 + 2] = this._pz[i];
1023
+ }
1024
+ const geo = new BufferGeometry();
1025
+ geo.setAttribute('position', new BufferAttribute(positions, 3));
1026
+ return geo;
1027
+ }
1028
+ /**
1029
+ * Copy current particle positions into an existing BufferGeometry.
1030
+ * More efficient than getGeometry() in the render loop.
1031
+ */
1032
+ updateGeometry(geo) {
1033
+ const attr = geo.getAttribute('position');
1034
+ const n = Math.min(attr.count, this.particleCount);
1035
+ for (let i = 0; i < n; i++) {
1036
+ attr.setXYZ(i, this._px[i], this._py[i], this._pz[i]);
1037
+ }
1038
+ attr.needsUpdate = true;
1039
+ }
1040
+ /** Per-particle density (read-only, updated after each step). */
1041
+ getDensity(i) { return this._density[i]; }
1042
+ /** Per-particle pressure (read-only). */
1043
+ getPressure(i) { return this._pressure[i]; }
1044
+ // ── Internals ─────────────────────────────────────────────────────────────
1045
+ _substep(dt) {
1046
+ this._buildSpatialGrid();
1047
+ this._computeDensityPressure();
1048
+ this._integrate(dt);
1049
+ if (this.viscosityType === 'xsph')
1050
+ this._applyXSPH();
1051
+ if (this.viscosityType === 'stiff')
1052
+ this._applyStiffViscosity(dt);
1053
+ this._enforceDomain();
1054
+ }
1055
+ // Uniform spatial grid: maps cell key → particle indices
1056
+ _buildSpatialGrid() {
1057
+ const h = this.parameters.smoothingRadius;
1058
+ this._grid.clear();
1059
+ for (let i = 0; i < this.particleCount; i++) {
1060
+ const key = this._cellKey(Math.floor(this._px[i] / h), Math.floor(this._py[i] / h), Math.floor(this._pz[i] / h));
1061
+ let cell = this._grid.get(key);
1062
+ if (!cell) {
1063
+ cell = [];
1064
+ this._grid.set(key, cell);
1065
+ }
1066
+ cell.push(i);
1067
+ }
1068
+ }
1069
+ _cellKey(cx, cy, cz) {
1070
+ // Simple hash: large primes, bit-truncated to 32-bit int range
1071
+ return ((cx * 92837111) ^ (cy * 689287499) ^ (cz * 283923481)) | 0;
1072
+ }
1073
+ _neighbours(i, callback) {
1074
+ const h = this.parameters.smoothingRadius;
1075
+ const h2 = h * h;
1076
+ const cx = Math.floor(this._px[i] / h);
1077
+ const cy = Math.floor(this._py[i] / h);
1078
+ const cz = Math.floor(this._pz[i] / h);
1079
+ for (let dz = -1; dz <= 1; dz++) {
1080
+ for (let dy = -1; dy <= 1; dy++) {
1081
+ for (let dx = -1; dx <= 1; dx++) {
1082
+ const cell = this._grid.get(this._cellKey(cx + dx, cy + dy, cz + dz));
1083
+ if (!cell)
1084
+ continue;
1085
+ for (const j of cell) {
1086
+ if (j === i)
1087
+ continue;
1088
+ const rx = this._px[j] - this._px[i];
1089
+ const ry = this._py[j] - this._py[i];
1090
+ const rz = this._pz[j] - this._pz[i];
1091
+ const r2 = rx * rx + ry * ry + rz * rz;
1092
+ if (r2 < h2)
1093
+ callback(j, r2);
1094
+ }
1095
+ }
1096
+ }
1097
+ }
1098
+ }
1099
+ // Müller 2003 Poly6 kernel
1100
+ _poly6(r2) {
1101
+ const h = this.parameters.smoothingRadius;
1102
+ const h2 = h * h;
1103
+ if (r2 > h2)
1104
+ return 0;
1105
+ const diff = h2 - r2;
1106
+ return (315 / (64 * Math.PI * Math.pow(h, 9))) * diff * diff * diff;
1107
+ }
1108
+ // Spiky kernel gradient magnitude ∂W/∂r (scalar, apply direction separately)
1109
+ _spikyGrad(r) {
1110
+ const h = this.parameters.smoothingRadius;
1111
+ if (r > h || r < 1e-6)
1112
+ return 0;
1113
+ const diff = h - r;
1114
+ return -(45 / (Math.PI * Math.pow(h, 6))) * diff * diff;
1115
+ }
1116
+ // Viscosity kernel Laplacian
1117
+ _viscLaplacian(r) {
1118
+ const h = this.parameters.smoothingRadius;
1119
+ if (r > h)
1120
+ return 0;
1121
+ return (45 / (Math.PI * Math.pow(h, 6))) * (h - r);
1122
+ }
1123
+ _computeDensityPressure() {
1124
+ const { restDensity, pressureK } = this.parameters;
1125
+ const particleMass = restDensity * Math.pow(this.parameters.smoothingRadius, 3) * 0.02;
1126
+ for (let i = 0; i < this.particleCount; i++) {
1127
+ let rho = this._poly6(0) * particleMass; // self contribution
1128
+ this._neighbours(i, (_j, r2) => {
1129
+ rho += particleMass * this._poly6(r2);
1130
+ });
1131
+ this._density[i] = Math.max(rho, restDensity * 0.001);
1132
+ // Tait equation of state (water)
1133
+ this._pressure[i] = Math.max(0, pressureK * (this._density[i] / restDensity - 1));
1134
+ }
1135
+ }
1136
+ _integrate(dt) {
1137
+ const { gravityX, gravityY, gravityZ, viscosity } = this.parameters;
1138
+ const particleMass = this.parameters.restDensity *
1139
+ Math.pow(this.parameters.smoothingRadius, 3) * 0.02;
1140
+ for (let i = 0; i < this.particleCount; i++) {
1141
+ let fx = 0, fy = 0, fz = 0;
1142
+ const pi = this._pressure[i];
1143
+ const rhoi = this._density[i];
1144
+ this._neighbours(i, (j, r2) => {
1145
+ const rx = this._px[j] - this._px[i];
1146
+ const ry = this._py[j] - this._py[i];
1147
+ const rz = this._pz[j] - this._pz[i];
1148
+ const r = Math.sqrt(r2);
1149
+ if (r < 1e-6)
1150
+ return;
1151
+ const nx = rx / r, ny = ry / r, nz = rz / r;
1152
+ const rhoj = this._density[j];
1153
+ const pj = this._pressure[j];
1154
+ const m = particleMass;
1155
+ // Pressure force (symmetric formulation)
1156
+ const spg = this._spikyGrad(r);
1157
+ const pf = -m * (pi + pj) / (2 * rhoj) * spg;
1158
+ fx += pf * nx;
1159
+ fy += pf * ny;
1160
+ fz += pf * nz;
1161
+ // Standard viscosity (Müller 2003) — skipped for xsph/stiff modes
1162
+ if (this.viscosityType === 'standard') {
1163
+ const visc = viscosity * m / rhoj * this._viscLaplacian(r);
1164
+ fx += visc * (this._vx[j] - this._vx[i]);
1165
+ fy += visc * (this._vy[j] - this._vy[i]);
1166
+ fz += visc * (this._vz[j] - this._vz[i]);
1167
+ }
1168
+ });
1169
+ // Gravity
1170
+ fx += gravityX * rhoi;
1171
+ fy += gravityY * rhoi;
1172
+ fz += gravityZ * rhoi;
1173
+ // Euler integration
1174
+ const invRho = 1 / rhoi;
1175
+ this._vx[i] += (fx * invRho) * dt;
1176
+ this._vy[i] += (fy * invRho) * dt;
1177
+ this._vz[i] += (fz * invRho) * dt;
1178
+ this._px[i] += this._vx[i] * dt;
1179
+ this._py[i] += this._vy[i] * dt;
1180
+ this._pz[i] += this._vz[i] * dt;
1181
+ }
1182
+ }
1183
+ /**
1184
+ * XSPH viscosity (Schechter & Bridson 2012).
1185
+ * Blends each particle's velocity toward a weighted average of its neighbours.
1186
+ * More stable than Müller viscosity, good for thick/honey-like fluids.
1187
+ * Controlled by parameters.xsphFactor [0–1].
1188
+ */
1189
+ _applyXSPH() {
1190
+ const h = this.parameters.smoothingRadius;
1191
+ const factor = this.parameters.xsphFactor;
1192
+ const particleMass = this.parameters.restDensity * Math.pow(h, 3) * 0.02;
1193
+ // Temporary correction arrays
1194
+ const cx = new Float32Array(this.particleCount);
1195
+ const cy = new Float32Array(this.particleCount);
1196
+ const cz = new Float32Array(this.particleCount);
1197
+ for (let i = 0; i < this.particleCount; i++) {
1198
+ this._neighbours(i, (j, r2) => {
1199
+ const w = this._poly6(r2);
1200
+ const avgRho = (this._density[i] + this._density[j]) * 0.5;
1201
+ const wm = w * particleMass / avgRho;
1202
+ cx[i] += wm * (this._vx[j] - this._vx[i]);
1203
+ cy[i] += wm * (this._vy[j] - this._vy[i]);
1204
+ cz[i] += wm * (this._vz[j] - this._vz[i]);
1205
+ });
1206
+ }
1207
+ for (let i = 0; i < this.particleCount; i++) {
1208
+ this._vx[i] += factor * cx[i];
1209
+ this._vy[i] += factor * cy[i];
1210
+ this._vz[i] += factor * cz[i];
1211
+ }
1212
+ }
1213
+ /**
1214
+ * Stiff viscosity (position-based / divergence damping).
1215
+ * Applies a velocity correction proportional to the local velocity divergence,
1216
+ * strongly suppressing compressible motion.
1217
+ * Best for near-incompressible high-viscosity simulations.
1218
+ * Controlled by parameters.stiffFactor.
1219
+ */
1220
+ _applyStiffViscosity(dt) {
1221
+ const h = this.parameters.smoothingRadius;
1222
+ const stiff = this.parameters.stiffFactor;
1223
+ const particleMass = this.parameters.restDensity * Math.pow(h, 3) * 0.02;
1224
+ const cx = new Float32Array(this.particleCount);
1225
+ const cy = new Float32Array(this.particleCount);
1226
+ const cz = new Float32Array(this.particleCount);
1227
+ for (let i = 0; i < this.particleCount; i++) {
1228
+ this._neighbours(i, (j, r2) => {
1229
+ const r = Math.sqrt(r2);
1230
+ if (r < 1e-6)
1231
+ return;
1232
+ const rx = this._px[j] - this._px[i];
1233
+ const ry = this._py[j] - this._py[i];
1234
+ const rz = this._pz[j] - this._pz[i];
1235
+ const dvx = this._vx[j] - this._vx[i];
1236
+ const dvy = this._vy[j] - this._vy[i];
1237
+ const dvz = this._vz[j] - this._vz[i];
1238
+ // Divergence contribution: (v_ij · r_ij) / |r_ij|²
1239
+ const dot = dvx * rx + dvy * ry + dvz * rz;
1240
+ const spg = this._spikyGrad(r);
1241
+ const coef = particleMass / this._density[j] * spg / r;
1242
+ // Velocity correction: push toward incompressibility
1243
+ cx[i] += coef * dot / r2 * rx;
1244
+ cy[i] += coef * dot / r2 * ry;
1245
+ cz[i] += coef * dot / r2 * rz;
1246
+ });
1247
+ }
1248
+ for (let i = 0; i < this.particleCount; i++) {
1249
+ this._vx[i] -= stiff * dt * cx[i];
1250
+ this._vy[i] -= stiff * dt * cy[i];
1251
+ this._vz[i] -= stiff * dt * cz[i];
1252
+ }
1253
+ }
1254
+ _enforceDomain() {
1255
+ const [minX, minY, minZ] = this._domainMin;
1256
+ const [maxX, maxY, maxZ] = this._domainMax;
1257
+ for (let i = 0; i < this.particleCount; i++) {
1258
+ if (this._px[i] < minX) {
1259
+ this._px[i] = minX;
1260
+ if (this._vx[i] < 0)
1261
+ this._vx[i] *= -0.3;
1262
+ }
1263
+ if (this._px[i] > maxX) {
1264
+ this._px[i] = maxX;
1265
+ if (this._vx[i] > 0)
1266
+ this._vx[i] *= -0.3;
1267
+ }
1268
+ if (this._py[i] < minY) {
1269
+ this._py[i] = minY;
1270
+ if (this._vy[i] < 0)
1271
+ this._vy[i] *= -0.3;
1272
+ }
1273
+ if (this._py[i] > maxY) {
1274
+ this._py[i] = maxY;
1275
+ if (this._vy[i] > 0)
1276
+ this._vy[i] *= -0.3;
1277
+ }
1278
+ if (this._pz[i] < minZ) {
1279
+ this._pz[i] = minZ;
1280
+ if (this._vz[i] < 0)
1281
+ this._vz[i] *= -0.3;
1282
+ }
1283
+ if (this._pz[i] > maxZ) {
1284
+ this._pz[i] = maxZ;
1285
+ if (this._vz[i] > 0)
1286
+ this._vz[i] *= -0.3;
1287
+ }
1288
+ }
1289
+ }
1290
+ }
1291
+
1292
+ class FluidEmitter {
1293
+ constructor(opts = {}) {
1294
+ this.enabled = true;
1295
+ const p = opts.position ?? [0, 0, 0];
1296
+ const v = opts.velocity ?? [0, 0, 0];
1297
+ this.parameters = {
1298
+ posX: p[0], posY: p[1], posZ: p[2],
1299
+ radius: opts.radius ?? 0.5,
1300
+ velX: v[0], velY: v[1], velZ: v[2],
1301
+ jitter: opts.jitter ?? 0.1,
1302
+ };
1303
+ }
1304
+ /** Sample a random spawn position within the emitter sphere. */
1305
+ samplePosition() {
1306
+ const r = this.parameters.radius * Math.cbrt(Math.random());
1307
+ const theta = Math.random() * Math.PI * 2;
1308
+ const phi = Math.acos(2 * Math.random() - 1);
1309
+ const sinPhi = Math.sin(phi);
1310
+ return [
1311
+ this.parameters.posX + r * sinPhi * Math.cos(theta),
1312
+ this.parameters.posY + r * sinPhi * Math.sin(theta),
1313
+ this.parameters.posZ + r * Math.cos(phi),
1314
+ ];
1315
+ }
1316
+ /** Sample initial velocity with jitter. */
1317
+ sampleVelocity() {
1318
+ const j = this.parameters.jitter;
1319
+ return [
1320
+ this.parameters.velX + (Math.random() - 0.5) * j * 2,
1321
+ this.parameters.velY + (Math.random() - 0.5) * j * 2,
1322
+ this.parameters.velZ + (Math.random() - 0.5) * j * 2,
1323
+ ];
1324
+ }
1325
+ }
1326
+
1327
+ // Full 256-entry Marching Cubes triangle table (Lorensen & Cline 1987).
1328
+ // Each entry lists edge indices (0–11) forming triangles; -1 = end.
1329
+ // Encoded as flat Int8Array, 16 values per cube case.
1330
+ const TRI_TABLE_FLAT = new Int8Array([
1331
+ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
1332
+ 0, 8, 3, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
1333
+ 0, 1, 9, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
1334
+ 1, 8, 3, 9, 8, 1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
1335
+ 1, 2, 10, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
1336
+ 0, 8, 3, 1, 2, 10, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
1337
+ 9, 2, 10, 0, 2, 9, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
1338
+ 2, 8, 3, 2, 10, 8, 10, 9, 8, -1, -1, -1, -1, -1, -1, -1,
1339
+ 3, 11, 2, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
1340
+ 0, 11, 2, 8, 11, 0, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
1341
+ 1, 9, 0, 2, 3, 11, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
1342
+ 1, 11, 2, 1, 9, 11, 9, 8, 11, -1, -1, -1, -1, -1, -1, -1,
1343
+ 3, 10, 1, 11, 10, 3, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
1344
+ 0, 10, 1, 0, 8, 10, 8, 11, 10, -1, -1, -1, -1, -1, -1, -1,
1345
+ 3, 9, 0, 3, 11, 9, 11, 10, 9, -1, -1, -1, -1, -1, -1, -1,
1346
+ 9, 8, 10, 10, 8, 11, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
1347
+ 4, 7, 8, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
1348
+ 4, 3, 0, 7, 3, 4, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
1349
+ 0, 1, 9, 8, 4, 7, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
1350
+ 4, 1, 9, 4, 7, 1, 7, 3, 1, -1, -1, -1, -1, -1, -1, -1,
1351
+ 1, 2, 10, 8, 4, 7, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
1352
+ 3, 4, 7, 3, 0, 4, 1, 2, 10, -1, -1, -1, -1, -1, -1, -1,
1353
+ 9, 2, 10, 9, 0, 2, 8, 4, 7, -1, -1, -1, -1, -1, -1, -1,
1354
+ 2, 10, 9, 2, 9, 7, 2, 7, 3, 7, 9, 4, -1, -1, -1, -1,
1355
+ 8, 4, 7, 3, 11, 2, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
1356
+ 11, 4, 7, 11, 2, 4, 2, 0, 4, -1, -1, -1, -1, -1, -1, -1,
1357
+ 9, 0, 1, 8, 4, 7, 2, 3, 11, -1, -1, -1, -1, -1, -1, -1,
1358
+ 4, 7, 11, 9, 4, 11, 9, 11, 2, 9, 2, 1, -1, -1, -1, -1,
1359
+ 3, 10, 1, 3, 11, 10, 7, 8, 4, -1, -1, -1, -1, -1, -1, -1,
1360
+ 1, 11, 10, 1, 4, 11, 1, 0, 4, 7, 11, 4, -1, -1, -1, -1,
1361
+ 4, 7, 8, 9, 0, 11, 9, 11, 10, 11, 0, 3, -1, -1, -1, -1,
1362
+ 4, 7, 11, 4, 11, 9, 9, 11, 10, -1, -1, -1, -1, -1, -1, -1,
1363
+ 9, 5, 4, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
1364
+ 9, 5, 4, 0, 8, 3, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
1365
+ 0, 5, 4, 1, 5, 0, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
1366
+ 8, 5, 4, 8, 3, 5, 3, 1, 5, -1, -1, -1, -1, -1, -1, -1,
1367
+ 1, 2, 10, 9, 5, 4, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
1368
+ 3, 0, 8, 1, 2, 10, 4, 9, 5, -1, -1, -1, -1, -1, -1, -1,
1369
+ 5, 2, 10, 5, 4, 2, 4, 0, 2, -1, -1, -1, -1, -1, -1, -1,
1370
+ 2, 10, 5, 3, 2, 5, 3, 5, 4, 3, 4, 8, -1, -1, -1, -1,
1371
+ 9, 5, 4, 2, 3, 11, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
1372
+ 0, 11, 2, 0, 8, 11, 4, 9, 5, -1, -1, -1, -1, -1, -1, -1,
1373
+ 0, 5, 4, 0, 1, 5, 2, 3, 11, -1, -1, -1, -1, -1, -1, -1,
1374
+ 2, 1, 5, 2, 5, 8, 2, 8, 11, 4, 8, 5, -1, -1, -1, -1,
1375
+ 10, 3, 11, 10, 1, 3, 9, 5, 4, -1, -1, -1, -1, -1, -1, -1,
1376
+ 4, 9, 5, 0, 8, 1, 8, 10, 1, 8, 11, 10, -1, -1, -1, -1,
1377
+ 5, 4, 0, 5, 0, 11, 5, 11, 10, 11, 0, 3, -1, -1, -1, -1,
1378
+ 5, 4, 8, 5, 8, 10, 10, 8, 11, -1, -1, -1, -1, -1, -1, -1,
1379
+ 9, 7, 8, 5, 7, 9, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
1380
+ 9, 3, 0, 9, 5, 3, 5, 7, 3, -1, -1, -1, -1, -1, -1, -1,
1381
+ 0, 7, 8, 0, 1, 7, 1, 5, 7, -1, -1, -1, -1, -1, -1, -1,
1382
+ 1, 5, 3, 3, 5, 7, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
1383
+ 9, 7, 8, 9, 5, 7, 10, 1, 2, -1, -1, -1, -1, -1, -1, -1,
1384
+ 10, 1, 2, 9, 5, 0, 5, 3, 0, 5, 7, 3, -1, -1, -1, -1,
1385
+ 8, 0, 2, 8, 2, 5, 8, 5, 7, 10, 5, 2, -1, -1, -1, -1,
1386
+ 2, 10, 5, 2, 5, 3, 3, 5, 7, -1, -1, -1, -1, -1, -1, -1,
1387
+ 7, 9, 5, 7, 8, 9, 3, 11, 2, -1, -1, -1, -1, -1, -1, -1,
1388
+ 9, 5, 7, 9, 7, 2, 9, 2, 0, 2, 7, 11, -1, -1, -1, -1,
1389
+ 2, 3, 11, 0, 1, 8, 1, 7, 8, 1, 5, 7, -1, -1, -1, -1,
1390
+ 11, 2, 1, 11, 1, 7, 7, 1, 5, -1, -1, -1, -1, -1, -1, -1,
1391
+ 9, 5, 8, 8, 5, 7, 10, 1, 3, 10, 3, 11, -1, -1, -1, -1,
1392
+ 5, 7, 0, 5, 0, 9, 7, 11, 0, 1, 0, 10, 11, 10, 0, -1,
1393
+ 11, 10, 0, 11, 0, 3, 10, 5, 0, 8, 0, 7, 5, 7, 0, -1,
1394
+ 11, 10, 5, 7, 11, 5, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
1395
+ 10, 6, 5, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
1396
+ 0, 8, 3, 5, 10, 6, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
1397
+ 9, 0, 1, 5, 10, 6, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
1398
+ 1, 8, 3, 1, 9, 8, 5, 10, 6, -1, -1, -1, -1, -1, -1, -1,
1399
+ 1, 6, 5, 2, 6, 1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
1400
+ 1, 6, 5, 1, 2, 6, 3, 0, 8, -1, -1, -1, -1, -1, -1, -1,
1401
+ 9, 6, 5, 9, 0, 6, 0, 2, 6, -1, -1, -1, -1, -1, -1, -1,
1402
+ 5, 9, 8, 5, 8, 2, 5, 2, 6, 3, 2, 8, -1, -1, -1, -1,
1403
+ 2, 3, 11, 10, 6, 5, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
1404
+ 11, 0, 8, 11, 2, 0, 10, 6, 5, -1, -1, -1, -1, -1, -1, -1,
1405
+ 0, 1, 9, 2, 3, 11, 5, 10, 6, -1, -1, -1, -1, -1, -1, -1,
1406
+ 5, 10, 6, 1, 9, 2, 9, 11, 2, 9, 8, 11, -1, -1, -1, -1,
1407
+ 6, 3, 11, 6, 5, 3, 5, 1, 3, -1, -1, -1, -1, -1, -1, -1,
1408
+ 0, 8, 11, 0, 11, 5, 0, 5, 1, 5, 11, 6, -1, -1, -1, -1,
1409
+ 3, 11, 6, 0, 3, 6, 0, 6, 5, 0, 5, 9, -1, -1, -1, -1,
1410
+ 6, 5, 9, 6, 9, 11, 11, 9, 8, -1, -1, -1, -1, -1, -1, -1,
1411
+ 5, 10, 6, 4, 7, 8, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
1412
+ 4, 3, 0, 4, 7, 3, 6, 5, 10, -1, -1, -1, -1, -1, -1, -1,
1413
+ 1, 9, 0, 5, 10, 6, 8, 4, 7, -1, -1, -1, -1, -1, -1, -1,
1414
+ 10, 6, 5, 1, 9, 7, 1, 7, 3, 7, 9, 4, -1, -1, -1, -1,
1415
+ 6, 1, 2, 6, 5, 1, 4, 7, 8, -1, -1, -1, -1, -1, -1, -1,
1416
+ 1, 2, 5, 5, 2, 6, 3, 0, 4, 3, 4, 7, -1, -1, -1, -1,
1417
+ 8, 4, 7, 9, 0, 5, 0, 6, 5, 0, 2, 6, -1, -1, -1, -1,
1418
+ 7, 3, 9, 7, 9, 4, 3, 2, 9, 5, 9, 6, 2, 6, 9, -1,
1419
+ 3, 11, 2, 7, 8, 4, 10, 6, 5, -1, -1, -1, -1, -1, -1, -1,
1420
+ 5, 10, 6, 4, 7, 2, 4, 2, 0, 2, 7, 11, -1, -1, -1, -1,
1421
+ 0, 1, 9, 4, 7, 8, 2, 3, 11, 5, 10, 6, -1, -1, -1, -1,
1422
+ 9, 2, 1, 9, 11, 2, 9, 4, 11, 7, 11, 4, 5, 10, 6, -1,
1423
+ 8, 4, 7, 3, 11, 5, 3, 5, 1, 5, 11, 6, -1, -1, -1, -1,
1424
+ 5, 1, 11, 5, 11, 6, 1, 0, 11, 7, 11, 4, 0, 4, 11, -1,
1425
+ 0, 5, 9, 0, 6, 5, 0, 3, 6, 11, 6, 3, 8, 4, 7, -1,
1426
+ 6, 5, 9, 6, 9, 11, 4, 7, 9, 7, 11, 9, -1, -1, -1, -1,
1427
+ 10, 4, 9, 6, 4, 10, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
1428
+ 4, 10, 6, 4, 9, 10, 0, 8, 3, -1, -1, -1, -1, -1, -1, -1,
1429
+ 10, 0, 1, 10, 6, 0, 6, 4, 0, -1, -1, -1, -1, -1, -1, -1,
1430
+ 8, 3, 1, 8, 1, 6, 8, 6, 4, 6, 1, 10, -1, -1, -1, -1,
1431
+ 1, 4, 9, 1, 2, 4, 2, 6, 4, -1, -1, -1, -1, -1, -1, -1,
1432
+ 3, 0, 8, 1, 2, 9, 2, 4, 9, 2, 6, 4, -1, -1, -1, -1,
1433
+ 0, 2, 4, 4, 2, 6, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
1434
+ 8, 3, 2, 8, 2, 4, 4, 2, 6, -1, -1, -1, -1, -1, -1, -1,
1435
+ 10, 4, 9, 10, 6, 4, 11, 2, 3, -1, -1, -1, -1, -1, -1, -1,
1436
+ 0, 8, 2, 2, 8, 11, 4, 9, 10, 4, 10, 6, -1, -1, -1, -1,
1437
+ 3, 11, 2, 0, 1, 6, 0, 6, 4, 6, 1, 10, -1, -1, -1, -1,
1438
+ 6, 4, 1, 6, 1, 10, 4, 8, 1, 2, 1, 11, 8, 11, 1, -1,
1439
+ 9, 6, 4, 9, 3, 6, 9, 1, 3, 11, 6, 3, -1, -1, -1, -1,
1440
+ 8, 11, 1, 8, 1, 0, 11, 6, 1, 9, 1, 4, 6, 4, 1, -1,
1441
+ 3, 11, 6, 3, 6, 0, 0, 6, 4, -1, -1, -1, -1, -1, -1, -1,
1442
+ 6, 4, 8, 11, 6, 8, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
1443
+ 7, 10, 6, 7, 8, 10, 8, 9, 10, -1, -1, -1, -1, -1, -1, -1,
1444
+ 0, 7, 3, 0, 10, 7, 0, 9, 10, 6, 7, 10, -1, -1, -1, -1,
1445
+ 10, 6, 7, 1, 10, 7, 1, 7, 8, 1, 8, 0, -1, -1, -1, -1,
1446
+ 10, 6, 7, 10, 7, 1, 1, 7, 3, -1, -1, -1, -1, -1, -1, -1,
1447
+ 1, 2, 6, 1, 6, 8, 1, 8, 9, 8, 6, 7, -1, -1, -1, -1,
1448
+ 2, 6, 9, 2, 9, 1, 6, 7, 9, 0, 9, 3, 7, 3, 9, -1,
1449
+ 7, 8, 0, 7, 0, 6, 6, 0, 2, -1, -1, -1, -1, -1, -1, -1,
1450
+ 7, 3, 2, 6, 7, 2, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
1451
+ 2, 3, 11, 10, 6, 8, 10, 8, 9, 8, 6, 7, -1, -1, -1, -1,
1452
+ 2, 0, 7, 2, 7, 11, 0, 9, 7, 6, 7, 10, 9, 10, 7, -1,
1453
+ 1, 8, 0, 1, 7, 8, 1, 10, 7, 6, 7, 10, 2, 3, 11, -1,
1454
+ 11, 2, 1, 11, 1, 7, 10, 6, 1, 6, 7, 1, -1, -1, -1, -1,
1455
+ 8, 9, 6, 8, 6, 7, 9, 1, 6, 11, 6, 3, 1, 3, 6, -1,
1456
+ 0, 9, 1, 11, 6, 7, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
1457
+ 7, 8, 0, 7, 0, 6, 3, 11, 0, 11, 6, 0, -1, -1, -1, -1,
1458
+ 7, 11, 6, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
1459
+ 7, 6, 11, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
1460
+ 3, 0, 8, 11, 7, 6, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
1461
+ 0, 1, 9, 11, 7, 6, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
1462
+ 8, 1, 9, 8, 3, 1, 11, 7, 6, -1, -1, -1, -1, -1, -1, -1,
1463
+ 10, 1, 2, 6, 11, 7, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
1464
+ 1, 2, 10, 3, 0, 8, 6, 11, 7, -1, -1, -1, -1, -1, -1, -1,
1465
+ 2, 9, 0, 2, 10, 9, 6, 11, 7, -1, -1, -1, -1, -1, -1, -1,
1466
+ 6, 11, 7, 2, 10, 3, 10, 8, 3, 10, 9, 8, -1, -1, -1, -1,
1467
+ 7, 2, 3, 6, 2, 7, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
1468
+ 7, 0, 8, 7, 6, 0, 6, 2, 0, -1, -1, -1, -1, -1, -1, -1,
1469
+ 2, 7, 6, 2, 3, 7, 0, 1, 9, -1, -1, -1, -1, -1, -1, -1,
1470
+ 1, 6, 2, 1, 8, 6, 1, 9, 8, 8, 7, 6, -1, -1, -1, -1,
1471
+ 10, 7, 6, 10, 1, 7, 1, 3, 7, -1, -1, -1, -1, -1, -1, -1,
1472
+ 10, 7, 6, 1, 7, 10, 1, 8, 7, 1, 0, 8, -1, -1, -1, -1,
1473
+ 0, 3, 7, 0, 7, 10, 0, 10, 9, 6, 10, 7, -1, -1, -1, -1,
1474
+ 7, 6, 10, 7, 10, 8, 8, 10, 9, -1, -1, -1, -1, -1, -1, -1,
1475
+ 6, 8, 4, 11, 8, 6, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
1476
+ 3, 6, 11, 3, 0, 6, 0, 4, 6, -1, -1, -1, -1, -1, -1, -1,
1477
+ 8, 6, 11, 8, 4, 6, 9, 0, 1, -1, -1, -1, -1, -1, -1, -1,
1478
+ 9, 4, 6, 9, 6, 3, 9, 3, 1, 11, 3, 6, -1, -1, -1, -1,
1479
+ 6, 8, 4, 6, 11, 8, 2, 10, 1, -1, -1, -1, -1, -1, -1, -1,
1480
+ 1, 2, 10, 3, 0, 11, 0, 6, 11, 0, 4, 6, -1, -1, -1, -1,
1481
+ 4, 11, 8, 4, 6, 11, 0, 2, 9, 2, 10, 9, -1, -1, -1, -1,
1482
+ 10, 9, 3, 10, 3, 2, 9, 4, 3, 11, 3, 6, 4, 6, 3, -1,
1483
+ 8, 2, 3, 8, 4, 2, 4, 6, 2, -1, -1, -1, -1, -1, -1, -1,
1484
+ 0, 4, 2, 4, 6, 2, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
1485
+ 1, 9, 0, 2, 3, 4, 2, 4, 6, 4, 3, 8, -1, -1, -1, -1,
1486
+ 1, 9, 4, 1, 4, 2, 2, 4, 6, -1, -1, -1, -1, -1, -1, -1,
1487
+ 8, 1, 3, 8, 6, 1, 8, 4, 6, 6, 10, 1, -1, -1, -1, -1,
1488
+ 10, 1, 0, 10, 0, 6, 6, 0, 4, -1, -1, -1, -1, -1, -1, -1,
1489
+ 4, 6, 3, 4, 3, 8, 6, 10, 3, 0, 3, 9, 10, 9, 3, -1,
1490
+ 10, 9, 4, 6, 10, 4, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
1491
+ 4, 9, 5, 7, 6, 11, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
1492
+ 0, 8, 3, 4, 9, 5, 11, 7, 6, -1, -1, -1, -1, -1, -1, -1,
1493
+ 5, 0, 1, 5, 4, 0, 7, 6, 11, -1, -1, -1, -1, -1, -1, -1,
1494
+ 11, 7, 6, 8, 3, 4, 3, 5, 4, 3, 1, 5, -1, -1, -1, -1,
1495
+ 9, 5, 4, 10, 1, 2, 7, 6, 11, -1, -1, -1, -1, -1, -1, -1,
1496
+ 6, 11, 7, 1, 2, 10, 0, 8, 3, 4, 9, 5, -1, -1, -1, -1,
1497
+ 7, 6, 11, 5, 4, 10, 4, 2, 10, 4, 0, 2, -1, -1, -1, -1,
1498
+ 3, 4, 8, 3, 5, 4, 3, 2, 5, 10, 5, 2, 11, 7, 6, -1,
1499
+ 7, 2, 3, 7, 6, 2, 5, 4, 9, -1, -1, -1, -1, -1, -1, -1,
1500
+ 9, 5, 4, 0, 8, 6, 0, 6, 2, 6, 8, 7, -1, -1, -1, -1,
1501
+ 3, 6, 2, 3, 7, 6, 1, 5, 0, 5, 4, 0, -1, -1, -1, -1,
1502
+ 6, 2, 8, 6, 8, 7, 2, 1, 8, 4, 8, 5, 1, 5, 8, -1,
1503
+ 9, 5, 4, 10, 1, 6, 1, 7, 6, 1, 3, 7, -1, -1, -1, -1,
1504
+ 1, 6, 10, 1, 7, 6, 1, 0, 7, 8, 7, 0, 9, 5, 4, -1,
1505
+ 4, 0, 10, 4, 10, 5, 0, 3, 10, 6, 10, 7, 3, 7, 10, -1,
1506
+ 7, 6, 10, 7, 10, 8, 5, 4, 10, 4, 8, 10, -1, -1, -1, -1,
1507
+ 6, 9, 5, 6, 11, 9, 11, 8, 9, -1, -1, -1, -1, -1, -1, -1,
1508
+ 3, 6, 11, 0, 6, 3, 0, 5, 6, 0, 9, 5, -1, -1, -1, -1,
1509
+ 0, 11, 8, 0, 5, 11, 0, 1, 5, 5, 6, 11, -1, -1, -1, -1,
1510
+ 6, 11, 3, 6, 3, 5, 5, 3, 1, -1, -1, -1, -1, -1, -1, -1,
1511
+ 1, 2, 10, 9, 5, 11, 9, 11, 8, 11, 5, 6, -1, -1, -1, -1,
1512
+ 0, 11, 3, 0, 6, 11, 0, 9, 6, 5, 6, 9, 1, 2, 10, -1,
1513
+ 11, 8, 5, 11, 5, 6, 8, 0, 5, 10, 5, 2, 0, 2, 5, -1,
1514
+ 6, 11, 3, 6, 3, 5, 2, 10, 3, 10, 5, 3, -1, -1, -1, -1,
1515
+ 5, 8, 9, 5, 2, 8, 5, 6, 2, 3, 8, 2, -1, -1, -1, -1,
1516
+ 9, 5, 6, 9, 6, 0, 0, 6, 2, -1, -1, -1, -1, -1, -1, -1,
1517
+ 1, 5, 8, 1, 8, 0, 5, 6, 8, 3, 8, 2, 6, 2, 8, -1,
1518
+ 1, 5, 6, 2, 1, 6, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
1519
+ 1, 3, 6, 1, 6, 10, 3, 8, 6, 5, 6, 9, 8, 9, 6, -1,
1520
+ 10, 1, 0, 10, 0, 6, 9, 5, 0, 5, 6, 0, -1, -1, -1, -1,
1521
+ 0, 3, 8, 5, 6, 10, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
1522
+ 10, 5, 6, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
1523
+ 11, 5, 10, 7, 5, 11, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
1524
+ 11, 5, 10, 11, 7, 5, 8, 3, 0, -1, -1, -1, -1, -1, -1, -1,
1525
+ 5, 11, 7, 5, 10, 11, 1, 9, 0, -1, -1, -1, -1, -1, -1, -1,
1526
+ 10, 7, 5, 10, 11, 7, 9, 8, 1, 8, 3, 1, -1, -1, -1, -1,
1527
+ 11, 1, 2, 11, 7, 1, 7, 5, 1, -1, -1, -1, -1, -1, -1, -1,
1528
+ 0, 8, 3, 1, 2, 7, 1, 7, 5, 7, 2, 11, -1, -1, -1, -1,
1529
+ 9, 7, 5, 9, 2, 7, 9, 0, 2, 2, 11, 7, -1, -1, -1, -1,
1530
+ 7, 5, 2, 7, 2, 11, 5, 9, 2, 3, 2, 8, 9, 8, 2, -1,
1531
+ 2, 5, 10, 2, 3, 5, 3, 7, 5, -1, -1, -1, -1, -1, -1, -1,
1532
+ 8, 2, 0, 8, 5, 2, 8, 7, 5, 10, 2, 5, -1, -1, -1, -1,
1533
+ 9, 0, 1, 5, 10, 3, 5, 3, 7, 3, 10, 2, -1, -1, -1, -1,
1534
+ 9, 8, 2, 9, 2, 1, 8, 7, 2, 10, 2, 5, 7, 5, 2, -1,
1535
+ 1, 3, 5, 3, 7, 5, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
1536
+ 0, 8, 7, 0, 7, 1, 1, 7, 5, -1, -1, -1, -1, -1, -1, -1,
1537
+ 9, 0, 3, 9, 3, 5, 5, 3, 7, -1, -1, -1, -1, -1, -1, -1,
1538
+ 9, 8, 7, 5, 9, 7, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
1539
+ 5, 8, 4, 5, 10, 8, 10, 11, 8, -1, -1, -1, -1, -1, -1, -1,
1540
+ 5, 0, 4, 5, 11, 0, 5, 10, 11, 11, 3, 0, -1, -1, -1, -1,
1541
+ 0, 1, 9, 8, 4, 10, 8, 10, 11, 10, 4, 5, -1, -1, -1, -1,
1542
+ 10, 11, 4, 10, 4, 5, 11, 3, 4, 9, 4, 1, 3, 1, 4, -1,
1543
+ 2, 5, 1, 2, 8, 5, 2, 11, 8, 4, 5, 8, -1, -1, -1, -1,
1544
+ 0, 4, 11, 0, 11, 3, 4, 5, 11, 2, 11, 1, 5, 1, 11, -1,
1545
+ 0, 2, 5, 0, 5, 9, 2, 11, 5, 4, 5, 8, 11, 8, 5, -1,
1546
+ 9, 4, 5, 2, 11, 3, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
1547
+ 2, 5, 10, 3, 5, 2, 3, 4, 5, 3, 8, 4, -1, -1, -1, -1,
1548
+ 5, 10, 2, 5, 2, 4, 4, 2, 0, -1, -1, -1, -1, -1, -1, -1,
1549
+ 3, 10, 2, 3, 5, 10, 3, 8, 5, 4, 5, 8, 0, 1, 9, -1,
1550
+ 5, 10, 2, 5, 2, 4, 1, 9, 2, 9, 4, 2, -1, -1, -1, -1,
1551
+ 8, 4, 5, 8, 5, 3, 3, 5, 1, -1, -1, -1, -1, -1, -1, -1,
1552
+ 0, 4, 5, 1, 0, 5, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
1553
+ 8, 4, 5, 8, 5, 3, 9, 0, 5, 0, 3, 5, -1, -1, -1, -1,
1554
+ 9, 4, 5, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
1555
+ 4, 11, 7, 4, 9, 11, 9, 10, 11, -1, -1, -1, -1, -1, -1, -1,
1556
+ 0, 8, 3, 4, 9, 7, 9, 11, 7, 9, 10, 11, -1, -1, -1, -1,
1557
+ 1, 10, 11, 1, 11, 4, 1, 4, 0, 7, 4, 11, -1, -1, -1, -1,
1558
+ 3, 1, 4, 3, 4, 8, 1, 10, 4, 7, 4, 11, 10, 11, 4, -1,
1559
+ 4, 11, 7, 9, 11, 4, 9, 2, 11, 9, 1, 2, -1, -1, -1, -1,
1560
+ 9, 7, 4, 9, 11, 7, 9, 1, 11, 2, 11, 1, 0, 8, 3, -1,
1561
+ 11, 7, 4, 11, 4, 2, 2, 4, 0, -1, -1, -1, -1, -1, -1, -1,
1562
+ 11, 7, 4, 11, 4, 2, 8, 3, 4, 3, 2, 4, -1, -1, -1, -1,
1563
+ 2, 9, 10, 2, 7, 9, 2, 3, 7, 7, 4, 9, -1, -1, -1, -1,
1564
+ 9, 10, 7, 9, 7, 4, 10, 2, 7, 8, 7, 0, 2, 0, 7, -1,
1565
+ 3, 7, 10, 3, 10, 2, 7, 4, 10, 1, 10, 0, 4, 0, 10, -1,
1566
+ 1, 10, 2, 8, 7, 4, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
1567
+ 4, 9, 1, 4, 1, 7, 7, 1, 3, -1, -1, -1, -1, -1, -1, -1,
1568
+ 4, 9, 1, 4, 1, 7, 0, 8, 1, 8, 7, 1, -1, -1, -1, -1,
1569
+ 4, 0, 3, 7, 4, 3, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
1570
+ 4, 8, 7, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
1571
+ 9, 10, 8, 10, 11, 8, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
1572
+ 3, 0, 9, 3, 9, 11, 11, 9, 10, -1, -1, -1, -1, -1, -1, -1,
1573
+ 0, 1, 10, 0, 10, 8, 8, 10, 11, -1, -1, -1, -1, -1, -1, -1,
1574
+ 3, 1, 10, 11, 3, 10, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
1575
+ 1, 2, 11, 1, 11, 9, 9, 11, 8, -1, -1, -1, -1, -1, -1, -1,
1576
+ 3, 0, 9, 3, 9, 11, 1, 2, 9, 2, 11, 9, -1, -1, -1, -1,
1577
+ 0, 2, 11, 8, 0, 11, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
1578
+ 3, 2, 11, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
1579
+ 2, 3, 8, 2, 8, 10, 10, 8, 9, -1, -1, -1, -1, -1, -1, -1,
1580
+ 9, 10, 2, 0, 9, 2, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
1581
+ 2, 3, 8, 2, 8, 10, 0, 1, 8, 1, 10, 8, -1, -1, -1, -1,
1582
+ 1, 10, 2, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
1583
+ 1, 3, 8, 9, 1, 8, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
1584
+ 0, 9, 1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
1585
+ 0, 3, 8, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
1586
+ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
1587
+ ]);
1588
+ // Pre-computed cube vertex offsets
1589
+ const CUBE_VERTS = [
1590
+ [0, 0, 0], [1, 0, 0], [1, 1, 0], [0, 1, 0],
1591
+ [0, 0, 1], [1, 0, 1], [1, 1, 1], [0, 1, 1],
1592
+ ];
1593
+ // Pre-computed cube edge endpoints
1594
+ const CUBE_EDGES = [
1595
+ [0, 1], [1, 2], [2, 3], [3, 0],
1596
+ [4, 5], [5, 6], [6, 7], [7, 4],
1597
+ [0, 4], [1, 5], [2, 6], [3, 7],
1598
+ ];
1599
+ // EDGE_TABLE from standard MC reference
1600
+ const EDGE_TABLE = new Uint16Array([
1601
+ 0x0, 0x109, 0x203, 0x30a, 0x406, 0x50f, 0x605, 0x70c,
1602
+ 0x80c, 0x905, 0xa0f, 0xb06, 0xc0a, 0xd03, 0xe09, 0xf00,
1603
+ 0x190, 0x99, 0x393, 0x29a, 0x596, 0x49f, 0x795, 0x69c,
1604
+ 0x99c, 0x895, 0xb9f, 0xa96, 0xd9a, 0xc93, 0xf99, 0xe90,
1605
+ 0x230, 0x339, 0x33, 0x13a, 0x636, 0x73f, 0x435, 0x53c,
1606
+ 0xa3c, 0xb35, 0x83f, 0x936, 0xe3a, 0xf33, 0xc39, 0xd30,
1607
+ 0x3a0, 0x2a9, 0x1a3, 0xaa, 0x7a6, 0x6af, 0x5a5, 0x4ac,
1608
+ 0xbac, 0xaa5, 0x9af, 0x8a6, 0xfaa, 0xea3, 0xda9, 0xca0,
1609
+ 0x460, 0x569, 0x663, 0x76a, 0x66, 0x16f, 0x265, 0x36c,
1610
+ 0xc6c, 0xd65, 0xe6f, 0xf66, 0x86a, 0x963, 0xa69, 0xb60,
1611
+ 0x5f0, 0x4f9, 0x7f3, 0x6fa, 0x1f6, 0xff, 0x3f5, 0x2fc,
1612
+ 0xdfc, 0xcf5, 0xfff, 0xef6, 0x9fa, 0x8f3, 0xbf9, 0xaf0,
1613
+ 0x650, 0x759, 0x453, 0x55a, 0x256, 0x35f, 0x55, 0x15c,
1614
+ 0xe5c, 0xf55, 0xc5f, 0xd56, 0xa5a, 0xb53, 0x859, 0x950,
1615
+ 0x7c0, 0x6c9, 0x5c3, 0x4ca, 0x3c6, 0x2cf, 0x1c5, 0xcc,
1616
+ 0xfcc, 0xec5, 0xdcf, 0xcc6, 0xbca, 0xac3, 0x9c9, 0x8c0,
1617
+ 0x8c0, 0x9c9, 0xac3, 0xbca, 0xcc6, 0xdcf, 0xec5, 0xfcc,
1618
+ 0xcc, 0x1c5, 0x2cf, 0x3c6, 0x4ca, 0x5c3, 0x6c9, 0x7c0,
1619
+ 0x950, 0x859, 0xb53, 0xa5a, 0xd56, 0xc5f, 0xf55, 0xe5c,
1620
+ 0x15c, 0x55, 0x35f, 0x256, 0x55a, 0x453, 0x759, 0x650,
1621
+ 0xaf0, 0xbf9, 0x8f3, 0x9fa, 0xef6, 0xfff, 0xcf5, 0xdfc,
1622
+ 0x2fc, 0x3f5, 0xff, 0x1f6, 0x6fa, 0x7f3, 0x4f9, 0x5f0,
1623
+ 0xb60, 0xa69, 0x963, 0x86a, 0xf66, 0xe6f, 0xd65, 0xc6c,
1624
+ 0x36c, 0x265, 0x16f, 0x66, 0x76a, 0x663, 0x569, 0x460,
1625
+ 0xca0, 0xda9, 0xea3, 0xfaa, 0x8a6, 0x9af, 0xaa5, 0xbac,
1626
+ 0x4ac, 0x5a5, 0x6af, 0x7a6, 0xaa, 0x1a3, 0x2a9, 0x3a0,
1627
+ 0xd30, 0xc39, 0xf33, 0xe3a, 0x936, 0x83f, 0xb35, 0xa3c,
1628
+ 0x53c, 0x435, 0x73f, 0x636, 0x13a, 0x33, 0x339, 0x230,
1629
+ 0xe90, 0xf99, 0xc93, 0xd9a, 0xa96, 0xb9f, 0x895, 0x99c,
1630
+ 0x69c, 0x795, 0x49f, 0x596, 0x29a, 0x393, 0x99, 0x190,
1631
+ 0xf00, 0xe09, 0xd03, 0xc0a, 0xb06, 0xa0f, 0x905, 0x80c,
1632
+ 0x70c, 0x605, 0x50f, 0x406, 0x30a, 0x203, 0x109, 0x0,
1633
+ ]);
1634
+ /**
1635
+ * MarchingCubes — extracts an iso-surface from a scalar field.
1636
+ * Used to turn SPH particle density fields into a mesh.
1637
+ * Blender: Fluid > Surface.
1638
+ *
1639
+ * Usage:
1640
+ * const mc = new MarchingCubes({ resolution: 32 })
1641
+ * mc.setField(myDensityFn) // (x,y,z) => density
1642
+ * const geo = mc.extract()
1643
+ */
1644
+ class MarchingCubes {
1645
+ constructor(opts = {}) {
1646
+ this._field = null;
1647
+ this._boundsMin = [-2, -2, -2];
1648
+ this._boundsMax = [2, 2, 2];
1649
+ this.parameters = {
1650
+ resolution: Math.max(4, Math.round(opts.resolution ?? 32)),
1651
+ isoLevel: opts.isoLevel ?? 0.5,
1652
+ };
1653
+ }
1654
+ /**
1655
+ * Set the scalar field function: (x,y,z) → density.
1656
+ * For SPH, use a metaball kernel summed over nearby particles.
1657
+ */
1658
+ setField(fn) {
1659
+ this._field = fn;
1660
+ }
1661
+ /** Set world-space bounds of the sampling volume. */
1662
+ setBounds(min, max) {
1663
+ this._boundsMin = [...min];
1664
+ this._boundsMax = [...max];
1665
+ }
1666
+ /**
1667
+ * Sample the field and extract the iso-surface as a BufferGeometry.
1668
+ * Returns null if no field is set.
1669
+ */
1670
+ extract() {
1671
+ if (!this._field)
1672
+ return null;
1673
+ const { resolution, isoLevel } = this.parameters;
1674
+ const [x0, y0, z0] = this._boundsMin;
1675
+ const [x1, y1, z1] = this._boundsMax;
1676
+ const dx = (x1 - x0) / resolution;
1677
+ const dy = (y1 - y0) / resolution;
1678
+ const dz = (z1 - z0) / resolution;
1679
+ const vertices = [];
1680
+ const size = resolution + 1;
1681
+ // Sample field on a uniform grid
1682
+ const field = new Float32Array(size * size * size);
1683
+ for (let iz = 0; iz <= resolution; iz++) {
1684
+ for (let iy = 0; iy <= resolution; iy++) {
1685
+ for (let ix = 0; ix <= resolution; ix++) {
1686
+ field[iz * size * size + iy * size + ix] = this._field(x0 + ix * dx, y0 + iy * dy, z0 + iz * dz);
1687
+ }
1688
+ }
1689
+ }
1690
+ const idx = (ix, iy, iz) => field[iz * size * size + iy * size + ix];
1691
+ // Edge vertex interpolation cache
1692
+ const edgeVerts = new Array(12);
1693
+ for (let iz = 0; iz < resolution; iz++) {
1694
+ for (let iy = 0; iy < resolution; iy++) {
1695
+ for (let ix = 0; ix < resolution; ix++) {
1696
+ let cubeIdx = 0;
1697
+ for (let v = 0; v < 8; v++) {
1698
+ const [vx, vy, vz] = CUBE_VERTS[v];
1699
+ if (idx(ix + vx, iy + vy, iz + vz) < isoLevel)
1700
+ cubeIdx |= (1 << v);
1701
+ }
1702
+ if (EDGE_TABLE[cubeIdx] === 0)
1703
+ continue;
1704
+ for (let e = 0; e < 12; e++) {
1705
+ if (EDGE_TABLE[cubeIdx] & (1 << e)) {
1706
+ const [v0, v1] = CUBE_EDGES[e];
1707
+ const [ax, ay, az] = CUBE_VERTS[v0];
1708
+ const [bx, by, bz] = CUBE_VERTS[v1];
1709
+ const fa = idx(ix + ax, iy + ay, iz + az);
1710
+ const fb = idx(ix + bx, iy + by, iz + bz);
1711
+ const t = Math.abs(fa - fb) < 1e-6 ? 0.5 : (isoLevel - fa) / (fb - fa);
1712
+ edgeVerts[e] = [
1713
+ x0 + (ix + ax + (bx - ax) * t) * dx,
1714
+ y0 + (iy + ay + (by - ay) * t) * dy,
1715
+ z0 + (iz + az + (bz - az) * t) * dz,
1716
+ ];
1717
+ }
1718
+ }
1719
+ // Walk triangle table at cubeIdx*16
1720
+ const base = cubeIdx * 16;
1721
+ for (let t = 0; t < 15; t += 3) {
1722
+ const i0 = TRI_TABLE_FLAT[base + t];
1723
+ if (i0 < 0)
1724
+ break;
1725
+ const i1 = TRI_TABLE_FLAT[base + t + 1];
1726
+ const i2 = TRI_TABLE_FLAT[base + t + 2];
1727
+ const [px, py, pz] = edgeVerts[i0];
1728
+ const [qx, qy, qz] = edgeVerts[i1];
1729
+ const [rx, ry, rz] = edgeVerts[i2];
1730
+ vertices.push(px, py, pz, qx, qy, qz, rx, ry, rz);
1731
+ }
1732
+ }
1733
+ }
1734
+ }
1735
+ const geo = new BufferGeometry();
1736
+ geo.setAttribute('position', new BufferAttribute(new Float32Array(vertices), 3));
1737
+ geo.computeVertexNormals();
1738
+ return geo;
1739
+ }
1740
+ }
1741
+
1742
+ export { FLIPSimulator, FluidEmitter, MACGrid, MarchingCubes, SPHSimulator };