# Atlantic swell: rendering notes

## Long-run slowdown and whale review · 23 September 2026

**Spray that never landed.** In an eight-minute real-time run of the FFT default, the spray pool doubled roughly every minute, from 91 k slots at 10 s to 1.28 M by about four minutes, and frame rate fell from 32 to about 14 fps. The same build with `?waves=analytic` and this morning's live build held about 54–71 k slots. Every droplet size in FFT mode was sinking far below the water: the 500 µm class reached 137 m down 31 s into the run. Two faults combined:
- The start-up replay of 4 s of spray history evaluated the FFT surface once, at its end, and extrapolated it linearly for earlier ticks, so replayed parcels met water metres off.
- Contact removes the fraction of a parcel that has crossed the surface, CDF(q1)/CDF(q0). More than about seven spreads below the surface both CDFs clamp at 10⁻¹², the ratio is 1, and the parcel is kept for ever.

Parcels that ended up that deep during the replay then fell indefinitely, and their growing age forced the ring pool to keep doubling. A parcel more than five spreads below the surface (CDF(−5) = 3×10⁻⁷) now counts as landed, and the replay updates the FFT surface at the middle of each 24-tick chunk. The large droplets now land within 1–1.7 s and stay within 2 m of the surface. In a five-minute run the pool stayed at 54–71 k slots, as in the analytic sea, and the heap stayed at 32–55 MB. Frame rate follows the scene: 25–50 fps at 1224 × 765 in headless Chrome on this machine. Heavy spray moments dip below 30 fps, and resolution stays locked after calibration.

**Whale rig and animation.** Skinning the real mesh across one stroke shows a body wave running from head to tail, with fluke tips moving 15.6% of body length peak to peak, all dorsoventral and none lateral. This is right for a cetacean; a fish would bend sideways. The flippers stay swept back, the surfacing arch humps the back and drops the tail before a dive, and tail trim and neck tuck bend in sensible directions. The stroke period of 7.3 s (8.4 s for the larger whale) at about 1.3 m/s gives a Strouhal number of about 0.20, the low end of efficient cetacean swimming.

**Whales carried by the swell.** First breaths used to come 7–18 s after a whale had fully surfaced, with either wave model, and some were cut short. Two causes:
- The head planner held the snout 3 cm under the lowest water within the quintic spline's 6 s support, in absolute terms. In a 3.7 m sea, a surfaced blowhole therefore sat about 35 cm under mean water.
- The same window also took the minimum of the planned draft, delaying every ascent and advancing every descent by about 3 s.

The planner now works relative to the heave of the long waves. A body of length L averages a wave over its length, so it heaves with sinc(kL/2) of each rendered wave's displacement, attenuated with depth as e^{kz}. The whale translates with that heave and its path through the water alone sets its pitch: deep-water waves are irrotational and carry a submerged body without turning it. Tilting the whale with the wave slope had swung the body at 27°/s². Only the waterline ceiling takes the minimum over the spline support, which keeps the curve under every sampled ceiling because the quintic weights are nonnegative and sum to one. The planned draft enters each control at its own time.

First breaths now start 0–7 s after nominal surfacing, most within 1 s (0.55, 6.85, 0.45, 0, 0 and 5 s for the first six with FFT waves). No attempt is fully submerged over 128 passes, even in 7 m seas. The snout stays covered, the approach stays submerged, backs show 0.3–1.6 m, and body vertical acceleration stays under 2.5 m/s². Head acceleration through the water stays under 0.5 m/s², and the carried heave reaches about 2.3 m/s², as for any floating body in this sea. Three tests now compare the head's direction and acceleration with its movement through the heaving water rather than in world space. The humpback film and stills were re-recorded: the first spout is now at 14.05 s, and the stills show 18 s. Earlier trials that shortened the window, or tilted the whale with the slope, broke these bounds.

The proper fix is to let the body dynamics follow a swell-riding head target. The FFT waves also moved the second whale's first breath from 18.8 s to 31.45 s.


The FFT wind sea, whitecaps and multi-scale foam are now the default. `sea.html` declares its wave model in `<meta name="sea-waves" content="fft">`. `?waves=analytic` returns the previous sea and `?waves=fft` forces the new one. The GPU test pages declare no model and stay analytic.

**Whales against the rendered surface.** The first captures of the new default showed the surfacing humpback almost entirely under water. Whales planned their waterline against the analytic short waves, which the FFT replaces, so the planned height was about half a metre off the rendered surface. The CPU surface query now keeps only the analytic bands still rendered (longer than 25 m) and adds the FFT's first cascade. That cascade is summed on the CPU from its 792 strongest modes, 95% of its variance, through the same aperiodic tiling, so heights can be asked for at future times as the planner needs. The sum uses a recurrence for the mode phases and costs 6 µs per query. With every mode kept it matches the GPU cascade within 3.5 mm, which is the texture filtering between 0.54 m texels. The kept modes leave 3.6 cm RMS ([`tests/sea-fft-surface-gpu.html`](tests/sea-fft-surface-gpu.html)). The two finer cascades, waves under about 5 m, are not included. Warm start-up stays at 1.4–1.5 s. In phone emulation the scene settles at 1.33× with 16.7 ms frames.

