Release notes
Every v1.x update is free for existing buyers — re-download the zip from your receipt.
v1.4.1 — 2026-09-14
A maintenance release for the surface crossing. Diving and resurfacing were a dropped frame each way and a hard cut; both are now seamless, and the crossing looks like a lens going through water.
Crossing the surface
- No more stall on a dive or a resurface. The demo swapped
scene.fogNodeat the crossing, and three r184 rebuilds every render object when the scene's fog node changes: 150–250 ms of shader work on each dive and each resurface, every time.createUnderwaterFognow takes the scene'sFogExp2asairFogand returns one node carrying both media behind asubmergeduniform. Assign it once and callunderwaterFog.setSubmerged(below)at the crossing. The dome, marine snow, light shafts and waterline are also drawn once behind the loader so the first dive does not pay for their pipelines. - The eye is tested against the wave, not the flat water level.
OceanFFT.setViewer(pos, surfaceY?)samples the wave under the camera (or takes the height you pass) to decide which side of the surface it is on. In a swell the flat test painted a camera standing in a trough as submerged, and one inside a crest as dry. While the eye is above the wave but below the mesh plane, the planar mirror plane follows it down so the reflection keeps rendering instead of going flat and dark. - The medium changes at once, the eye adapts. Fog, lights, sky and backdrop snap at the crossing. Exposure starts from a dim first look going in and a dazzle coming out, then settles over about a second. The above-water haze and the underwater fog no longer lerp through each other, so the sky does not turn grey for half a second after surfacing.
- Lens water (
Postprocess.crossSurface(below), on by default,setLensWater(false)to disable). Going in: a sheet of water and a burst of soft bubbles over the glass that clear in half a second. Coming out: the sheet runs off and leaves droplets that refract the frame and drain over a few seconds. Procedural, and free while idle. - Audio follows the ear under water:
TidewaterAudio.setSubmerged(below)closes a low-pass over the bed and plays a plunge or a resurface transient.Waterline.update(camera, waterLevel)takes the local surface height.
WebGL2
- A dive no longer drops the surface for a frame. With the eye below the surface, three r184's reflector clears its target from inside the main pass, and the WebGL backend left that framebuffer bound, so the rest of the frame drew into the mirror target the water samples (Chrome reported a feedback loop and skipped those draws). The reflector guard now rebinds the interrupted pass's framebuffer.
- The water mask no longer asks the WebGL backend for a blend equation it does not map (
INVALID_ENUM: blendEquationSeparateonce per frame on every scene). WebGPU keeps the exact max reduction.
Surface
- Shore foam no longer breaks into blocks near the camera. The foam dissolve pattern had no octave finer than about a metre, so close surf stencilled into hard islands with sand-lit water showing through, and the shallow-water field's bilinear fetch left straight-edged facets in the breaking lip. A fourth, rotated foam octave fades in within 18 m of the eye, the dissolve ramp widens there, and the shallow field is read with a C1 reconstruction (the CPU buoyancy read matches). Beyond 18 m nothing changes.
- Big crests no longer mirror a ghost hull metres below the surface. The planar mirror is only right at its own plane, so its scene objects fade out with height off that plane (
mirrorFadeHeight, default 3 m) and the sky from the environment takes over.setMirrorPlaneHeight(y)puts the plane where the reflection matters: the demo samples the wave under the launch each frame, so on a crest the hull's reflection sits at its real waterline and the mirror's clip plane no longer paints the lifted hull's dark bottom into the sea beside it (the "ghost hull" on mid swells). - The Snell window has a soft edge. Seen from below, the window's boundary is the critical angle, and off the full per-pixel normal every ripple pushed facets across it, so the window was a ragged patch. It is now taken off a normal with the ripple detail damped; everything else keeps the full normal.
Look
- The beach has sand. The beach material samples the sand texture at two scales and a rotation, derives a normal from its grain, adds wind ripples and swash bars, and breaks up the roughness; the wet-sand band and its reflections behave as before.
- River banks are no longer near black for the same reason as the sea stacks below.
- Sea stacks are no longer black. The rock material tinted an already dark texture with a mid-grey in linear space and cut its environment fill to 30%, leaving an albedo near zero. The tint is now near white and the fill full.
- The harbour has light at night. Each dock lamp carries a warm point light over the boards, the pilings and the berth alongside, and a cool moon fill keeps silhouettes readable; both ramp in with the night and leave with the harbour on the lake, river and pool scenes.
- Sunset skies are no longer one sepia wash.
Skygrades the upper sky toward a cool dusk blue by elevation while the sun is low (sky.zenithCool, default 0.75, 0 restores the old look), so the frame runs from an orange horizon through rose to a blue zenith and the water near the camera picks up the blue. The Sunset preset's haze is rose rather than tan and its god rays are a touch lower. - The sun no longer whites out a quarter of the frame. The sky's Mie halo sat far above white for tens of degrees around a mid-elevation sun.
Skynow compresses luminance above a knee with a hue-preserving log curve, so the halo is a soft ring around a sun that is still the brightest thing in the sky; nothing below the knee changes. - A sunset brightens all the way down to the horizon. With the sun low the sky model darkened the last few degrees above the horizon to a dim band, and the horizon haze then painted the fog colour over it. The haze is now lit toward the sun's azimuth while the sun is within about ten degrees of the horizon.
- God rays no longer streak vertically from a sun on the horizon. Above water the ray source is now the sky alone (by scene depth); the sun's glint path on the water, which the radial blur smeared into a column through the disc, no longer feeds it. Below water the source is unchanged.
- Caustics are cells again, not squares. The Worley cell search in the seabed and the prop projector nested two TSL loops, and the inner counter shadowed the outer in the generated shader, so only the diagonal neighbours were ever visited: every cell was a single point inside hard square edges (the "woven basket" the domain warp was hiding). One loop over both ranges visits all nine. The surface-focus term now takes its curvature from the slope atlas rather than second differences of the bilinear height, which was piecewise constant per texel.
- A shallow sand floor no longer renders as white paper. The seabed's sunlight now loses red through the water column over the first ten metres and the caustics brighten the sand instead of adding white to it (the deep reef floor keeps its palette), and the ripple shading's gradient step is finer than the noise it differentiates.
- Light shafts no longer stack into a slab at the horizon just under the surface. Their tops ease in over the first metres instead of starting hard at the surface, and the cluster fades in with eye depth, so a lens looking level a metre under sees water, not a bright band.
Demo
- The launch drives like a boat. Throttle is a demand the engine spools to over a second or so; thrust falls off with speed; hull resistance is linear plus square-law with a bump at hull speed; gravity along the hull slows it climbing a wave face and surges it down the back; a slam scrubs speed off; and a rough sea costs more the harder it is driven into. Flat out it reaches about 30 knots in fifteen seconds and coasts down over ten.
Boat.setPropulsion({...})retunes it. - The sea stacks no longer risk a texture-upload error when their model loads before the rock maps do; the stone is painted once the maps are in.
- The film rig can average sub-frames into each output frame (
shutter) and crossfade a clip's tail into its head (loop) so a page loop has no cut. The hero clip uses both: the launch crosses the glint path under a fixed camera twelve metres up, from and to an empty frame. The docs example stills are shot through the rig (tools/film/examples.mjs).
v1.4 — 2026-09-12
This update adds pressure-driven wakes and hydrostatic hulls, revises the surface lighting, and includes a custom launch, buoy and jetty made in Blender.
Fixes (14 September 2026)
The 1.4 download was rebuilt with these fixes. Download it again from your purchase page; the version number is unchanged.
- The WebGPU renderer no longer stops with
reading 'mipLevelCount'when the canvas resizes while the camera is underwater. Dynamic resolution does this on most dives. Three r184's planar reflector resized its target and then skipped rendering it;OceanFFTandOceanNodenow allocate the target. - Refraction no longer draws a tinted copy of a hull in the water beneath it, and planar reflections no longer sit below the objects they mirror. Both distortions were offset by the flat-water normal.
- Caustics no longer bake each material's emissive into a shared constant, so one material's colour stops lighting others (the green cast on launch fittings).
- Additive particles fade through alpha, which removes the dark specks spray left in reflections. Underwater groupers are cloned with their skeletons instead of rendering stacked at the origin.
- Tropical and Beach skies no longer clip to cyan, and presets whose subsurface or fog colours leaned mint now read turquoise.
Waves and contact
Ripplesnow advances dispersive gravity waves with a fixed clock. Moving hull pressure produces transverse and diverging wakes; impulses travel at wavelength-dependent speeds. A damped border limits wraparound. Low quality retains a cheaper non-dispersive solver.wakeField.setSurface(ocean, ripples, flowMap)couples registered generators to that field. Footprints follow the actual hull dimensions and surface, measure speed relative to currents, and ignore hidden objects and teleports.OceanFFT({ ripples })includes local waves in CPU queries. Ambient height queries invert horizontal FFT displacement and return the world water level. Atlas lookups now land on texel centres, matching CPU sample coordinates.- Contact crossings use wave-relative velocity and a swept crossing position.
onSplash(event)exposes position, entry/exit, relative speed and energy. hydrostatic: trueapplies mass/volume-based lift and probe torque at 120 Hz.attachHull()enables it by default;hydrostatic: falsekeeps the spring response. Current drift works under rotated parent groups.
Lighting and surface
- View-ray optical depth, RGB transmission without a fixed opaque tint, and foreground rejection during refraction. Microfacet Fresnel uses the light half-vector; projected normal variance filters small highlights.
- Cloud shapes remain in the procedural sky's cached environment bake. Surface sun shadows reuse the occlusion map on devices with spare texture slots; zero-intensity local lights skip their shading work.
- Scene SSR is opt-in in the full demo (
?ssr=1); planar and environment water reflections remain active. Manual SSR gain survives submersion changes. - Smaller foam detail, broken prop-wash coverage and tighter contact foam. Foam follows sun colour and shadow. Tropical and Offshore carry more short waves. Optics controls are available through
water.optics.set().
Developer integration
await createWater({ scene, camera, renderer })creates the core runtime.update(dt),addBoat(), automatic buoyancy contact anddispose()cover common integrations. The host retains its renderer and model assets.- Preset files include optics, wake wave strength and local-wave settings. New debug views: 22 for local height/slopes, 23 for filtered roughness.
- Original Blender assets: 8.6 m launch (13,332 triangles), buoy (3,352), jetty (22,824). Editable scenes and the reproducible builder are in the paid zip.
examples/launch.htmldemonstrates the managed runtime and custom models. - Updated declarations and handbook, including units, update order, ownership, quality budgets, model waterlines and migration notes. Vanilla TypeScript consumers no longer require a React namespace just to import the types.
Performance and fixes
- One ripple texture sample supplies slopes and foam, replacing four gradient taps. Idle local fields skip transforms/uploads; wake stamps reuse a brush. Quality tiers budget local wave resolution and foam buffers explicitly.
- Zero FFT choppiness no longer divides height slopes by zero. Compute atlases dispose correctly, and sun-occlusion teardown restores registered layers.
- Numerical tests cover dispersion, pressure, superposition, frame-rate agreement, buoyancy, relative impacts, hidden objects and world transforms.
See the handbook's upgrade notes before reading the ripple texture directly: its channels are now height, X slope, Z slope and foam. Local wave history is not part of the ambient ocean's multiplayer sync state.
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. - Beach sand tone — the demo beach lifts the sand map to a pale, warm cream and takes a little of the sky PMREM (
envMapIntensity0.25) instead of almost none, so dry sand reads as sand in daylight rather than as mud. - 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.