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}