**Films.** Both films, their posters and the social previews were re-rendered from the new default, with the same framing, frame rates and sizes as before. The recorder is [`artifacts/sea-films-20260923/record.mjs`](artifacts/sea-films-20260923/record.mjs). It steps the live page with `advanceTo(i/fps)` after a one-second lead-in, as the earlier captures did, and encodes each frame in the browser with WebCodecs H.264. A small writer ([`mp4.js`](artifacts/sea-films-20260923/mp4.js)) puts the movie box before the media data, so no ffmpeg is needed. It records at 1× device pixels, because frames under 651 CSS pixels on a side take the page's phone path, with lower resolution and foam detail; the 1200 × 630 previews are rendered at 1524 × 800 and scaled down. The humpback stills moved from 16.4 s to 25 s: in the current choreography nothing surfaces in frame at 16.4 s, and the first spout is at 24.15 s. Bitrates of 3.8 and 5.6 Mbps keep both films near 21 MB, under Cloudflare Pages' 25 MiB file limit. The earlier previews stay published for links shared before.

## Close-range foam structure · 23 September 2026

**The problem.** Up close, foam was a single-scale blob texture with outlines: flat grey shapes of one size, and whitecaps as solid white slabs. Real sea foam has several scales: Plateau borders form a lace of cells a few centimetres to decimetres wide, the cells are packed with millimetre-to-centimetre bubbles, and drained foam gathers into patches with holes. Fresh foam is a nearly solid blanket. As it drains, holes open and grow until only a lace remains, and then the lace breaks into fragments.

**Coverage from a quantile.** The foam amount is treated as the covered fraction, and the structure only decides where that area lies. Foam covers the points where a weighted sum of three cell fields exceeds a threshold. The fields come from hashed jittered-Voronoi lattices at 0.5 m (patches), 12 cm (lace) and 2.2 cm (bubble clusters). Each field is made uniform over area by its measured CDF. The threshold is the quantile of the weighted sum, from the Irwin–Hall form for three uniforms, with the upper tail taken by symmetry for single-precision accuracy. The covered fraction then equals the amount within 0.4% at every amount from 1% to 99% and every freshness ([`tests/sea-foam-structure.test.mjs`](tests/sea-foam-structure.test.mjs)). The three-stage life cycle falls out of the amount decreasing: holes, then lace, then fragments. Foam disappears by breaking up, not by opacity. Drained foam weights the lace most (0.44, 0.40, 0.16); fresh foam weights the bubble clusters more (0.40, 0.32, 0.28). Cells are stretched 1.7× along the wind, as shear and Langmuir circulation stretch real foam. The area-preserving stretch keeps the fields uniform.

**Rounded, polydisperse cells.** The first version used distance to the nearest cell wall, which gave polygonal holes and straight, zigzag strands; it looked angular. Each field is now the distance to the nearest seed over a soft minimum (width 0.1 cell) of the distances to the other seeds. Its iso-lines are rounded, so holes open round, and borders thicken towards the junctions as Plateau borders do. Each cell's distances are also scaled by exp(±0.3), a multiplicatively weighted diagram. Cell sizes then vary, and walls become circular arcs that bulge out of the smaller cells, as films do between bubbles at unequal pressure. Equal-sized cells had made partly drained foam read as snake skin. Weighted cells need the nearest 4×4 cells, which agree with a 7×7 search to 0.005, where a 3×3 search was off by up to 0.17 for 1% of points. Patches and lace are foam along their cell walls, but the bubble-cluster field is inverted, so clusters are the cells themselves. Thinning lace therefore breaks into chains of rounded clusters instead of thin strands. The ratio's area CDF is tabulated at 33 points from 10⁶ samples and interpolated to within 0.0025.

**Filtering.** Each scale is seen by the pixel as a box of field values, as wide as the field changes across the footprint. The box widens to the whole range once the cells shrink below a pixel, and scales below a pixel are not evaluated. Coverage is the share of the sum of boxes above the threshold, from the same three-uniform CDF. With every scale unresolved, it is exactly the amount, so distant foam, and default-mode foam at the usual ranges, is unchanged. There is no blend between separate near and far patterns, so nothing changes shape with distance.

**Shading and whitecaps.** Foam just above the threshold is thin and down to 0.62 as bright. Dense foam heaps into 7 cm clumps, and fresh foam into roller lumps of about 0.45 m. Their relief normal lights the tops and shades the creases. Resolved 8 mm bubbles keep their cap-shaped glints. A first attempt that widened the whitecap edge below the calibrated threshold visibly enlarged the whitecaps; whitecaps now come only from the record described in the next paragraph. The threshold is read from a table over five freshnesses and 33 amounts, spaced by the cube root of the amount because the tail goes as c^(1/3); it is within 0.03% cover of the exact quantile. Each cell takes its position and size from one hash, and the cell search runs in one pass without arrays. On its own, the structure costs 0.26 ms for every pixel of a 2828 × 1768 screen. In the close breaker view, frames take 16.7 ms against 16.2 ms with foam coverage set straight to the amount (medians of ten; minima 14.1 and 14.3 ms). Some earlier interleaved runs had put this at 4 ms; single frames of either build range from 14 to 29 ms, so those runs were noise. See the [comparison](artifacts/sea-foam-structure-20260923/index.html).

**Whitecap life cycle.** A frame sequence showed whitecaps appearing full-size in one frame and nearly gone about 0.45 s later. The bright blob was a per-pixel test on the instantaneous crest, so it switched off with the crest. The record meant to hold the leftover foam had 0.75 m texels and a smoothed wave field, so small whitecaps barely registered in it. Both records now share one texture array: 768 m at 0.75 m, and the 128 m around the camera at 12.5 cm, which blends into the coarse record between 46 and 59 m. Each is thresholded at its own footprint (0.75 m and 0.25 m), measured over the full 768 m, because a finer footprint resolves steeper slopes (quantiles 0.272 and 0.300). Breaking raises foam with a 0.35 s rise time, so a patch grows while its crest keeps breaking. The foam then stays where it formed and decays over 4 s, while the bright active phase fades over 0.8 s. The record value is the whitecap's covered fraction, and freshness follows the active phase and then the foam level, so a patch ages through lace. The per-pixel test is gone, which made the close view faster (14.7 ms median, against 16.7 ms).

