nx_nxa_texbake_decode_t279.nx source
↩ module page · 562 lines · 25908 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// NTB_N_MID / NTB_N_SCALE deliberately STAY here: a byte midpoint and scale are an ATLAS FORMAT
250// decision, not a property of skin, and nothing outside an atlas should ever need them.
251const NTB_N_MID: i64 = 128
252const NTB_N_SCALE: i64 = 127
253const NTB_GREY_MAX: i64 = 255
254
255// F0 = ((n-1)/(n+1))^2, computed from the index above. Returns x1000.
256// Explicit normal-map transfer contracts; IDs are wire semantics, not inferred from pixels.
257// Numerators retain exact RGB8 quantization. Normalize the decoded vector after XY scale
258// and tangent-frame transformation; this decoder does not claim unit length or provenance.
259const NTB_NORMAL_ENCODING_NATIVE_MID_SCALE: i64 = 1
260const NTB_NORMAL_ENCODING_GLTF_UNORM: i64 = 2
261func ntb_normal_decode_rgb8(encoding:i64,r:i64,g:i64,b:i64,out3:*i64)->i64 {
262 if (out3 as i64)==0{return 0-1}
263 if r<0||r>NTB_GREY_MAX||g<0||g>NTB_GREY_MAX||b<0||b>NTB_GREY_MAX{return 0-2}
264 if encoding==NTB_NORMAL_ENCODING_NATIVE_MID_SCALE{
265 out3[0]=r-NTB_N_MID;out3[1]=g-NTB_N_MID;out3[2]=b-NTB_N_MID
266 return NTB_N_SCALE
267 }
268 if encoding==NTB_NORMAL_ENCODING_GLTF_UNORM{
269 out3[0]=2*r-NTB_GREY_MAX;out3[1]=2*g-NTB_GREY_MAX;out3[2]=2*b-NTB_GREY_MAX
270 return NTB_GREY_MAX
271 }
272 return 0-3
273}
274func ntb_f0_skin() -> i64 {
275 let num: i64 = NTB_IOR_SKIN - NTB_ONE
276 let den: i64 = NTB_IOR_SKIN + NTB_ONE
277 let r: i64 = num * NTB_ONE / den
278 return r * r / NTB_ONE
279}
280func ntb_rough_of_region(reg: i64) -> i64 {
281 if reg == NTB_FACE { return NTB_ROUGH_BASE + NTB_ROUGH_FACE }
282 if reg == NTB_TORSO { return NTB_ROUGH_BASE + NTB_ROUGH_TORSO }
283 if reg == NTB_LIMBS { return NTB_ROUGH_BASE + NTB_ROUGH_LIMBS }
284 return NTB_ROUGH_BASE + NTB_ROUGH_GENS
285}
286// ★BOTH DELEGATE to nx_relief_lib, the ONE OWNER of the relief law and its field (2026-08-25). The
287// bodies moved VERBATIM -- the atlas bakes exactly what it baked before, and the SDF renderer now reads
288// the SAME field instead of a hand-copied constant beside a procedural stand-in.
289func ntb_texel_um(res: i64) -> i64 { return rlf_texel_um(res) }
290func ntb_relief_h(x: i64, y: i64, res: i64, seed: i64) -> i64 { return rlf_relief_h(x, y, res, seed) }
291// Encode a relief gradient (raw height deltas across two texels) as a tangent-space normal triple.
292// Slopes are PHYSICAL: microns of relief over microns of texel, so strength follows the resolution
293// instead of being dialled. ONE owner of this arithmetic -- the per-texel probe and the streaming
294// baker below both call it, so they cannot drift apart.
295// THE PHYSICAL CORE (2026-08-30): slopes already in MICRONS over a run in MICRONS -> RGB8 texel. The
296// per-region baker (nx_nxa_texbake_region_lib) composes multi-band heights in microns directly and
297// enters here; ntb_encode_normal below is the codomain-unit wrapper and DELEGATES -- one encoder, the
298// same integer operations in the same order, so the whole-body bake is unchanged by construction.
299func ntb_encode_normal_um(dzx_um: i64, dzy_um: i64, run_um: i64, out3: *i64) -> i64 {
300 let nx: i64 = 0 - dzx_um
301 let ny: i64 = 0 - dzy_um
302 let nz: i64 = run_um
303 var len: i64 = tm_isqrt(nx*nx + ny*ny + nz*nz)
304 if len < 1 { len = 1 }
305 out3[0] = NTB_N_MID + NTB_N_SCALE * nx / len
306 out3[1] = NTB_N_MID + NTB_N_SCALE * ny / len
307 out3[2] = NTB_N_MID + NTB_N_SCALE * nz / len
308 return 0
309}
310func ntb_encode_normal(dhx: i64, dhy: i64, res: i64, out3: *i64) -> i64 {
311 return ntb_encode_normal_um(rlf_slope_um(dhx), rlf_slope_um(dhy), rlf_run_um(res), out3)
312}
313// single-texel probe: four samples. Correct but O(4) per texel -- fine for a probe, NOT for a bake.
314func ntb_normal_at(x: i64, y: i64, res: i64, seed: i64, out3: *i64) -> i64 {
315 let hxp: i64 = ntb_relief_h(x+1, y, res, seed)
316 let hxm: i64 = ntb_relief_h(x-1, y, res, seed)
317 let hyp: i64 = ntb_relief_h(x, y+1, res, seed)
318 let hym: i64 = ntb_relief_h(x, y-1, res, seed)
319 return ntb_encode_normal(hxp - hxm, hyp - hym, res, out3)
320}
321// fill one scanline of relief heights at row y into hb[base..base+res)
322func ntb_fill_row(hb: *i64, base: i64, y: i64, res: i64, seed: i64) -> i64 {
323 var x: i64 = 0
324 while x < res { hb[base + x] = ntb_relief_h(x, y, res, seed); x = x + 1 }
325 return 0
326}
327
328// Bake ONE map of the PBR set. map_id: 0 albedo, 1 specular(F0), 2 gloss, 3 tangent-space normal.
329// Same TEXC grid and same region classification as the albedo bake -- one owner of the layout.
330func ntb_bake_pbr(b: *u8, flen: i64, res: i64, seed: i64, hue: i64, map_id: i64, out: *u8) -> i64 {
331 if map_id == NTB_MAP_ALBEDO { return ntb_bake_gene(b, flen, res, seed, hue, out) }
332 if map_id < 0 { nt_err("TEXBAKE-REFUSE unknown map id\n" as *u8); return 0 - NTB_E_BAD }
333 if map_id >= NTB_NMAP { nt_err("TEXBAKE-REFUSE unknown map id\n" as *u8); return 0 - NTB_E_BAD }
334 let xwo: i64 = nxa_find(b, flen, nxa_tag4("TEXC" as *u8))
335 if xwo < 0 { nt_err("TEXBAKE-REFUSE no TEXC section -- run nx_nxa_texc first\n" as *u8); return 0 - NTB_E_NOSEC }
336 let w: *i64 = b as *i64
337 let g: i64 = w[xwo + 2]
338 if g < 1 { nt_err("TEXBAKE-REFUSE TEXC grid invalid\n" as *u8); return 0 - NTB_E_BAD }
339 let regs: *i64 = sys_mmap(NTB_MAGIC_4096*8) as *i64
340 let perms: *i64 = sys_mmap(NTB_MAGIC_4096*8) as *i64
341 let nj: i64 = ntb_regions(b, flen, regs, perms)
342 if nj < 0 { return nj }
343 // resolve the per-region scalars ONCE, never per pixel
344 let spec: i64 = ntb_f0_skin() * NTB_GREY_MAX / NTB_ONE
345 let gl: *i64 = sys_mmap(NTB_NMAP*8) as *i64
346 var rq: i64 = 0
347 while rq < NTB_NMAP {
348 gl[rq] = (NTB_ONE - ntb_rough_of_region(rq)) * NTB_GREY_MAX / NTB_ONE
349 rq = rq + 1
350 }
351 // GE67b: one inverse map for BOTH the normal and the spec/gloss loops below, read from the
352 // layout's owner so the baker holds no copy of the allocation it does not decide.
353 let jm: *i64 = sys_mmap(res*res*8 + 64) as *i64
354 ntx_jmap(b, flen, res, nj, jm)
355 let n3: *i64 = sys_mmap(3*8) as *i64
356 // NORMAL is texel-space and region-independent, so it gets a STREAMING loop: three sliding
357 // scanlines of height, ONE noise evaluation per texel instead of four. At 4096 that is 16.7M
358 // evaluations rather than 67M -- the four-sample version was MEASURED as a gate timeout, which is
359 // what a complexity budget is for. Void texels take the IDENTITY normal, because a leak there
360 // should read as flat; the albedo map is what makes leaks visible, in cool grey.
361 // Edge convention: the gradient is one-sided on the first and last column and row.
362 if map_id == NTB_MAP_NORMAL {
363 let hb: *i64 = sys_mmap(3*res*8) as *i64
364 var r0: i64 = 0
365 var r1: i64 = 1
366 var r2: i64 = 2
367 ntb_fill_row(hb, r0*res, 0-1, res, seed)
368 ntb_fill_row(hb, r1*res, 0, res, seed)
369 ntb_fill_row(hb, r2*res, 1, res, seed)
370 var yy: i64 = 0
371 while yy < res {
372 var xx: i64 = 0
373 while xx < res {
374 let j2: i64 = jm[yy*res + xx]
375 var er: i64 = NTB_N_MID
376 var eg: i64 = NTB_N_MID
377 var eb: i64 = NTB_N_MID + NTB_N_SCALE
378 if j2 < nj {
379 var xp: i64 = xx + 1
380 if xp > res - 1 { xp = res - 1 }
381 var xm: i64 = xx - 1
382 if xm < 0 { xm = 0 }
383 ntb_encode_normal(hb[r1*res+xp] - hb[r1*res+xm], hb[r2*res+xx] - hb[r0*res+xx], res, n3)
384 er = n3[0]
385 eg = n3[1]
386 eb = n3[2]
387 }
388 let o2: i64 = (yy*res + xx)*3
389 out[o2] = er as u8
390 out[o2+1] = eg as u8
391 out[o2+2] = eb as u8
392 xx = xx + 1
393 }
394 let rt: i64 = r0
395 r0 = r1
396 r1 = r2
397 r2 = rt
398 ntb_fill_row(hb, r2*res, yy+2, res, seed)
399 yy = yy + 1
400 }
401 sys_munmap(hb, 3*res*8)
402 return 0
403 }
404 var y: i64 = 0
405 while y < res {
406 var x: i64 = 0
407 while x < res {
408 let j: i64 = jm[y*res + x]
409 var r: i64 = NTB_VOID_R
410 var gg: i64 = NTB_VOID_G
411 var bb: i64 = NTB_VOID_B
412 if j < nj {
413 let reg: i64 = regs[j]
414 if map_id == NTB_MAP_SPEC { r = spec; gg = spec; bb = spec }
415 if map_id == NTB_MAP_GLOSS {
416 var q: i64 = 0
417 if reg >= 0 { if reg < NTB_NMAP { q = gl[reg] } }
418 r = q; gg = q; bb = q
419 }
420 if map_id == NTB_MAP_NORMAL {
421 ntb_normal_at(x, y, res, seed, n3)
422 r = n3[0]; gg = n3[1]; bb = n3[2]
423 }
424 }
425 let off: i64 = (y*res + x)*3
426 out[off] = r as u8
427 out[off+1] = gg as u8
428 out[off+2] = bb as u8
429 x = x + 1
430 }
431 y = y + 1
432 }
433 return 0
434}
435
436// the original five-argument contract, preserved exactly: bake at the genome midpoint
437func ntb_bake(b: *u8, flen: i64, res: i64, seed: i64, out: *u8) -> i64 {
438 return ntb_bake_gene(b, flen, res, seed, NTB_DEFAULT_HUE, out)
439}
440
441// atlas -> PNG (shared by CLI + gate; write_png wants *i64 pixels c=r+g<<8+b<<16)
442func ntb_write_atlas_png(atlas: *u8, res: i64, path: *u8) -> i64 {
443 let fbp: *i64 = sys_mmap(res*res*8) as *i64
444 var i: i64 = 0
445 while i < res*res {
446 fbp[i] = (atlas[i*3] as i64) + (atlas[i*3+1] as i64)*256 + (atlas[i*3+2] as i64)*NTB_MAGIC_65536
447 i = i + 1
448 }
449 write_png(fbp, res, res, path)
450 return 0
451}
452
453// bake + bind + render a 3-view textured turntable of the asset -> PNG. Uses the trimesh renderer's
454// existing per-fragment image path (tm_set_image + tm_vuv + smooth=2). Unbinds the image afterwards.
455func ntb_view(b: *u8, flen: i64, res: i64, seed: i64, outpng: *u8) -> i64 {
456 let atlas: *u8 = sys_mmap(res*res*3 + 64)
457 let rc: i64 = ntb_bake(b, flen, res, seed, atlas)
458 if rc != 0 { return rc }
459 let vwo: i64 = nxa_find(b, flen, nxa_tag4("VERT" as *u8))
460 let two: i64 = nxa_find(b, flen, nxa_tag4("TRIS" as *u8))
461 let xwo: i64 = nxa_find(b, flen, nxa_tag4("TEXC" as *u8))
462 if vwo < 0 { nt_err("TEXVIEW-REFUSE no VERT\n" as *u8); return 0 - NTB_E_NOSEC }
463 if two < 0 { nt_err("TEXVIEW-REFUSE no TRIS\n" as *u8); return 0 - NTB_E_NOSEC }
464 if xwo < 0 { nt_err("TEXVIEW-REFUSE no TEXC\n" as *u8); return 0 - NTB_E_NOSEC }
465 let w: *i64 = b as *i64
466 let nv: i64 = w[vwo]
467 let ntr: i64 = w[two]
468 if w[xwo] != nv { nt_err("TEXVIEW-REFUSE TEXC count does not match VERT\n" as *u8); return 0 - NTB_E_MISMATCH }
469 var xmin: i64 = w[vwo+1]
470 var xmax: i64 = xmin
471 var ymin: i64 = w[vwo+2]
472 var ymax: i64 = ymin
473 var zmin: i64 = w[vwo+3]
474 var zmax: i64 = zmin
475 var i: i64 = 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 if vx < xmin { xmin = vx }
481 if vx > xmax { xmax = vx }
482 if vy < ymin { ymin = vy }
483 if vy > ymax { ymax = vy }
484 if vz < zmin { zmin = vz }
485 if vz > zmax { zmax = vz }
486 i = i + 1
487 }
488 // up axis derived from the asset itself (ntb_up_axis): z-up native, y-up R22-ingested
489 let upax: i64 = ntb_up_axis(w, vwo, nv)
490 var spanz: i64 = zmax - zmin
491 if upax == 1 { spanz = ymax - ymin }
492 if spanz <= 0 { nt_err("TEXVIEW-REFUSE degenerate span\n" as *u8); return 0 - NTB_E_BAD }
493 let cx: i64 = (xmin+xmax)/2
494 let cy: i64 = (ymin+ymax)/2
495 let cz: i64 = (zmin+zmax)/2
496 tm_reset()
497 i = 0
498 while i < nv {
499 let vx: i64 = w[vwo + 1 + i*3]
500 let vy: i64 = w[vwo + 1 + i*3 + 1]
501 let vz: i64 = w[vwo + 1 + i*3 + 2]
502 // trimesh is y-up, z-depth. Native NXA (z-up) maps (x, z, y); R22-ingested (y-up) maps (x, y, z).
503 var uu: i64 = vz - cz
504 var dd: i64 = vy - cy
505 if upax == 1 { uu = vy - cy; dd = vz - cz }
506 let id: i64 = tm_vert((vx-cx)*NTB_MODEL_H/spanz, uu*NTB_MODEL_H/spanz, dd*NTB_MODEL_H/spanz)
507 tm_vcol(id, NTB_FLESH)
508 tm_vuv(id, w[xwo + 4 + i*3], w[xwo + 4 + i*3 + 1])
509 i = i + 1
510 }
511 var t: i64 = 0
512 while t < ntr {
513 let a: i64 = w[two + 1 + t*3]
514 let b2: i64 = w[two + 1 + t*3 + 1]
515 let c2: i64 = w[two + 1 + t*3 + 2]
516 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 }
517 tm_tri(a, b2, c2, NTB_FLESH)
518 t = t + 1
519 }
520 if tm_ovf() != 0 { nt_err("TEXVIEW-REFUSE mesh capacity overflow\n" as *u8); return 0 - NTB_E_BAD }
521 tm_compute_normals()
522 tm_set_image(atlas, res, res)
523 let npx: i64 = NTB_W*NTB_H
524 let zb: *i64 = sys_mmap(npx*8) as *i64
525 let cell: *i64 = sys_mmap(npx*8) as *i64
526 let GWD: i64 = NTB_W*3
527 let gal: *i64 = sys_mmap(GWD*NTB_H*8) as *i64
528 var gi: i64 = 0
529 while gi < GWD*NTB_H { gal[gi] = NTB_BG; gi = gi + 1 }
530 let yaws: *i64 = sys_mmap(3*8) as *i64
531 yaws[0] = NTB_MAGIC_1400
532 yaws[1] = NTB_MAGIC_3200
533 yaws[2] = NTB_MAGIC_5600
534 var filled: i64 = 0
535 var v: i64 = 0
536 while v < 3 {
537 var ci: i64 = 0
538 while ci < npx { cell[ci] = NTB_BG; ci = ci + 1 }
539 trimesh_zclear(zb, npx)
540 trimesh_render(cell, zb, NTB_W, NTB_H, yaws[v], 0-200, NTB_MAGIC_2600, 520, 2)
541 var y2: i64 = 0
542 while y2 < NTB_H {
543 var x2: i64 = 0
544 while x2 < NTB_W {
545 let pc: i64 = cell[y2*NTB_W + x2]
546 gal[y2*GWD + v*NTB_W + x2] = pc
547 if pc != NTB_BG { filled = filled + 1 }
548 x2 = x2 + 1
549 }
550 y2 = y2 + 1
551 }
552 v = v + 1
553 }
554 tm_set_image(0 as *u8, 0, 0)
555 write_png(gal, GWD, NTB_H, outpng)
556 nt_outs("TEXVIEW-OK nv=" as *u8); nt_outn(nv)
557 nt_outs(" tris=" as *u8); nt_outn(ntr)
558 nt_outs(" res=" as *u8); nt_outn(res)
559 nt_outs(" filled3=" as *u8); nt_outn(filled)
560 nt_outs("\n" as *u8)
561 return 0
562}