Why naive fluid solvers explode
The Navier-Stokes equations describe how incompressible fluids move, and the textbook way to simulate them numerically is an explicit finite-difference scheme: advance velocity forward by a small time step, using the current field's own derivatives. Explicit schemes like that are only stable if the time step stays below a limit set by the CFL condition — roughly, nothing may cross more than one grid cell per step — and honouring that limit at real-time frame rates would demand hundreds of substeps per frame on any reasonably fine grid. Jos Stam's 1999 SIGGRAPH paper Stable Fluids sidesteps the whole problem: it achieves stability at any time step, however large, by replacing forward integration of velocity with something that literally cannot blow up — interpolation.
The three-step solve
Every frame of a Stable Fluids simulation runs the same short pipeline on its velocity grid: apply external forces, move the field along itself, then correct it so mass is conserved.
addForce(w, dt) // buoyancy, an injected smoke source, wind advect(w, w, dt) // semi-Lagrangian: trace back, bilinear sample project(w) // enforce ∇·w = 0 — incompressibility
Semi-Lagrangian advection: trading accuracy for stability
Instead of asking "where does this cell's content go next," semi-Lagrangian advection asks the reverse question for every cell: "where did this cell's content come from?" It traces a virtual particle backward through the current velocity field by one time step, then reads off the previous frame's value at that point using bilinear interpolation:
for each cell x: x0 = x - dt * velocity(x) // trace backward one time step q(x) = bilinearSample(q_prev, x0) // read the previous field there
Because the update is nothing more than sampling an existing field, it cannot amplify a value beyond what was already present — there is no feedback loop for an error to grow through, so the method is unconditionally stable regardless of how large the time step is. The price is numerical diffusion: bilinear interpolation quietly averages away fine detail every single step, so simulated smoke visibly smears and loses sharp structure faster than real smoke would.
The projection step: making it incompressible
The Helmholtz-Hodge decomposition says any vector field can be split into a divergence-free part and the gradient of a scalar. The projection step exploits that directly: it solves a Poisson equation for a scalar pressure field, then subtracts that pressure's gradient from the velocity field, leaving a result with zero divergence — the discrete equivalent of "no fluid is being created or destroyed at any point."
∇²p = ∇·w // Poisson equation for pressure w = w - ∇p // subtract the pressure gradient → divergence-free velocity
The Poisson equation is solved iteratively on the grid — Gauss-Seidel or Jacobi relaxation is standard — typically 20 to 40 iterations are enough for visually stable, incompressible-looking smoke at interactive frame rates.
Getting the curl back: vorticity confinement
The one real cost of this stable, diffusive scheme is that the fine swirling vortices a real smoke plume develops get smoothed away by repeated interpolation, leaving the result looking flat and mushy instead of turbulent. Fedkiw, Stam and Jensen's 2001 vorticity confinement technique measures the local curl of the velocity field, then adds a small artificial force that pushes fluid toward regions of higher vorticity magnitude — deliberately reinjecting the rotational energy the numerical scheme keeps dissipating, without needing a much finer, far more expensive grid.
ω = ∇ × w // vorticity — the curl of velocity N = ∇|ω| / |∇|ω|| // normalised gradient of vorticity magnitude f_conf = ε * (N × ω) // confinement force added back into w
Why this method still dominates real-time VFX
Unconditional stability at large time steps on a coarse grid is exactly what makes interactive smoke and fire possible in games and browser demos at 60 frames per second at all. Offline film production, using Houdini's FLIP or particle-based solvers, runs at far higher resolution with more physically accurate schemes because it does not have a real-time budget to respect. But the same core recipe — semi-Lagrangian advection, a pressure projection step, and vorticity confinement to restore lost swirl — at higher resolution and with more substeps, still underlies most GPU smoke and fire solvers used in production today, more than two decades after Stam's original paper.
Frequently asked questions
Why is semi-Lagrangian advection unconditionally stable?
It never integrates a forward difference equation that can blow up. Instead it traces each cell backward through the velocity field and interpolates the previous frame's values at that point, and interpolation can only average existing values, never amplify them, no matter how large the time step is.
Why does the simulated smoke look softer than real smoke?
Every semi-Lagrangian step resamples the field with bilinear interpolation, which smooths away fine detail a little more each frame — a numerical side effect called artificial diffusion. Vorticity confinement injects some of that lost swirl back in, but it only partially compensates for the smoothing.
What does the projection step actually enforce?
Incompressibility. It solves a Poisson equation for a scalar pressure field and subtracts its gradient from the velocity field, leaving a result with zero divergence — matching the physical fact that a gas or liquid does not locally compress or expand under typical simulated conditions.
Try it live
Everything above runs in your browser — open Smoke VFX — Stable Fluids and change the parameters while it is running. Nothing is installed, nothing is uploaded, the whole model lives in one tab.
▶ Open Smoke VFX — Stable Fluids simulation