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}