NatureGL Weatherv1.1.0

Start here

Quick start

Rain, puddles and lightning in an ordinary three.js scene, with a shelter whose floor stays dry. This is the complete examples/basic/ integration, with commentary.

Every step is required except where it says optional.

#Set up the renderer

NatureGL Weather needs WebGL2 with float render targets. Turn shadows on if you want shadowed light shafts in the fog. The tone mapping below is only used by the plain renderer.render() path; weather.render() has its own ACES tonemapper.

js
import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
import { WeatherSystem, PRESETS } from 'naturegl-weather';

const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setPixelRatio(Math.min(devicePixelRatio, 1.5));
renderer.setSize(innerWidth, innerHeight);
renderer.shadowMap.enabled = true;
renderer.toneMapping = THREE.ACESFilmicToneMapping; // used only by the renderer.render() path
document.body.appendChild(renderer.domElement);

#Build your scene as usual

NatureGL Weather works in metres. Rain wraps in a 36 × 26 × 36 m box around the camera, so a normal street-level camera is what it's tuned for.

js
const scene = new THREE.Scene();
scene.background = new THREE.Color(0x5d6670);
const camera = new THREE.PerspectiveCamera(50, innerWidth / innerHeight, 0.1, 500);
camera.position.set(7, 3.5, 9);
const controls = new OrbitControls(camera, renderer.domElement);
controls.target.set(0, 1, 0);

const sun = new THREE.DirectionalLight(0xffffff, 1.2);
sun.position.set(8, 12, 5);
sun.castShadow = true;
scene.add(sun, new THREE.HemisphereLight(0xb0c0d0, 0x303030, 1.2));

#Create the weather

create() adds the rain, splash, snow and bolt meshes to your scene. With lights: true (the default) it also adds a flash DirectionalLight and HemisphereLight for lightning. The preset is applied instantly.

js
const weather = await WeatherSystem.create({ renderer, scene, camera, quality: 'high', preset: 'downpour' });

#Patch your materials

patchMaterial() works on MeshStandardMaterial and MeshPhysicalMaterial and returns the same material. { ground: true } adds puddles, the full snow blanket and the reflectivity that the puddle reflections read.

js
const ground = new THREE.Mesh(
  new THREE.PlaneGeometry(60, 60),
  weather.patchMaterial(new THREE.MeshStandardMaterial({ color: 0x77736d, roughness: 0.9 }), { ground: true }),
);
ground.rotation.x = -Math.PI / 2;
ground.receiveShadow = true;

const stone = weather.patchMaterial(new THREE.MeshStandardMaterial({ color: 0xa89f92, roughness: 0.8 }));
const roof = new THREE.Mesh(new THREE.BoxGeometry(4, 0.2, 3), stone);
roof.position.y = 2.6;
const posts = [[-1.8, -1.3], [1.8, -1.3], [-1.8, 1.3], [1.8, 1.3]].map(([x, z]) => {
  const p = new THREE.Mesh(new THREE.BoxGeometry(0.2, 2.5, 0.2), stone);
  p.position.set(x, 1.25, z);
  return p;
});
for (const m of [roof, ...posts]) m.castShadow = m.receiveShadow = true;
scene.add(ground, roof, ...posts);

#Tell it what shelters and what lights

setOccluders() picks the objects drawn into the top-down occlusion map: no rain under the roof, and a dry floor. setLighting() feeds your light to the particles, the fog and the snow glints. shadowLight gives the fog shadowed shafts.

js
weather.setOccluders([ground, roof, ...posts]);
weather.setLighting({
  sunDirection: sun.position.clone().normalize(),
  sunColor: sun.color, sunIntensity: 1.2,
  ambient: [0.3, 0.33, 0.36], skyHorizon: [0.45, 0.5, 0.56], skyZenith: [0.3, 0.36, 0.45],
  shadowLight: sun,
});
weather.on('strike', e => console.log(`strike ${e.distance.toFixed(0)} m away, thunder in ${e.delay.toFixed(1)} s`));

#Update, then render

Call update(dt) once per frame after moving the camera. It reads the camera's transform, eases the weather, accumulates wetness and snow and fires strikes. Then render through the weather pipeline, or render the scene yourself.

js
const plain = new URLSearchParams(location.search).has('plain');
const timer = new THREE.Timer();
renderer.setAnimationLoop(t => {
  timer.update(t);
  controls.update();
  weather.update(timer.getDelta());
  if (plain) renderer.render(scene, camera);   // scene fog, env-map puddles
  else weather.render();                       // volumetric fog, SSR puddles, bloom, ACES
});

addEventListener('resize', () => {
  renderer.setSize(innerWidth, innerHeight);
  camera.aspect = innerWidth / innerHeight;
  camera.updateProjectionMatrix();
  weather.resize();                            // render() also detects size changes
});

Open examples/basic/ from npm run dev, and add ?plain to its URL to try the renderer.render() path. Render paths compares the two.

The same idea in the demo: snowfall around the well. The occlusion map stops the flakes at the well roof and the benches, and the cobbles under them stay clear.

#Change the weather

js
weather.loadPreset('thunderstorm');          // eased: most of the way in ~3 s; surfaces over ~4.5 s
weather.loadPreset('snowfall', { instant: true });   // snap
weather.set({ rain: 0.4, wind: 0.8 });       // any subset of the preset keys, eased
weather.strike();                            // a lightning strike right now
weather.audio.unmute();                      // from a click or key handler

Click anywhere in examples/basic/ to cycle through the presets with loadPreset.

#Keep going