nx_gi_path_lib.nx source
↩ module page · 316 lines · 16498 B
1// nx_gi_path_lib.nx -- THE BEAUTY TIER'S FIRST BYTE (LIBRARY: every function of the sovereign path tracer; nx_gi_path.nx is the CLI, nx_gi_path_gate.nx the teeth): a sovereign INTEGER PATH TRACER over the procgen heightfield.
2// graphics GR42 (env_beauty_scene) + GR10 (gp_bounce_gi); operator 2026-09-02, looking at /world/beach: "that looks like
3// our junk cpu stuff ... lets get things infinigen generating to photoreal". Infinigen renders offline through a path
4// tracer; this organ is that twin, first byte up: no float, no library, no third party -- the same fx1024 arithmetic,
5// the same fBm heightfield (nx_worldgen wg_terrain_h), the same sky model (wg_sky), the same soft-shadow march
6// (wg_sun_pen) and the same PNG writer the estate already ships. What is NEW is the light transport: every pixel is
7// the mean of spp Monte Carlo paths, each path = primary ray -> Lambertian hit (direct sun x soft shadow) -> cosine-
8// weighted bounce (BRDF x cos / pdf = albedo, so no division noise) -> up to GP_BOUNCES hits -> sky gather when the
9// path escapes. Colour bleed and sky occlusion therefore emerge from transport instead of being painted on, which is
10// the difference between the raymarcher's "ambient = 42" and a beauty tier.
11//
12// DETERMINISM: the sampler is a seeded integer LCG advanced per draw; two runs with the same argv are byte-identical,
13// so a gate can diff frames (the GR10 cache-vs-rebuild law carried into path tracing).
14// NO TERRACING: the march step grows with distance (footprint-sized) and every hit is REFINED by GP_REFINE bisections,
15// so the surface lands within a fraction of a unit -- the stair-steps visible in every nx_worldgen slope are gone.
16// HONEST LIMITS OF THIS FIRST BYTE, stated so the next reader does not credit them: terrain is Lambertian only (no
17// specular, no BRDF model beyond albedo); water is a single mirror event tinted by a deep colour, not refraction;
18// the sky is nx_worldgen's gradient+sun+cloud model, not GR26's Rayleigh/Mie atmosphere; no cast meshes yet; no
19// depth/normal/semantic ground-truth siblings yet (the dataset half of GR42). Each of those is a named rung.
20// UNITS: directions are fx1024 unit vectors; radiance is carried as throughput(fx1024) x colour(0..255), then averaged,
21// Reinhard-mapped and display-encoded ONCE at output (GR14's linear-light law: all transport in linear).
22//
23// nx_gi_path <out.png> [seed] [spp] [weather 0 clear | 1 sunset | 2 overcast] [yaw 0..4095]
24// exit: 0 written | 2 usage | 5 png-write-fail. license_tier: ORIGINAL No hw writes (Rule 26).
25import "nx_syscalls.nx"
26import "nx_itrig.nx"
27import "nx_vecmath.nx"
28import "nx_worldgen.nx"
29import "nx_png_write.nx"
30
31// ---- frame: 3/8 of the raymarcher's frame. The primary-ray budget the raymarcher spends on 1280x768 is spent here on
32// bounces instead; the fraction is a ratio of the incumbent's frame, not a fresh resolution literal.
33const GP_FRAC_NUM: i64 = 3
34const GP_FRAC_DEN: i64 = 8
35const GP_FX: i64 = 1024 // the estate's fixed-point unit, the one nx_worldgen's vectors already use
36const GP_HALF: i64 = 512 // half a fx unit: the pixel-jitter recentring term
37const GP_CIRCLE: i64 = 4096 // it_sin4096: 4096 angle units per turn AND amplitude 4096
38const GP_SPP_DEFAULT: i64 = 4
39const GP_BOUNCES: i64 = 2 // primary + 2 diffuse bounces: the shortest transport that carries colour bleed AND sky occlusion
40const GP_T0: i64 = 4 // first march distance in front of an origin
41const GP_BIAS: i64 = 8 // bounce origin lifted along the normal so a path never re-hits its own surface
42const GP_TMAX: i64 = 60000 // nx_worldgen's far plane
43const GP_SKY_Y: i64 = 2900 // nx_worldgen's sun-march ceiling: no terrain exists above it
44const GP_STEP_MIN: i64 = 6
45const GP_STEP_DIV: i64 = 96 // step = min + t/96: half the raymarcher's growth (t/48), then bisection-refined
46const GP_REFINE: i64 = 8 // bisections per hit: the last step / 256 -> sub-unit placement, no terracing
47const GP_MAX_STEPS: i64 = 400
48const GP_NRM_EPS: i64 = 24 // the raymarcher's normal footprint, kept so both tiers agree on the same surface
49const GP_SUN_OVER_SKY: i64 = 2 // sun irradiance relative to the sky dome on a Lambertian surface; DECLARED, to be fitted against the reference frame
50const GP_EXPOSURE_NUM: i64 = 3 // Reinhard exposure 3/2
51const GP_EXPOSURE_DEN: i64 = 2
52const GP_MAXCH: i64 = 255 // display channel ceiling, the PNG's own
53const GP_LCG_A: i64 = 1103515245 // the classic 31-bit LCG (Numerical Recipes / glibc rand family)
54const GP_LCG_C: i64 = 12345
55const GP_LCG_MASK: i64 = 2147483647
56const GP_RND_SHIFT: i64 = 11 // drop the weak low bits of the LCG
57const GP_RND_MASK: i64 = 1023 // a draw is an fx1024 fraction
58const GP_AXIS_TILT: i64 = 600 // |n.x| above this: pick z as the helper axis when building the tangent frame
59const GP_STEEP_NY: i64 = 640 // the raymarcher's rock threshold on the normal's y
60const GP_SAND_BAND: i64 = 130 // the raymarcher's shoreline band above water
61const GP_SNOW_NY: i64 = 600 // the raymarcher's snow slope threshold
62const GP_VEG_SCALE: i64 = 1500 // the raymarcher's vegetation patch scale (world units per noise cell)
63const GP_VEG_SEED: i64 = 99
64const GP_NOISE_SPAN: i64 = 65536 // wg_vnoise range
65// ---- albedo palette, the raymarcher's own so both tiers paint the same world (0..255)
66const GP_GRASS_R: i64 = 60
67const GP_GRASS_G: i64 = 94
68const GP_GRASS_B: i64 = 42
69const GP_GRASS_VR: i64 = 44
70const GP_GRASS_VG: i64 = 50
71const GP_GRASS_VB: i64 = 22
72const GP_ROCK_R: i64 = 112
73const GP_ROCK_G: i64 = 101
74const GP_ROCK_B: i64 = 92
75const GP_SAND_R: i64 = 198
76const GP_SAND_G: i64 = 186
77const GP_SAND_B: i64 = 152
78const GP_SNOW_R: i64 = 234
79const GP_SNOW_G: i64 = 240
80const GP_SNOW_B: i64 = 248
81const GP_WATER_R: i64 = 24
82const GP_WATER_G: i64 = 74
83const GP_WATER_B: i64 = 116
84const GP_OUT: i64 = 4096
85const GP_WORDS: i64 = 16 // scratch vectors are 16 words each (page-rounded by mmap anyway)
86const GP_EXIT_OK: i64 = 0
87const GP_EXIT_USAGE: i64 = 2
88const GP_EXIT_WRITE: i64 = 5
89const GP_CH_0: i64 = 48
90const GP_CH_9: i64 = 57
91const GP_CH_MINUS: i64 = 45
92const GP_ST_SKY: i64 = 0
93const GP_ST_WATER: i64 = 1
94const GP_ST_TERRAIN: i64 = 2
95const GP_ST_MARCHES: i64 = 3
96
97func gp_slen(s: *u8) -> i64 { var n: i64 = 0; while s[n] != (0 as u8) { n = n + 1 } return n }
98func gp_cat(d: *u8, o: i64, s: *u8) -> i64 { var i: i64 = 0; var p: i64 = o; while s[i] != (0 as u8) { d[p] = s[i]; p = p + 1; i = i + 1 } return p }
99func gp_num(d: *u8, o: i64, v: i64) -> i64 {
100 var m: i64 = v
101 var p: i64 = o
102 if m < 0 { d[p] = GP_CH_MINUS as u8; p = p + 1; m = 0 - m }
103 if m == 0 { d[p] = GP_CH_0 as u8; return p + 1 }
104 let t: *u8 = sys_mmap(32)
105 var k: i64 = 0
106 while m > 0 { t[k] = (GP_CH_0 + (m % 10)) as u8; m = m / 10; k = k + 1 }
107 var i: i64 = k - 1
108 while i >= 0 { d[p] = t[i]; p = p + 1; i = i - 1 }
109 sys_munmap(t, 32)
110 return p
111}
112func gp_atoi(s: *u8) -> i64 {
113 var v: i64 = 0
114 var i: i64 = 0
115 var neg: i64 = 0
116 if s[0] == (GP_CH_MINUS as u8) { neg = 1; i = 1 }
117 var go: i64 = 1
118 while go == 1 {
119 let c: i64 = s[i] as i64
120 if c < GP_CH_0 { go = 0 } else { if c > GP_CH_9 { go = 0 } else { v = v*10 + (c - GP_CH_0); i = i + 1 } }
121 }
122 if neg == 1 { return 0 - v }
123 return v
124}
125func gp_clamp(v: i64, lo: i64, hi: i64) -> i64 { if v < lo { return lo } if v > hi { return hi } return v }
126// display value (0..255, the palette and sky model are authored in display space) -> linear radiance 0..255.
127// GR14's law: transport in linear, encode ONCE. The first frame skipped this and came out pale -- every colour
128// was display-encoded twice. Gamma 2.0 decode (square) pairs exactly with gp_encode's square-root encode.
129// Radiance carries GP_FX fractional bits: an 8-bit linear value cannot hold the display gamut (c=102 decodes to 40 and
130// re-encodes to 100 -- the banding-in-the-darks reason every linear pipeline is wider than 8 bits). Measured by the gate.
131func gp_lin(c: i64) -> i64 { let v: i64 = gp_clamp(c, 0, GP_MAXCH); return v*v*GP_FX/GP_MAXCH }
132// the seeded sampler: one draw = one fx1024 fraction; state advances per draw so paths never share a sequence
133func gp_rnd(st: *i64) -> i64 {
134 st[0] = (st[0]*GP_LCG_A + GP_LCG_C) & GP_LCG_MASK
135 return (st[0] >> GP_RND_SHIFT) & GP_RND_MASK
136}
137// normalise v[0..2] to an fx1024 unit vector in place; returns the length it had (0 = degenerate, left untouched)
138func gp_norm3(v: *i64) -> i64 {
139 let l: i64 = vm_isqrt(v[0]*v[0] + v[1]*v[1] + v[2]*v[2])
140 if l > 0 { v[0] = v[0]*GP_FX/l; v[1] = v[1]*GP_FX/l; v[2] = v[2]*GP_FX/l }
141 return l
142}
143
144// march the heightfield from (ox,oy,oz) along the fx1024 unit direction (dx,dy,dz).
145// out[0]=hit 0/1, out[1..3]=point, out[4]=terrain height under it, out[5]=t. A miss leaves out[5] at the escape t.
146func gp_march(ox: i64, oy: i64, oz: i64, dx: i64, dy: i64, dz: i64, seed: i64, out: *i64) -> i64 {
147 var t: i64 = GP_T0
148 var tprev: i64 = 0
149 var step: i64 = 0
150 var hit: i64 = 0
151 while step < GP_MAX_STEPS {
152 let wx: i64 = ox + dx*t/GP_FX
153 let wy: i64 = oy + dy*t/GP_FX
154 let wz: i64 = oz + dz*t/GP_FX
155 var done: i64 = 0
156 if wy > GP_SKY_Y { if dy > 0 { done = 1 } } // climbing above every peak: this ray is sky
157 if done == 0 {
158 let th: i64 = wg_terrain_h(wx, wz, seed)
159 if wy < th { hit = 1; done = 1 }
160 else {
161 tprev = t
162 t = t + GP_STEP_MIN + t/GP_STEP_DIV
163 if t > GP_TMAX { done = 1 }
164 }
165 }
166 if done == 1 { step = GP_MAX_STEPS } else { step = step + 1 }
167 }
168 if hit == 1 {
169 var lo: i64 = tprev
170 var hi: i64 = t
171 var k: i64 = 0
172 while k < GP_REFINE {
173 let mid: i64 = (lo + hi) / 2
174 let mx: i64 = ox + dx*mid/GP_FX
175 let my: i64 = oy + dy*mid/GP_FX
176 let mz: i64 = oz + dz*mid/GP_FX
177 if my < wg_terrain_h(mx, mz, seed) { hi = mid } else { lo = mid }
178 k = k + 1
179 }
180 t = hi
181 }
182 out[0] = hit
183 out[1] = ox + dx*t/GP_FX
184 out[2] = oy + dy*t/GP_FX
185 out[3] = oz + dz*t/GP_FX
186 out[4] = wg_terrain_h(out[1], out[3], seed)
187 out[5] = t
188 return hit
189}
190func gp_normal(hx: i64, hz: i64, seed: i64, n: *i64) -> i64 {
191 let e: i64 = GP_NRM_EPS
192 let hl: i64 = wg_terrain_h(hx - e, hz, seed)
193 let hr: i64 = wg_terrain_h(hx + e, hz, seed)
194 let hd: i64 = wg_terrain_h(hx, hz - e, seed)
195 let hu: i64 = wg_terrain_h(hx, hz + e, seed)
196 n[0] = (hl - hr)*GP_FX/(2*e)
197 n[1] = GP_FX
198 n[2] = (hd - hu)*GP_FX/(2*e)
199 gp_norm3(n)
200 return 0
201}
202// albedo (0..255 rgb) by height and slope -- the raymarcher's temperate palette, so both tiers paint one world
203func gp_albedo(hx: i64, hz: i64, hh: i64, ny: i64, seed: i64, a: *i64) -> i64 {
204 let nv: i64 = wg_vnoise(hx*256/GP_VEG_SCALE, hz*256/GP_VEG_SCALE, seed + GP_VEG_SEED)
205 a[0] = GP_GRASS_R + nv*GP_GRASS_VR/GP_NOISE_SPAN
206 a[1] = GP_GRASS_G + nv*GP_GRASS_VG/GP_NOISE_SPAN
207 a[2] = GP_GRASS_B + nv*GP_GRASS_VB/GP_NOISE_SPAN
208 if ny < GP_STEEP_NY { a[0] = GP_ROCK_R; a[1] = GP_ROCK_G; a[2] = GP_ROCK_B }
209 if hh < WATER + GP_SAND_BAND { a[0] = GP_SAND_R; a[1] = GP_SAND_G; a[2] = GP_SAND_B }
210 if hh > SNOW { if ny > GP_SNOW_NY { a[0] = GP_SNOW_R; a[1] = GP_SNOW_G; a[2] = GP_SNOW_B } }
211 return 0
212}
213// a cosine-weighted direction about the unit normal n. scr = 6 scratch words (tangent, bitangent); d = the result.
214func gp_cosine_dir(n: *i64, st: *i64, scr: *i64, d: *i64) -> i64 {
215 var ax: i64 = GP_FX
216 var ay: i64 = 0
217 var az: i64 = 0
218 if n[0] > GP_AXIS_TILT { ax = 0; az = GP_FX }
219 if n[0] < 0 - GP_AXIS_TILT { ax = 0; az = GP_FX }
220 // t = a x n
221 scr[0] = ay*n[2]/GP_FX - az*n[1]/GP_FX
222 scr[1] = az*n[0]/GP_FX - ax*n[2]/GP_FX
223 scr[2] = ax*n[1]/GP_FX - ay*n[0]/GP_FX
224 gp_norm3(scr)
225 // b = n x t
226 scr[3] = n[1]*scr[2]/GP_FX - n[2]*scr[1]/GP_FX
227 scr[4] = n[2]*scr[0]/GP_FX - n[0]*scr[2]/GP_FX
228 scr[5] = n[0]*scr[1]/GP_FX - n[1]*scr[0]/GP_FX
229 let u1: i64 = gp_rnd(st)
230 let u2: i64 = gp_rnd(st)
231 let r: i64 = vm_isqrt(u1*GP_FX) // sqrt(u1) as an fx1024 fraction
232 let phi: i64 = u2*GP_CIRCLE/GP_FX
233 let x: i64 = r*it_cos4096(phi)/GP_CIRCLE
234 let z: i64 = r*it_sin4096(phi)/GP_CIRCLE
235 var yy: i64 = GP_FX*GP_FX - x*x - z*z
236 if yy < 0 { yy = 0 }
237 let y: i64 = vm_isqrt(yy)
238 d[0] = (x*scr[0] + y*n[0] + z*scr[3])/GP_FX
239 d[1] = (x*scr[1] + y*n[1] + z*scr[4])/GP_FX
240 d[2] = (x*scr[2] + y*n[2] + z*scr[5])/GP_FX
241 gp_norm3(d)
242 return 0
243}
244// one path. L[0..2] receives throughput(fx1024) x radiance(0..255) summed over the path's light events.
245func gp_trace(ox: i64, oy: i64, oz: i64, dx: i64, dy: i64, dz: i64, seed: i64, Wp: *i64, st: *i64,
246 hitb: *i64, n: *i64, alb: *i64, scr: *i64, dir: *i64, sky: *i64, L: *i64, stats: *i64) -> i64 {
247 var tr: i64 = GP_FX
248 var tg: i64 = GP_FX
249 var tb: i64 = GP_FX
250 L[0] = 0; L[1] = 0; L[2] = 0
251 var cx: i64 = ox; var cy: i64 = oy; var cz: i64 = oz
252 var ddx: i64 = dx; var ddy: i64 = dy; var ddz: i64 = dz
253 var b: i64 = 0
254 var alive: i64 = 1
255 while alive == 1 {
256 let hit: i64 = gp_march(cx, cy, cz, ddx, ddy, ddz, seed, hitb)
257 stats[GP_ST_MARCHES] = stats[GP_ST_MARCHES] + 1
258 var twater: i64 = 0 - 1
259 if ddy < 0 { twater = (WATER - cy)*GP_FX/ddy }
260 var surf: i64 = 0
261 if hit == 1 { if hitb[4] >= WATER { surf = 1 } }
262 if twater > 0 { if hit == 0 { surf = 2 } else { if twater < hitb[5] { surf = 2 } } }
263 if surf == 0 {
264 wg_sky(ddx, ddy, ddz, Wp, sky)
265 L[0] = L[0] + tr*gp_lin(sky[0]); L[1] = L[1] + tg*gp_lin(sky[1]); L[2] = L[2] + tb*gp_lin(sky[2])
266 stats[GP_ST_SKY] = stats[GP_ST_SKY] + 1
267 alive = 0
268 }
269 if surf == 2 {
270 // one specular event: the mirrored sky, tinted by the deep-water colour, then the path ends
271 wg_sky(ddx, 0 - ddy, ddz, Wp, sky)
272 L[0] = L[0] + tr*(gp_lin(GP_WATER_R) + gp_lin(sky[0]))/2
273 L[1] = L[1] + tg*(gp_lin(GP_WATER_G) + gp_lin(sky[1]))/2
274 L[2] = L[2] + tb*(gp_lin(GP_WATER_B) + gp_lin(sky[2]))/2
275 stats[GP_ST_WATER] = stats[GP_ST_WATER] + 1
276 alive = 0
277 }
278 if surf == 1 {
279 let hx: i64 = hitb[1]
280 let hy: i64 = hitb[4]
281 let hz: i64 = hitb[3]
282 gp_normal(hx, hz, seed, n)
283 gp_albedo(hx, hz, hy, n[1], seed, alb)
284 var nd: i64 = (n[0]*Wp[0] + n[1]*Wp[1] + n[2]*Wp[2])/GP_FX
285 if nd < 0 { nd = 0 }
286 let pen: i64 = wg_sun_pen(seed, hx, hy, hz, Wp)
287 let dl: i64 = nd*pen/GP_FX // direct term, fx1024
288 // linear albedo and linear sun colour: the palette rows and the weather table are display values
289 let ar: i64 = gp_lin(alb[0]); let ag: i64 = gp_lin(alb[1]); let ab: i64 = gp_lin(alb[2])
290 L[0] = L[0] + tr*(ar*gp_lin(Wp[3])/(GP_MAXCH*GP_FX))*GP_SUN_OVER_SKY*dl/GP_FX
291 L[1] = L[1] + tg*(ag*gp_lin(Wp[4])/(GP_MAXCH*GP_FX))*GP_SUN_OVER_SKY*dl/GP_FX
292 L[2] = L[2] + tb*(ab*gp_lin(Wp[5])/(GP_MAXCH*GP_FX))*GP_SUN_OVER_SKY*dl/GP_FX
293 stats[GP_ST_TERRAIN] = stats[GP_ST_TERRAIN] + 1
294 tr = tr*ar/(GP_MAXCH*GP_FX); tg = tg*ag/(GP_MAXCH*GP_FX); tb = tb*ab/(GP_MAXCH*GP_FX)
295 if b >= GP_BOUNCES { alive = 0 }
296 else {
297 gp_cosine_dir(n, st, scr, dir)
298 cx = hx + n[0]*GP_BIAS/GP_FX
299 cy = hy + n[1]*GP_BIAS/GP_FX
300 cz = hz + n[2]*GP_BIAS/GP_FX
301 ddx = dir[0]; ddy = dir[1]; ddz = dir[2]
302 }
303 }
304 b = b + 1
305 }
306 return 0
307}
308// mean radiance (fx1024 x 0..255) -> display value 0..255: Reinhard at a declared exposure, then gamma-2 encode ONCE
309func gp_encode(m: i64) -> i64 {
310 var x: i64 = m*GP_EXPOSURE_NUM/GP_EXPOSURE_DEN
311 if x < 0 { x = 0 }
312 let white: i64 = GP_MAXCH*GP_FX*GP_FX // throughput(fx) x radiance(fx) of a full-white source
313 let y: i64 = x*white/(x + white) // 0..white
314 let c: i64 = vm_isqrt(y*GP_MAXCH/(GP_FX*GP_FX)) // sqrt(0..255*255) = 0..255
315 return gp_clamp(c, 0, GP_MAXCH)
316}