moon: real lunar map (CC BY 4.0) with correct near-side geometry - the old normal sign lit the far side, so the disc came out black; stars are now occluded by the disc, sun and moon discs drawn at 12x physical without changing surface brightness; build.py inlines the map as a data URI

This commit is contained in:
Richard Zink committed 2026-10-11 22:46:35 +02:00
1 parent afc779e27e
commit ee56f2a1c8
4 files changed
+215 -25

No files matched your search

+14 -5
View File
@@ -11,6 +11,7 @@ What gets inlined:
- vendor/suncalc.js, vendor/plane3d.js, - vendor/suncalc.js, vendor/plane3d.js,
vendor/clouds3d.js (plain <script> swap) vendor/clouds3d.js (plain <script> swap)
- vendor/stars.json (window.__starsData) - vendor/stars.json (window.__starsData)
- vendor/moon.jpg (window.__moonTex, data URI)
- every stylesheet referenced by window.THEMES - every stylesheet referenced by window.THEMES
(window.__inlineThemeCss, so (window.__inlineThemeCss, so
theme switching works offline) theme switching works offline)
@@ -24,6 +25,7 @@ Overpass cache, which is the gentlest way to hit those upstreams.
The build fails loudly if any external script or stylesheet reference survives. The build fails loudly if any external script or stylesheet reference survives.
""" """
import base64
import json import json
import os import os
import re import re
@@ -128,12 +130,18 @@ def main():
# embedded data preamble (must run before the scene scripts) # embedded data preamble (must run before the scene scripts)
stars = json.loads(read('vendor/stars.json')) stars = json.loads(read('vendor/stars.json'))
theme_css = {name: read(fname) for name, fname in themes.items()} theme_css = {name: read(fname) for name, fname in themes.items()}
moon_path = os.path.join(ROOT, 'vendor', 'moon.jpg')
moon_uri = ''
if os.path.exists(moon_path):
with open(moon_path, 'rb') as f:
moon_uri = 'data:image/jpeg;base64,' + base64.b64encode(f.read()).decode('ascii')
embed = ( embed = (
'<script>\n' '<script>\n'
'window.__starsData = %s;\n' 'window.__starsData = %s;\n'
'window.__moonTex = "%s";\n'
'window.__inlineThemeCss = %s;\n' 'window.__inlineThemeCss = %s;\n'
'</script>\n' '</script>\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 # must come before config.js so the early theme loader sees the CSS
anchor = '<script src="config.js"></script>' anchor = '<script src="config.js"></script>'
@@ -158,8 +166,8 @@ def main():
with tempfile.NamedTemporaryFile('w', suffix='.html', delete=False) as f: with tempfile.NamedTemporaryFile('w', suffix='.html', delete=False) as f:
f.write(html) f.write(html)
tmp = f.name tmp = f.name
print('OK: self-contained (%d bytes, %d themes, %d stars)' print('OK: self-contained (%d bytes, %d themes, %d stars, moon map %d KB)'
% (len(html), len(themes), len(stars))) % (len(html), len(themes), len(stars), len(moon_uri) // 1024))
os.unlink(tmp) os.unlink(tmp)
return return
@@ -167,8 +175,9 @@ def main():
f.write(html) f.write(html)
print('Wrote %s (%d bytes)' % (out, len(html))) print('Wrote %s (%d bytes)' % (out, len(html)))
print('Inlined: %s, stars.json (%d entries), themes: %s' print('Inlined: %s, stars.json (%d entries), moon.jpg (%d KB), themes: %s'
% (', '.join(INLINED_SCRIPTS), len(stars), ', '.join(sorted(themes)))) % (', '.join(INLINED_SCRIPTS), len(stars), len(moon_uri) // 1024,
', '.join(sorted(themes))))
if __name__ == '__main__': if __name__ == '__main__':
+8
View File
@@ -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 (5th ed., public catalogue data). Generated for this project; see the
real-sky notes in clouds3d.js. 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) plane3d.js - low-poly airliner mesh (480 triangles, flat shaded, no textures)
Original geometry generated by tools/plane_gen.py in this repository; offered 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. under CC0 1.0 like the rest of the project's own assets.
+193 -20
View File
@@ -97,6 +97,17 @@ var FRAG_SRC = [
'uniform vec3 uCloudAlbedo;', // theme-tinted but brightness-normalised 'uniform vec3 uCloudAlbedo;', // theme-tinted but brightness-normalised
'uniform float uAirDensity;', // slant optical depth per world unit 'uniform float uAirDensity;', // slant optical depth per world unit
'uniform float uCloudHaze;', // clouds sit above most of the haze: much weaker '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 // 3D hash and value noise
'float hash3(vec3 p) {', 'float hash3(vec3 p) {',
'p = fract(p * vec3(0.1031, 0.1030, 0.0973));', 'p = fract(p * vec3(0.1031, 0.1030, 0.0973));',
@@ -224,6 +235,11 @@ var FRAG_SRC = [
'vec3 col = mix(uBg, uSky, skyT);', 'vec3 col = mix(uBg, uSky, skyT);',
'float nightFactor = 1.0 - smoothstep(-0.15, 0.05, uSunAlt);', 'float nightFactor = 1.0 - smoothstep(-0.15, 0.05, uSunAlt);',
'float skyMask = smoothstep(-0.20, 0.40, rd.y);', '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 // night darkening of the gradient BEFORE star emission is added
'col *= (1.0 - nightFactor * 0.55);', 'col *= (1.0 - nightFactor * 0.55);',
// ---- real star catalog (Yale BSC5 baked into a texture) ---- // ---- 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 // stars only once the sky is actually dark: the dusk palette
// is rust/blood, and additive white stars on a red sky read // is rust/blood, and additive white stars on a red sky read
// as red stars // as red stars
'col += st * starVis * skyMask * twk * 1.45;', 'col += st * starVis * skyMask * twk * 1.45 * moonOcc;',
'}', '}',
// ---- Sun disc ------------------------------------------------ // ---- Sun disc ------------------------------------------------
// angular radius ~0.27 deg -> chord length ~0.0047 // angular radius ~0.27 deg -> chord length ~0.0047
'if (uSunAlt > -0.03) {', 'if (uSunAlt > -0.03) {',
'vec3 qs = rd - uSunDir;', 'vec3 qs = rd - uSunDir;',
'float angS = length(qs);', 'float angS = length(qs);',
'float discS = 1.0 - smoothstep(0.0040, 0.0050, angS);', // The disc is drawn at uSunR (CONFIG.sun.angularRadius, 12x
'float haloS = exp(-angS * 55.0) * 0.30 + exp(-angS * 14.0) * 0.12;', // 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);', 'vec3 sunCol = mix(uLight, vec3(1.0, 0.92, 0.72), 0.35);',
'col += sunCol * (discS * 1.15 + haloS) * (1.0 - nightFactor * 0.85);', 'col += sunCol * (discS * 1.15 + haloS) * (1.0 - nightFactor * 0.85);',
'}', '}',
// ---- Moon disc with real terminator --------------------------- // ---- Moon disc: lunar map + geometric terminator --------------
// lit side = dot(surface normal, sun dir) -> crescent/gibbous // The near side always faces the observer, so the map is sampled
// falls out of geometry, phase tracks __currentMoonDir. // in a selenographic frame built from the north celestial pole:
'if (uMoonDir.y > -0.01) {', // 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;', 'vec3 qm = rd - uMoonDir;',
'float angM = length(qm);', 'float angM = length(qm);',
'if (angM < 0.02) {', 'if (angM < MR) {',
'float z = sqrt(max(0.0, 0.0046 * 0.0046 - angM * angM));', 'float rr = angM / MR;',
'vec3 nM = normalize(qm + uMoonDir * (z + 0.00001));', '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 litM = dot(nM, uSunDir);',
'float phase = smoothstep(-0.08, 0.12, litM);', 'float phase = smoothstep(-0.05, 0.12, litM);',
'float discM = 1.0 - smoothstep(0.0041, 0.0046, angM);', 'float lam = max(0.0, litM);',
'vec3 moonCol = mix(vec3(0.62, 0.64, 0.68), vec3(0.97, 0.95, 0.88), phase);', 'float limb = 1.0 - uMoonLimb * rr * rr * rr;',
'float haloM = exp(-angM * 60.0) * 0.10 * (0.25 + 0.75 * phase);', 'float shade = lam * lam * 0.85 + lam * 0.18;',
'col += (moonCol * discM * phase + vec3(0.75, 0.78, 0.85) * haloM) * 1.25;', '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 // faint procedural fill for stars below the catalog limit
'float stars = starField(rd) * skyMask * nightFactor;', 'float stars = starField(rd) * skyMask * nightFactor;',
'col += stars * uLight * 0.85;', 'col += stars * uLight * 0.85 * moonOcc;',
// ---- Horizon airlight ------------------------------------- // ---- Horizon airlight -------------------------------------
// At and below the horizon the sky shows the same scattered // At and below the horizon the sky shows the same scattered
// column the terrain fades into. Without it the outer rim of // 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 uSkyOn = gl.getUniformLocation(prog, 'uSkyOn');
var uStarLon = gl.getUniformLocation(prog, 'uStarLon'); var uStarLon = gl.getUniformLocation(prog, 'uStarLon');
var uPole = gl.getUniformLocation(prog, 'uPole'); 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 uSunColor = gl.getUniformLocation(prog, 'uSunColor');
var uSunInt = gl.getUniformLocation(prog, 'uSunInt'); var uSunInt = gl.getUniformLocation(prog, 'uSunInt');
var uMoonColor = gl.getUniformLocation(prog, 'uMoonColor'); var uMoonColor = gl.getUniformLocation(prog, 'uMoonColor');
@@ -801,6 +858,10 @@ function init(canvas) {
uPhysical: uPhysical, uAirColor: uAirColor, uCloudAlbedo: uCloudAlbedo, uPhysical: uPhysical, uAirColor: uAirColor, uCloudAlbedo: uCloudAlbedo,
uAirDensity: uAirDensity, uCloudHaze: uCloudHaze, uAirDensity: uAirDensity, uCloudHaze: uCloudHaze,
uSkyTex: uSkyTex, uSkyOn: uSkyOn, uStarLon: uStarLon, uPole: uPole, 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 program
terrain: { terrain: {
prog: terrainProg, 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_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_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 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 // CONFIG.clouds = { top: ..., bottom: ... } overrides the defaults; the
// deck must stay below the camera and keep a minimum thickness. // deck must stay below the camera and keep a minimum thickness.
var CLOUD_TOP = 0.9, CLOUD_BOT = -0.3; 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 = (window.CONFIG && window.CONFIG.atmosphere) || {};
var ATMO_TURBIDITY = (typeof ATMO.turbidity === 'number') ? Math.max(0.2, ATMO.turbidity) : 1.0; 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; 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_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_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; 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 beam = beamColor(altRad);
var kDay = ATMO_AIR_GAIN * skyAmbient(altRad) * beamLuma(altRad); var kDay = ATMO_AIR_GAIN * skyAmbient(altRad) * beamLuma(altRad);
var up = Math.max(0, Math.sin(moonAlt || 0)); 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 mBeam = beamColor(moonAlt || 0);
var kMoon = ATMO_AIR_GAIN * mDiff * beamLuma(moonAlt || 0); var kMoon = ATMO_AIR_GAIN * mDiff * beamLuma(moonAlt || 0);
return [RAYLEIGH[0] * (beam[0] * kDay + mBeam[0] * kMoon), return [RAYLEIGH[0] * (beam[0] * kDay + mBeam[0] * kMoon),
@@ -4551,9 +4647,12 @@ var SKYPTS_VERT = [
'uniform vec3 uPole;', 'uniform vec3 uPole;',
'uniform float uLon;', // sidereal angle 'uniform float uLon;', // sidereal angle
'uniform float uSizeScale;', 'uniform float uSizeScale;',
'uniform vec3 uMoonDir;',
'uniform float uMoonR;',
'varying vec4 vC;', 'varying vec4 vC;',
'varying float vAlt;', 'varying float vAlt;',
'varying float vSeed;', 'varying float vSeed;',
'varying float vMoon;',
'void main() {', 'void main() {',
'vec3 d = aSd.xyz;', 'vec3 d = aSd.xyz;',
// stable per-star seed: derived from the star's own direction, so it // 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);', 'float c = cos(uLon), s = sin(uLon);',
'vec3 rp = d * c + cross(uPole, d) * s + uPole * dot(uPole, d) * (1.0 - c);', 'vec3 rp = d * c + cross(uPole, d) * s + uPole * dot(uPole, d) * (1.0 - c);',
'vAlt = rp.y;', '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 // finite radius (inside the far plane): w=0 would put z_ndc just
// outside [-1,1] and clip every single point away // outside [-1,1] and clip every single point away
'gl_Position = uMVP * vec4(rp * 88.0, 1.0);', 'gl_Position = uMVP * vec4(rp * 88.0, 1.0);',
@@ -4579,6 +4679,7 @@ var SKYPTS_FRAG = [
'varying vec4 vC;', 'varying vec4 vC;',
'varying float vAlt;', 'varying float vAlt;',
'varying float vSeed;', 'varying float vSeed;',
'varying float vMoon;',
'void main() {', 'void main() {',
'vec2 pc = gl_PointCoord - 0.5;', 'vec2 pc = gl_PointCoord - 0.5;',
'float d = length(pc) * 2.0;', 'float d = length(pc) * 2.0;',
@@ -4590,7 +4691,7 @@ var SKYPTS_FRAG = [
'float altf = smoothstep(-0.02, 0.12, vAlt);', 'float altf = smoothstep(-0.02, 0.12, vAlt);',
// vC.w = per-star brightness (mag-based, floored so faint stars // vC.w = per-star brightness (mag-based, floored so faint stars
// stay clearly visible), giving an even overall look // 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);', 'gl_FragColor = vec4(vC.rgb * g * a * uBright, 1.0);',
'}' '}'
].join('\n'); ].join('\n');
@@ -4703,6 +4804,8 @@ function buildSkyTexture(stars) {
pole: gl.getUniformLocation(prog, 'uPole'), pole: gl.getUniformLocation(prog, 'uPole'),
lon: gl.getUniformLocation(prog, 'uLon'), lon: gl.getUniformLocation(prog, 'uLon'),
sizeScale: gl.getUniformLocation(prog, 'uSizeScale'), sizeScale: gl.getUniformLocation(prog, 'uSizeScale'),
moonDir: gl.getUniformLocation(prog, 'uMoonDir'),
moonR: gl.getUniformLocation(prog, 'uMoonR'),
night: gl.getUniformLocation(prog, 'uNight'), night: gl.getUniformLocation(prog, 'uNight'),
time: gl.getUniformLocation(prog, 'uTime'), time: gl.getUniformLocation(prog, 'uTime'),
bright: gl.getUniformLocation(prog, 'uBright') 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) { window.clouds3dRender = function(target) {
if (!target || typeof target !== 'object') { if (!target || typeof target !== 'object') {
console.error('clouds3d: invalid target'); console.error('clouds3d: invalid target');
@@ -4890,6 +5016,22 @@ window.clouds3dRender = function(target) {
gl.bindTexture(gl.TEXTURE_2D, skyTexture || s.blackTex); gl.bindTexture(gl.TEXTURE_2D, skyTexture || s.blackTex);
gl.uniform1i(s.uSkyTex, 4); gl.uniform1i(s.uSkyTex, 4);
gl.uniform1f(s.uSkyOn, skyTexture ? 1.0 : 0.0); 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, // 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 // which with u = fract(0.5 + (H + uStarLon)/2PI) means uStarLon = LST - 3PI/2
var _sidTex = starSidereal(); var _sidTex = starSidereal();
@@ -4943,12 +5085,13 @@ window.clouds3dRender = function(target) {
var _star = starAmbient(sunAlt); var _star = starAmbient(sunAlt);
var _mBeam = beamColor(_moonAlt), _mL = beamLuma(_moonAlt); var _mBeam = beamColor(_moonAlt), _mL = beamLuma(_moonAlt);
var _moonUp = Math.max(0, Math.sin(_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 // diffuse moonlight: the whole sky gets brighter with the phase, not
// only the side the moon happens to point at // 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 _amb = skyAmbient(sunAlt) + _moonDiff;
var _air = airlightColor(sunAlt, _moonAlt, _moonFrac); var _air = airlightColor(sunAlt, _moonAlt, _moonFrac);
gl.uniform1f(s.uMoonFrac, _moonFrac);
// Theme tint on top of the physics (see themeLighting above). The // Theme tint on top of the physics (see themeLighting above). The
// exposure factor ramps from gain (day) to nightGain (night) with the // exposure factor ramps from gain (day) to nightGain (night) with the
// same altitude ramp the terrain uses for its night cross-fade. // 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; if (camY < CAM_MIN_Y) camY = CAM_MIN_Y;
else if (camY > CAM_MAX_Y) camY = CAM_MAX_Y; else if (camY > CAM_MAX_Y) camY = CAM_MAX_Y;
var pitch = BOB_ON ? 0.45 - BOB_TILT * Math.cos(bob) : 0.45; 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; var aspect = canvas.width / canvas.height;
// The original cloud shader used a non-standard (wider) vertical fov // 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 // 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) ? -((window.__currentDayProgress || 0) * 6.2831853)
: 1.5707963267948966 - _sidPts); : 1.5707963267948966 - _sidPts);
gl.uniform1f(skyPts.u.sizeScale, canvas.height / 900); 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.night, starNight);
gl.uniform1f(skyPts.u.time, now); gl.uniform1f(skyPts.u.time, now);
gl.uniform1f(skyPts.u.bright, STAR_BRIGHT); gl.uniform1f(skyPts.u.bright, STAR_BRIGHT);
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 169 KiB