Guides
Flow, rocks and floaters
Every river knows which way it runs and how fast, and you can ask it. Rocks that break the surface get foam and a wake, objects can ride the current downstream, and your own game code can query height and velocity anywhere.
#Ask the water
All queries run on the CPU against the same paths the meshes are built from. They cost a grid lookup, not a GPU readback.
const s = water.sample(x, z);
// null where there is no water, otherwise:
// { height, flow: Vector2 (m/s), speed, edge, inside, body }
water.sampleHeight(x, z); // water level or null
water.sampleFlow(x, z, out); // velocity into `out` (a Vector2) or null
water.isInWater(x, z); // inside a river's banks or a lake's shore
water.isUnderwater(camera); // camera below a surface (defaults to the system's camera)
water.getFlowMultiplier(); // the eased water.flow| Field | Meaning |
|---|---|
height | Water level. Where bodies overlap, the highest surface wins |
flow | Surface velocity in m/s, (x, z), with the global water.flow multiplier applied |
speed | Length of flow |
edge | abs(lateral distance) / half width: 0 on the centre line, 1 at the bank. Always 0 on lakes |
inside | true between the banks and between the path ends. false in the margin past the bank |
body | The River or Lake that answered |
#Obstacles
A rock, pier or post that pokes through the surface can get a foam ring that hugs the waterline, a bow pillow upstream and a wake that follows the local flow downstream.
const o = water.addObstacle(new THREE.Vector3(3, 0, -2), 0.9); // position, radius at the waterline
water.addObstacle({ x: -1, z: 8 }, 0.5, 0.6); // strength 0.6: a gentler wake
water.removeObstacle(o);radiusis measured at the waterline, not the rock's full size. The demo computes it from how far each boulder sticks out of the water.- The foam and roughness scale with the local speed, so the same rock foams more in the rapids.
- Each frame, only the nearest
maxObstaclesto the camera are used: 12 onlowup to 64 onultra. - Anything that crosses the surface also gets thin depth-based shoreline foam without being registered. Registering adds the ring, the bow and the wake.
addObstacle(). Here water.foam is raised to 2.5 to make the rings and wakes easy to see.#Floaters
water.floaters moves objects with the current. Anything with a position works: a mesh, a group, or a plain proxy object that you copy into an InstancedMesh yourself.
const log = new THREE.Mesh(logGeometry, woodMaterial);
log.position.set(0, 0, -6);
scene.add(log);
water.floaters.add(log, {
radius: 0.5,
drag: 0.85, // fraction of the surface speed it reaches
bob: 0.012, // bobbing amplitude, metres
draft: -0.02, // sits this much below (−) or above (+) the surface
spin: 0.5, // yaw rate while drifting
align: false, // true: point along the direction of travel instead of spinning
onExit: obj => obj.position.set(0, 0, -6), // left the water: respawn it
});
water.floaters.remove(log);
water.floaters.clear();| Option | Default | |
|---|---|---|
radius | 0.3 | Size |
drag | 0.9 | How closely it follows the current |
bob | 0.012 | Bob amplitude (m) |
draft | 0 | Offset from the surface (m) |
spin | 0.4 | Spin rate, faster in faster water |
align | false | Yaw along the velocity instead of spinning |
onExit | — | Called when the object leaves the water |
Floaters accelerate toward the local surface velocity with inertia and bounce back off the banks. If rotation exists, they also rock gently. They are updated inside water.update(dt), so there is nothing else to call.
#Drive your own effects
The demo's falling leaves are not library floaters. They are an InstancedMesh driven by sample(), which shows how to build your own:
const w = water.sample(leaf.x, leaf.z);
if (w && w.inside && leaf.y <= w.height + 0.02) {
leaf.x += w.flow.x * 0.9 * dt;
leaf.z += w.flow.y * 0.9 * dt;
leaf.y = w.height + 0.015;
}The same queries work for a boat's physics, a fishing float, or sound: set the volume of a rapids loop from sample(x, z)?.speed.