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.
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
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 positionThe 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.
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.
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)
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).
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();
});#Change the look
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