An interactive, real-time 2D fluid dynamics simulator built from scratch.
An interactive, real-time 2D fluid dynamics simulator built from scratch in C++20 using the Stable Fluids method by Jos Stam (SIGGRAPH 1999). No physics engines, no external math libraries — every solver is hand-coded. Watch fluid densities advect and diffuse in stunning real-time mathematical art! ✨
| Key / Action | Description |
|---|---|
| Left Mouse Drag | Inject density (dye) + velocity in drag direction |
| Right Mouse Drag | Inject velocity only (invisible force) |
| V | Toggle velocity vector overlay |
| B | Toggle boundary mode: Solid Wall ↔ Wrap-Around |
| C | Clear all fields (reset simulation) |
| Space / P | Pause / Resume |
| + / = | Increase viscosity |
| - | Decrease viscosity |
| ] | Increase brush size |
| [ | Decrease brush size |
| Esc | Quit |
Fluid manager/
├── Constants.hpp # Grid size, physics coefficients, rendering params
├── Solver.hpp/.cpp # Navier-Stokes solver (diffuse, advect, project)
├── FluidCube.hpp/.cpp # Fluid state container & simulation pipeline
├── Renderer.hpp/.cpp # SFML visualisation with colour LUT
├── main.cpp # Entry point, input handling, game loop
├── Makefile # Build system (make / make run / make clean)
└── README.md # This file
1. Constants & Configurations (Constants.hpp)
All compile-time parameters reside here. This includes the grid size N (number of cells), time-step dt, viscosity ν, diffusion rates, Gauss-Seidel solver iteration caps, and rendering parameters like window dimensions and scalar multipliers for visual output.
2. The Navier-Stokes Implementations (Solver.hpp/.cpp)
Pure mathematical functions implementing the four-step simulation pipeline. These are written with zero state and zero side-effects beyond modifying the buffers passed into them:
lin_solve(): Core mathematical workhorse using Gauss-Seidel iterative relaxation to solve the sparse linear systems generated by implicit integration.diffuse(): Spreads density and velocity using an implicit backward-Euler approach for absolute stability at large time-steps.advect(): A Semi-Lagrangian backward particle trace that moves fields cleanly through the vector field. Includes bilinear interpolation to handle sub-grid sampling gracefully.project(): Employs Helmholtz-Hodge pressure projection to force the velocity field to be mass-conserving (divergence-free). This generates the realistic swirling vortices.set_bnd(): Strict boundary condition enforcement (reflective walls vs wrap-around mapping) applied natively on the 1D flattened array.
3. Fluid State Management (FluidCube.hpp/.cpp)
Acting as the primary state container, FluidCube allocates the 1D arrays for density (s, density) and velocity (u/v, u0/v0). It handles dual-buffering (previous vs current state) necessary for solving equations over time. Also exposes high-level APIs like addDensity() and addVelocity() mapped perfectly to 2D coordinates for injecting fluid impulses dynamically.
4. Hardware-Accelerated Rendering (Renderer.hpp/.cpp)
Transforms the floating-point density field into an SFML sf::VertexArray (using `sf::PrimitiveType::Quads`). Recomputes vertex colours on the CPU side every frame using a highly-optimized fire/plasma custom Colour Lookup Table (LUT). Also generates dynamic line primitives for overlaying the velocity vector field to debug fluid flow paths and magnitudes visually.
5. Main Engine Loop (main.cpp)
Handles window creation, event loop, mouse tracking, keyboard shortcuts, and rendering dispatches. Bridges SFML screen space into simulator grid coordinates effortlessly.
The simulator solves the incompressible Navier-Stokes equations:
Momentum equation:
∂u/∂t + (u · ∇)u = −∇p/ρ + ν∇²u + f
Incompressibility constraint:
∇ · u = 0
Where u is velocity, p is pressure, ρ is density, ν is kinematic viscosity, and f represents external forces.
Jos Stam's key insight: split the equation into four independent sub-problems and solve each sequentially. This "operator splitting" approach is unconditionally stable for any time-step.
u ← u + dt · f
User mouse input is converted to local velocity impulses and density injections.
∂φ/∂t = ν ∇²φ
Solved implicitly using backward Euler:
(I − dt·ν·∇²) φⁿ⁺¹ = φⁿ
This produces a linear system solved by Gauss-Seidel relaxation:
x[i,j] = (x0[i,j] + a · (x[i-1,j] + x[i+1,j] + x[i,j-1] + x[i,j+1])) / (1 + 4a)
where a = dt · ν · N². The implicit formulation is the reason Stable Fluids is stable — explicit methods would blow up for large time-steps.
Instead of pushing quantities forward (unstable), we ask: "Where did this cell's value come from?"
For each cell (i,j):
- Trace backward:
pos = (i,j) − dt · u(i,j) - Bilinearly interpolate the source field at
pos - Store the result
This backward trace guarantees stability because it always samples existing field values.
This is the most mathematically important step. It enforces mass conservation (∇·u = 0).
By the Helmholtz-Hodge decomposition theorem, any vector field can be uniquely decomposed:
u = u_divergence_free + ∇p
Therefore:
u_divergence_free = u − ∇p
To find p, we take the divergence of both sides:
∇ · u = ∇²p (since ∇ · u_df = 0 by definition)
This is a Poisson equation — a classic elliptic PDE. We solve it with Gauss-Seidel:
- Compute divergence:
div[i,j] = −½h · (u[i+1,j] − u[i−1,j] + v[i,j+1] − v[i,j−1]) - Solve
∇²p = diviteratively - Subtract pressure gradient:
u -= ½N · ∂p/∂x,v -= ½N · ∂p/∂y
After projection, the velocity field creates closed streamlines — fluid swirls and vortices emerge naturally.
Solid Wall (default): Zero-velocity at edges. Velocity components normal to the wall are negated (reflection), enforcing the no-penetration condition.
Wrap-Around (Toroidal): Each boundary cell copies from the opposite interior edge, creating seamless periodic boundaries. Fluid flowing off one edge appears on the opposite side.
- C++20 compiler (clang++ 15+ or g++ 12+)
- SFML 3.x graphics library
brew install sfmlsudo apt install libsfml-dev(Note: package managers may ship SFML 2.x — check and adjust Makefile linking if needed.)
A compile_flags.txt is included in the project root. If you use clangd as your language server, it will automatically detect the /opt/homebrew include paths, ensuring zero false-positive syntax errors in your editor.
# Optimised build (recommended)
make
# Debug build with sanitizers
make debug
# Build and run
make run
# Clean build artefacts
make clean./fluid_sim- 1D Array Indexing: All 2D fields use flat arrays indexed via
IX(x,y) = y·SIZE + xfor cache-friendly row-major traversal. - O3 + LTO + march=native: The Makefile enables aggressive optimisation, link-time optimisation, and CPU-specific instruction tuning.
- Hardware-Agnostic Mouse Mapping: Uses
window.mapPixelToCoords()to perfectly register mouse forces across High-DPI (Apple Retina) and standard displays without coordinate shifting. - Stunning Visuals: The renderer employs a soft-exposure curve combined with a deep navy-to-plasma colour LUT for incredibly vibrant fluids.
- Gauss-Seidel: 20 iterations per solve (configurable in
Constants.hpp). In-place updates exploit data locality better than Jacobi iteration.
- Jos Stam, "Stable Fluids", SIGGRAPH 1999 — Paper (PDF)
- Jos Stam, "Real-Time Fluid Dynamics for Games", GDC 2003 — Paper (PDF)
- Mike Ash, "Fluid Simulation for Dummies" — Blog Post
© 2026 Aniket Goel. All Rights Reserved.
This repository and its entire source code, documentation, and graphical assets are the sole property of the author. This codebase is strictly proprietary. It may not be copied, distributed, modified, reproduced, or used in any capacity whatsoever without explicitly obtaining prior written permission from the copyright holder.
Any unauthorized use, redistribution, or reproduction of this codebase is strictly prohibited.