NatureGL Riverv1.0.0

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.

js
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
FieldMeaning
heightWater level. Where bodies overlap, the highest surface wins
flowSurface velocity in m/s, (x, z), with the global water.flow multiplier applied
speedLength of flow
edgeabs(lateral distance) / half width: 0 on the centre line, 1 at the bank. Always 0 on lakes
insidetrue between the banks and between the path ends. false in the margin past the bank
bodyThe 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.

js
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);
  • radius is 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 maxObstacles to the camera are used: 12 on low up to 64 on ultra.
  • Anything that crosses the surface also gets thin depth-based shoreline foam without being registered. Registering adds the ring, the bow and the wake.
Boulders in the demo's main run, registered with 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.

js
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();
OptionDefault
radius0.3Size
drag0.9How closely it follows the current
bob0.012Bob amplitude (m)
draft0Offset from the surface (m)
spin0.4Spin rate, faster in faster water
alignfalseYaw 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:

js
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.