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