Physics Simulation · Numerical Methods · Computer Graphics
📅 March 2026 ⏱ ≈ 14 min read 🎯 Advanced

Material Point Method (MPM) — How to Simulate Snow, Sand, and Soft Bodies

The Material Point Method is the simulation technique behind Disney's Frozen snowfall, Taichi's avalanche in Coco, and real-time sand destruction in many modern games. It is a hybrid approach: Lagrangian particles carry material properties and deformation history, while an Eulerian background grid handles collision detection and force integration — giving the best of both worlds.

1. Why MPM? Lagrangian vs Eulerian

Continuum mechanics simulations split into two families:

MPM sits in between: particles carry the material history (deformation gradient, stress, velocity, density) while a fresh background grid is recreated every time step to handle contact, pressure projection, and boundary conditions. The grid is discarded after each step — no tangling possible.

Origins: MPM was introduced by Sulsky et al. (1994) as an extension of the Particle-In-Cell (PIC) method from plasma physics. Disney Research's 2013 SIGGRAPH paper ("A Material Point Method for Snow Simulation") brought it to film production, and the open-source Taichi language (2019) made GPU MPM accessible to researchers worldwide.

2. Algorithm Overview: Four Steps per Frame

Each simulation step performs four key operations:

  1. P2G (Particle → Grid): splat particle mass and momentum onto nearby grid nodes using interpolation weights
  2. Grid forces: compute and apply forces (elastic stress, gravity, collision impulses) to grid node velocities
  3. Boundary conditions: enforce walls, sticky/slippery surfaces, objects on the grid
  4. G2P (Grid → Particle): update particle velocities and positions from the grid; update deformation gradient
Per particle p, per grid node i: w_{ip} = weight of node i affecting particle p (quadratic B-spline or cubic, based on grid distance) P2G: m_i += w_{ip} · m_p mv_i += w_{ip} · m_p · v_p (+ APIC angular term) f_i += −w_{ip} · vol_p · σ_p · ∇w_{ip} (elastic force) Grid: v_i^new = (mv_i + dt · f_i) / m_i G2P: v_p += Σ_i w_{ip} · v_i^new (PIC reconstruction) x_p += dt · v_p F_p = (I + dt · Σ_i v_i ⊗ ∇w_{ip}) · F_p (deform. gradient)

3. P2G — Particle-to-Grid Transfer

Each particle affects the nearest 3×3 (2D) or 3×3×3 (3D) grid nodes. The interpolation kernel is a quadratic B-spline — chosen for its compact support (2 cells), C¹ continuity, and partition of unity property:

1-D quadratic B-spline weight for distance d = |x_p − x_i| / h: N(d) = 3/4 − d² if d ≤ 1/2 (3/2 − d)² / 2 if 1/2 < d ≤ 3/2 0 otherwise 2-D: w_{ip} = N(|(x_p − x_i)/h|) · N(|(y_p − y_i)/h|) Partition of unity: Σ_i w_{ip} = 1 (total weight sums to 1) Completing interpolation: x_p = Σ_i w_{ip} · x_i (recovers particle pos)

The elastic stress contribution to the grid force comes from the First Piola-Kirchhoff stress P = ∂ψ/∂F, where ψ is the strain energy density and F is the deformation gradient:

Grid force from particle p: f_i += −vol_p^0 · P_p · F_p^T · ∇w_{ip} where vol_p^0 = initial particle volume (conserved by mass / ρ₀) P_p = first Piola-Kirchhoff stress tensor F_p = deformation gradient (3×3 matrix, initially I)

4. Grid Update — Forces and Boundary Conditions

Once mass and momentum are rasterized onto the grid, forces are integrated explicitly:

v_i^{n+1} = v_i^n + dt · (f_i / m_i + g) g = gravitational acceleration [0, −9.8] m/s² Boundary conditions (sticky wall at x = 0): if (x_i ≤ 0 && v_i.x < 0): v_i.x = 0 // no penetration Optionally also: v_i.y = 0 // friction (sticky) Or just: (leave v_i.y unchanged) // frictionless

Imposing BCs on the grid is trivial compared to Lagrangian FEM where contact requires complex gap-function formulations. This is one of MPM's key practical advantages.

5. G2P — Grid-to-Particle Transfer

After the grid is updated, information flows back to particles. The critical operation is updating the deformation gradient F, which encodes how the material has stretched and sheared since the start of the simulation:

Velocity gradient at particle p: (∇v)_p = Σ_i v_i^new ⊗ ∇w_{ip} (outer product) Update deformation gradient: F_p^{n+1} = (I + dt · (∇v)_p) · F_p^n This is the multiplicative update of the deformation gradient. For hyperelastic materials, J = det(F) tracks volume change: J = 1: incompressible flow J < 1: compression J > 1: expansion (tension)

Particle positions are updated last, after the plastic return mapping has clamped F to the yield surface (for snow/sand models):

x_p^{n+1} = x_p^n + dt · v_p^{n+1}

6. Constitutive Models

The deformation gradient F drives the stress computation. Different materials use different strain energy densities:

Neo-Hookean Elastic Solid

