@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 { loop { if ((*it).emitted >= (*it).maxEmit) { return NEIGHBOR_NONE; } if ((*it).r > (*it).cy + (*it).reach) { return NEIGHBOR_NONE; } if ((*it).k < (*it).maxK) { let id = GRID_INDICES[(*it).base + (*it).k]; (*it).k = (*it).k + 1u; if (id != selfIndex) { (*it).emitted = (*it).emitted + 1u; return id; } else { continue; } } (*it).c = (*it).c + 1; if ((*it).c > (*it).cx + (*it).reach) { (*it).c = (*it).cx - (*it).reach; (*it).r = (*it).r + 1; } if ((*it).r > (*it).cy + (*it).reach) { return NEIGHBOR_NONE; } let cell = grid_cell_index_from_rc((*it).r, (*it).c); let cnt = atomicLoad(&GRID_COUNTS[cell]); (*it).maxK = min(cnt, GRID_MAX_PER_CELL()); (*it).base = cell * GRID_MAX_PER_CELL(); (*it).k = 0u; } }`);
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
- this.resources.createSimStateBuffer(this.bufferMaxParticles, 4);
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 = 1;
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
- this.write({
6283
- influenceRadius: opts?.influenceRadius ?? DEFAULT_FLUIDS_INFLUENCE_RADIUS,
6284
- targetDensity: opts?.targetDensity ?? DEFAULT_FLUIDS_TARGET_DENSITY,
6285
- pressureMultiplier: opts?.pressureMultiplier ?? DEFAULT_FLUIDS_PRESSURE_MULTIPLIER,
6286
- viscosity: opts?.viscosity ?? DEFAULT_FLUIDS_VISCOSITY,
6287
- nearPressureMultiplier: opts?.nearPressureMultiplier ?? DEFAULT_FLUIDS_NEAR_PRESSURE_MULTIPLIER,
6288
- nearThreshold: opts?.nearThreshold ?? DEFAULT_FLUIDS_NEAR_THRESHOLD,
6289
- enableNearPressure: opts?.enableNearPressure ?? DEFAULT_FLUIDS_ENABLE_NEAR_PRESSURE ? 1 : 0,
6290
- maxAcceleration: opts?.maxAcceleration ?? DEFAULT_FLUIDS_MAX_ACCELERATION,
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: precompute density and near-density per particle
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
- // Predict current particle position for this frame (approximate)
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
- // Precompute radius powers for kernels
6356
- let r2 = rad * rad;
6357
- let r4 = r2 * r2;
6358
- let r6 = r4 * r2;
6359
- let r8 = r4 * r4;
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
- // Iterate neighbors using the spatial grid
6362
- var it = neighbor_iter_init(posPred, rad);
6363
- loop {
6364
- let j = neighbor_iter_next(&it, index);
6365
- if (j == NEIGHBOR_NONE) { break; }
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
- let d = posPred - other.position;
6371
- let dist2 = dot(d, d);
6372
- if (dist2 <= 0.0) { continue; }
6373
- let dist = sqrt(dist2);
6374
- let factor = max(rad - dist, 0.0);
6375
- if (factor <= 0.0) { continue; }
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
- // Density kernel (CPU: (factor^2) / (pi/6 * r^4))
6378
- let kDensity = (factor * factor) / ((3.14159265 / 6.0) * r4);
6379
- density = density + kDensity * 1000.0 * other.mass;
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
- // Near-density kernel (CPU: (factor^4) / (pi/15 * r^6))
6382
- if (${getUniform("enableNearPressure")} != 0.0) {
6383
- let f2 = factor * factor;
6384
- let kNear = (f2 * f2) / ((3.14159265 / 15.0) * r6);
6385
- nearDensity = nearDensity + kNear * 1000.0 * other.mass;
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
- // Store results in shared SIM_STATE for use in apply pass
6390
- ${setState("density", "density")};
6391
- ${setState("nearDensity", "nearDensity")};
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: compute pressure and viscosity forces using precomputed densities
6394
- apply: ({ particleVar, getUniform, getState }) => `{
6395
- let rad = ${getUniform("influenceRadius")};
6396
- let targetDensity = ${getUniform("targetDensity")};
6397
- let pressureMul = ${getUniform("pressureMultiplier")};
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
- // Derivative kernel (CPU: scale * (dist - rad), scale = (-12/pi)/r^4)
6428
- let scale = (-12.0 / 3.14159265) / r4;
6429
- 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")};
6430
6510
 
6431
- // Neighbor pressures from precomputed densities
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
- // Gradient contribution
6440
- gradSum = gradSum + (dir * (effectivePressure * slope) / dN);
6441
- }
6442
- // Pressure force is negative gradient
6443
- var pressureForce = -gradSum;
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
- // Viscosity accumulation
6446
- var viscosityForce: vec2<f32> = vec2<f32>(0.0, 0.0);
6447
- if (visc != 0.0) {
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(&it2, index);
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
- // Viscosity kernel (CPU: (max(0, r^2 - d^2)^3) / (pi/4 * r^8))
6461
- let val = max(0.0, r2 - dist2);
6462
- let kVisc = (val * val * val) / ((3.14159265 / 4.0) * r8);
6463
- viscosityForce = viscosityForce + (other.velocity - ${particleVar}.velocity) * kVisc;
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
- viscosityForce = viscosityForce * visc;
6466
- }
6550
+ // Pressure force is negative gradient
6551
+ var pressureForce = -gradSum;
6467
6552
 
