Lesson 01 · The velocity field
A fluid is a field of little arrows
We don't track individual molecules. Instead the flow is a velocity field: at every point in space there is an arrow, u(x, y), giving the speed and direction of the fluid there. In code this is simply a 2D grid where every cell stores one velocity vector.
Every rule that follows is just a recipe for how this grid of arrows changes from one time step to the next.
Lesson 02 · Advection
The flow carries things along with it
Drop dye into a river and the current sweeps it downstream, stretching it into ribbons. That transport of stuff, and of the fluid's own velocity, by the flow is called advection. The usual way to compute it: for each cell, step backwards along the velocity and copy whatever was there a moment ago (this is "semi-Lagrangian" advection, and it stays stable at any step size).
In the equation
The fluid advects itself: this term says the velocity is carried along by the velocity. That feedback is what makes fluids swirl, and also what makes the equations hard to solve.
Lesson 03 · Vorticity
Straight-looking flow can still spin
Here the flow is perfectly horizontal: every arrow points sideways. But the top moves faster than the bottom (a shear), and that difference makes little paddle-wheels turn. That local spin is the vorticity, ω = ∇×u. It is worth tracking because it is where the visible structure in a fluid lives.
Smoke rings, whirlpools, and the eddies trailing a canoe paddle are all concentrated vorticity. Many solvers even add a little of it back each step (called vorticity confinement) because coarse grids tend to smear these swirls away.
Lesson 04 · Viscosity & diffusion
Friction smears sharp motion smooth
Real fluids have internal friction. A fast patch drags its slower neighbours along and gets pulled back, so sharp differences blur out over time. That smoothing is exactly the Laplacian, ∇², and how strongly it acts is the viscosity ν. In code it is a blur: repeatedly nudge each cell toward the average of its four neighbours.
In the equation
The same Laplacian that blurs this pattern also diffuses the fluid's velocity. That is the viscous term.
Lesson 05 · Pressure & incompressibility
You can't squash water into a corner
Water is (almost) incompressible: you can't pile more into a spot without the same amount leaving. So the velocity field has to be divergence-free, ∇⋅u = 0. Wherever the flow starts to pile up, pressure builds and pushes back to cancel it.
In the equation
Solving for the pressure that keeps ∇⋅u = 0 everywhere is the projection step. It is a Poisson equation, and it is usually the slowest part of a solver, so it is the first place people optimise.
Lesson 07 · The Kármán vortex street
Why a flag flutters and a wire hums
So far the fluid has only reacted to you. Now watch it move on its own. Send a steady stream past a blunt obstacle like a chimney or a wire. At low speed the stream parts and closes behind it, calm and symmetric. Past a certain speed that wake goes unstable: the obstacle sheds spinning vortices, first from one side, then the other, in a staggered double row trailing downstream. This is the Kármán vortex street.
The alternating vortices tug the obstacle sideways at a steady beat. That is what makes a flag ripple and a power line hum in the wind. If the shedding frequency matches a structure's own natural sway it can shake the structure apart, which is part of what happened to the Tacoma Narrows Bridge in 1940.
The one number that decides
Which regime you get is set almost entirely by the Reynolds number: the ratio of inertia (flow speed U times obstacle size L) to viscosity ν. It is dimensionless, so a marble in syrup and a submarine in the sea behave alike if their Reynolds numbers match. The street lives roughly between Re ≈ 50 and a few hundred; below that the wake is steady, above it turns turbulent.
Under the hood
How this is simulated, and why C++ and Vulkan
Every demo here uses the same recipe, the one Jos Stam published in 1999 as Stable Fluids. Space is a regular grid, and each frame the velocity field runs through a short pipeline: advect it along itself, optionally diffuse it for viscosity, then project it (solve for the pressure that removes any divergence) so the flow stays incompressible. The pressure solve is a Poisson equation, handled by repeatedly averaging each cell with its neighbours (a Jacobi iteration). That is the whole solver: five short passes over a grid.
The key fact for performance is that every cell updates from its immediate neighbours and nothing else. There is no global dependency, so a million cells can be updated at once instead of one after another. That is exactly the kind of problem a GPU is built for: thousands of tiny cores running the same short program over different data, while a CPU mostly works through cells in sequence. This page is the proof; it runs the identical algorithm on your GPU through WebGL2.
So why does the native version reach for C++ and Vulkan?
Once the whole thing runs on the GPU, you want the most direct, cross-vendor control the hardware offers, and you want the simulation and its rendering on one API so the fluid never makes a slow round-trip back to the CPU just to be drawn. Vulkan gives that: explicit control of memory and synchronisation, real compute shaders writing into storage buffers, low driver overhead, and it renders too. Here is how the realistic options compare:
| Choice | Why you'd pick it | What it costs |
|---|---|---|
| Vulkan + C++ | Cross-vendor (AMD, Intel, NVIDIA, mobile); compute and rendering in one API with no CPU round-trip; explicit, low-overhead control | Enormous boilerplate; you manage memory, queues and synchronisation by hand |
| CUDA | By far the friendliest GPU-compute tooling and libraries; great profilers | NVIDIA-only; you still need a separate graphics API to display the result |
| OpenGL / WebGL | Much simpler; portable; WebGL needs zero install, ideal for teaching (it runs this page) | Sandboxed and higher-overhead; limited compute and precision; not built for a research-grade solver |
| WebGPU | Modern, portable, real compute shaders, runs in a browser and natively | Still maturing; less raw control and headroom than native Vulkan |
| Rust + wgpu / ash | Memory safety without a garbage collector; the same GPU power as C++ | Younger ecosystem; the canonical Vulkan SDK, samples and tutorials are still C/C++-first |
C++ earns its place on the native side for practical reasons: the official Vulkan SDK is C/C++, so the samples, headers and tooling all assume it; you get manual memory control with no garbage-collector pauses mid-frame; and it links straight to the usual graphics libraries (GLFW for windowing, GLM for maths). The cost is real: it is verbose and unforgiving, and a modern project might reasonably pick Rust to keep the same speed with far fewer memory bugs.
An honest verdict. If the only goal were to teach fluid dynamics, C++ and Vulkan would be overkill; a WebGPU or WebGL page like this one reaches far more people with a fraction of the code. Vulkan pays off when the goal shifts from explaining to pushing the hardware: maximum control, cross-vendor performance, and one pipeline for both compute and rendering at large grid sizes. That is why this repository keeps both: a browser page to build the intuition, and a native Vulkan engine to take it as far as the silicon allows. If you are writing your first fluid solver, start where this page starts (a grid, five passes, WebGL or WebGPU), and reach for Vulkan only once you actually need the headroom.