Web-LBM: A Real-Time CFD Wind Tunnel in the Browser

The von Kármán vortex street behind a cylinder, computed and rendered live in the browser

In one minute

Web-LBM is an interactive two-dimensional wind tunnel that runs entirely in your browser. Behind the canvas sits a genuine computational fluid dynamics (CFD) solver: a Lattice Boltzmann (D2Q9) engine written in C, compiled to WebAssembly, and rendered on the GPU through WebGL2. You can drop in a cylinder or a NACA airfoil, change the angle of attack, spin an obstacle to feel the Magnus effect, or paint your own geometry with the mouse and watch the wake respond. Lift and drag coefficients update live, and virtual microphones even let you see the sound of vortex shedding.

CFD normally lives on clusters and overnight batch jobs. Bringing it into a browser tab, with no install, no server, no framework, and hosted as plain static files on GitHub Pages, is both a pedagogical statement and an engineering exercise. Every constraint of the platform had to be solved without giving up physical fidelity or interactivity: no shared memory by default, no filesystem, and a single thread unless you earn more.

👉 Try the simulation · Source code on GitHub

Highlights

  • A real solver. D2Q9 Lattice Boltzmann with BGK (or compile-time MRT) collision, a Smagorinsky LES turbulence model, Hermite-regularized collision for stability, a Zou–He velocity inlet, absorbing sponge layers, and momentum-exchange force evaluation that yields live $C_l$ and $C_d$.
  • Real-time performance. A branchless, SIMD-vectorized hot loop combined with WebAssembly threads (pthreads over SharedArrayBuffer) and a deterministic, lock-free row partition.
  • Zero-copy rendering. Field data flows from the wasm heap to the screen with no intermediate copy: typed-array views feed an R32F WebGL2 texture, and a colormap lookup runs in the fragment shader.
  • Aeroacoustics included. Because LBM is weakly compressible, the engine resolves acoustic waves. It offers schlieren ($|\nabla\rho|$) and dilatation ($\nabla\cdot\mathbf{u}$) views, plus virtual microphones with a live FFT that picks up the aeolian tone of the shedding wake.
  • 100 % static deployment. Two engine builds (single- and multi-threaded) are selected at runtime, and a service worker supplies the cross-origin isolation headers that GitHub Pages cannot send.

Why CFD in the browser?

Three reasons, in increasing order of difficulty.

Pedagogy. Fluid dynamics is taught with equations and validated with expensive simulations, but intuition comes from immediate cause and effect: tilt an airfoil and watch it stall, spin a cylinder and watch the lift appear. An interactive simulation whose feedback loop is measured in milliseconds is a fundamentally different learning tool from a static plot.

Accessibility. A browser page with no install and no backend reaches anyone (students, colleagues, recruiters) on any machine, indefinitely, at zero hosting cost. That requires the entire solver to run client-side, which is precisely what WebAssembly makes possible.

The engineering challenge. Real-time CFD stress-tests the whole stack at once: a numerical method stable enough to survive whatever the user throws at it, a memory layout the compiler can vectorize, a threading model that fits the browser’s security constraints, and a rendering path fast enough never to be the bottleneck. Solving all four together is the core of this project.

The physics: Lattice Boltzmann in brief

A different route to fluid dynamics

Classical CFD discretizes the Navier–Stokes equations directly: velocity and pressure live on a mesh, and each timestep requires solving coupled, global systems, typically a pressure Poisson equation whose solution links every cell in the domain to every other.

The Lattice Boltzmann Method (LBM) works one level below the macroscopic description. It evolves particle distribution functions $f_i(\mathbf{x}, t)$, the amount of fluid at node $\mathbf{x}$ moving along a small set of discrete velocities $\mathbf{c}_i$. The macroscopic fields are simply moments of these distributions:

$$\rho = \sum_i f_i, \qquad \rho\,\mathbf{u} = \sum_i f_i\,\mathbf{c}_i$$

A Chapman–Enskog expansion shows that this kinetic system reproduces the incompressible Navier–Stokes equations in the low-Mach-number limit. The fluid viscosity is controlled by a single relaxation time $\tau$:

$$\nu = c_s^2\left(\tau - \tfrac{1}{2}\right)$$

What makes LBM exceptional for this project is its locality: each node updates from its immediate neighbours only. There is no global solve, no linear algebra, and no convergence loop, so the cost per timestep is fixed and predictable, the algorithm parallelizes almost trivially, and the simulation is naturally unsteady. Vortex shedding is not an option you enable; it is what the method computes by default. As a bonus, LBM is weakly compressible, so pressure waves propagate physically and the simulation does acoustics for free.

Collision and streaming

Each timestep reduces to two conceptually simple operations. Collision relaxes the distributions toward a local equilibrium (the BGK approximation):

$$f_i(\mathbf{x}, t^{+}) = f_i(\mathbf{x}, t) - \frac{1}{\tau}\left[f_i(\mathbf{x}, t) - f_i^{\mathrm{eq}}(\rho, \mathbf{u})\right]$$

where the equilibrium is a second-order expansion of the Maxwell–Boltzmann distribution:

