Ocean
A real-time ocean for three.js: FFT waves with breakers on the shore, foam, caustics, a sky with clouds and an underwater view with sun shafts, set up for games.
It's Ultra-Water by Humpalumps (MIT), set up for games by Patch: seven scenes, from a sunset at sea and a storm to a tropical lagoon, breakers, a rock in the surf and a calm lake, all switched in a frame, with a sky, a water colour and the wind to choose. Take the camera below the surface and you're under water. For a game, it also floats things on the GPU's own waves and has an outboard boat to drive (the demo shows neither). In the live demo, drag to look and scroll to zoom.
What Ultra-Water does
Everything here that looks like water is Ultra-Water's, unmodified in lib/ultra-water/ (its README.md explains each part in depth):
- Waves: a Tessendorf FFT in four cascades (600, 85, 12 and 2.1 m), a JONSWAP wind sea plus swell, capillary ripples, foam from the waves' folding.
- Shore breakers: waves that slow down, bunch up and refract around the seabed, lean and break with translucent lips, peel along the beach, and run up the sand as swash with a wet line; spray off the breaking crests.
- Shading: sun glints that twinkle facet by facet, planar reflections, refraction with depth colour, light scattered through crests, crest and shore foam that churns and thins to lace.
- Caustics focused by the actual waves onto the seabed and anything under water.
- Under water: Snell's window, total internal reflection, sun shafts through the waves, marine snow, a meniscus at the waterline and drops on the lens when you come up.
- Sky: a physical atmosphere (sunrise to noon, the sun below the horizon) with clouds, which lights everything.
- Its island with sand ripples and rocks, its crate, barrel and buoy, and its own post (auto exposure, bloom, AgX).
What Patch adds
ocean.js, the API for a game:createOcean({ renderer, camera, quality }), thenrender(dt)every frame.SCENES: golden hour, open ocean, stormy sea, tropical lagoon, breakers, surf rock and a calm lake. Each sets the coast, the waves, the sun and clouds, the water's colour and a camera;setScene(id)swaps them in a frame, because every coast is built once (build()). The lagoon, breakers and surf rock are Ultra-Water's own views and shore waves, as it ships them. Thewindsetting doesn't raise the surf past the scene's.set({ wind, sun, clouds, water, clarity, glitter, caustics, spray, windDirection, swellDirection, waveSpeed, exposure });WATERShas Ultra-Water's six water colours and Patch's lake and storm grey.float(object, { type: 'boat' } | { size, density })floats anything. Each body samples the water heights the GPU computes for exactly the drawn surface, with drag against the moving water, so it bobs, drifts and rides the surf; it rests on the seabed and beaches.drop(kind)andflotsam()drop Ultra-Water's crate, barrel and buoy, which splash and start ripple rings.- The boat (
lib/boat.js): a 4.4 m open boat with an outboard, off untilsetBoat(true).drive(thrust, steer),goTo(point), or leave it circling; a keel keeps it upright in steep seas, and it leaves a wake of ripple rings. - Under water needs no switch: whenever the camera is below the surface (Ultra-Water reads the water height at the camera every frame) it draws the underwater view, with the same look however the camera got there.
setUnder(true)only moves the camera to a good spot under water near the view (on any coast, about 9 m deep looking up the slope towards the shore; on open sea, looking up at the surface).
lib/pipeline.js: Ultra-Water's frame, adapted from itssrc/main.js(MIT): several coasts in one pipeline, two quality tiers, the caller's floating bodies.lib/query.js: water heights that hold in steep seas (a damped inversion of the choppy displacement).lib/lake.js: a lake shore with forested hills and mountains, as a height function with meshes.
Use it in a game
It draws the whole frame with three's WebGLRenderer (Ultra-Water is GLSL), so give it the renderer and the camera:
import * as THREE from 'three';
import { createOcean } from './ocean.js';
const renderer = new THREE.WebGLRenderer({ antialias: false, depth: false, stencil: false });
const camera = new THREE.PerspectiveCamera(50, innerWidth / innerHeight);
const ocean = createOcean({ renderer, camera, quality: 'low' });
await ocean.build(); // bakes every coast's shore maps, about a second
ocean.setScene('tropical');
ocean.world.add(myDock); // your own things, lit by its sky
const crate = ocean.float(myCrate, { size: new THREE.Vector3(1, 1, 1), density: 500 });
renderer.setAnimationLoop(() => {
ocean.drive(input.forward, input.turn); // the boat, -1 to 1 each
ocean.render(clock.getDelta());
});
ocean.waterHeight() is the water at the camera, for sounds and swimming; ocean.groundAt(x, z) is the seabed or land.
Warm it up before showing it. The first draw of each scene and of the underwater view compiles shaders and bakes its sky. demo.js's warmUp() does all of it (every coast, every scene above and under water, every sky, both quality tiers) with the canvas hidden before the demo says it's ready, so nothing stalls after.
Cost
It renders the whole frame (sky, sea, coast, reflections, its own post), so it's measured as the whole scene. On an RTX 4090 at 1920 × 1080, back-to-back frames timed to the GPU finishing:
| Quality | Every scene, above and under water | Slowest |
|---|---|---|
| low (2× MSAA, reflections at 0.35, no bloom) | 0.8 to 1.15 ms | 1.14 ms (surf rock) |
| high (4× MSAA, reflections at 0.5, bloom) | 0.9 to 1.4 ms | 1.37 ms (surf rock) |
Ultra-Water's own README has a 3060 at 3.3 MP (3.3 to 4.8 ms) and a phone. On a slower GPU the demo lowers its resolution, to half at most, whenever more than one frame in 25 misses 60 fps, and raises it again once frames are clean (Ultra-Water's mobile profile, for every device). Its page is 0.8 MB and loads nothing else.
What it's bad at
- It owns the frame. It renders the whole picture with its own post, on
WebGLRenderer; it isn't a material you drop into a WebGPU game or someone else's pipeline. - Big breakers still read as thin bands of foam. Waves are a height field, so nothing curls over. From high above (over about 100 m) the crest foam on the island's shelf still shows as thin curved lines.
- One sea level. No rivers, no terraced water, no flooding.
- New coasts cost load time. Each bakes its shore maps once, about a fifth of a second on a fast PC, and needs its height as a function.
Controls
The block page talks to the demo over the patch-block message channel (lib/patch-block.js).
For people (the block page shows these):
| id | Control | Type | Range | What it does |
|---|---|---|---|---|
scene | Scene | choice | 7 options in 2 groups, each with a picture; default Golden hour | Where you are: each sets the coast, the waves, the sky, the water and the camera together. |
sky | Sky | choice | 7 options, each with a picture; default the scene's own | The time of day and the clouds: the scene's own, dawn, golden hour, morning, midday, overcast, after sunset. It stays when the scene changes. |
water | Water | choice | 9 options in 2 groups, each with a picture; default the scene's own | The water's colour (absorption, scattering, foam and murk). Natural: the scene's own, clear, tropical, storm grey, lake, murky brown. Fantasy: toxic green, purple, rust orange. |
wind | Wind and waves | number | 0.2 to 2 ×, default 1 | The wind and the swell, as a factor on the scene's. Below 1 it calms the surf too; above 1 the surf stays as it is. |
The scenes: Open sea: Golden hour, Open ocean, Stormy sea. Shores: Tropical lagoon, Breakers, Surf rock, Calm lake. Every option of the three choices has a picture, thumbs/<scenes|skies|water>/<id>.webp: a still of this demo (make-thumbs.mjs). Each scene in its own light, each sky over Golden hour, each water in the Tropical lagoon's shallows. Since 1.0.4 the scene id beach (Island beach) is gone: use breakers.
There are no triggers: nothing floats in the demo.
For agents (14 more, in recipe.json with every range and default): sun (the sun's height, which scenes and skies set), under (Dive: moves the camera under water near the view), clouds, sun.azimuth, wind.direction, swell.direction, wave.speed, deep (a color: the deep water's colour), clarity, glitter, caustics, spray, exposure and quality (low or high). A scene, sky or water change sends the page the sun, clouds and deep colour it set.
In the frame: drag to look and scroll to zoom; orbit or zoom below the surface to go under water.
Build it
npm install
npm run build # writes recipe.json, then dist/ (0.8 MB) with thumbs/ copied in
node make-thumbs.mjs # (only to retake the pictures and cover.jpg from dist/)
dist/ is the live demo exactly as released, with index.html at the top and nothing loaded from other hosts.
| Path | What |
|---|---|
ocean.js | The game API, scenes, floating things and the boat (Patch, MIT) |
demo.js, index.html | The demo page: the block controls, camera and warm-up (Patch, MIT) |
lib/ultra-water/ | Ultra-Water by Humpalumps, unmodified (its src, LICENSE, README.md, THIRD_PARTY_NOTICES.md), commit 30ea65e |
lib/pipeline.js | Ultra-Water's frame, adapted from its src/main.js (MIT) |
lib/boat.js, lib/query.js, lib/lake.js | The boat and floating, water heights, the lake (Patch, MIT) |
recipe.json, make-recipe.mjs | The release recipe (mode browser with its block section) |
thumbs/, cover.jpg, make-thumbs.mjs | The Scene, Sky and Water choices' pictures and the cover, stills of this demo |
lib/patch-block.js | Patch's block demo helper, as served at patch.gg/patch-block.js |
fonts/ | Red Hat Mono 500, latin, for the hint |
build.mjs, vite.config.mjs | The build |
Left out of Ultra-Water: its index.html, package.json and lockfile. Its src/main.js (the demo page with its GUI and performance panel) is kept as it is but unused: demo.js replaces it.
Credits and licences
- Ultra-Water by Humpalumps, MIT, © 2026 Humpalumps (
lib/ultra-water/LICENSE): the ocean, shore breakers, foam, caustics, underwater view, sky, island, floating props and post. Small parts of it are adapted from three.js, Dave Hoskins' Hash without Sine and Sébastien Hillaire's UnrealEngineSkyAtmosphere, all MIT (lib/ultra-water/THIRD_PARTY_NOTICES.md). Credit line: "Ultra-Water by Humpalumps (MIT)". https://github.com/Humpalumps/Ultra-Water - three.js: MIT, the three.js authors.
- Red Hat Mono: The Red Hat Project Authors, SIL Open Font License 1.1 (
fonts/OFL-red-hat-mono.txt). - Patch's parts (
ocean.js,demo.js,lib/pipeline.js's changes, the boat, the water query, the lake, the scenes and the pictures): Patch, MIT (lib/boat.js,lib/query.jsandlib/lake.jscame from Patch's lab as CC0, as their headers still say, and may be used under either). There are no image, model or sound files: everything the demo draws is made in code.
Code: MIT (LICENSE).