Spaces:
Running
Running
File size: 5,570 Bytes
d9a31a1 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 | // 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;
}
}
|