← Index

AI · Code · 2026

Lee Shore Ocean: a sea the whole stack agrees on

A from-scratch ocean simulation for Unreal Engine 5, built for a period sailing game, where the rendered water, the buoyancy solver and the dedicated server all sample provably the same surface.

Role
Systems design, engineering
Engine
Unreal 5.8
Scale
47,275 lines · 108 tests
For
A period sailing game
The Lee Shore ocean in the Unreal editor at sunset: FFT wave cascades catching low light out to the horizon, with the Ultra Dynamic Weather panel open beside the viewport

The idea

One surface, three consumers

A from-scratch ocean plugin: roughly 47,000 lines of C++ and HLSL, five FFT wave cascades, a three-tier buoyancy ladder, and its own storm system. Plenty of projects have an FFT ocean. The distinguishing property here is that the rendered surface, the physics surface, and the dedicated server's surface are provably the same surface, with an automation test that fails the build the moment they diverge.

That agreement is enforced, not hoped for. The CPU sampler reproduces the GPU's random field bit-exactly (a C++ port of the shader's hash, byte for byte), and every amplitude knob carries an explicit contract that it be applied identically by the GPU seeding, the CPU sampler, and the physics integration. The headless server can't dispatch compute shaders at all, so rather than settle for a cheap approximation it evaluates the same multi-cascade ocean on the CPU and publishes that as the authoritative field the clients' water must match.

The discipline paid off in a satisfying way: a units audit found wind speed in knots being fed to a boundary expecting metres per second, masked for months by a compensating clamp. Fixing it collapsed the peak height disagreement between CPU and GPU in a storm sea from 3.66 m to 0.275 m.

The build

Five cascades, one dispatch

The wave field is a JONSWAP spectrum with real directional spreading, evaluated as four independently-tiled FFT cascades covering disjoint wavelength bands from 1024 m down to 16 m, plus a fifth swell cascade carrying its own direction, wind speed and fetch, so long-period waves arrive across the local wind and telegraph weather before it lands. One GPU dispatch reconstructs height, slopes, choppy displacement and the deformation Jacobian that foam, caustics and breaking detection all key off, measured at 0.34 ms median against a 1.2 ms budget.

The hardest problem wasn't the waves; it was keeping the consumers honest about them. The material doesn't draw the literal sum of cascades. It fades them by camera distance and pushes every vertex sideways with the chop. So the physics sampler has to mirror both, chop inversion included, or the hull floats above the drawn wave and then plunges through it.

The engineering culture shows in how decisions were made: the naive transform stayed because it profiled at 2.6× under budget; the spatial storage layout was chosen by benchmark; the GPU-readback cadence dropped to every third frame after a 3.6 ms measurement. Each call is written down with the number that decided it, and 108 automation tests hold the line: spectrum moments against analytic values, determinism soaks, regime boundaries at exact depths.

The work

Three solvers, not one solver with a quality dial

Buoyancy is a real LOD ladder. The full tier clips every hull triangle against the curved water surface and integrates hydrostatic pressure over the submerged pieces, so heel, trim and pitch come from geometry alone. The pontoon tier holds the same waterline from a handful of sphere probes with added mass and righting moments, for fleets. The anchored tier skips physics entirely and rides the sampled wave kinematically, for moored hulls that must look alive and cost nothing. Switching is distance-bucketed with hysteresis and a blended handoff, so a promotion never pops, and forces apply per physics substep rather than once a frame.

The part I like most: nothing in the code detects a wave face or applies a "surf impulse." Pressure is integrated along each submerged facet's own normal, and on the flank of a swell those normals tilt downhill, so down-slope thrust that grows with steepness simply falls out of the math. Shoaling keyed to real seabed depth then steepens waves as they approach shore, which is what makes running a following sea into shallow water feel dangerous rather than decorative.

Note

The honest version

This page is written from an engineering audit of the module, and the numbers above are measured, not estimated. The audit's gaps belong here too. The module boundary has eroded: a ship-damage system, a sail rig and a soundscape live inside the ocean plugin (roughly 40% of its public surface isn't ocean), so the ocean can't yet be lifted out and reused cleanly. The sampled water velocity is vertical only, so wave-driven drift doesn't exist yet; the fix is known and documented. And the GPU-readback path is proven to sixty ships. Fleet counts beyond that aren't.

None of those are hidden in the code. They're written down next to the measurements, which is the working method this plugin argues for.