ψ(F) = μ/2 · (tr(FᵀF) − 3) − μ·ln(J) + λ/2·ln(J)² First PK stress: P = μ(F − F⁻ᵀ) + λ·ln(J)·F⁻ᵀ Lamé parameters from Young's modulus E and Poisson ratio ν: μ = E / (2(1+ν)) λ = Eν / ((1+ν)(1−2ν))

Snow (Disney 2013)

Snow fractures under tension and compacts under compression. This is modeled via multiplicative plasticity:

F = F_E · F_P (elastic × plastic decomposition) SVD of F_E: F_E = U · Σ · Vᵀ Clamp singular values (stretch limits): Σ_i = clamp(Σ_i, 1 − θ_c, 1 + θ_s) θ_c = critical compression (e.g. 0.025) θ_s = critical stretch (e.g. 0.0075) Hardening: μ, λ scaled by exp(ξ(1 − J_p)) ξ = hardening coefficient ≈ 10 (fresh snow) to 0 (wet slush)

Drucker-Prager Sand

Sand has a friction-angle yield criterion — it can sustain compression but not tension, and shear strength scales with pressure:

Yield surface (Drucker-Prager): f(σ) = ||dev(σ)|| + sin(φ)·tr(σ) ≤ 0 φ = friction angle (sand: ~30°) Dilatancy: volume change on plastic shear Return mapping: project stress to closest point on yield surface If in tension (tr(σ) > 0): fracture → F = I, zero stress

Weakly Compressible Fluid (SPH-like pressure)

Equation of state pressure: p = k₀ · ((J⁻γ) − 1) k₀ = bulk modulus, γ = 7 (water) Cauchy stress: σ = −pI (isotropic pressure only) No shear stress → inviscid fluid approximation

7. APIC and FLIP Variants

The basic PIC scheme (reconstruct velocity purely from grid) is numerically diffusive — angular momentum is lost. Two improvements exist:

APIC P2G (particle → grid): mv_i += w_{ip} · m_p · (v_p + C_p · (x_i − x_p)) APIC G2P (grid → particle): v_p = Σ_i w_{ip} · v_i C_p = (4/h²) · Σ_i w_{ip} · v_i ⊗ (x_i − x_p) (velocity gradient)
MLS-MPM (Moving Least Squares, Hu et al. 2018): Further unifies APIC with quadrature-based force computation. The grid force term naturally falls out of the APIC momentum transfer, halving code complexity. The 88-line Taichi implementation of MLS-MPM can simulate elastic, snow, fluid, and sand with just a constitutive model swap.

8. MLS-MPM Core Loop in JavaScript

The following shows the essential 2-D MLS-MPM loop — streamlined for clarity. A full runnable implementation with canvas rendering needs ~200 lines:

// MLS-MPM core: 64×64 grid, quadratic B-spline weights
const n = 64; // grid resolution
const dt = 1e-4;
const dx = 1 / n;
const inv_dx = n;

// Particle state
let pos, vel, C, J; // position, velocity, affine matrix, volume ratio

function mpmStep() {
  // 1. Reset grid
  const gm  = new Float32Array(n * n);      // mass
  const gmv = new Float32Array(n * n * 2); // momentum xy

  // 2. P2G
  for (let p = 0; p < pos.length; p++) {
    const [px, py] = pos[p];
    const base = [Math.floor(px * inv_dx - .5), Math.floor(py * inv_dx - .5)];

    // Quadratic B-spline weights  (3 nodes per axis)
    const fx = [px * inv_dx - base[0], px * inv_dx - base[0] - 1, px * inv_dx - base[0] - 2];
    const fy = [py * inv_dx - base[1], py * inv_dx - base[1] - 1, py * inv_dx - base[1] - 2];
    const wx = [.5 * (1.5-fx[0])**2, .75-fx[1]**2, .5*(.5+fx[2])**2];
    const wy = [.5 * (1.5-fy[0])**2, .75-fy[1]**2, .5*(.5+fy[2])**2];

    // MLS-MPM stress term (Neo-Hookean fluid: P = k₀(1 - 1/J) I)
    const stress = -dt * 4 * inv_dx * inv_dx * 400 * (1 - 1 / J[p]);

    for (let di = 0; di < 3; di++)
      for (let dj = 0; dj < 3; dj++) {
        const w = wx[di] * wy[dj];
        const ix = base[0] + di, iy = base[1] + dj;
        if (ix < 0 || ix >= n || iy < 0 || iy >= n) continue;
        const idx = ix * n + iy;
        const dpos = [(di - fx[di]) * dx, (dj - fy[dj]) * dx];
        gm[idx]        += w;
        gmv[idx*2]   += w * (vel[p][0] + C[p][0]*dpos[0] + stress * dpos[0]);
        gmv[idx*2+1] += w * (vel[p][1] + C[p][3]*dpos[1] + stress * dpos[1]);
      }
  }
  // 3. Grid update: normalize by mass, add gravity, enforce boundary
  // 4. G2P: reconstruct velocity and update F, x  (loop over particles)
}
Performance note: A naive JS implementation handles ~5,000 particles at 60 fps. WebGL compute shaders (Transform Feedback or WebGPU compute) can push this to 100,000–1,000,000 particles, since all P2G and G2P loops are embarrassingly parallel per-particle.
🏖️ Try the Sand Simulation →