NatureGL Riverv1.0.0

Start here

Quick start

A lake that spills over a waterfall into a winding stream, with terrain carved to fit, caustics on the bed and a crate drifting downstream. This is examples/basic/ with commentary.

#Set up your renderer, scene and camera

Nothing here is specific to NatureGL River. The system turns on ACESFilmicToneMapping for you if the renderer has no tone mapping, because the water is composited in linear HDR.

js
import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
import { RiverSystem } from 'naturegl-river';

const renderer = new THREE.WebGLRenderer({ antialias: false });
renderer.setPixelRatio(Math.min(devicePixelRatio, 1.5));
renderer.setSize(innerWidth, innerHeight);
renderer.shadowMap.enabled = true;
document.body.appendChild(renderer.domElement);

const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(55, innerWidth / innerHeight, 0.1, 1000);
camera.position.set(-16, 13, 24);
const controls = new OrbitControls(camera, renderer.domElement);

const sun = new THREE.DirectionalLight(0xffffff, 3);
sun.castShadow = true;
const hemi = new THREE.HemisphereLight(0xbcd8ff, 0x3b4a2c, 1);
scene.add(sun, sun.target, hemi);

antialias: false is deliberate. Your scene is drawn into the system's own render target, so the canvas's MSAA would be wasted. The ultra tier turns on MSAA for that target instead.

#Create the system and bind your lights

js
const water = await RiverSystem.create({ renderer, scene, camera, quality: 'high', preset: 'clear' });
water.bindLights(sun, hemi);   // the preset now sets their colour, intensity and the sun's position

The system adds only its sky dome to your scene. The water surfaces, cascades and particles live in a separate overlay scene that render() draws.

#Author the water

A lake at y = 3, a short outlet to the lip, a 3 m waterfall, and a stream that winds away from the pool. Every y is a water level, not a bed height.

js
water.addLake({ center: new THREE.Vector3(0, 3, -24), radius: 9, depth: 2.5 });
water.addRiver({
  path: [new THREE.Vector3(0, 3, -17), new THREE.Vector3(0, 3, -11)],
  width: 6, depth: 1, flowSpeed: 1, extend: [0, 0],
});
water.addWaterfall({ top: new THREE.Vector3(0, 3, -11), bottom: new THREE.Vector3(0, 0, -9.8), width: 5 });
water.addRiver({
  path: [-9.8, -2, 6, 14, 24, 36].map((z, i) => new THREE.Vector3(Math.sin(i * 1.3) * 4, 0, z)),
  width: 7, depth: 1.6, flowSpeed: 1.6,
});
water.addObstacle(new THREE.Vector3(Math.sin(1.3) * 4 + 1, 0, -2), 0.9);   // foam ring + wake

#Carve the terrain to fit

carveTerrain() wraps your height function. The result has the river beds, the lake basin and the plunge pool cut in. Patch the ground material so it gets caustics and a wet band at the waterline.

js
const plateau = z => 3.2 * Math.min(1, Math.max(0, (-9 - z) / 3));   // upper level for the lake
const hills = (x, z) => 0.6 + Math.max(0, Math.abs(x) - 5) * 0.2
  + 0.6 * Math.sin(x * 0.3) * Math.cos(z * 0.2) + plateau(z);
const height = water.carveTerrain(hills);

const geo = new THREE.PlaneGeometry(90, 90, 220, 220).rotateX(-Math.PI / 2);
const p = geo.attributes.position;
for (let i = 0; i < p.count; i++) p.setY(i, height(p.getX(i), p.getZ(i)));
geo.computeVertexNormals();

const ground = new THREE.Mesh(geo, water.patchMaterial(
  new THREE.MeshStandardMaterial({ color: 0x8a8272, roughness: 0.9 }),
));
ground.receiveShadow = true;
scene.add(ground);

#Float something downstream (optional)

js
const box = new THREE.Mesh(new THREE.BoxGeometry(0.6, 0.2, 0.4), new THREE.MeshStandardMaterial({ color: 0x8b5a2b }));
box.position.set(0, 0, -6);
scene.add(box);
water.floaters.add(box, { onExit: o => o.position.set(0, 0, -6) });   // respawn at the pool

#Update, then let the system render

update(dt) advances time and easing and rebuilds meshes after authoring. render() replaces renderer.render(scene, camera).

js
addEventListener('resize', () => {
  renderer.setSize(innerWidth, innerHeight);
  camera.aspect = innerWidth / innerHeight;
  camera.updateProjectionMatrix();
  water.resize();
});

const timer = new THREE.Timer();
renderer.setAnimationLoop(t => {
  timer.update(t);
  controls.update();
  water.update(timer.getDelta());
  water.render();
});
The same building blocks at the scale of the demo valley: the main run below the cascade, carved into the demo's hills, with its boulders and forest.

#Change the look

js
water.loadPreset('glacial');                 // lighting eases in over ~1.5 s, water over ~0.3 s
water.loadPreset('night', { instant: true }); // snap
water.setParam('water.clarity', 1.6);        // one value by path
water.params.water.foam = 1.4;               // params is live too

#Keep going