code wiki / (root) / nx_atmosphere.nx

nx_atmosphere.nx source

↩ module page · 579 lines · 21973 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_itrig.nx" 25import "nx_vecmath.nx" 26import "nx_tier.nx" 27const NX_EXPNEG_E1_Q14: i64 = 6029 28const NX_EXPNEG_E2_Q14: i64 = 2212 29const NX_T2_E1_LO_Q14: i64 = 5800 30const NX_T2_E1_HI_Q14: i64 = 6200 31 32const NX_ATM_Q: nx_int = 16384 33 34// ===== THE 1/LAMBDA^4 LAW, IMPLEMENTED RATHER THAN MERELY DOCUMENTED ================= 35// This organ's header has cited rayleigh_1871 since 2026-05-16 and its coefficient comment says 36// "1/lambda^4 wavelength dep" -- but nx_atmosphere_rayleigh_q14 returns ONE SCALAR PER BODY, which 37// has no wavelength dependence at all, and the sky colour was a HARDCODED PALETTE (70/130/240). 38// The citation did not match the code, so the blue of the sky was an art choice wearing a physics 39// reference. ★A CAPABILITY IS WHAT ITS CODE DOES, NEVER WHAT ITS HEADER CLAIMS. 40// 41// Rayleigh scattering goes as lambda^-4, so the RGB ratios are DERIVED from the channel wavelengths 42// and are not free parameters. Wavelengths are the standard triple used throughout atmospheric 43// rendering (Nishita 1993, Preetham 1999, Bruneton & Neyret 2008). Blue is the reference channel 44// because it scatters most, so every ratio is <= 1 and the arithmetic stays in range. 45const NX_LAMBDA_R_NM: nx_int = 680 46const NX_LAMBDA_G_NM: nx_int = 550 47const NX_LAMBDA_B_NM: nx_int = 440 48// Horizon airmass CLAMP. A plane-parallel atmosphere gives airmass = sec(z), which DIVERGES at the 49// horizon; the real value is finite because the atmosphere is curved. 38 is the measured horizon 50// airmass (Kasten & Young 1989), so this bound is physical, not a fudge to stop a divide by zero -- 51// and it is the reason the horizon whitens instead of going black. 52const NX_AIRMASS_MAX: nx_int = 38 53// Optical depth of one airmass at the blue reference, in permil of NX_ATM_Q. Sets how saturated the 54// zenith blue is; it is the one free knob and it is NAMED as such rather than buried in a palette. 55const NX_TAU_BLUE_PERMIL: nx_int = 700 56 57// Rayleigh scattering coefficient per channel, RELATIVE TO BLUE, in permil. DERIVED at every call 58// from the wavelengths above -- (lambda_B/lambda)^4 -- so changing a wavelength moves the colour and 59// no ratio can silently drift from the law it claims to obey. 60func nx_atm_beta_permil(lambda_nm: nx_int) -> nx_int { 61 let b2: nx_int = NX_LAMBDA_B_NM * NX_LAMBDA_B_NM 62 let l2: nx_int = lambda_nm * lambda_nm 63 return (b2 * b2 * 1000) / (l2 * l2) 64} 65 66// Airmass in Q, from a zenith angle expressed 0 (straight up) .. Q (horizon). 67// sec(z), clamped to the measured horizon value above. 68func nx_atm_airmass_q(zenith_angle_q14: nx_int) -> nx_int { 69 let q: nx_int = NX_ATM_Q 70 var t: nx_int = zenith_angle_q14 71 if t < 0 { t = 0 } 72 if t > q { t = q } 73 // IT_QUARTER is a quarter turn in it_cos4096's angle units: zenith..horizon IS 0..90 degrees. 74 let c: nx_int = it_cos4096(t * IT_PI / (2 * q)) 75 if c <= 0 { return q * NX_AIRMASS_MAX } 76 var am: nx_int = q * 4096 / c 77 if am > q * NX_AIRMASS_MAX { am = q * NX_AIRMASS_MAX } 78 if am < q { am = q } 79 return am 80} 81 82// RADIANCE IS NOT A PIXEL. A display encodes roughly the square root of linear light (sRGB's transfer 83// is close to a 2.2 gamma), so writing linear radiance straight into a framebuffer produces a sky that 84// is far too dark -- the derived zenith is (20,46,113) linear and (80,117,176) encoded. Skipping this 85// step is how a physically correct model gets rejected for "looking wrong" and replaced by a palette. 86// 87// The square root is used as an integer-cheap stand-in for the 2.2 exponent, and the imprecision is 88// DECLARED rather than hidden: sqrt is x^0.500 against sRGB's x^0.455, so this runs slightly DARK. 89// It errs toward under-bright, which cannot manufacture detail that the scattering did not produce. 90func nx_atm_display_255(radiance_q: nx_int) -> nx_int { 91 let q: nx_int = NX_ATM_Q 92 var r: nx_int = radiance_q 93 if r < 0 { r = 0 } 94 if r > q { r = q } 95 // 255 * sqrt(r/q) == sqrt(r * 255*255 / q), kept in integers with no intermediate fraction. 96 return vm_isqrt(r * 255 * 255 / q) 97} 98 99// Single-scattered sky radiance for one channel, 0..Q. 100// I = 1 - exp(-tau), the standard single-scattering form: a channel that has scattered a lot of light 101// out of the direct beam is exactly the channel the sky glows in. Blue saturates first at the zenith, 102// which IS the blue sky; near the horizon every channel saturates, which IS the pale horizon. Both 103// fall out of lambda^-4 plus airmass -- neither is painted in. 104func nx_atm_channel_q(lambda_nm: nx_int, airmass_q: nx_int) -> nx_int { 105 let q: nx_int = NX_ATM_Q 106 let beta: nx_int = nx_atm_beta_permil(lambda_nm) 107 let tau: nx_int = (NX_TAU_BLUE_PERMIL * beta / 1000) * airmass_q / 1000 108 return q - _atm_exp_neg_q14(tau) 109} 110 111// ===== Per-body atmospheric coefficients =========================== 112// These match the body_id space from nx_solar_system_catalog. 113// Coefficients are normalised so Earth = 1.0Q at sea level. 114// 115// Rayleigh: predominantly blue scattering (1/lambda^4 wavelength dep). 116// Mie: predominantly white/aerosol scattering (wavelength-independent). 117// Density scale: relative to Earth sea level. 118// 119// Earth: R=Q, M=Q, density=Q 120// Venus: R=Q*4, M=Q*2, density=Q*92 (dense CO2) 121// Mars: R=Q/100, M=Q/200, density=Q/100 (thin) 122// Titan: R=Q*2, M=Q*3, density=Q (thick orange haze) 123// Airless: all 0 124const NX_BODY_SUN_LOCAL: nx_int = 0 // local alias for SUN 125const NX_BODY_MERCURY_LOCAL: nx_int = 1 126const NX_BODY_VENUS_LOCAL: nx_int = 2 127const NX_BODY_EARTH_LOCAL: nx_int = 3 128const NX_BODY_MARS_LOCAL: nx_int = 4 129const NX_BODY_TITAN_LOCAL: nx_int = 16 130 131func nx_atmosphere_rayleigh_q14(body_id: nx_int) -> nx_int { 132 let q: nx_int = NX_ATM_Q 133 if body_id == NX_BODY_EARTH_LOCAL { return q } 134 if body_id == NX_BODY_VENUS_LOCAL { return q * 4 } 135 if body_id == NX_BODY_MARS_LOCAL { return q / 100 } 136 if body_id == NX_BODY_TITAN_LOCAL { return q * 2 } 137 return 0 138} 139 140func nx_atmosphere_mie_q14(body_id: nx_int) -> nx_int { 141 let q: nx_int = NX_ATM_Q 142 if body_id == NX_BODY_EARTH_LOCAL { return q } 143 if body_id == NX_BODY_VENUS_LOCAL { return q * 2 } 144 if body_id == NX_BODY_MARS_LOCAL { return q / 200 } 145 if body_id == NX_BODY_TITAN_LOCAL { return q * 3 } 146 return 0 147} 148 149// ===== Beer-Lambert extinction ===================================== 150// I = I_0 * exp(-tau * d) 151// Approximate exp(-x) for x in [0, 4Q] via piecewise-linear table. 152// Returns attenuation in Q14: 1.0 = no attenuation, 0 = fully extincted. 153func _atm_exp_neg_q14(x_q14: nx_int) -> nx_int { 154 let q: nx_int = NX_ATM_Q 155 if x_q14 <= 0 { return q } 156 if x_q14 >= 4 * q { return 0 } 157 // Anchors: exp(-0)=1.0, exp(-1)=0.368, exp(-2)=0.135, exp(-3)=0.050, exp(-4)=0.018 158 // In Q14: 16384 / 6029 / 2212 / 819 / 295. 159 if x_q14 <= q { 160 // Linear from 16384 to 6029 over [0, Q]. 161 return q - ((q - NX_EXPNEG_E1_Q14) * x_q14) / q 162 } 163 if x_q14 <= 2 * q { 164 let off: nx_int = x_q14 - q 165 return NX_EXPNEG_E1_Q14 - ((NX_EXPNEG_E1_Q14 - NX_EXPNEG_E2_Q14) * off) / q 166 } 167 if x_q14 <= 3 * q { 168 let off: nx_int = x_q14 - 2 * q 169 return NX_EXPNEG_E2_Q14 - ((NX_EXPNEG_E2_Q14 - 819) * off) / q 170 } 171 let off: nx_int = x_q14 - 3 * q 172 return 819 - ((819 - 295) * off) / q 173} 174 175// Beer-Lambert extinction at a given distance for a given body. 176// distance_q14_km: viewing distance in Q14 km 177// body_id : the body whose atmosphere we're looking through 178// Returns transmittance in Q14 [0, Q]. 179func nx_atmosphere_extinction_q14( 180 distance_q14_km: nx_int, body_id: nx_int 181) -> nx_int { 182 let q: nx_int = NX_ATM_Q 183 let ray: nx_int = nx_atmosphere_rayleigh_q14(body_id) 184 let mie: nx_int = nx_atmosphere_mie_q14(body_id) 185 let tau: nx_int = ray + mie 186 if tau <= 0 { return q } 187 // Optical depth = tau * distance / scale_height (~ 8 km for Earth). 188 // tau_eff_q14 = tau * distance / (8 * q) 189 let scale_h_q: nx_int = 8 * q 190 let tau_eff: nx_int = (tau * distance_q14_km) / scale_h_q 191 return _atm_exp_neg_q14(tau_eff) 192} 193 194// ===== Fog density at altitude ===================================== 195// Fog density decreases exponentially with altitude (Beer-Lambert). 196// fog(h) = base * exp(-h / scale_h) 197// Returns Q14 fog density. 198func nx_atmosphere_fog_density_q14( 199 altitude_q14_km: nx_int, body_id: nx_int 200) -> nx_int { 201 let q: nx_int = NX_ATM_Q 202 let base_density: nx_int = nx_atmosphere_mie_q14(body_id) 203 if base_density <= 0 { return 0 } 204 let scale_h: nx_int = 2 * q // 2 km scale height for fog (vs 8 km for full atm) 205 let frac: nx_int = (altitude_q14_km * q) / scale_h 206 return (base_density * _atm_exp_neg_q14(frac)) / q 207} 208 209// ===== Sky color (simplified Preetham) ============================ 210// Sky color = base_horizon_color * (1 - extinction) + zenith_color * extinction 211// modulated by sun-angle proximity. 212// 213// Writes (R, G, B) Q14 [0, Q] to out_rgb. 214// zenith_angle_q14: 0 = zenith, Q = horizon (90 deg) 215// sun_zenith_q14: sun's zenith angle (0 = noon, Q = sunset) 216// body_id: atmospheric body 217func nx_atmosphere_sky_color_q14( 218 zenith_angle_q14: nx_int, sun_zenith_q14: nx_int, 219 body_id: nx_int, out_rgb: *i64 220) { 221 let q: nx_int = NX_ATM_Q 222 // EARTH: DERIVED, not painted. The zenith and horizon colours are the single-scattering radiance 223 // at 1 airmass and at the cited horizon airmass, per channel, from lambda^-4. The palette that 224 // used to sit here (zenith 70/130/240, horizon 200/220/250) had no source and contradicted this 225 // organ's own rayleigh_1871 citation -- see the law block at the top of this file. 226 let amZ: nx_int = nx_atm_airmass_q(0) 227 let amH: nx_int = nx_atm_airmass_q(q) 228 var zR: nx_int = nx_atm_channel_q(NX_LAMBDA_R_NM, amZ) 229 var zG: nx_int = nx_atm_channel_q(NX_LAMBDA_G_NM, amZ) 230 var zB: nx_int = nx_atm_channel_q(NX_LAMBDA_B_NM, amZ) 231 var hR: nx_int = nx_atm_channel_q(NX_LAMBDA_R_NM, amH) 232 var hG: nx_int = nx_atm_channel_q(NX_LAMBDA_G_NM, amH) 233 var hB: nx_int = nx_atm_channel_q(NX_LAMBDA_B_NM, amH) 234 // OTHER BODIES REMAIN EXPLICIT PALETTES AND ARE DECLARED AS SUCH. No mirrored measurement of 235 // Martian or Titanian sky radiance exists in this estate, and deriving them from invented 236 // coefficients would be rigour theatre -- the same defect this block was written to remove. 237 238 // Mars: pinkish/orange sky. 239 if body_id == NX_BODY_MARS_LOCAL { 240 zR = 200 * q / 255; zG = 130 * q / 255; zB = 100 * q / 255 241 hR = 240 * q / 255; hG = 180 * q / 255; hB = 130 * q / 255 242 } 243 // Titan: orange haze. 244 if body_id == NX_BODY_TITAN_LOCAL { 245 zR = 220 * q / 255; zG = 120 * q / 255; zB = 30 * q / 255 246 hR = 240 * q / 255; hG = 160 * q / 255; hB = 60 * q / 255 247 } 248 // Venus: yellow-white. 249 if body_id == NX_BODY_VENUS_LOCAL { 250 zR = 220 * q / 255; zG = 200 * q / 255; zB = 100 * q / 255 251 hR = 250 * q / 255; hG = 230 * q / 255; hB = 150 * q / 255 252 } 253 // Mercury / airless: black. 254 if body_id == NX_BODY_MERCURY_LOCAL { 255 zR = 0; zG = 0; zB = 0 256 hR = 0; hG = 0; hB = 0 257 } 258 259 // Mix zenith vs horizon by zenith_angle. 260 let t: nx_int = zenith_angle_q14 261 let one_minus_t: nx_int = q - t 262 var R: nx_int = (zR * one_minus_t + hR * t) / q 263 var G: nx_int = (zG * one_minus_t + hG * t) / q 264 var B: nx_int = (zB * one_minus_t + hB * t) / q 265 266 // Sunset reddening: when sun is near horizon (sun_zenith_q14 ~ Q), 267 // warm the color toward red/orange. Apply if sun_zenith >= 0.7Q. 268 let sunset_band: nx_int = q * 7 / 10 269 if sun_zenith_q14 >= sunset_band { 270 let intensity: nx_int = ((sun_zenith_q14 - sunset_band) * q) / (q - sunset_band) 271 // R bumps up; B drops. 272 R = R + (intensity * (q - R)) / (q * 2) 273 B = B - (intensity * B) / (q * 2) 274 if B < 0 { B = 0 } 275 } 276 277 out_rgb[0] = R 278 out_rgb[1] = G 279 out_rgb[2] = B 280} 281 282// ===== Self-test ==================================================== 283// ===== TRANSMITTANCE RUNG (2026-08-26) ============================================== 284 285// Full 3-species Beer-Lambert transmittance through the curved atmosphere, per the banked 286 287// implementation contract knowledge/library/sky_volumetrics_impl_spec_2026-08-26.md. Every 288 289// constant below is [bytes]-provenanced there (hillaire2020_common_cpp, corroborated by 290 291// bruneton_demo_cc) or [derived] with its derivation named. Q16 fixed point throughout this 292 293// block (the legacy API above stays Q14; Q16 here because the smallest sigma is sub-ulp at Q14). 294 295// The i64 algorithm was simulated against a 20k-sample double reference BEFORE this code was 296 297// written: worst KAT deviation 23/65536 (0.035%) -- the gate pins those KATs at +-64. 298 299const ATM_Q16: nx_int = 65536 300 301const ATM_RB_Q16: nx_int = 416808960 // 6360 km [bytes] both sources 302 303const ATM_RT_Q16: nx_int = 423362560 // 6460 km [bytes] hillaire (bruneton says 6420; divergence adopted+named in the spec) 304 305const ATM_PRO_Q16: nx_int = 655 // PLANET_RADIUS_OFFSET 0.01 km [bytes] 306 307// sigma per km, Q24: rayleigh scattering RGB, mie extinction, ozone absorption RGB [bytes] 308 309const ATM_SRAY_R_Q24: nx_int = 97336 310 311const ATM_SRAY_G_Q24: nx_int = 227447 312 313const ATM_SRAY_B_Q24: nx_int = 555289 314 315const ATM_SMIE_EXT_Q24: nx_int = 74485 316 317const ATM_SOZ_R_Q24: nx_int = 10905 318 319const ATM_SOZ_G_Q24: nx_int = 31557 320 321const ATM_SOZ_B_Q24: nx_int = 1426 322 323const ATM_INV_HRAY_Q16: nx_int = 8192 // 1/(8 km) [bytes] scale height 324 325const ATM_INV_HMIE_Q16: nx_int = 54613 // 1/(1.2 km) [bytes] 326 327// ozone tent integers [derived]: 1/15 -> 4369, 2/3 -> 43691, 8/3 -> 174763 (~0.002% rounding, named) 328 329const ATM_OZ_SLOPE_Q16: nx_int = 4369 330 331const ATM_OZ_LO_Q16: nx_int = 43691 332 333const ATM_OZ_HI_Q16: nx_int = 174763 334 335const ATM_OZ_PEAK_KM_Q16: nx_int = 1638400 // 25 km 336 337const ATM_LOG2E_Q16: nx_int = 94548 // [derived] log2(e)*65536 338 339const ATM_LN2_Q16: nx_int = 45426 // [derived] ln(2)*65536 340 341const ATM_TRANS_SAMPLES: nx_int = 500 // [bytes] bruneton_functions trapezoid count 342 343const ATM_SUNLUT_N: nx_int = 64 // sun-zenith LUT rows emitted to the page (Q16 mu in [0,1]) 344 345const ATM_POW32: nx_int = 4294967296 346 347 348 349// e^-x for x_q16 >= 0. Decompose x = k*ln2 + r, e^-r by 5-term Taylor (max err 0.13% of value, 350 351// named imprecision; the composed transmittance KATs land within 23 q16 of the double reference). 352 353func atm_expneg_q16(x_q16: nx_int) -> nx_int { 354 355 if x_q16 <= 0 { return ATM_Q16 } 356 357 var ki: nx_int = (x_q16 * ATM_LOG2E_Q16) / ATM_POW32 358 359 var r: nx_int = x_q16 - ki * ATM_LN2_Q16 360 361 if r < 0 { r = 0 } 362 363 let t2: nx_int = (r * r) / ATM_Q16 364 365 let t3: nx_int = (t2 * r) / ATM_Q16 366 367 let t4: nx_int = (t2 * t2) / ATM_Q16 368 369 var s: nx_int = ATM_Q16 - r + t2 / 2 - t3 / 6 + t4 / 24 370 371 if s < 0 { s = 0 } 372 373 if ki >= 48 { return 0 } 374 375 while ki > 0 { s = s / 2; ki = ki - 1 } 376 377 return s 378 379} 380 381 382 383// density of species (0 rayleigh, 1 mie, 2 ozone tent) at altitude h (Q16 km), Q16 result 384 385func atm_dens_q16(species: nx_int, h_q16: nx_int) -> nx_int { 386 387 var h: nx_int = h_q16 388 389 if h < 0 { h = 0 } 390 391 if species == 0 { return atm_expneg_q16((h * ATM_INV_HRAY_Q16) / ATM_Q16) } 392 393 if species == 1 { return atm_expneg_q16((h * ATM_INV_HMIE_Q16) / ATM_Q16) } 394 395 let hk: nx_int = (h * ATM_OZ_SLOPE_Q16) / ATM_Q16 396 397 var d: nx_int = 0 398 399 if h < ATM_OZ_PEAK_KM_Q16 { d = hk - ATM_OZ_LO_Q16 } else { d = ATM_OZ_HI_Q16 - hk } 400 401 if d < 0 { d = 0 } 402 403 if d > ATM_Q16 { d = ATM_Q16 } 404 405 return d 406 407} 408 409 410 411// Transmittance RGB (Q16) from radius r_q16 (km Q16), view cos-zenith mu_q16 in [0, Q16], to the 412 413// top of atmosphere. 500-sample midpoint march, matching the double reference sample-for-sample. 414 415func atm_trans_q16(r_q16: nx_int, mu_q16: nx_int, out_rgb: *i64) { 416 417 var mu: nx_int = mu_q16 418 419 if mu < 0 { mu = 0 } 420 421 if mu > ATM_Q16 { mu = ATM_Q16 } 422 423 let rmu: nx_int = (r_q16 * mu) / ATM_Q16 424 425 var disc: nx_int = rmu * rmu - r_q16 * r_q16 + ATM_RT_Q16 * ATM_RT_Q16 426 427 if disc < 0 { disc = 0 } 428 429 let d: nx_int = 0 - rmu + vm_isqrt(disc) 430 431 if d <= 0 { 432 433 out_rgb[0] = ATM_Q16; out_rgb[1] = ATM_Q16; out_rgb[2] = ATM_Q16 434 435 return 436 437 } 438 439 let dt: nx_int = d / ATM_TRANS_SAMPLES 440 441 var tauR: nx_int = 0 442 443 var tauG: nx_int = 0 444 445 var tauB: nx_int = 0 446 447 var i: nx_int = 0 448 449 while i < ATM_TRANS_SAMPLES { 450 451 let t: nx_int = i * dt + dt / 2 452 453 let h2: nx_int = r_q16 * r_q16 + t * t + 2 * rmu * t 454 455 let h: nx_int = vm_isqrt(h2) - ATM_RB_Q16 456 457 let dr: nx_int = atm_dens_q16(0, h) 458 459 let dm: nx_int = atm_dens_q16(1, h) 460 461 let dz: nx_int = atm_dens_q16(2, h) 462 463 let sR: nx_int = (ATM_SRAY_R_Q24 * dr + ATM_SMIE_EXT_Q24 * dm + ATM_SOZ_R_Q24 * dz) / ATM_Q16 464 465 let sG: nx_int = (ATM_SRAY_G_Q24 * dr + ATM_SMIE_EXT_Q24 * dm + ATM_SOZ_G_Q24 * dz) / ATM_Q16 466 467 let sB: nx_int = (ATM_SRAY_B_Q24 * dr + ATM_SMIE_EXT_Q24 * dm + ATM_SOZ_B_Q24 * dz) / ATM_Q16 468 469 tauR = tauR + (sR * dt) / ATM_Q16 470 471 tauG = tauG + (sG * dt) / ATM_Q16 472 473 tauB = tauB + (sB * dt) / ATM_Q16 474 475 i = i + 1 476 477 } 478 479 out_rgb[0] = atm_expneg_q16(tauR / 256) 480 481 out_rgb[1] = atm_expneg_q16(tauG / 256) 482 483 out_rgb[2] = atm_expneg_q16(tauB / 256) 484 485} 486 487 488 489// Sun colour at ground level for sun cos-zenith mu (Q16): the transmittance itself IS the colour 490 491// of direct sunlight -- horizon reddening and zenith white fall out of the medium, no palette. 492 493func atm_sun_rgb_q16(mu_q16: nx_int, out_rgb: *i64) { 494 495 atm_trans_q16(ATM_RB_Q16 + ATM_PRO_Q16, mu_q16, out_rgb) 496 497} 498 499 500 501// Bake the page-facing sun LUT: ATM_SUNLUT_N rows of RGB Q16, mu = row/(N-1). out must hold 3*N i64. 502 503func atm_bake_sun_lut(out_rgb: *i64) { 504 505 var row: nx_int = 0 506 507 while row < ATM_SUNLUT_N { 508 509 let mu: nx_int = (row * ATM_Q16) / (ATM_SUNLUT_N - 1) 510 511 atm_sun_rgb_q16(mu, out_rgb + row * 24) 512 513 row = row + 1 514 515 } 516 517} 518 519 520 521func main() -> i64 { 522 let q: nx_int = NX_ATM_Q 523 524 // T1: Per-body coefficients. 525 if nx_atmosphere_rayleigh_q14(NX_BODY_EARTH_LOCAL) != q { return __syscall(93, 1, 0, 0, 0, 0, 0) } 526 if nx_atmosphere_rayleigh_q14(NX_BODY_MERCURY_LOCAL) != 0 { return __syscall(93, 2, 0, 0, 0, 0, 0) } 527 if nx_atmosphere_mie_q14(NX_BODY_VENUS_LOCAL) != 2 * q { return __syscall(93, 3, 0, 0, 0, 0, 0) } 528 529 // T2: exp(-0) = Q; exp(-1) ~ 6029; exp(-4) ~ 295. 530 if _atm_exp_neg_q14(0) != q { return __syscall(93, 10, 0, 0, 0, 0, 0) } 531 let e1: nx_int = _atm_exp_neg_q14(q) 532 if e1 < NX_T2_E1_LO_Q14 { return __syscall(93, 11, 0, 0, 0, 0, 0) } 533 if e1 > NX_T2_E1_HI_Q14 { return __syscall(93, 12, 0, 0, 0, 0, 0) } 534 if _atm_exp_neg_q14(5 * q) != 0 { return __syscall(93, 13, 0, 0, 0, 0, 0) } 535 536 // T3: Extinction. Earth at 0 distance -> full transmittance. 537 if nx_atmosphere_extinction_q14(0, NX_BODY_EARTH_LOCAL) != q { 538 return __syscall(93, 20, 0, 0, 0, 0, 0) 539 } 540 // Earth at very long distance -> fully extincted. 541 let ext_far: nx_int = nx_atmosphere_extinction_q14(1000 * q, NX_BODY_EARTH_LOCAL) 542 if ext_far >= q / 4 { return __syscall(93, 21, 0, 0, 0, 0, 0) } 543 // Mercury (no atm) at any distance -> full transmittance. 544 if nx_atmosphere_extinction_q14(1000 * q, NX_BODY_MERCURY_LOCAL) != q { 545 return __syscall(93, 22, 0, 0, 0, 0, 0) 546 } 547 548 // T4: Fog density: high at sea level, low at altitude. 549 let fog_low: nx_int = nx_atmosphere_fog_density_q14(0, NX_BODY_EARTH_LOCAL) 550 let fog_high: nx_int = nx_atmosphere_fog_density_q14(10 * q, NX_BODY_EARTH_LOCAL) 551 if fog_low <= fog_high { return __syscall(93, 30, 0, 0, 0, 0, 0) } 552 553 // T5: Sky color. 554 let rgb: *i64 = (sys_mmap(3 * NX_SIZEOF_NX_INT)) as *i64 555 // Earth, zenith=0 (looking straight up), noon -> deep blue 556 nx_atmosphere_sky_color_q14(0, 0, NX_BODY_EARTH_LOCAL, rgb) 557 // B should be the highest of the three (Earth daytime zenith). 558 if rgb[2] < rgb[0] { return __syscall(93, 40, 0, 0, 0, 0, 0) } 559 if rgb[2] < rgb[1] { return __syscall(93, 41, 0, 0, 0, 0, 0) } 560 561 // Mars zenith -> R highest (orange sky). 562 nx_atmosphere_sky_color_q14(0, 0, NX_BODY_MARS_LOCAL, rgb) 563 if rgb[0] < rgb[2] { return __syscall(93, 50, 0, 0, 0, 0, 0) } 564 565 // Mercury airless -> black. 566 nx_atmosphere_sky_color_q14(0, 0, NX_BODY_MERCURY_LOCAL, rgb) 567 if rgb[0] != 0 { return __syscall(93, 60, 0, 0, 0, 0, 0) } 568 if rgb[1] != 0 { return __syscall(93, 61, 0, 0, 0, 0, 0) } 569 if rgb[2] != 0 { return __syscall(93, 62, 0, 0, 0, 0, 0) } 570 571 // Sunset reddening: Earth, zenith=horizon (Q), sun_zenith near horizon (0.9Q). 572 nx_atmosphere_sky_color_q14(q, q * 9 / 10, NX_BODY_EARTH_LOCAL, rgb) 573 // R should be higher than the noon-Q-zenith earth sky. 574 let rgb_noon: *i64 = (sys_mmap(3 * NX_SIZEOF_NX_INT)) as *i64 575 nx_atmosphere_sky_color_q14(q, 0, NX_BODY_EARTH_LOCAL, rgb_noon) 576 if rgb[0] <= rgb_noon[0] { return __syscall(93, 70, 0, 0, 0, 0, 0) } 577 578 return 0 579}