A Navier–Stokes solver running in fragment shaders: velocity, pressure and dye all live in textures that ping-pong every frame.
Velocity, pressure and dye each live in a floating-point texture that is read from one copy and written to the other every frame (ping-pong). The pointer adds a Gaussian splat of velocity and colour; vorticity confinement keeps small swirls alive; a Jacobi pressure solve runs 20 iterations to remove divergence; then the velocity field advects itself and the dye by sampling backwards along the flow. If the GPU cannot render to half-float textures the setup falls back to float, then to 8-bit, and throws a readable error when none work.
// Pressure: one Jacobi iteration of the Poisson solve.
float L = texture2D(uPressure, vL).x;
float R = texture2D(uPressure, vR).x;
float T = texture2D(uPressure, vT).x;
float B = texture2D(uPressure, vB).x;
float divergence = texture2D(uDivergence, vUv).x;
float pressure = (L + R + B + T - divergence) * 0.25;
gl_FragColor = vec4(pressure, 0.0, 0.0, 1.0);
// Advection: semi-Lagrangian step, trace back along the velocity and decay.
vec2 coord = vUv - dt * texture2D(uVelocity, vUv).xy * texelSize;
vec4 result = texture2D(uSource, coord);
float decay = 1.0 + dissipation * dt;
gl_FragColor = result / decay;
// Projection: subtract the pressure gradient so the flow stays incompressible.
vec2 velocity = texture2D(uVelocity, vUv).xy;
velocity.xy -= vec2(R - L, T - B);
gl_FragColor = vec4(velocity, 0.0, 1.0);