@cazala/party 0.1.1 → 0.2.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
CHANGED
|
@@ -2366,7 +2366,35 @@ fn grid_cell_index(pos: vec2<f32>) -> u32 { let col = i32(floor((pos.x - GRID_MI
|
|
|
2366
2366
|
fn grid_cell_index_from_rc(r: i32, c: i32) -> u32 { let rr = max(0, min(r, i32(GRID_ROWS()) - 1)); let cc = max(0, min(c, i32(GRID_COLS()) - 1)); return u32(rr) * GRID_COLS() + u32(cc); }
|
|
2367
2367
|
struct NeighborIter { cx: i32, cy: i32, r: i32, c: i32, k: u32, reach: i32, maxK: u32, base: u32, emitted: u32, maxEmit: u32 }
|
|
2368
2368
|
fn neighbor_iter_init(pos: vec2<f32>, radius: f32) -> NeighborIter { let cx = i32(floor((pos.x - GRID_MINX()) / GRID_CELL_SIZE())); let cy = i32(floor((pos.y - GRID_MINY()) / GRID_CELL_SIZE())); let reach = max(1, i32(ceil(radius / GRID_CELL_SIZE()))); var it: NeighborIter; it.cx = cx; it.cy = cy; it.reach = reach; it.r = cy - reach; it.c = cx - reach; let firstCell = grid_cell_index_from_rc(it.r, it.c); let cnt = atomicLoad(&GRID_COUNTS[firstCell]); it.maxK = min(cnt, GRID_MAX_PER_CELL()); it.base = firstCell * GRID_MAX_PER_CELL(); it.k = 0u; it.emitted = 0u; it.maxEmit = max(1u, SIM_MAX_NEIGHBORS()); return it; }
|
|
2369
|
-
fn neighbor_iter_next(it: ptr<function, NeighborIter>, selfIndex: u32) -> u32 {
|
|
2369
|
+
fn neighbor_iter_next(it: ptr<function, NeighborIter>, selfIndex: u32) -> u32 {
|
|
2370
|
+
// Naga (Firefox) sometimes fails to prove that a loop always returns, so
|
|
2371
|
+
// we structure this as break + return result to keep validation happy.
|
|
2372
|
+
var result: u32 = NEIGHBOR_NONE;
|
|
2373
|
+
loop {
|
|
2374
|
+
if ((*it).emitted >= (*it).maxEmit) { break; }
|
|
2375
|
+
if ((*it).r > (*it).cy + (*it).reach) { break; }
|
|
2376
|
+
if ((*it).k < (*it).maxK) {
|
|
2377
|
+
let id = GRID_INDICES[(*it).base + (*it).k];
|
|
2378
|
+
(*it).k = (*it).k + 1u;
|
|
2379
|
+
if (id != selfIndex) {
|
|
2380
|
+
(*it).emitted = (*it).emitted + 1u;
|
|
2381
|
+
result = id;
|
|
2382
|
+
break;
|
|
2383
|
+
} else {
|
|
2384
|
+
continue;
|
|
2385
|
+
}
|
|
2386
|
+
}
|
|
2387
|
+
(*it).c = (*it).c + 1;
|
|
2388
|
+
if ((*it).c > (*it).cx + (*it).reach) { (*it).c = (*it).cx - (*it).reach; (*it).r = (*it).r + 1; }
|
|
2389
|
+
if ((*it).r > (*it).cy + (*it).reach) { break; }
|
|
2390
|
+
let cell = grid_cell_index_from_rc((*it).r, (*it).c);
|
|
2391
|
+
let cnt = atomicLoad(&GRID_COUNTS[cell]);
|
|
2392
|
+
(*it).maxK = min(cnt, GRID_MAX_PER_CELL());
|
|
2393
|
+
(*it).base = cell * GRID_MAX_PER_CELL();
|
|
2394
|
+
(*it).k = 0u;
|
|
2395
|
+
}
|
|
2396
|
+
return result;
|
|
2397
|
+
}`);
|
|
2370
2398
|
// Optional: allow force modules to inject globals
|
|
2371
2399
|
const addGlobal = (module, descriptor) => {
|
|
2372
2400
|
if (module.role !== ModuleRole.Force)
|
|
@@ -3566,7 +3594,12 @@ class WebGPUEngine extends AbstractEngine {
|
|
|
3566
3594
|
this.registry.initialize(this.resources);
|
|
3567
3595
|
const program = this.registry.getProgram();
|
|
3568
3596
|
if (program.extraBindings.simState) {
|
|
3569
|
-
|
|
3597
|
+
// SIM_STATE is a per-particle float array used by integration and force-module state.
|
|
3598
|
+
// Its stride is program-dependent (base 4 + per-module state slots), so we must
|
|
3599
|
+
// allocate using the generated program's stride. Allocating a fixed 4-float stride
|
|
3600
|
+
// causes out-of-bounds reads/writes for particles beyond a threshold (e.g. ~40k),
|
|
3601
|
+
// resulting in "exploding" velocities/positions.
|
|
3602
|
+
this.resources.createSimStateBuffer(this.bufferMaxParticles, program.simStateStride);
|
|
3570
3603
|
}
|
|
3571
3604
|
// Build compute pipelines
|
|
3572
3605
|
this.sim.initialize(this.resources, program);
|
|
@@ -6241,14 +6274,30 @@ class Collisions extends Module {
|
|
|
6241
6274
|
* - apply(): compute pressure gradient and viscosity forces; clamp max accel
|
|
6242
6275
|
* Stores per-particle state in shared SIM_STATE via the Program-provided helpers.
|
|
6243
6276
|
*/
|
|
6277
|
+
var FluidsMethod;
|
|
6278
|
+
(function (FluidsMethod) {
|
|
6279
|
+
FluidsMethod["Sph"] = "sph";
|
|
6280
|
+
FluidsMethod["Picflip"] = "picflip";
|
|
6281
|
+
})(FluidsMethod || (FluidsMethod = {}));
|
|
6244
6282
|
const DEFAULT_FLUIDS_INFLUENCE_RADIUS = 100;
|
|
6245
|
-
const DEFAULT_FLUIDS_TARGET_DENSITY =
|
|
6283
|
+
const DEFAULT_FLUIDS_TARGET_DENSITY = 2;
|
|
6246
6284
|
const DEFAULT_FLUIDS_PRESSURE_MULTIPLIER = 30;
|
|
6247
6285
|
const DEFAULT_FLUIDS_VISCOSITY = 1;
|
|
6248
6286
|
const DEFAULT_FLUIDS_NEAR_PRESSURE_MULTIPLIER = 50;
|
|
6249
6287
|
const DEFAULT_FLUIDS_NEAR_THRESHOLD = 20;
|
|
6250
6288
|
const DEFAULT_FLUIDS_ENABLE_NEAR_PRESSURE = true;
|
|
6251
6289
|
const DEFAULT_FLUIDS_MAX_ACCELERATION = 75;
|
|
6290
|
+
// Default values for PIC/FLIP parameters (exposed via Fluids when method=picflip)
|
|
6291
|
+
const DEFAULT_PICFLIP_FLIP_RATIO = 0.9;
|
|
6292
|
+
const DEFAULT_PICFLIP_TARGET_DENSITY = 2.0;
|
|
6293
|
+
const DEFAULT_PICFLIP_INFLUENCE_RADIUS = 50.0;
|
|
6294
|
+
const DEFAULT_PICFLIP_PRESSURE_MULTIPLIER = 500.0;
|
|
6295
|
+
const DEFAULT_PICFLIP_INTERNAL_MAX_ACCELERATION = 20000.0;
|
|
6296
|
+
// Backwards-compatible aliases for previous standalone Picflip module constants.
|
|
6297
|
+
// (The recommended names are the *_TARGET_DENSITY / *_INFLUENCE_RADIUS / *_PRESSURE_MULTIPLIER ones.)
|
|
6298
|
+
const DEFAULT_PICFLIP_DENSITY = DEFAULT_PICFLIP_TARGET_DENSITY;
|
|
6299
|
+
const DEFAULT_PICFLIP_RADIUS = DEFAULT_PICFLIP_INFLUENCE_RADIUS;
|
|
6300
|
+
const DEFAULT_PICFLIP_PRESSURE = DEFAULT_PICFLIP_PRESSURE_MULTIPLIER;
|
|
6252
6301
|
class Fluids extends Module {
|
|
6253
6302
|
constructor(opts) {
|
|
6254
6303
|
super();
|
|
@@ -6269,6 +6318,7 @@ class Fluids extends Module {
|
|
|
6269
6318
|
configurable: true,
|
|
6270
6319
|
writable: true,
|
|
6271
6320
|
value: {
|
|
6321
|
+
method: DataType.NUMBER,
|
|
6272
6322
|
influenceRadius: DataType.NUMBER,
|
|
6273
6323
|
targetDensity: DataType.NUMBER,
|
|
6274
6324
|
pressureMultiplier: DataType.NUMBER,
|
|
@@ -6277,22 +6327,56 @@ class Fluids extends Module {
|
|
|
6277
6327
|
nearThreshold: DataType.NUMBER,
|
|
6278
6328
|
enableNearPressure: DataType.NUMBER,
|
|
6279
6329
|
maxAcceleration: DataType.NUMBER,
|
|
6330
|
+
flipRatio: DataType.NUMBER,
|
|
6280
6331
|
}
|
|
6281
6332
|
});
|
|
6282
|
-
|
|
6283
|
-
|
|
6284
|
-
|
|
6285
|
-
|
|
6286
|
-
|
|
6287
|
-
|
|
6288
|
-
|
|
6289
|
-
|
|
6290
|
-
|
|
6291
|
-
|
|
6333
|
+
const method = opts?.method ?? FluidsMethod.Sph;
|
|
6334
|
+
const isPicflip = method === FluidsMethod.Picflip;
|
|
6335
|
+
if (isPicflip) {
|
|
6336
|
+
const o = opts;
|
|
6337
|
+
this.write({
|
|
6338
|
+
method: 1,
|
|
6339
|
+
influenceRadius: o?.influenceRadius ?? DEFAULT_PICFLIP_INFLUENCE_RADIUS,
|
|
6340
|
+
targetDensity: o?.targetDensity ?? DEFAULT_PICFLIP_TARGET_DENSITY,
|
|
6341
|
+
pressureMultiplier: o?.pressureMultiplier ?? DEFAULT_PICFLIP_PRESSURE_MULTIPLIER,
|
|
6342
|
+
// SPH-only; keep initialized but unused for PICFLIP
|
|
6343
|
+
viscosity: 0,
|
|
6344
|
+
// Keep SPH-only uniforms initialized (unused when method=picflip)
|
|
6345
|
+
nearPressureMultiplier: DEFAULT_FLUIDS_NEAR_PRESSURE_MULTIPLIER,
|
|
6346
|
+
nearThreshold: DEFAULT_FLUIDS_NEAR_THRESHOLD,
|
|
6347
|
+
enableNearPressure: 1 ,
|
|
6348
|
+
maxAcceleration: DEFAULT_FLUIDS_MAX_ACCELERATION,
|
|
6349
|
+
flipRatio: o?.flipRatio ?? DEFAULT_PICFLIP_FLIP_RATIO,
|
|
6350
|
+
});
|
|
6351
|
+
}
|
|
6352
|
+
else {
|
|
6353
|
+
const o = opts;
|
|
6354
|
+
this.write({
|
|
6355
|
+
method: 0,
|
|
6356
|
+
influenceRadius: o?.influenceRadius ?? DEFAULT_FLUIDS_INFLUENCE_RADIUS,
|
|
6357
|
+
targetDensity: o?.targetDensity ?? DEFAULT_FLUIDS_TARGET_DENSITY,
|
|
6358
|
+
pressureMultiplier: o?.pressureMultiplier ?? DEFAULT_FLUIDS_PRESSURE_MULTIPLIER,
|
|
6359
|
+
viscosity: o?.viscosity ?? DEFAULT_FLUIDS_VISCOSITY,
|
|
6360
|
+
nearPressureMultiplier: o?.nearPressureMultiplier ?? DEFAULT_FLUIDS_NEAR_PRESSURE_MULTIPLIER,
|
|
6361
|
+
nearThreshold: o?.nearThreshold ?? DEFAULT_FLUIDS_NEAR_THRESHOLD,
|
|
6362
|
+
enableNearPressure: o?.enableNearPressure ?? DEFAULT_FLUIDS_ENABLE_NEAR_PRESSURE ? 1 : 0,
|
|
6363
|
+
maxAcceleration: o?.maxAcceleration ?? DEFAULT_FLUIDS_MAX_ACCELERATION,
|
|
6364
|
+
// Keep PICFLIP-only uniform initialized (unused when method=sph)
|
|
6365
|
+
flipRatio: DEFAULT_PICFLIP_FLIP_RATIO,
|
|
6366
|
+
});
|
|
6367
|
+
}
|
|
6292
6368
|
if (opts?.enabled !== undefined) {
|
|
6293
6369
|
this.setEnabled(!!opts.enabled);
|
|
6294
6370
|
}
|
|
6295
6371
|
}
|
|
6372
|
+
setMethod(method) {
|
|
6373
|
+
this.write({ method: method === FluidsMethod.Picflip ? 1 : 0 });
|
|
6374
|
+
}
|
|
6375
|
+
getMethod() {
|
|
6376
|
+
return this.readValue("method") >= 0.5
|
|
6377
|
+
? FluidsMethod.Picflip
|
|
6378
|
+
: FluidsMethod.Sph;
|
|
6379
|
+
}
|
|
6296
6380
|
setInfluenceRadius(v) {
|
|
6297
6381
|
this.write({ influenceRadius: v });
|
|
6298
6382
|
}
|
|
@@ -6305,6 +6389,9 @@ class Fluids extends Module {
|
|
|
6305
6389
|
setViscosity(v) {
|
|
6306
6390
|
this.write({ viscosity: v });
|
|
6307
6391
|
}
|
|
6392
|
+
setFlipRatio(v) {
|
|
6393
|
+
this.write({ flipRatio: v });
|
|
6394
|
+
}
|
|
6308
6395
|
setNearPressureMultiplier(v) {
|
|
6309
6396
|
this.write({ nearPressureMultiplier: v });
|
|
6310
6397
|
}
|
|
@@ -6329,6 +6416,9 @@ class Fluids extends Module {
|
|
|
6329
6416
|
getViscosity() {
|
|
6330
6417
|
return this.readValue("viscosity");
|
|
6331
6418
|
}
|
|
6419
|
+
getFlipRatio() {
|
|
6420
|
+
return this.readValue("flipRatio");
|
|
6421
|
+
}
|
|
6332
6422
|
getNearPressureMultiplier() {
|
|
6333
6423
|
return this.readValue("nearPressureMultiplier");
|
|
6334
6424
|
}
|
|
@@ -6343,111 +6433,94 @@ class Fluids extends Module {
|
|
|
6343
6433
|
}
|
|
6344
6434
|
webgpu() {
|
|
6345
6435
|
return {
|
|
6346
|
-
states: ["density", "nearDensity"],
|
|
6347
|
-
// State pass:
|
|
6436
|
+
states: ["density", "nearDensity", "prevVelX", "prevVelY"],
|
|
6437
|
+
// State pass:
|
|
6438
|
+
// - SPH: precompute density and near-density per particle
|
|
6439
|
+
// - PICFLIP: store previous velocity per particle (for FLIP delta)
|
|
6348
6440
|
state: ({ particleVar, dtVar, getUniform, setState }) => `{
|
|
6349
|
-
|
|
6350
|
-
let rad = ${getUniform("influenceRadius")};
|
|
6351
|
-
let posPred = ${particleVar}.position + ${particleVar}.velocity * (${dtVar});
|
|
6352
|
-
var density: f32 = 0.0;
|
|
6353
|
-
var nearDensity: f32 = 0.0;
|
|
6441
|
+
let method = ${getUniform("method")};
|
|
6354
6442
|
|
|
6355
|
-
|
|
6356
|
-
|
|
6357
|
-
|
|
6358
|
-
|
|
6359
|
-
|
|
6443
|
+
if (method < 0.5) {
|
|
6444
|
+
// SPH: Predict current particle position for this frame (approximate)
|
|
6445
|
+
let rad = ${getUniform("influenceRadius")};
|
|
6446
|
+
let posPred = ${particleVar}.position + ${particleVar}.velocity * (${dtVar});
|
|
6447
|
+
var density: f32 = 0.0;
|
|
6448
|
+
var nearDensity: f32 = 0.0;
|
|
6360
6449
|
|
|
6361
|
-
|
|
6362
|
-
|
|
6363
|
-
|
|
6364
|
-
let
|
|
6365
|
-
|
|
6366
|
-
let other = particles[j];
|
|
6367
|
-
// Skip removed or pinned neighbors
|
|
6368
|
-
if (other.mass <= 0.0) { continue; }
|
|
6450
|
+
// Precompute radius powers for kernels
|
|
6451
|
+
let r2 = rad * rad;
|
|
6452
|
+
let r4 = r2 * r2;
|
|
6453
|
+
let r6 = r4 * r2;
|
|
6454
|
+
let r8 = r4 * r4;
|
|
6369
6455
|
|
|
6370
|
-
|
|
6371
|
-
|
|
6372
|
-
|
|
6373
|
-
|
|
6374
|
-
|
|
6375
|
-
|
|
6456
|
+
// Iterate neighbors using the spatial grid
|
|
6457
|
+
var it = neighbor_iter_init(posPred, rad);
|
|
6458
|
+
loop {
|
|
6459
|
+
let j = neighbor_iter_next(&it, index);
|
|
6460
|
+
if (j == NEIGHBOR_NONE) { break; }
|
|
6461
|
+
let other = particles[j];
|
|
6462
|
+
// Skip removed or pinned neighbors
|
|
6463
|
+
if (other.mass <= 0.0) { continue; }
|
|
6376
6464
|
|
|
6377
|
-
|
|
6378
|
-
|
|
6379
|
-
|
|
6465
|
+
let d = posPred - other.position;
|
|
6466
|
+
let dist2 = dot(d, d);
|
|
6467
|
+
if (dist2 <= 0.0) { continue; }
|
|
6468
|
+
let dist = sqrt(dist2);
|
|
6469
|
+
let factor = max(rad - dist, 0.0);
|
|
6470
|
+
if (factor <= 0.0) { continue; }
|
|
6380
6471
|
|
|
6381
|
-
|
|
6382
|
-
|
|
6383
|
-
|
|
6384
|
-
|
|
6385
|
-
|
|
6472
|
+
// Density kernel (CPU: (factor^2) / (pi/6 * r^4))
|
|
6473
|
+
let kDensity = (factor * factor) / ((3.14159265 / 6.0) * r4);
|
|
6474
|
+
density = density + kDensity * 1000.0 * other.mass;
|
|
6475
|
+
|
|
6476
|
+
// Near-density kernel (CPU: (factor^4) / (pi/15 * r^6))
|
|
6477
|
+
if (${getUniform("enableNearPressure")} != 0.0) {
|
|
6478
|
+
let f2 = factor * factor;
|
|
6479
|
+
let kNear = (f2 * f2) / ((3.14159265 / 15.0) * r6);
|
|
6480
|
+
nearDensity = nearDensity + kNear * 1000.0 * other.mass;
|
|
6481
|
+
}
|
|
6386
6482
|
}
|
|
6387
|
-
}
|
|
6388
6483
|
|
|
6389
|
-
|
|
6390
|
-
|
|
6391
|
-
|
|
6484
|
+
// Store results in shared SIM_STATE for use in apply pass
|
|
6485
|
+
${setState("density", "density")};
|
|
6486
|
+
${setState("nearDensity", "nearDensity")};
|
|
6487
|
+
} else {
|
|
6488
|
+
// PICFLIP: Store current velocity before blend/pressure
|
|
6489
|
+
${setState("prevVelX", `${particleVar}.velocity.x`)};
|
|
6490
|
+
${setState("prevVelY", `${particleVar}.velocity.y`)};
|
|
6491
|
+
}
|
|
6392
6492
|
}`,
|
|
6393
|
-
// Apply pass:
|
|
6394
|
-
|
|
6395
|
-
|
|
6396
|
-
|
|
6397
|
-
let
|
|
6398
|
-
let visc = ${getUniform("viscosity")};
|
|
6399
|
-
let nearMul = ${getUniform("nearPressureMultiplier")};
|
|
6400
|
-
let nearThreshold = ${getUniform("nearThreshold")};
|
|
6401
|
-
let useNear = ${getUniform("enableNearPressure")};
|
|
6402
|
-
|
|
6403
|
-
let myDensity = max(${getState("density")}, 1e-6);
|
|
6404
|
-
|
|
6405
|
-
// Precompute radius powers for kernels
|
|
6406
|
-
let r2 = rad * rad;
|
|
6407
|
-
let r4 = r2 * r2;
|
|
6408
|
-
let r6 = r4 * r2;
|
|
6409
|
-
let r8 = r4 * r4;
|
|
6410
|
-
|
|
6411
|
-
// Pressure gradient accumulation
|
|
6412
|
-
var gradSum: vec2<f32> = vec2<f32>(0.0, 0.0);
|
|
6413
|
-
var it1 = neighbor_iter_init(${particleVar}.position, rad);
|
|
6414
|
-
loop {
|
|
6415
|
-
let j = neighbor_iter_next(&it1, index);
|
|
6416
|
-
if (j == NEIGHBOR_NONE) { break; }
|
|
6417
|
-
let other = particles[j];
|
|
6418
|
-
// Skip removed or pinned neighbors
|
|
6419
|
-
if (other.mass <= 0.0) { continue; }
|
|
6420
|
-
let delta = other.position - ${particleVar}.position;
|
|
6421
|
-
let dist2 = dot(delta, delta);
|
|
6422
|
-
if (dist2 <= 0.0) { continue; }
|
|
6423
|
-
let dist = sqrt(dist2);
|
|
6424
|
-
if (dist <= 0.0 || dist >= rad) { continue; }
|
|
6425
|
-
let dir = delta / dist;
|
|
6493
|
+
// Apply pass:
|
|
6494
|
+
// - SPH: pressure + viscosity using precomputed densities
|
|
6495
|
+
// - PICFLIP: simplified local-pressure PIC/FLIP blend (+ optional viscosity)
|
|
6496
|
+
apply: ({ particleVar, dtVar, getUniform, getState }) => `{
|
|
6497
|
+
let method = ${getUniform("method")};
|
|
6426
6498
|
|
|
6427
|
-
|
|
6428
|
-
|
|
6429
|
-
|
|
6499
|
+
if (method < 0.5) {
|
|
6500
|
+
// ==========================
|
|
6501
|
+
// SPH
|
|
6502
|
+
// ==========================
|
|
6503
|
+
let rad = ${getUniform("influenceRadius")};
|
|
6504
|
+
let targetDensity = ${getUniform("targetDensity")};
|
|
6505
|
+
let pressureMul = ${getUniform("pressureMultiplier")};
|
|
6506
|
+
let visc = ${getUniform("viscosity")};
|
|
6507
|
+
let nearMul = ${getUniform("nearPressureMultiplier")};
|
|
6508
|
+
let nearThreshold = ${getUniform("nearThreshold")};
|
|
6509
|
+
let useNear = ${getUniform("enableNearPressure")};
|
|
6430
6510
|
|
|
6431
|
-
|
|
6432
|
-
let dN = max(${getState("density", "j")}, 1e-6);
|
|
6433
|
-
let nearN = select(0.0, ${getState("nearDensity", "j")}, useNear != 0.0);
|
|
6434
|
-
let densityDiff = dN - targetDensity;
|
|
6435
|
-
let pressure = densityDiff * pressureMul;
|
|
6436
|
-
let nearPressure = nearN * nearMul;
|
|
6437
|
-
let effectivePressure = select(pressure, nearPressure, dist < nearThreshold);
|
|
6511
|
+
let myDensity = max(${getState("density")}, 1e-6);
|
|
6438
6512
|
|
|
6439
|
-
//
|
|
6440
|
-
|
|
6441
|
-
|
|
6442
|
-
|
|
6443
|
-
|
|
6513
|
+
// Precompute radius powers for kernels
|
|
6514
|
+
let r2 = rad * rad;
|
|
6515
|
+
let r4 = r2 * r2;
|
|
6516
|
+
let r6 = r4 * r2;
|
|
6517
|
+
let r8 = r4 * r4;
|
|
6444
6518
|
|
|
6445
|
-
|
|
6446
|
-
|
|
6447
|
-
|
|
6448
|
-
var it2 = neighbor_iter_init(${particleVar}.position, rad);
|
|
6519
|
+
// Pressure gradient accumulation
|
|
6520
|
+
var gradSum: vec2<f32> = vec2<f32>(0.0, 0.0);
|
|
6521
|
+
var it1 = neighbor_iter_init(${particleVar}.position, rad);
|
|
6449
6522
|
loop {
|
|
6450
|
-
let j = neighbor_iter_next(&
|
|
6523
|
+
let j = neighbor_iter_next(&it1, index);
|
|
6451
6524
|
if (j == NEIGHBOR_NONE) { break; }
|
|
6452
6525
|
let other = particles[j];
|
|
6453
6526
|
// Skip removed or pinned neighbors
|
|
@@ -6456,141 +6529,249 @@ class Fluids extends Module {
|
|
|
6456
6529
|
let dist2 = dot(delta, delta);
|
|
6457
6530
|
if (dist2 <= 0.0) { continue; }
|
|
6458
6531
|
let dist = sqrt(dist2);
|
|
6459
|
-
if (dist >= rad) { continue; }
|
|
6460
|
-
|
|
6461
|
-
|
|
6462
|
-
|
|
6463
|
-
|
|
6532
|
+
if (dist <= 0.0 || dist >= rad) { continue; }
|
|
6533
|
+
let dir = delta / dist;
|
|
6534
|
+
|
|
6535
|
+
// Derivative kernel (CPU: scale * (dist - rad), scale = (-12/pi)/r^4)
|
|
6536
|
+
let scale = (-12.0 / 3.14159265) / r4;
|
|
6537
|
+
let slope = (dist - rad) * scale;
|
|
6538
|
+
|
|
6539
|
+
// Neighbor pressures from precomputed densities
|
|
6540
|
+
let dN = max(${getState("density", "j")}, 1e-6);
|
|
6541
|
+
let nearN = select(0.0, ${getState("nearDensity", "j")}, useNear != 0.0);
|
|
6542
|
+
let densityDiff = dN - targetDensity;
|
|
6543
|
+
let pressure = densityDiff * pressureMul;
|
|
6544
|
+
let nearPressure = nearN * nearMul;
|
|
6545
|
+
let effectivePressure = select(pressure, nearPressure, dist < nearThreshold);
|
|
6546
|
+
|
|
6547
|
+
// Gradient contribution
|
|
6548
|
+
gradSum = gradSum + (dir * (effectivePressure * slope) / dN);
|
|
6464
6549
|
}
|
|
6465
|
-
|
|
6466
|
-
|
|
6550
|
+
// Pressure force is negative gradient
|
|
6551
|
+
var pressureForce = -gradSum;
|
|
6467
6552
|
|
|
6468
|
-
|
|
6469
|
-
|
|
6470
|
-
|
|
6471
|
-
|
|
6472
|
-
|
|
6553
|
+
// Viscosity accumulation
|
|
6554
|
+
var viscosityForce: vec2<f32> = vec2<f32>(0.0, 0.0);
|
|
6555
|
+
if (visc != 0.0) {
|
|
6556
|
+
var it2 = neighbor_iter_init(${particleVar}.position, rad);
|
|
6557
|
+
loop {
|
|
6558
|
+
let j = neighbor_iter_next(&it2, index);
|
|
6559
|
+
if (j == NEIGHBOR_NONE) { break; }
|
|
6560
|
+
let other = particles[j];
|
|
6561
|
+
// Skip removed or pinned neighbors
|
|
6562
|
+
if (other.mass <= 0.0) { continue; }
|
|
6563
|
+
let delta = other.position - ${particleVar}.position;
|
|
6564
|
+
let dist2 = dot(delta, delta);
|
|
6565
|
+
if (dist2 <= 0.0) { continue; }
|
|
6566
|
+
let dist = sqrt(dist2);
|
|
6567
|
+
if (dist >= rad) { continue; }
|
|
6568
|
+
// Viscosity kernel (CPU: (max(0, r^2 - d^2)^3) / (pi/4 * r^8))
|
|
6569
|
+
let val = max(0.0, r2 - dist2);
|
|
6570
|
+
let kVisc = (val * val * val) / ((3.14159265 / 4.0) * r8);
|
|
6571
|
+
viscosityForce = viscosityForce + (other.velocity - ${particleVar}.velocity) * kVisc;
|
|
6572
|
+
}
|
|
6573
|
+
viscosityForce = viscosityForce * visc;
|
|
6574
|
+
}
|
|
6473
6575
|
|
|
6474
|
-
|
|
6475
|
-
|
|
6476
|
-
|
|
6477
|
-
|
|
6478
|
-
|
|
6479
|
-
|
|
6480
|
-
|
|
6576
|
+
// Convert to acceleration-like effect: a = F / density
|
|
6577
|
+
var force = (pressureForce / myDensity) * 1000000.0;
|
|
6578
|
+
if (visc != 0.0) {
|
|
6579
|
+
force = force + (viscosityForce * 1000.0) / myDensity;
|
|
6580
|
+
}
|
|
6581
|
+
|
|
6582
|
+
// Clamp force magnitude to avoid instabilities (tunable)
|
|
6583
|
+
let maxLen = ${getUniform("maxAcceleration")};
|
|
6584
|
+
let f2 = dot(force, force);
|
|
6585
|
+
if (f2 > maxLen * maxLen) {
|
|
6586
|
+
let fLen = sqrt(f2);
|
|
6587
|
+
force = force * (maxLen / fLen);
|
|
6588
|
+
}
|
|
6589
|
+
|
|
6590
|
+
// Apply directly to velocity (CPU mirrors this behavior)
|
|
6591
|
+
${particleVar}.velocity = ${particleVar}.velocity + force;
|
|
6592
|
+
} else {
|
|
6593
|
+
// ==========================
|
|
6594
|
+
// PICFLIP (simplified local-pressure approximation)
|
|
6595
|
+
// ==========================
|
|
6596
|
+
// Scale UI-friendly values to PIC/FLIP internal tuning:
|
|
6597
|
+
// - influenceRadius: /2
|
|
6598
|
+
// - targetDensity: *3
|
|
6599
|
+
// - pressureMultiplier: *30
|
|
6600
|
+
let flipRatio = ${getUniform("flipRatio")};
|
|
6601
|
+
let rad = ${getUniform("influenceRadius")} * 0.5;
|
|
6602
|
+
let targetDensity = ${getUniform("targetDensity")} * 3.0;
|
|
6603
|
+
let pressureScale = ${getUniform("pressureMultiplier")} * 30.0;
|
|
6604
|
+
|
|
6605
|
+
// Get stored previous velocity
|
|
6606
|
+
let prevVelX = ${getState("prevVelX")};
|
|
6607
|
+
let prevVelY = ${getState("prevVelY")};
|
|
6608
|
+
|
|
6609
|
+
var newVelX = ${particleVar}.velocity.x;
|
|
6610
|
+
var newVelY = ${particleVar}.velocity.y;
|
|
6611
|
+
|
|
6612
|
+
// Local density + average neighbor velocity (PIC grid approximation)
|
|
6613
|
+
var density: f32 = 0.0;
|
|
6614
|
+
var avgVelX: f32 = 0.0;
|
|
6615
|
+
var avgVelY: f32 = 0.0;
|
|
6616
|
+
var count: f32 = 0.0;
|
|
6617
|
+
|
|
6618
|
+
var it = neighbor_iter_init(${particleVar}.position, rad);
|
|
6619
|
+
loop {
|
|
6620
|
+
let j = neighbor_iter_next(&it, index);
|
|
6621
|
+
if (j == NEIGHBOR_NONE) { break; }
|
|
6622
|
+
let other = particles[j];
|
|
6623
|
+
if (other.mass <= 0.0) { continue; }
|
|
6624
|
+
|
|
6625
|
+
let d = ${particleVar}.position - other.position;
|
|
6626
|
+
let dist2 = dot(d, d);
|
|
6627
|
+
if (dist2 <= 0.0 || dist2 > rad * rad) { continue; }
|
|
6628
|
+
|
|
6629
|
+
let dist = sqrt(dist2);
|
|
6630
|
+
let weight = 1.0 - dist / rad;
|
|
6631
|
+
|
|
6632
|
+
density = density + weight;
|
|
6633
|
+
avgVelX = avgVelX + other.velocity.x * weight;
|
|
6634
|
+
avgVelY = avgVelY + other.velocity.y * weight;
|
|
6635
|
+
count = count + weight;
|
|
6636
|
+
}
|
|
6637
|
+
|
|
6638
|
+
if (count > 0.0) {
|
|
6639
|
+
avgVelX = avgVelX / count;
|
|
6640
|
+
avgVelY = avgVelY / count;
|
|
6641
|
+
|
|
6642
|
+
// PIC/FLIP blend
|
|
6643
|
+
let picVelX = avgVelX;
|
|
6644
|
+
let picVelY = avgVelY;
|
|
6645
|
+
let flipVelX = newVelX + (avgVelX - prevVelX);
|
|
6646
|
+
let flipVelY = newVelY + (avgVelY - prevVelY);
|
|
6647
|
+
|
|
6648
|
+
newVelX = mix(picVelX, flipVelX, flipRatio);
|
|
6649
|
+
newVelY = mix(picVelY, flipVelY, flipRatio);
|
|
6650
|
+
|
|
6651
|
+
// Simple pressure force based on local density
|
|
6652
|
+
let maxPressureFactor = abs(pressureScale) * 10.0;
|
|
6653
|
+
let pressureFactor = clamp(
|
|
6654
|
+
(density - targetDensity) * pressureScale,
|
|
6655
|
+
-maxPressureFactor,
|
|
6656
|
+
maxPressureFactor
|
|
6657
|
+
);
|
|
6658
|
+
|
|
6659
|
+
// Apply pressure gradient (push away from high density regions)
|
|
6660
|
+
var gradX: f32 = 0.0;
|
|
6661
|
+
var gradY: f32 = 0.0;
|
|
6662
|
+
|
|
6663
|
+
var it2 = neighbor_iter_init(${particleVar}.position, rad);
|
|
6664
|
+
loop {
|
|
6665
|
+
let j = neighbor_iter_next(&it2, index);
|
|
6666
|
+
if (j == NEIGHBOR_NONE) { break; }
|
|
6667
|
+
let other = particles[j];
|
|
6668
|
+
if (other.mass <= 0.0) { continue; }
|
|
6481
6669
|
|
|
6482
|
-
|
|
6483
|
-
|
|
6670
|
+
let d = ${particleVar}.position - other.position;
|
|
6671
|
+
let dist2 = dot(d, d);
|
|
6672
|
+
if (dist2 <= 1.0 || dist2 > rad * rad) { continue; }
|
|
6673
|
+
|
|
6674
|
+
let dist = sqrt(dist2);
|
|
6675
|
+
let dir = d / dist;
|
|
6676
|
+
let weight = 1.0 - dist / rad;
|
|
6677
|
+
|
|
6678
|
+
gradX = gradX + dir.x * weight * pressureFactor;
|
|
6679
|
+
gradY = gradY + dir.y * weight * pressureFactor;
|
|
6680
|
+
}
|
|
6681
|
+
|
|
6682
|
+
// NOTE: no max-acceleration clamp for PIC/FLIP (it tends to need the max anyway)
|
|
6683
|
+
newVelX = newVelX + gradX * ${dtVar};
|
|
6684
|
+
newVelY = newVelY + gradY * ${dtVar};
|
|
6685
|
+
}
|
|
6686
|
+
|
|
6687
|
+
${particleVar}.velocity.x = newVelX;
|
|
6688
|
+
${particleVar}.velocity.y = newVelY;
|
|
6689
|
+
}
|
|
6484
6690
|
}`,
|
|
6485
6691
|
};
|
|
6486
6692
|
}
|
|
6487
6693
|
cpu() {
|
|
6488
6694
|
return {
|
|
6489
|
-
states: ["density", "nearDensity"],
|
|
6490
|
-
// State pass:
|
|
6695
|
+
states: ["density", "nearDensity", "prevVelX", "prevVelY"],
|
|
6696
|
+
// State pass:
|
|
6697
|
+
// - SPH: precompute density and near-density per particle
|
|
6698
|
+
// - PICFLIP: store previous velocity per particle
|
|
6491
6699
|
state: ({ particle, getNeighbors, dt, setState }) => {
|
|
6492
|
-
|
|
6493
|
-
|
|
6494
|
-
|
|
6495
|
-
|
|
6496
|
-
|
|
6497
|
-
|
|
6498
|
-
|
|
6499
|
-
|
|
6500
|
-
|
|
6501
|
-
|
|
6502
|
-
|
|
6503
|
-
|
|
6504
|
-
|
|
6505
|
-
|
|
6506
|
-
|
|
6507
|
-
|
|
6508
|
-
|
|
6509
|
-
|
|
6510
|
-
|
|
6511
|
-
|
|
6512
|
-
|
|
6513
|
-
|
|
6514
|
-
|
|
6515
|
-
|
|
6516
|
-
|
|
6517
|
-
|
|
6518
|
-
|
|
6519
|
-
|
|
6520
|
-
|
|
6521
|
-
|
|
6522
|
-
|
|
6523
|
-
|
|
6524
|
-
|
|
6525
|
-
|
|
6526
|
-
|
|
6527
|
-
|
|
6528
|
-
|
|
6700
|
+
const method = this.readValue("method");
|
|
6701
|
+
if (method < 0.5) {
|
|
6702
|
+
// SPH
|
|
6703
|
+
const rad = this.readValue("influenceRadius");
|
|
6704
|
+
const enableNearPressure = this.readValue("enableNearPressure");
|
|
6705
|
+
// Predict current particle position for this frame (approximate)
|
|
6706
|
+
const posPredX = particle.position.x + particle.velocity.x * dt;
|
|
6707
|
+
const posPredY = particle.position.y + particle.velocity.y * dt;
|
|
6708
|
+
let density = 0.0;
|
|
6709
|
+
let nearDensity = 0.0;
|
|
6710
|
+
// Precompute radius powers for kernels
|
|
6711
|
+
const r2 = rad * rad;
|
|
6712
|
+
const r4 = r2 * r2;
|
|
6713
|
+
const r6 = r4 * r2;
|
|
6714
|
+
// Get neighbors using predicted position
|
|
6715
|
+
const neighbors = getNeighbors({ x: posPredX, y: posPredY }, rad);
|
|
6716
|
+
for (const other of neighbors) {
|
|
6717
|
+
if (other.id === particle.id)
|
|
6718
|
+
continue; // Skip self
|
|
6719
|
+
// Skip removed or pinned neighbors
|
|
6720
|
+
if (other.mass <= 0)
|
|
6721
|
+
continue;
|
|
6722
|
+
const dX = posPredX - other.position.x;
|
|
6723
|
+
const dY = posPredY - other.position.y;
|
|
6724
|
+
const dist2 = dX * dX + dY * dY;
|
|
6725
|
+
if (dist2 <= 0.0)
|
|
6726
|
+
continue;
|
|
6727
|
+
const dist = Math.sqrt(dist2);
|
|
6728
|
+
const factor = Math.max(rad - dist, 0.0);
|
|
6729
|
+
if (factor <= 0.0)
|
|
6730
|
+
continue;
|
|
6731
|
+
// Density kernel: (factor^2) / (pi/6 * r^4)
|
|
6732
|
+
const kDensity = (factor * factor) / ((Math.PI / 6.0) * r4);
|
|
6733
|
+
density += kDensity * 1000.0 * other.mass;
|
|
6734
|
+
// Near-density kernel: (factor^4) / (pi/15 * r^6)
|
|
6735
|
+
if (enableNearPressure !== 0.0) {
|
|
6736
|
+
const f2 = factor * factor;
|
|
6737
|
+
const kNear = (f2 * f2) / ((Math.PI / 15.0) * r6);
|
|
6738
|
+
nearDensity += kNear * 1000.0 * other.mass;
|
|
6739
|
+
}
|
|
6529
6740
|
}
|
|
6741
|
+
// Store results in shared state for use in apply pass
|
|
6742
|
+
setState("density", density);
|
|
6743
|
+
setState("nearDensity", nearDensity);
|
|
6530
6744
|
}
|
|
6531
|
-
|
|
6532
|
-
|
|
6533
|
-
|
|
6534
|
-
|
|
6535
|
-
// Apply pass: compute pressure and viscosity forces using precomputed densities
|
|
6536
|
-
apply: ({ particle, getNeighbors, getState }) => {
|
|
6537
|
-
// Get fluid parameters
|
|
6538
|
-
const rad = this.readValue("influenceRadius");
|
|
6539
|
-
const targetDensity = this.readValue("targetDensity");
|
|
6540
|
-
const pressureMul = this.readValue("pressureMultiplier");
|
|
6541
|
-
const visc = this.readValue("viscosity");
|
|
6542
|
-
const nearMul = this.readValue("nearPressureMultiplier");
|
|
6543
|
-
const nearThreshold = this.readValue("nearThreshold");
|
|
6544
|
-
const useNear = this.readValue("enableNearPressure");
|
|
6545
|
-
const maxAccel = this.readValue("maxAcceleration");
|
|
6546
|
-
const myDensity = Math.max(getState("density"), 1e-6);
|
|
6547
|
-
// Precompute radius powers for kernels
|
|
6548
|
-
const r2 = rad * rad;
|
|
6549
|
-
const r4 = r2 * r2;
|
|
6550
|
-
const r8 = r4 * r4;
|
|
6551
|
-
// Pressure gradient accumulation
|
|
6552
|
-
let gradSumX = 0.0;
|
|
6553
|
-
let gradSumY = 0.0;
|
|
6554
|
-
const neighbors = getNeighbors(particle.position, rad);
|
|
6555
|
-
for (const other of neighbors) {
|
|
6556
|
-
if (other.id === particle.id)
|
|
6557
|
-
continue; // Skip self
|
|
6558
|
-
// Skip removed or pinned neighbors
|
|
6559
|
-
if (other.mass <= 0)
|
|
6560
|
-
continue;
|
|
6561
|
-
const deltaX = other.position.x - particle.position.x;
|
|
6562
|
-
const deltaY = other.position.y - particle.position.y;
|
|
6563
|
-
const dist2 = deltaX * deltaX + deltaY * deltaY;
|
|
6564
|
-
if (dist2 <= 0.0)
|
|
6565
|
-
continue;
|
|
6566
|
-
const dist = Math.sqrt(dist2);
|
|
6567
|
-
if (dist <= 0.0 || dist >= rad)
|
|
6568
|
-
continue;
|
|
6569
|
-
const dirX = deltaX / dist;
|
|
6570
|
-
const dirY = deltaY / dist;
|
|
6571
|
-
// Derivative kernel: scale * (dist - rad), scale = (-12/pi)/r^4
|
|
6572
|
-
const scale = -12 / Math.PI / r4;
|
|
6573
|
-
const slope = (dist - rad) * scale;
|
|
6574
|
-
// Neighbor pressures from precomputed densities
|
|
6575
|
-
const dN = Math.max(getState("density", other.id), 1e-6);
|
|
6576
|
-
const nearN = useNear !== 0.0 ? getState("nearDensity", other.id) : 0.0;
|
|
6577
|
-
const densityDiff = dN - targetDensity;
|
|
6578
|
-
const pressure = densityDiff * pressureMul;
|
|
6579
|
-
const nearPressure = nearN * nearMul;
|
|
6580
|
-
const effectivePressure = dist < nearThreshold ? nearPressure : pressure;
|
|
6581
|
-
// Gradient contribution
|
|
6582
|
-
const gradContribX = (dirX * (effectivePressure * slope)) / dN;
|
|
6583
|
-
const gradContribY = (dirY * (effectivePressure * slope)) / dN;
|
|
6584
|
-
gradSumX += gradContribX;
|
|
6585
|
-
gradSumY += gradContribY;
|
|
6745
|
+
else {
|
|
6746
|
+
// PICFLIP
|
|
6747
|
+
setState("prevVelX", particle.velocity.x);
|
|
6748
|
+
setState("prevVelY", particle.velocity.y);
|
|
6586
6749
|
}
|
|
6587
|
-
|
|
6588
|
-
|
|
6589
|
-
|
|
6590
|
-
|
|
6591
|
-
|
|
6592
|
-
|
|
6593
|
-
if (
|
|
6750
|
+
},
|
|
6751
|
+
// Apply pass:
|
|
6752
|
+
// - SPH: pressure + viscosity using precomputed densities
|
|
6753
|
+
// - PICFLIP: simplified local-pressure PIC/FLIP blend (+ optional viscosity)
|
|
6754
|
+
apply: ({ particle, getNeighbors, dt, getState }) => {
|
|
6755
|
+
const method = this.readValue("method");
|
|
6756
|
+
if (method < 0.5) {
|
|
6757
|
+
// SPH
|
|
6758
|
+
const rad = this.readValue("influenceRadius");
|
|
6759
|
+
const targetDensity = this.readValue("targetDensity");
|
|
6760
|
+
const pressureMul = this.readValue("pressureMultiplier");
|
|
6761
|
+
const visc = this.readValue("viscosity");
|
|
6762
|
+
const nearMul = this.readValue("nearPressureMultiplier");
|
|
6763
|
+
const nearThreshold = this.readValue("nearThreshold");
|
|
6764
|
+
const useNear = this.readValue("enableNearPressure");
|
|
6765
|
+
const maxAccel = this.readValue("maxAcceleration");
|
|
6766
|
+
const myDensity = Math.max(getState("density"), 1e-6);
|
|
6767
|
+
// Precompute radius powers for kernels
|
|
6768
|
+
const r2 = rad * rad;
|
|
6769
|
+
const r4 = r2 * r2;
|
|
6770
|
+
const r8 = r4 * r4;
|
|
6771
|
+
// Pressure gradient accumulation
|
|
6772
|
+
let gradSumX = 0.0;
|
|
6773
|
+
let gradSumY = 0.0;
|
|
6774
|
+
const neighbors = getNeighbors(particle.position, rad);
|
|
6594
6775
|
for (const other of neighbors) {
|
|
6595
6776
|
if (other.id === particle.id)
|
|
6596
6777
|
continue; // Skip self
|
|
@@ -6603,34 +6784,155 @@ class Fluids extends Module {
|
|
|
6603
6784
|
if (dist2 <= 0.0)
|
|
6604
6785
|
continue;
|
|
6605
6786
|
const dist = Math.sqrt(dist2);
|
|
6606
|
-
if (dist >= rad)
|
|
6787
|
+
if (dist <= 0.0 || dist >= rad)
|
|
6607
6788
|
continue;
|
|
6608
|
-
|
|
6609
|
-
const
|
|
6610
|
-
|
|
6611
|
-
|
|
6612
|
-
|
|
6789
|
+
const dirX = deltaX / dist;
|
|
6790
|
+
const dirY = deltaY / dist;
|
|
6791
|
+
// Derivative kernel: scale * (dist - rad), scale = (-12/pi)/r^4
|
|
6792
|
+
const scale = -12 / Math.PI / r4;
|
|
6793
|
+
const slope = (dist - rad) * scale;
|
|
6794
|
+
// Neighbor pressures from precomputed densities
|
|
6795
|
+
const dN = Math.max(getState("density", other.id), 1e-6);
|
|
6796
|
+
const nearN = useNear !== 0.0 ? getState("nearDensity", other.id) : 0.0;
|
|
6797
|
+
const densityDiff = dN - targetDensity;
|
|
6798
|
+
const pressure = densityDiff * pressureMul;
|
|
6799
|
+
const nearPressure = nearN * nearMul;
|
|
6800
|
+
const effectivePressure = dist < nearThreshold ? nearPressure : pressure;
|
|
6801
|
+
// Gradient contribution
|
|
6802
|
+
const gradContribX = (dirX * (effectivePressure * slope)) / dN;
|
|
6803
|
+
const gradContribY = (dirY * (effectivePressure * slope)) / dN;
|
|
6804
|
+
gradSumX += gradContribX;
|
|
6805
|
+
gradSumY += gradContribY;
|
|
6613
6806
|
}
|
|
6614
|
-
|
|
6615
|
-
|
|
6616
|
-
|
|
6617
|
-
|
|
6618
|
-
|
|
6619
|
-
|
|
6620
|
-
|
|
6621
|
-
|
|
6622
|
-
|
|
6807
|
+
// Pressure force is negative gradient
|
|
6808
|
+
let pressureForceX = -gradSumX;
|
|
6809
|
+
let pressureForceY = -gradSumY;
|
|
6810
|
+
// Viscosity accumulation
|
|
6811
|
+
let viscosityForceX = 0.0;
|
|
6812
|
+
let viscosityForceY = 0.0;
|
|
6813
|
+
if (visc !== 0.0) {
|
|
6814
|
+
for (const other of neighbors) {
|
|
6815
|
+
if (other.id === particle.id)
|
|
6816
|
+
continue; // Skip self
|
|
6817
|
+
// Skip removed or pinned neighbors
|
|
6818
|
+
if (other.mass <= 0)
|
|
6819
|
+
continue;
|
|
6820
|
+
const deltaX = other.position.x - particle.position.x;
|
|
6821
|
+
const deltaY = other.position.y - particle.position.y;
|
|
6822
|
+
const dist2 = deltaX * deltaX + deltaY * deltaY;
|
|
6823
|
+
if (dist2 <= 0.0)
|
|
6824
|
+
continue;
|
|
6825
|
+
const dist = Math.sqrt(dist2);
|
|
6826
|
+
if (dist >= rad)
|
|
6827
|
+
continue;
|
|
6828
|
+
// Viscosity kernel: (max(0, r^2 - d^2)^3) / (pi/4 * r^8)
|
|
6829
|
+
const val = Math.max(0.0, r2 - dist2);
|
|
6830
|
+
const kVisc = (val * val * val) / ((Math.PI / 4.0) * r8);
|
|
6831
|
+
viscosityForceX +=
|
|
6832
|
+
(other.velocity.x - particle.velocity.x) * kVisc;
|
|
6833
|
+
viscosityForceY +=
|
|
6834
|
+
(other.velocity.y - particle.velocity.y) * kVisc;
|
|
6835
|
+
}
|
|
6836
|
+
viscosityForceX *= visc;
|
|
6837
|
+
viscosityForceY *= visc;
|
|
6838
|
+
}
|
|
6839
|
+
// Convert to acceleration-like effect: a = F / density
|
|
6840
|
+
let forceX = (pressureForceX / myDensity) * 1000000.0;
|
|
6841
|
+
let forceY = (pressureForceY / myDensity) * 1000000.0;
|
|
6842
|
+
if (visc !== 0.0) {
|
|
6843
|
+
forceX += (viscosityForceX * 1000.0) / myDensity;
|
|
6844
|
+
forceY += (viscosityForceY * 1000.0) / myDensity;
|
|
6845
|
+
}
|
|
6846
|
+
// Clamp force magnitude to avoid instabilities
|
|
6847
|
+
const f2 = forceX * forceX + forceY * forceY;
|
|
6848
|
+
if (f2 > maxAccel * maxAccel) {
|
|
6849
|
+
const fLen = Math.sqrt(f2);
|
|
6850
|
+
forceX = forceX * (maxAccel / fLen);
|
|
6851
|
+
forceY = forceY * (maxAccel / fLen);
|
|
6852
|
+
}
|
|
6853
|
+
// Apply directly to velocity (matching WebGPU behavior)
|
|
6854
|
+
particle.velocity.x += forceX;
|
|
6855
|
+
particle.velocity.y += forceY;
|
|
6623
6856
|
}
|
|
6624
|
-
|
|
6625
|
-
|
|
6626
|
-
|
|
6627
|
-
|
|
6628
|
-
|
|
6629
|
-
|
|
6857
|
+
else {
|
|
6858
|
+
// PICFLIP
|
|
6859
|
+
// Scale UI-friendly values to PIC/FLIP internal tuning:
|
|
6860
|
+
// - influenceRadius: /2
|
|
6861
|
+
// - targetDensity: *3
|
|
6862
|
+
// - pressureMultiplier: *30
|
|
6863
|
+
const flipRatio = this.readValue("flipRatio");
|
|
6864
|
+
const targetDensity = this.readValue("targetDensity") * 3.0;
|
|
6865
|
+
const rad = this.readValue("influenceRadius") * 0.5;
|
|
6866
|
+
const pressureScale = this.readValue("pressureMultiplier") * 30.0;
|
|
6867
|
+
// Get stored previous velocity
|
|
6868
|
+
const prevVelX = getState("prevVelX");
|
|
6869
|
+
const prevVelY = getState("prevVelY");
|
|
6870
|
+
let newVelX = particle.velocity.x;
|
|
6871
|
+
let newVelY = particle.velocity.y;
|
|
6872
|
+
// Local pressure approximation using neighbor density
|
|
6873
|
+
let density = 0.0;
|
|
6874
|
+
let avgVelX = 0.0;
|
|
6875
|
+
let avgVelY = 0.0;
|
|
6876
|
+
let count = 0.0;
|
|
6877
|
+
const neighbors = getNeighbors(particle.position, rad);
|
|
6878
|
+
for (const other of neighbors) {
|
|
6879
|
+
if (other.id === particle.id)
|
|
6880
|
+
continue;
|
|
6881
|
+
if (other.mass <= 0)
|
|
6882
|
+
continue;
|
|
6883
|
+
const dx = particle.position.x - other.position.x;
|
|
6884
|
+
const dy = particle.position.y - other.position.y;
|
|
6885
|
+
const dist2 = dx * dx + dy * dy;
|
|
6886
|
+
if (dist2 <= 0 || dist2 > rad * rad)
|
|
6887
|
+
continue;
|
|
6888
|
+
const dist = Math.sqrt(dist2);
|
|
6889
|
+
const weight = 1.0 - dist / rad;
|
|
6890
|
+
density += weight;
|
|
6891
|
+
avgVelX += other.velocity.x * weight;
|
|
6892
|
+
avgVelY += other.velocity.y * weight;
|
|
6893
|
+
count += weight;
|
|
6894
|
+
}
|
|
6895
|
+
if (count > 0) {
|
|
6896
|
+
avgVelX /= count;
|
|
6897
|
+
avgVelY /= count;
|
|
6898
|
+
// PIC/FLIP blend
|
|
6899
|
+
const picVelX = avgVelX;
|
|
6900
|
+
const picVelY = avgVelY;
|
|
6901
|
+
const flipVelX = newVelX + (avgVelX - prevVelX);
|
|
6902
|
+
const flipVelY = newVelY + (avgVelY - prevVelY);
|
|
6903
|
+
newVelX = picVelX * (1 - flipRatio) + flipVelX * flipRatio;
|
|
6904
|
+
newVelY = picVelY * (1 - flipRatio) + flipVelY * flipRatio;
|
|
6905
|
+
// Simple pressure force based on local density
|
|
6906
|
+
const maxPressureFactor = Math.abs(pressureScale) * 10.0;
|
|
6907
|
+
const rawPressureFactor = (density - targetDensity) * pressureScale;
|
|
6908
|
+
const pressureFactor = Math.max(-maxPressureFactor, Math.min(maxPressureFactor, rawPressureFactor));
|
|
6909
|
+
// Apply pressure gradient
|
|
6910
|
+
let gradX = 0.0;
|
|
6911
|
+
let gradY = 0.0;
|
|
6912
|
+
for (const other of neighbors) {
|
|
6913
|
+
if (other.id === particle.id)
|
|
6914
|
+
continue;
|
|
6915
|
+
if (other.mass <= 0)
|
|
6916
|
+
continue;
|
|
6917
|
+
const dx = particle.position.x - other.position.x;
|
|
6918
|
+
const dy = particle.position.y - other.position.y;
|
|
6919
|
+
const dist2 = dx * dx + dy * dy;
|
|
6920
|
+
if (dist2 <= 1.0 || dist2 > rad * rad)
|
|
6921
|
+
continue;
|
|
6922
|
+
const dist = Math.sqrt(dist2);
|
|
6923
|
+
const dirX = dx / dist;
|
|
6924
|
+
const dirY = dy / dist;
|
|
6925
|
+
const weight = 1.0 - dist / rad;
|
|
6926
|
+
gradX += dirX * weight * pressureFactor;
|
|
6927
|
+
gradY += dirY * weight * pressureFactor;
|
|
6928
|
+
}
|
|
6929
|
+
// NOTE: no max-acceleration clamp for PIC/FLIP (it tends to need the max anyway)
|
|
6930
|
+
newVelX += gradX * dt;
|
|
6931
|
+
newVelY += gradY * dt;
|
|
6932
|
+
}
|
|
6933
|
+
particle.velocity.x = newVelX;
|
|
6934
|
+
particle.velocity.y = newVelY;
|
|
6630
6935
|
}
|
|
6631
|
-
// Apply directly to velocity (matching WebGPU behavior)
|
|
6632
|
-
particle.velocity.x += forceX;
|
|
6633
|
-
particle.velocity.y += forceY;
|
|
6634
6936
|
},
|
|
6635
6937
|
};
|
|
6636
6938
|
}
|
|
@@ -9680,5 +9982,5 @@ class Particles extends Module {
|
|
|
9680
9982
|
}
|
|
9681
9983
|
}
|
|
9682
9984
|
|
|
9683
|
-
export { AbstractEngine, Behavior, Boundary, CanvasComposition, Collisions, DEFAULT_BEHAVIOR_ALIGNMENT, DEFAULT_BEHAVIOR_AVOID, DEFAULT_BEHAVIOR_CHASE, DEFAULT_BEHAVIOR_COHESION, DEFAULT_BEHAVIOR_REPULSION, DEFAULT_BEHAVIOR_SEPARATION, DEFAULT_BEHAVIOR_VIEW_ANGLE, DEFAULT_BEHAVIOR_VIEW_RADIUS, DEFAULT_BEHAVIOR_WANDER, DEFAULT_BOUNDARY_FRICTION, DEFAULT_BOUNDARY_MODE, DEFAULT_BOUNDARY_REPEL_DISTANCE, DEFAULT_BOUNDARY_REPEL_STRENGTH, DEFAULT_BOUNDARY_RESTITUTION, DEFAULT_COLLISIONS_RESTITUTION, DEFAULT_ENVIRONMENT_DAMPING, DEFAULT_ENVIRONMENT_FRICTION, DEFAULT_ENVIRONMENT_GRAVITY_ANGLE, DEFAULT_ENVIRONMENT_GRAVITY_DIRECTION, DEFAULT_ENVIRONMENT_GRAVITY_STRENGTH, DEFAULT_ENVIRONMENT_INERTIA, DEFAULT_FLUIDS_ENABLE_NEAR_PRESSURE, DEFAULT_FLUIDS_INFLUENCE_RADIUS, DEFAULT_FLUIDS_MAX_ACCELERATION, DEFAULT_FLUIDS_NEAR_PRESSURE_MULTIPLIER, DEFAULT_FLUIDS_NEAR_THRESHOLD, DEFAULT_FLUIDS_PRESSURE_MULTIPLIER, DEFAULT_FLUIDS_TARGET_DENSITY, DEFAULT_FLUIDS_VISCOSITY, DEFAULT_GRAB_GRABBED_INDEX, DEFAULT_GRAB_POSITION_X, DEFAULT_GRAB_POSITION_Y, DEFAULT_INTERACTION_MODE, DEFAULT_INTERACTION_RADIUS, DEFAULT_INTERACTION_STRENGTH, DEFAULT_JOINTS_ENABLE_JOINT_COLLISIONS, DEFAULT_JOINTS_ENABLE_PARTICLE_COLLISIONS, DEFAULT_JOINTS_FRICTION, DEFAULT_JOINTS_MOMENTUM, DEFAULT_JOINTS_RESTITUTION, DEFAULT_JOINTS_SEPARATION, DEFAULT_JOINTS_STEPS, DEFAULT_SENSORS_COLOR_SIMILARITY_THRESHOLD, DEFAULT_SENSORS_FLEE_ANGLE, DEFAULT_SENSORS_FLEE_BEHAVIOR, DEFAULT_SENSORS_FOLLOW_BEHAVIOR, DEFAULT_SENSORS_SENSOR_ANGLE, DEFAULT_SENSORS_SENSOR_DISTANCE, DEFAULT_SENSORS_SENSOR_RADIUS, DEFAULT_SENSORS_SENSOR_STRENGTH, DEFAULT_SENSORS_SENSOR_THRESHOLD, DEFAULT_TRAILS_TRAIL_DECAY, DEFAULT_TRAILS_TRAIL_DIFFUSE, DataType, Engine, Environment, Fluids, Grab, Interaction, Joints, Lines, Module, ModuleRole, OscillatorManager, Particles, ParticlesColorType, RenderPassKind, Sensors, Spawner, Trails, Vector, degToRad, radToDeg };
|
|
9985
|
+
export { AbstractEngine, Behavior, Boundary, CanvasComposition, Collisions, DEFAULT_BEHAVIOR_ALIGNMENT, DEFAULT_BEHAVIOR_AVOID, DEFAULT_BEHAVIOR_CHASE, DEFAULT_BEHAVIOR_COHESION, DEFAULT_BEHAVIOR_REPULSION, DEFAULT_BEHAVIOR_SEPARATION, DEFAULT_BEHAVIOR_VIEW_ANGLE, DEFAULT_BEHAVIOR_VIEW_RADIUS, DEFAULT_BEHAVIOR_WANDER, DEFAULT_BOUNDARY_FRICTION, DEFAULT_BOUNDARY_MODE, DEFAULT_BOUNDARY_REPEL_DISTANCE, DEFAULT_BOUNDARY_REPEL_STRENGTH, DEFAULT_BOUNDARY_RESTITUTION, DEFAULT_COLLISIONS_RESTITUTION, DEFAULT_ENVIRONMENT_DAMPING, DEFAULT_ENVIRONMENT_FRICTION, DEFAULT_ENVIRONMENT_GRAVITY_ANGLE, DEFAULT_ENVIRONMENT_GRAVITY_DIRECTION, DEFAULT_ENVIRONMENT_GRAVITY_STRENGTH, DEFAULT_ENVIRONMENT_INERTIA, DEFAULT_FLUIDS_ENABLE_NEAR_PRESSURE, DEFAULT_FLUIDS_INFLUENCE_RADIUS, DEFAULT_FLUIDS_MAX_ACCELERATION, DEFAULT_FLUIDS_NEAR_PRESSURE_MULTIPLIER, DEFAULT_FLUIDS_NEAR_THRESHOLD, DEFAULT_FLUIDS_PRESSURE_MULTIPLIER, DEFAULT_FLUIDS_TARGET_DENSITY, DEFAULT_FLUIDS_VISCOSITY, DEFAULT_GRAB_GRABBED_INDEX, DEFAULT_GRAB_POSITION_X, DEFAULT_GRAB_POSITION_Y, DEFAULT_INTERACTION_MODE, DEFAULT_INTERACTION_RADIUS, DEFAULT_INTERACTION_STRENGTH, DEFAULT_JOINTS_ENABLE_JOINT_COLLISIONS, DEFAULT_JOINTS_ENABLE_PARTICLE_COLLISIONS, DEFAULT_JOINTS_FRICTION, DEFAULT_JOINTS_MOMENTUM, DEFAULT_JOINTS_RESTITUTION, DEFAULT_JOINTS_SEPARATION, DEFAULT_JOINTS_STEPS, DEFAULT_PICFLIP_DENSITY, DEFAULT_PICFLIP_FLIP_RATIO, DEFAULT_PICFLIP_INFLUENCE_RADIUS, DEFAULT_PICFLIP_INTERNAL_MAX_ACCELERATION, DEFAULT_PICFLIP_PRESSURE, DEFAULT_PICFLIP_PRESSURE_MULTIPLIER, DEFAULT_PICFLIP_RADIUS, DEFAULT_PICFLIP_TARGET_DENSITY, DEFAULT_SENSORS_COLOR_SIMILARITY_THRESHOLD, DEFAULT_SENSORS_FLEE_ANGLE, DEFAULT_SENSORS_FLEE_BEHAVIOR, DEFAULT_SENSORS_FOLLOW_BEHAVIOR, DEFAULT_SENSORS_SENSOR_ANGLE, DEFAULT_SENSORS_SENSOR_DISTANCE, DEFAULT_SENSORS_SENSOR_RADIUS, DEFAULT_SENSORS_SENSOR_STRENGTH, DEFAULT_SENSORS_SENSOR_THRESHOLD, DEFAULT_TRAILS_TRAIL_DECAY, DEFAULT_TRAILS_TRAIL_DIFFUSE, DataType, Engine, Environment, Fluids, FluidsMethod, Grab, Interaction, Joints, Lines, Module, ModuleRole, OscillatorManager, Particles, ParticlesColorType, RenderPassKind, Sensors, Spawner, Trails, Vector, degToRad, radToDeg };
|
|
9684
9986
|
//# sourceMappingURL=index.js.map
|