nx_fbx_core.nx source
↩ module page · 270 lines · 11815 B
1// nx_fbx_core.nx -- shared binary-FBX FORMAT knowledge. One place that knows how a Kaydara file is
2// laid out, so consumers differ only in WHAT THEY EXTRACT, not in how they read a node.
3//
4// ★WHY THIS EXISTS (seq1788). The node walk was written twice: nx_fbx_rig.nx (rig survey -- joints,
5// skin clusters, weights, animation curves; emits nothing by design) and nx_fbx_import.nx (geometry
6// -> fx1024). The PURPOSES are genuinely different; the FORMAT READING was one thing duplicated.
7// Worse, the two disagreed on capability: nx_fbx_rig handles BOTH u32 and u64 node offsets, while
8// the geometry reader refused v7500+ outright. Consolidating here means a node-format bug has ONE
9// home and geometry extraction inherits u64 support instead of re-deriving it.
10//
11// ★VERSION WIDENS THE HEADER, NOT JUST THE OFFSETS. v<7500: EndOffset/NumProps/PropListLen are u32
12// and the record header is 13 bytes. v>=7500: all three are u64 and the header is 25. Getting this
13// wrong does not error -- it mis-strides the whole tree and yields plausible counts over garbage,
14// which is exactly the failure shape this lane keeps meeting.
15// license_tier: ORIGINAL
16import "nx_deflate.nx"
17
18const FBXC_MAXOUT: i64 = 134217728
19// fbx_hdr() out[] layout
20const FBXC_END: i64 = 0
21const FBXC_NPROPS: i64 = 1
22const FBXC_PLEN: i64 = 2
23const FBXC_NAMELEN: i64 = 3
24const FBXC_NAMEOFF: i64 = 4
25const FBXC_PROPSOFF: i64 = 5
26const FBXC_HDRSZ: i64 = 6
27const FBXC_OUTN: i64 = 7
28
29func fbxc_u32(b: *u8, o: i64) -> i64 {
30 return ((b[o] & 0xff) as i64) | (((b[o+1] & 0xff) as i64) << 8) | (((b[o+2] & 0xff) as i64) << 16) | (((b[o+3] & 0xff) as i64) << 24)
31}
32func fbxc_u64(b: *u8, o: i64) -> i64 { return fbxc_u32(b, o) | (fbxc_u32(b, o + 4) << 32) }
33func fbxc_i32(b: *u8, o: i64) -> i64 {
34 var v: i64 = fbxc_u32(b, o)
35 if v > 2147483647 { v = v - 4294967296 }
36 return v
37}
38func fbxc_i64le(b: *u8, o: i64) -> i64 {
39 var v: i64 = 0
40 var i: i64 = 0
41 while i < 8 { v = v | (((b[o+i] & 0xff) as i64) << (i * 8)); i = i + 1 }
42 return v
43}
44// "Kaydara FBX Binary \0" + 0x1A 0x00 + version u32 -> version, or -1 if not an FBX
45func fbxc_version(b: *u8, n: i64) -> i64 {
46 if n < 32 { return 0 - 1 }
47 let sig: *u8 = "Kaydara" as *u8
48 var i: i64 = 0
49 while i < 7 { if b[i] != sig[i] { return 0 - 1 } i = i + 1 }
50 return fbxc_u32(b, 23)
51}
52// 1 if this version uses u64 node offsets and a 25-byte record header
53func fbxc_big(ver: i64) -> i64 { if ver >= 7500 { return 1 } return 0 }
54
55// Decode one node record header at `pos`. Writes FBXC_OUTN fields into out[].
56// Returns 0 ok, -1 if the record is truncated or is the null terminator.
57func fbxc_hdr(b: *u8, n: i64, pos: i64, big: i64, out: *i64) -> i64 {
58 var hs: i64 = 13
59 if big == 1 { hs = 25 }
60 if pos + hs > n { return 0 - 1 }
61 var eo: i64 = 0
62 var np: i64 = 0
63 var pl: i64 = 0
64 var nl: i64 = 0
65 if big == 1 {
66 eo = fbxc_u64(b, pos)
67 np = fbxc_u64(b, pos + 8)
68 pl = fbxc_u64(b, pos + 16)
69 nl = (b[pos + 24] & 0xff) as i64
70 } else {
71 eo = fbxc_u32(b, pos)
72 np = fbxc_u32(b, pos + 4)
73 pl = fbxc_u32(b, pos + 8)
74 nl = (b[pos + 12] & 0xff) as i64
75 }
76 if eo == 0 { return 0 - 1 }
77 out[FBXC_END] = eo
78 out[FBXC_NPROPS] = np
79 out[FBXC_PLEN] = pl
80 out[FBXC_NAMELEN] = nl
81 out[FBXC_NAMEOFF] = pos + hs
82 out[FBXC_PROPSOFF] = pos + hs + nl
83 out[FBXC_HDRSZ] = hs
84 return 0
85}
86// exact-length name compare
87func fbxc_name_is(b: *u8, o: i64, n: i64, s: *u8) -> i64 {
88 var i: i64 = 0
89 while s[i] != (0 as u8) {
90 if i >= n { return 0 }
91 if b[o+i] != s[i] { return 0 }
92 i = i + 1
93 }
94 if i != n { return 0 }
95 return 1
96}
97// bytes one property occupies, starting at its type char; -1 on an unknown type
98func fbxc_prop_size(b: *u8, p: i64) -> i64 {
99 let t: i64 = (b[p] & 0xff) as i64
100 if t == 89 { return 3 }
101 if t == 67 { return 2 }
102 if t == 73 { return 5 }
103 if t == 70 { return 5 }
104 if t == 68 { return 9 }
105 if t == 76 { return 9 }
106 if t == 83 { return 5 + fbxc_u32(b, p + 1) }
107 if t == 82 { return 5 + fbxc_u32(b, p + 1) }
108 if t == 102 { return 13 + fbxc_u32(b, p + 9) }
109 if t == 100 { return 13 + fbxc_u32(b, p + 9) }
110 if t == 108 { return 13 + fbxc_u32(b, p + 9) }
111 if t == 105 { return 13 + fbxc_u32(b, p + 9) }
112 if t == 98 { return 13 + fbxc_u32(b, p + 9) }
113 return 0 - 1
114}
115// IEEE754 double bit pattern -> value * 1024, integer-only (this toolchain has no float type).
116// Decompose sign/exponent/mantissa, restore the implicit leading 1, shift by (exp-1023-52+10).
117func fbxc_f64_fx(bits: i64) -> i64 {
118 if bits == 0 { return 0 }
119 let sign: i64 = (bits >> 63) & 1
120 let e: i64 = (bits >> 52) & 2047
121 if e == 0 { return 0 }
122 if e == 2047 { return 0 }
123 let m: i64 = (bits & 4503599627370495) | 4503599627370496
124 let sh: i64 = e - 1023 - 52 + 10
125 var v: i64 = 0
126 if sh >= 0 {
127 if sh > 40 { return 0 }
128 v = m << sh
129 } else {
130 let ns: i64 = 0 - sh
131 if ns > 62 { return 0 }
132 v = m >> ns
133 }
134 if sign == 1 { return 0 - v }
135 return v
136}
137// Decode an array property whose header starts just past the type char.
138// out[0]=payload address, out[1]=payload bytes, out[2]=element count, out[3]=1 if heap-allocated.
139// ★FBX compressed arrays are ZLIB streams ([CMF][FLG] + deflate + adler32), and nx_deflate_inflate
140// consumes RAW deflate -- the 2-byte header must be skipped or EVERY compressed array fails at the
141// first bit. That single omission cost 453 of 597 arrays and read as "41 vertices" with rc=0.
142func fbxc_array(b: *u8, p: i64, elemsz: i64, out: *i64) -> i64 {
143 let alen: i64 = fbxc_u32(b, p)
144 let enc: i64 = fbxc_u32(b, p + 4)
145 let clen: i64 = fbxc_u32(b, p + 8)
146 let data: i64 = p + 12
147 out[2] = alen
148 if enc == 0 {
149 out[0] = (b as i64) + data
150 out[1] = alen * elemsz
151 out[3] = 0
152 return 0
153 }
154 if enc != 1 { return 0 - 1 }
155 if clen < 3 { return 0 - 2 }
156 let want: i64 = alen * elemsz
157 if want > FBXC_MAXOUT { return 0 - 3 }
158 let src: *u8 = ((b as i64) + data + 2) as *u8
159 let r: *NxDeflateResult = nx_deflate_inflate(src, clen - 2, want + 64)
160 if (r as i64) == 0 { return 0 - 4 }
161 if r.error_code != 0 { return 0 - 5 }
162 out[0] = r.output_data as i64
163 out[1] = r.output_size
164 out[3] = 1
165 return 0
166}
167
168// ---- GlobalSettings unit scale (2026-08-23) --------------------------------------------------
169// FBX declares its length unit as GlobalSettings/Properties70/P"UnitScaleFactor" (a double,
170// factor-to-centimeters; the format's native unit). Every importer previously ASSUMED factor 1.0
171// (all four real donors measure exactly 1.0, so the assumption happened to hold -- but a donor
172// authored in meters ships factor 100 and would import 100x small, SILENTLY). One parser here,
173// composed by every FBX consumer, so the assumption becomes a measurement.
174// FBXC_MICRO: the factor travels as an integer in MICRO units (1.0 -> 1000000). Six decimal
175// digits cover every Autodesk-documented factor exactly (mm 0.1, cm 1, dm 10, m 100, in 2.54,
176// ft 30.48, yd 91.44) and the p20 decode below quantizes at 2^-20 ~ 1e-6, matching -- neither
177// leg wastes the other's precision.
178const FBXC_MICRO: i64 = 1000000
179const FBXC_P20: i64 = 1048576
180const FBXC_TOPOFF: i64 = 27
181// IEEE754 double -> value * 2^20, integer-only. Same decomposition as fbxc_f64_fx but at 2^20:
182// a mantissa-first multiply by 10^6 would overflow i64 (2^53 * 10^6 > 2^63), so decode at a
183// power-of-two scale (pure shifts, guarded) and convert to micro afterwards where the operand
184// is already small. sh guard 40: values >= 2^20 in magnitude refuse to 0, far above any unit.
185func fbxc_f64_p20(bits: i64) -> i64 {
186 if bits == 0 { return 0 }
187 let sign: i64 = (bits >> 63) & 1
188 let e: i64 = (bits >> 52) & 2047
189 if e == 0 { return 0 }
190 if e == 2047 { return 0 }
191 let m: i64 = (bits & 4503599627370495) | 4503599627370496
192 let sh: i64 = e - 1023 - 52 + 20
193 var v: i64 = 0
194 if sh >= 0 {
195 if sh > 40 { return 0 }
196 v = m << sh
197 } else {
198 let ns: i64 = 0 - sh
199 if ns > 62 { return 0 }
200 v = m >> ns
201 }
202 if sign == 1 { return 0 - v }
203 return v
204}
205// Walk top-level nodes for GlobalSettings -> Properties70 -> P records; return the
206// UnitScaleFactor in MICRO units. -1 = record absent (caller announces the assumed 1.0;
207// the FBX property template defaults it to 1.0, so absence is a defined state, not an error).
208// -2 = record present but not a decodable positive double (a caller must REFUSE: a zero or
209// negative unit factor has no geometric meaning and only a forged file carries one).
210func fbxc_unit_scale_micro(b: *u8, n: i64) -> i64 {
211 let ver: i64 = fbxc_version(b, n)
212 if ver <= 0 { return 0 - 2 }
213 let big: i64 = fbxc_big(ver)
214 let hd: *i64 = sys_mmap(FBXC_OUTN * 8) as *i64
215 var pos: i64 = FBXC_TOPOFF
216 while pos < n {
217 if fbxc_hdr(b, n, pos, big, hd) != 0 { break }
218 if fbxc_name_is(b, hd[FBXC_NAMEOFF], hd[FBXC_NAMELEN], "GlobalSettings" as *u8) == 1 {
219 // children start after this node's own properties
220 var cp: i64 = hd[FBXC_PROPSOFF] + hd[FBXC_PLEN]
221 let gend: i64 = hd[FBXC_END]
222 let ch: *i64 = sys_mmap(FBXC_OUTN * 8) as *i64
223 while cp < gend {
224 if fbxc_hdr(b, n, cp, big, ch) != 0 { break }
225 if fbxc_name_is(b, ch[FBXC_NAMEOFF], ch[FBXC_NAMELEN], "Properties70" as *u8) == 1 {
226 var pp: i64 = ch[FBXC_PROPSOFF] + ch[FBXC_PLEN]
227 let pend: i64 = ch[FBXC_END]
228 let ph: *i64 = sys_mmap(FBXC_OUTN * 8) as *i64
229 while pp < pend {
230 if fbxc_hdr(b, n, pp, big, ph) != 0 { break }
231 if fbxc_name_is(b, ph[FBXC_NAMEOFF], ph[FBXC_NAMELEN], "P" as *u8) == 1 {
232 // P props: S name, S type, S label, S flags, then the typed value
233 var q: i64 = ph[FBXC_PROPSOFF]
234 if (b[q] & 0xff) as i64 == 83 {
235 let slen: i64 = fbxc_u32(b, q + 1)
236 if fbxc_name_is(b, q + 5, slen, "UnitScaleFactor" as *u8) == 1 {
237 // skip properties until the D scalar carrying the value
238 var np: i64 = ph[FBXC_NPROPS]
239 var k: i64 = 0
240 while k < np {
241 let t: i64 = (b[q] & 0xff) as i64
242 if t == 68 {
243 let fx20: i64 = fbxc_f64_p20(fbxc_u64(b, q + 1))
244 if fx20 <= 0 { return 0 - 2 }
245 return fx20 * FBXC_MICRO / FBXC_P20
246 }
247 let psz: i64 = fbxc_prop_size(b, q)
248 if psz <= 0 { return 0 - 2 }
249 q = q + psz
250 k = k + 1
251 }
252 return 0 - 2
253 }
254 }
255 }
256 if ph[FBXC_END] <= pp { break }
257 pp = ph[FBXC_END]
258 }
259 return 0 - 1
260 }
261 if ch[FBXC_END] <= cp { break }
262 cp = ch[FBXC_END]
263 }
264 return 0 - 1
265 }
266 if hd[FBXC_END] <= pos { break }
267 pos = hd[FBXC_END]
268 }
269 return 0 - 1
270}