---
name: ocean-foam
description: >
  Build a single-file Three.js ocean: a 16-component Gerstner sum with analytic
  normals, a baked seabed for water-column thickness, planar reflection,
  crest foam that slips down the wave, and shore foam that drains with the
  swash. Use when adding or changing ocean, shoreline, foam or wet sand in
  WebGPU TSL. The reference file is ocean.html beside this skill. If they
  disagree, the HTML wins.
---

# Ocean foam

One HTML file. No bundler, no npm, no texture pack. Three.js 0.186.1 from a CDN via an import map (`three.webgpu.js`, `three.tsl.js`, addons). `WebGPURenderer` only. Set `renderer._getFallback = null`. If `navigator.gpu` is missing, or the backend is not WebGPU after `init()`, stop. No WebGL fallback. No GLSL strings.

`MeshBasicNodeMaterial` for the water. The colour is written entirely in `colorNode`. The seabed and rocks are `MeshPhysicalNodeMaterial`, because the wet look is albedo plus roughness plus a clearcoat sheen.

`mix(a, b, t)` is the free function. The method form reorders its arguments so the receiver becomes the blend factor.

Open it through a server. `file://` will not load the ES modules.

## Waves

`WAVE_COUNT` is 16. Wavelengths run geometrically from `Lmax` 140 to `Lmin` 5.5. Amplitude falls with wavelength (`pow(L / Lmax, 0.85)`), then the set is normalised so the amplitudes sum to `TOTAL_AMP` (2.35). Short components spread wider around the wind bearing than long ones. Seed `0x5eed`.

Deep-water phase speed is `sqrt(9.81 / k)` with `k = 2π / L`.

`gerstner()` unrolls the sum at graph-build time. It returns displacement, tangent, binormal, normal and `fold`. The normal is `cross(binormal, tangent)`, so it matches the displaced mesh. Carry normal, tangent, binormal, fold and height as `varying`. Do not recompute sixteen components in the fragment.

`fold` is the sum of `Q * A * k * sin`. Divide by `FOLD_SUM` (the sum of `A * k` for this spectrum, about 0.36 at full chop) before the crest threshold. Leave amplitude and choppiness inside the term.

`k`, phase speed and phase are compiled constants. Wind bearing and spread update direction and amplitude uniforms through `refreshWaveUniforms()`. Changing the count means rebuilding the material.

The water mesh is `makeWaterGeometry(400, 170, 1500, 0.55)`: fine grid out to 170, then a power curve out to 1500. `frustumCulled = false`. Displacement is multiplied by `vertFade`, which goes to zero between 140 and 620 from the camera.

## Thickness

Do not read thickness from the depth buffer. Bake `bedHeight(x, z)` once into a 512² `RedFormat` / `HalfFloatType` texture covering `BED_EXTENT` 800. `r16float` is filterable. Clamp wrapping. Edges of the texture are already deep water.

```
column = max(positionWorld.y - texture(bed, uv).r, 0)
thick  = column / max(view.y, 0.22)
```

Absorption, red fastest:

```
exp(-thick * absorb * vec3(1.0, 0.42, 0.28))
```

`opacityNode` fades the last `shoreFade` units of column so the plane does not cut a hard line into the beach.

## Reflection and refraction

`reflector()` with `resolutionScale` 0.75. Rotate the target so the plane normal is +Y and set it to `WATER_LEVEL` (0). It hides this material while rendering, so there is no self-reflection. Add normal.xz offset to the captured `uvNode`, scaled down in the distance.

Refraction is `viewportSharedTexture`. Fade the UV offset with `column.smoothstep(0, 2)` so a displaced tap in the shallows does not sample a rock in front of the surface. Clamp the UV away from 0 and 1.

MSAA stays off. `viewportSharedTexture` copies the framebuffer, and WebGPU will not copy a multisampled texture. FXAA is in the `RenderPipeline`. Bloom is optional (`USE_BLOOM`) and only spills the sun glint. Threshold 0.85.

Schlick Fresnel: `0.02 + 0.98 * (1 - NoV)^5`, times reflectivity.

Ripples: `mx_fractal_noise_float` with time on the third axis, finite-differenced, subtracted in tangent space from the analytic normal. Fade with distance. Clamp normal.y above 0.06 so the surface cannot flip under the camera.

## Foam

Crest foam:

- Sample noise in a frame drifting at `crestDrift * phaseSpeed` of the dominant component. Drift under 1 slips backwards down the face. Drift of 1 locks to the crest.
- `sinceCrest` is phase measured from the peak (`π/2` of the dominant component). `exp(-sinceCrest * crestSkew)` parks foam on the back of the crest.
- Coverage is `fold / FOLD_SUM`, smoothed from `crestThresh` across 0.12, times the skew and a grain.
- `dissolve(cells(...), coverage, softness)` breaks the sheet into rafts. `mx_worley_noise_float` is metric 1, squared distance, 0 at cell centres. Invert it or you get walls.

Shore foam uses the same dissolve. Coverage is how thin the column is, times `wetnessAt` when dissolve-over-time is on. Advect the cell UV along the seabed gradient: uphill while `cos(phi)` of the dominant wave is positive, downhill while it drains.

`wetnessAt` stores no history. For amplitude `A` and dominant phase `phi`, a point at height `y` is submerged when `sin(phi) > y/A`. Elapsed time since the water left is a wrapped phase difference, divided by angular frequency. Returns 1 while submerged, `exp(-seconds / dryTime)` after, and 0 above the run-up. The same function drives sand albedo, roughness and clearcoat, and the foam collar on rocks. The collar is weighted by the fraction of the cycle that height spends underwater, so a permanently submerged rock stays unpainted.

`dissolve` remaps the band so zero coverage sits entirely above the pattern maximum. A centre of `1 - cover` leaves specks wherever a cell reaches 1.0.

Bubble specks modulate foam brightness (dark gaps). Do not add white on top of foam that is already near white. Tone mapping eats it, and the sliders look dead.

## Buoyancy

`sampleSurface(x, z, t)` is the CPU copy of the Gerstner sum, same waves, same chop. The buoy reads height and normal from it. If the GPU sum and this function drift, the buoy sinks through the mesh.

## If this goes into a level

The planar reflection is the expensive pass. Bloom is the first thing to turn down. Wave count is compiled, so dropping it is a material rebuild, not a uniform.

Match the shallow colour to the sand or the waterline reads as a cut-out. Keep the camera above the surface (`maxPolarAngle` just under `π/2`). The soft waterline is an opacity fade of the last few units of column. Deleting it puts a polygon edge through the beach.
