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;
    }
}