Guides
Buoyancy and wakes
The CPU reads the same wave field the GPU draws, so objects float on the waves you see. Buoyancy moves them up and down and tilts them. Wakes turn their motion into foam, a Kelvin V and a trail that stays behind.
addWake measures its speed from its motion: contact foam hugs the hull, the Kelvin V spreads behind it, and the trail stays in the persistent foam field.#Sample the surface
const y = water.sampleHeight(x, z); // surface height in metres
const n = water.sampleNormal(x, z); // THREE.Vector3, from ±0.5 m differences
water.isUnderwater(camera); // or any Object3DsampleHeight includes the swell, shoaling and shore breakers. It inverts the waves' horizontal shift in 4 steps, so it returns the height at (x, z) rather than at the point that started there.
| Tier | Where the height comes from | Cost |
|---|---|---|
| Gerstner (low, medium) | Evaluates the same Gerstner sum as the shader | About 5 × waves sin/cos per call |
| FFT (high, ultra) | Bilinear read of the swell cascade, read back from the GPU asynchronously | A texture lookup, but 1–3 frames old |
On FFT tiers floating objects follow the swell but not the small chop, which lives in the detail cascades.
#Float an object
// a buoy: one probe, spring-damper heave, tilts toward the local normal
water.buoyancy.addObject(buoy, { draft: 0.2 });
// a boat: four probes (bow, stern, port, starboard) → heave, pitch and roll
water.buoyancy.addObject(boat, { size: { length: 12, beam: 4 }, draft: 0.4 });
// any hull shape: your own probe offsets, in local (x, z), bow along +Z
water.buoyancy.addObject(raft, { probes: [[-2, 3], [2, 3], [-2, -3], [2, -3]] });| Option | Default | |
|---|---|---|
probes | [[0, 0]] | Local (x, z) sample offsets. The bow is along local +Z |
size | — | { length, beam }: a shortcut for four probes |
draft | 0 | How far the object's origin sits below the mean surface. Negative lifts it |
heightSmoothing | 2.5 | Smoothing rate for the height, per second |
rotationSmoothing | heightSmoothing | Smoothing rate for pitch and roll (×1.6 for single-probe tilt) |
spring | true | Single probe: spring-damper heave instead of plain smoothing |
tiltSample | 0.8 | Single probe: distance for the normal sample, in metres |
rollJitter | 0.012 | Idle roll in radians, so nothing sits perfectly still |
With one probe the object bobs on a spring and leans with the local slope. With several, a least-squares plane through the sampled heights gives heave, pitch and roll, each smoothed.
const handle = water.buoyancy.addObject(crate, { draft: 0.1 });
handle.enabled = false; // pause it (e.g. while it's carried)
water.buoyancy.removeObject(crate); // or pass the handle#Add a wake
water.addWake(ship, { length: 26, beam: 9.5 });
// each frame, move the ship yourself: the wake measures the motion
ship.position.x += Math.sin(ship.rotation.y) * speed * dt;
ship.position.z += Math.cos(ship.rotation.y) * speed * dt;| Option | Default | |
|---|---|---|
length | 10 | Hull length in metres |
beam | length / 3 | Hull width |
headingOffset | 0 | Radians added to the yaw if the model's bow isn't along local +Z |
useVelocityHeading | false | Point the wake along the direction of motion instead of the yaw |
Every tracked hull gets white water along its sides. Once it moves, it adds a Kelvin V with 19.5° arms and stamps the persistent foam field, so a trail stays behind it. The handle's speed reports the measured speed in m/s. Teleports of more than 80 m/s are ignored, so resetting a boat doesn't read as a burst of speed.
Up to MAX_HULLS (8) wakes are drawn. removeWake(objOrHandle) frees a slot.
#Floating without the WaterSystem
Buoyancy works with anything that has sampleHeight(x, z), for example a flat test surface in a unit test:
import { Buoyancy } from 'naturegl-water';
const flat = { sampleHeight: () => 0 };
const buoyancy = new Buoyancy(flat);
buoyancy.addObject(mesh, { draft: 0.3 });
buoyancy.update(dt);