Skip to main content

MLSMPMFluidSimulation

MLSMPMFluidSimulation is an experimental WebGPU-only, fixed-capacity two-dimensional fluid solver. Each step uses moving least squares material point method (MLS-MPM) transfers with an APIC affine velocity field to move state from particles to a nodal grid and back.

The class owns its compute pipelines, atomic grid, uniforms, and double-buffered particle state. It records work into the application's CommandEncoder and never submits commands or reads data back. Applications can therefore encode simulation and storage-backed particle rendering in one frame command buffer.

GitHub

Usage

import {MLSMPMFluidSimulation} from '@luma.gl/experimental';

const simulation = new MLSMPMFluidSimulation(device, {
gridSize: [96, 64],
particleCount: 16_384,
seed: 42,
velocityDamping: 0.1,
maxVelocity: 2
});

const commandEncoder = device.createCommandEncoder({id: 'fluid-frame'});
const particleBuffer = simulation.encode(commandEncoder, {
deltaTime: 1 / 960,
gravity: [0, -9.81],
force: pointerDown
? {position: pointerPosition, radius: 0.08, vector: pointerForce}
: undefined
});

particleModel.setBindings({particles: particleBuffer});
particleModel.predraw(commandEncoder);
const renderPass = commandEncoder.beginRenderPass({framebuffer});
particleModel.draw(renderPass);
renderPass.end();

device.submit(commandEncoder.finish());

encode() returns the new current particle buffer. The same buffer is available through simulation.particleBuffer; bind it as read-only storage after the encoded compute step. The public gridBuffer exposes the current atomic grid for diagnostics or specialized consumers. Both buffers remain owned by the simulation and are destroyed with it. Because particle state alternates between two buffers, update a renderer's binding from the return value or accessor after every encode() instead of caching the first buffer.

Stable simulation substeps

One encode(commandEncoder, options) call splits the requested delta into one or more stable substeps. Each substep records four ordered stages in the same compute pass:

  1. Clear the atomic grid.
  2. Scatter particle mass and momentum to a quadratic three-by-three grid stencil.
  3. Convert momentum to velocity, then apply gravity, an optional radial force, global velocity damping, boundary conditions, and the configured velocity limit.
  4. Gather grid velocity back to the output particle buffer, update the APIC affine field and deformation estimate, and advect particles.

The conservative substep bound combines half-cell advection CFL with the configured material wave speed at the maximum supported deformation, grid resolution, and hard velocity limit. stableDeltaTime reports that bound. The solver uses ceil(deltaTime / stableDeltaTime) equal substeps, so one uniform upload safely serves the entire encoded compute pass. To bound command recording, one encode is limited to 128 substeps (512 compute dispatches); encode() throws before recording if the requested delta would exceed that work budget. Reduce deltaTime or solver resolution in that case. maximumSubstepCount, lastSubstepCount, and lastSubstepDeltaTime expose the budget and actual split. getMLSMPMFluidStableDeltaTime() performs the same calculation without allocating GPU resources.

Grid scatter uses signed fixed-point integer atomics so accumulation order does not change mass or momentum sums. Mass and signed momentum scales are the largest safe powers of two up to the shader's precision cap for the configured particle count, mass, and velocity limit. This preserves small-mass stencil contributions while keeping conservative worst-case accumulation below signed 32-bit capacity. The grid stores fixed-point mass and velocity after a complete substep; its signed fields contain momentum only between the scatter and grid-update stages inside that substep.

export type MLSMPMFluidSimulationStepOptions = {
deltaTime: number;
gravity?: readonly [number, number];
force?: {
position: readonly [number, number];
radius: number;
vector: readonly [number, number];
};
};

Positions and force centers use normalized [0, 1] simulation coordinates. The requested frame deltaTime must be at least 1e-7 and no larger than 1 / 30; the solver automatically records smaller numerical substeps when required. Gravity defaults to [0, -9.81].

Construction

new MLSMPMFluidSimulation(device, props?)
PropDefaultMeaning
id'mls-mpm-fluid-simulation'Debug-resource prefix.
gridSize[64, 64]Nodal grid width and height; each dimension must be from 8 through 512.
particleCount8192Fixed particle capacity when initialParticles is omitted; maximum 65,536.
seed1Unsigned 32-bit seed for generated particle jitter.
initialParticlesgenerated blockExplicit normalized positions and optional velocities.
boundaryCells2Solid boundary thickness, at most one quarter of the smaller grid dimension.
particleMass1Uniform particle mass used by transfer and fixed-point capacity planning.
restDensity4Density used to derive particle volume.
stiffness8Isotropic equation-of-state stiffness; zero disables pressure.
velocityDamping0.08Global grid-velocity damping rate in inverse seconds.
maxVelocity16Stability and signed-atomic velocity bound.

When initialParticles is supplied, its length defines the fixed capacity and must match an explicit particleCount. Every particle position must be inside the normalized domain. Accepted generated, explicit, and reset positions are clamped to the configured solid-boundary interior so their complete quadratic transfer stencil lies on the grid. Particle velocity is validated by vector magnitude, which must not exceed maxVelocity.

The public particleBuffer uses a stable 48-byte particle record:

Float offsetValue
0–1Normalized position.
2–3Normalized-domain velocity per second.
4–7Two APIC affine matrix columns.
8Deformation estimate.
9–11Reserved padding.

Reset and diagnostics

simulation.reset(commandEncoder);
simulation.reset(commandEncoder, replacementParticles);

reset() records uploads into both particle buffers, restores buffer A as current, and resets the encode and substep diagnostics. Omitting particles restores the exact generated or explicit constructor seed; supplying particles requires exactly the original fixed count. The caller still submits the encoder. The grid is cleared by the next encoded step, not by a separate reset dispatch.

stats reports grid and buffer sizes, particle and grid workgroup counts, selected fixed-point scales and conservative signed-atomic bounds, the per-substep stage order, stable delta, public encode count, total numerical step count, and most recent substep split. getMLSMPMFluidFixedPointBounds() exposes the same scale and capacity calculation without creating GPU resources. MLS_MPM_FLUID_STAGE_ORDER publishes the per-substep diagnostic stage names.

Support and lifecycle

getMLSMPMFluidSimulationSupport(device, props?) validates props and checks for WebGPU, three compute-stage storage buffers, 64 compute invocations per workgroup, storage-buffer capacity, and dispatch limits. Construction performs the same checks before allocating resources.

destroy() is idempotent and releases every owned computation and buffer. Accessing encode() or reset() after destruction throws; application models that borrow particleBuffer or gridBuffer must stop using them before destruction.

This foundation intentionally solves one bounded two-dimensional domain. It does not provide a particle renderer, scene integration, adaptive particle allocation, obstacles beyond the solid domain boundary, or general three-dimensional material simulation.