Case study

Wave Laboratory: technical report

Side project · simulation & graphics

Summary

Wave Laboratory is a browser-based ocean-wave and ship-motion sandbox: it builds a sea state from oceanographic spectra, synthesises the surface with FFTs, floats 6-degree-of-freedom ships in it and records the results with virtual instruments. It is a personal project under the MIT license, version 0.1.0. It needs Node 22 or later to build and a browser with WebGL 2 to run. The conventions in docs/PHYSICS.md are the single source of truth for both the CPU physics and the GPU renderer; the document states that if code and file disagree, that is a bug.

This is a simulation tool. It has been checked against analytic and textbook results, listed below, and not against measurements from a wave tank or sea trial.

System and data

Physics models

Linear waves. Dispersion is ω² = (g k + (σ/ρ) k³) tanh(k h), with finite depth h and surface tension σ. Dynamic pressure attenuates with depth by cosh(k(z+h)) / cosh(kh). Below the surface, pressure uses Wheeler stretching.

Spectra. Each wave system has an energy spectrum, a directional spreading function and a seed. The table gives what is implemented.

Model Role Key parameters and notes
JONSWAP Developing wind sea α, peak frequency ω_p, peak enhancement γ (3.3 used for focused groups), σ = 0.07 below and 0.09 above the peak; normalised so 4√m₀ = H_s
Pierson–Moskowitz Fully developed sea JONSWAP with γ = 1, normalised to H_s
Bretschneider Two-parameter sea Same family as above, normalised to H_s
TMA Finite-depth correction Kitaigorodskii depth factor, applied when depth is under 1000 m; H_s re-normalised
Hasselmann et al. (1973) Fetch-limited wind sea Growth with fetch, capped at the fully developed limit, H_s ≈ 0.209 U²/g
Regular (Airy) Single analytic wave η = a cos θ; a second-order Stokes harmonic is added when choppiness is above zero
NewWave (Tromans et al. 1991) Focused rogue group 40 linear components between 0.6 ω_p and 3 ω_p, phased so crests coincide at a chosen place and time

Directional spreading is one of cos-2s, Mitsuyasu (frequency dependent) or Donelan–Banner sech², each normalised so the integral over direction is 1. A uniform surface current Doppler-shifts the whole sea.

Surface synthesis. The surface is a sum of four periodic cascades with sizes 1024, 256, 64 and 16 m. The first three are evaluated by the CPU physics on 128 by 128 grids; the fourth runs to the grid limit and is visual only. Initial amplitudes h₀(k) are made once on the CPU from a counter-based hash of the system seed, cascade and wavenumber index, so every grid resolution sees the same sea. Horizontal choppy displacement uses a choppiness factor, and the Jacobian of the displacement flags folding: J below zero marks a breaking crest, which drives foam.

Rogue waves. A focused wave adds to the background sea, so the crest at the focus sits above it by the chosen crest amplitude. The inspector compares the equivalent steepness with the Miche limit, 0.142 tanh(kh). The note records Draupner as 18.5 m over 11.9 m, a crest-to-H_s ratio of 1.55, in a typical recorded range of 1.2 to 1.6.

Ships. Hull forces come from integrating pressure over the instantaneous wetted hull, as hydrostatics plus Froude–Krylov, sampled on up to 280 water columns. Added mass uses strip theory with Lewis-form sections and a three-dimensional end reduction. Radiation damping is tuned to 15 per cent of critical in heave and pitch, and roll damping follows an Ikeda-type decomposition. Wind acts through frontal and lateral areas with drag coefficients of 0.7 and 0.9. Square sails, flooding through watertight compartments with free-surface loss of stability, and an elastic catenary anchor line are modelled; each has its own section in the physics note.

Weather. Gusts come from 24 Fourier modes drawn from a von Kármán spectrum, carried downwind as frozen turbulence. Whitecap coverage follows Monahan and O’Muircheartaigh (1980), visibility follows Koschmieder’s law with a rain term, and lightning is visual only.

