Radek, a self-taught engineer who now works as a DevOps engineer, wanted to understand how fluid simulations in games actually work and found the learning material sparse and hard to follow. So he built one from scratch in GDScript inside Godot and wrote it up — CPU-only, deliberately redundant variables, readability over frame rate.
The whole thing is drawn with Godot’s own _draw(), so density and velocity arrows appear on screen as each piece lands. Every chapter links a project snapshot and a git diff, which is a better teaching device than a final repo dump.
The structure of the solver, in the order it is built:
- Grid first —
Ncells per side plus a one-cell border, stored as flatPackedFloat32Arrays:density,uandvfor horizontal and vertical velocity, each with a_prevsnapshot so iterative solvers never read values they are mid-way through overwriting. - Density injection — click to add density, drag to add velocity and density together, turning the mouse into a live debugging tool for the field.
- Diffusion — Gauss-Seidel relaxation, ~20 iterations per frame: each iteration anchors on the snapshot value, adds the four neighbours, and divides by
1.0 + 4.0 * aso five cells’ worth of density doesn’t inflate the result. - Boundaries — border cells copy their real neighbour’s density, and velocity gets its wall-normal component inverted so the border reflects instead of leaking.
- Advection and projection — velocity is advanced along the field with bilinear interpolation, then the pressure gradient is subtracted (
0.5 * (p[i+1] - p[i-1]) / h) after both diffusion and advection, because each step introduces divergence.
Three deliberate accuracy trades make it viable in a game loop at all: a small grid with large cells, arbitrary time steps rather than fixed ones, and approximation instead of exact solutions.
The most useful habit here is that the original condensed Stam set_bnd is kept in the file, unused, next to the expanded version — so you meet the terse form you’ll actually see in the paper with a readable companion right beside it. The author’s own honesty about scope is refreshing too: this is the learning implementation, not the fast one. The stated next step is porting the whole thing to a Godot compute shader, on the reasoning that “textures [are] arrays of floats” and should hold density, velocity, divergence and pressure just as well as the CPU arrays do.