A hands-on introduction

How fluids move — from a single arrow to the full equations

Almost every fluid you can watch move (smoke, water, coffee) follows the same short list of rules. This page builds them up one at a time, in the order you would add them to your own simulation. We start with the one thing every fluid solver stores: a grid of velocity arrows. Then we add each effect that makes a fluid behave like a fluid: how it carries itself along, how it spins, how friction smooths it, and how pressure stops it piling up. Every idea has a small demo to poke at, and by the end they combine into the Navier–Stokes equations, running live on your GPU.

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.

Try it Move your cursor over the grid: your pointer stirs the field, and every arrow responds. Colour shows speed.

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).

Flow speed
Try it Release dye into the two rotating gyres and watch it get carried and stretched, never created or destroyed, only moved.

In the equation

−(u⋅∇)u

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.

Shear strength
Try it Slide the shear to zero and the wheels stop: no shear, no spin. Flip it negative and they reverse.

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.

Viscosity ν
Try it Turn up ν and the crisp pattern melts into a smooth blur. Water is barely viscous; honey, very.

In the equation

+ν∇2u

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.

red = piling up  ·  blue = thinning out
Try it Move your cursor over the canvas to push fluid outward, and red divergence blooms where it piles up. Now switch on the constraint: the pressure solve wipes it back to zero.

In the equation

−1ρ∇p

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.

Reynolds number
Try it Flow runs left to right; colour is vorticity (clockwise / counter-clockwise). Sweep the Reynolds number to move through the regimes: low Re gives a smooth, attached wake; the middle band sheds the clean alternating street; crank it up and the wake breaks into busy turbulence. Drag the circle to disturb it at any time.

The one number that decides

Re=ULν

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:

ChoiceWhy you'd pick itWhat it costs
Vulkan + C++Cross-vendor (AMD, Intel, NVIDIA, mobile); compute and rendering in one API with no CPU round-trip; explicit, low-overhead controlEnormous boilerplate; you manage memory, queues and synchronisation by hand
CUDABy far the friendliest GPU-compute tooling and libraries; great profilersNVIDIA-only; you still need a separate graphics API to display the result
OpenGL / WebGLMuch 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
WebGPUModern, portable, real compute shaders, runs in a browser and nativelyStill maturing; less raw control and headroom than native Vulkan
Rust + wgpu / ashMemory 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.