Instruments. Welch’s method gives spectra from Hann-windowed segments with 50 per cent overlap, scaled so the integrated density equals the signal variance. Zero-up-crossing statistics follow IAHR and PIANC conventions. Motion-sickness incidence follows O’Hanlon and McCauley (1974).

Underwater optics. Jerlov water types give diffuse attenuation coefficients K_d for red, green and blue. They drive rendering only, not the physics. Light is attenuated as c·e^(−K_d·d) plus in-scattered colour, and from below the surface shows the rippled sky inside Snell’s window (48.6°) and a mirror outside it.

Architecture

The main thread runs the React UI, a Zustand store and the renderer. A Web Worker runs the simulation: the CPU ocean field, the vessels and a fixed-rate recorder. SimClient keeps one request in flight. Edits go through a Zod-validated store, which also keeps undo history and bumps a revision that reloads the worker.

Stage Module path
Maths, RNG, fixed-step clock, units src/core/
Experiment schema (Zod) and presets src/schema/
Dispersion, spectra, spreading, cascades, CPU FFT src/ocean/
Hulls, hydrostatics, hydrodynamics, wind, sails, flooding, mooring src/vessel/
Gusts, squalls, rain, lightning src/weather/
Welch PSD, statistics, MSI src/instruments/
Simulation kernel, worker, client src/sim/
WebGL 2 renderer, GPU FFT ocean, post stack, underwater pass src/render/
React application src/ui/

Only src/render imports Three.js. The renderer is the sole GPU dependency and is isolated behind render/api.ts.

Performance budget at default settings: one CPU ocean snapshot (three cascades, six 128 by 128 FFTs each) takes about 15 ms in the worker at 20 Hz, and a vessel step takes under 1 ms per vessel at 120 Hz. The GPU FFT (four cascades at 256 by 256) takes a few milliseconds.

The GPU and CPU cross-check

The GPU inverse-FFTs the same h₀ amplitudes the CPU uses. On each new sea the renderer reads one texel back, compares it with the CPU transform and calls console.error on a mismatch, which e2e/app.spec.ts fails on. Devices without 32-bit float render targets use half-float and skip the readback. The GPU draws only elevation and horizontal displacement; slope, Jacobian and foam are finite differences of the displaced surface, and pressure and orbital velocity stay on the CPU.

Iteration history

The repository has 131 commits, from 2026-10-01 to 2026-10-08. The plan document’s status table lists what shipped in v0.1.

  • Core — tooling, schema, deterministic fixed-step core. Shipped.
  • Ocean — spectra, wind-sea growth, spreading, four cascades, choppy displacement, regular waves, CPU reference and GPU. Shipped.
  • Vessels — pressure integration with waterline clipping, radiation, viscous drag, propulsion, rudder and autopilot. Shipped.
  • Damage, moorings, instruments, UI — flooding, anchors, Welch PSD, MSI, CSV export, presets, share links, phone layout. Shipped.
  • Rendering extras — HDR post stack with bloom, underwater camera with Snell’s window. Shipped.

Results

There are no benchmark numbers against external data. The evidence is the validation suite below.

Validation suite

The longer statistical checks live in *.validation.ts files and run with pnpm validate, with a 120-second timeout per test. There are 9 files and 25 test definitions.

File What it checks
ocean/ocean.validation.ts A virtual wave gauge reproduces the requested JONSWAP spectrum; linear seas have Gaussian elevations and H1/3 close to Hm0; choppy seas show positive skewness (sharper crests)
sim/rogue.validation.ts A 12 m NewWave crest peaks at the focus gauge near t = 75 s and the ship ships green water
vessel/vessel.waves.validation.ts Heave RAO close to 1 for 20 s head seas; slamming and green water in steep head seas
vessel/vessel.manoeuvring.validation.ts Plausible steady speeds, 30° autopilot turns without large overshoot, rudder sign and rate limit
vessel/vessel.wind.validation.ts Leeway and heel in a beam wind, carrier course holding, square-rig sailing, reefing in a storm
vessel/flooding.validation.ts, damage.validation.ts Flooding and damage behaviour, including Torricelli decay and flooded-barge roll period
vessel/mooring.validation.ts A trawler in a 15 m/s wind settles at a chain pull of 3.9 kN against a 3.8 kN wind load; a cargo ship stays within its watch circle in a 2 m swell; the anchor drags in a 45 m/s gale
vessel/vessel.perf.validation.ts Performance check

