Back to projects
POGL 1 month 2 pers.

Real-Time Fluid Simulation Engine

Real-time 3D SPH fluid simulation engine handling 75,000+ particles at 60 FPS — full GPU physics via Compute Shaders and Screen-Space Fluid Rendering (SSFR).

C++20 OpenGL 4.6 GLSL Compute Shaders CMake Dear ImGui
Aperçu du projet POGL

Context & Objectives

This project is a real-time 3D fluid simulation engine developed in C++20 and OpenGL 4.6 Core Profile, built for the Object-Oriented Programming and OpenGL (POGL) course at EPITA.

The goal was to design an engine executing two core graphics pillars in parallel:


Architecture: Data-Oriented Design GPU

The engine architecture follows Data-Oriented Design (DOD): all particle data resides in VRAM as Shader Storage Buffer Objects (SSBOs) in std430, eliminating PCIe transfer overhead between CPU and GPU during simulation frames.

The pipeline executes two distinct loops that chain together each frame:

Phase Responsibility Tool
CPU Inputs, SimSettings parameters C++20, Dear ImGui
GPU — Physics 7 Compute Shader passes GLSL 4.60
GPU — Rendering 5 Graphics Shader passes (SSFR) GLSL 4.60

The 8 SSBOs allocated in VRAM maintain positions, velocities, densities, spatial hashing, and rendering buffers — without ever transferring back to CPU during simulation.


Physics Engine: GPU SPH Simulation

The SPH method is a Lagrangian formulation of the Navier-Stokes equations: fluid is represented by discrete particles whose properties (density, pressure, viscosity) are estimated via weighted interpolation over neighboring particles using smoothing kernels.

Density & Pressure

The local density $\rho_i$ of a particle is the sum of contributions from neighboring particles $j$ within radius $h$:

$$\rho_i = \sum_{j} W_{\text{spiky2}}(\|\mathbf{r}_i - \mathbf{r}_j\|, h)$$

A short-range secondary density $\rho_{\text{near}, i}$ (Spiky Power 3 kernel) strongly repels overly close particles, preventing excessive clustering. Pressure follows directly from deviation against target density $\rho_0$:

$$P_i = k \cdot (\rho_i - \rho_0), \qquad P_{\text{near}, i} = k_{\text{near}} \cdot \rho_{\text{near}, i}$$

Forces & Integration

Pressure and viscosity forces are applied symmetrically (Newton’s 3rd Law):

$$\mathbf{F}_{\text{pressure}, i} = -\sum_{j} \frac{P_i + P_j}{2 \rho_j} \nabla W_{\text{spiky2}}(\|\mathbf{r}_{ij}\|, h) \cdot \hat{\mathbf{r}}_{ij}$$ $$\mathbf{F}_{\text{viscosity}, i} = \mu \sum_{j} (\mathbf{v}_j - \mathbf{v}_i) \cdot W_{\text{poly6}}(\|\mathbf{r}_{ij}\|, h)$$
SPH density maps and smoothing kernel behavior
SPH density maps and smoothing kernel behavior

3D Spatial Hashing & Bitonic GPU Sort

Without optimization, neighbor search is $O(N^2)$, unfeasible for 75,000 particles. The 3D domain is subdivided into a regular grid (cells of size $h$) to reduce search complexity to $O(1)$.

Acceleration Pipeline (3 Compute Passes):

  1. Spatial Hashing: Each particle computes a hash of its 3D cell $\lfloor \mathbf{P}/h \rfloor$ using a prime-coefficient hash function.
  2. Bitonic GPU Sort: Pairs (particleIndex, cellKey) are sorted in parallel on GPU in $O(\log^2 N)$ steps — zero CPU transfers required.
  3. Index Offset Table: A fast pass identifies the starting index of each cell in the sorted array. Each particle then explores only its 27 adjacent 3D cells.


Rendering Pipeline: Screen-Space Fluid Rendering (SSFR)

Rendering particles as simple spheres results in a disjointed visual. Screen-Space Fluid Rendering converts point clouds into a continuous, realistic liquid surface across 5 sequential shader passes.

01 Depth Map Point Sprites (R32F)
02 Bilateral Blur Profile Smoothing
03 Normal Map 3D Reconstruction
04 Thickness Map Beer-Lambert
05 Composite Fresnel + Refraction

Pass 1 — Initial Depth Map

Each particle is rendered as a Point Sprite, projected into a 3D sphere in fluid_depth.frag. Fragments outside the sphere radius are discarded and exact depth $z_{\text{eye}}$ is saved into a GL_R32F texture.

Pass 1: raw depth map of individual spheres
Pass 1: raw depth map of individual spheres

Pass 2 — Adaptive Bilateral Filtering

A separable bilateral filter (two H/V passes) smoothes the depth map without blurring silhouette edges. Samples are weighted by both spatial distance and depth discrepancy:

$$W(i, j) = \exp\!\left(-\frac{\|\mathbf{x}_i - \mathbf{x}_j\|^2}{2 \sigma_s^2}\right) \cdot \exp\!\left(-\frac{|z_i - z_j|^2}{2 \sigma_r^2}\right)$$
Pass 2: smoothed depth map, continuous surface
Pass 2: smoothed depth map, continuous surface

Pass 3 — Screen-Space 3D Normal Reconstruction

From smoothed depth $z(u, v)$, the 3D position $\mathbf{P}(u, v)$ is reconstructed per pixel. The normal field is derived via the cross product of partial derivatives:

$$\mathbf{N} = \text{normalize}\!\left( \frac{\partial \mathbf{P}}{\partial x} \times \frac{\partial \mathbf{P}}{\partial y} \right)$$
Pass 3: 3D normal field reconstructed in screen-space
Pass 3: 3D normal field reconstructed in screen-space

Pass 4 — Thickness & Optical Absorption (Beer-Lambert)

Traversed water thickness is accumulated using additive blending (GL_ONE, GL_ONE). Chromatic attenuation follows the Beer-Lambert law:

$$I_{\text{refracted}} = I_{\text{scene}} \cdot \exp\!\left(-\text{thickness} \cdot \alpha \cdot (1 - \mathbf{C}_{\text{water}})\right)$$
Pass 4: thickness map of the water volume
Pass 4: thickness map of the water volume

Pass 5 — Final Composition: Refraction & Fresnel Reflections

The final pass combines all buffers:

Pass 5: Global Reflection & Refraction Pass 5: Sun Specular Highlights
Pass 5: Fresnel reflection and refraction Pass 5: Sun specular highlights

Interactive UI & Controls

The application embeds Dear ImGui to allow dynamic real-time tuning of parameters: particle count, gravity $g$, stiffness $k$, viscosity $\mu$, water color, absorption, Fresnel power, and bilateral blur radius.

The camera is orbital (left click + drag) with scroll zoom.


GPU Optimizations


Real-Time Simulation Demo