6468
- // Convert to acceleration-like effect: a = F / density
6469
- var force = (pressureForce / myDensity) * 1000000.0;
6470
- if (visc != 0.0) {
6471
- force = force + (viscosityForce * 1000.0) / myDensity;
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
- // Clamp force magnitude to avoid instabilities (tunable)
6475
- let maxLen = ${getUniform("maxAcceleration")};
6476
- let f2 = dot(force, force);
6477
- if (f2 > maxLen * maxLen) {
6478
- let fLen = sqrt(f2);
6479
- force = force * (maxLen / fLen);
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
- // Apply directly to velocity (CPU mirrors this behavior)
6483
- ${particleVar}.velocity = ${particleVar}.velocity + force;
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: precompute density and near-density per particle
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
- // Get fluid parameters
6493
- const rad = this.readValue("influenceRadius");
6494
- const enableNearPressure = this.readValue("enableNearPressure");
6495
- // Predict current particle position for this frame (approximate)
6496
- const posPredX = particle.position.x + particle.velocity.x * dt;
6497
- const posPredY = particle.position.y + particle.velocity.y * dt;
6498
- let density = 0.0;
6499
- let nearDensity = 0.0;
6500
- // Precompute radius powers for kernels
6501
- const r2 = rad * rad;
6502
- const r4 = r2 * r2;
6503
- const r6 = r4 * r2;
6504
- // Get neighbors using predicted position
6505
- const neighbors = getNeighbors({ x: posPredX, y: posPredY }, rad);
6506
- for (const other of neighbors) {
6507
- if (other.id === particle.id)
6508
- continue; // Skip self
6509
- // Skip removed or pinned neighbors
6510
- if (other.mass <= 0)
6511
- continue;
6512
- const dX = posPredX - other.position.x;
6513
- const dY = posPredY - other.position.y;
6514
- const dist2 = dX * dX + dY * dY;
6515
- if (dist2 <= 0.0)
6516
- continue;
6517
- const dist = Math.sqrt(dist2);
6518
- const factor = Math.max(rad - dist, 0.0);
6519
- if (factor <= 0.0)
6520
- continue;
6521
- // Density kernel: (factor^2) / (pi/6 * r^4)
6522
- const kDensity = (factor * factor) / ((Math.PI / 6.0) * r4);
6523
- density += kDensity * 1000.0 * other.mass;
6524
- // Near-density kernel: (factor^4) / (pi/15 * r^6)
6525
- if (enableNearPressure !== 0.0) {
6526
- const f2 = factor * factor;
6527
- const kNear = (f2 * f2) / ((Math.PI / 15.0) * r6);
6528
- nearDensity += kNear * 1000.0 * other.mass;
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
- // Store results in shared state for use in apply pass
6532
- setState("density", density);
6533
- setState("nearDensity", nearDensity);
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
- // Pressure force is negative gradient
6588
- let pressureForceX = -gradSumX;
6589
- let pressureForceY = -gradSumY;
6590
- // Viscosity accumulation
6591
- let viscosityForceX = 0.0;
6592
- let viscosityForceY = 0.0;
6593
- if (visc !== 0.0) {
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
- // Viscosity kernel: (max(0, r^2 - d^2)^3) / (pi/4 * r^8)
6609
- const val = Math.max(0.0, r2 - dist2);
6610
- const kVisc = (val * val * val) / ((Math.PI / 4.0) * r8);
6611
- viscosityForceX += (other.velocity.x - particle.velocity.x) * kVisc;
6612
- viscosityForceY += (other.velocity.y - particle.velocity.y) * kVisc;
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
- viscosityForceX *= visc;
6615
- viscosityForceY *= visc;
6616
- }
6617
- // Convert to acceleration-like effect: a = F / density
6618
- let forceX = (pressureForceX / myDensity) * 1000000.0;
6619
- let forceY = (pressureForceY / myDensity) * 1000000.0;
6620
- if (visc !== 0.0) {
6621
- forceX += (viscosityForceX * 1000.0) / myDensity;
6622
- forceY += (viscosityForceY * 1000.0) / myDensity;
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
- // Clamp force magnitude to avoid instabilities
6625
- const f2 = forceX * forceX + forceY * forceY;
6626
- if (f2 > maxAccel * maxAccel) {
6627
- const fLen = Math.sqrt(f2);
6628
- forceX = forceX * (maxAccel / fLen);
6629
- forceY = forceY * (maxAccel / fLen);
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