// Time and weather. A world that is always noon in fair weather is a diagram of a // world; moving the sun and letting it rain is what makes the same terrain read // as a place you could be standing in at a particular hour. // // Climate is not chosen by the user — it is read back from the world itself, so a // glacier world gets snow and a desert stays dry without anyone saying so. import * as THREE from 'three'; import { GRID } from './world.js'; // key times of day: sun colour, sky colour, ambient, and how strong the sun is const KEYS = [ { t: 0.00, sun: '#243043', sky: '#0e1622', amb: '#1b2433', power: 0.12 }, // night { t: 0.22, sun: '#e8915c', sky: '#7d7a8e', amb: '#5c5f70', power: 0.75 }, // dawn { t: 0.32, sun: '#ffd9a8', sky: '#9fc4dc', amb: '#c8dcea', power: 1.30 }, // morning { t: 0.50, sun: '#fff3dd', sky: '#a8ccdf', amb: '#cfe3f2', power: 1.55 }, // noon { t: 0.72, sun: '#ffc07a', sky: '#b39a92', amb: '#a89aa0', power: 1.05 }, // afternoon { t: 0.82, sun: '#f2764a', sky: '#7c5f6b', amb: '#5f5460', power: 0.55 }, // dusk { t: 1.00, sun: '#243043', sky: '#0e1622', amb: '#1b2433', power: 0.12 }, ]; function sample(t) { t = ((t % 1) + 1) % 1; let a = KEYS[0], b = KEYS[KEYS.length - 1]; for (let i = 0; i < KEYS.length - 1; i++) { if (t >= KEYS[i].t && t <= KEYS[i + 1].t) { a = KEYS[i]; b = KEYS[i + 1]; break; } } const k = (t - a.t) / Math.max(1e-6, b.t - a.t); return { sun: new THREE.Color(a.sun).lerp(new THREE.Color(b.sun), k), sky: new THREE.Color(a.sky).lerp(new THREE.Color(b.sky), k), amb: new THREE.Color(a.amb).lerp(new THREE.Color(b.amb), k), power: a.power + (b.power - a.power) * k, }; } /** * Read the climate off the finished world. Snow biomes and a high mean elevation * mean precipitation falls as snow; low moisture means it rarely falls at all. */ export function climateOf(world) { const cold = world.regions .filter(r => ['snow', 'alpine', 'tundra'].includes(r.key)) .reduce((a, r) => a + r.coverage, 0); const arid = world.regions .filter(r => ['desert', 'badlands', 'mesa', 'canyon'].includes(r.key)) .reduce((a, r) => a + r.coverage, 0); let mean = 0; for (let i = 0; i < world.moisture.length; i++) mean += world.moisture[i]; mean /= world.moisture.length; return { cold, arid, moisture: mean, precipitation: cold > 0.3 ? 'snow' : (arid > 0.45 || mean < 0.12 ? 'none' : 'rain'), }; } export class Sky { constructor(scene) { this.scene = scene; this.hemi = new THREE.HemisphereLight('#cfe3f2', '#4a4636', 1.2); this.sun = new THREE.DirectionalLight('#fff3dd', 1.5); scene.add(this.hemi, this.sun); scene.fog = new THREE.Fog('#9fc4dc', 140, 400); this.particles = null; this.kind = 'none'; } /** @param {number} t 0..1 through the day, 0.5 = noon */ setTime(t, radius = 150) { const s = sample(t); // the sun rides an arc; below the horizon it simply stops lighting anything const ang = (t - 0.25) * Math.PI * 2; this.sun.position.set(Math.cos(ang) * radius, Math.sin(ang) * radius, radius * 0.35); this.sun.color.copy(s.sun); this.sun.intensity = Math.max(0.02, s.power); this.hemi.color.copy(s.amb); this.hemi.intensity = 0.45 + s.power * 0.55; this.scene.background = s.sky.clone(); this.scene.fog.color.copy(s.sky); return s; } /** Rain or snow as a drifting particle column that follows the camera. */ setWeather(kind, extent = 220) { if (kind === this.kind) return; this.kind = kind; if (this.particles) { this.scene.remove(this.particles); this.particles.geometry.dispose(); this.particles.material.dispose(); this.particles = null; } if (kind === 'none') return; const count = kind === 'snow' ? 2600 : 4200; const pos = new Float32Array(count * 3); for (let i = 0; i < count; i++) { pos[i * 3] = (Math.random() - 0.5) * extent; pos[i * 3 + 1] = Math.random() * 90; pos[i * 3 + 2] = (Math.random() - 0.5) * extent; } const geo = new THREE.BufferGeometry(); geo.setAttribute('position', new THREE.BufferAttribute(pos, 3)); this.particles = new THREE.Points(geo, new THREE.PointsMaterial({ color: kind === 'snow' ? '#ffffff' : '#a9c6d8', size: kind === 'snow' ? 0.55 : 0.28, transparent: true, opacity: kind === 'snow' ? 0.85 : 0.55, depthWrite: false, })); this.particles.frustumCulled = false; this.scene.add(this.particles); this.extent = extent; } stepWeather(dt, camera) { if (!this.particles) return; const p = this.particles.geometry.attributes.position; const fall = this.kind === 'snow' ? 4 : 34; const drift = this.kind === 'snow' ? 1.6 : 0.4; for (let i = 0; i < p.count; i++) { let y = p.getY(i) - fall * dt; let x = p.getX(i) + Math.sin((y + i) * 0.15) * drift * dt; if (y < -4) { y = 85 + Math.random() * 8; x = camera.position.x + (Math.random() - 0.5) * this.extent; p.setZ(i, camera.position.z + (Math.random() - 0.5) * this.extent); } p.setX(i, x); p.setY(i, y); } p.needsUpdate = true; } }