code wiki / (root) / nx_fbx2nxa.nx

nx_fbx2nxa.nx source

↩ module page · 224 lines · 11031 B

1// nx_fbx2nxa.nx -- binary FBX GEOMETRY -> NXANIM01 (VERT + TRIS). The missing first half of the 2// FBX rig ingest: nx_nxa_skin <in.fbx> <in.nxa> <out.nxa> fills SKEL+SKIN but requires an in.nxa 3// that already carries the mesh, and NOTHING produced one from an FBX -- fbx_read (nx_fbx_import, 4// the consolidated geometry reader) had exactly ONE caller and it was a probe that prints counts. 5// This organ is the second caller the consolidation was built for: fbx_read -> NXA writer, in the 6// exact section shapes nx_nxa_skin copies and nx_asset_floor_gate reads. 7// 8// usage: nx_fbx2nxa <in.fbx> <out.nxa> 9// exit 0 OK | 2 usage | 3 unreadable/not-FBX | 4 no-geometry | 5 multi-geometry (refused BY NAME) 10// | 6 write-failed 11// 12// UNITS, stated not guessed: fbxc_f64_fx returns value x 1024 (the lib's contract, its comment); 13// the FBX native unit is the CENTIMETRE (FBX SDK convention; Fab/UE exports ship cm). NXA VERT is 14// 0.01 mm (spec correction seq1285), so one cm = 1000 VERT units and the conversion is 15// fx * NXA_UNITS_PER_CM / FBXC_FX_SCALE. The receipt prints the measured height in mm so a donor 16// authored in some other unit is VISIBLE in one glance instead of silently wrong; and the axes the 17// rig floor grades (counts and ratios) are unit-independent, so a mis-united donor still counts 18// its joints correctly. UnitScaleFactor parsing is deliberately deferred until a donor measurably 19// needs it -- deferring is stated here, not hidden. 20// 21// MULTI-GEOMETRY IS REFUSED BY NAME, not merged wrong: fbx_read keeps the LARGEST Vertices array 22// but ACCUMULATES triangles across every Geometry node, and those indices reference each node's 23// OWN vertex space -- concatenating them against one vertex array is a shredded mesh with 24// plausible counts (the exact failure shape the import lane keeps meeting). One Geometry node is 25// the honest v1; the refusal names the count so the donor can be re-exported merged. 26// license_tier: ORIGINAL No hw writes (Rule 26). expect_exit: 0 27import "nx_syscalls.nx" 28import "nx_nxa.nx" 29import "nx_fbx_import.nx" 30 31const F2_EXIT_OK: i64 = 0 32const F2_EXIT_USAGE: i64 = 2 33const F2_EXIT_UNREADABLE: i64 = 3 34const F2_EXIT_NOGEOM: i64 = 4 35const F2_EXIT_MULTIGEOM: i64 = 5 36const F2_EXIT_WRITEFAIL: i64 = 6 37 38// fbxc_f64_fx returns value x 1024 (nx_fbx_core's stated contract) 39const FBXC_FX_SCALE: i64 = 1024 40// NXA VERT unit is 0.01 mm; one centimetre is 10 mm = 1000 such units 41const NXA_UNITS_PER_CM: i64 = 1000 42const F2_MM_PER_CM: i64 = 10 43// buffer bounds DERIVED from the input file: a vertex needs 3 doubles = 24 bytes of payload and a 44// triangle at least one 4-byte index, so the file size itself bounds both counts. Derived, never 45// picked; and fbx_read REFUSES nothing silently against them because they cannot be exceeded. 46const F2_VERT_PAYLOAD_BYTES: i64 = 24 47const F2_IDX_PAYLOAD_BYTES: i64 = 4 48const F2_HDR_BYTES: i64 = 32 49const F2_TOCE_BYTES: i64 = 32 50const F2_SECTIONS: i64 = 2 51const F2_WORD: i64 = 8 52const F2_COUNTS: i64 = 8 53 54func f2_puts(s: *u8) -> i64 { var n: i64 = 0; while s[n] != (0 as u8) { n = n + 1 } sys_write(1, s, n); return 0 } 55func f2_putn(v: i64) -> i64 { 56 if v == 0 { sys_write(1, "0" as *u8, 1); return 0 } 57 var m: i64 = v 58 if m < 0 { sys_write(1, "-" as *u8, 1); m = 0 - m } 59 let d: *u8 = sys_mmap(24) 60 var k: i64 = 0 61 while m > 0 { d[k] = (48 + (m % 10)) as u8; m = m / 10; k = k + 1 } 62 let o: *u8 = sys_mmap(24) 63 var w: i64 = 0 64 while w < k { o[w] = d[k - 1 - w]; w = w + 1 } 65 sys_write(1, o, k) 66 return 0 67} 68 69func main(argc: i64, argv: *i64) -> i64 { 70 if argc < 3 { 71 f2_puts("usage: nx_fbx2nxa <in.fbx> <out.nxa>\n" as *u8) 72 sys_exit(F2_EXIT_USAGE) 73 return F2_EXIT_USAGE 74 } 75 // size the read buffers from the file itself 76 let lp: *i64 = sys_mmap(16) as *i64 77 let probe: *u8 = sys_read_file(argv[1] as *u8, lp) 78 if (probe as i64) == 0 { 79 f2_puts("FBX2NXA REFUSED: cannot read input\n" as *u8) 80 sys_exit(F2_EXIT_UNREADABLE) 81 return F2_EXIT_UNREADABLE 82 } 83 let fsz: i64 = lp[0] 84 // UNIT SCALE (2026-08-23): measured, never assumed. All four real donors carry factor 1.0 85 // (GlobalSettings dumped 2026-08-23), so the old assumed_unit=cm happened to be right -- but a 86 // meters-authored FBX ships factor 100 and imported 100x small with no announcement. -2 is a 87 // present-but-undecodable-or-nonpositive factor: geometrically meaningless, only a forged or 88 // corrupt file carries one, so it REFUSES rather than guessing. 89 var usfm: i64 = fbxc_unit_scale_micro(probe, fsz) 90 if usfm == 0 - 2 { 91 f2_puts("FBX2NXA REFUSED: UnitScaleFactor present but not a positive double (forged or corrupt GlobalSettings)\n" as *u8) 92 sys_exit(F2_EXIT_UNREADABLE) 93 return F2_EXIT_UNREADABLE 94 } 95 if usfm == 0 - 1 { 96 f2_puts(" unit_scale=ABSENT assumed=1000000 micro (factor 1.0 -- the FBX property-template default)\n" as *u8) 97 usfm = FBXC_MICRO 98 } else { 99 f2_puts(" unit_scale_micro=" as *u8); f2_putn(usfm) 100 f2_puts(" source=GlobalSettings (factor-to-cm, applied below)\n" as *u8) 101 } 102 let vcap: i64 = fsz / F2_VERT_PAYLOAD_BYTES + 1 103 let fcap: i64 = fsz / F2_IDX_PAYLOAD_BYTES + 1 104 let vbuf: *i64 = sys_mmap(vcap * 3 * F2_WORD) as *i64 105 let fbuf: *i64 = sys_mmap(fcap * 3 * F2_WORD) as *i64 106 let counts: *i64 = sys_mmap(F2_COUNTS * F2_WORD) as *i64 107 let rc: i64 = fbx_read(argv[1] as *u8, vbuf, fbuf, vcap, fcap, counts) 108 if rc != 0 { 109 f2_puts("FBX2NXA REFUSED: fbx_read rc=" as *u8); f2_putn(rc) 110 f2_puts(" (-1 unreadable, -2 not-FBX, -4 no vertices)\n" as *u8) 111 sys_exit(F2_EXIT_UNREADABLE) 112 return F2_EXIT_UNREADABLE 113 } 114 let nv: i64 = counts[0] 115 let nt: i64 = counts[1] 116 let ngeom: i64 = counts[5] 117 let fails: i64 = counts[7] 118 f2_puts("FBX2NXA read nv=" as *u8); f2_putn(nv) 119 f2_puts(" ntri=" as *u8); f2_putn(nt) 120 f2_puts(" geometry_nodes=" as *u8); f2_putn(ngeom) 121 f2_puts(" decode_fails=" as *u8); f2_putn(fails) 122 f2_puts(" src_bytes=" as *u8); f2_putn(fsz) 123 f2_puts("\n" as *u8) 124 if nv <= 0 { f2_puts("FBX2NXA REFUSED: no geometry\n" as *u8); sys_exit(F2_EXIT_NOGEOM); return F2_EXIT_NOGEOM } 125 if nt <= 0 { f2_puts("FBX2NXA REFUSED: no triangles\n" as *u8); sys_exit(F2_EXIT_NOGEOM); return F2_EXIT_NOGEOM } 126 // MULTI-GEOMETRY IS SUPPORTED AS OF 2026-08-23: nx_fbx_import appends each Geometry node's 127 // vertices at a running base and offsets its indices by that base, so many nodes concatenate 128 // into one coherent space (a Fab character ships body/armor/hair as separate nodes; v1 of this 129 // organ refused at >1 and that blocked every real donor). The per-index bound check below is 130 // the guard that would catch any pairing failure -- it validates the GLOBAL index space. 131 // convert fx-cm -> 0.01 mm and measure the bbox so the unit assumption is VISIBLE 132 var minx: i64 = 0 133 var maxx: i64 = 0 134 var miny: i64 = 0 135 var maxy: i64 = 0 136 var minz: i64 = 0 137 var maxz: i64 = 0 138 var first: i64 = 1 139 var i: i64 = 0 140 while i < nv*3 { 141 // order: multiply both scale legs before dividing (precision), overflow-safe by bound: 142 // |fx| <= 210m = 21000cm x1024 ~ 2.2e7; x1000 = 2.2e10; x usfm(<=1e10 for factor 1e4) 143 // would overflow, so divide FBXC_MICRO between the multiplies; identity at factor 1.0. 144 let w: i64 = vbuf[i] * NXA_UNITS_PER_CM * (usfm / 1000) / (FBXC_MICRO / 1000) / FBXC_FX_SCALE 145 vbuf[i] = w 146 let ax: i64 = i % 3 147 if first == 1 { if ax == 2 { first = 0 } } 148 if ax == 0 { if w < minx { minx = w } if w > maxx { maxx = w } } 149 if ax == 1 { if w < miny { miny = w } if w > maxy { maxy = w } } 150 if ax == 2 { if w < minz { minz = w } if w > maxz { maxz = w } } 151 i = i + 1 152 } 153 // (precision note, chosen and documented: the factor is applied at MILLI granularity -- 154 // usfm/1000 -- because full-micro multiply overflows i64 at the vertex leg. Every Autodesk- 155 // documented factor is milli-exact; a sub-0.001-precision factor would lose <=0.1% and no 156 // real exporter emits one.) 157 f2_puts(" unit=cm x factor (fx scale " as *u8); f2_putn(FBXC_FX_SCALE) 158 f2_puts(") height_mm=" as *u8); f2_putn((maxy - miny) / F2_MM_PER_CM / F2_MM_PER_CM * F2_MM_PER_CM) 159 f2_puts(" bbox_units001mm y=[" as *u8); f2_putn(miny) 160 f2_puts("," as *u8); f2_putn(maxy) 161 f2_puts("]\n" as *u8) 162 163 // index bound check: every triangle index must reference the vertex table 164 var bad: i64 = 0 165 var t: i64 = 0 166 while t < nt*3 { 167 if fbuf[t] < 0 { bad = bad + 1 } 168 if fbuf[t] >= nv { bad = bad + 1 } 169 t = t + 1 170 } 171 if bad > 0 { 172 f2_puts("FBX2NXA REFUSED: " as *u8); f2_putn(bad) 173 f2_puts(" triangle indices out of the vertex table -- refusing to emit a shredded mesh\n" as *u8) 174 sys_exit(F2_EXIT_NOGEOM) 175 return F2_EXIT_NOGEOM 176 } 177 178 // ---- emit NXANIM01: header + 2-entry TOC + VERT + TRIS (the exact shape nx_nxa_skin emits) ---- 179 let vwl: i64 = 1 + nv*3 180 let twl: i64 = 1 + nt*3 181 let vpay: *i64 = sys_mmap(vwl * F2_WORD + F2_WORD) as *i64 182 vpay[0] = nv 183 var c: i64 = 0 184 while c < nv*3 { vpay[1 + c] = vbuf[c]; c = c + 1 } 185 let tpay: *i64 = sys_mmap(twl * F2_WORD + F2_WORD) as *i64 186 tpay[0] = nt 187 var c2: i64 = 0 188 while c2 < nt*3 { tpay[1 + c2] = fbuf[c2]; c2 = c2 + 1 } 189 190 let hdr: *i64 = sys_mmap(F2_HDR_BYTES + F2_SECTIONS*F2_TOCE_BYTES + F2_WORD) as *i64 191 hdr[0] = nxa_magic() 192 hdr[1] = 1 193 hdr[2] = F2_SECTIONS 194 var o: i64 = F2_HDR_BYTES + F2_SECTIONS*F2_TOCE_BYTES 195 hdr[4] = nxa_tag4("VERT" as *u8) 196 hdr[5] = o 197 hdr[6] = vwl 198 hdr[7] = nxa_check2(1, vpay, vwl) 199 o = o + vwl*F2_WORD 200 hdr[8] = nxa_tag4("TRIS" as *u8) 201 hdr[9] = o 202 hdr[10] = twl 203 hdr[11] = nxa_check2(1, tpay, twl) 204 // header word 3 is the TOC CHECKSUM (nxa_find refuses -3 corrupt without it) -- computed over 205 // the ns*4 TOC words AFTER they are filled, exactly as nxa_find recomputes it 206 hdr[3] = nxa_check2(1, ((hdr as i64) + F2_HDR_BYTES) as *i64, F2_SECTIONS*4) 207 208 let fd: i64 = sys_openat_wr(argv[2] as *u8, MODE_0644) 209 if fd < 0 { f2_puts("FBX2NXA REFUSED: cannot open output\n" as *u8); sys_exit(F2_EXIT_WRITEFAIL); return F2_EXIT_WRITEFAIL } 210 let hb: i64 = F2_HDR_BYTES + F2_SECTIONS*F2_TOCE_BYTES 211 var wr: i64 = sys_write(fd, hdr as *u8, hb) 212 wr = wr + sys_write(fd, vpay as *u8, vwl*F2_WORD) 213 wr = wr + sys_write(fd, tpay as *u8, twl*F2_WORD) 214 sys_close(fd) 215 let want: i64 = hb + vwl*F2_WORD + twl*F2_WORD 216 f2_puts("FBX2NXA-OK nv=" as *u8); f2_putn(nv) 217 f2_puts(" tris=" as *u8); f2_putn(nt) 218 f2_puts(" bytes=" as *u8); f2_putn(wr) 219 f2_puts(" of=" as *u8); f2_putn(want) 220 f2_puts("\n" as *u8) 221 if wr != want { f2_puts("FBX2NXA REFUSED: short write\n" as *u8); sys_exit(F2_EXIT_WRITEFAIL); return F2_EXIT_WRITEFAIL } 222 sys_exit(F2_EXIT_OK) 223 return F2_EXIT_OK 224}