A wave is just a moving sine
Drop one ripple on a flat sea and it's a sine wave: a smooth up-and-down with a height
(amplitude), a crest-to-crest distance (wavelength), and
a speed it travels at. On its own it looks like a bedsheet flapping — too regular to pass
for the sea.
The trick is that real water is many of these added together: long, slow swells
carry the big silhouette, and shorter, faster chop rides on top for texture. Stack a few
and the regularity disappears. That's the entire idea behind the field — a
sum of sines.
Pull the Wave layers slider below from 1 up to 6. One layer is that
flapping bedsheet. By six, you've got a believable, choppy ocean — and the orange bike is
floating on it the whole time.
Our default sea is six waves: two long swells (50 m and 85 m) whose periods beat
together into an "occasional big set" every ~24 seconds, plus four bands of chop fanned
within ±25° of the swell direction. Keeping every wave within a narrow fan is deliberate —
an earlier tune spread them across 190° (literally the physics definition of "confused
seas") and the bike just got jostled. Coherent swell marching one way is what you can
learn to ride.
Gerstner, minus the expensive half
Film and AAA water usually reaches for Gerstner waves, which don't just
move points up and down — they also pull them sideways toward each crest, giving
that sharp, pinched peak. Looks fantastic. The catch: once points slide horizontally, you
can no longer ask "what's the height at this exact spot?" without solving the motion
backwards. For physics that's death by a thousand iterations.
So we keep the vertical motion and drop the sideways shove. The height at any point is a
plain closed-form sum:
y(x, z, t) = baseY + Σ Aᵢ · sin(kᵢ · (Dᵢ · xz) − ωᵢ · t + φᵢ)
No inverse solve, no iteration — one pass and you have the height and the exact
slope (the surface normal) for free, because the derivative of a sine is just a cosine.
The visual loss is small at arcade amplitudes, and the part players actually feel — the
launch off a crest — comes from the slope, which we keep. The fine sub-meter chop that the
dropped term would have added is faked back in on the GPU with scrolling detail-normal
textures, purely cosmetic.
That cosmetic layer has had its own long tuning arc. An early version piled on extra
readability overlays — contour-line foam, posterized value bands, rising-face strokes — and
at full strength they read as allover noise rather than a legible sea. The fix was
to dial almost all of them back to a whisper (and delete three outright once we measured
what each cost per fragment), letting a curvature-driven whitecap and a roughness-coupled
reflection carry the swell read instead. How the water spends its visual budget so the
gameplay stays the loudest thing on screen is its own story —
Chapter 08.
Why it matters for feel: because height is closed-form, the game can ask
the sea "how high are you, right here, right now?" thousands of times per frame for almost
nothing. That budget is what lets every bike float on the same waves you see, instead of
on a cheaper stand-in.
One formula, two consumers
Here's the rule that keeps the water honest: the height function lives in exactly
one place, and two very different systems read from it.
-
The physics (CPU). Every fixed step, each bike samples the surface
under its hull to know where the water is and which way it's tilted, then floats and
pitches accordingly.
-
The visuals (GPU). The water shader evaluates the
same sum of sines, with the same parameters and the same clock, to move the
mesh vertices you see.
Because both sides compute identical math, the crest you watch roll under the bike is the
crest the physics launches it off — to within floating-point precision. There's no "visual
ocean" and "physics ocean" drifting apart. The readout panel in the demo shows the height
the physics would feel under the bike at this instant; watch it rise and fall as crests
pass, and watch the bike's tilt track the slope.
The wake you can jump
A moving bike drags a wake — the V-shaped wash trailing a boat. We add it as another term
in the very same field: a Kelvin-style V that widens behind the rider, with its ridge
riding the edge of the V and gentle "scallops" scrolling backward down its length.
Crucially, the wake isn't just a foam decal — it's real displacement in the height
function, which both the visuals and the physics read. So a trailing rider who clips your
wake genuinely gets bumped by it, and a well-timed line can jump off it. Switch
the bike to Ride a wake in the demo and orbit around behind it to see the
V carve into the surface.
Per-track wave zones
One global sea would make eleven tracks feel the same. So tracks can drop
wave zones — boxes that locally crank the amplitude, tighten the chop,
re-aim the swell, or add a periodic surge. A calm lagoon and a track with a tsunami
rolling through an arch can share the exact same wave engine; the zone just blends in its
overrides where the author painted them. (We'll give zones their own demo in a later
chapter.)
Read the code
Everything above lives in one focused, dependency-free module:
src/engine/sim/water/wave-field.ts. It's pure math with no rendering
imports, which is exactly why this page could pull it straight into a Three.js demo. The
design notes and references (Sea of Thieves, the Atlas water talk, Tessendorf) are in
docs/water-deep-dive.md.