web-ocean appeared on GitHub on 16 October and had 611 stars and 99 forks within two days. It is a real-time ocean renderer written against WebGPU directly — no engine, no three.js, no rendering library — and it ships as ocean.html, a self-contained 61 KB minified page that works from file://.
There is a live demo. You need a recent Chrome with WebGPU and, on a laptop, you need to force Chrome onto the discrete GPU, or the README warns it will quietly fall back to the integrated one.
What makes it worth reading rather than just looking at is the README, which is a genuinely complete description of how a modern ocean is assembled. Most projects at this level publish a screenshot and a shader file. This one publishes its spectrum.
The waves
The sea is a Tessendorf FFT simulation with six cascades of 256 × 256 samples. The patch sizes span 1103 m down to 0.26 m, so wavelengths run from about a kilometre to a few millimetres. The smallest cascade exists specifically to produce the sharp sun sparkles near the camera.
The spectrum is a fetch-limited JONSWAP wind sea with Donelan-Banner directional spreading, plus a narrow long-crested swell layered on top. Dispersion includes the capillary term — the surface-tension correction that matters only at the millimetre end, which is exactly the end that produces glitter. Choppy horizontal displacement sharpens the crests.
The inverse FFTs are radix-2 compute shaders working in workgroup memory. The surface itself is a CDLOD quadtree with 17 levels: vertices 6.25 cm apart near the camera, the mesh reaching 160 km, with continuous vertex morphing between levels so no popping is visible as you fly.
One detail worth calling out because it is so often skipped: the camera keeps itself above the displaced water surface, so waves never pass through it.
The shading
This is where the project earns its look. Four things in the list are the difference between “an FFT ocean” and “an ocean”:
- LEAN mapping. Mip-mapped first and second moments of the wave slopes give correct anisotropic roughness at every distance. The result is that sun glitter widens with distance and the far sea settles into a sheen instead of aliasing into static. Screen-space slope derivatives add specular anti-aliasing on top.
- Sun glitter uses an anisotropic Gaussian slope distribution with Smith masking and the actual angular size of the solar disc, rather than a point light.
- Sky reflection comes from an environment cube map rebuilt every frame with roughness-filtered mips. At grazing angles the reflection direction is tilted to account for which wave facets the viewer can actually see — a correction for the fact that near the horizon you are looking at the sides of waves, not their tops.
- Whitecaps start where the total surface Jacobian folds, which is the physically motivated criterion rather than a height threshold. Foam lives in two camera-centred world-space maps (128 m and 1 km wide), decays over time, drifts downwind, and breaks into streaks as it ages, with a procedural bubble texture for structure.
The water body is turbid blue-green, lit by the whole sky hemisphere, with subsurface glow on backlit crests and wind-gust patches modulating the short ripples.
The sky
The atmosphere follows Hillaire’s approach — transmittance, multiple-scattering and sky-view lookup tables, plus aerosol haze. Two cloud layers sit on top: a broken low cumulus and a fibrous altostratus deck, each with self-shadowing toward the sun, lit rims, silver linings when backlit, and direction-dependent multiple scattering.
The anti-aliasing trick here is neat. Cloud noise octaves are filtered by pixel footprint, and silhouettes are anti-aliased analytically: the screen-space derivative of each layer’s edge function sets the width of its opacity edge. Cloud transmittance along the sun direction then dims the sunlight reaching the water, and aerial perspective is applied over the sea.
Everything renders in HDR with 4× MSAA, then 8-level bloom, AgX tone mapping, a slight white-balance shift and dithering.
It is fast, and the reason is boring
At 2560 × 1440 on an RTX 5070 Ti the GPU takes about 6 to 8 ms a frame, measured with other programs also using the GPU. The HUD reports FPS and GPU time where the browser supports timestamp-query.
The per-frame code reuses preallocated typed arrays and descriptors, so the garbage collector has almost nothing to do. The only recurring allocations are the ones WebGPU itself requires — the swap-chain view and the command buffers. That is the whole optimisation story, and it is the one most browser graphics projects skip.
Settings worth touching
Press H for the panel: wind, swell, choppiness, turbidity, foam, sun, haze, clouds, exposure, bloom. URL parameters do the same job for testing — ?w=2560&h=1440 forces render resolution, cam=x,y,z,yawDeg,pitchDeg places the camera, fov=deg, ui=0 hides the HUD, and any settings key works, e.g. windSpeed=12&swellHs=2&sunElev=8.
Turning turbidity down gives darker, bluer deep water; the default is tuned toward turbid coastal water, which is one of the four limitations the README lists itself. The others: the far water is still somewhat darker than the bright horizon sky above it, the scene is open ocean only with no shoreline or surf, and the clouds are two flat layers rather than volumetric.
How it was made
The author says so plainly, so we will too. From the README:
I built this with Claude Code (Opus 5.5). I asked for the most realistic water it could produce, in WebGPU, with a floating camera, and left the architecture and implementation to it. It checked its own work by rendering screenshots in headless Chrome and having separate critic subagents review them, so it was not grading its own output. I gave feedback along the way, mainly on the clouds, which led to replacing the volumetric clouds with the flat layers and later fixing their edge aliasing.
Take that as stated rather than as a claim about what tooling can do in general. The verifiable part is the artefact: the repository layout is clean (js/wgsl/ holds sim.js, sky.js, ocean.js, post.js, common.js as shader modules), the build script produces both the readable and minified pages, and the technique list is specific enough to check against the literature. The screenshot-and-critic loop is the interesting methodological detail, because rendering is one of the few domains where an automated reviewer can actually see the output.
Running from source
npm install
npm start # serves http://localhost:8080/
npm run build # regenerates ocean.html and ocean.min.html
The minified build strips WGSL comments and whitespace and minifies the JavaScript with esbuild. Licence is MIT.
Sources
- Noniv/web-ocean — GitHub
- Live demo
- README — technique list, performance figures and known limitations