Interactive ground fog
Ground fog that opens when someone walks through it, curls into the gap, then slowly closes again. Underneath is a 2D fluid sim on a 256 x 256 grid, drawn as 16 flat planes.
The problem
Scrolling a noise texture gives cheap ground fog, but nothing in the scene can touch it. A character walks through and the fog slides past as if nobody was there. A full 3D smoke sim would react, but it costs far more than a thin layer at shin height needs.
How it works
The fog is treated as dye in a 2D fluid seen from above. Each step runs the stable fluids loop on the grid: add forces, diffuse the velocity, project it so nothing piles up, carry the velocity along itself, project again. Then the dye is carried by that velocity and diffused.
Projection is where the cost sits. It solves a pressure equation with 120 Jacobi iterations, and each iteration reads four neighbours per cell. Two projections plus two diffusions make 480 solver passes per step.
The walker writes into the grid in two places. A force brush adds its movement to the velocity. A second brush removes dye under its feet. Fixed fog sources keep adding dye and the dye fades a little every step, so the field settles at a level and grows back after being cut.
To draw it, the dye becomes a texture on 16 flat planes stacked 2.5 cm apart. Each plane sets its opacity from the density under it.
What I hit
With the repo's force strength the wake ran at about 2.2 cells per step while the walker moved 0.87. It dragged fog straight back into the gap and the trail closed within about a metre. I cut the force to 0.14.
The source shader sets opacity to density times layer height. In my scene that made a flat milky sheet. I changed it so density also sets how tall the fog column is. Thin fog now stays near the ground and the cut shows the stones.
The repo's diffusion pass feeds its own running result back in as the right-hand side, so it acts as a small blur. I kept it as written.
My build
WebGL2 has no compute shaders, so each kernel became a fragment pass that ping-pongs between float textures in three.js.
256 x 256 cells over 12.8 m, 5 cm per cell. 120 iterations per solve, one step per frame. 32 fog sources, 3 slow force streams for drift.
The top plane sits 37.5 cm above the ground. The fog lighting, the lamp shadow and the 32-step raymarch comparison are my additions.
Trade-offs
The field is 2D. Fog cannot rise or spill over a ledge. It does not flow around the stones here, because I left out the repo's obstacle kernels. Removing dye under the feet is a trick: real fog would be pushed aside. Up close the flat layers can show as bands where they cut through a leg or a post. 480 solver passes per step is heavy, so the iteration count is the first thing to cut.
When to use it
Low fog, mist on water or floor dust that should react to characters in a bounded area. For fog that fills a room or needs light shafts, use a volumetric method.
Source and credits
Technique source: Compute-Shaders-Fluid-Dynamic- by IRCSS (Shahriar Shahrabi), https://github.com/IRCSS/Compute-Shaders-Fluid-Dynamic-, MIT license, commit 03765e79d48ba7c2900532e1b9b140f5746b7ad6. Ported to WebGL2 fragment passes from its compute kernels: advection, divergence and calculate_divergence_free (Navier_Stokes), Jacobi_Solve (Solvers), AddForce_mouse, Add_constant_force_at, AddDye and Add_constant_dye (UserInput), NeuMannBoundary (Boundary_Handling) and the bilinear load in FluidDynamicUtility. The step order, the parameters and the stacked fog planes follow its VastLand fog demo (VastLandDemoMaster, the VastlandFog shader and TheVastLandFogDemo scene).
Render framework: three.js by mrdoob and contributors, https://github.com/mrdoob/three.js, MIT license, r187. Used for drawing, lighting and shadows. It is not the source of the fluid technique.
My own code: the scene, the capsule walker, the fog source layout and wind schedule, the negative dye brush, the fog layer shaping and lighting, the scrolling noise fog and the raymarch used for comparison, the debug views, camera moves, labels and video. No external models or assets. None of the repo's models or textures are used.