diff options
| author | skylar <me@casper> | 2026-11-01 05:43:24 -0800 |
|---|---|---|
| committer | skylar <me@casper> | 2026-11-01 05:43:24 -0800 |
| commit | e69ad01e700e80fa959dfee08cd556ca0d777ffd (patch) | |
| tree | 645fc9db2142e87a5ef302733a77ba2fec848d88 /public/js/components/sky.js | |
Yesod: self-hosted WebXR rooms
Start a room in a world and hang out in VR from the headset's browser:
A-Frame + Networked-A-Frame over EasyRTC, with a coturn relay that can hide
everyone's IP address. Ships with the Campfire, Temple, Cabin, Balcony and
Test room worlds.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Diffstat (limited to 'public/js/components/sky.js')
| -rw-r--r-- | public/js/components/sky.js | 498 |
1 files changed, 498 insertions, 0 deletions
diff --git a/public/js/components/sky.js b/public/js/components/sky.js new file mode 100644 index 0000000..49867bd --- /dev/null +++ b/public/js/components/sky.js @@ -0,0 +1,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)); + }, + }); +})(); |