A mesoscopic detour around Navier-Stokes
Most computational fluid dynamics codes attack the Navier-Stokes equations head-on: discretise the macroscopic fields — density ρ, velocity u, pressure p — on a mesh and march them forward, typically requiring a global pressure-Poisson solve every step to keep the flow divergence-free. The Lattice-Boltzmann Method (LBM) takes a different route entirely, borrowed from kinetic theory: instead of macroscopic fields, it tracks populations of fictitious particles hopping between neighbouring lattice cells and colliding locally. Density and velocity are recovered afterward as simple sums — fluid behaviour is not imposed, it emerges.
The continuous Boltzmann transport equation, ∂f/∂t + ξ·∇f = Ω(f), splits fluid evolution into a streaming term (particles moving freely) and a collision term Ω that pushes the distribution f back toward local equilibrium. LBM's central trick is to discretise both space and velocity at once: space becomes a regular grid, and the continuous velocity space collapses to a handful of fixed directions per node. Despite the drastic simplification, a Chapman-Enskog expansion shows the resulting update recovers incompressible Navier-Stokes to second order in the Mach number.
D2Q9 — nine directions per node
The most common 2D lattice is called D2Q9: 2 dimensions, 9 discrete velocity directions per node — the eight compass directions plus staying still. Each direction carries a weight chosen so that the lattice correctly reproduces the mass and momentum moments of the continuous equilibrium distribution:
e0=(0,0) rest w0 = 4/9 e1..4 = axis-aligned N/E/S/W w1..4 = 1/9 e5..8 = diagonal NE/NW/SE/SW w5..8 = 1/36 // Σ w_i = 1 → conserves mass // speed of sound on the lattice: c_s² = 1/3
Collide, then stream
Every time step does exactly two things at every node. First, collision: each of the nine populations f_i relaxes toward its local equilibrium value f_i^eq at a rate set by the relaxation time τ — the BGK approximation (Bhatnagar-Gross-Krook), which replaces the full collision integral with a single linear relaxation:
f_i* = f_i − (1/τ) · (f_i − f_i^eq) // collision f_i^eq = w_i · ρ · [1 + 3(e_i·u) + 4.5(e_i·u)² − 1.5·u²] kinematic viscosity: ν = (τ − 0.5) / 3 // τ sets viscosity directly
Second, streaming: each post-collision population f_i* is shifted exactly one cell in its own direction e_i, so f_i(x + e_i, t+1) = f_i*(x, t). The collision step is entirely local — every node updates independently of its neighbours, using only its own nine numbers — while streaming needs only first-neighbour data. That locality is precisely why LBM parallelises so well on GPUs: there is no global linear system to solve at any point in the loop.
τ must stay above 0.5 for stability, and in practice the Mach number |u|/c_s should stay comfortably below about 0.3 too — push either limit and the low-Mach assumption underlying the whole scheme breaks down and the simulation blows up.
Recovering density and velocity
The macroscopic quantities that a user actually cares about — density and velocity — are just moments of the nine populations sitting at a node, computed with a single pass over local data:
ρ(x,t) = Σ_i f_i(x,t) // zeroth moment: mass ρ·u_x(x,t) = Σ_i f_i(x,t) · e_ix // first moment: momentum ρ·u_y(x,t) = Σ_i f_i(x,t) · e_iy pressure (ideal-gas form, lattice units): p = ρ·c_s² = ρ/3
Solid boundaries use the simplest no-slip trick available: bounce-back — a population arriving at a wall node is reflected straight back the direction it came from, which reproduces zero velocity at the wall using nothing but local operations. Inlets and outlets with a prescribed velocity or pressure use the more involved Zou-He scheme, which analytically solves for the unknown incoming populations at the boundary.
How the simulation here uses it
The simulation on this site runs the full collide-stream cycle on a D2Q9 grid every frame, with a solid obstacle placed in a uniform inflow and bounce-back applied at its boundary. At a modest grid resolution the entire loop — collision, streaming, boundary handling and a colour-by-speed render — runs comfortably at 60 fps in plain JavaScript on typed arrays; no WebGL compute pass is needed until the grid gets much larger. Watch the flow at moderate Reynolds number and you'll see the classic Kármán vortex street emerge behind the obstacle — alternating vortices peeling off, entirely as a consequence of the same local collision rule running at every node, with no vortex-shedding logic written anywhere in the code.
Frequently asked questions
Why does the Lattice-Boltzmann Method simulate fictitious particles instead of solving Navier-Stokes directly?
LBM works at a mesoscopic level, tracking particle-population distributions on a small fixed set of lattice directions and letting a simple local collision rule relax them toward equilibrium. The Chapman-Enskog expansion shows this recovers the incompressible Navier-Stokes equations as an emergent consequence, while the actual per-step computation is only local additions and lookups, which parallelises far more naturally than solving a global pressure-Poisson equation every step.
What does the relaxation time τ actually control in the BGK collision step?
τ sets the kinematic viscosity of the simulated fluid, via ν = (τ − 0.5)/3 in lattice units. Larger τ means a more viscous, syrupy fluid; τ approaching 0.5 means viscosity approaching zero. The simulation becomes numerically unstable as τ gets too close to 0.5, especially at higher flow velocities, which is why real implementations keep the Mach number small and τ comfortably above that bound.
Why is bounce-back used for solid walls instead of just setting velocity to zero?
Bounce-back reflects each incoming particle population straight back the direction it came from at the wall node, which reproduces a no-slip boundary condition (zero velocity at the wall) purely through local, first-neighbour operations that fit LBM's streaming structure. Directly imposing velocity would require solving for consistent distribution functions at the boundary, which the Zou-He scheme does when a specific inflow or outflow velocity needs to be enforced rather than a simple solid wall.
Try it live
Everything above runs in your browser — open Lattice-Boltzmann Flow and watch a vortex street form behind the obstacle in real time. Nothing is installed, nothing is uploaded, the whole model lives in one tab.
▶ Open Lattice-Boltzmann Flow simulation