diff --git a/build.py b/build.py
index 097864d..2f2b67c 100644
--- a/build.py
+++ b/build.py
@@ -11,6 +11,7 @@ What gets inlined:
- vendor/suncalc.js, vendor/plane3d.js,
vendor/clouds3d.js (plain \n'
- % (json.dumps(stars, separators=(',', ':')), json.dumps(theme_css))
+ % (json.dumps(stars, separators=(',', ':')), moon_uri, json.dumps(theme_css))
)
# must come before config.js so the early theme loader sees the CSS
anchor = ''
@@ -158,8 +166,8 @@ def main():
with tempfile.NamedTemporaryFile('w', suffix='.html', delete=False) as f:
f.write(html)
tmp = f.name
- print('OK: self-contained (%d bytes, %d themes, %d stars)'
- % (len(html), len(themes), len(stars)))
+ print('OK: self-contained (%d bytes, %d themes, %d stars, moon map %d KB)'
+ % (len(html), len(themes), len(stars), len(moon_uri) // 1024))
os.unlink(tmp)
return
@@ -167,8 +175,9 @@ def main():
f.write(html)
print('Wrote %s (%d bytes)' % (out, len(html)))
- print('Inlined: %s, stars.json (%d entries), themes: %s'
- % (', '.join(INLINED_SCRIPTS), len(stars), ', '.join(sorted(themes))))
+ print('Inlined: %s, stars.json (%d entries), moon.jpg (%d KB), themes: %s'
+ % (', '.join(INLINED_SCRIPTS), len(stars), len(moon_uri) // 1024,
+ ', '.join(sorted(themes))))
if __name__ == '__main__':
diff --git a/vendor/LICENSE b/vendor/LICENSE
index 8b5d326..d7a6172 100644
--- a/vendor/LICENSE
+++ b/vendor/LICENSE
@@ -8,6 +8,14 @@ stars.json - star catalogue derived from the Yale Bright Star Catalogue
(5th ed., public catalogue data). Generated for this project; see the
real-sky notes in clouds3d.js.
+moon.jpg - equirectangular near-side lunar map (1024x512, downscaled from
+the 2048x1024 original) used for the moon disc texture.
+Source: https://www.solarsystemscope.com/textures/ (via Wikimedia Commons,
+"Solarsystemscope texture 2k moon.jpg")
+Author: Solar System Scope
+License: CC BY 4.0 (https://creativecommons.org/licenses/by/4.0/)
+Attribution required: Solar System Scope / solarsystemscope.com
+
plane3d.js - low-poly airliner mesh (480 triangles, flat shaded, no textures)
Original geometry generated by tools/plane_gen.py in this repository; offered
under CC0 1.0 like the rest of the project's own assets.
diff --git a/vendor/clouds3d.js b/vendor/clouds3d.js
index 08b801f..3e78efc 100644
--- a/vendor/clouds3d.js
+++ b/vendor/clouds3d.js
@@ -97,6 +97,17 @@ var FRAG_SRC = [
'uniform vec3 uCloudAlbedo;', // theme-tinted but brightness-normalised
'uniform float uAirDensity;', // slant optical depth per world unit
'uniform float uCloudHaze;', // clouds sit above most of the haze: much weaker
+ 'uniform sampler2D uMoonTex;', // equirectangular near-side lunar map
+ 'uniform float uMoonTexOn;', // 0 until the map is loaded (flat disc)
+ 'uniform float uMoonR;', // disc angular radius in radians
+ 'uniform float uMoonFrac;', // illuminated fraction (drives the halo)
+ 'uniform float uMoonEarth;', // earthshine on the dark side
+ 'uniform float uMoonLimb;', // limb darkening strength
+ 'uniform float uMoonBright;', // per-pixel luminance of the disc
+ 'uniform float uMoonHalo;',
+ 'uniform float uMoonFlipU;',
+ 'uniform float uMoonFlipV;',
+ 'uniform float uSunR;', // sun disc angular radius (radians)
// 3D hash and value noise
'float hash3(vec3 p) {',
'p = fract(p * vec3(0.1031, 0.1030, 0.0973));',
@@ -224,6 +235,11 @@ var FRAG_SRC = [
'vec3 col = mix(uBg, uSky, skyT);',
'float nightFactor = 1.0 - smoothstep(-0.15, 0.05, uSunAlt);',
'float skyMask = smoothstep(-0.20, 0.40, rd.y);',
+ // Stars must not sit on the moon: the disc is opaque, so every
+ // star term is masked out inside (a little beyond) its rim.
+ // 0 inside the disc, 1 outside it - the smoothstep runs along the
+ // angular distance, so an extra "1.0 -" would blank the whole sky.
+ 'float moonOcc = smoothstep(uMoonR * 0.92, uMoonR * 1.35, length(rd - uMoonDir));',
// night darkening of the gradient BEFORE star emission is added
'col *= (1.0 - nightFactor * 0.55);',
// ---- real star catalog (Yale BSC5 baked into a texture) ----
@@ -243,38 +259,68 @@ var FRAG_SRC = [
// stars only once the sky is actually dark: the dusk palette
// is rust/blood, and additive white stars on a red sky read
// as red stars
- 'col += st * starVis * skyMask * twk * 1.45;',
+ 'col += st * starVis * skyMask * twk * 1.45 * moonOcc;',
'}',
// ---- Sun disc ------------------------------------------------
// angular radius ~0.27 deg -> chord length ~0.0047
'if (uSunAlt > -0.03) {',
'vec3 qs = rd - uSunDir;',
'float angS = length(qs);',
- 'float discS = 1.0 - smoothstep(0.0040, 0.0050, angS);',
- 'float haloS = exp(-angS * 55.0) * 0.30 + exp(-angS * 14.0) * 0.12;',
+ // The disc is drawn at uSunR (CONFIG.sun.angularRadius, 12x
+ // physical by default) but the surface brightness stays
+ // exactly what it was: the halo profile is normalised to the
+ // disc radius, so the sun is bigger, not brighter.
+ 'float angN = angS * (0.00465 / uSunR);',
+ 'float discS = 1.0 - smoothstep(uSunR * 0.86, uSunR, angS);',
+ 'float haloS = exp(-angN * 55.0) * 0.30 + exp(-angN * 14.0) * 0.12;',
'vec3 sunCol = mix(uLight, vec3(1.0, 0.92, 0.72), 0.35);',
'col += sunCol * (discS * 1.15 + haloS) * (1.0 - nightFactor * 0.85);',
'}',
- // ---- Moon disc with real terminator ---------------------------
- // lit side = dot(surface normal, sun dir) -> crescent/gibbous
- // falls out of geometry, phase tracks __currentMoonDir.
- 'if (uMoonDir.y > -0.01) {',
+ // ---- Moon disc: lunar map + geometric terminator --------------
+ // The near side always faces the observer, so the map is sampled
+ // in a selenographic frame built from the north celestial pole:
+ // north up, east to the right, exactly like a printed lunar map,
+ // and it rotates as the moon crosses the sky. The terminator is
+ // pure geometry (surface normal dot sun direction), so crescent,
+ // gibbous and the orientation towards the sun all fall out.
+ 'if (uMoonDir.y > -0.02) {',
+ 'float MR = uMoonR;',
'vec3 qm = rd - uMoonDir;',
'float angM = length(qm);',
- 'if (angM < 0.02) {',
- 'float z = sqrt(max(0.0, 0.0046 * 0.0046 - angM * angM));',
- 'vec3 nM = normalize(qm + uMoonDir * (z + 0.00001));',
+ 'if (angM < MR) {',
+ 'float rr = angM / MR;',
+ 'float z = sqrt(max(0.0, 1.0 - rr * rr));',
+ // outward normal of the NEAR hemisphere: the offset part
+ // plus -uMoonDir (towards the observer), not +uMoonDir.
+ // With the wrong sign the lit side was the far side and
+ // the disc came out black at and near full moon.
+ 'vec3 nM = normalize(qm / MR - uMoonDir * z);',
+ 'vec3 upTry = uPole - uMoonDir * dot(uPole, uMoonDir);',
+ 'vec3 fb = (abs(uMoonDir.y) < 0.9) ? vec3(0.0, 1.0, 0.0) : vec3(1.0, 0.0, 0.0);',
+ 'vec3 fb2 = fb - uMoonDir * dot(fb, uMoonDir);',
+ 'vec3 uU = (dot(upTry, upTry) > 1e-6) ? normalize(upTry) : normalize(fb2);',
+ 'vec3 uE = cross(uU, uMoonDir);',
+ 'float lon = atan(dot(nM, uE), dot(nM, -uMoonDir));',
+ 'float lat = asin(clamp(dot(nM, uU), -1.0, 1.0));',
+ 'vec2 uv = vec2(lon / 6.2831853 + 0.5, lat / 3.14159265 + 0.5);',
+ 'uv.x = mix(uv.x, 1.0 - uv.x, uMoonFlipU);',
+ 'uv.y = mix(uv.y, 1.0 - uv.y, uMoonFlipV);',
+ 'vec3 alb = mix(vec3(0.62, 0.63, 0.66), texture2D(uMoonTex, uv).rgb, uMoonTexOn);',
'float litM = dot(nM, uSunDir);',
- 'float phase = smoothstep(-0.08, 0.12, litM);',
- 'float discM = 1.0 - smoothstep(0.0041, 0.0046, angM);',
- 'vec3 moonCol = mix(vec3(0.62, 0.64, 0.68), vec3(0.97, 0.95, 0.88), phase);',
- 'float haloM = exp(-angM * 60.0) * 0.10 * (0.25 + 0.75 * phase);',
- 'col += (moonCol * discM * phase + vec3(0.75, 0.78, 0.85) * haloM) * 1.25;',
+ 'float phase = smoothstep(-0.05, 0.12, litM);',
+ 'float lam = max(0.0, litM);',
+ 'float limb = 1.0 - uMoonLimb * rr * rr * rr;',
+ 'float shade = lam * lam * 0.85 + lam * 0.18;',
+ 'float earth = uMoonEarth * (1.0 - phase) * (0.35 + 0.65 * (1.0 - rr));',
+ 'float discM = 1.0 - smoothstep(MR * 0.965, MR, angM);',
+ 'col += alb * (shade * limb + earth) * discM * uMoonBright;',
'}',
+ 'float haloM = exp(-angM * 60.0) * 0.10 + exp(-angM * 16.0) * 0.04;',
+ 'col += vec3(0.75, 0.78, 0.85) * haloM * uMoonHalo * (0.25 + 0.75 * uMoonFrac);',
'}',
// faint procedural fill for stars below the catalog limit
'float stars = starField(rd) * skyMask * nightFactor;',
- 'col += stars * uLight * 0.85;',
+ 'col += stars * uLight * 0.85 * moonOcc;',
// ---- Horizon airlight -------------------------------------
// At and below the horizon the sky shows the same scattered
// column the terrain fades into. Without it the outer rim of
@@ -703,6 +749,17 @@ function init(canvas) {
var uSkyOn = gl.getUniformLocation(prog, 'uSkyOn');
var uStarLon = gl.getUniformLocation(prog, 'uStarLon');
var uPole = gl.getUniformLocation(prog, 'uPole');
+ var uMoonTex = gl.getUniformLocation(prog, 'uMoonTex');
+ var uMoonTexOn = gl.getUniformLocation(prog, 'uMoonTexOn');
+ var uMoonR = gl.getUniformLocation(prog, 'uMoonR');
+ var uMoonFrac = gl.getUniformLocation(prog, 'uMoonFrac');
+ var uMoonEarth = gl.getUniformLocation(prog, 'uMoonEarth');
+ var uMoonLimb = gl.getUniformLocation(prog, 'uMoonLimb');
+ var uMoonBright = gl.getUniformLocation(prog, 'uMoonBright');
+ var uMoonHalo = gl.getUniformLocation(prog, 'uMoonHalo');
+ var uMoonFlipU = gl.getUniformLocation(prog, 'uMoonFlipU');
+ var uMoonFlipV = gl.getUniformLocation(prog, 'uMoonFlipV');
+ var uSunR = gl.getUniformLocation(prog, 'uSunR');
var uSunColor = gl.getUniformLocation(prog, 'uSunColor');
var uSunInt = gl.getUniformLocation(prog, 'uSunInt');
var uMoonColor = gl.getUniformLocation(prog, 'uMoonColor');
@@ -801,6 +858,10 @@ function init(canvas) {
uPhysical: uPhysical, uAirColor: uAirColor, uCloudAlbedo: uCloudAlbedo,
uAirDensity: uAirDensity, uCloudHaze: uCloudHaze,
uSkyTex: uSkyTex, uSkyOn: uSkyOn, uStarLon: uStarLon, uPole: uPole,
+ uMoonTex: uMoonTex, uMoonTexOn: uMoonTexOn, uMoonR: uMoonR,
+ uMoonFrac: uMoonFrac, uMoonEarth: uMoonEarth, uMoonLimb: uMoonLimb,
+ uMoonBright: uMoonBright, uMoonHalo: uMoonHalo,
+ uMoonFlipU: uMoonFlipU, uMoonFlipV: uMoonFlipV, uSunR: uSunR,
// terrain program
terrain: {
prog: terrainProg,
@@ -918,6 +979,17 @@ var CAM_MID_Y = (FL_MIN_KM + FL_MAX_KM) / 2 / KM_PER_TILE;
var BOB_AMP = typeof ALT_BOB.amp === 'number' ? ALT_BOB.amp : 0.77;
var BOB_W = 6.2831853 / (typeof ALT_BOB.periodS === 'number' ? ALT_BOB.periodS : 90);
var BOB_TILT = typeof ALT_BOB.tilt === 'number' ? ALT_BOB.tilt : 0.30; // pitch response
+// ---- Camera aim (CONFIG.camera) ---------------------------------------
+// The camera always flies the configured path; 'aim' only changes where it
+// looks. 'moon' turns the view to the moon - heading to its azimuth, pitch
+// to its altitude - while the flight path, the fetch corridor and the
+// traffic stay untouched. aimBob keeps a little altitude bob so the view
+// never looks frozen, aimEase smooths the turn.
+var CAM_CFG = (window.CONFIG && window.CONFIG.camera) || {};
+var CAM_AIM_MOON = (CAM_CFG.aim === 'moon');
+var CAM_AIM_EASE = (typeof CAM_CFG.aimEase === 'number') ? Math.max(0, Math.min(0.999, CAM_CFG.aimEase)) : 0.94;
+var CAM_AIM_BOB = (typeof CAM_CFG.aimBob === 'number') ? Math.max(0, Math.min(1, CAM_CFG.aimBob)) : 0.25;
+var aimHdX = 0, aimHdZ = 1, aimPitch = 0.45, aimReady = false;
// CONFIG.clouds = { top: ..., bottom: ... } overrides the defaults; the
// deck must stay below the camera and keep a minimum thickness.
var CLOUD_TOP = 0.9, CLOUD_BOT = -0.3;
@@ -1925,6 +1997,30 @@ function buildTerrainMesh(gl, state) {
var ATMO = (window.CONFIG && window.CONFIG.atmosphere) || {};
var ATMO_TURBIDITY = (typeof ATMO.turbidity === 'number') ? Math.max(0.2, ATMO.turbidity) : 1.0;
var ATMO_MOON_GAIN = (typeof ATMO.moonGain === 'number') ? Math.max(0, ATMO.moonGain) : 0.16;
+// ---- Moon appearance (CONFIG.moon) ------------------------------------
+// The real disc is 0.0046 rad; the scene draws it larger so texture and
+// phase are readable. Scene moonlight follows the disc, but only half of
+// the excess over the physical size counts (see CONFIG.moon.lightGain).
+var MOON_CFG = (window.CONFIG && window.CONFIG.moon) || {};
+var MOON_PHYS_R = 0.0046;
+var MOON_ANG_R = (typeof MOON_CFG.angularRadius === 'number')
+ ? Math.max(MOON_PHYS_R, MOON_CFG.angularRadius) : MOON_PHYS_R * 8;
+var MOON_LIGHT_GAIN = (typeof MOON_CFG.lightGain === 'number')
+ ? Math.max(0, MOON_CFG.lightGain) : 1 + 0.5 * (MOON_ANG_R / MOON_PHYS_R - 1);
+var MOON_EARTHSHINE = (typeof MOON_CFG.earthshine === 'number') ? Math.max(0, MOON_CFG.earthshine) : 0.10;
+var MOON_LIMB_DARK = (typeof MOON_CFG.limbDarkening === 'number') ? Math.max(0, Math.min(0.9, MOON_CFG.limbDarkening)) : 0.30;
+var MOON_BRIGHT = (typeof MOON_CFG.brightness === 'number') ? Math.max(0, MOON_CFG.brightness) : 1.25;
+var MOON_HALO = (typeof MOON_CFG.halo === 'number') ? Math.max(0, MOON_CFG.halo) : 1.0;
+var MOON_FLIP_U = MOON_CFG.flipU ? 1 : 0;
+var MOON_FLIP_V = MOON_CFG.flipV ? 1 : 0;
+// The real sun is 0.00465 rad. Scaling the disc changes its size only -
+// the scene lighting (_beam/_beamL) is untouched.
+var SUN_CFG = (window.CONFIG && window.CONFIG.sun) || {};
+var SUN_ANG_R = (typeof SUN_CFG.angularRadius === 'number')
+ ? Math.max(0.00465, SUN_CFG.angularRadius) : 0.00465 * 12;
+var MOON_TEX_SRC = (typeof window.__moonTex === 'string' && window.__moonTex)
+ ? window.__moonTex
+ : ((typeof MOON_CFG.texture === 'string' && MOON_CFG.texture) ? MOON_CFG.texture : 'vendor/moon.jpg');
var ATMO_NIGHT_FLOOR = (typeof ATMO.nightFloor === 'number') ? Math.max(0.01, ATMO.nightFloor) : 0.05;
var ATMO_ALBEDO_GAIN = (typeof ATMO.albedoGain === 'number') ? Math.max(0.1, ATMO.albedoGain) : 1.0;
var ATMO_SKY_GAIN = (typeof ATMO.skyGain === 'number') ? Math.max(0.05, ATMO.skyGain) : 0.55;
@@ -1976,7 +2072,7 @@ function airlightColor(altRad, moonAlt, moonFrac) {
var beam = beamColor(altRad);
var kDay = ATMO_AIR_GAIN * skyAmbient(altRad) * beamLuma(altRad);
var up = Math.max(0, Math.sin(moonAlt || 0));
- var mDiff = ATMO_MOON_GAIN * (moonFrac === undefined ? 0.6 : moonFrac) * up * 0.9;
+ var mDiff = ATMO_MOON_GAIN * MOON_LIGHT_GAIN * (moonFrac === undefined ? 0.6 : moonFrac) * up * 0.9;
var mBeam = beamColor(moonAlt || 0);
var kMoon = ATMO_AIR_GAIN * mDiff * beamLuma(moonAlt || 0);
return [RAYLEIGH[0] * (beam[0] * kDay + mBeam[0] * kMoon),
@@ -4551,9 +4647,12 @@ var SKYPTS_VERT = [
'uniform vec3 uPole;',
'uniform float uLon;', // sidereal angle
'uniform float uSizeScale;',
+ 'uniform vec3 uMoonDir;',
+ 'uniform float uMoonR;',
'varying vec4 vC;',
'varying float vAlt;',
'varying float vSeed;',
+ 'varying float vMoon;',
'void main() {',
'vec3 d = aSd.xyz;',
// stable per-star seed: derived from the star's own direction, so it
@@ -4563,6 +4662,7 @@ var SKYPTS_VERT = [
'float c = cos(uLon), s = sin(uLon);',
'vec3 rp = d * c + cross(uPole, d) * s + uPole * dot(uPole, d) * (1.0 - c);',
'vAlt = rp.y;',
+ 'vMoon = smoothstep(uMoonR * 0.92, uMoonR * 1.35, length(rp - uMoonDir));',
// finite radius (inside the far plane): w=0 would put z_ndc just
// outside [-1,1] and clip every single point away
'gl_Position = uMVP * vec4(rp * 88.0, 1.0);',
@@ -4579,6 +4679,7 @@ var SKYPTS_FRAG = [
'varying vec4 vC;',
'varying float vAlt;',
'varying float vSeed;',
+ 'varying float vMoon;',
'void main() {',
'vec2 pc = gl_PointCoord - 0.5;',
'float d = length(pc) * 2.0;',
@@ -4590,7 +4691,7 @@ var SKYPTS_FRAG = [
'float altf = smoothstep(-0.02, 0.12, vAlt);',
// vC.w = per-star brightness (mag-based, floored so faint stars
// stay clearly visible), giving an even overall look
- 'float a = uNight * altf * twk * vC.w;',
+ 'float a = uNight * altf * twk * vC.w * vMoon;',
'gl_FragColor = vec4(vC.rgb * g * a * uBright, 1.0);',
'}'
].join('\n');
@@ -4703,6 +4804,8 @@ function buildSkyTexture(stars) {
pole: gl.getUniformLocation(prog, 'uPole'),
lon: gl.getUniformLocation(prog, 'uLon'),
sizeScale: gl.getUniformLocation(prog, 'uSizeScale'),
+ moonDir: gl.getUniformLocation(prog, 'uMoonDir'),
+ moonR: gl.getUniformLocation(prog, 'uMoonR'),
night: gl.getUniformLocation(prog, 'uNight'),
time: gl.getUniformLocation(prog, 'uTime'),
bright: gl.getUniformLocation(prog, 'uBright')
@@ -4803,6 +4906,29 @@ if (window.CONFIG && window.CONFIG.clouds && window.CONFIG.clouds.density) {
}
}
+var moonTexture = null, moonTexFailed = false;
+function loadMoonTexture(gl) {
+ var img = new Image();
+ img.onload = function () {
+ moonTexture = gl.createTexture();
+ // flip so v = 0 is the southern edge: the map is north-up
+ gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, true);
+ gl.bindTexture(gl.TEXTURE_2D, moonTexture);
+ gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGB, gl.RGB, gl.UNSIGNED_BYTE, img);
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.REPEAT);
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
+ gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, false);
+ dbgLog('[clouds3d] moon map ready (' + img.width + 'x' + img.height + ')');
+ };
+ img.onerror = function () {
+ moonTexFailed = true;
+ console.warn('[clouds3d] moon map failed to load - flat disc instead');
+ };
+ img.src = MOON_TEX_SRC;
+}
+
window.clouds3dRender = function(target) {
if (!target || typeof target !== 'object') {
console.error('clouds3d: invalid target');
@@ -4890,6 +5016,22 @@ window.clouds3dRender = function(target) {
gl.bindTexture(gl.TEXTURE_2D, skyTexture || s.blackTex);
gl.uniform1i(s.uSkyTex, 4);
gl.uniform1f(s.uSkyOn, skyTexture ? 1.0 : 0.0);
+ // Lunar map on texture unit 6. Until it is loaded the shader falls back
+ // to a flat grey disc (uMoonTexOn = 0), so a missing asset never blanks
+ // the moon.
+ if (!moonTexture && !moonTexFailed) loadMoonTexture(gl);
+ gl.activeTexture(gl.TEXTURE6);
+ gl.bindTexture(gl.TEXTURE_2D, moonTexture || s.blackTex);
+ gl.uniform1i(s.uMoonTex, 6);
+ gl.uniform1f(s.uMoonTexOn, moonTexture ? 1.0 : 0.0);
+ gl.uniform1f(s.uMoonR, MOON_ANG_R);
+ gl.uniform1f(s.uMoonEarth, MOON_EARTHSHINE);
+ gl.uniform1f(s.uMoonLimb, MOON_LIMB_DARK);
+ gl.uniform1f(s.uMoonBright, MOON_BRIGHT);
+ gl.uniform1f(s.uMoonHalo, MOON_HALO);
+ gl.uniform1f(s.uMoonFlipU, MOON_FLIP_U);
+ gl.uniform1f(s.uMoonFlipV, MOON_FLIP_V);
+ gl.uniform1f(s.uSunR, SUN_ANG_R);
// texture path: RA alpha must land at world angle alpha + PI/2 - LST,
// which with u = fract(0.5 + (H + uStarLon)/2PI) means uStarLon = LST - 3PI/2
var _sidTex = starSidereal();
@@ -4943,12 +5085,13 @@ window.clouds3dRender = function(target) {
var _star = starAmbient(sunAlt);
var _mBeam = beamColor(_moonAlt), _mL = beamLuma(_moonAlt);
var _moonUp = Math.max(0, Math.sin(_moonAlt));
- var _moonI = ATMO_MOON_GAIN * _moonFrac * _moonUp * _mL;
+ var _moonI = ATMO_MOON_GAIN * MOON_LIGHT_GAIN * _moonFrac * _moonUp * _mL;
// diffuse moonlight: the whole sky gets brighter with the phase, not
// only the side the moon happens to point at
- var _moonDiff = ATMO_MOON_GAIN * _moonFrac * _moonUp * 0.9;
+ var _moonDiff = ATMO_MOON_GAIN * MOON_LIGHT_GAIN * _moonFrac * _moonUp * 0.9;
var _amb = skyAmbient(sunAlt) + _moonDiff;
var _air = airlightColor(sunAlt, _moonAlt, _moonFrac);
+ gl.uniform1f(s.uMoonFrac, _moonFrac);
// Theme tint on top of the physics (see themeLighting above). The
// exposure factor ramps from gain (day) to nightGain (night) with the
// same altitude ramp the terrain uses for its night cross-fade.
@@ -5054,6 +5197,33 @@ window.clouds3dRender = function(target) {
if (camY < CAM_MIN_Y) camY = CAM_MIN_Y;
else if (camY > CAM_MAX_Y) camY = CAM_MAX_Y;
var pitch = BOB_ON ? 0.45 - BOB_TILT * Math.cos(bob) : 0.45;
+ // ---- aim override: look at the moon, fly the same path -------------
+ // __currentMoonDir is a unit vector in the same world frame the view
+ // matrix uses, so the heading is just its horizontal part and the pitch
+ // is its altitude (negative pitch = above the horizon). Below the
+ // horizon the view holds the moon's azimuth at the horizon line instead
+ // of snapping back to the flight heading.
+ if (CAM_AIM_MOON) {
+ var _md = window.__currentMoonDir;
+ if (_md) {
+ var _hlen = Math.hypot(_md[0], _md[2]);
+ if (_hlen > 1e-4) {
+ var _alt = Math.asin(Math.max(-1, Math.min(1, _md[1])));
+ if (_alt < 0.02) _alt = 0.02;
+ else if (_alt > 1.40) _alt = 1.40;
+ var _tx = _md[0] / _hlen, _tz = _md[2] / _hlen, _tp = -_alt;
+ if (!aimReady) { aimHdX = _tx; aimHdZ = _tz; aimPitch = _tp; aimReady = true; }
+ var _k = 1 - CAM_AIM_EASE;
+ aimHdX += (_tx - aimHdX) * _k;
+ aimHdZ += (_tz - aimHdZ) * _k;
+ aimPitch += (_tp - aimPitch) * _k;
+ var _al = Math.hypot(aimHdX, aimHdZ) || 1;
+ hdX = aimHdX / _al;
+ hdZ = aimHdZ / _al;
+ pitch = aimPitch - (BOB_ON ? BOB_TILT * Math.cos(bob) * CAM_AIM_BOB : 0);
+ }
+ }
+ }
var aspect = canvas.width / canvas.height;
// The original cloud shader used a non-standard (wider) vertical fov
// so the top of the screen looked above the horizon (rd.y ~ 0.2 at
@@ -5139,6 +5309,9 @@ window.clouds3dRender = function(target) {
? -((window.__currentDayProgress || 0) * 6.2831853)
: 1.5707963267948966 - _sidPts);
gl.uniform1f(skyPts.u.sizeScale, canvas.height / 900);
+ var _mpt = window.__currentMoonDir || [0, 1, 0];
+ gl.uniform3f(skyPts.u.moonDir, _mpt[0], _mpt[1], _mpt[2]);
+ gl.uniform1f(skyPts.u.moonR, MOON_ANG_R);
gl.uniform1f(skyPts.u.night, starNight);
gl.uniform1f(skyPts.u.time, now);
gl.uniform1f(skyPts.u.bright, STAR_BRIGHT);
diff --git a/vendor/moon.jpg b/vendor/moon.jpg
new file mode 100644
index 0000000..18719b3
Binary files /dev/null and b/vendor/moon.jpg differ