// Fellwing sky and air: one GLSL atmosphere shared by the sky dome, the terrain's aerial perspective, the sea and the // clouds, so far mountains fade into exactly the sky behind them. Analytic single-scattering look (Rayleigh blue, // Mie haze and sun glow, sunset reddening by air mass), Earth curvature for the horizon, and a time-of-day table. // (c) 2026 CyberMax. All rights reserved. export const ATMOS_GLSL = /* glsl */` uniform vec3 uSun; // unit vector to the sun (world) uniform float uHaze; // 0 = crystal clear, 1 = thick haze uniform float uOvercast; // 0..1 grey sky uniform float uNight; // 0 day .. 1 night uniform vec3 uSunCol; // sun colour * intensity (linear) const float R_EARTH = 6378137.0; vec3 skyColor(vec3 dir) { float sunH = uSun.y; float h = max(dir.y, -0.02); float mu = dot(dir, uSun); // air mass along the view (more air near the horizon) float am = 1.0 / (max(h, 0.0) + 0.05); vec3 rayleigh = vec3(0.18, 0.42, 1.0); vec3 zenith = mix(vec3(0.10, 0.32, 0.78), vec3(0.20, 0.44, 0.86), uHaze * 0.5); vec3 horizon = mix(vec3(0.66, 0.78, 0.92), vec3(0.80, 0.84, 0.88), uHaze); vec3 col = mix(horizon, zenith, pow(clamp(h, 0.0, 1.0), 0.45)); // sunset: horizon and the half of the sky around the sun go warm when the sun is low float low = 1.0 - smoothstep(-0.06, 0.32, sunH); vec3 warm = vec3(1.0, 0.48, 0.22); float side = 0.5 + 0.5 * mu; col = mix(col, mix(vec3(0.95, 0.62, 0.45), warm, side) * (0.55 + 0.45 * side), low * (1.0 - smoothstep(0.0, 0.5, h)) * 0.85); col = mix(col, col * vec3(0.55, 0.62, 0.95), low * smoothstep(0.1, 0.7, h) * 0.5); // Mie: haze glow around the sun float mie = pow(max(mu, 0.0), 8.0) * 0.35 + pow(max(mu, 0.0), 64.0) * 0.6; col += uSunCol * mie * (0.25 + uHaze * 0.6) * (1.0 - uOvercast * 0.8); // daylight level from the sun's height float day = smoothstep(-0.12, 0.18, sunH); col *= mix(0.04, 1.0, day); // overcast: flat grey that still brightens a little toward the sun vec3 grey = vec3(0.62, 0.65, 0.70) * mix(0.05, 1.0, day) * (0.85 + 0.15 * max(mu, 0.0)); col = mix(col, grey, uOvercast); // night: deep blue col = mix(col, vec3(0.006, 0.010, 0.026) + vec3(0.02, 0.03, 0.06) * pow(1.0 - h, 3.0), uNight); return col; } // Aerial perspective: blend a lit surface colour toward the sky along the view ray, with air density falling with height. vec3 aerial(vec3 col, vec3 camPos, vec3 wpos, out float fogAmt) { vec3 d = wpos - camPos; float dist = length(d); vec3 dir = d / max(dist, 1.0); float hAvg = max(0.0, 0.5 * (camPos.y + wpos.y)); float dens = exp(-hAvg / 7000.0); float beta = mix(1.0 / 95000.0, 1.0 / 16000.0, uHaze) * (1.0 + uOvercast * 0.8); fogAmt = 1.0 - exp(-dist * beta * dens); vec3 sky = skyColor(normalize(vec3(dir.x, max(dir.y, 0.0) * 0.6, dir.z))); // sunlight scattered toward the eye (in-scatter) warms the haze toward the sun float mu = max(dot(dir, uSun), 0.0); sky += uSunCol * pow(mu, 6.0) * 0.18 * (1.0 - uOvercast); // the haze is a little bluer and darker than the bright horizon itself return mix(col, sky * vec3(0.92, 0.96, 1.02), fogAmt); } // Earth curvature relative to the camera (visual only): far terrain drops below the horizon like the real thing. float curveDrop(vec3 wpos, vec3 camPos) { vec2 d = wpos.xz - camPos.xz; return dot(d, d) / (2.0 * R_EARTH); } `; /** * Times of day: sun elevation and azimuth (deg, azimuth 0 = north), sun colour/intensity, ambient, night amount. * 'day' is free; the rest come with the Fun Pass. */ export const TIMES = { day: { name: 'Midday', el: 38, az: 150, sun: [1.0, 0.97, 0.92], i: 3.0, amb: 1.0, night: 0, free: true }, morning: { name: 'Morning', el: 22, az: 110, sun: [1.0, 0.9, 0.78], i: 2.6, amb: 0.85, night: 0 }, golden: { name: 'Golden hour', el: 7, az: 255, sun: [1.0, 0.68, 0.40], i: 2.3, amb: 0.6, night: 0 }, dusk: { name: 'Dusk', el: -2.5, az: 275, sun: [1.0, 0.45, 0.25], i: 1.0, amb: 0.32, night: 0.25 }, night: { name: 'Night', el: -18, az: 300, sun: [0.55, 0.62, 0.85], i: 0.22, amb: 0.12, night: 1 }, live: { name: 'Live (local time)', el: 30, az: 180, sun: [1.0, 0.97, 0.92], i: 3.0, amb: 1.0, night: 0, live: true }, }; /** Light for any sun elevation: interpolates the presets above by elevation (so a real sun gets matching colours). */ export function lightFor(el) { const K = [TIMES.night, TIMES.dusk, TIMES.golden, TIMES.morning, TIMES.day]; if (el <= K[0].el) return K[0]; if (el >= K[4].el) return { ...K[4], el }; let i = 0; while (el > K[i + 1].el) i++; const a = K[i], b = K[i + 1], t = (el - a.el) / (b.el - a.el), m = (x, y) => x + (y - x) * t; return { el, sun: a.sun.map((v, j) => m(v, b.sun[j])), i: m(a.i, b.i), amb: m(a.amb, b.amb), night: m(a.night, b.night) }; } /** Weather presets. 'fair' is free. wind = [from deg, knots], turb 0..1. */ export const WEATHER = { fair: { name: 'Fair', haze: 0.08, overcast: 0, clouds: 0.32, cloudAlt: 2600, wind: [270, 6], turb: 0.05, free: true }, clear: { name: 'Crystal clear', haze: 0.0, overcast: 0, clouds: 0, cloudAlt: 3000, wind: [0, 0], turb: 0 }, hazy: { name: 'Summer haze', haze: 0.7, overcast: 0, clouds: 0.18, cloudAlt: 3200, wind: [200, 8], turb: 0.12 }, overcast: { name: 'Overcast', haze: 0.45, overcast: 0.85, clouds: 0.92, cloudAlt: 1900, wind: [240, 14], turb: 0.3 }, windy: { name: 'Strong wind', haze: 0.25, overcast: 0.15, clouds: 0.45, cloudAlt: 2400, wind: [300, 28], turb: 0.65 }, }; /** * The real sun for a place and moment (NOAA low-precision solar position; better than 0.5 deg until 2100). * Returns { el, az } in degrees (azimuth 0 = north, clockwise). date: a JS Date (UTC instant). */ export function sunPosition(lat, lon, date) { const D = Math.PI / 180; const jd = date.getTime() / 864e5 + 2440587.5, n = jd - 2451545.0; const L = (280.46 + 0.9856474 * n) % 360, g = ((357.528 + 0.9856003 * n) % 360) * D; const lam = (L + 1.915 * Math.sin(g) + 0.02 * Math.sin(2 * g)) * D, eps = (23.439 - 0.0000004 * n) * D; const ra = Math.atan2(Math.cos(eps) * Math.sin(lam), Math.cos(lam)), dec = Math.asin(Math.sin(eps) * Math.sin(lam)); const gmst = (18.697374558 + 24.06570982441908 * n) % 24; const ha = ((gmst * 15 + lon) * D - ra); const la = lat * D; const el = Math.asin(Math.sin(la) * Math.sin(dec) + Math.cos(la) * Math.cos(dec) * Math.cos(ha)); const az = Math.atan2(-Math.sin(ha), Math.tan(dec) * Math.cos(la) - Math.sin(la) * Math.cos(ha)); return { el: el / D, az: ((az / D) + 360) % 360 }; } /** Today's UTC instant at a local mean solar time (hours) for a longitude. */ export function solarTime(lon, hours, day = new Date()) { const d = Date.UTC(day.getUTCFullYear(), day.getUTCMonth(), day.getUTCDate()); return new Date(d + (hours - lon / 15) * 3600e3); } /** The sun's local solar times for each preset: noon, mid-morning, ~40 min before sunset, just after it, late night. */ export const SOLAR = { day: 12.5, morning: 8.75, golden: 'set-0.6', dusk: 'set+0.35', night: 23.5 }; /** Sunset solar hour for lat on a date (sun centre at -0.83 deg); 18 when the sun never sets or never rises. */ export function sunsetHour(lat, date = new Date()) { const D = Math.PI / 180; const doy = Math.floor((date - Date.UTC(date.getUTCFullYear(), 0, 0)) / 864e5); const dec = -23.44 * Math.cos((360 / 365) * (doy + 10) * D) * D; const c = (Math.sin(-0.83 * D) - Math.sin(lat * D) * Math.sin(dec)) / (Math.cos(lat * D) * Math.cos(dec)); if (c <= -1 || c >= 1) return 18; return 12 + Math.acos(c) / D / 15; }