Original by Evan Wallace, WebGPU port jeantimex, this fork nmarchand73/water. WebGPU required; classic WebGL version: madebyevan.com/webgl-water.
Instead of solving the full 3D Navier-Stokes equations for an incompressible fluid, this demo uses a 2D height field h(x,z,t): each texel stores surface height (red) and one vertical velocity component (green). That is a standard reduced model: only the free surface moves; the bulk flow inside the water volume is not simulated.
Each integration step uses the four cardinal neighbors only (a five-point stencil for the horizontal Laplacian on a square grid: same scaling as d^2h/dx^2 + d^2h/dz^2 in the continuum limit). Samples outside the simulation texture are read back using mirrored UVs at the pool rim so the boundary behaves like a reflecting wall instead of a fixed zero height.
The coupling term is (neighbor average − local height): that quantity is scaled by Wave response and accumulated into the velocity channel, then height is updated by explicit integration in time. The frame runs several substeps (see Settings → Wave simulation); each substep uses the same discrete coupling gain, not rescaled by variable frame duration, so a browser hitch does not suddenly inject extra energy into the height field. Because an explicit scheme would diverge if the gain were too large, the shader limits the Laplacian drive per step (curvature clamp), clamps velocity, and caps simulated height. The separate sphere/UFO pass also limits how much height can change in one go from the moving solid. Velocity damping removes energy (model viscosity and losses).
User ripples and the solid apply forcing in other render passes. Two textures ping-pong: draw from A into B, swap, repeat.
With gravity, buoyancy uses Archimedes' principle in a blended form: the net vertical force is driven by the fraction of the volume below the water plane y = 0, as if buoyant force rho_water g V_sub and weight rho_body g V swap roles by a smooth interpolation. Interior (Air-filled, Solid, Dense, Custom) picks preset body density relative to water for the ball or UFO mesh; Custom exposes the density slider. Default interior is Solid so the object behaves like a uniform solid unless you choose air-filled or another preset. The density slider applies when you fine-tune Custom. The fluid does not exert reaction forces on the solid (no two-way fluid–solid coupling), so this stays a real-time demo model, not a research CFD solve.
The air-water interface uses geometric optics. Snell's law ties incident and refracted directions (n1 sin θ1 = n2 sin θ2). Underwater tinting traces refracted rays into the pool. Settings → Water: Refraction is the water index of refraction (≈1.33); Reflection sets a minimum reflectance so the surface stays visibly glossy when viewed straight on (on top of physics-based Fresnel). Fresnel blending uses Schlick’s approximation with F0 computed from that IOR at normal incidence; grazing angles still trend toward full reflection.
The sky is a cubemap; scene geometry appears in traced rays. Surface roughness widens the sun glint (specular exponent ramps from soft to very sharp) and blurs the reflected sky by averaging a few directions around the reflection vector — there is no mip pyramid on the cubemap, so this is a cheap stand-in for glossy env sampling. Foam scales a white mix driven by the discrete 5-point Laplacian of the height field (high curvature → brighter foam), not a separate fluid spray simulation.
Caustics are focused irradiance on the pool floor: the wavy surface refracts parallel sunlight into envelopes that brighten where ray density peaks (same idea as light patterns under a pool). This implementation uses a fast screen-space approximation ( Evan Wallace). Shadows and rim terms are analytic shortcuts in the fragment shader instead of global illumination.
Depth absorption (Beer-Lambert): in real water, light is absorbed along path length; red is damped faster than blue, so deep views go darker and more cyan. The demo multiplies per-channel transmittance exp(-sigma * distance) for the camera-to-surface path, the surface-to-scene ray in water, and the view ray to pool tiles and the object when they are underwater. Depth absorption in Settings (Water) scales that model: 0 turns it off, 1 is the default strength, values above 1 exaggerate murk. This is not full volumetric multiple scattering; it is a real-time single-segment tint.
This demo trades physics detail for 60+ FPS on a laptop. If compute were not a limit, you would likely move to 3D incompressible flow (Navier-Stokes or lattice Boltzmann) with a proper free-surface representation (level sets, cut-cell grids, or hybrid FLIP/PIC particles) instead of a single height per column. You could add two-way coupling so the solid pushes the fluid and vortices and pressure react back (CFD with moving bodies), plus turbulence (LES or even DNS on a fine enough grid).
On the light side, path tracing or photon mapping could replace analytic caustics; volumetric multiple scattering and in-beam light stages would go beyond the screen-space Beer-Lambert hint you can tune in Settings. Dispersion-correct water waves (frequency-dependent speed) and three-dimensional foam / spray from sheet breakup and entrained air would still need extra passes or particles beyond the height-field curvature hint. Grid resolution could jump by orders of magnitude with adaptive refinement near the object and shore.
Pool tile texture: zooboing / Flickr