The pool this page boots with is about 27,000 particles, simulated in compute shaders on your GPU. The slimes are soft bodies solved on your CPU. The smoke is a third simulation, a grid fluid running in WebGL. The three of them share exactly one thing: a grid of 8-pixel cells that says solid or not. Draw a wall and all three feel it.
Everything here was pulled from Sluice, the little mining game that lives on this site, where the water floods dug tunnels, the smoke is diesel exhaust, and the slimes are rare buried creatures that cushion a bad fall. This page is those three systems with the game stripped away and a pencil put in your hand.
The water
The water is the game's fluid solver, byte for byte. It is an MLS-MPM sim (Hu et al. 2018; nialltl's guide is the friendliest walk-through): particles splat mass and momentum onto a grid, the grid resolves pressure, particles gather velocity back and move. All of it runs as WebGPU compute, advancing in fixed 1/120-second substeps (several per frame, so the physics is identical at 60 and 120 Hz), and the grid work only touches 16-by-16-cell blocks that contain water, so the cost scales with how wet the box is. The page allows 100,000 particles on desktop and 40,000 on a phone.
A min-separation pass shoves over-packed particles apart each substep, because particle water that is allowed to over-pack builds pressure spikes and pops. The kernels also carry a per-particle sleep system, and in practice the shipped tuning barely uses it: the weakly-compressible pressure model keeps a settled pool faintly breathing (I measured a still pool for half a minute, and not one particle dozed off). The real savings are the sparse grid and the fact that the page parks the whole loop when the toy scrolls off screen. The pressure tuning follows saharan's water demo.
This is the one engine with a hardware requirement: no WebGPU, no water. The smoke and the slimes run anyway.
The slimes
Each slime is a lattice of 20 to 130 points, depending on its size, solved on the CPU. Distance constraints hold it together, solved with XPBD (Macklin, Müller, Chentanez 2016) as three tiny sub-iterations inside each 1/240-second step, because many small solves make a stiffness that survives any frame rate. A shape-matching pull (Müller et al. 2005) remembers the round rest shape, and a gas-pressure term holds the area, which is why a squashed slime bulges at the sides instead of shrinking.
Slime-on-slime contact runs inside the same substep loop through a count-sort spatial hash, the approach in Macklin et al.'s unified particle physics. Running contact per substep instead of once per frame is the difference between blobs that stack and blobs that jitter and merge. The game learned that the hard way.
Drop one in a pool and it floats. Each frame the toy builds a small map of the water (density and mean flow per 8-pixel cell) and reads each body a pool-local waterline: the level in the columns beside it, walked so that a wall or an air gap ends the pool, which is what keeps a slime from feeling water on another floor or behind a drawn barrier. Every lattice point below that line trades gravity for lift, and every submerged point takes drag toward the local flow. That one rule set covers a lot: currents carry a floater, a waterfall shoves a slime down instead of levitating it, a plunge damps itself out, and in zero-g a thrown blob of water drags a slime along with it. Net lift runs about a quarter beyond gravity, so slimes ride mostly submerged, like dense gel. The coupling is one-way on purpose. These forces never push the water; the push the water feels comes from the boundary rings below. The game tried honest two-way coupling once, spent three versions on it, and reverted the whole thing.
The smoke
The smoke is the oldest method on the page: a classic Eulerian grid fluid in WebGL, based on Pavel Dobryakov's much-copied demo. Velocity advects itself, 25 Jacobi iterations per frame make the field incompressible, vorticity confinement restores the curl the coarse grid smears away, and what you actually see is a dye field carried along by all of it.
The drawn walls are rasterized into its obstacle mask whenever they change, and so are the slime outlines and the water itself (any 8-pixel cell holding about a quarter of rest density counts as solid to the smoke). A plume will split around a blob that is wobbling in real time, and bank off the surface of a pool.
One dumb grid
There is no shared physics library underneath. There is one flat array of 8-pixel cells. The water kernels read it packed as a bitmask, the slime solver probes it point by point, the smoke receives it as a texture. Drawing a line edits all three simulations at once, which is why the page feels like one world instead of three separate demos.
The direct couplings between engines are thinner, and here is the honest list. Water lifts and drags every slime, per lattice point. The eight wettest slime outlines are handed to the water solver as moving boundaries, resampled to 20 vertices each, so rain piles on a slime and slides off as it wobbles, and a raft of floaters visibly displaces the pool. Smoke banks off slime silhouettes and off water. Beyond the eight tracked outlines water falls straight through a slime, and nothing pushes the smoke back. Nobody has complained yet.
Worth trying
- Slide gravity to zero while the falls are running. Water already in flight keeps its momentum and drifts into floating blobs; the trickle becomes a chain of pearls. Then try 2g.
- Set time to 15 percent and poke the pool, hard. The crown splash is the whole reason the slider exists.
- Draw a bowl under the spout, let it fill, then erase the bowl's floor. Erasing wakes the sleeping water above it.
- Open the spa scene and drop a slime from the very top of the box. It plunges, surfaces, and bobs its way calm.
There is no framework and no build step; the page is static files, and view source shows all of it, including the mistakes. If you want the version with fuel and consequences, it is called Sluice and it is free forever.