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
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
|
/* global AFRAME, THREE, Astro */
// The night sky over the site's location, live: the 9,096 stars of the Yale Bright Star
// Catalog, the Milky Way from NASA's Deep Star Maps, and the Moon and planets
// where they really are (js/astro.js). It always stays night: the Sun isn't
// drawn, but it still lights the Moon, so the Moon shows its real phase, and a
// bright Moon washes out the faint stars and lights the clearing a little.
//
// The sky is drawn as from a dark site; a city's own light would hide most of
// it. The location comes from the server (SKY_LATITUDE, SKY_LONGITUDE), which
// app.js passes in when it loads a world. The celestial sphere follows the head, since stars show no parallax, and
// turns with the sidereal time every frame.
//
// To see another time, add ?sky=2027-07-04T23:00-07:00 to the page's address,
// and &skyspeed=600 to run the sky 600 times faster.
(function () {
const SKY_DIR = 'assets/sky/';
const RAD = Math.PI / 180;
const R_MILKY_WAY = 450;
const R_STARS = 400;
const R_PLANETS = 390;
const R_MOON = 380;
const EARTH_RADIUS_KM = 6378.14;
const MOON_RADIUS_KM = 1737.4;
const PLANET_COLORS = {
mercury: '#e8ddd0', venus: '#fff8e8', mars: '#ffb98a',
jupiter: '#fff0d8', saturn: '#f6e2b0', uranus: '#d8f4f6',
};
// The larger maria, to paint the Moon's face: selenographic latitude,
// longitude (east positive) and radius, in degrees.
const MARIA = [
[33, -16, 17], [28, 17, 10], [8, 31, 12], [17, 59, 8], [-8, 51, 9],
[-15, 35, 5.5], [-21, -17, 10], [-24, -39, 6], [13, 4, 4], [-10, -23, 6],
[25, -55, 16], [5, -50, 13], [7.5, -31, 6], [56, -10, 6], [57, 15, 6],
];
// Night-sky colours (sRGB). The horizon colour also becomes the fog colour,
// so the forest fades into the sky. Moonlight adds a deep blue.
const ZENITH = '#000002';
const MOON_SKY = '#0a1330';
const MOON_GLOW = '#c4d0ff';
// Air near the horizon dims what's behind it: the airmass for a direction
// whose sine of altitude is h, and 0.25 magnitudes lost per airmass.
const AIRMASS = /* glsl */ `
float airmass(float h) {
h = max(h, 0.0);
return 1.0 / (h + 0.025 * exp(-11.0 * h));
}
`;
const STAR_VERTEX = /* glsl */ `
attribute float aMag;
attribute vec3 aColor;
attribute float aTwinkle;
uniform float uTime;
uniform float uPxScale;
uniform float uFaint;
uniform float uWash;
varying vec3 vColor;
${AIRMASS}
void main() {
vec4 world = modelMatrix * vec4(position, 1.0);
vec3 dir = normalize(world.xyz - cameraPosition);
float x = airmass(dir.y);
// 1 for a star at the naked-eye limit (6.5) overhead, times uFaint.
float lum = uFaint * exp(0.921 * (6.5 - aMag - 0.25 * (x - 1.0)));
// Stars twinkle, more so low down; planets hardly do.
float tw = sin(uTime * 6.3 + position.x * 0.37) * sin(uTime * 2.9 + position.y * 0.53);
lum *= 1.0 + aTwinkle * min(x, 8.0) * 0.05 * tw;
lum = max(lum - uWash, 0.0);
vColor = aColor * min(lum, 1.0);
// Brighter than the screen can show: grow instead.
gl_PointSize = 2.2 * uPxScale * min(pow(max(lum, 1.0), 0.25), 7.0);
gl_Position = projectionMatrix * viewMatrix * world;
// Below the horizon or washed out: move it outside the clip volume.
if (dir.y < 0.0 || lum < 0.004) gl_Position = vec4(0.0, 0.0, 2.0, 1.0);
}
`;
const STAR_FRAGMENT = /* glsl */ `
varying vec3 vColor;
void main() {
vec2 p = gl_PointCoord * 2.0 - 1.0;
float r2 = dot(p, p);
if (r2 > 1.0) discard;
gl_FragColor = vec4(vColor * exp(-3.0 * r2), 1.0);
#include <colorspace_fragment>
}
`;
const SKY_VERTEX = /* glsl */ `
varying vec2 vUv;
varying vec3 vDir;
void main() {
vUv = uv;
vec4 world = modelMatrix * vec4(position, 1.0);
vDir = world.xyz - cameraPosition;
gl_Position = projectionMatrix * viewMatrix * world;
}
`;
const SKY_FRAGMENT = /* glsl */ `
uniform sampler2D uMap;
uniform float uGain;
uniform float uSaturation;
uniform vec3 uZenith;
uniform vec3 uHorizon;
uniform vec3 uMoonDir;
uniform vec3 uMoonGlow;
varying vec2 vUv;
varying vec3 vDir;
${AIRMASS}
void main() {
vec3 dir = normalize(vDir);
float up = step(0.0, dir.y);
vec3 col = mix(uZenith, uHorizon, pow(1.0 - max(dir.y, 0.0), 4.0));
vec3 mw = texture2D(uMap, vUv).rgb;
mw = mix(vec3(dot(mw, vec3(0.2126, 0.7152, 0.0722))), mw, uSaturation);
col += mw * uGain * exp(-0.23 * (airmass(dir.y) - 1.0)) * up;
// Moonlight scattered in the air: a bright aureole and a wide glow.
float theta = sqrt(max(2.0 * (1.0 - dot(dir, uMoonDir)), 0.0));
col += uMoonGlow * (0.8 * exp(-theta / 0.03) + 0.2 * exp(-theta / 0.35)) * up;
gl_FragColor = vec4(col, 1.0);
#include <colorspace_fragment>
// Dither, so the dark gradients don't band.
gl_FragColor.rgb += (fract(sin(dot(gl_FragCoord.xy, vec2(12.9898, 78.233))) * 43758.5453) - 0.5) / 255.0;
}
`;
const MOON_VERTEX = /* glsl */ `
varying vec3 vLocal;
varying vec3 vNormal;
varying vec3 vView;
void main() {
vLocal = normalize(position);
vec4 world = modelMatrix * vec4(position, 1.0);
vNormal = mat3(modelMatrix) * position;
vView = normalize(cameraPosition - world.xyz);
gl_Position = projectionMatrix * viewMatrix * world;
}
`;
const MOON_FRAGMENT = /* glsl */ `
uniform vec4 uMaria[${MARIA.length}];
uniform vec3 uSunDir;
uniform vec3 uTint;
varying vec3 vLocal;
varying vec3 vNormal;
varying vec3 vView;
void main() {
vec3 n = normalize(vNormal);
// Lommel-Seeliger scattering: why the full Moon looks like a flat disc.
float mu0 = dot(n, uSunDir);
float mu = max(dot(n, vView), 0.0);
float lit = mu0 > 0.0 ? 2.0 * mu0 / (mu0 + mu + 1e-3) : 0.0;
float sea = 0.0;
for (int i = 0; i < ${MARIA.length}; i++) {
float a = acos(clamp(dot(vLocal, uMaria[i].xyz), -1.0, 1.0));
sea = max(sea, 1.0 - smoothstep(uMaria[i].w * 0.7, uMaria[i].w, a));
}
// Faint earthshine on the dark part.
gl_FragColor = vec4(uTint * (1.0 - 0.42 * sea) * (0.95 * lit + 0.006), 1.0);
#include <colorspace_fragment>
}
`;
// A sphere in J2000 equatorial coordinates (pole on +z) whose texture
// coordinates follow the NASA map: plate carrée, 0h in the middle, right
// ascension increasing to the left.
function celestialSphere(radius, raSteps, decSteps) {
const positions = [];
const uvs = [];
const index = [];
for (let j = 0; j <= decSteps; j++) {
const v = j / decSteps;
const dec = (v - 0.5) * Math.PI;
for (let i = 0; i <= raSteps; i++) {
const u = i / raSteps;
const ra = (0.5 - u) * 2 * Math.PI;
positions.push(radius * Math.cos(dec) * Math.cos(ra), radius * Math.cos(dec) * Math.sin(ra), radius * Math.sin(dec));
uvs.push(u, v);
}
}
for (let j = 0; j < decSteps; j++) {
for (let i = 0; i < raSteps; i++) {
const a = j * (raSteps + 1) + i;
const c = a + raSteps + 1;
index.push(a, a + 1, c + 1, a, c + 1, c);
}
}
const geometry = new THREE.BufferGeometry();
geometry.setAttribute('position', new THREE.Float32BufferAttribute(positions, 3));
geometry.setAttribute('uv', new THREE.Float32BufferAttribute(uvs, 2));
geometry.setIndex(index);
return geometry;
}
// B-V colour index to a gentle tint: Ballesteros' formula for the
// temperature, then a blackbody's colour (Tanner Helland's fit), mostly
// washed back toward white, because the eye sees little colour in starlight.
function starColor(bv, out) {
const b = Math.min(2, Math.max(-0.4, bv));
const k = 46 * (1 / (0.92 * b + 1.7) + 1 / (0.92 * b + 0.62));
const clamp = (x) => Math.min(255, Math.max(0, x)) / 255;
const r = k <= 66 ? 1 : clamp(329.698727446 * Math.pow(k - 60, -0.1332047592));
const g = k <= 66 ? clamp(99.4708025861 * Math.log(k) - 161.1195681661) : clamp(288.1221695283 * Math.pow(k - 60, -0.0755148492));
const bl = k >= 66 ? 1 : k <= 19 ? 0 : clamp(138.5177312231 * Math.log(k - 10) - 305.0447927307);
const max = Math.max(r, g, bl);
return out.setRGB(0.5 + 0.5 * r / max, 0.5 + 0.5 * g / max, 0.5 + 0.5 * bl / max, THREE.SRGBColorSpace);
}
function toMatrix4(rows, out) {
const [a, b, c] = rows;
return out.set(a[0], a[1], a[2], 0, b[0], b[1], b[2], 0, c[0], c[1], c[2], 0, 0, 0, 0, 1);
}
const smooth = THREE.MathUtils.smoothstep;
AFRAME.registerComponent('sky', {
schema: {
latitude: { default: 51.4769 }, // Greenwich, until app.js sets the site's
longitude: { default: -0.0005 },
// Which way true South lies in the scene, as an angle like campsite.js's
// polar(): toward the pond, the most open view. The Moon, the planets
// and the Milky Way's core all pass through the southern sky here.
south: { default: 132 },
milkyWay: { default: 0.1 },
// How much a full Moon overhead adds to the two lights.
moonlight: { default: 0.08 },
ambientLight: { type: 'selector', default: '#ambient-light' },
moonLight: { type: 'selector', default: '#moon-light' },
// A world seen through one window can hold the Moon in it: the sky turns
// about the Moon to keep it at this bearing (an angle like `south`) and
// altitude (degrees), so the stars stay right around it and its phase is
// still real. moonSize makes it look bigger than it really is.
pinMoon: { default: false },
moonBearing: { default: 180 },
moonAltitude: { default: 10 },
moonSize: { default: 1 },
},
init() {
const params = new URLSearchParams(location.search);
const start = Date.parse(params.get('sky') || '');
this.speed = Number(params.get('skyspeed')) || 1;
this.realStart = Date.now();
this.skyStart = Number.isFinite(start) ? start : this.realStart;
this.bodiesAt = -Infinity;
const scene = this.el.sceneEl.object3D;
this.horizonBase = scene.fog ? scene.fog.color.clone() : new THREE.Color('#020306');
this.ambientBase = this.data.ambientLight ? this.data.ambientLight.getAttribute('light').intensity : 0;
this.moonLightBase = this.data.moonLight ? this.data.moonLight.getAttribute('light').intensity : 0;
this.moonLightHome = this.data.moonLight ? this.data.moonLight.object3D.position.clone() : null;
// Equator and equinox of the date; the catalogue and map are J2000.
this.celestial = new THREE.Group();
this.j2000 = new THREE.Group();
this.celestial.matrixAutoUpdate = this.j2000.matrixAutoUpdate = false;
this.celestial.add(this.j2000);
scene.add(this.celestial);
const s = this.data.south * RAD;
const north = new THREE.Vector3(-Math.sin(s), 0, -Math.cos(s));
const east = new THREE.Vector3(-north.z, 0, north.x);
this.basis = new THREE.Matrix4().makeBasis(north, east, new THREE.Vector3(0, 1, 0));
this.horizon = new THREE.Matrix4();
this.rotation = new THREE.Matrix4();
this.lightRotation = new THREE.Matrix4();
this.pin = new THREE.Matrix4();
this.pinQuat = new THREE.Quaternion();
this.moonNow = new THREE.Vector3();
const b = this.data.moonBearing * RAD;
const h = this.data.moonAltitude * RAD;
this.moonTarget = new THREE.Vector3(Math.sin(b) * Math.cos(h), Math.sin(h), Math.cos(b) * Math.cos(h));
this.head = new THREE.Vector3();
this.bufferSize = new THREE.Vector2();
this.sunEq = new THREE.Vector3(1, 0, 0);
this.moonEq = new THREE.Vector3(1, 0, 0);
this.moonLit = 0;
this.buildMilkyWay();
this.buildPoints();
this.buildMoon();
this.loadStars();
},
remove() {
this.el.sceneEl.object3D.remove(this.celestial);
},
buildMilkyWay() {
const texture = new THREE.TextureLoader().load(SKY_DIR + 'milkyway-4k.jpg');
texture.colorSpace = THREE.SRGBColorSpace;
this.skyUniforms = {
uMap: { value: texture },
uGain: { value: this.data.milkyWay },
uSaturation: { value: 0.6 },
uZenith: { value: new THREE.Color(ZENITH) },
uHorizon: { value: this.horizonBase.clone() },
uMoonDir: { value: new THREE.Vector3(0, -1, 0) },
uMoonGlow: { value: new THREE.Color(0, 0, 0) },
};
const sphere = new THREE.Mesh(celestialSphere(R_MILKY_WAY, 96, 48), new THREE.ShaderMaterial({
uniforms: this.skyUniforms, vertexShader: SKY_VERTEX, fragmentShader: SKY_FRAGMENT,
side: THREE.DoubleSide, depthWrite: false, fog: false,
}));
sphere.renderOrder = -10;
sphere.frustumCulled = false;
this.j2000.add(sphere);
},
buildPoints() {
this.pointUniforms = {
uTime: { value: 0 },
uPxScale: { value: 1 },
uFaint: { value: 0.14 },
uWash: { value: 0 },
};
this.pointMaterial = new THREE.ShaderMaterial({
uniforms: this.pointUniforms, vertexShader: STAR_VERTEX, fragmentShader: STAR_FRAGMENT,
transparent: true, depthWrite: false, blending: THREE.AdditiveBlending, fog: false,
});
const names = Object.keys(PLANET_COLORS);
const geometry = new THREE.BufferGeometry();
const color = new THREE.Color();
geometry.setAttribute('position', new THREE.BufferAttribute(new Float32Array(names.length * 3), 3));
geometry.setAttribute('aMag', new THREE.BufferAttribute(new Float32Array(names.length).fill(99), 1));
geometry.setAttribute('aColor', new THREE.Float32BufferAttribute(names.flatMap((n) => color.set(PLANET_COLORS[n]).toArray()), 3));
geometry.setAttribute('aTwinkle', new THREE.BufferAttribute(new Float32Array(names.length).fill(0.15), 1));
this.planets = new THREE.Points(geometry, this.pointMaterial);
this.planets.renderOrder = 2;
this.planets.frustumCulled = false;
this.celestial.add(this.planets);
},
buildMoon() {
this.moonUniforms = {
uMaria: {
value: MARIA.map(([lat, lon, r]) => new THREE.Vector4(
Math.cos(lat * RAD) * Math.sin(lon * RAD), Math.sin(lat * RAD), Math.cos(lat * RAD) * Math.cos(lon * RAD), r * RAD,
)),
},
uSunDir: { value: new THREE.Vector3(1, 0, 0) },
uTint: { value: new THREE.Color(1, 1, 1) },
};
// Local +z faces the observer and +y celestial north (see updateBodies),
// so selenographic east is +x: Mare Crisium on the right, as seen from
// the northern hemisphere.
this.moon = new THREE.Mesh(new THREE.SphereGeometry(1, 32, 16), new THREE.ShaderMaterial({
uniforms: this.moonUniforms, vertexShader: MOON_VERTEX, fragmentShader: MOON_FRAGMENT,
transparent: true, blending: THREE.AdditiveBlending, fog: false,
}));
this.moon.renderOrder = 1; // before the stars, so it hides the ones behind it
this.moon.visible = false;
this.moon.frustumCulled = false;
this.celestial.add(this.moon);
},
loadStars() {
fetch(SKY_DIR + 'stars.bin')
.then((res) => {
if (!res.ok) throw new Error(res.statusText);
return res.arrayBuffer();
})
.then((buffer) => {
// Four floats a star: J2000 right ascension and declination (radians), V magnitude, B-V.
const data = new Float32Array(buffer);
const count = data.length / 4;
const position = new Float32Array(count * 3);
const color = new Float32Array(count * 3);
const mag = new Float32Array(count);
const c = new THREE.Color();
for (let i = 0; i < count; i++) {
const ra = data[i * 4];
const dec = data[i * 4 + 1];
position[i * 3] = R_STARS * Math.cos(dec) * Math.cos(ra);
position[i * 3 + 1] = R_STARS * Math.cos(dec) * Math.sin(ra);
position[i * 3 + 2] = R_STARS * Math.sin(dec);
mag[i] = data[i * 4 + 2];
starColor(data[i * 4 + 3], c).toArray(color, i * 3);
}
const geometry = new THREE.BufferGeometry();
geometry.setAttribute('position', new THREE.BufferAttribute(position, 3));
geometry.setAttribute('aMag', new THREE.BufferAttribute(mag, 1));
geometry.setAttribute('aColor', new THREE.BufferAttribute(color, 3));
geometry.setAttribute('aTwinkle', new THREE.BufferAttribute(new Float32Array(count).fill(1), 1));
const stars = new THREE.Points(geometry, this.pointMaterial);
stars.renderOrder = 2;
stars.frustumCulled = false;
this.j2000.add(stars);
})
.catch((err) => console.warn("Couldn't load the stars", err));
},
tick(time) {
const { latitude, longitude } = this.data;
const ms = this.skyStart + (Date.now() - this.realStart) * this.speed;
if (Math.abs(ms - this.bodiesAt) > 30000) this.updateBodies(ms);
this.skyRotation(ms, this.rotation);
const camera = this.el.sceneEl.camera;
if (camera) camera.getWorldPosition(this.head);
this.celestial.matrix.copy(this.rotation).setPosition(this.head);
this.celestial.matrixWorldNeedsUpdate = true;
this.moonUniforms.uSunDir.value.copy(this.sunEq).transformDirection(this.rotation);
this.skyUniforms.uMoonDir.value.copy(this.moonEq).transformDirection(this.rotation);
this.pointUniforms.uTime.value = time / 1000;
this.pointUniforms.uPxScale.value = THREE.MathUtils.clamp(this.pixelsPerRadian() / 1300, 1, 2.5);
},
/** Equatorial to scene directions at a time: the real sky, or turned to hold the Moon where pinMoon wants it. */
skyRotation(ms, out) {
out.multiplyMatrices(this.basis, toMatrix4(Astro.horizonMatrix(ms, this.data.latitude, this.data.longitude), this.horizon));
if (!this.data.pinMoon) return out;
this.moonNow.copy(this.moonEq).transformDirection(out);
return out.premultiply(this.pin.makeRotationFromQuaternion(this.pinQuat.setFromUnitVectors(this.moonNow, this.moonTarget)));
},
/** Roughly how many pixels a radian of sky covers, so stars look the same size on any screen. */
pixelsPerRadian() {
const renderer = this.el.sceneEl.renderer;
if (renderer.xr.isPresenting) {
const view = renderer.xr.getCamera().cameras[0];
return view ? view.viewport.w * 0.5 * view.projectionMatrix.elements[5] : 0;
}
const camera = this.el.sceneEl.camera;
if (!camera) return 0;
return renderer.getDrawingBufferSize(this.bufferSize).y * 0.5 * camera.projectionMatrix.elements[5];
},
updateBodies(ms) {
this.bodiesAt = ms;
const { latitude, longitude } = this.data;
const sky = Astro.bodies(ms, latitude, longitude);
const horizon = Astro.horizonMatrix(ms, latitude, longitude);
toMatrix4(Astro.precessionMatrix(ms), this.j2000.matrix);
this.j2000.matrixWorldNeedsUpdate = true;
this.sunEq.fromArray(sky.sun);
this.moonEq.fromArray(sky.moon.dir);
const position = this.planets.geometry.attributes.position;
const mag = this.planets.geometry.attributes.aMag;
sky.planets.forEach((p, i) => {
position.setXYZ(i, p.dir[0] * R_PLANETS, p.dir[1] * R_PLANETS, p.dir[2] * R_PLANETS);
mag.setX(i, p.magnitude);
});
position.needsUpdate = mag.needsUpdate = true;
// The Moon, at its true size, turned so its north points to the celestial pole.
// A pinned Moon is lit and coloured as if well up, even when it's held low
// in a window, and it's there whether or not it has really risen.
const moonAlt = this.data.pinMoon ? Math.max(this.data.moonAltitude, 30) : Astro.altAz(sky.moon.dir, horizon).alt;
const toUs = this.moonEq.clone().negate();
const up = new THREE.Vector3(0, 0, 1).addScaledVector(this.moonEq, -this.moonEq.z).normalize();
const right = new THREE.Vector3().crossVectors(up, toUs);
this.moon.quaternion.setFromRotationMatrix(new THREE.Matrix4().makeBasis(right, up, toUs));
this.moon.position.copy(this.moonEq).multiplyScalar(R_MOON);
this.moon.scale.setScalar(this.data.moonSize * R_MOON * Math.tan(Math.asin(MOON_RADIUS_KM / (sky.moon.dist * EARTH_RADIUS_KM))));
this.moon.visible = moonAlt > -0.5;
// Low down, the Moon is dimmer and orange.
const high = smooth(moonAlt, 0, 20);
this.moonUniforms.uTint.value.setRGB(1, 0.62 + 0.36 * high, 0.36 + 0.58 * high).multiplyScalar(0.55 + 0.45 * high);
// How much the Moon lights things: 1 for a full Moon well up, about a
// third at quarter (by eye, not by light meter), nothing once it's set.
const flux = Math.pow(10, -0.4 * (sky.moon.magnitude + 12.73));
const m = Math.sqrt(flux) * smooth(moonAlt, -0.5, 12);
this.moonLit = m;
const moonSky = new THREE.Color(MOON_SKY).multiplyScalar(m);
this.skyUniforms.uHorizon.value.copy(this.horizonBase).add(moonSky);
this.skyUniforms.uZenith.value.set(ZENITH).add(moonSky.multiplyScalar(0.6));
this.skyUniforms.uMoonGlow.value.set(MOON_GLOW).multiplyScalar(0.05 * m);
this.skyUniforms.uGain.value = this.data.milkyWay * (1 - 0.75 * m);
this.pointUniforms.uWash.value = 0.5 * m;
const fog = this.el.sceneEl.object3D.fog;
if (fog) fog.color.copy(this.skyUniforms.uHorizon.value);
this.updateLights(m);
},
updateLights(m) {
const { ambientLight, moonLight, moonlight } = this.data;
if (ambientLight) ambientLight.setAttribute('light', 'intensity', this.ambientBase + moonlight * 0.5 * m);
if (!moonLight) return;
moonLight.setAttribute('light', 'intensity', this.moonLightBase + moonlight * m);
// Shine from the Moon while it's up.
const toMoon = this.moonEq.clone().transformDirection(this.skyRotation(this.bodiesAt, this.lightRotation));
const home = this.moonLightHome.clone().normalize();
moonLight.object3D.position.copy(home.lerp(toMoon, Math.min(1, m * 4)).normalize().multiplyScalar(40));
},
});
})();
|