$$f_i^{\mathrm{eq}} = w_i\,\rho\left[1 + \frac{\mathbf{c}_i\cdot\mathbf{u}}{c_s^2} + \frac{(\mathbf{c}_i\cdot\mathbf{u})^2}{2c_s^4} - \frac{\mathbf{u}\cdot\mathbf{u}}{2c_s^2}\right]$$

Streaming then shifts each post-collision distribution to the neighbouring node in its direction of travel:

$$f_i(\mathbf{x} + \mathbf{c}_i\,\Delta t,\; t + \Delta t) = f_i(\mathbf{x}, t^{+})$$

Collision is purely local arithmetic; streaming is a pure memory move. Heavy math with no communication, followed by communication with no math, is exactly the structure that SIMD units and multicore processors love, and the implementation below exploits it deliberately.

Why D2Q9

The engine uses the D2Q9 lattice: two dimensions and nine velocities (rest, four axis-aligned, four diagonal) with weights $w_0 = 4/9$, $w_{1\text{–}4} = 1/9$, and $w_{5\text{–}8} = 1/36$. It is the smallest 2D lattice whose velocity moments are isotropic enough to recover Navier–Stokes correctly. Fewer directions would bias the physics along the grid axes, and more would cost memory and bandwidth for no gain at this level of description. Nine float fields per node also set the memory budget: a 600 × 300 grid fits comfortably in a few tens of megabytes of wasm heap.

Boundary conditions

Boundaries are where LBM implementations earn their keep, because after streaming the distributions entering the domain from outside are unknown and must be reconstructed.

  • Inlet. A Zou–He velocity boundary imposes the inflow profile (uniform, shear layer, or jet) by reconstructing the unknown distributions from mass and momentum constraints. A 400-step soft start ramps the velocity after every (re)initialization, so the impulsive start does not launch a shock down the tunnel.
  • Outlet. Zero-gradient extrapolation, backed by an absorbing sponge layer that smoothly raises dissipation near the boundary so vortices and acoustic waves leave the domain instead of reflecting back into it.
  • Obstacles. Bounce-back sends distributions that hit a solid node back the way they came, producing a no-slip wall with no meshing whatsoever. Its moving-wall variant adds the wall-velocity momentum to the reflected populations, which is the entire implementation of the rotating cylinder, with the Magnus effect following from the physics. Summing the momentum exchanged in these reflections gives the aerodynamic force on each body (the live $C_l$ and $C_d$ in the HUD) at almost no cost.
  • Tunnel walls. Selectable free-slip (specular reflection) or no-slip bounce-back.

Implementation

Architecture

The project is two cleanly separated parts joined by one shared memory buffer:

C engine (lbm.c, C99)  --emcc-->  WebAssembly + wasm heap
                                        │ zero-copy Float32Array views
vanilla ES6 frontend  ── WebGL2 R32F textures ──>  colormap LUT in shader
  • The engine is a single C99 file holding the D2Q9 solver, LES, boundary conditions, analytic shape rasterizers (cylinder, flat plate, NACA 0012 / 4412), force evaluation, and derived-field computation (velocity, pressure, vorticity, schlieren, dilatation). It exposes a small flat C API (lbm_init, lbm_step, lbm_set_params, …) and knows nothing about JavaScript.
  • The frontend is vanilla ES6 modules, with no framework, no bundler, and no build step beyond emcc itself. main.js drives the simulation loop and UI, renderer.js owns WebGL2, and obstacles.js defines the analytic presets.
  • Two engine builds come from the same source: a single-threaded variant that runs everywhere, and a -pthread variant using WebAssembly threads. The app feature-detects crossOriginIsolated at boot, picks the best one, and falls back gracefully.

The zero-copy data path

The cardinal rule of the rendering pipeline is that field data is never copied on the JavaScript side. The C engine computes vorticity, say, into a float array in the wasm heap. JavaScript wraps that memory in a Float32Array view (no allocation, no copy) and hands it straight to texSubImage2D, which uploads it into a single-channel floating-point (R32F) texture. The fragment shader then maps values to colour through a small lookup-table texture, so the entire visual styling costs one texture fetch per pixel and lives on the GPU.

Two subtleties keep this honest. When the wasm heap grows (the user picks a finer grid), the underlying ArrayBuffer detaches and every view must be rebuilt, which the app does lazily, on demand. And in the multithreaded build the heap is a SharedArrayBuffer, which some browsers refuse to upload from directly; a single small staging buffer is the one sanctioned exception to the no-copy rule.

Multithreading in a browser

The interior sweep is row-partitioned across a pool of wasm pthreads, with barriers separating the phases of each timestep. The partition is designed so that every slot of the destination array has exactly one producer. Threads never contend, so there are no locks and no atomics in the hot path. Force contributions are reduced in fixed thread order, which makes results bit-deterministic for a given thread count, a property borrowed from serious HPC practice where “fast but slightly different every run” is how bugs hide.

