Spaces:
Running
Running
| // 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; | |
| } | |
| } | |