@cazala/party 0.1.2 → 0.2.1
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
|
@@ -3594,7 +3594,12 @@ class WebGPUEngine extends AbstractEngine {
|
|
|
3594
3594
|
this.registry.initialize(this.resources);
|
|
3595
3595
|
const program = this.registry.getProgram();
|
|
3596
3596
|
if (program.extraBindings.simState) {
|
|
3597
|
-
|
|
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);
|
|
3598
3603
|
}
|
|
3599
3604
|
// Build compute pipelines
|
|
3600
3605
|
this.sim.initialize(this.resources, program);
|
|
@@ -6269,14 +6274,30 @@ class Collisions extends Module {
|
|
|
6269
6274
|
* - apply(): compute pressure gradient and viscosity forces; clamp max accel
|
|
6270
6275
|
* Stores per-particle state in shared SIM_STATE via the Program-provided helpers.
|
|
6271
6276
|
*/
|
|
6277
|
+
var FluidsMethod;
|
|
6278
|
+
(function (FluidsMethod) {
|
|
6279
|
+
FluidsMethod["Sph"] = "sph";
|
|
6280
|
+
FluidsMethod["Picflip"] = "picflip";
|
|
6281
|
+
})(FluidsMethod || (FluidsMethod = {}));
|
|
6272
6282
|
const DEFAULT_FLUIDS_INFLUENCE_RADIUS = 100;
|
|
6273
|
-
const DEFAULT_FLUIDS_TARGET_DENSITY =
|
|
6283
|
+
const DEFAULT_FLUIDS_TARGET_DENSITY = 2;
|
|
6274
6284
|
const DEFAULT_FLUIDS_PRESSURE_MULTIPLIER = 30;
|
|
6275
6285
|
const DEFAULT_FLUIDS_VISCOSITY = 1;
|
|
6276
6286
|
const DEFAULT_FLUIDS_NEAR_PRESSURE_MULTIPLIER = 50;
|
|
6277
6287
|
const DEFAULT_FLUIDS_NEAR_THRESHOLD = 20;
|
|
6278
6288
|
const DEFAULT_FLUIDS_ENABLE_NEAR_PRESSURE = true;
|
|
6279
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;
|
|
6280
6301
|
class Fluids extends Module {
|
|
6281
6302
|
constructor(opts) {
|
|
6282
6303
|
super();
|
|
@@ -6297,6 +6318,7 @@ class Fluids extends Module {
|
|
|
6297
6318
|
configurable: true,
|
|
6298
6319
|
writable: true,
|
|
6299
6320
|
value: {
|
|
6321
|
+
method: DataType.NUMBER,
|
|
6300
6322
|
influenceRadius: DataType.NUMBER,
|
|
6301
6323
|
targetDensity: DataType.NUMBER,
|
|
6302
6324
|
pressureMultiplier: DataType.NUMBER,
|
|
@@ -6305,22 +6327,56 @@ class Fluids extends Module {
|
|
|
6305
6327
|
nearThreshold: DataType.NUMBER,
|
|
6306
6328
|
enableNearPressure: DataType.NUMBER,
|
|
6307
6329
|
maxAcceleration: DataType.NUMBER,
|
|
6330
|
+
flipRatio: DataType.NUMBER,
|
|
6308
6331
|
}
|
|
6309
6332
|
});
|
|
6310
|
-
|
|
6311
|
-
|
|
6312
|
-
|
|
6313
|
-
|
|
6314
|
-
|
|
6315
|
-
|
|
6316
|
-
|
|
6317
|
-
|
|
6318
|
-
|
|
6319
|
-
|
|
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
|
+
}
|
|
6320
6368
|
if (opts?.enabled !== undefined) {
|
|
6321
6369
|
this.setEnabled(!!opts.enabled);
|
|
6322
6370
|
}
|
|
6323
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
|
+
}
|
|
6324
6380
|
setInfluenceRadius(v) {
|
|
6325
6381
|
this.write({ influenceRadius: v });
|
|
6326
6382
|
}
|
|
@@ -6333,6 +6389,9 @@ class Fluids extends Module {
|
|
|
6333
6389
|
setViscosity(v) {
|
|
6334
6390
|
this.write({ viscosity: v });
|
|
6335
6391
|
}
|
|
6392
|
+
setFlipRatio(v) {
|
|
6393
|
+
this.write({ flipRatio: v });
|
|
6394
|
+
}
|
|
6336
6395
|
setNearPressureMultiplier(v) {
|
|
6337
6396
|
this.write({ nearPressureMultiplier: v });
|
|
6338
6397
|
}
|
|
@@ -6357,6 +6416,9 @@ class Fluids extends Module {
|
|
|
6357
6416
|
getViscosity() {
|
|
6358
6417
|
return this.readValue("viscosity");
|
|
6359
6418
|
}
|
|
6419
|
+
getFlipRatio() {
|
|
6420
|
+
return this.readValue("flipRatio");
|
|
6421
|
+
}
|
|
6360
6422
|
getNearPressureMultiplier() {
|
|
6361
6423
|
return this.readValue("nearPressureMultiplier");
|
|
6362
6424
|
}
|
|
@@ -6371,111 +6433,94 @@ class Fluids extends Module {
|
|
|
6371
6433
|
}
|
|
6372
6434
|
webgpu() {
|
|
6373
6435
|
return {
|
|
6374
|
-
states: ["density", "nearDensity"],
|
|
6375
|
-
// 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)
|
|
6376
6440
|
state: ({ particleVar, dtVar, getUniform, setState }) => `{
|
|
6377
|
-
|
|
6378
|
-
let rad = ${getUniform("influenceRadius")};
|
|
6379
|
-
let posPred = ${particleVar}.position + ${particleVar}.velocity * (${dtVar});
|
|
6380
|
-
var density: f32 = 0.0;
|
|
6381
|
-
var nearDensity: f32 = 0.0;
|
|
6441
|
+
let method = ${getUniform("method")};
|
|
6382
6442
|
|
|
6383
|
-
|
|
6384
|
-
|
|
6385
|
-
|
|
6386
|
-
|
|
6387
|
-
|
|
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;
|
|
6388
6449
|
|
|
6389
|
-
|
|
6390
|
-
|
|
6391
|
-
|
|
6392
|
-
let
|
|
6393
|
-
|
|
6394
|
-
let other = particles[j];
|
|
6395
|
-
// Skip removed or pinned neighbors
|
|
6396
|
-
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;
|
|
6397
6455
|
|
|
6398
|
-
|
|
6399
|
-
|
|
6400
|
-
|
|
6401
|
-
|
|
6402
|
-
|
|
6403
|
-
|
|
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; }
|
|
6404
6464
|
|
|
6405
|
-
|
|
6406
|
-
|
|
6407
|
-
|
|
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; }
|
|
6408
6471
|
|
|
6409
|
-
|
|
6410
|
-
|
|
6411
|
-
|
|
6412
|
-
|
|
6413
|
-
|
|
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
|
+
}
|
|
6414
6482
|
}
|
|
6415
|
-
}
|
|
6416
6483
|
|
|
6417
|
-
|
|
6418
|
-
|
|
6419
|
-
|
|
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
|
+
}
|
|
6420
6492
|
}`,
|
|
6421
|
-
// Apply pass:
|
|
6422
|
-
|
|
6423
|
-
|
|
6424
|
-
|
|
6425
|
-
let
|
|
6426
|
-
let visc = ${getUniform("viscosity")};
|
|
6427
|
-
let nearMul = ${getUniform("nearPressureMultiplier")};
|
|
6428
|
-
let nearThreshold = ${getUniform("nearThreshold")};
|
|
6429
|
-
let useNear = ${getUniform("enableNearPressure")};
|
|
6430
|
-
|
|
6431
|
-
let myDensity = max(${getState("density")}, 1e-6);
|
|
6432
|
-
|
|
6433
|
-
// Precompute radius powers for kernels
|
|
6434
|
-
let r2 = rad * rad;
|
|
6435
|
-
let r4 = r2 * r2;
|
|
6436
|
-
let r6 = r4 * r2;
|
|
6437
|
-
let r8 = r4 * r4;
|
|
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")};
|
|
6438
6498
|
|
|
6439
|
-
|
|
6440
|
-
|
|
6441
|
-
|
|
6442
|
-
|
|
6443
|
-
let
|
|
6444
|
-
|
|
6445
|
-
let
|
|
6446
|
-
|
|
6447
|
-
|
|
6448
|
-
let
|
|
6449
|
-
let
|
|
6450
|
-
if (dist2 <= 0.0) { continue; }
|
|
6451
|
-
let dist = sqrt(dist2);
|
|
6452
|
-
if (dist <= 0.0 || dist >= rad) { continue; }
|
|
6453
|
-
let dir = delta / dist;
|
|
6454
|
-
|
|
6455
|
-
// Derivative kernel (CPU: scale * (dist - rad), scale = (-12/pi)/r^4)
|
|
6456
|
-
let scale = (-12.0 / 3.14159265) / r4;
|
|
6457
|
-
let slope = (dist - rad) * scale;
|
|
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")};
|
|
6458
6510
|
|
|
6459
|
-
|
|
6460
|
-
let dN = max(${getState("density", "j")}, 1e-6);
|
|
6461
|
-
let nearN = select(0.0, ${getState("nearDensity", "j")}, useNear != 0.0);
|
|
6462
|
-
let densityDiff = dN - targetDensity;
|
|
6463
|
-
let pressure = densityDiff * pressureMul;
|
|
6464
|
-
let nearPressure = nearN * nearMul;
|
|
6465
|
-
let effectivePressure = select(pressure, nearPressure, dist < nearThreshold);
|
|
6511
|
+
let myDensity = max(${getState("density")}, 1e-6);
|
|
6466
6512
|
|
|
6467
|
-
//
|
|
6468
|
-
|
|
6469
|
-
|
|
6470
|
-
|
|
6471
|
-
|
|
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;
|
|
6472
6518
|
|
|
6473
|
-
|
|
6474
|
-
|
|
6475
|
-
|
|
6476
|
-
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);
|
|
6477
6522
|
loop {
|
|
6478
|
-
let j = neighbor_iter_next(&
|
|
6523
|
+
let j = neighbor_iter_next(&it1, index);
|
|
6479
6524
|
if (j == NEIGHBOR_NONE) { break; }
|
|
6480
6525
|
let other = particles[j];
|
|
6481
6526
|
// Skip removed or pinned neighbors
|
|
@@ -6484,141 +6529,257 @@ class Fluids extends Module {
|
|
|
6484
6529
|
let dist2 = dot(delta, delta);
|
|
6485
6530
|
if (dist2 <= 0.0) { continue; }
|
|
6486
6531
|
let dist = sqrt(dist2);
|
|
6487
|
-
if (dist >= rad) { continue; }
|
|
6488
|
-
|
|
6489
|
-
|
|
6490
|
-
|
|
6491
|
-
|
|
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);
|
|
6492
6549
|
}
|
|
6493
|
-
|
|
6494
|
-
|
|
6550
|
+
// Pressure force is negative gradient
|
|
6551
|
+
var pressureForce = -gradSum;
|
|
6495
6552
|
|
|
6496
|
-
|
|
6497
|
-
|
|
6498
|
-
|
|
6499
|
-
|
|
6500
|
-
|
|
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
|
+
}
|
|
6501
6575
|
|
|
6502
|
-
|
|
6503
|
-
|
|
6504
|
-
|
|
6505
|
-
|
|
6506
|
-
|
|
6507
|
-
|
|
6508
|
-
|
|
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;
|
|
6509
6641
|
|
|
6510
|
-
|
|
6511
|
-
|
|
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; }
|
|
6669
|
+
|
|
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
|
+
// Clamp acceleration magnitude (units: velocity / second) to avoid dt-dependent instability
|
|
6683
|
+
// (notably visible at lower FPS / larger dt near boundaries).
|
|
6684
|
+
let maxAccel = 30000.0;
|
|
6685
|
+
var grad = vec2<f32>(gradX, gradY);
|
|
6686
|
+
let gl = length(grad);
|
|
6687
|
+
if (gl > maxAccel) {
|
|
6688
|
+
grad = grad * (maxAccel / max(gl, 1e-6));
|
|
6689
|
+
}
|
|
6690
|
+
|
|
6691
|
+
newVelX = newVelX + grad.x * ${dtVar};
|
|
6692
|
+
newVelY = newVelY + grad.y * ${dtVar};
|
|
6693
|
+
}
|
|
6694
|
+
|
|
6695
|
+
${particleVar}.velocity.x = newVelX;
|
|
6696
|
+
${particleVar}.velocity.y = newVelY;
|
|
6697
|
+
}
|
|
6512
6698
|
}`,
|
|
6513
6699
|
};
|
|
6514
6700
|
}
|
|
6515
6701
|
cpu() {
|
|
6516
6702
|
return {
|
|
6517
|
-
states: ["density", "nearDensity"],
|
|
6518
|
-
// State pass:
|
|
6703
|
+
states: ["density", "nearDensity", "prevVelX", "prevVelY"],
|
|
6704
|
+
// State pass:
|
|
6705
|
+
// - SPH: precompute density and near-density per particle
|
|
6706
|
+
// - PICFLIP: store previous velocity per particle
|
|
6519
6707
|
state: ({ particle, getNeighbors, dt, setState }) => {
|
|
6520
|
-
|
|
6521
|
-
|
|
6522
|
-
|
|
6523
|
-
|
|
6524
|
-
|
|
6525
|
-
|
|
6526
|
-
|
|
6527
|
-
|
|
6528
|
-
|
|
6529
|
-
|
|
6530
|
-
|
|
6531
|
-
|
|
6532
|
-
|
|
6533
|
-
|
|
6534
|
-
|
|
6535
|
-
|
|
6536
|
-
|
|
6537
|
-
|
|
6538
|
-
|
|
6539
|
-
|
|
6540
|
-
|
|
6541
|
-
|
|
6542
|
-
|
|
6543
|
-
|
|
6544
|
-
|
|
6545
|
-
|
|
6546
|
-
|
|
6547
|
-
|
|
6548
|
-
|
|
6549
|
-
|
|
6550
|
-
|
|
6551
|
-
|
|
6552
|
-
|
|
6553
|
-
|
|
6554
|
-
|
|
6555
|
-
|
|
6556
|
-
|
|
6708
|
+
const method = this.readValue("method");
|
|
6709
|
+
if (method < 0.5) {
|
|
6710
|
+
// SPH
|
|
6711
|
+
const rad = this.readValue("influenceRadius");
|
|
6712
|
+
const enableNearPressure = this.readValue("enableNearPressure");
|
|
6713
|
+
// Predict current particle position for this frame (approximate)
|
|
6714
|
+
const posPredX = particle.position.x + particle.velocity.x * dt;
|
|
6715
|
+
const posPredY = particle.position.y + particle.velocity.y * dt;
|
|
6716
|
+
let density = 0.0;
|
|
6717
|
+
let nearDensity = 0.0;
|
|
6718
|
+
// Precompute radius powers for kernels
|
|
6719
|
+
const r2 = rad * rad;
|
|
6720
|
+
const r4 = r2 * r2;
|
|
6721
|
+
const r6 = r4 * r2;
|
|
6722
|
+
// Get neighbors using predicted position
|
|
6723
|
+
const neighbors = getNeighbors({ x: posPredX, y: posPredY }, rad);
|
|
6724
|
+
for (const other of neighbors) {
|
|
6725
|
+
if (other.id === particle.id)
|
|
6726
|
+
continue; // Skip self
|
|
6727
|
+
// Skip removed or pinned neighbors
|
|
6728
|
+
if (other.mass <= 0)
|
|
6729
|
+
continue;
|
|
6730
|
+
const dX = posPredX - other.position.x;
|
|
6731
|
+
const dY = posPredY - other.position.y;
|
|
6732
|
+
const dist2 = dX * dX + dY * dY;
|
|
6733
|
+
if (dist2 <= 0.0)
|
|
6734
|
+
continue;
|
|
6735
|
+
const dist = Math.sqrt(dist2);
|
|
6736
|
+
const factor = Math.max(rad - dist, 0.0);
|
|
6737
|
+
if (factor <= 0.0)
|
|
6738
|
+
continue;
|
|
6739
|
+
// Density kernel: (factor^2) / (pi/6 * r^4)
|
|
6740
|
+
const kDensity = (factor * factor) / ((Math.PI / 6.0) * r4);
|
|
6741
|
+
density += kDensity * 1000.0 * other.mass;
|
|
6742
|
+
// Near-density kernel: (factor^4) / (pi/15 * r^6)
|
|
6743
|
+
if (enableNearPressure !== 0.0) {
|
|
6744
|
+
const f2 = factor * factor;
|
|
6745
|
+
const kNear = (f2 * f2) / ((Math.PI / 15.0) * r6);
|
|
6746
|
+
nearDensity += kNear * 1000.0 * other.mass;
|
|
6747
|
+
}
|
|
6557
6748
|
}
|
|
6749
|
+
// Store results in shared state for use in apply pass
|
|
6750
|
+
setState("density", density);
|
|
6751
|
+
setState("nearDensity", nearDensity);
|
|
6558
6752
|
}
|
|
6559
|
-
|
|
6560
|
-
|
|
6561
|
-
|
|
6562
|
-
|
|
6563
|
-
// Apply pass: compute pressure and viscosity forces using precomputed densities
|
|
6564
|
-
apply: ({ particle, getNeighbors, getState }) => {
|
|
6565
|
-
// Get fluid parameters
|
|
6566
|
-
const rad = this.readValue("influenceRadius");
|
|
6567
|
-
const targetDensity = this.readValue("targetDensity");
|
|
6568
|
-
const pressureMul = this.readValue("pressureMultiplier");
|
|
6569
|
-
const visc = this.readValue("viscosity");
|
|
6570
|
-
const nearMul = this.readValue("nearPressureMultiplier");
|
|
6571
|
-
const nearThreshold = this.readValue("nearThreshold");
|
|
6572
|
-
const useNear = this.readValue("enableNearPressure");
|
|
6573
|
-
const maxAccel = this.readValue("maxAcceleration");
|
|
6574
|
-
const myDensity = Math.max(getState("density"), 1e-6);
|
|
6575
|
-
// Precompute radius powers for kernels
|
|
6576
|
-
const r2 = rad * rad;
|
|
6577
|
-
const r4 = r2 * r2;
|
|
6578
|
-
const r8 = r4 * r4;
|
|
6579
|
-
// Pressure gradient accumulation
|
|
6580
|
-
let gradSumX = 0.0;
|
|
6581
|
-
let gradSumY = 0.0;
|
|
6582
|
-
const neighbors = getNeighbors(particle.position, rad);
|
|
6583
|
-
for (const other of neighbors) {
|
|
6584
|
-
if (other.id === particle.id)
|
|
6585
|
-
continue; // Skip self
|
|
6586
|
-
// Skip removed or pinned neighbors
|
|
6587
|
-
if (other.mass <= 0)
|
|
6588
|
-
continue;
|
|
6589
|
-
const deltaX = other.position.x - particle.position.x;
|
|
6590
|
-
const deltaY = other.position.y - particle.position.y;
|
|
6591
|
-
const dist2 = deltaX * deltaX + deltaY * deltaY;
|
|
6592
|
-
if (dist2 <= 0.0)
|
|
6593
|
-
continue;
|
|
6594
|
-
const dist = Math.sqrt(dist2);
|
|
6595
|
-
if (dist <= 0.0 || dist >= rad)
|
|
6596
|
-
continue;
|
|
6597
|
-
const dirX = deltaX / dist;
|
|
6598
|
-
const dirY = deltaY / dist;
|
|
6599
|
-
// Derivative kernel: scale * (dist - rad), scale = (-12/pi)/r^4
|
|
6600
|
-
const scale = -12 / Math.PI / r4;
|
|
6601
|
-
const slope = (dist - rad) * scale;
|
|
6602
|
-
// Neighbor pressures from precomputed densities
|
|
6603
|
-
const dN = Math.max(getState("density", other.id), 1e-6);
|
|
6604
|
-
const nearN = useNear !== 0.0 ? getState("nearDensity", other.id) : 0.0;
|
|
6605
|
-
const densityDiff = dN - targetDensity;
|
|
6606
|
-
const pressure = densityDiff * pressureMul;
|
|
6607
|
-
const nearPressure = nearN * nearMul;
|
|
6608
|
-
const effectivePressure = dist < nearThreshold ? nearPressure : pressure;
|
|
6609
|
-
// Gradient contribution
|
|
6610
|
-
const gradContribX = (dirX * (effectivePressure * slope)) / dN;
|
|
6611
|
-
const gradContribY = (dirY * (effectivePressure * slope)) / dN;
|
|
6612
|
-
gradSumX += gradContribX;
|
|
6613
|
-
gradSumY += gradContribY;
|
|
6753
|
+
else {
|
|
6754
|
+
// PICFLIP
|
|
6755
|
+
setState("prevVelX", particle.velocity.x);
|
|
6756
|
+
setState("prevVelY", particle.velocity.y);
|
|
6614
6757
|
}
|
|
6615
|
-
|
|
6616
|
-
|
|
6617
|
-
|
|
6618
|
-
|
|
6619
|
-
|
|
6620
|
-
|
|
6621
|
-
if (
|
|
6758
|
+
},
|
|
6759
|
+
// Apply pass:
|
|
6760
|
+
// - SPH: pressure + viscosity using precomputed densities
|
|
6761
|
+
// - PICFLIP: simplified local-pressure PIC/FLIP blend (+ optional viscosity)
|
|
6762
|
+
apply: ({ particle, getNeighbors, dt, getState }) => {
|
|
6763
|
+
const method = this.readValue("method");
|
|
6764
|
+
if (method < 0.5) {
|
|
6765
|
+
// SPH
|
|
6766
|
+
const rad = this.readValue("influenceRadius");
|
|
6767
|
+
const targetDensity = this.readValue("targetDensity");
|
|
6768
|
+
const pressureMul = this.readValue("pressureMultiplier");
|
|
6769
|
+
const visc = this.readValue("viscosity");
|
|
6770
|
+
const nearMul = this.readValue("nearPressureMultiplier");
|
|
6771
|
+
const nearThreshold = this.readValue("nearThreshold");
|
|
6772
|
+
const useNear = this.readValue("enableNearPressure");
|
|
6773
|
+
const maxAccel = this.readValue("maxAcceleration");
|
|
6774
|
+
const myDensity = Math.max(getState("density"), 1e-6);
|
|
6775
|
+
// Precompute radius powers for kernels
|
|
6776
|
+
const r2 = rad * rad;
|
|
6777
|
+
const r4 = r2 * r2;
|
|
6778
|
+
const r8 = r4 * r4;
|
|
6779
|
+
// Pressure gradient accumulation
|
|
6780
|
+
let gradSumX = 0.0;
|
|
6781
|
+
let gradSumY = 0.0;
|
|
6782
|
+
const neighbors = getNeighbors(particle.position, rad);
|
|
6622
6783
|
for (const other of neighbors) {
|
|
6623
6784
|
if (other.id === particle.id)
|
|
6624
6785
|
continue; // Skip self
|
|
@@ -6631,34 +6792,163 @@ class Fluids extends Module {
|
|
|
6631
6792
|
if (dist2 <= 0.0)
|
|
6632
6793
|
continue;
|
|
6633
6794
|
const dist = Math.sqrt(dist2);
|
|
6634
|
-
if (dist >= rad)
|
|
6795
|
+
if (dist <= 0.0 || dist >= rad)
|
|
6635
6796
|
continue;
|
|
6636
|
-
|
|
6637
|
-
const
|
|
6638
|
-
|
|
6639
|
-
|
|
6640
|
-
|
|
6797
|
+
const dirX = deltaX / dist;
|
|
6798
|
+
const dirY = deltaY / dist;
|
|
6799
|
+
// Derivative kernel: scale * (dist - rad), scale = (-12/pi)/r^4
|
|
6800
|
+
const scale = -12 / Math.PI / r4;
|
|
6801
|
+
const slope = (dist - rad) * scale;
|
|
6802
|
+
// Neighbor pressures from precomputed densities
|
|
6803
|
+
const dN = Math.max(getState("density", other.id), 1e-6);
|
|
6804
|
+
const nearN = useNear !== 0.0 ? getState("nearDensity", other.id) : 0.0;
|
|
6805
|
+
const densityDiff = dN - targetDensity;
|
|
6806
|
+
const pressure = densityDiff * pressureMul;
|
|
6807
|
+
const nearPressure = nearN * nearMul;
|
|
6808
|
+
const effectivePressure = dist < nearThreshold ? nearPressure : pressure;
|
|
6809
|
+
// Gradient contribution
|
|
6810
|
+
const gradContribX = (dirX * (effectivePressure * slope)) / dN;
|
|
6811
|
+
const gradContribY = (dirY * (effectivePressure * slope)) / dN;
|
|
6812
|
+
gradSumX += gradContribX;
|
|
6813
|
+
gradSumY += gradContribY;
|
|
6641
6814
|
}
|
|
6642
|
-
|
|
6643
|
-
|
|
6644
|
-
|
|
6645
|
-
|
|
6646
|
-
|
|
6647
|
-
|
|
6648
|
-
|
|
6649
|
-
|
|
6650
|
-
|
|
6815
|
+
// Pressure force is negative gradient
|
|
6816
|
+
let pressureForceX = -gradSumX;
|
|
6817
|
+
let pressureForceY = -gradSumY;
|
|
6818
|
+
// Viscosity accumulation
|
|
6819
|
+
let viscosityForceX = 0.0;
|
|
6820
|
+
let viscosityForceY = 0.0;
|
|
6821
|
+
if (visc !== 0.0) {
|
|
6822
|
+
for (const other of neighbors) {
|
|
6823
|
+
if (other.id === particle.id)
|
|
6824
|
+
continue; // Skip self
|
|
6825
|
+
// Skip removed or pinned neighbors
|
|
6826
|
+
if (other.mass <= 0)
|
|
6827
|
+
continue;
|
|
6828
|
+
const deltaX = other.position.x - particle.position.x;
|
|
6829
|
+
const deltaY = other.position.y - particle.position.y;
|
|
6830
|
+
const dist2 = deltaX * deltaX + deltaY * deltaY;
|
|
6831
|
+
if (dist2 <= 0.0)
|
|
6832
|
+
continue;
|
|
6833
|
+
const dist = Math.sqrt(dist2);
|
|
6834
|
+
if (dist >= rad)
|
|
6835
|
+
continue;
|
|
6836
|
+
// Viscosity kernel: (max(0, r^2 - d^2)^3) / (pi/4 * r^8)
|
|
6837
|
+
const val = Math.max(0.0, r2 - dist2);
|
|
6838
|
+
const kVisc = (val * val * val) / ((Math.PI / 4.0) * r8);
|
|
6839
|
+
viscosityForceX +=
|
|
6840
|
+
(other.velocity.x - particle.velocity.x) * kVisc;
|
|
6841
|
+
viscosityForceY +=
|
|
6842
|
+
(other.velocity.y - particle.velocity.y) * kVisc;
|
|
6843
|
+
}
|
|
6844
|
+
viscosityForceX *= visc;
|
|
6845
|
+
viscosityForceY *= visc;
|
|
6846
|
+
}
|
|
6847
|
+
// Convert to acceleration-like effect: a = F / density
|
|
6848
|
+
let forceX = (pressureForceX / myDensity) * 1000000.0;
|
|
6849
|
+
let forceY = (pressureForceY / myDensity) * 1000000.0;
|
|
6850
|
+
if (visc !== 0.0) {
|
|
6851
|
+
forceX += (viscosityForceX * 1000.0) / myDensity;
|
|
6852
|
+
forceY += (viscosityForceY * 1000.0) / myDensity;
|
|
6853
|
+
}
|
|
6854
|
+
// Clamp force magnitude to avoid instabilities
|
|
6855
|
+
const f2 = forceX * forceX + forceY * forceY;
|
|
6856
|
+
if (f2 > maxAccel * maxAccel) {
|
|
6857
|
+
const fLen = Math.sqrt(f2);
|
|
6858
|
+
forceX = forceX * (maxAccel / fLen);
|
|
6859
|
+
forceY = forceY * (maxAccel / fLen);
|
|
6860
|
+
}
|
|
6861
|
+
// Apply directly to velocity (matching WebGPU behavior)
|
|
6862
|
+
particle.velocity.x += forceX;
|
|
6863
|
+
particle.velocity.y += forceY;
|
|
6651
6864
|
}
|
|
6652
|
-
|
|
6653
|
-
|
|
6654
|
-
|
|
6655
|
-
|
|
6656
|
-
|
|
6657
|
-
|
|
6865
|
+
else {
|
|
6866
|
+
// PICFLIP
|
|
6867
|
+
// Scale UI-friendly values to PIC/FLIP internal tuning:
|
|
6868
|
+
// - influenceRadius: /2
|
|
6869
|
+
// - targetDensity: *3
|
|
6870
|
+
// - pressureMultiplier: *30
|
|
6871
|
+
const flipRatio = this.readValue("flipRatio");
|
|
6872
|
+
const targetDensity = this.readValue("targetDensity") * 3.0;
|
|
6873
|
+
const rad = this.readValue("influenceRadius") * 0.5;
|
|
6874
|
+
const pressureScale = this.readValue("pressureMultiplier") * 30.0;
|
|
6875
|
+
// Get stored previous velocity
|
|
6876
|
+
const prevVelX = getState("prevVelX");
|
|
6877
|
+
const prevVelY = getState("prevVelY");
|
|
6878
|
+
let newVelX = particle.velocity.x;
|
|
6879
|
+
let newVelY = particle.velocity.y;
|
|
6880
|
+
// Local pressure approximation using neighbor density
|
|
6881
|
+
let density = 0.0;
|
|
6882
|
+
let avgVelX = 0.0;
|
|
6883
|
+
let avgVelY = 0.0;
|
|
6884
|
+
let count = 0.0;
|
|
6885
|
+
const neighbors = getNeighbors(particle.position, rad);
|
|
6886
|
+
for (const other of neighbors) {
|
|
6887
|
+
if (other.id === particle.id)
|
|
6888
|
+
continue;
|
|
6889
|
+
if (other.mass <= 0)
|
|
6890
|
+
continue;
|
|
6891
|
+
const dx = particle.position.x - other.position.x;
|
|
6892
|
+
const dy = particle.position.y - other.position.y;
|
|
6893
|
+
const dist2 = dx * dx + dy * dy;
|
|
6894
|
+
if (dist2 <= 0 || dist2 > rad * rad)
|
|
6895
|
+
continue;
|
|
6896
|
+
const dist = Math.sqrt(dist2);
|
|
6897
|
+
const weight = 1.0 - dist / rad;
|
|
6898
|
+
density += weight;
|
|
6899
|
+
avgVelX += other.velocity.x * weight;
|
|
6900
|
+
avgVelY += other.velocity.y * weight;
|
|
6901
|
+
count += weight;
|
|
6902
|
+
}
|
|
6903
|
+
if (count > 0) {
|
|
6904
|
+
avgVelX /= count;
|
|
6905
|
+
avgVelY /= count;
|
|
6906
|
+
// PIC/FLIP blend
|
|
6907
|
+
const picVelX = avgVelX;
|
|
6908
|
+
const picVelY = avgVelY;
|
|
6909
|
+
const flipVelX = newVelX + (avgVelX - prevVelX);
|
|
6910
|
+
const flipVelY = newVelY + (avgVelY - prevVelY);
|
|
6911
|
+
newVelX = picVelX * (1 - flipRatio) + flipVelX * flipRatio;
|
|
6912
|
+
newVelY = picVelY * (1 - flipRatio) + flipVelY * flipRatio;
|
|
6913
|
+
// Simple pressure force based on local density
|
|
6914
|
+
const maxPressureFactor = Math.abs(pressureScale) * 10.0;
|
|
6915
|
+
const rawPressureFactor = (density - targetDensity) * pressureScale;
|
|
6916
|
+
const pressureFactor = Math.max(-maxPressureFactor, Math.min(maxPressureFactor, rawPressureFactor));
|
|
6917
|
+
// Apply pressure gradient
|
|
6918
|
+
let gradX = 0.0;
|
|
6919
|
+
let gradY = 0.0;
|
|
6920
|
+
for (const other of neighbors) {
|
|
6921
|
+
if (other.id === particle.id)
|
|
6922
|
+
continue;
|
|
6923
|
+
if (other.mass <= 0)
|
|
6924
|
+
continue;
|
|
6925
|
+
const dx = particle.position.x - other.position.x;
|
|
6926
|
+
const dy = particle.position.y - other.position.y;
|
|
6927
|
+
const dist2 = dx * dx + dy * dy;
|
|
6928
|
+
if (dist2 <= 1.0 || dist2 > rad * rad)
|
|
6929
|
+
continue;
|
|
6930
|
+
const dist = Math.sqrt(dist2);
|
|
6931
|
+
const dirX = dx / dist;
|
|
6932
|
+
const dirY = dy / dist;
|
|
6933
|
+
const weight = 1.0 - dist / rad;
|
|
6934
|
+
gradX += dirX * weight * pressureFactor;
|
|
6935
|
+
gradY += dirY * weight * pressureFactor;
|
|
6936
|
+
}
|
|
6937
|
+
// Clamp acceleration magnitude (units: velocity / second) to avoid dt-dependent instability
|
|
6938
|
+
// (notably visible at lower FPS / larger dt near boundaries).
|
|
6939
|
+
const maxAccel = 20000.0;
|
|
6940
|
+
const gradLen = Math.hypot(gradX, gradY);
|
|
6941
|
+
if (gradLen > maxAccel && gradLen > 1e-6) {
|
|
6942
|
+
const s = maxAccel / gradLen;
|
|
6943
|
+
gradX *= s;
|
|
6944
|
+
gradY *= s;
|
|
6945
|
+
}
|
|
6946
|
+
newVelX += gradX * dt;
|
|
6947
|
+
newVelY += gradY * dt;
|
|
6948
|
+
}
|
|
6949
|
+
particle.velocity.x = newVelX;
|
|
6950
|
+
particle.velocity.y = newVelY;
|
|
6658
6951
|
}
|
|
6659
|
-
// Apply directly to velocity (matching WebGPU behavior)
|
|
6660
|
-
particle.velocity.x += forceX;
|
|
6661
|
-
particle.velocity.y += forceY;
|
|
6662
6952
|
},
|
|
6663
6953
|
};
|
|
6664
6954
|
}
|
|
@@ -9708,5 +9998,5 @@ class Particles extends Module {
|
|
|
9708
9998
|
}
|
|
9709
9999
|
}
|
|
9710
10000
|
|
|
9711
|
-
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 };
|
|
10001
|
+
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 };
|
|
9712
10002
|
//# sourceMappingURL=index.js.map
|