code wiki / (root) / nx_nxa_texbake_lib.nx

nx_nxa_texbake_lib.nx source

↩ module page · 540 lines · 24890 B

1// nx_nxa_texbake_lib.nx -- ★TEXTURE LADDER rungs 2b+3+4 (debt 1786546859, successor of the TEXC format rung): 2// (2b) REGION GROUPING: every joint classified Face|Torso|Limbs|Gens from its OWN bind anatomy (permil of 3// the vertex z-span + laterality -- the same normalization basis nx_nxa_dyna uses, joint ladder is 4// shorter than the mesh so the VERTEX span is the ruler). 5// (3) ALBEDO BAKE: fills the TEXC atlas (grid g read FROM the TEXC section -- the wire format is the SSOT, 6// two copies of one capacity are two capacities) with per-region skin albedo + a WHISPER of fine grain. 7// Real skin is EVEN -- the coarse-blotch regression is banked (HONEST_vs_REAL_PEOPLE); grain is +-3. 8// (4) SAMPLER VIEW: binds the baked atlas to the trimesh renderer's EXISTING per-fragment image-texture 9// path (tm_set_image + tm_vuv + smooth=2 -- it was already there; this wires TEXC into it) and renders 10// a lit, z-buffered, textured turntable of the asset. check-before-build: NO new rasterizer. 11// Output atlas format == TM_IMG contract: res*res*3 u8 RGB row-major. 12// FAIL-CLOSED: no TEXC -> refuse (run nx_nxa_texc first); counts mismatch -> refuse. Deterministic by seed. 13// license_tier: ORIGINAL No hw writes (Rule 26). 14import "nx_syscalls.nx" 15import "nx_nxa.nx" 16import "nx_nxa_texc_lib.nx" 17import "nx_trimesh.nx" 18import "nx_png.nx" 19import "nx_skin_ita.nx" 20import "nx_relief_lib.nx" 21const NTB_MAGIC_4096: i64 = 4096 22const NTB_MAGIC_4095: i64 = 4095 23const NTB_MAGIC_65536: i64 = 65536 24const NTB_MAGIC_1400: i64 = 1400 25const NTB_MAGIC_3200: i64 = 3200 26const NTB_MAGIC_5600: i64 = 5600 27const NTB_MAGIC_2600: i64 = 2600 28 29// region ids 30const NTB_FACE: i64 = 0 31const NTB_TORSO: i64 = 1 32const NTB_LIMBS: i64 = 2 33const NTB_GENS: i64 = 3 34// classifier anatomy (permil of vertex z-span; anchors per the dyna/floor provenance: head 917, pelvis 580) 35const NTB_FACE_PERMIL: i64 = 860 36const NTB_GENS_LO: i64 = 540 37const NTB_GENS_HI: i64 = 620 38const NTB_LAT_DIV: i64 = 12 39const NTB_MID_DIV: i64 = 40 40const NTB_LEG_PERMIL: i64 = 500 41// per-region albedo: DERIVED FROM THE GENOME, not declared here (charsim R0, debt 1786549493 residue a). 42// Twelve free sRGB constants used to sit at this spot. They gave EVERY creature in the estate the same 43// skin, nothing derived them, and no test could falsify them. nx_skin_ita replaces all twelve: skin is 44// placed on the published ITA locus and driven by the genome's hue trait, and the per-region difference 45// is now an offset in L* -- a perceptually uniform axis, so "slightly lighter" means one thing everywhere. 46const NTB_DEFAULT_HUE: i64 = 500 // the genome midpoint, used only when a caller supplies no genome 47// empty tiles (no joint owns them): cool gray -- visibly NON-skin, so a sampling leak is SEEN 48const NTB_VOID_R: i64 = 118 49const NTB_VOID_G: i64 = 118 50const NTB_VOID_B: i64 = 124 51// grain: +-3 brightness hash noise (whisper of fine grain, never coarse blotch) 52const NTB_HASH_A: i64 = 374761393 53const NTB_HASH_B: i64 = 668265263 54const NTB_HASH_C: i64 = 1274126177 55// view framing (the proven gltf-gate camera) 56const NTB_W: i64 = 300 57const NTB_H: i64 = 460 58const NTB_BG: i64 = 24 + 26*256 + 34*65536 59const NTB_FLESH: i64 = 222 + 184*256 + 158*65536 60const NTB_MODEL_H: i64 = 1900 61// refusal codes 62const NTB_E_BAD: i64 = 3 63const NTB_E_NOSEC: i64 = 4 64const NTB_E_MISMATCH: i64 = 5 65 66// all three triangle indices must name a loaded vertex 67func ntb_idx3(a: i64, b: i64, c: i64, nv: i64) -> i64 { 68 if a < 0 { return 0 } 69 if a >= nv { return 0 } 70 if b < 0 { return 0 } 71 if b >= nv { return 0 } 72 if c < 0 { return 0 } 73 if c >= nv { return 0 } 74 return 1 75} 76 77// ---- UP-AXIS DETECTION (2026-08-23). Native NXA is authored z-up; the R22 glTF ingest emits 78// axes AS AUTHORED ("axes as authored, not remapped" in its own receipt), which for glTF means 79// y-up. Classifying or rendering a y-up asset under the z-up assumption normalizes by the body 80// DEPTH instead of the stature -- a ~7x scale error measured as the donor close-up defect -- and 81// reads joint height off the wrong axis. No container in this estate is x-up, so the decision is 82// y versus z, and between those two the STATURE always exceeds the body depth for a character: 83// the larger span IS the up axis. Derived from the asset's own vertices; no new constant. 84func ntb_axspan(w: *i64, vwo: i64, nv: i64, ax: i64) -> i64 { 85 var mn: i64 = w[vwo + 1 + ax] 86 var mx: i64 = mn 87 var i: i64 = 0 88 while i < nv { 89 let v: i64 = w[vwo + 1 + i*3 + ax] 90 if v < mn { mn = v } 91 if v > mx { mx = v } 92 i = i + 1 93 } 94 return mx - mn 95} 96func ntb_up_axis(w: *i64, vwo: i64, nv: i64) -> i64 { 97 let sy: i64 = ntb_axspan(w, vwo, nv, 1) 98 let sz: i64 = ntb_axspan(w, vwo, nv, 2) 99 if sy > sz { return 1 } 100 return 2 101} 102 103func ntb_region(permil: i64, absx: i64, spanz: i64) -> i64 { 104 105 if permil >= NTB_FACE_PERMIL { return NTB_FACE } 106 if permil >= NTB_GENS_LO { if permil <= NTB_GENS_HI { if absx < spanz/NTB_MID_DIV { return NTB_GENS } } } 107 if absx >= spanz/NTB_LAT_DIV { return NTB_LIMBS } 108 if permil < NTB_LEG_PERMIL { return NTB_LIMBS } 109 return NTB_TORSO 110} 111 112// classify every joint; regout[j]=region, permout[j]=permil. returns nj, or negative refusal. 113func ntb_regions(b: *u8, flen: i64, regout: *i64, permout: *i64) -> i64 { 114 let vwo: i64 = nxa_find(b, flen, nxa_tag4("VERT" as *u8)) 115 let swo: i64 = nxa_find(b, flen, nxa_tag4("SKEL" as *u8)) 116 if vwo < 0 { nt_err("TEXBAKE-REFUSE no valid VERT section\n" as *u8); return 0 - NTB_E_NOSEC } 117 if swo < 0 { nt_err("TEXBAKE-REFUSE no valid SKEL section\n" as *u8); return 0 - NTB_E_NOSEC } 118 let w: *i64 = b as *i64 119 let nv: i64 = w[vwo] 120 let nj: i64 = w[swo] 121 if nv < 1 { nt_err("TEXBAKE-REFUSE empty VERT\n" as *u8); return 0 - NTB_E_BAD } 122 if nj < 1 { nt_err("TEXBAKE-REFUSE empty SKEL\n" as *u8); return 0 - NTB_E_BAD } 123 let upax: i64 = ntb_up_axis(w, vwo, nv) 124 var zmin: i64 = w[vwo + 1 + upax] 125 var zmax: i64 = zmin 126 var i: i64 = 0 127 while i < nv { 128 let vz: i64 = w[vwo + 1 + i*3 + upax] 129 if vz < zmin { zmin = vz } 130 if vz > zmax { zmax = vz } 131 i = i + 1 132 } 133 let spanz: i64 = zmax - zmin 134 if spanz <= 0 { nt_err("TEXBAKE-REFUSE degenerate vertex span\n" as *u8); return 0 - NTB_E_BAD } 135 var j: i64 = 0 136 while j < nj { 137 let jx: i64 = w[swo + 1 + j*8 + 1] 138 let jz: i64 = w[swo + 1 + j*8 + 1 + upax] 139 var absx: i64 = jx 140 if absx < 0 { absx = 0 - absx } 141 let permil: i64 = (jz - zmin)*1000/spanz 142 regout[j] = ntb_region(permil, absx, spanz) 143 permout[j] = permil 144 j = j + 1 145 } 146 return nj 147} 148 149// bake the albedo atlas into out (res*res*3 u8). Atlas layout comes FROM the TEXC section (grid g). 150// THE R0 CONTRACT: a region's albedo for a given genome, on the published ITA locus. 151func ntb_albedo_genome(hue: i64, reg: i64, out3: *i64) -> i64 { 152 return si_albedo_of_gene(hue, si_dl_of_region(reg), out3) 153} 154 155// Bake with an explicit genome. ntb_bake keeps its original five-argument contract by delegating here 156// with the midpoint, so every existing caller is unchanged and nothing had to be migrated. 157func ntb_bake_gene(b: *u8, flen: i64, res: i64, seed: i64, hue: i64, out: *u8) -> i64 { 158 let xwo: i64 = nxa_find(b, flen, nxa_tag4("TEXC" as *u8)) 159 if xwo < 0 { nt_err("TEXBAKE-REFUSE no TEXC section -- run nx_nxa_texc first\n" as *u8); return 0 - NTB_E_NOSEC } 160 let w: *i64 = b as *i64 161 let g: i64 = w[xwo + 2] 162 if g < 1 { nt_err("TEXBAKE-REFUSE TEXC grid invalid\n" as *u8); return 0 - NTB_E_BAD } 163 let regs: *i64 = sys_mmap(NTB_MAGIC_4096*8) as *i64 164 let perms: *i64 = sys_mmap(NTB_MAGIC_4096*8) as *i64 165 let nj: i64 = ntb_regions(b, flen, regs, perms) 166 if nj < 0 { return nj } 167 if nj > NTB_MAGIC_4095 { nt_err("TEXBAKE-REFUSE joint count over classifier capacity\n" as *u8); return 0 - NTB_E_BAD } 168 // Resolve the four region albedos ONCE. The locus does root-finding, so calling it per pixel would 169 // put a bisection inside a res*res loop -- 4 calls here, never 4 million. 170 let pal: *i64 = sys_mmap(4*3*8) as *i64 171 let tmp3: *i64 = sys_mmap(3*8) as *i64 172 var rq: i64 = 0 173 while rq < 4 { 174 ntb_albedo_genome(hue, rq, tmp3) 175 pal[rq*3] = tmp3[0] 176 pal[rq*3+1] = tmp3[1] 177 pal[rq*3+2] = tmp3[2] 178 rq = rq + 1 179 } 180 // GE67b: the texel->joint map is READ FROM THE LAYOUT'S OWNER (ntx_jmap), never recomputed 181 // from the grid here. That local copy is exactly what broke the hour the allocation changed: 182 // ty*g+tx named the wrong joint under a per-region atlas, so the face quadrant took torso and 183 // limb tones and texels past nj fell to the VOID grey. An unowned texel now reads nj, which 184 // the SAME range test below already sends to VOID. 185 let jm: *i64 = sys_mmap(res*res*8 + 64) as *i64 186 ntx_jmap(b, flen, res, nj, jm) 187 var y: i64 = 0 188 while y < res { 189 var x: i64 = 0 190 while x < res { 191 let j: i64 = jm[y*res + x] 192 var r: i64 = NTB_VOID_R 193 var gg: i64 = NTB_VOID_G 194 var bb: i64 = NTB_VOID_B 195 if j < nj { 196 let reg: i64 = regs[j] 197 if reg >= 0 { if reg < 4 { r = pal[reg*3]; gg = pal[reg*3+1]; bb = pal[reg*3+2] } } 198 let h: i64 = x*NTB_HASH_A + y*NTB_HASH_B + seed*NTB_HASH_C 199 let d: i64 = (h >> 10) % 7 - 3 200 r = r + d 201 gg = gg + d 202 bb = bb + d 203 if r < 0 { r = 0 } 204 if r > 255 { r = 255 } 205 if gg < 0 { gg = 0 } 206 if gg > 255 { gg = 255 } 207 if bb < 0 { bb = 0 } 208 if bb > 255 { bb = 255 } 209 } 210 let off: i64 = (y*res + x)*3 211 out[off] = r as u8 212 out[off+1] = gg as u8 213 out[off+2] = bb as u8 214 x = x + 1 215 } 216 y = y + 1 217 } 218 return 0 219} 220 221// ================= PBR MAP SET (charsim R4, debt 1786549493 residue b+d) ================= 222// FOUR maps from ONE pass each, so peak memory is ONE map and never four. Every constant below is 223// either COMPUTED from a published physical quantity or declared as a model with its units. 224const NTB_MAP_ALBEDO: i64 = 0 225const NTB_MAP_SPEC: i64 = 1 226const NTB_MAP_GLOSS: i64 = 2 227const NTB_MAP_NORMAL: i64 = 3 228const NTB_NMAP: i64 = 4 229// Skin optics. The refractive index of the skin/sebum surface layer is ~1.4 (published). The specular 230// reflectance F0 is COMPUTED from it by the Fresnel normal-incidence law -- it is never typed in, which 231// is the whole point: change the index and the map changes, because one derives the other. 232const NTB_IOR_SKIN: i64 = 1400 // x1000 233const NTB_ONE: i64 = 1000 234// Roughness: a base plus a per-region offset. STATED MODEL, not measured -- the T-zone carries more 235// sebum and reads smoother, limbs are drier and read rougher. 236const NTB_ROUGH_BASE: i64 = 500 237const NTB_ROUGH_FACE: i64 = 0 - 80 238const NTB_ROUGH_TORSO: i64 = 0 239const NTB_ROUGH_LIMBS: i64 = 60 240const NTB_ROUGH_GENS: i64 = 20 241// Micro-relief in MICRONS, with the wavelength it varies over. The normal-map strength is DERIVED from 242// these against the texel footprint rather than being an artist slider -- which is the parameter this 243// whole class of pipeline usually hand-waves. It also tells the truth about resolution: at 4096 over a 244// whole body a texel is ~415um, so fine wrinkles (~1.2mm) resolve and PORES (~100um) DO NOT. Pores need 245// a per-region atlas, not a bigger whole-body one; that is a rung, not a knob. 246// ★THE FOUR RELIEF CONSTANTS MOVED to nx_relief_lib 2026-08-25: RLF_RELIEF_UM / RLF_RELIEF_LAMBDA_UM / 247// RLF_BODY_UM / RLF_H_RANGE. They are read by the SDF RENDERER as well, and a constant read by two 248// organs but DECLARED in one of them is a mirror waiting to drift -- which is exactly what it did. 249// Byte encoding belongs to the shared transfer contract, not skin biology. 250import "nx_normal_encoding_contract.nx" 251// F0 = ((n-1)/(n+1))^2, computed from the index above. Returns x1000. 252func ntb_f0_skin() -> i64 { 253 let num: i64 = NTB_IOR_SKIN - NTB_ONE 254 let den: i64 = NTB_IOR_SKIN + NTB_ONE 255 let r: i64 = num * NTB_ONE / den 256 return r * r / NTB_ONE 257} 258func ntb_rough_of_region(reg: i64) -> i64 { 259 if reg == NTB_FACE { return NTB_ROUGH_BASE + NTB_ROUGH_FACE } 260 if reg == NTB_TORSO { return NTB_ROUGH_BASE + NTB_ROUGH_TORSO } 261 if reg == NTB_LIMBS { return NTB_ROUGH_BASE + NTB_ROUGH_LIMBS } 262 return NTB_ROUGH_BASE + NTB_ROUGH_GENS 263} 264// ★BOTH DELEGATE to nx_relief_lib, the ONE OWNER of the relief law and its field (2026-08-25). The 265// bodies moved VERBATIM -- the atlas bakes exactly what it baked before, and the SDF renderer now reads 266// the SAME field instead of a hand-copied constant beside a procedural stand-in. 267func ntb_texel_um(res: i64) -> i64 { return rlf_texel_um(res) } 268func ntb_relief_h(x: i64, y: i64, res: i64, seed: i64) -> i64 { return rlf_relief_h(x, y, res, seed) } 269// Encode a relief gradient (raw height deltas across two texels) as a tangent-space normal triple. 270// Slopes are PHYSICAL: microns of relief over microns of texel, so strength follows the resolution 271// instead of being dialled. ONE owner of this arithmetic -- the per-texel probe and the streaming 272// baker below both call it, so they cannot drift apart. 273// THE PHYSICAL CORE (2026-08-30): slopes already in MICRONS over a run in MICRONS -> RGB8 texel. The 274// per-region baker (nx_nxa_texbake_region_lib) composes multi-band heights in microns directly and 275// enters here; ntb_encode_normal below is the codomain-unit wrapper and DELEGATES -- one encoder, the 276// same integer operations in the same order, so the whole-body bake is unchanged by construction. 277func ntb_encode_normal_um(dzx_um: i64, dzy_um: i64, run_um: i64, out3: *i64) -> i64 { 278 let nx: i64 = 0 - dzx_um 279 let ny: i64 = 0 - dzy_um 280 let nz: i64 = run_um 281 var len: i64 = tm_isqrt(nx*nx + ny*ny + nz*nz) 282 if len < 1 { len = 1 } 283 out3[0] = NTB_N_MID + NTB_N_SCALE * nx / len 284 out3[1] = NTB_N_MID + NTB_N_SCALE * ny / len 285 out3[2] = NTB_N_MID + NTB_N_SCALE * nz / len 286 return 0 287} 288func ntb_encode_normal(dhx: i64, dhy: i64, res: i64, out3: *i64) -> i64 { 289 return ntb_encode_normal_um(rlf_slope_um(dhx), rlf_slope_um(dhy), rlf_run_um(res), out3) 290} 291// single-texel probe: four samples. Correct but O(4) per texel -- fine for a probe, NOT for a bake. 292func ntb_normal_at(x: i64, y: i64, res: i64, seed: i64, out3: *i64) -> i64 { 293 let hxp: i64 = ntb_relief_h(x+1, y, res, seed) 294 let hxm: i64 = ntb_relief_h(x-1, y, res, seed) 295 let hyp: i64 = ntb_relief_h(x, y+1, res, seed) 296 let hym: i64 = ntb_relief_h(x, y-1, res, seed) 297 return ntb_encode_normal(hxp - hxm, hyp - hym, res, out3) 298} 299// fill one scanline of relief heights at row y into hb[base..base+res) 300func ntb_fill_row(hb: *i64, base: i64, y: i64, res: i64, seed: i64) -> i64 { 301 var x: i64 = 0 302 while x < res { hb[base + x] = ntb_relief_h(x, y, res, seed); x = x + 1 } 303 return 0 304} 305 306// Bake ONE map of the PBR set. map_id: 0 albedo, 1 specular(F0), 2 gloss, 3 tangent-space normal. 307// Same TEXC grid and same region classification as the albedo bake -- one owner of the layout. 308func ntb_bake_pbr(b: *u8, flen: i64, res: i64, seed: i64, hue: i64, map_id: i64, out: *u8) -> i64 { 309 if map_id == NTB_MAP_ALBEDO { return ntb_bake_gene(b, flen, res, seed, hue, out) } 310 if map_id < 0 { nt_err("TEXBAKE-REFUSE unknown map id\n" as *u8); return 0 - NTB_E_BAD } 311 if map_id >= NTB_NMAP { nt_err("TEXBAKE-REFUSE unknown map id\n" as *u8); return 0 - NTB_E_BAD } 312 let xwo: i64 = nxa_find(b, flen, nxa_tag4("TEXC" as *u8)) 313 if xwo < 0 { nt_err("TEXBAKE-REFUSE no TEXC section -- run nx_nxa_texc first\n" as *u8); return 0 - NTB_E_NOSEC } 314 let w: *i64 = b as *i64 315 let g: i64 = w[xwo + 2] 316 if g < 1 { nt_err("TEXBAKE-REFUSE TEXC grid invalid\n" as *u8); return 0 - NTB_E_BAD } 317 let regs: *i64 = sys_mmap(NTB_MAGIC_4096*8) as *i64 318 let perms: *i64 = sys_mmap(NTB_MAGIC_4096*8) as *i64 319 let nj: i64 = ntb_regions(b, flen, regs, perms) 320 if nj < 0 { return nj } 321 // resolve the per-region scalars ONCE, never per pixel 322 let spec: i64 = ntb_f0_skin() * NTB_GREY_MAX / NTB_ONE 323 let gl: *i64 = sys_mmap(NTB_NMAP*8) as *i64 324 var rq: i64 = 0 325 while rq < NTB_NMAP { 326 gl[rq] = (NTB_ONE - ntb_rough_of_region(rq)) * NTB_GREY_MAX / NTB_ONE 327 rq = rq + 1 328 } 329 // GE67b: one inverse map for BOTH the normal and the spec/gloss loops below, read from the 330 // layout's owner so the baker holds no copy of the allocation it does not decide. 331 let jm: *i64 = sys_mmap(res*res*8 + 64) as *i64 332 ntx_jmap(b, flen, res, nj, jm) 333 let n3: *i64 = sys_mmap(3*8) as *i64 334 // NORMAL is texel-space and region-independent, so it gets a STREAMING loop: three sliding 335 // scanlines of height, ONE noise evaluation per texel instead of four. At 4096 that is 16.7M 336 // evaluations rather than 67M -- the four-sample version was MEASURED as a gate timeout, which is 337 // what a complexity budget is for. Void texels take the IDENTITY normal, because a leak there 338 // should read as flat; the albedo map is what makes leaks visible, in cool grey. 339 // Edge convention: the gradient is one-sided on the first and last column and row. 340 if map_id == NTB_MAP_NORMAL { 341 let hb: *i64 = sys_mmap(3*res*8) as *i64 342 var r0: i64 = 0 343 var r1: i64 = 1 344 var r2: i64 = 2 345 ntb_fill_row(hb, r0*res, 0-1, res, seed) 346 ntb_fill_row(hb, r1*res, 0, res, seed) 347 ntb_fill_row(hb, r2*res, 1, res, seed) 348 var yy: i64 = 0 349 while yy < res { 350 var xx: i64 = 0 351 while xx < res { 352 let j2: i64 = jm[yy*res + xx] 353 var er: i64 = NTB_N_MID 354 var eg: i64 = NTB_N_MID 355 var eb: i64 = NTB_N_MID + NTB_N_SCALE 356 if j2 < nj { 357 var xp: i64 = xx + 1 358 if xp > res - 1 { xp = res - 1 } 359 var xm: i64 = xx - 1 360 if xm < 0 { xm = 0 } 361 ntb_encode_normal(hb[r1*res+xp] - hb[r1*res+xm], hb[r2*res+xx] - hb[r0*res+xx], res, n3) 362 er = n3[0] 363 eg = n3[1] 364 eb = n3[2] 365 } 366 let o2: i64 = (yy*res + xx)*3 367 out[o2] = er as u8 368 out[o2+1] = eg as u8 369 out[o2+2] = eb as u8 370 xx = xx + 1 371 } 372 let rt: i64 = r0 373 r0 = r1 374 r1 = r2 375 r2 = rt 376 ntb_fill_row(hb, r2*res, yy+2, res, seed) 377 yy = yy + 1 378 } 379 sys_munmap(hb, 3*res*8) 380 return 0 381 } 382 var y: i64 = 0 383 while y < res { 384 var x: i64 = 0 385 while x < res { 386 let j: i64 = jm[y*res + x] 387 var r: i64 = NTB_VOID_R 388 var gg: i64 = NTB_VOID_G 389 var bb: i64 = NTB_VOID_B 390 if j < nj { 391 let reg: i64 = regs[j] 392 if map_id == NTB_MAP_SPEC { r = spec; gg = spec; bb = spec } 393 if map_id == NTB_MAP_GLOSS { 394 var q: i64 = 0 395 if reg >= 0 { if reg < NTB_NMAP { q = gl[reg] } } 396 r = q; gg = q; bb = q 397 } 398 if map_id == NTB_MAP_NORMAL { 399 ntb_normal_at(x, y, res, seed, n3) 400 r = n3[0]; gg = n3[1]; bb = n3[2] 401 } 402 } 403 let off: i64 = (y*res + x)*3 404 out[off] = r as u8 405 out[off+1] = gg as u8 406 out[off+2] = bb as u8 407 x = x + 1 408 } 409 y = y + 1 410 } 411 return 0 412} 413 414// the original five-argument contract, preserved exactly: bake at the genome midpoint 415func ntb_bake(b: *u8, flen: i64, res: i64, seed: i64, out: *u8) -> i64 { 416 return ntb_bake_gene(b, flen, res, seed, NTB_DEFAULT_HUE, out) 417} 418 419// atlas -> PNG (shared by CLI + gate; write_png wants *i64 pixels c=r+g<<8+b<<16) 420func ntb_write_atlas_png(atlas: *u8, res: i64, path: *u8) -> i64 { 421 let fbp: *i64 = sys_mmap(res*res*8) as *i64 422 var i: i64 = 0 423 while i < res*res { 424 fbp[i] = (atlas[i*3] as i64) + (atlas[i*3+1] as i64)*256 + (atlas[i*3+2] as i64)*NTB_MAGIC_65536 425 i = i + 1 426 } 427 write_png(fbp, res, res, path) 428 return 0 429} 430 431// bake + bind + render a 3-view textured turntable of the asset -> PNG. Uses the trimesh renderer's 432// existing per-fragment image path (tm_set_image + tm_vuv + smooth=2). Unbinds the image afterwards. 433func ntb_view(b: *u8, flen: i64, res: i64, seed: i64, outpng: *u8) -> i64 { 434 let atlas: *u8 = sys_mmap(res*res*3 + 64) 435 let rc: i64 = ntb_bake(b, flen, res, seed, atlas) 436 if rc != 0 { return rc } 437 let vwo: i64 = nxa_find(b, flen, nxa_tag4("VERT" as *u8)) 438 let two: i64 = nxa_find(b, flen, nxa_tag4("TRIS" as *u8)) 439 let xwo: i64 = nxa_find(b, flen, nxa_tag4("TEXC" as *u8)) 440 if vwo < 0 { nt_err("TEXVIEW-REFUSE no VERT\n" as *u8); return 0 - NTB_E_NOSEC } 441 if two < 0 { nt_err("TEXVIEW-REFUSE no TRIS\n" as *u8); return 0 - NTB_E_NOSEC } 442 if xwo < 0 { nt_err("TEXVIEW-REFUSE no TEXC\n" as *u8); return 0 - NTB_E_NOSEC } 443 let w: *i64 = b as *i64 444 let nv: i64 = w[vwo] 445 let ntr: i64 = w[two] 446 if w[xwo] != nv { nt_err("TEXVIEW-REFUSE TEXC count does not match VERT\n" as *u8); return 0 - NTB_E_MISMATCH } 447 var xmin: i64 = w[vwo+1] 448 var xmax: i64 = xmin 449 var ymin: i64 = w[vwo+2] 450 var ymax: i64 = ymin 451 var zmin: i64 = w[vwo+3] 452 var zmax: i64 = zmin 453 var i: i64 = 0 454 while i < nv { 455 let vx: i64 = w[vwo + 1 + i*3] 456 let vy: i64 = w[vwo + 1 + i*3 + 1] 457 let vz: i64 = w[vwo + 1 + i*3 + 2] 458 if vx < xmin { xmin = vx } 459 if vx > xmax { xmax = vx } 460 if vy < ymin { ymin = vy } 461 if vy > ymax { ymax = vy } 462 if vz < zmin { zmin = vz } 463 if vz > zmax { zmax = vz } 464 i = i + 1 465 } 466 // up axis derived from the asset itself (ntb_up_axis): z-up native, y-up R22-ingested 467 let upax: i64 = ntb_up_axis(w, vwo, nv) 468 var spanz: i64 = zmax - zmin 469 if upax == 1 { spanz = ymax - ymin } 470 if spanz <= 0 { nt_err("TEXVIEW-REFUSE degenerate span\n" as *u8); return 0 - NTB_E_BAD } 471 let cx: i64 = (xmin+xmax)/2 472 let cy: i64 = (ymin+ymax)/2 473 let cz: i64 = (zmin+zmax)/2 474 tm_reset() 475 i = 0 476 while i < nv { 477 let vx: i64 = w[vwo + 1 + i*3] 478 let vy: i64 = w[vwo + 1 + i*3 + 1] 479 let vz: i64 = w[vwo + 1 + i*3 + 2] 480 // trimesh is y-up, z-depth. Native NXA (z-up) maps (x, z, y); R22-ingested (y-up) maps (x, y, z). 481 var uu: i64 = vz - cz 482 var dd: i64 = vy - cy 483 if upax == 1 { uu = vy - cy; dd = vz - cz } 484 let id: i64 = tm_vert((vx-cx)*NTB_MODEL_H/spanz, uu*NTB_MODEL_H/spanz, dd*NTB_MODEL_H/spanz) 485 tm_vcol(id, NTB_FLESH) 486 tm_vuv(id, w[xwo + 4 + i*3], w[xwo + 4 + i*3 + 1]) 487 i = i + 1 488 } 489 var t: i64 = 0 490 while t < ntr { 491 let a: i64 = w[two + 1 + t*3] 492 let b2: i64 = w[two + 1 + t*3 + 1] 493 let c2: i64 = w[two + 1 + t*3 + 2] 494 if ntb_idx3(a, b2, c2, nv) == 0 { nt_err("TEXVIEW-REFUSE triangle index out of range\n" as *u8); return 0 - NTB_E_MISMATCH } 495 tm_tri(a, b2, c2, NTB_FLESH) 496 t = t + 1 497 } 498 if tm_ovf() != 0 { nt_err("TEXVIEW-REFUSE mesh capacity overflow\n" as *u8); return 0 - NTB_E_BAD } 499 tm_compute_normals() 500 tm_set_image(atlas, res, res) 501 let npx: i64 = NTB_W*NTB_H 502 let zb: *i64 = sys_mmap(npx*8) as *i64 503 let cell: *i64 = sys_mmap(npx*8) as *i64 504 let GWD: i64 = NTB_W*3 505 let gal: *i64 = sys_mmap(GWD*NTB_H*8) as *i64 506 var gi: i64 = 0 507 while gi < GWD*NTB_H { gal[gi] = NTB_BG; gi = gi + 1 } 508 let yaws: *i64 = sys_mmap(3*8) as *i64 509 yaws[0] = NTB_MAGIC_1400 510 yaws[1] = NTB_MAGIC_3200 511 yaws[2] = NTB_MAGIC_5600 512 var filled: i64 = 0 513 var v: i64 = 0 514 while v < 3 { 515 var ci: i64 = 0 516 while ci < npx { cell[ci] = NTB_BG; ci = ci + 1 } 517 trimesh_zclear(zb, npx) 518 trimesh_render(cell, zb, NTB_W, NTB_H, yaws[v], 0-200, NTB_MAGIC_2600, 520, 2) 519 var y2: i64 = 0 520 while y2 < NTB_H { 521 var x2: i64 = 0 522 while x2 < NTB_W { 523 let pc: i64 = cell[y2*NTB_W + x2] 524 gal[y2*GWD + v*NTB_W + x2] = pc 525 if pc != NTB_BG { filled = filled + 1 } 526 x2 = x2 + 1 527 } 528 y2 = y2 + 1 529 } 530 v = v + 1 531 } 532 tm_set_image(0 as *u8, 0, 0) 533 write_png(gal, GWD, NTB_H, outpng) 534 nt_outs("TEXVIEW-OK nv=" as *u8); nt_outn(nv) 535 nt_outs(" tris=" as *u8); nt_outn(ntr) 536 nt_outs(" res=" as *u8); nt_outn(res) 537 nt_outs(" filled3=" as *u8); nt_outn(filled) 538 nt_outs("\n" as *u8) 539 return 0 540}