Fluid Lab is a browser-based water simulator. It is not a looping video, and it is not just a shader moving a flat plane up and down. The water is a moving 3D volume inside a tank.
The visible part is made of particles, but the particles are not solving everything by themselves. Each frame, they report their motion to a grid. The grid fixes the pressure and velocity field. Then the particles read the corrected motion back and move again.
That back-and-forth is the core of the project: particles make the water visible, and the grid makes the water solvable.
1. Most browser water is a surface
A lot of real-time water starts as a surface. You take a flat sheet, calculate wave heights across it, add nice normals and reflections, and it can look great. That works well for oceans, lakes, puddles, and background scenery.
But a surface is not the same thing as a body of water. It cannot really pour into a box, pile up, splash into droplets, fall over itself, or collide with walls from the inside. It can look like water, but it is mostly describing the top skin.
Fluid Lab goes after the more annoying version: a bounded 3D tank of water. That means the app has to keep track of water throughout the volume, not just along a nice 2D sheet.
- Surface water A 2D sheet with waves, normals, and shading.
- Volumetric water A 3D body that can pile up, fall, splash, and separate.
- Fluid Lab Particles for motion, plus a grid for pressure.
The rendering problem changes too. One particle does not look like water, just like one blade of grass does not look like a field. The image comes from drawing a huge number of small pieces together and making their motion agree.
2. Why particles alone won’t work
Particles are the obvious place to start. Give each one a position and a velocity, apply gravity, bounce them off the tank, and you already have something that can fall and splash. For small scenes, particle-only fluid methods can work.
The problem is pressure. Water is not a pile of independent dots. If too much water moves into one region, it should push back out. If a region expands too much, nearby water should pull it back together. That is what makes the motion read as liquid instead of sand or spray.
A direct particle version has to keep asking local questions: which particles are nearby, how crowded is this region, what pressure should push them apart, and how does that affect the next particle? Doing that naively for millions of particles every frame is not a reasonable CPU job. A naive GPU version is not automatically saved either. If every particle has to search too much of the world or fight over the same memory, the parallelism stops being useful.
- Particles move well They are great for falling, splashing, and carrying detail.
- Pressure is harder Each particle needs nearby density and crowding information.
- The grid helps It gives the simulation a shared place to solve flow.
So Fluid Lab uses particles for the part they are good at — moving through space — and gives the pressure problem to a grid.
3. The grid trick
The grid is the shared workspace for the fluid. Instead of asking every particle to understand every other particle, the simulator lets particles write their motion into nearby grid cells and grid faces. Once the data is on the grid, the GPU can run the same small calculation across a huge number of cells at once.
Fluid Lab uses a MAC grid. Pressure and cell type live at cell centers. Velocity lives on cell faces: one buffer for x velocity, one for y velocity, and one for z velocity. That layout makes it easier to measure how much flow enters and leaves each cell.
particles
↓ write velocity into nearby grid faces
grid velocities
↓ measure flow into and out of cells
divergence
↓ solve pressure
pressure field
↓ correct grid velocity
corrected velocities
↓ sampled back by particles
particles
This is the trick that makes the problem fit the GPU. Particle motion is messy and local. Grid pressure is structured. Once the work is arranged as cells, faces, buffers, and repeated passes, the GPU can do millions of small operations in parallel.
4. One frame of the simulation
One frame is mostly data moving between particles and the grid. In slow motion, the loop looks like this:
clear temporary grid buffers
↓
mark occupied cells
↓
classify cells as solid, liquid, or air
↓
scatter particle velocity to grid faces
↓
normalize grid velocity
↓
apply gravity and tank boundaries
↓
measure divergence
↓
solve pressure
↓
subtract the pressure gradient
↓
transfer corrected velocity back to particles
↓
move particles
↓
render
Mark the water
First, the simulator clears the temporary grid state and marks which cells contain particles. From there, cells become solid wall, liquid, or air. That classification matters because the pressure solve should only operate on the water, while walls need to block velocity.
Move velocity onto the grid
Particles know their own velocity, but pressure is solved on the grid. So each particle contributes velocity to nearby grid faces. Many particles may write to the same face, so the GPU accumulates weighted sums and weights, then normalizes them into final face velocities.
In WebGPU, that scatter step has a catch: the shader language does not give me floating-point atomic adds for this. Fluid Lab accumulates fixed-point integers during the scatter, then turns them back into floats in the normalize pass. It is one of those details that sounds small until you actually have a million threads trying to write into the same field.
Find where the water is compressing
Once velocity is on the grid, the simulator adds gravity, blocks motion through the tank walls, and measures divergence. Divergence is just a way of asking whether too much flow is entering or leaving a cell.
- Too much flow in The cell is compressing.
- Too much flow out The cell is expanding.
- Goal Flow in and out should roughly balance.
Solve pressure
Pressure is the correction. If the velocity field says water is crowding into a cell, pressure pushes back. If the field says water is expanding too much, pressure pulls the motion back toward balance.
Under the hood, Fluid Lab solves a grid pressure system with Conjugate Gradient. The important part for this report is not the name of the solver. It is what the solver produces: one pressure value per liquid cell.
Use pressure to fix velocity
After pressure is solved, the simulator subtracts the pressure gradient from the grid velocity. That means each face velocity is corrected by the pressure difference across that face. This is the step that turns “where the particles wanted to go” into “where the water is allowed to go.”
Move the particles again
Finally, particles sample the corrected grid velocity, blend between calmer PIC-style motion and livelier FLIP-style motion, then move forward. If a particle escapes the tank, the recovery path clamps it back inside and removes the velocity pointing through the wall.
5. Why this becomes many GPU passes
The clean diagram lies a little. It makes the simulation look like one neat loop. On the GPU, it becomes a pile of small jobs.
The reason is boring but important: the shader cannot bind the whole simulator at once. Fluid Lab has particle buffers, velocity buffers, pressure buffers, cell-type buffers, divergence buffers, accumulation buffers, and scratch buffers for the pressure solve. Real browser GPU limits are much smaller than that.
So the frame is split into narrow passes. Clear. Mark. Classify. Scatter. Normalize. Apply forces. Enforce boundaries. Compute divergence. Solve pressure. Subtract pressure. Transfer back to particles. Render.
clear
mark / classify
scatter u, v, w
normalize u, v, w
save velocity
forces
boundaries
divergence
pressure solve
pressure gradient
boundaries again
grid-to-particles
render
This was the part I underestimated. The algorithm is already hard, but the real project is making all of these passes agree on the same buffer layout, grid indexing, units, and boundary rules. When one of those contracts is wrong, the bug rarely looks local. It usually just looks like the water exploded.
6. Tuning and profiling the mess
Fluid simulation has too many knobs to tune by vibes. If the water looks wrong, the cause might be pressure iterations, grid resolution, particle count, FLIP/PIC blend, wall friction, wave strength, particle size, or a bug in one of the transfer passes.
That is why Fluid Lab has a heavy config panel. Some controls are there for normal interaction. Some are there because rebuilding the app every time I want to test a pressure setting or wave strength would make the project miserable to work on.
The profiler matters for the same reason. FPS can tell me that something is slow. It cannot tell me whether pressure, rendering, particle transfer, or another pass is the actual problem. The profiler panel is there so I can see where the frame went instead of guessing.
Why the UI is part of the engineering
The controls and profiler are not just polish. They are how I tune the simulation, catch bad assumptions, and decide what is actually worth optimizing.
7. Current scale
On my gaming PC, the current high-scale version can run around 4 million particles, roughly 400,000 liquid cells, at about 30 FPS.
At that scale, the CPU is not moving the water. The browser shell drives the frame loop, while the simulation buffers stay on the GPU and the renderer reads from those buffers directly.
Video placeholder: high-scale capture on a strong gaming PC.
8. Bigger improvements
The current version is focused on particle rendering, liquid cells, and the core pressure loop. The next improvements I care about fall into three buckets.
Rendering upgrades
A better surface renderer is the obvious visual upgrade. I experimented with marching cubes, but paused it for now. I want the next surface path to fit the current simulator instead of feeling bolted on. Better lighting, transparency, foam, or a particle/surface hybrid could all live here once the surface representation earns its cost.
Physics upgrades
The biggest physics work is improving how the particles and grid transfer information back and forth. Better volume preservation, better handling of thin sheets and splashes, and solver improvements would all make the water feel more convincing without turning the project into a fake surface shader.
Interactivity upgrades
The tank is already interactive, but more scenes would make the simulator much more fun to explore. I would like to add stronger presets, obstacles, different starting shapes, and more direct ways to disturb the water.
Open the demo
The best way to judge the project is to run it. The demo is interactive, so you can pause the sim, change settings, watch the profiler, and see how the water responds.