@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/FLIPSimulator.d.ts +98 -0
- package/dist/FLIPSimulator.d.ts.map +1 -0
- package/dist/FLIPSimulator.js +404 -0
- package/dist/FLIPSimulator.js.map +1 -0
- package/dist/FluidEmitter.d.ts +35 -0
- package/dist/FluidEmitter.d.ts.map +1 -0
- package/dist/FluidEmitter.js +35 -0
- package/dist/FluidEmitter.js.map +1 -0
- package/dist/MACGrid.d.ts +58 -0
- package/dist/MACGrid.d.ts.map +1 -0
- package/dist/MACGrid.js +154 -0
- package/dist/MACGrid.js.map +1 -0
- package/dist/MarchingCubes.d.ts +40 -0
- package/dist/MarchingCubes.d.ts.map +1 -0
- package/dist/MarchingCubes.js +416 -0
- package/dist/MarchingCubes.js.map +1 -0
- package/dist/SPHSimulator.d.ts +139 -0
- package/dist/SPHSimulator.d.ts.map +1 -0
- package/dist/SPHSimulator.js +393 -0
- package/dist/SPHSimulator.js.map +1 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1742 -0
- package/dist/index.js.map +1 -0
- package/package.json +51 -0
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 };
|