The earlier cover figures (0.46% active, 1.5% total) counted only texels above one half. Measured as the white fraction the shader actually draws, cover was about 6.5%, because the record went through a saturating curve built for particle deposits, which drew faint old foam as a lot of white. With the direct mapping, 16% of W breaking at any instant gives a mean visible cover of 1.86% far and 1.91% near (four times from 40 to 85 s), against W = 1.84%. The active phase is about 28% of that. Field measurements at 12 m/s mostly fall between about 1% and 3%, so the sea looks noticeably less white than before.

## Start-up time and aperiodic FFT tiling · 22 September 2026

**Start-up.** On a first visit, or after any shader edit, the page took about 14 s to show its first frame on an M5 Max; the pipeline compile of the spray-physics shader alone took 10.5 s. That shader nests three flight call sites, two transport calls per flight and two airflow evaluations per transport, which the compiler inlined into about a hundred copies of the 64-band ocean, each unrolled. One call site each for flight and transport inside runtime loops, and a runtime bound on the wave loop in the two physics shaders only, bring that compile to 0.83 s with the same equations. Airborne parcels match the previous build to a median of 21 µm (maximum 4 mm) after 5.5 s. The same change made the physics far cheaper to run: a heavy-spray frame at 2016 × 1260 fell from 21.1 ms to 15.6 ms on an idle GPU.

Two stalls followed. A spray-pool capacity check read the entire state back three seconds into the four-second pre-roll, although nothing in a fresh pool can be old enough to need more slots; the first check now follows the pre-roll. Routine checks copy the state on the GPU and read it after a fence, with a synchronous read only when a catch-up covers more than half a second of ticks. Whale motion preparation yielded with nested `setTimeout(0)`, which browsers clamp to 4 ms per slice. It now runs in 12 ms slices with a message-port yield. The first visible frame arrives after 2.8 s cold and 1.2 s warm, down from 14.2 s and 2.3–2.5 s. In the FFT preview it takes 3.5 s and 1.4 s.

**Start-up progress.** A progress bar under the summary reports measured work in four stages: bytes received, shaders compiled, spray history replayed and the first frame completed on the GPU. It shows the stages' weighted sum and never moves backwards. The spray physics reads no downloaded asset, so its four-second history replay now starts right after shader compilation. It runs on the GPU in chunks of 24 ticks, with two always queued, while assets download and the whales are prepared on the CPU. On a throttled 25 Mbps first visit, the ocean appears 1.3 s after the downloads finish, down from 3.1 s. Locally it appears after 0.9 s warm and 2.5 s cold. A redundant preset reset at the end of start-up, which discarded the replay and recomputed the FFT spectrum, was removed.

**Aperiodic FFT tiling.** Each FFT cascade is now read through a stochastic hexagonal tiling, as the foam material already was. Every lattice cell samples its cascade at its own random translation. Rotation is excluded because it would turn waves away from the wind. The blend weights have a unit sum of squares, so the Gaussian field keeps its spectrum and variance. LEAN roughness combines tiles with squared weights. Heights one full period (137.3 m) apart correlate at 0.94 with plain wrapped sampling and at −0.01 with the tiling; the variance is unchanged ([`tests/sea-fft-tiling-gpu.html`](tests/sea-fft-tiling-gpu.html)). The diagnostic 3D spray-detail volume uses the same scheme with eight lattice corners. The other textures were already tiled this way, sampled over bounded domains, or not tiled at all.

To pay for the extra taps, the FFT mesh derives its motion vectors from one velocity evaluation instead of two extra surface evaluations. The mesh shading normal now carries only the analytic waves; each pixel adds the full FFT slope and stretch at its own scale, and the whitecap test reuses those samples. Pixel shading therefore takes 18 FFT fetches instead of 39, and physics samples only the two larger cascades; the finest has about 1.5 cm amplitude, against spray parcels at least 7 cm wide. A heavy-spray FFT frame costs 21.7 ms, about the original renderer's cost.

## Frame cost, resolution calibration and an FFT wind sea · 22 September 2026

**Where the time goes.** Paired skip-one-pass runs in a heavy-spray moment (2016 × 1260, Apple M5 Max, headless Chrome) put spray physics at about 8.7 ms of a 21 ms frame. Foam particle updates took 3.9 ms, motion blur about 4 ms, ocean shading 2.7 ms and the surface mesh 2.2 ms; spray rendering itself was 1.8 ms. About 60% of a heavy frame therefore does not scale with resolution. The benchmark advances two 1/60 s physics ticks per frame; a device running at 30 fps pays the same. Per-pass GPU timer queries on ANGLE/Metal summed to four to five times the frame and were not used.

**Surface foam updates in quarters.** The 262,144 surface-foam particles now integrate in four round-robin chunks, each over its own elapsed time. Material coordinates already carry the orbital motion, so a chunk moves about 2 cm between its updates. On an identical timeline, foam mass, covered area and peak density agree with the single-chunk update within 1–3%.

**Resolution calibration.** The first 0.7 s after playback begins is no longer measured: it contains shader compilation, the four-second spray pre-roll and whale set-up, and previously pushed this machine from 1440 × 900 down to 1224 × 765. During the remaining 1.2 s, one elapsed-time query per frame measures GPU work, and a single upward step is allowed when the median frame would fit 75% of the 45 fps budget at the larger size, assuming all cost scales with pixels. The resolution then locks as before; the mobile 1.0× floor is unchanged. In phone emulation the scene now settles at 1.445× instead of 1.25×.

