WorldForge / sky.js
openfree's picture
WorldForge: prompt-driven region plan, semantic layout map and composite height field
d9a31a1 verified
Raw
History Blame Contribute Delete
5.57 kB
// 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;
}
}