Browser threading comes with a catch: SharedArrayBuffer requires the page to be cross-origin isolated, which needs COOP/COEP HTTP headers that a static host like GitHub Pages cannot send. Web-LBM solves this with a root-scoped service worker that injects the headers client-side (one automatic reload on first visit). If anything in that chain fails (an older browser, partial isolation), the app silently boots the single-threaded engine instead. This is progressive enhancement applied to HPC.

Interaction

Everything in the tunnel is manipulable while the simulation runs. Obstacles can be placed by click, rotated, spun (for rotating cylinders), or painted freehand with the mouse directly into the obstacle mask. Painted cells are resampled when the grid resolution changes, while preset shapes are re-rasterized analytically from their registry, so a NACA profile stays a crisp NACA profile at any resolution. Flow can be visualized as velocity, pressure, vorticity, schlieren, or dilatation, with streamline and particle overlays. Virtual microphones can be dropped anywhere in the field to plot a live FFT of the local pressure signal; placing one behind a cylinder reveals the sharp spectral peak of the shedding frequency, the same physics as the hum of wind through wires.

Engineering challenges

Numerical stability without a referee

An interactive simulation cannot reject user input: whatever Reynolds number, obstacle shape, or resolution the visitor chooses, the solver must not blow up. Plain BGK becomes unstable as $\tau \to 1/2$ (high Reynolds numbers), so the engine layers several defences, each addressing a distinct failure mode.

  • A Smagorinsky LES model ($C_s = 0.18$) computes a local eddy viscosity from the strain rate (captured in LBM directly from the non-equilibrium distributions, with no finite differences). It both models the unresolved turbulent scales and keeps the effective $\tau$ away from the unstable limit exactly where the flow is most strained.
  • Hermite regularization projects the non-equilibrium part of the distributions onto its physically meaningful (second-order Hermite) component before collision, filtering the spurious higher-order modes that otherwise accumulate and destabilize coarse grids.
  • Bulk-viscosity damping selectively dissipates the acoustic part of the stress, taming pressure oscillations without touching the shear physics that creates vortices.
  • Sponge layers and a soft-started inlet prevent the two classic transients, reflected outflow waves and the initial impulse, from contaminating the domain.

The result is a tunnel that survives being abused, which is the difference between a demo and a product.

What you can explore

A few experiments that each take under a minute in the live tunnel:

  1. Vortex shedding. Keep the default cylinder, switch to the vorticity view, and watch the von Kármán street establish itself; the HUD shows $C_l$ oscillating at the shedding frequency.
  2. Stall. Place a NACA 4412 airfoil and increase the angle of attack: the lift coefficient climbs, then the suction-side flow separates.
  3. The Magnus effect. Spin a cylinder and watch the wake deflect and a steady lift force appear, the physics behind curved free kicks.
  4. Aeroacoustics. Switch to the schlieren or dilatation view to see pressure waves radiate from the wake, then drop a microphone behind the cylinder and find the shedding tone in the live spectrum.
  5. Your own geometry. Paint an arbitrary obstacle with the mouse and see how the flow negotiates it.

Skills demonstrated

  • Computational fluid dynamics: the Lattice Boltzmann Method end to end, including lattice choice, collision models (BGK, MRT, regularization), boundary conditions, turbulence modelling (Smagorinsky LES), force evaluation, and aeroacoustics.
  • Numerical methods and scientific computing: stability analysis in practice, identifying failure modes and layering targeted remedies, in a physics engine written in portable C99 with embedded smoke tests.
  • Performance engineering: SIMD-friendly branchless kernels, a structure-of-arrays memory layout, lock-free deterministic multithreading, and profiling-driven separation of hot and cold paths.
  • Software architecture: a strict engine/frontend boundary with a zero-copy data contract across the C/JavaScript frontier, plus runtime feature detection with graceful degradation.
  • Web technologies: WebAssembly (Emscripten, wasm threads, shared memory), WebGL2 (floating-point textures, shader-based colormapping), service workers, and fully static deployment.
  • Interactive scientific visualization and UI design: real-time field rendering, multiple physically meaningful views, and an interface that invites experimentation without a manual.
  • Scientific communication: making graduate-level fluid dynamics tangible, and fun, for a general technical audience.

Repository

The full source is open: the C engine (extensively commented, including every compiler flag and its rationale), the WebGL2 renderer, the build scripts, and the physics smoke tests.

The code lives at github.com/vcaries/web_lbm, and the tunnel itself runs at vcaries.github.io/web_lbm.

Conclusion

Web-LBM compresses the whole span of simulation engineering into a single page that anyone can open: a kinetic numerical method, turbulence modelling, SIMD and multithreaded optimization, GPU rendering, and product-grade interaction design. The Lattice Boltzmann Method is the enabling choice, being local, parallel, naturally unsteady, and quietly capable of acoustics. The browser is the forcing function, since every one of its constraints had to be engineered around rather than waved away. The result is a wind tunnel in a tab, and a demonstration that real scientific computing does not have to live behind a queue on a cluster.

Valentin Caries
Valentin Caries
Research Engineer @ IFPEN · PhD · Scientific Python | CFD | Simulation

I build fast, reliable scientific software for fluid mechanics, from CFD and reduced-order models to data and post-processing pipelines.