An interactive, browser-based fluid simulation implementing two approaches to fluid dynamics, rendered in real time with WebGL via Three.js.
2D smoke simulation based on Jos Stam's Stable Fluids (SIGGRAPH 1999). The velocity and density fields are stored as floating-point render targets and each simulation step runs as a WebGL fragment shader pass on the GPU.
Pipeline per frame:
- Add source — inject smoke density at the bottom-centre every frame; mouse drag injects additional density and velocity
- Diffuse velocity — viscosity via Backward Euler + Gauss-Seidel (Jacobi) iterations
- Advect velocity — semi-Lagrangian backtrace
- Project — enforce incompressibility: divergence → pressure Jacobi solve → subtract pressure gradient
- Advect density — carry smoke along the divergence-free velocity field
2D water simulation using Smoothed Particle Hydrodynamics. Runs on the CPU and uploads particle positions and velocities to the GPU each frame via a Three.js Points object with a custom additive point-sprite shader.
Pipeline per frame:
- Spatial hash rebuild — O(n) neighbourhood queries with cell size = smoothing radius
- Density — Poly6 kernel
- Pressure — equation of state
p = k(ρ − ρ_rest) - Forces — pressure gradient (Spiky kernel) + viscosity (Viscosity Laplacian kernel)
- Gravity —
−9.8 m/s²applied as a direct acceleration - Integrate — Forward Euler
- Boundary — clamp + reflect with damping coefficient 0.3
| Input | Stable Fluids | SPH |
|---|---|---|
| Drag | Add velocity force + smoke density | Push particles |
| Click | Inject smoke density | Spawn 10 particles |
| Space | Pause / resume | Pause / resume |
Parameters (viscosity, diffusion, grid size, particle count, stiffness, etc.) are adjustable in the right panel at runtime.
| Item | Technology |
|---|---|
| Language | JavaScript (ES Modules) |
| Rendering | Three.js r170 + WebGL |
| UI controls | Tweakpane 4 |
| Bundler | Vite 6 |
| GLSL loader | vite-plugin-glsl |
| Environment | Nix flake (optional) |
fluid/
├── index.html
├── vite.config.js
├── package.json
├── flake.nix # Nix dev shell (Node 22 + npm)
└── src/
├── main.js # Entry point: UI, mouse input, animation loop
├── renderer.js # Three.js WebGLRenderer + orthographic camera
├── stable-fluids/
│ ├── FboPingPong.js # Ping-pong render target pair (read/write/swap)
│ ├── GPUComputationPass.js # Full-screen quad pass wrapper
│ ├── StableFluids.js # Simulation manager
│ └── shaders/
│ ├── addForce.glsl # Splat force/density at a point (Gaussian kernel)
│ ├── advect.glsl # Semi-Lagrangian advection
│ ├── diffuse.glsl # Jacobi diffusion iteration
│ ├── divergence.glsl # Central-difference divergence
│ ├── pressure.glsl # Jacobi pressure solve
│ ├── project.glsl # Subtract pressure gradient from velocity
│ └── render.glsl # Density → warm colour palette
└── sph/
├── SPH.js # Simulation manager + Three.js Points upload
├── SpatialHash.js # Hash-grid neighbourhood search
├── Kernels.js # Poly6, Spiky, Viscosity kernel functions
└── shaders/
├── sph.vert.glsl # Point-sprite vertex shader (passes velocity)
└── sph.frag.glsl # Speed → blue/cyan/white colour with soft circle
npm install
npm run devOpen http://localhost:5173.
nix develop
npm install
npm run devnpm run build
npm run previewStable Fluids — GPU compute pattern
FboPingPong owns two WebGLRenderTarget instances. .read always holds the freshest result; .write is the next render target. Calling .swap() flips them.
GPUComputationPass wraps a RawShaderMaterial (with explicit attribute declarations required by WebGL) on a full-screen PlaneGeometry. Its .compute(renderer, fbo) method renders into fbo.write then calls fbo.swap(), so the result is immediately available on fbo.read for the next pass.
All uniform updates happen on the material before each call — no global state.
SPH — CPU simulation, GPU rendering
Particle state (pos, vel, den, pres) lives in Float32Array buffers updated on the CPU each frame. After integration the positions and velocities are written into Two BufferAttribute arrays (position, aVelocity) and uploaded to the GPU via needsUpdate = true. The fragment shader colours each particle by speed using an additive blend so overlapping particles accumulate brightness.
- Jos Stam, Stable Fluids, SIGGRAPH 1999
- fluid-three — Stable Fluids reference demo
- david.li/fluid — SPH reference demo
- M. Müller et al., Particle-Based Fluid Simulation for Interactive Applications, SCA 2003