The physics note also records that the simulated roll period of a flooded barge matches the closed-form value within 1 per cent, and that the elastic catenary was compared with a direct numerical integration of the governing equations for three stiffnesses.

Test counts

These are counts of test definitions found by grep, not a run result. I did not run the suite for this report, so I make no pass-rate claim.

Suite Count Where
Unit 328 definitions in 44 files src/**/*.test.ts
Validation 25 definitions in 9 files src/**/*.validation.ts
Browser (Playwright) 24 definitions in 2 files: 19 desktop, 5 mobile e2e/app.spec.ts, e2e/mobile.spec.ts

The grep counts it( and test( at the start of a line, so parametrised and template-generated tests are undercounted. The Playwright config defines desktop Chromium at 1440 by 900, a Pixel 7 profile and an iPhone 14 viewport, so each browser test can run several times.

Figures

The app in light theme: a container ship with a white wake on a blue-green sea, scene list on the left, sea-state panel on the right and playback controls below.

Light theme overview with a moderate sea and a container ship.

The app in dark theme in a hurricane. Rain streaks across a grey sea, a dark hull is faintly visible, and the sea-state panel reports an 11.23 m significant height and sea state 8.

Dark theme hurricane preset with a tanker and a carrier.

Dark theme view following a container ship through a rough sea. The side panel explains a 12 m focused crest in an 8 m significant-height sea peaking at 75 seconds.

Rogue wave preset following a container ship.

A view from underwater: a smooth wash of blue light with no hull or seabed in frame, a depth badge reading 4.0 m and a round chart panel at lower right.

Underwater camera looking up at 4.0 m.

A dark green hull runs diagonally across the lower left under a bright rippled surface with foam patches.

Underwater view of a hull at 6.1 m.

A dark hull edge cuts diagonally across the lower left. Above it the underside of the sea surface shows pale blue light with dark foam patches against deep blue water.

Clear oceanic water at 3.9 m.

An entirely dark green underwater view with faint lighter and darker patches and a chart panel at lower right.

Turbid coastal water, Jerlov type 9, at 3.7 m.

A container ship on a sunset sea with a large glowing sun and glare spreading over the ship and water.

HDR bloom on.

The same sunset view with a smaller, crisper sun and no glow across the ship.

HDR bloom off.

Reproducibility

pnpm install
pnpm dev            # http://localhost:5173
pnpm test           # unit tests
pnpm validate       # longer statistical physics validation
pnpm build && pnpm test:e2e   # browser tests on desktop and phone viewports
pnpm check          # format, lint, typecheck, unit tests and build

Experiments are deterministic: the same saved file produces the same sea, because amplitudes are seeded per cell with a counter-based hash and the physics advances in exact fixed steps. Tools are pinned in package.json and pnpm-lock.yaml: for example Three.js ~0.186.1, Playwright 1.56.1 and TypeScript 5.9.3. The package manager is pnpm 10.28.0.

The live demo is the production build committed to docs/ on main by pnpm deploy:pages and served by GitHub Pages from a branch. There are no GitHub Actions workflows. The build uses relative asset paths, so it runs from any static host.

Engineering quality

Limitations and next steps

What the physics note says is not modelled or is simplified:

Plan versus shipped

The master plan (last updated 2026-10-02) proposed raw WebGPU. The shipped renderer uses WebGL 2 through Three.js, because every current browser supports it and the renderer is isolated behind an interface so a WebGPU backend could follow. The plan also had vessels read the GPU surface back. The shipped design samples a CPU evaluation of the same spectral components in the worker, which keeps physics deterministic and testable in Node. The plan’s later phases are not built: Stokes and cnoidal waves, grid shallow-water and Boussinesq solvers, a scripting API, and higher-fidelity radiation. The plan also describes a “Reference” CPU float64 mode that I have not verified as shipped.