Release notes
Every v1.x update is free for existing buyers — re-download the zip from your receipt.
v1.3 — 2026-08-23
Free v1.x update: "Coast & contact". The water now reacts to anything that touches it, knows about beaches, rivers and lamps, carries your own boats, runs its FFT as compute on WebGPU and holds its frame rate on its own.
Contact
- Anything-touches-water API (
src/Interaction.js,water.interaction) — register any Object3D and it leaves a wake footprint when it moves, ripples when it bobs and a splash ring + spray burst when it lands. Bodies attached throughBuoyancyget it automatically. - Wake generators —
WakeField.addGenerator(obj, { radius, depth, offset, strength }): any number of contact points per hull (bow V + prop wash), per object.Spray.burst()for one-shot splashes; the demo's crates (Fkey) show the whole chain, and they drift on the river. - Drop-in boats —
attachHull(gltf.scene, opts)(src/Hull.js) measures the hull, seats the keel, gives it a buoyancy body (drivable or anchored), a hull-interior water mask, bow + stern wake generators and bow spray in one call. The harbour's yacht and trawler (Kenney CC0) run through it; the trawler patrols.
Coast
- Shallow water (
src/ShallowWater.js) — a depth field (baked from a function or your terrain meshes) fades the deep-water cascades out as the bed rises (tanh(kh) per cascade), and an analytical shore train shoals by Green's law, sharpens, breaks at H/h = 0.78 and spills a foam lip, bore and swash; a wet-sand run-up band follows. The CPU height mirror applies the same attenuation, so hulls ground and buoyancy agrees. New Beach scene and sea mode; Lake and River use the field for their banks. Debug views 19–21. - Flow maps (
src/FlowMap.js) — a world-locked current field advects the fine ripple normals and foam (two-phase, stretch-free) and drifts floaters (buoyancy.setFlow). The River scene runs a channel profile. - Multiple water bodies — several
OceanFFTs per scene, each at its own height with its own spectrum, colours and bed (reflector: falsefor secondary bodies;setReflectionLayerskeeps them out of the primary mirror).examples/multi-body.html.
Light
- Occluded caustics (
src/SunOcclusion.js) — a light-space depth map of registered occluders; seabed caustics, prop caustics and the underwater light shafts go dark under hulls, the dock and the sea stacks (2×2 PCF). - Lights on the water — up to six point/spot lights (three.js lights or plain descriptors) plus a moon glint in the surface shader; the harbour's buoy lamps and dock posts glint at night; Hurricane flashes lightning.
- Underwater round 3 — sun in-scatter fog (the volume glows toward the sun), prop-wash bubbles behind a driven hull, boid fish schools that part around the boat, light shafts faded by the fog's transmittance (no more hard band at the horizon), the dome continuous with the fog at the horizon.
envMapIntensityworks per material again on WebGPU (createCausticProjector({ envMap })/bindEnvMap): the beach sand no longer mirrors the sky.
Performance
- WebGPU compute FFT (
src/GPUFFTCompute.js) — three dispatches per cascade (row FFT in workgroup memory with the spectrum evolution fused in, column FFT, pack into a storage-texture atlas) instead of 4·log₂N + 4 render passes. Identical output to float precision; WebGL2 keeps the fragment chain. Demo at 1400×900 High: 22.6 → 16.1 ms/frame; JS+submit 8.9 → 4.4 ms.fftMode: auto | compute | fragment. - Dynamic resolution (
src/DynamicResolution.js) — holds the frame rate by moving the pixel ratio between 50 % and the tier's ceiling (60 fps by default, "display" for the refresh rate).water.performance, the demo's Performance folder,<water-canvas dynamic-resolution>. - The ocean draws first among transparents (its origin sits at the camera, so the distance sort drew it last, over every farther transparent); the WebGL2 texture-unit limit is honoured (the flow map guard never fired there, so the water failed to compile on WebGL since it landed); the sun-occlusion map clears with alpha 1 (the WebGL backend premultiplies the clear colour).
Integration
- React Three Fiber wrapper (
r3f/) —<Tidewater>(sky, sun, environment, fog, dynamic resolution) and<Ocean>components plususeTidewater(), plaincreateElementwith declarations;examples/r3f.htmlruns it off esm.sh. - Preset files —
water.exportPreset()/importPreset()round-trip a tuned look (base mode, every dial, spectrum, wake, time) as JSON; the demo GUI has export / copy / import buttons. WaterSystem1.3.0:interaction,performance,lighting.setLights / setLight / clearLights / setMoon,wake.addGenerator,spray.burst,exportPreset / importPreset. New declarations:Interaction,ShallowWater,Hull,SunOcclusion,FlowMap,DynamicResolution,r3f/index.d.ts.- Docs: Interaction, Coast & shallow water, Drop-in boats, Occluded caustics, Lights on the water, Flow maps, Multiple water bodies, Preset files, Dynamic resolution, Compute FFT, React Three Fiber, a wave-tuning guide with recipes and a glossary; an examples gallery; a changelog page.
Tooling
- Visual regression rig (
tools/regress/) — node + playwright-core drive the demo frame-by-frame in a deterministic mode (?regress=1: seeded, fixed clock, paused loop stepped by__tidewaterStep) and compare 44 scenes against goldens on WebGPU (bit-exact) and WebGL2;tests/README.md. main.jssplit intosrc/scenes/*(Underwater, Weather, Harbour, Interactables) andsrc/demo/*(gui, capture, helpers). Persistent sky PMREM target; opaque canvas with the post exposure trim that keeps the 1.2 look; Snell window depth-attenuated (setWindowAbsorb); particle sprites (util/Particles.js) replace THREE.Points, which render as 1 px on WebGPU.
v1.2 — 2026-08-22
Free v1.x update. The surface is rebuilt for high-end realism, and the three "competitor parity" features each go a step further.
Surface realism
- Camera-centred radial LOD mesh (
OceanGeometry.js) — sub-metre quads at the hull, hundred-metre quads at the horizon, 12 km radius, one draw. The flat 5 km plane (≈9 m quads) is gone; the 16 m cascade is finally geometry, not just a normal map. - GPU FFT, actually correct. The fragment-pass iFFT was silently broken by three r184 (colorNode clamps data to ≥0 and forces alpha = 1; render targets read flipped in v; the stage-0 LUT lacked bit reversal). It now matches the CPU spectrum to float precision at 64/128/256. Grid per tier: 64 / 128 / 256 / 256.
- Energy-normalised spectrum — a cascade's
amplitudeis now its significant wave height in metres; wind speed only shapes the spectrum. Mid and short cascades carry real chop instead of centimetres. - Per-pixel normals from cascade derivative textures (slope + Jacobian), distance-faded per cascade. Shading no longer depends on mesh density.
- Sky reflection from the live PMREM by the perturbed normal — every facet mirrors its own patch of sky. The planar reflector now carries scene objects only (sky layer hidden from the mirror, alpha marks coverage).
- GGX sun specular with distance-filtered roughness and a decoupled glitter lobe; wind-gust field (slicks vs ruffled patches) modulating the fine ripple and roughness.
- RGB Beer–Lambert extinction on the refracted scene; forward-scatter SSS (sun behind the crest, height-lifted) replaces the Crytek stub.
- Persistent foam field (
FoamField.js) — world-locked GPU accumulate/decay field seeded from the fold, drifting downwind; blended with a sharp instantaneous crest line, contact foam at depth edges, shore foam and the wake. Foam gates retuned; foam shaded. - Seabed and prop caustics driven by the real surface — Hessian focusing of the cascade height field (
Caustics.surfaceCaustic), blended with the Worley field; the dancing light matches the waves overhead. - Sky pass. The dome no longer takes the renderer's distance fog (it sits 5 km out, so FogExp2 was painting the entire sky the fog colour — the muddy brown dusk).
TidewaterSkynow wraps the Preetham colour with an elevation-based horizon haze that tracksscene.fog(sky.setHaze()), a twilight fill (warm band toward a setting sun, purple belt opposite, dusky wash overhead), a night floor with a sparse star field, and per-modesky.gain/sky.nightoverrides (Moonlit uses them for a dark, starlit dome). Dense-fog modes read as overcast instead of a black zenith. - Surface from below. Inside the Snell window the facet now transmits the real sky — the PMREM sampled along the refracted ray, so the dome, haze and time of day show through — and the TIR region mirrors the water volume with wave structure instead of a flat fill; the sun through the window is compared against the refracted ray. Underwater debug probes 13–18.
- Sky PMREM bake fix (the IBL had been baking an empty scene); the previous PMREM is retired one bake late so a material still bound to it can't trip the WebGPU bind-group crash; god rays tamed at low sun; Offshore sky turbidity lowered; Hurricane foam gains brought back to ~1 for the normalised spectrum (the old ×2.7 / ×3.0 painted a white sheet).
Competitor parity, one step further
- Multiplayer determinism v2 — wavenumber-keyed phase hash, so any two clients (and the CPU mirror vs the GPU grid) agree at any quality tier;
getSyncState()/applySyncState()(one ~300-byte blob brings a joiner onto the exact sea);NetClockslews the local clock toward a latency- compensated host tick instead of snapping; seedable bow spray;<water-canvas seed deterministic>withtick/syncToTick/netClock;demo/multiplayer.html— two independent clients with simulated latency. - Water masking v2 — depth-aware volume masks (front/back depth interval per pixel), so a wave in front of the hull rolls past intact;
invertkeeps water only inside volumes (the Pool preset uses one box);softnessfeathers the cut;createBoxProxy(); the runabout ships a hull mask. - WebGL2: full FFT, no noise downgrade — the GPU cascades are fragment passes, so the same surface runs on the WebGL2 backend;
createRenderernow reports the real backend.
API
OceanFFTis the default surface (?ocean=gerstnerkeeps the old path for one release). New:advance/setViewer/setEnvironment,setMaskInvert/ Softness,setDebugView(12 isolated shading terms),setSunRoughness/ setExtinction/setGustStrength,foamField,atlasInfo, sync state.WaterSystem1.2.0:masking.addBox/setInvert/setSoftness,foam.field,sync.*,waves.setDebugView/setSeaParams. Types updated (NetClock.d.ts,FoamField.d.ts).- Docs: rewritten Ocean section, new Masking / Foam / Multiplayer sync / Debug views sections.
Tooling and packaging
- Offline capture (
?capture=1,__capture.runPlanOffline()): the marketing rig can now drive the simulation frame-by-frame throughrenderFrame()and post every frame as a JPEG to a local collector, then ffmpeg assembles the clips — deterministic, never drops a frame, and the tab can be occluded (MediaRecorder needed a steady real-time rAF). All site videos and mode stills were regenerated from the 1.2 surface with it. - The public demo bundles are obfuscated in the Pages build (
javascript-obfuscator, identifier renaming + string array; no control-flow flattening).TIDEWATER_OBFUSCATE=0 tools/build-pages.shskips it. The buyer zip still ships readablesrc/. - Release audit fixes:
<water-canvas>applies preset water colours again, honoursquality, drops the deadaudioattribute and frees controls/sky/ floor on disconnect; docs samples corrected (createRenderershape, Ripples / Buoyancy / TimeOfDay /pickQualitysignatures, sea-mode object shape); type declarations brought in line with the source (setWakeField, foam and wake setters,SeaMode,Ripples,Buoyancy,TimeOfDay,Quality,Sky,Caustics,FoamField);demo/multiplayer.htmlships in the zip;docs/feature-tracking.mdno longer publishes to the site; FoamField / WaterMask / reflector disposal leaks closed; the Pool scene parks the hull mask while the mask is inverted.
v1.1 — 2026-06-15
Free v1.x update. Adds physically-grounded optics, weather and motion off the hull, scene masking, deterministic multiplayer, and a unified API.
Surface & optics
- Physical dielectric Fresnel — replaced the Schlick approximation (above water) and the hand-tuned Snell-window smoothstep (below) with one unpolarised dielectric curve parameterised by the water's index of refraction (
ior≈ 1.333). Above water it's the air→water curve; below, the same equations yield total internal reflection past the ~48.6° critical angle, so the Snell window's bright disc and dark rim fall out of the physics. Applied toOceanNodeandOceanFFT; livesetIordial. - Persistent wave-crest foam with a dissolve mask, and a reworked, translucent boat wake (no more solid "milk" slab behind the hull).
Motion & weather
- Bow spray emitter — pooled GPU particles throw sheets off the moving bow and fire a burst on every wave slam via a hull-impact probe.
- Rain — slanted streaks in a camera-anchored column plus live dimples poked into the ripple height-field. Per-preset intensity, gated above water.
- Waterline meniscus — a refractive rim overlay for the half-submerged "camera at the waterline" shot, tracking the projected water plane.
Scene & multiplayer
- Water masking (
WaterMask) — a screen-space stencil that clips the ocean out of registered proxy geometry (hull interiors, pool walls). The Pool preset ships with it on. - Deterministic multiplayer — the surface is a closed-form function of an integer tick.
seed+ fixed-stepadvance(dt)+syncToTick(n)for byte-identical waves across clients.
API
- **
WaterSystemfaçade** — one object over every subsystem (waves,foam,sss,ssr,wake,buoyancy,spray,rain,masking,floor,lighting,fog,underwater,waterline) with chainable setters,loadPreset(string | object),printSettings()authoring, andgetDebugData(). Exposed aswindow.Tidewater; fully typed intypes/. - Demo gains a Debug panel — buoyancy/wake/spray probes + a live WebGPU↔WebGL switch (
?renderer=webgl).
v1.0 — 2026-05-21
First release. Eight weeks of work across surface, sky, sea floor, and the underwater scene, plus the sales site, docs and TypeScript types.
Surface
- Cascaded FFT ocean with three Tessendorf spectra at 256 m / 64 m / 16 m bands. CPU
Spectrummirror exposessampleHeight(x, z)so buoyancy is frame-perfect with no GPU readback latency. - GPU FFT path —
GPUFFTCascade.jsruns the per-cascade iFFT in TSL fragment passes (works on WebGPU + WebGL2 from the same graph). 27 passes per cascade per frame at N=64. CPU and GPU adopt the same h₀(k) so the rendered surface and the buoyant boat agree. - Gerstner swell layer — analytical band table on top of (or instead of) the FFT. Used on Low tier and as the swell mix for the FFT path.
- Cascaded foam Jacobian —
1 − Jpacked into the spectrum's.achannel via finite differences. Crests gate on real surface compression instead of a noise hack. - Physically-based shading — Fresnel, depth-graded absorption (analytical, not screen-space depth), dual-lobe sun glint, normal-map cascade, planar reflector + SSR composite (Med+ tier).
- Real subsurface scattering — Crytek-style fake-SSS, sun bent toward the wave normal, thickness proxy from height + slope, mode-tuneable
sssColor / sssAmbient / sssPower / sssDistortion. - Persistent wake field — world-locked 512² canvas the boat stamps into every frame. Sampled in vertex + fragment for displacement and foam. The wake is the integral of the path.
Sky + Atmosphere
- Dynamic Preetham sky —
TidewaterSkywraps three.jsSkyMeshand bakes a PMREM cube each frame (throttled to ~4 Hz with a sun-delta epsilon so the time-of-day slider doesn't thrash). - Volumetric cloud layer — built-in TSL fbm cumulus on the same dome. Per-mode coverage, density, elevation, scale, speed.
- Time-of-day dial — single 0–1 slider drives sun elevation + azimuth, repaints the sky, rebakes IBL, rebalances fog, swings the god-ray anchor, re-tints water. Each mode has a canonical hour; the dial scrubs around it while preserving mode character.
Sea floor
- Procedural seabed — Worley caustic-field on mottled sand, depth- tinted, fogged. Caustics shadow under overhanging geometry.
- Caustic projection on submerged props —
createCausticProjector()walks the scene graph and splices the same caustic field into every submerged material'semissiveNode. Per-mesh material clone so shared GLTF materials are not mutated in place (fixed a WebGPU bind-group crash that appeared after dives). - Reef life — bundled kelp, coral and reef-fish props for the Reef preset, hidden in other modes.
Underwater scene
- Snell's window above the camera; total-internal reflection past 49°.
- Directional Beer–Lambert fog — looking horizontal hazes fast, looking up keeps the Snell window bright. Eye-depth gradient from shallow tint near the surface to deep tint past ~40 m.
- Screen-space god rays — Postprocess radial-blur, anchored on the surface-above-camera projection when underwater. Luma threshold drops to ~0.18 so the dimmer Snell-window glow drives the shafts.
- Twinkling marine snow — per-particle phase + speed, vertex-colour modulated for sun-catch dwell.
Buoyancy
- **
Buoyancyclass** — reusable, multi-point sampling for genuine pitch + roll. Single-point mode for buoys / debris. - Three procedural boats — runabout (primitive geometry), yacht and trawler (CC0 GLBs). All wave-coupled.
- Click-to-splash ripples — Müller height-field on top of the FFT. River mode adds a flow direction so ripples drift downstream.
Quality + integration
- Four quality tiers — Low / Med / High / Ultra, auto-detected from backend + UA + pixel budget + hardware concurrency. URL override via
?quality=. Tiers shape ocean segments, FFT N + cascade count, max DPR, anisotropy ceiling, reflector resolution, cloud-layer compile. - Zero build step — plain ES modules with an import map. No Vite, no webpack.
- **
<water-canvas>web component** — drop-in custom element for non- Three.js sites. Visibility-aware autoplay, ResizeObserver, optional OrbitControls, mode + time attributes. - TypeScript declarations — hand-authored
.d.tscovering every public module. Source stays JS. - Hand-rolled docs site at
/docs/. No static-site generator. - Sales site at
/site/. Coastal-marine palette, oceanographic- chart visual reference, 12-feature long-scroll.
Modes
10 sea-state presets + 3 water-body presets:
- Reef · Tropical · Offshore · Huge Swell · Sunset · Tranquil · Moonlit · Foggy · Arctic · Hurricane
- Lake · River · Pool
Each preset swaps waves, foam, sky, fog, water tint, sea-floor look, bundled props, and ambient audio mix.