code wiki / (root) / nx_gi_path_lib.nx

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}