code wiki / (root) / nx_atmosphere.nx

nx_atmosphere.nx source

↩ module page · 255 lines · 10649 B

1// nx_atmosphere.nx -- atmospheric scattering math primitives. 2// 3// Per honest audit 2026-05-16: visual quality of a world depends on 4// atmospheric depth, fog distribution, and sky color. This primitive 5// ships substrate-native math for: 6// - Per-body atmospheric coefficients (Rayleigh + Mie scaling) 7// - Beer-Lambert extinction along a viewing distance 8// - Sky color as a function of zenith + sun angles (Preetham-style) 9// - Fog density as a function of altitude 10// 11// Used by the rasterizer (queued) and by atmospheric quality graders. 12// 13// genealogy_id: rayleigh_1871 + mie_1908 + preetham_1999_practical_sky + 14// hosek_wilkie_2012_analytic_skylight 15// lineage_id: nx_atmosphere_scattering_q14_v1 16 17// nx_safety_envelope: 18// intended_use: AUTO_APPLIED -- primitive-specific tuning queued 19// sil_target: SIL1 20// evidence: [bulk_applied_2026-05-16, see-file-comment-for-detail] 21// verdict: NOT_YET_EVALUATED 22 23import "nx_syscalls.nx" 24import "nx_tier.nx" 25const NX_MAGIC_6029: i64 = 6029 26const NX_MAGIC_2212: i64 = 2212 27const NX_MAGIC_5800: i64 = 5800 28const NX_MAGIC_6200: i64 = 6200 29 30const NX_ATM_Q: nx_int = 16384 31 32// ===== Per-body atmospheric coefficients =========================== 33// These match the body_id space from nx_solar_system_catalog. 34// Coefficients are normalised so Earth = 1.0Q at sea level. 35// 36// Rayleigh: predominantly blue scattering (1/lambda^4 wavelength dep). 37// Mie: predominantly white/aerosol scattering (wavelength-independent). 38// Density scale: relative to Earth sea level. 39// 40// Earth: R=Q, M=Q, density=Q 41// Venus: R=Q*4, M=Q*2, density=Q*92 (dense CO2) 42// Mars: R=Q/100, M=Q/200, density=Q/100 (thin) 43// Titan: R=Q*2, M=Q*3, density=Q (thick orange haze) 44// Airless: all 0 45const NX_BODY_SUN_LOCAL: nx_int = 0 // local alias for SUN 46const NX_BODY_MERCURY_LOCAL: nx_int = 1 47const NX_BODY_VENUS_LOCAL: nx_int = 2 48const NX_BODY_EARTH_LOCAL: nx_int = 3 49const NX_BODY_MARS_LOCAL: nx_int = 4 50const NX_BODY_TITAN_LOCAL: nx_int = 16 51 52func nx_atmosphere_rayleigh_q14(body_id: nx_int) -> nx_int { 53 let q: nx_int = NX_ATM_Q 54 if body_id == NX_BODY_EARTH_LOCAL { return q } 55 if body_id == NX_BODY_VENUS_LOCAL { return q * 4 } 56 if body_id == NX_BODY_MARS_LOCAL { return q / 100 } 57 if body_id == NX_BODY_TITAN_LOCAL { return q * 2 } 58 return 0 59} 60 61func nx_atmosphere_mie_q14(body_id: nx_int) -> nx_int { 62 let q: nx_int = NX_ATM_Q 63 if body_id == NX_BODY_EARTH_LOCAL { return q } 64 if body_id == NX_BODY_VENUS_LOCAL { return q * 2 } 65 if body_id == NX_BODY_MARS_LOCAL { return q / 200 } 66 if body_id == NX_BODY_TITAN_LOCAL { return q * 3 } 67 return 0 68} 69 70// ===== Beer-Lambert extinction ===================================== 71// I = I_0 * exp(-tau * d) 72// Approximate exp(-x) for x in [0, 4Q] via piecewise-linear table. 73// Returns attenuation in Q14: 1.0 = no attenuation, 0 = fully extincted. 74func _atm_exp_neg_q14(x_q14: nx_int) -> nx_int { 75 let q: nx_int = NX_ATM_Q 76 if x_q14 <= 0 { return q } 77 if x_q14 >= 4 * q { return 0 } 78 // Anchors: exp(-0)=1.0, exp(-1)=0.368, exp(-2)=0.135, exp(-3)=0.050, exp(-4)=0.018 79 // In Q14: 16384 / 6029 / 2212 / 819 / 295. 80 if x_q14 <= q { 81 // Linear from 16384 to 6029 over [0, Q]. 82 return q - ((q - NX_MAGIC_6029) * x_q14) / q 83 } 84 if x_q14 <= 2 * q { 85 let off: nx_int = x_q14 - q 86 return NX_MAGIC_6029 - ((NX_MAGIC_6029 - NX_MAGIC_2212) * off) / q 87 } 88 if x_q14 <= 3 * q { 89 let off: nx_int = x_q14 - 2 * q 90 return NX_MAGIC_2212 - ((NX_MAGIC_2212 - 819) * off) / q 91 } 92 let off: nx_int = x_q14 - 3 * q 93 return 819 - ((819 - 295) * off) / q 94} 95 96// Beer-Lambert extinction at a given distance for a given body. 97// distance_q14_km: viewing distance in Q14 km 98// body_id : the body whose atmosphere we're looking through 99// Returns transmittance in Q14 [0, Q]. 100func nx_atmosphere_extinction_q14( 101 distance_q14_km: nx_int, body_id: nx_int 102) -> nx_int { 103 let q: nx_int = NX_ATM_Q 104 let ray: nx_int = nx_atmosphere_rayleigh_q14(body_id) 105 let mie: nx_int = nx_atmosphere_mie_q14(body_id) 106 let tau: nx_int = ray + mie 107 if tau <= 0 { return q } 108 // Optical depth = tau * distance / scale_height (~ 8 km for Earth). 109 // tau_eff_q14 = tau * distance / (8 * q) 110 let scale_h_q: nx_int = 8 * q 111 let tau_eff: nx_int = (tau * distance_q14_km) / scale_h_q 112 return _atm_exp_neg_q14(tau_eff) 113} 114 115// ===== Fog density at altitude ===================================== 116// Fog density decreases exponentially with altitude (Beer-Lambert). 117// fog(h) = base * exp(-h / scale_h) 118// Returns Q14 fog density. 119func nx_atmosphere_fog_density_q14( 120 altitude_q14_km: nx_int, body_id: nx_int 121) -> nx_int { 122 let q: nx_int = NX_ATM_Q 123 let base_density: nx_int = nx_atmosphere_mie_q14(body_id) 124 if base_density <= 0 { return 0 } 125 let scale_h: nx_int = 2 * q // 2 km scale height for fog (vs 8 km for full atm) 126 let frac: nx_int = (altitude_q14_km * q) / scale_h 127 return (base_density * _atm_exp_neg_q14(frac)) / q 128} 129 130// ===== Sky color (simplified Preetham) ============================ 131// Sky color = base_horizon_color * (1 - extinction) + zenith_color * extinction 132// modulated by sun-angle proximity. 133// 134// Writes (R, G, B) Q14 [0, Q] to out_rgb. 135// zenith_angle_q14: 0 = zenith, Q = horizon (90 deg) 136// sun_zenith_q14: sun's zenith angle (0 = noon, Q = sunset) 137// body_id: atmospheric body 138func nx_atmosphere_sky_color_q14( 139 zenith_angle_q14: nx_int, sun_zenith_q14: nx_int, 140 body_id: nx_int, out_rgb: *i64 141) { 142 let q: nx_int = NX_ATM_Q 143 // Earth default colors: zenith deep blue (R=70, G=130, B=240), horizon 144 // pale (R=200, G=220, B=250). 145 var zR: nx_int = 70 * q / 255 146 var zG: nx_int = 130 * q / 255 147 var zB: nx_int = 240 * q / 255 148 var hR: nx_int = 200 * q / 255 149 var hG: nx_int = 220 * q / 255 150 var hB: nx_int = 250 * q / 255 151 152 // Mars: pinkish/orange sky. 153 if body_id == NX_BODY_MARS_LOCAL { 154 zR = 200 * q / 255; zG = 130 * q / 255; zB = 100 * q / 255 155 hR = 240 * q / 255; hG = 180 * q / 255; hB = 130 * q / 255 156 } 157 // Titan: orange haze. 158 if body_id == NX_BODY_TITAN_LOCAL { 159 zR = 220 * q / 255; zG = 120 * q / 255; zB = 30 * q / 255 160 hR = 240 * q / 255; hG = 160 * q / 255; hB = 60 * q / 255 161 } 162 // Venus: yellow-white. 163 if body_id == NX_BODY_VENUS_LOCAL { 164 zR = 220 * q / 255; zG = 200 * q / 255; zB = 100 * q / 255 165 hR = 250 * q / 255; hG = 230 * q / 255; hB = 150 * q / 255 166 } 167 // Mercury / airless: black. 168 if body_id == NX_BODY_MERCURY_LOCAL { 169 zR = 0; zG = 0; zB = 0 170 hR = 0; hG = 0; hB = 0 171 } 172 173 // Mix zenith vs horizon by zenith_angle. 174 let t: nx_int = zenith_angle_q14 175 let one_minus_t: nx_int = q - t 176 var R: nx_int = (zR * one_minus_t + hR * t) / q 177 var G: nx_int = (zG * one_minus_t + hG * t) / q 178 var B: nx_int = (zB * one_minus_t + hB * t) / q 179 180 // Sunset reddening: when sun is near horizon (sun_zenith_q14 ~ Q), 181 // warm the color toward red/orange. Apply if sun_zenith >= 0.7Q. 182 let sunset_band: nx_int = q * 7 / 10 183 if sun_zenith_q14 >= sunset_band { 184 let intensity: nx_int = ((sun_zenith_q14 - sunset_band) * q) / (q - sunset_band) 185 // R bumps up; B drops. 186 R = R + (intensity * (q - R)) / (q * 2) 187 B = B - (intensity * B) / (q * 2) 188 if B < 0 { B = 0 } 189 } 190 191 out_rgb[0] = R 192 out_rgb[1] = G 193 out_rgb[2] = B 194} 195 196// ===== Self-test ==================================================== 197func main() -> i64 { 198 let q: nx_int = NX_ATM_Q 199 200 // T1: Per-body coefficients. 201 if nx_atmosphere_rayleigh_q14(NX_BODY_EARTH_LOCAL) != q { return __syscall(93, 1, 0, 0, 0, 0, 0) } 202 if nx_atmosphere_rayleigh_q14(NX_BODY_MERCURY_LOCAL) != 0 { return __syscall(93, 2, 0, 0, 0, 0, 0) } 203 if nx_atmosphere_mie_q14(NX_BODY_VENUS_LOCAL) != 2 * q { return __syscall(93, 3, 0, 0, 0, 0, 0) } 204 205 // T2: exp(-0) = Q; exp(-1) ~ 6029; exp(-4) ~ 295. 206 if _atm_exp_neg_q14(0) != q { return __syscall(93, 10, 0, 0, 0, 0, 0) } 207 let e1: nx_int = _atm_exp_neg_q14(q) 208 if e1 < NX_MAGIC_5800 { return __syscall(93, 11, 0, 0, 0, 0, 0) } 209 if e1 > NX_MAGIC_6200 { return __syscall(93, 12, 0, 0, 0, 0, 0) } 210 if _atm_exp_neg_q14(5 * q) != 0 { return __syscall(93, 13, 0, 0, 0, 0, 0) } 211 212 // T3: Extinction. Earth at 0 distance -> full transmittance. 213 if nx_atmosphere_extinction_q14(0, NX_BODY_EARTH_LOCAL) != q { 214 return __syscall(93, 20, 0, 0, 0, 0, 0) 215 } 216 // Earth at very long distance -> fully extincted. 217 let ext_far: nx_int = nx_atmosphere_extinction_q14(1000 * q, NX_BODY_EARTH_LOCAL) 218 if ext_far >= q / 4 { return __syscall(93, 21, 0, 0, 0, 0, 0) } 219 // Mercury (no atm) at any distance -> full transmittance. 220 if nx_atmosphere_extinction_q14(1000 * q, NX_BODY_MERCURY_LOCAL) != q { 221 return __syscall(93, 22, 0, 0, 0, 0, 0) 222 } 223 224 // T4: Fog density: high at sea level, low at altitude. 225 let fog_low: nx_int = nx_atmosphere_fog_density_q14(0, NX_BODY_EARTH_LOCAL) 226 let fog_high: nx_int = nx_atmosphere_fog_density_q14(10 * q, NX_BODY_EARTH_LOCAL) 227 if fog_low <= fog_high { return __syscall(93, 30, 0, 0, 0, 0, 0) } 228 229 // T5: Sky color. 230 let rgb: *i64 = (sys_mmap(3 * NX_SIZEOF_NX_INT)) as *i64 231 // Earth, zenith=0 (looking straight up), noon -> deep blue 232 nx_atmosphere_sky_color_q14(0, 0, NX_BODY_EARTH_LOCAL, rgb) 233 // B should be the highest of the three (Earth daytime zenith). 234 if rgb[2] < rgb[0] { return __syscall(93, 40, 0, 0, 0, 0, 0) } 235 if rgb[2] < rgb[1] { return __syscall(93, 41, 0, 0, 0, 0, 0) } 236 237 // Mars zenith -> R highest (orange sky). 238 nx_atmosphere_sky_color_q14(0, 0, NX_BODY_MARS_LOCAL, rgb) 239 if rgb[0] < rgb[2] { return __syscall(93, 50, 0, 0, 0, 0, 0) } 240 241 // Mercury airless -> black. 242 nx_atmosphere_sky_color_q14(0, 0, NX_BODY_MERCURY_LOCAL, rgb) 243 if rgb[0] != 0 { return __syscall(93, 60, 0, 0, 0, 0, 0) } 244 if rgb[1] != 0 { return __syscall(93, 61, 0, 0, 0, 0, 0) } 245 if rgb[2] != 0 { return __syscall(93, 62, 0, 0, 0, 0, 0) } 246 247 // Sunset reddening: Earth, zenith=horizon (Q), sun_zenith near horizon (0.9Q). 248 nx_atmosphere_sky_color_q14(q, q * 9 / 10, NX_BODY_EARTH_LOCAL, rgb) 249 // R should be higher than the noon-Q-zenith earth sky. 250 let rgb_noon: *i64 = (sys_mmap(3 * NX_SIZEOF_NX_INT)) as *i64 251 nx_atmosphere_sky_color_q14(q, 0, NX_BODY_EARTH_LOCAL, rgb_noon) 252 if rgb[0] <= rgb_noon[0] { return __syscall(93, 70, 0, 0, 0, 0, 0) } 253 254 return 0 255}