**FFT wind sea (`?waves=fft`, opt-in).** With one direction per frequency, the 64 analytic bands form long, glassy crests; the visible chop is the micro-detail normal overlay, which does not move the geometry and produced the evenly spaced ripple rings. In the preview, waves of at least 25 m stay analytic. That keeps the swell, the dominant wind waves, the camera motion and the whales' long-wave forcing. Shorter waves come from three 256² FFT cascades (137.3, 31.986 and 7.474 m) with the same normalized JONSWAP energy and Donelan–Banner directional spreading (as parameterized in Horvath, *Empirical directional wave spectra for computer graphics*, DigiPro 2015). Each cascade owns a separate wavenumber annulus, and the length ratios have no near-common period below a few kilometres. The mesh, spray launch and contact, surface foam and pixel shading sample the same textures, with LEAN moments for filtered roughness. The FFT carries Hs 0.658 m against 0.662 m for the 28 bands it replaces. GPU output matches a direct sum over every mode within half-float storage precision. The FFT code is compiled only with the flag.

**Whitecaps across the sea (FFT preview).** A crest breaks where 1−J of waves longer than about a metre, analytic and FFT combined, exceeds a quantile measured on the wave field. The target is the empirical cover of Monahan and O'Muircheartaigh (*J. Phys. Oceanogr.*, 1980), W = 3.84×10⁻⁶ U₁₀^3.41, 1.84% at 12 m/s. A material-space record decays over four seconds and leaves foam behind a travelling crest. The cover figures given here at first (0.46% active and 1.5–1.7% total) undercounted the white actually drawn; see *Whitecap life cycle* in the 23 September section. Both feed the existing near-binary foam material. See the [matched comparison](artifacts/sea-fft-preview-20260922/index.html).

**Limits.** The whales' waterline was coupled to the FFT surface on 23 September; see above. Spray is still emitted only by the three authored breakers. Resolved slopes total about half of clean-surface Cox–Munk. The shortfall is added only for pixels wider than 25 cm, because adding it nearer spread the near glints into a grey sheen. Close-range foam is covered in the 23 September section.

## Reflected skylight · 14 September 2026

The sky/upwelling blend now evaluates the existing water Fresnel function at the resolved surface's viewing angle. The removed effective-cosine adjustment suppressed grazing reflections, leaving the away-from-sun water too uniformly dark. The updated view restores a broad silvery-blue skylight sheen while retaining dark foreground troughs.

Surface normals, the existing rough-sky lookup and reflected-direction approximation, direct sun reflections, glare, exposure, and wave/spray transport retain their previous equations. Changing the reflection fraction also changes the complementary upwelling and entrained-bubble blend. This remains an approximate filtered environment model, not a new exact rough-dielectric integration.

The three reviewed views match the prior reflection-weight diagnostic pixel for pixel. The [matched before/after comparison](artifacts/sea-skylight-fix-20260914/index.html) also includes the sun-reflection view; [validation and scope](artifacts/sea-skylight-fix-20260914/report.md) record the applied source and checks.

## Spray visibility and performance · 14 September 2026

Spray now rejects its conservative parent-and-child support against the four side planes of the camera view before projecting a draw rectangle. A one-pixel guard preserves the boundary. Spray intersecting the camera plane still uses a full-screen rectangle when it can contribute to the view; off-screen volumes no longer trigger that expensive fallback or leave an oversized rectangle across the image.

This addresses the sunward slowdown after old mist accumulates. It changes visibility work, preserving the existing Gaussian kernels, transport, optical integration, shadows, glare, and skylight. The [matched mature-scene comparison and validation](artifacts/sea-spray-culling-fix-20260914/report.md) record image agreement and local timing evidence. The GPU visibility regression is included in the [spray filtering checks](tests/sea-spray-filter-gpu.html).

## Current spray model · 13 September 2026

The live spray uses persistent, size-resolved Gaussian water distributions. [The shared model parameters](src/sea-volume-physics.js) define droplet radii, water-mass fractions, emission quadrature, mixing and retention capacity. Wave and scene settings are defined in [sea-state.js](src/sea-state.js).

Each population first follows a short lifted-water phase, represented by an equivalent ligament radius. A capillary-time breakup model, accelerated by aerodynamic stress, hands its existing position, velocity and water mass to free droplets. The four droplet radii are 20, 60, 180 and 500 micrometres. The smaller groups respond more quickly to the shared airflow, settle more slowly, and follow more of its unresolved turbulent mixing. Most emitted water mass belongs to the larger groups.

