---
name: fft-ocean
description: >
  A wind-driven FFT ocean and one rock in one Three.js file. Three cascades,
  persistent foam from the Jacobian, spray, and a rock whose wetness remembers
  the high-water mark. Use when changing the spectrum, the foam, the spray or
  the rock on WebGPU. The reference file is ocean.html beside this skill.
  If they disagree, the HTML wins.
---

# FFT ocean

One HTML file plus `tex2.jpg`. Three.js 0.186.1 from a CDN via an import map (`three` and `three/webgpu` both point at `three.webgpu.js`, `three/tsl` at `three.tsl.js`, `three/addons/` at `examples/jsm/`). `WebGPURenderer` only. Set `renderer._getFallback = null` before `init()`. If `navigator.gpu` is missing, or the backend is not WebGPU after `init()`, stop. No WebGL fallback.

Open it through a server. `file://` will not load the ES modules. `php -S localhost:8000` or `npx serve`.

## Cascades

`CASCADES` is the swell, the chop and the ripples. Tile sizes are `503`, `47` and `23`. They share no factors. Do not put a round `L` back. `256` across a `512` plane tiled.

Resolutions are `256`, `256` and `128`. The fine cascade is half resolution on purpose. Its amplitude is `4.0` at `N=128`. The `1/N^2` IFFT scale means that number is not the old `N=256` amplitude. Do not copy it across.

`h0` is Phillips-like: `exp(-1/a^2) / k^4`, wind alignment `dp^2`, cutoff `exp(-k^2 * cutoff^2)`, against-wind damped to `0.08`. Gaussians are Box-Muller. `windSpeed` is `14`.

Each frame runs two spectra and two IFFTs per cascade. Field 1 packs height and `Dx`. Field 2 is `Dz`. `heightScale` is `40`. `choppy` is `1.3`. `log2(N)` is even for both sizes, so the butterfly ends in buffer A. Do not add a stage without checking that.

`surfaceHeightAt` inverts the horizontal displacement with 4 fixed-point steps. Two steps bury the spray under a crest. Do not drop them.

## Foam

`buildDeriv` stores foam per cell. Jacobian `J` below `foamThreshold` (`0.62`) births foam at `foamBirth` (`2.6`). It decays by `foamDecay` (`0.5`) per second and stays in `0..1`. The finest cascade is left out of the foam sum. It folds all the time and paints the whole sea.

The bubble mat runs only where foam is above `0.01`. Do not lift that gate.

## Rock

`wetMax` is its own `256` grid on the swell tile. Each step keeps `max(height, previous - dryRate * dt)`. `dryRate` is `0.14` m/s. Wet albedo is `0.26` of the dry colour, slightly warmer. The splash zone stays soaked.

`tex2.jpg` is triplanar. An icosphere's own UVs pinch. Foam on the rock is offset on Y only. An X or Z offset makes it slide sideways.

Spray is `PCOUNT` `80000`, emitted in a disc of radius `19` around the rock. The budget sets the density. Dead particles respawn immediately, so a spawn-probability tweak does almost nothing. Bubbles are `BCOUNT` `20000` inside `BUBBLE_R` `22`, and they die when the foam under them dies.

`OrbitControls`. `maxPolarAngle` is `Math.PI * 0.497`, so the camera stays above the water. Drag right turns the scene with the cursor. Do not swap the spherical mapping.
