The earlier parts used pictures to explain physics. This one goes the other way. It's a toy: the same advection and Poisson solve that real fluid simulators use, applied to a substance that doesn't exist, with every constant chosen by how it looks. Color doesn't mean temperature, and nothing has units.
Visual effects people call this art direction. Most of the series' machinery is still useful when the goal is something that feels good to stir instead of something that predicts anything, and this part goes through the pieces that made it in.
Step 1 · Incompressible flowAdvection and projection
What makes a fluid look like a fluid is that it goes around things. Push your hand through smoke and the smoke doesn't pile up in front of your palm. It slides sideways, curls behind your wrist, and closes back up in a pair of vortices. That behaviour, flow refusing to pile up, is what incompressible looks like, and enforcing it takes a Poisson equation.
Part five solved ∇²φ = 4πGρ because gravity needed it. Fluids need the
same equation with different names. Update a velocity field u however
you like (move it, stir it, add wind) and it comes out slightly wrong, with spots
where flow converges into a sink or spreads out of a source. An incompressible fluid
allows neither. Its divergence ∇·u has to be zero everywhere. The fix
is to find a pressure field whose gradient cancels the part that's wrong:
∇²p = ∇·u // pressure that resists the pile-up
u ← u − ∇p // and lets it push back
Here's one projection, a stage at a time:
In part five the Laplacian turned density into gravitational potential. Here it turns a velocity field that's almost a fluid into one that is, every frame.
The other half of the loop moves the field along with itself, which is called advection. Pushing each cell's value forward is unstable: with a big enough timestep, values overshoot and grow. The standard approach is to go backwards. Stand at each cell, ask where the fluid here was one timestep ago, and copy the value from there:
u_new(x) = u_old(x − u(x)·dt) // semi-Lagrangian: look up where you came from
The lookup interpolates between the eight cells around that point, and an interpolated value can't be bigger than the values it came from. So the scheme is stable at any timestep, and no amount of stirring makes it blow up. The cost is that interpolation smears the field a little every step, which softens the motion. For a toy that's fine.
Solving the projection only as well as you can see
The demo runs this on a 64³ grid, 262,144 cells, every frame. Part five solved its Poisson equation exactly with an FFT, but that relied on a periodic box. This box has walls, so it uses ten sweeps of Jacobi relaxation instead. Each sweep replaces every cell's pressure with the average of its six neighbours, corrected by the divergence there, and the pressure starts from zero every frame.
Ten sweeps is nowhere near converged. Pressure only spreads about one cell per sweep, so divergence on scales much bigger than ten cells never fully goes away. Whatever's left gets another pass next frame, and a fluid that's very slightly compressible at large scales looks the same as one that isn't. For this, solving it as well as the eye can tell is enough.
Step 2 · Ambient motionCurl noise
A projected grid behaves like a fluid, but left alone it goes still. Stir it, the vortices fade, and a few seconds later nothing is moving. Films and games keep their smoke alive with ambient wind. The obvious way to make wind is to take some smooth random noise and push particles along its gradient. That fails in a useful way:
Follow the gradient and every tracer slides up to the nearest peak of the noise and stays there. A gradient field is all sources and sinks, which is the opposite of what a fluid needs. Turn the same gradient 90° and the flow runs along the contour lines of the noise instead of across them, so no point gains or loses fluid. In 2D the rotation is a swap and a minus sign. In 3D the equivalent is the curl of a noisy vector field:
v = ∇ × ψ // ψ: three channels of smooth noise
∇·(∇ × ψ) = 0 // the divergence of a curl is always zero
That's curl noise: motion that needs no pressure solve, never runs down, and never pools. The demo adds a slowly drifting curl-noise field to the grid as a force every frame, and that's the breathing you see when nobody is touching it. It costs three noise-gradient evaluations per cell. The shader works out each gradient analytically from the same eight corner values the noise itself uses, instead of by finite differences, which would take eighteen noise samples.
Step 3 · Tracer particlesPaint that goes where the fluid goes
In parts one through five the particles were the simulation. They carried mass, they pulled on each other, and losing one would change the answer. Here they carry nothing. They're massless tracers in the grid's velocity field: each one reads the velocity at its position by trilinear interpolation (the 3D version of part five's gather) and moves with it. Nothing is deposited back, so the fluid doesn't know the particles exist. They're there so you can see it move. The playground below starts with 240,000 of them, and its slider goes from 50,000 to a million.
Each particle lives a few seconds. When it dies it respawns, in ink and nebula a bit over half the time next to whatever is stirring the fluid, so paint seems to pour out of your cursor. The rest respawn somewhere in the volume so the whole box stays filled. Each particle also gets a small random wobble on top of the flow, which keeps the streams feathery instead of collapsing into single lines.
Everything you actually see is decided after the physics.
Stretch. Each particle is drawn as a capsule stretched back along its velocity, part two's camera-facing billboard with a tail. Fast paint becomes streaks and slow paint becomes dots, so you can read speed at a glance.
Trails. The renderer never clears its image. It draws into a high-dynamic-range buffer that gets multiplied by a constant each frame instead of erased, so every particle leaves a fading wake, the same way a long-exposure photo turns stars into trails. That constant is the trail length. The modes keep between 84% and 90% of the previous frame, and the trails slider sets it anywhere from 50% to 97%.
Palette. Color is speed, mapped through a three-stop gradient picked by eye, and each mode also changes the physics a little. Ink runs from deep blue to near-white. Embers runs from dark red through orange to pale gold. Stirring leaves an invisible dye in the fluid, and in embers dyed fluid rises, while new particles mostly come up from a bed of coals near the floor. Nebula runs from purple through magenta to teal, has a lower speed limit, a gentle pull toward the centre, and the least drag, so motion lasts longest.
The tracers are cheap. Each one reads eight grid cells and writes itself back, with no tree, no sort and no FFT, and no particle ever looks at another one. The interesting work all happens in the 64³ grid that tells them where to go.
Step 4 · Hand trackingTwenty-one landmarks
A cursor is one point. Hand tracking gives you several, and all it needs is a webcam and about ten megabytes of neural network weights running on the GPU.
The model is micro-handpose, a small WebGPU reimplementation of Google's MediaPipe hand pipeline. It finds palms in the frame, then fits twenty-one landmarks to each hand: four points along each finger plus the wrist. Once it has found a hand, it tracks it from where it was in the previous frame and can usually skip the palm search. It all runs in the tab. The camera only starts when you click, and no video leaves your machine. The only thing downloaded is the model.
The fluid treats each hand as a set of stirrers. The palm is a wide stirrer that pushes fluid along with its motion, harder when your fingers are spread. The index fingertip is a smaller, stronger one, good for drawing. The third control is a pinch: the distance from thumb tip to index tip, divided by the size of your hand so it works at any distance from the camera.
hand_scale = |wrist − middle_finger_base| // doesn't change as the fingers move
pinch = clamp(1 − |thumb_tip − index_tip| / (1.05 · hand_scale), 0, 1)
Past a pinch of 0.45, a third stirrer appears between your thumb and finger. It pulls fluid inward and spins it around the camera's line of sight, harder the tighter you pinch. None of this is physics. It's all mapping from gestures to forces, and getting that mapping to feel natural is trial and error rather than equations.
Step 5 · All togetherThe playground
Here's everything at full size. Move your cursor over it to stir, and drag to orbit the box. To zoom, pinch on a trackpad or hold ⌘/ctrl while scrolling; plain scrolling just scrolls the page. The mode buttons re-dye the same fluid without restarting it. ✋ asks for your camera and turns on hand control: wave to stir, draw with your index finger, pinch for vortices.
Try a slow figure-eight in nebula and then hold still. Nebula has the least drag, so your loop keeps circulating for a few seconds after you stop. If nothing touches the box for two and a half seconds, an automatic stirrer takes over and traces a slow Lissajous curve through it, so the toy never goes completely still.
EpilogueWhere the pieces came from
Most of the toy is borrowed from earlier parts:
- Part one: one GPU thread per particle, each reading a shared structure and writing only itself.
- Part two: camera-facing billboards and the orbit camera.
- Part three: a fluid is local. There it was particles pushing on their neighbours; here it's grid cells averaging theirs.
- Part four: separate pieces run together in one frame, here a grid solver driving particles that never push back.
- Part five: particles reading from a mesh, and a Poisson solve, with pressure in place of gravity.
The toy throws out physical units and any conservation law that doesn't help the interaction. It's still held together by the same numerical methods as the rest of the series. Here they're there to keep it responsive and smooth, not to predict anything. Things I'd try next: a multigrid pressure solve, so the large-scale divergence goes away too, and a sharper advection scheme (BFECC or MacCormack) to cut down the smearing.
That's the end of the series. It started with two falling dots and one line of physics, and almost everything since has been about data structures: a tree with nothing to build, an octree bent over a dome, a sort that puts neighbours next to each other in memory, a hash that removes the walls, a transform that turns an equation into a division, and finally all of it put to work on something to play with.
Back to the start: Part one, Two Falling Dots. One formula, a slingshot, and the first wall.