Droplet motion retains nonlinear spherical-drop drag and gravity with the air-buoyancy correction. The airflow matches the moving water at the interface, blends toward the stated wind with height, and includes localized crest wakes. All sizes sample this same air field; size changes the response to that field. The spherical-drop drag model follows the Clift–Gauvin formulation summarized in [Veron's Ocean Spray review](https://cpb-us-w2.wpmucdn.com/sites.udel.edu/dist/b/10612/files/2020/12/Veron-2015-spray-annurev.pdf).

Contact with the moving water transfers a fraction of a distribution into the deposition ledger and existing foam field. The surviving mass, mean and covariance are updated using a local Gaussian moment approximation. Camera movement and frame delivery do not reset this state. Every fixed physics tick crosses the same Float32 storage boundary. Retention capacity expands before older droplets block new emissions, preserving physical state, emission seed identity and cumulative ledgers; an explicit seek restores the base allocation. Still-image seeks initialize a four-second emission history; continuous animation retains the older mist that remains in the domain.

The renderer and its shadows use positive Gaussian distributions. Its constituent scale is bounded by droplet size, so expanding a plume cannot turn its fine mist into growing coarse clumps. Pixel filtering broadens unresolved constituents before merging their spacing into an ensemble average, preserving optical mass and the first two moments. The coarse shadow atlas retains its stronger ensemble filter; local self-shadow uses the finer view mixture after subtracting the parcel's filtered atlas contribution. The deposited-water ledger persists independently of bubble visibility.

Breakup, sub-grid mixing and partial contact are reduced models. The implementation does not resolve individual ligaments, droplet deformation, evaporation or the full turbulent flow. Reconstructing the surviving distribution as a Gaussian approximates its surface-truncated shape; deposited foam uses an averaged location and time. These checks establish numerical and rendering behavior within that model, not measured ocean-spray concentration.

See the [matched comparison and motion clip](artifacts/sea-size-resolved/index.html), [GPU transport and optical checks](artifacts/sea-size-resolved/gpu-tests.json), and [capture evidence](artifacts/sea-size-resolved/verification.json).

## Reference appearance refinement · 13 September 2026

The [updated comparison](artifacts/sea-reference-gap-20260913/index.html) keeps the two generated targets frozen. The sea state, wave shapes, exposure, camera, shutter, emission, drag, and settling remain the same. At 0, 2.5, and 6 seconds, the spray-state buffers match the baseline exactly.

Surface foam now uses the existing material's patch and microbubble channels as well as its cellular film. This produces a mixture of denser patches, thinner strands, and larger water gaps. Nearby resolved films have sharper covered edges; distant pixels return to mip-filtered area coverage. These are changes to reconstructed bubble appearance, not changes to the stored water or foam particles.

The current view filter adds the pixel footprint to each constituent Gaussian's covariance separately, while retaining its world-space mean. This supersedes the earlier scalar approximation, which preserved total moments but stretched thin parcels into visible streaks. The coarser lighting atlas retains its ensemble averaging.

The stochastic water slopes have a 1.6 gain, with their unresolved second moments scaled consistently. Solar reflection now uses Beckmann masking and the microfacet reflection denominator, removing the extra incoming-angle attenuation. The formulation follows [PBRT's microfacet model](https://www.pbr-book.org/3ed-2018/Reflection_Models/Microfacet_Models); separating resolved slopes from unresolved reflection variance also follows the approach described by [Bruneton, Neyret, and Holzschuch](https://www-evasion.inrialpes.fr/Membres/Fabrice.Neyret/images/fluids-nuages/waves/Jonathan/articlesCG/real-time-realistic-ocean-lighting-using-seamless-transitions-from-geometry-to-BRDF-10.pdf).

The retained numerical checks include optical mass across all four spray radii, depth clipping, analytic shadows, slope-filter limits, reflection reciprocity, an oblique mirror case, and sampled hemispherical reflection integrals. The generated references remain appearance guides: they contain different small-wave geometry, lighting, and spray detail, and do not establish physical accuracy.

## Reflected-sun bloom · 13 September 2026

The current bloom uses stronger redistribution around bright reflections. Its exposed-luminance threshold is 0.55 with a 0.25 soft knee; the three blur scales have weights of 0.68, 0.20 and 0.04. [sea-camera.js](src/sea-camera.js) remains their source of truth. The normalized filters return selected light to the image, so stronger glare also softens and reduces the brightest cores.

The extraction pass now selects each source pixel's bright radiance before averaging it into the smaller bloom image. Area-weighted footprints handle source dimensions that are not multiples of four. Previously, bilinear averaging preceded this nonlinear selection; the thin-glint regression demonstrates a pixel position where that order discarded all selected light. All 32 tested positions across even and odd source dimensions now recover the expected light.

The [full-frame comparison](artifacts/sea-visible-bloom-20260913/index.html) switches original bloom, bloom off and the new treatment in the same image position. This replaces the earlier subtle refinement, which the user found visually indistinguishable from the other versions. At the old comparison's display size, that earlier refinement differed from bloom off by only about 0.02 on a 0–255 channel scale in the sun-glitter view. Its numerical changes did not establish a useful visual improvement.

All eight still views and 90 moving frames pass the selected detail limits, and all four scene-linear HDR buffers and their spray states match exactly. The current local timing is 22.2 ms versus 21.65 ms for original bloom at 2400 × 1600, about 2.5% higher. These are finite rendering checks and model visual inspection; the settings are not a measured lens profile. See [the evidence and limits](artifacts/sea-visible-bloom-20260913/report.md).

## Rendering cost · 13 September 2026

The renderer caches each spray parcel's external optical depth and world-space gradient after the shadow atlas updates. Fine view extinction and local self-shadow remain analytic. This replaces repeated external-shadow sampling inside every spray fragment with a first-order lighting approximation.

Motion blur shades one centered scene sample and reconstructs the exposure with the existing nine-tap water/spray motion filter. Reconstruction accumulates directly into the linear-HDR exposure buffer before the lens effects. Removing the intermediate RGBA16F motion image saves 8 bytes per output pixel: 29.3 MiB at 2400 × 1600. Water geometry, spray transport, emission, shadows, bloom, focus and tone mapping retain their existing equations.

The current [motion-blur experiments](artifacts/sea-motion-speed-20260913/report.md) compare against the preceding two-scene renderer. Normal-shutter paired frame time fell from about 26.9 ms to 22.4 ms on this desktop browser. All eleven matched stills and both 90-frame motion sequences pass the frozen image limits; spray-state buffers match at every compared time. The retained GPU checks cover reconstruction, depth rejection, exposure weighting and the existing lens effects.

Shared water derivatives and adaptive tap counts produced no consistent speed gain. Separate water/spray reconstruction also passed the image checks, but a simpler one-scene control achieved comparable speed without an additional layer buffer. The retained filter still uses an opacity-weighted motion field, so overlapping transparent layers with different motion remain an approximation. One shaded sample also approximates changing illumination and visibility over the shutter; the finite comparisons are not a general perceptual equivalence claim.

The earlier [render-cost pass](artifacts/sea-speed-experiments-20260913/report.md) reduced frame time from 42–43 ms to about 27 ms using cached parcel lighting and two scene samples. Its rejected one-scene candidate was tested against an older four-scene baseline. The new results use the current two-scene baseline and do not requalify that older comparison.

## Fine mist and distant streak correction · 13 September 2026

The view renderer now convolves each spray constituent with a Gaussian pixel footprint independently. The eight constituent means remain fixed in world space. A thin distribution can remain resolved along its long projected axis, and its culling bounds include every filtered child. This prevents a long source-sampling cell from turning into an enlarged visible streak. Optical extinction and local self-shadow use the same positive mixture.

The largest droplet reconstruction scale is now 6 mm rather than 40 mm; physical drop radii, velocities, covariance, and water-mass ledgers are unchanged. Fully fragmented 20 and 60 micrometre populations render as continuous mist. Larger populations retain a radius-dependent discrete component, with the remainder contributing to the same continuous distribution. These are ensemble appearance choices, not a simulation of every visible droplet.

The [before/after comparison](artifacts/sea-mist-filter-20260913/index.html) reproduces the reported pattern after twenty seconds and includes near/distant views through eighty seconds. All six saved simulation states match exactly. The actual GPU filter passes 36 anisotropy, distance, rotation, and radius cases; optical mass and clipping checks also pass. Paired rendering cost remains approximately 27 ms at 2400 × 1600. See [the evidence and limits](artifacts/sea-mist-filter-20260913/report.md).

<details>
<summary>Earlier implementation notes</summary>

The following notes document earlier snapshots and their validation. Parameter counts, airflow models and deposition methods below have been superseded by later revisions; the current spray model is described above.

# Original scene and particle implementation

The viewer at `/sea.html` now contains only Atlantic swell. The weather switcher and wind/sun controls were removed. Pause, camera movement, camera effects and image quality remain. Legacy condition URLs open Atlantic. Additional conditions live only in numerical test fixtures.

The fixed scene has a 10 m/s wind from 70°, an incoming 2.4 m / 11 s swell from 25°, and a wind sea of approximately 1.81 m / 6.73 s. The combined spectral significant height is 3.01 m. `src/sea-state.js` is the source of truth. [Current whitewater validation](artifacts/sea-surface-particles/validation.md) and [trajectory illustration](artifacts/sea-atlantic-physics/trajectories.png) record the motion checks; the trajectory illustration predates surface-foam conversion. The earlier [image-edited reference](artifacts/sea-airborne/index.html) remains a frozen appearance comparison.

## Surface and launch

The water uses 48 directional wind-wave components and 16 incoming-swell components with independent energy and heading. Every component obeys deep-water dispersion, omega² = gk. Wind-sea energy and peak period use the existing Pierson–Moskowitz energy and normalized JONSWAP shape with an assumed degree of development. This is a sustained-condition illustration, not a fetch or weather solver.

A camera-adaptive mesh evaluates displaced waves and three finite authored breaking packets. Mesh filtering, residual normal detail and unresolved slope variance share the spectrum. Distant normals transition to the pixel-filtered spectrum; the mean-plane extension uses the same water lighting beyond the mesh. Geometry derivatives, crest foam and emission share the breaker definition.

The spray launch now inherits the local water material velocity: the time derivative of displacement at fixed surface coordinates. It no longer inherits the speed at which a crest pattern travels. Spectral velocity is analytic; the authored breaker correction uses a 2 ms central time difference. This distinction follows the orbital water motion described in [MIT's wave-flow notes](https://ocw.mit.edu/courses/12-090-introduction-to-fluid-motions-sediment-transport-and-current-generated-sedimentary-structures-fall-2006/a2feced79e03dfae8b58fdf193b85f5a_ch6.pdf).

A bounded upward/forward breaking impulse is added to that water velocity. Its specific kinetic energy is less than one quarter of g times the local carrier height. The impulse distribution is an authored approximation to unresolved breakup, not a measured ejection distribution. The fixed 2.5 cm launch footprint is independent of camera movement.

## Airborne trajectories

122,880 Gaussian optical kernels retain world position, velocity, physical droplet radius, dimensions and emission strength at birth in two GPU buffers. Wave motion does not update airborne position. Gravity and drag act on the stored state. One in every 32 airborne slots is eligible to become surface foam on contact; the others retire. Converted slots retain surface state for their foam lifetime before becoming available for new spray. This representative sampling bounds the deposition cost; it is not a mass-conserving conversion of every physical droplet.

Physical droplet radii range from 20 to 800 micrometres. Radius is distinct from the width of an optical kernel, which can represent a group of droplets. Drag now depends on radius and air-relative speed, rather than a fixed coefficient assigned to a visual class. The model uses seawater density 1025 kg/m³, air density 1.225 kg/m³ and air viscosity 1.81e-5 Pa·s. Gravity includes the small air-buoyancy correction.

The spherical-drop drag coefficient uses the Clift–Gauvin correction, with Reynolds number based on droplet diameter and slip speed. This follows equations 7–8 in [Veron's Ocean Spray review](https://cpb-us-w2.wpmucdn.com/sites.udel.edu/dist/b/10612/files/2020/12/Veron-2015-spray-annurev.pdf). Fine mist quickly approaches the air velocity and settles slowly; larger droplets retain more momentum and have a larger terminal fall speed. Spherical drag does not model droplet deformation, breakup, evaporation or resolved turbulent gusts. The near-surface air velocity remains a uniform 70% of the stated 10 m wind, an environmental approximation.

Slip-dependent drag is integrated with an exponential midpoint method at substeps of at most 1/240 s. Tests compare it to an independent force formulation and fine-step RK4. GPU state checks separately verify launch velocity, flight, independence from source changes, pause and water contact. Contact height is found by four Newton iterations that invert the surface's horizontal displacement. Contact is checked once per rendered simulation step; the renderer caps ordinary frame steps at 50 ms. Initial snapshots reconstruct a birth history and test current contact; they do not resolve every historical impact.

## Surface-particle whitewater

Foam is now a secondary GPU particle simulation, replacing the analytic crest-age trails. The fixed pool contains 122,880 airborne slots and 65,536 direct surface-foam slots. Each surface particle stores water material coordinates, relative drift velocity, represented foam amount, size, age and lifetime. Its represented amount does not fade with age. Deterministic, independently phased lifetimes range from 22 to 70 seconds; retirement removes a particle and permits a new emission. This is an authored breakup/lifetime model, not measured bubble kinetics.

Material coordinates follow the analytic water's orbital displacement automatically. Surface drift and particle interactions move those coordinates; the renderer samples the resulting foam on the same water surface, so no independent foam plane can float above it. Landing spray is mapped back to material coordinates through the surface inversion. Its bounded relative velocity supplies initial surface slip, and the deposited amount is weighted by the retained emission strength. Direct foam emission uses the existing breaking-crest source field.

Particles deposit normalized Gaussian kernels into a 1024 × 1024 material-space density grid spanning 384 metres. The grid uses RGBA32F when float blending and linear filtering are available, otherwise RGBA16F. The finite three-sigma support omits about 1.1% of the continuous Gaussian mass; grid quadrature and precision add error. This grid stores represented foam amount, not kilograms of seawater. The renderer adjusts density for local surface-area deformation.

Fine-scale density pressure spreads crowded groups; a broader density gradient supplies cohesion to sparse groups. This is a grid-assisted approximation to local particle interactions, with damping and a bounded relative speed. It does not solve incompressible pressure or transfer water momentum as FLIP does, and it does not feed forces back into the ocean. Forces and drift are evaluated in material coordinates; a full surface-metric SPH force solve is not performed. The design follows the secondary-whitewater separation in [SideFX's whitewater workflow](https://www.sidefx.com/docs/houdini/fluid/sopwhitewater.html) and draws on surface-constrained particle foam in [Guided Bubbles and Wet Foam](https://alexey.stomakhin.com/research/whitewater.html); it is not an implementation of that paper's full SPH solver.

Opaque coverage is reconstructed from particle density and stable material detail. A fixed threshold defines patch boundaries, with fractional values only at filtered edges. There is no exponential trail-strength decay, age-dependent threshold, compensating coverage exponent or whole-patch opacity fade. Particle motion, interactions, emission and retirement change the patches.

Initial or explicitly sought snapshots seed a plausible birth history using launch-time emission and material drift. They do not replay past particle interactions. Once animation runs, subsequent state is integrated and persists across camera movement and view changes. The finite density domain covers the authored nearby breakers; distant whitewater and resolved underwater bubbles are not simulated. A breaking crest can lift existing foam into spray. Each surface particle integrates an emission hazard instead of drawing a new death/launch chance on every frame. Lifted foam carries its represented amount and remaining lifetime through flight; on landing those values return to the surface, and expiration during flight still removes it. The existing crest emitter also supplies ordinary airborne spray.

## Appearance and limits

Gaussian airborne-spray extinction is integrated analytically to the first visible surface. Kernel expansion and pixel filtering preserve represented mass before the existing finite spray-visibility fade. Source strength, represented amount, emission counts and lifetimes remain approximations; this is a one-way secondary whitewater simulation, not a conserved multi-phase fluid solve.

Water uses Fresnel reflection, unresolved slope statistics, sky reflections, approximate subsurface upwelling and crest transmission. Cached spherical and camera-aligned skies supply illumination. The photographic pass applies restrained bloom, depth-aware focus, tone mapping and grain. Camera motion comes from the same long waves. Cloud integration occurs only when a sky cache rebuilds.

The numerical checks establish implementation accuracy within this model. They do not certify measured spray concentration or photorealism. The separate cloud study at `/` and previous reference artifacts remain unchanged.
# Precomputed materials and surface-oriented spray release

Fine water shading now uses a precomputed stochastic slope field with anisotropic filtering and second-moment roughness. The unmeshed coherent short-wave overlay is removed from the normal and its variance contributes to roughness. This supersedes the insufficient gradient-noise-only correction; see the [second striation correction](artifacts/sea-striation-second/report.md).

Surface foam now retires through gradual footprint loss near the end of each parcel's lifetime, replacing the earlier whole-parcel cutoff. Represented mass and footprint area decrease together while surviving foam stays opaque. See the [retirement validation](artifacts/sea-foam-retirement/report.md).

The sea now uses four randomized foam exemplars, a filtered 64³ spray-detail volume, and precomputed irregular emission candidates. Spray inherits local water velocity and receives a bounded impulse in the actual surface frame before wind drag and gravity act. Shading detail uses that same ambient wind, without an artificial upward drift. See the [matched captures, validation and limits](artifacts/sea-assets-trial/report.md). Numerical assets can be regenerated with `python3 scripts/precompute-sea-assets.py --publish`; the resulting manifest records their encoding and hashes.

## Cellular foam coverage

The latest [appearance pass](artifacts/sea-cellular-foam/report.md) separates particle-density occupancy from filtered cellular-film coverage. Dense particles no longer fill all material pores. This supersedes earlier descriptions of universally opaque foam interiors: fractional coverage can also represent sparse film within a patch, while particle retirement and stored amounts are unchanged. The current surface pool is 262,144 particles with base sigma 0.07 m and a 4096-square density grid. Fine spray has reduced optical birth strength, faster optical-parcel expansion, and smaller fine-droplet radii; the existing wind drag and gravity integrate their motion. Generated references remain appearance guides, not physical validation.

Fine-mist motion now includes an authored transient upward/crosswind breaking wake, superseding the earlier uniform-horizontal-air approximation for small droplets. Larger drops retain ambient wind. See the [plume comparison and limits](artifacts/sea-spray-plume/report.md).

The current default is a [choppier 12 m/s sea](artifacts/sea-choppier/report.md): wave age 0.9, wind peak enhancement 1.3, and horizontal choppiness 1.05. The 2.4 m / 11 s swell remains unchanged. Earlier 10 m/s captures are historical comparisons.

The [fivefold spray update](artifacts/sea-spray-5x/report.md) expands airborne slots to 614,400 while retaining particle size and per-particle optics. Surface foam remains 262,144 slots. One in 160 ordinary airborne slots is eligible for surface conversion, replacing the previous one-in-32 sampling rate.

</details>


## Humpbacks

The three animals use the corrected textured mesh and 24-control armature imported from the Humpback project. The complete mesh, UVs, weights and textures are retained. See [model details and credits](/whale-credits.html) and [provenance](/src/sea-assets/whales/thread-humpback/provenance.json).

The head leads a continuous swimming path in world coordinates. Its forward axis is the tangent of that path, and the body follows behind the snout anchor. Turning the body or adding a separate neck bob cannot move the head sideways. This is an explicit locomotion constraint: the force model supplies course, speed and stroke cues, while steering and propulsion reactions maintain forward swimming. It is not an unconstrained six-degree-of-freedom simulation.

Depth is planned before orientation. A lower-only quintic spline uses the moving waterline at the snout and jaw, including the pitch produced by the preceding planning pass. The final path supplies height, vertical speed and acceleration together. The long descent and distributed back flexion keep the trailing body and flukes immersed instead of rotating a rigid tail upward around the nose. The deeper whale holds its cruising depth relative to mean sea level. The rear animals have independent 61- and 65-second surfacing cycles, and may breathe during the shallow beginning of a dive when both launch points are clear. These timings and control limits are modeling choices.

The model includes gravity, depth-dependent buoyancy, partial loss of displaced volume while surfacing, added inertia, wave forcing, parasite and crossflow drag, lift-induced drag, propulsion and steering torques. Wave flow uses the ocean spectrum averaged over the body, with depth attenuation and spatial transport in its acceleration. The inertia-force formulation follows the moving-body form of [Morison's equation](https://www.orcina.com/webhelp/OrcaFlex/Content/html/Morison%27sequation.htm).

The tissue-density estimate of 1031.6 kg/m³, diving gas volume of 27.7 mL/kg, and references for parasite drag and flipper geometry come from [Narazaki et al. (2018)](https://journals.plos.org/plosone/article?id=10.1371/journal.pone.0200287). They describe population estimates and modeling inputs, not measurements of these rendered individuals. Gas volume compresses with depth. Added-mass ratio, steering gains, the ellipsoid approximation and the smooth extension of flow across the mean waterline are explicit modeling assumptions.

The underlying force trajectory integrates at 30 Hz with fourth-order steps and quintic position/velocity/acceleration interpolation. The clearance spline is evaluated before deriving the rendered orientation, rather than clamping a rendered head or rotating it independently. Its extra steering and clearance reactions are ideal constraints; the rendered depth should not be interpreted as a freely integrated buoyant body. Bounded caches preserve deterministic time seeking. Motion, clearance dependencies and exact breath births are prepared ahead in small CPU slices; each emitted parcel still requires both blowhole launch points to be above the actual waterline. The initial ten-second underwater approach, distant surfacing pair and later larger underwater pass remain.

The animals render into a geometry buffer before ocean shading. Surfaced anatomy is depth-tested against the wave mesh; submerged anatomy uses wavelength-dependent attenuation and approximate refraction beneath the Fresnel reflection. This is a one-way, reduced physical model: the whales do not displace the ocean, lung volume changes during exhalation are not simulated, and the coefficients have not been fitted to individual animal tracks. The numerical checks establish consistency within the model, not validated biomechanics.

## Reference-based humpback blows

NOAA’s Stellwagen Bank spouting photograph and NMFS Circular 396 (Figure 33, printed page 41) show bushy plumes, narrow attachment to the blowhole, broad or wind-distorted crowns, and translucent edges. The Hawaiian Islands behavior sheet also shows the subtle appearance at distance. These photographs are shape references, not calibrated timing or velocity measurements.

The updated exhale uses deterministic per-breath 3D parcels: 224 mist parcels, 96 stretched filaments and 128 droplets per whale. Each newborn parcel has a checked position on the moving blowhole trajectory at its exact birth time. Parcel optical area is normalized by its changing volume, so expansion dilutes the cloud rather than adding opacity. A small finite exhalation jet entrains air and is carried downwind; coarse droplets fall under gravity. Eight-corner projected bounds limit each draw, while an analytic Gaussian ray integral clips the plume at the ocean/whale depth. The same density controls optical self-shadow. Individual timings, velocities and strengths remain artistic parameters.

References: https://stellwagen.noaa.gov/visit/whalewatching/whale-behaviors.html ; https://spo.nmfs.noaa.gov/sites/default/files/legacy-pdfs/CIRC396.pdf ; https://hawaiihumpbackwhale.noaa.gov/documents/pdfs_info_facts/behavior_sheet.pdf .
