code wiki / (root) / nx_fbx_core.nx

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}