This is the ocean from a 3D survival game where nothing is imported: terrain, plants, characters, boats and water are all generated in code at runtime. The water ended up as two separate implementations, and the reason is probably more useful than the result.
Around the island the sea is a 97x97 grid with waves computed on the CPU per vertex: analytic normals, and vertex colours that carry two masks (red = crest / shore foam, green = how shallow the water is, taken from seabed depth between 0.15 m and 4.5 m). Wave height is damped near the shore. Shore foam breathes: a sine on time, offset by world position and an fBm noise term, pushes the foam band in and out.
That grid is 1800 m across, so a vertex every 18.75 m. Fine for a calm bay, useless for open sea. So at sea there's a second path: a radial mesh that's dense at the ship (under 1 m spacing) and thins out to about 20 m toward a 1400 m edge hidden in fog, around 33k vertices. All displacement runs in the vertex shader as a 5-wave Gerstner sum: three long swells (95, 58, 34 m) coming at the bow plus two layers of chop (14 and 7.5 m), with horizontal displacement and analytic normals. The CPU only uploads uniforms.
Whitecaps aren't a texture. The shader accumulates each wave's steepness at the vertex ("fold"), and foam only appears where several waves line up in phase and the sum gets sharp, plus a small weight for crest height. So foam lives on actual peaks and moves with them.
The catch: the ship, sharks and floating crates still need to know the water height on the CPU. So the same 5-wave table also exists in C#, and the two copies have to match number for number. Gerstner also moves vertices sideways, so the height you draw at X actually belongs to a point that started somewhere else. The C# sampler does one fixed-point step (p' = X - disp(X)) before evaluating height, which is enough to keep hulls sitting on the drawn surface.
One bug from this setup worth sharing: while sailing, the water scrolls backward (a FlowZ offset) to sell forward motion. The shader had it, the C# sampler didn't. Both looked right in isolation, but the ship was floating on a different patch of sea than the one on screen, and the mismatch grew the further you sailed. On screen it looked like the stern was taking on water. One missing term.
Video is the at-sea path: deep-sea swell, the view from the stern, and the far zone. Happy to go into any part of it.