code wiki / (root) / nx_meshrig.nx

nx_meshrig.nx source

↩ module page · 610 lines · 23693 B

1// nx_meshrig.nx -- THE BINDER: a GENERATED mesh + a GENERATED skeleton -> a first-class rigged NXA. 2// 3// WHY THIS EXISTS (measured 2026-08-23, capsearch over 1,284 tools): the ONLY organ that filled 4// SKEL+SKIN was nx_nxa_skin, and it requires a BINARY FBX. So every GENERATED body was unriggable -- 5// it could not enter a world, could not be scored on the asset floor's rig axes, and could not 6// receive the soft-tissue or skeletal-animation work that shipped the same day; all of that reached 7// IMPORTED donors only. It is also why the one rigged character in the corpus was the corpus 8// MINIMUM (14,164 verts) while the donors ran to 423,919. 9// 10// nx_meshrig <mesh.nxmesh> <skel.txt> <out.nxa> 11// 12// INPUT A -- NXMSH2 triangle soup: byte0=N, byte5=2, u32 at 8 = nlayers, u32 at 12 = ntris, 13// header = 16 + nlayers*24, then ntris records of 84 bytes whose first 9 f32 are the triangle's 14// three xyz corners in MILLIMETRES. 15// INPUT B -- nx_skelgen's emitted rest skeleton: J rows (J idx parent side x y z) in world 16// MILLIMETRES. Its B rows are bone lengths and joint limits and are skipped here. 17// OUTPUT -- NXANIM01 carrying VERT + TRIS + SKEL + SKIN per knowledge/nxa_format_spec.md: 18// VERT and SKEL translations are 0.01 mm; SKIN is 8 words per vertex, [j0 j1 j2 j3][w0..w3 q12], 19// the four weights summing to EXACTLY 4096; every section carries the spec's rolling 20// order-sensitive checksum, and the TOC carries one over its own words. 21// 22// THE WEIGHTING METHOD, AND WHY -- the one real design choice, stated rather than buried: 23// Shepard normalised INVERSE DISTANCE (exponent 1) over the k nearest BONE SEGMENTS, where a 24// bone is a (joint,parent) pair -- the canon's OWN structure, never an invented body-part map. 25// k = 4 is STRUCTURAL, not a cap: the NXA SKIN record holds exactly four joint/weight lanes, so 26// the FORMAT fixes it and no author picked it. 27// Exponent 1 is used because it is the only exponent that requires no justification: every other 28// value is a sharpness knob, and a knob with no derivation is the magic number this estate bans. 29// Distance is to the SEGMENT (clamped projection), not to the joint -- a joint-distance bind 30// pulls mid-limb vertices toward the wrong end and is the classic candy-wrapper cause. 31// HONEST CEILING, NAMED NOT SILENTLY APPROXIMATED: bone-heat and bounded-biharmonic weights are 32// the higher-quality methods because they respect mesh CONNECTIVITY, so a weight cannot leak 33// across a gap the way a pure distance metric can (the inner thigh and the armpit are where this 34// shows). Both require a sparse Laplacian solve over the mesh, which in integer-only NishiLang is 35// an iterative solver needing its own convergence proof. That is the named next rung. 36// 37// UP-AXIS IS DERIVED FROM BOTH INPUTS, NEVER ASSUMED: the stature axis of each is its own LARGEST 38// EXTENT -- the estate's proven method, the same one that caught FBX Y-up against NXA Z-up on the 39// donors and confirmed axis 2 on the shipped character. A mismatch is corrected by a PROPER 40// rotation (determinant +1, never a mirror); an unhandled pairing REFUSES BY NAME rather than 41// shipping a silently wrong bind. 42// 43// NO CAPS BY CONSTRUCTION: the mesh is read with sys_read_file, which sizes from the file itself 44// and cannot short-read; every buffer is sized from the counts the inputs declare. (The incumbent 45// NXMSH2 readers carry BP_CAP = 33,554,432 and BP_MAXTRI = 400,000, both of which silently drop 46// data -- named here for the cap census, and deliberately not inherited.) 47// license_tier: ORIGINAL expect_exit: 0 48import "nx_syscalls.nx" 49 50const MR_MODE: i64 = 420 51const MR_Q12: i64 = 4096 52const MR_INF: i64 = 4 53const MR_BIG: i64 = 4611686018427387903 54const MR_M23: i64 = 8388607 55const MR_M24: i64 = 8388608 56const MR_MMQ: i64 = 100 57const MR_TRIB: i64 = 84 58const MR_HDR0: i64 = 16 59const MR_LAYB: i64 = 24 60const MR_RECIP: i64 = 1073741824 61const MR_CHK: i64 = 1000003 62const MR_NSECT: i64 = 4 63const MR_SP: i64 = 32 64const MR_NL: i64 = 10 65const MR_JCH: i64 = 74 66const MR_MINUS: i64 = 45 67const MR_D0: i64 = 48 68const MR_D9: i64 = 57 69 70func mr_puts(s: *u8) -> i64 { var n: i64 = 0; while s[n] != (0 as u8) { n = n + 1 } sys_write(1, s, n); return 0 } 71func mr_num(v: i64) -> i64 { 72 let b: *u8 = sys_mmap(32) as *u8 73 var x: i64 = v 74 var ng: i64 = 0 75 if x < 0 { ng = 1; x = 0 - x } 76 var i: i64 = 31 77 if x == 0 { b[i] = MR_D0 as u8; i = i - 1 } 78 while x > 0 { b[i] = (MR_D0 + x % 10) as u8; x = x / 10; i = i - 1 } 79 if ng == 1 { b[i] = MR_MINUS as u8; i = i - 1 } 80 sys_write(1, ((b as i64) + i + 1) as *u8, 31 - i) 81 return 0 82} 83func mr_kv(k: *u8, v: i64) -> i64 { mr_puts(k); mr_num(v); mr_puts("\n" as *u8); return 0 } 84func mr_refuse(reason: *u8) -> i64 { mr_puts("MESHRIG REFUSED: " as *u8); mr_puts(reason); mr_puts("\n" as *u8); return 0 } 85 86func mr_u32(b: *u8, o: i64) -> i64 { 87 return (b[o] as i64) + ((b[o+1] as i64) << 8) + ((b[o+2] as i64) << 16) + ((b[o+3] as i64) << 24) 88} 89// IEEE754 f32 millimetres -> integer 0.01 mm quanta (NishiLang has no float type) 90func mr_f32q(w: i64) -> i64 { 91 let sign: i64 = (w >> 31) & 1 92 let expo: i64 = (w >> 23) & 255 93 if expo == 0 { return 0 } 94 let mant: i64 = (w & MR_M23) | MR_M24 95 let sh: i64 = expo - 127 96 var v: i64 = 0 97 if sh >= 23 { if sh - 23 > 30 { return 0 } } 98 if sh >= 23 { v = mant * MR_MMQ * (1 << (sh - 23)) } 99 if sh < 23 { if 23 - sh > 62 { return 0 } } 100 if sh < 23 { v = (mant * MR_MMQ) >> (23 - sh) } 101 if sign == 1 { return 0 - v } 102 return v 103} 104// exact integer square root (Newton; monotone, terminates). A TRUE distance is required because 105// weighting on SQUARED distance would silently be exponent 2 -- a sharpness knob nobody derived. 106func mr_isqrt(n: i64) -> i64 { 107 if n <= 0 { return 0 } 108 var x: i64 = n 109 var y: i64 = (x + 1) / 2 110 while y < x { x = y; y = (x + n / x) / 2 } 111 return x 112} 113func mr_dseg(px: i64, py: i64, pz: i64, ax: i64, ay: i64, az: i64, bx: i64, by: i64, bz: i64) -> i64 { 114 let ux: i64 = bx - ax 115 let uy: i64 = by - ay 116 let uz: i64 = bz - az 117 let wx: i64 = px - ax 118 let wy: i64 = py - ay 119 let wz: i64 = pz - az 120 let uu: i64 = ux*ux + uy*uy + uz*uz 121 var t: i64 = 0 122 if uu > 0 { 123 let wu: i64 = wx*ux + wy*uy + wz*uz 124 t = (wu * MR_Q12) / uu 125 if t < 0 { t = 0 } 126 if t > MR_Q12 { t = MR_Q12 } 127 } 128 let cx: i64 = ax + (ux * t) / MR_Q12 129 let cy: i64 = ay + (uy * t) / MR_Q12 130 let cz: i64 = az + (uz * t) / MR_Q12 131 let dx: i64 = px - cx 132 let dy: i64 = py - cy 133 let dz: i64 = pz - cz 134 return mr_isqrt(dx*dx + dy*dy + dz*dz) 135} 136// rolling order-sensitive checksum, exactly as knowledge/nxa_format_spec.md defines it 137func mr_check(w: *i64, n: i64) -> i64 { 138 var c: i64 = 1 139 var i: i64 = 0 140 while i < n { c = c * MR_CHK + w[i]; i = i + 1 } 141 return c 142} 143func mr_tag(a: i64, b: i64, c: i64, d: i64) -> i64 { return a + (b << 8) + (c << 16) + (d << 24) } 144 145// signed-integer scanner. FLAG-BASED exits: writing a sentinel into the cursor would destroy the 146// position the caller needs (the banked cursor-clobber defect). 147func mr_scan_int(b: *u8, n: i64, pos: *i64) -> i64 { 148 var i: i64 = pos[0] 149 var go: i64 = 1 150 while go == 1 { 151 if i >= n { go = 0 } 152 if go == 1 { if b[i] == (MR_SP as u8) { i = i + 1 } else { go = 0 } } 153 } 154 var ng: i64 = 0 155 if i < n { if b[i] == (MR_MINUS as u8) { ng = 1; i = i + 1 } } 156 var v: i64 = 0 157 var got: i64 = 0 158 go = 1 159 while go == 1 { 160 if i >= n { go = 0 } 161 if go == 1 { 162 let c: i64 = b[i] as i64 163 var isd: i64 = 0 164 if c >= MR_D0 { if c <= MR_D9 { isd = 1 } } 165 if isd == 1 { v = v*10 + (c - MR_D0); i = i + 1; got = 1 } else { go = 0 } 166 } 167 } 168 pos[0] = i 169 if got == 0 { return MR_BIG } 170 if ng == 1 { return 0 - v } 171 return v 172} 173 174func mr_load_mesh(path: *u8, vxp: *i64) -> i64 { 175 let ln: *i64 = sys_mmap(16) as *i64 176 ln[0] = 0 177 let b: *u8 = sys_read_file(path, ln) 178 if (b as i64) == 0 { mr_refuse("mesh unreadable" as *u8); return 0 - 1 } 179 let n: i64 = ln[0] 180 if n < 44 { mr_refuse("mesh shorter than an NXMSH2 header" as *u8); return 0 - 1 } 181 if b[0] != (78 as u8) { mr_refuse("not NXMSH2 (magic byte 0)" as *u8); return 0 - 1 } 182 if b[5] != (50 as u8) { mr_refuse("not NXMSH2 (magic byte 5)" as *u8); return 0 - 1 } 183 let nlay: i64 = mr_u32(b, 8) 184 let nt: i64 = mr_u32(b, 12) 185 if nt <= 0 { mr_refuse("NXMSH2 declares zero triangles" as *u8); return 0 - 1 } 186 let hdr: i64 = MR_HDR0 + nlay * MR_LAYB 187 if hdr + nt * MR_TRIB > n { 188 mr_refuse("NXMSH2 declares more triangles than the file carries -- truncated input, refusing rather than binding a partial mesh" as *u8) 189 return 0 - 1 190 } 191 var t: i64 = 0 192 while t < nt { 193 var c: i64 = 0 194 while c < 9 { vxp[t*9 + c] = mr_f32q(mr_u32(b, hdr + t*MR_TRIB + c*4)); c = c + 1 } 195 t = t + 1 196 } 197 return nt 198} 199 200// nx_skelgen J rows -> parent + world position in 0.01 mm. Returns njoints (max idx + 1), or -1. 201func mr_load_skel(path: *u8, jp: *i64, jx: *i64, jy: *i64, jz: *i64, maxj: i64) -> i64 { 202 let ln: *i64 = sys_mmap(16) as *i64 203 ln[0] = 0 204 let b: *u8 = sys_read_file(path, ln) 205 if (b as i64) == 0 { mr_refuse("skeleton unreadable" as *u8); return 0 - 1 } 206 let n: i64 = ln[0] 207 let pos: *i64 = sys_mmap(16) as *i64 208 let v: *i64 = sys_mmap(8 * 8) as *i64 209 var i: i64 = 0 210 var nj: i64 = 0 211 var bad: i64 = 0 212 while i < n { 213 var isj: i64 = 0 214 if b[i] == (MR_JCH as u8) { 215 if i == 0 { isj = 1 } 216 if i > 0 { if b[i-1] == (MR_NL as u8) { isj = 1 } } 217 } 218 if isj == 1 { 219 pos[0] = i + 1 220 var f: i64 = 0 221 var ok: i64 = 1 222 while f < 6 { 223 let x: i64 = mr_scan_int(b, n, pos) 224 if x == MR_BIG { ok = 0 } 225 v[f] = x 226 f = f + 1 227 } 228 if ok == 1 { 229 let idx: i64 = v[0] 230 if idx >= 0 { 231 if idx < maxj { 232 jp[idx] = v[1] 233 jx[idx] = v[3] * MR_MMQ 234 jy[idx] = v[4] * MR_MMQ 235 jz[idx] = v[5] * MR_MMQ 236 if idx + 1 > nj { nj = idx + 1 } 237 } else { bad = bad + 1 } 238 } 239 } 240 } 241 var go: i64 = 1 242 while go == 1 { 243 if i >= n { go = 0 } 244 if go == 1 { if b[i] == (MR_NL as u8) { go = 0 } else { i = i + 1 } } 245 } 246 i = i + 1 247 } 248 if bad > 0 { mr_refuse("skeleton declares a joint index beyond the declared joint capacity" as *u8); return 0 - 1 } 249 return nj 250} 251 252// largest-extent axis of a coordinate triple stream: 0=x 1=y 2=z. The estate's proven stature test. 253func mr_up_axis(c: *i64, n: i64, stride: i64, base: i64, ext_out: *i64) -> i64 { 254 var lo0: i64 = MR_BIG 255 var hi0: i64 = 0 - MR_BIG 256 var lo1: i64 = MR_BIG 257 var hi1: i64 = 0 - MR_BIG 258 var lo2: i64 = MR_BIG 259 var hi2: i64 = 0 - MR_BIG 260 var i: i64 = 0 261 while i < n { 262 let a: i64 = c[i*stride + base] 263 let b: i64 = c[i*stride + base + 1] 264 let d: i64 = c[i*stride + base + 2] 265 if a < lo0 { lo0 = a } 266 if a > hi0 { hi0 = a } 267 if b < lo1 { lo1 = b } 268 if b > hi1 { hi1 = b } 269 if d < lo2 { lo2 = d } 270 if d > hi2 { hi2 = d } 271 i = i + 1 272 } 273 let e0: i64 = hi0 - lo0 274 let e1: i64 = hi1 - lo1 275 let e2: i64 = hi2 - lo2 276 ext_out[0] = e0 277 ext_out[1] = e1 278 ext_out[2] = e2 279 var ax: i64 = 0 280 var be: i64 = e0 281 if e1 > be { be = e1; ax = 1 } 282 if e2 > be { be = e2; ax = 2 } 283 return ax 284} 285 286func main(argc: i64, argv: *i64) -> i64 { 287 if argc < 4 { 288 mr_puts("usage: nx_meshrig <mesh.nxmesh> <skel.txt> <out.nxa>\n" as *u8) 289 mr_puts(" binds a GENERATED mesh to a GENERATED skeleton: emits VERT+TRIS+SKEL+SKIN\n" as *u8) 290 return 3 291 } 292 let mesh_path: *u8 = argv[1] as *u8 293 let skel_path: *u8 = argv[2] as *u8 294 let out_path: *u8 = argv[3] as *u8 295 296 // ---- skeleton ---- 297 let maxj: i64 = 4096 298 let jp: *i64 = sys_mmap(maxj*8) as *i64 299 let jx: *i64 = sys_mmap(maxj*8) as *i64 300 let jy: *i64 = sys_mmap(maxj*8) as *i64 301 let jz: *i64 = sys_mmap(maxj*8) as *i64 302 var z: i64 = 0 303 while z < maxj { jp[z] = 0 - 2; z = z + 1 } 304 let nj: i64 = mr_load_skel(skel_path, jp, jx, jy, jz, maxj) 305 if nj <= 0 { return 4 } 306 mr_kv("joints=" as *u8, nj) 307 308 // ---- mesh (uncapped: sized from the file itself) ---- 309 let lnq: *i64 = sys_mmap(16) as *i64 310 lnq[0] = 0 311 let probe: *u8 = sys_read_file(mesh_path, lnq) 312 if (probe as i64) == 0 { mr_refuse("mesh unreadable" as *u8); return 4 } 313 if lnq[0] < 44 { mr_refuse("mesh shorter than an NXMSH2 header" as *u8); return 4 } 314 let nt_dec: i64 = mr_u32(probe, 12) 315 if nt_dec <= 0 { mr_refuse("NXMSH2 declares zero triangles" as *u8); return 4 } 316 let vx: *i64 = sys_mmap(nt_dec*9*8 + 64) as *i64 317 let nt: i64 = mr_load_mesh(mesh_path, vx) 318 if nt < 0 { return 4 } 319 let nv: i64 = nt * 3 320 mr_kv("tris=" as *u8, nt) 321 mr_kv("verts=" as *u8, nv) 322 323 // ---- derive both up-axes from their own extents; align by a PROPER rotation or refuse ---- 324 let mext: *i64 = sys_mmap(32) as *i64 325 let sext: *i64 = sys_mmap(32) as *i64 326 let mup: i64 = mr_up_axis(vx, nv, 3, 0, mext) 327 let sxyz: *i64 = sys_mmap(nj*3*8 + 64) as *i64 328 var q: i64 = 0 329 while q < nj { sxyz[q*3] = jx[q]; sxyz[q*3+1] = jy[q]; sxyz[q*3+2] = jz[q]; q = q + 1 } 330 let sup: i64 = mr_up_axis(sxyz, nj, 3, 0, sext) 331 mr_kv("mesh_up_axis=" as *u8, mup) 332 mr_kv("skel_up_axis=" as *u8, sup) 333 if mup != sup { 334 var handled: i64 = 0 335 if sup == 1 { if mup == 2 { handled = 1 } } 336 if handled == 0 { 337 mr_refuse("up-axis pairing not handled by a proper rotation -- refusing rather than binding a silently mis-oriented skeleton" as *u8) 338 return 5 339 } 340 // skeleton Y-up -> mesh Z-up: rotate +90 about X, (x,y,z) -> (x,-z,y). Determinant +1. 341 var k: i64 = 0 342 while k < nj { 343 let oy: i64 = jy[k] 344 let oz: i64 = jz[k] 345 jy[k] = 0 - oz 346 jz[k] = oy 347 k = k + 1 348 } 349 mr_puts("aligned=rotX+90 (skeleton Y-up -> mesh Z-up, det +1, not a mirror)\n" as *u8) 350 } 351 352 // ---- SPEC CONFORMANCE: knowledge/nxa_format_spec.md declares VERT model space Z-UP. The 353 // NXMSH2 generators emit Y-up (measured, not assumed: nx_body_gen's female_v1 reports 354 // mesh_up_axis=1). Emitting a Y-up NXA would produce a file whose bytes disagree with the 355 // format that names them -- the same class as the POSE dt unit mismatch found the same day. 356 // Both mesh and skeleton are rotated together by the SAME proper rotation, so the bind is 357 // untouched: rotX+90, (x,y,z) -> (x,-z,y), determinant +1, never a mirror. 358 if mup == 1 { 359 var vi0: i64 = 0 360 while vi0 < nv { 361 let oy: i64 = vx[vi0*3+1] 362 let oz: i64 = vx[vi0*3+2] 363 vx[vi0*3+1] = 0 - oz 364 vx[vi0*3+2] = oy 365 vi0 = vi0 + 1 366 } 367 var k0: i64 = 0 368 while k0 < nj { 369 let jy0: i64 = jy[k0] 370 let jz0: i64 = jz[k0] 371 jy[k0] = 0 - jz0 372 jz[k0] = jy0 373 k0 = k0 + 1 374 } 375 mr_puts("spec_up=rotX+90 applied to BOTH mesh and skeleton (Y-up source -> Z-up NXA per spec, det +1)\n" as *u8) 376 } 377 if mup == 0 { 378 mr_refuse("mesh stature axis is X -- no proper rotation to the spec's Z-up is declared for this pairing, refusing rather than emitting a file whose bytes disagree with the format that names them" as *u8) 379 return 5 380 } 381 382 // ---- bones are the canon's OWN (joint,parent) pairs ---- 383 let bch: *i64 = sys_mmap(nj*8 + 64) as *i64 384 let bpa: *i64 = sys_mmap(nj*8 + 64) as *i64 385 var nb: i64 = 0 386 var j: i64 = 0 387 while j < nj { 388 let p: i64 = jp[j] 389 if p >= 0 { if p < nj { bch[nb] = j; bpa[nb] = p; nb = nb + 1 } } 390 j = j + 1 391 } 392 mr_kv("bones=" as *u8, nb) 393 if nb <= 0 { mr_refuse("skeleton has no (joint,parent) bone segments" as *u8); return 5 } 394 395 // ---- weights: Shepard inverse distance over the 4 nearest bone SEGMENTS ---- 396 let sj: *i64 = sys_mmap(nv*MR_INF*8 + 64) as *i64 397 let sw: *i64 = sys_mmap(nv*MR_INF*8 + 64) as *i64 398 let bd: *i64 = sys_mmap(MR_INF*8 + 64) as *i64 399 let bi: *i64 = sys_mmap(MR_INF*8 + 64) as *i64 400 let rc: *i64 = sys_mmap(MR_INF*8 + 64) as *i64 401 var vi: i64 = 0 402 while vi < nv { 403 let px: i64 = vx[vi*3] 404 let py: i64 = vx[vi*3+1] 405 let pz: i64 = vx[vi*3+2] 406 var k: i64 = 0 407 while k < MR_INF { bd[k] = MR_BIG; bi[k] = 0; k = k + 1 } 408 var bnum: i64 = 0 409 while bnum < nb { 410 let cj: i64 = bch[bnum] 411 let pj: i64 = bpa[bnum] 412 let d: i64 = mr_dseg(px, py, pz, jx[pj], jy[pj], jz[pj], jx[cj], jy[cj], jz[cj]) 413 var placed: i64 = 0 414 var s: i64 = 0 415 while s < MR_INF { 416 if placed == 0 { 417 if d < bd[s] { 418 var r: i64 = MR_INF - 1 419 while r > s { bd[r] = bd[r-1]; bi[r] = bi[r-1]; r = r - 1 } 420 bd[s] = d 421 bi[s] = cj 422 placed = 1 423 } 424 } 425 s = s + 1 426 } 427 bnum = bnum + 1 428 } 429 var sum: i64 = 0 430 k = 0 431 while k < MR_INF { 432 var dd: i64 = bd[k] 433 if dd >= MR_BIG { rc[k] = 0 } else { 434 if dd < 1 { dd = 1 } 435 rc[k] = MR_RECIP / dd 436 } 437 sum = sum + rc[k] 438 k = k + 1 439 } 440 if sum <= 0 { 441 sj[vi*MR_INF] = bi[0] 442 sw[vi*MR_INF] = MR_Q12 443 k = 1 444 while k < MR_INF { sj[vi*MR_INF+k] = 0; sw[vi*MR_INF+k] = 0; k = k + 1 } 445 } else { 446 var tot: i64 = 0 447 var big: i64 = 0 448 k = 0 449 while k < MR_INF { 450 let w: i64 = (rc[k] * MR_Q12) / sum 451 sj[vi*MR_INF+k] = bi[k] 452 sw[vi*MR_INF+k] = w 453 tot = tot + w 454 if w > sw[vi*MR_INF+big] { big = k } 455 k = k + 1 456 } 457 // the format requires the four lanes to sum to EXACTLY 4096; give the residue to the 458 // dominant lane so the correction can never invent a new influence. 459 sw[vi*MR_INF+big] = sw[vi*MR_INF+big] + (MR_Q12 - tot) 460 } 461 vi = vi + 1 462 } 463 464 // ---- ANTI-VACUITY EVIDENCE, MEASURED AND PRINTED, NOT ASSERTED ---- 465 // A RIGID bind (every vertex nailed to one bone at full weight) satisfies "the asset has a SKIN 466 // section" and produces candy-wrapper garbage. It is the trivial wrong implementation, and it is 467 // distinguishable from a real bind by exactly two numbers: under a rigid bind EVERY vertex has 468 // one influence and a dominant weight of 4096. Both are emitted here so the claim is checkable 469 // from the organ's own output rather than believed. 470 var blended: i64 = 0 471 var domsum: i64 = 0 472 var wsum_bad: i64 = 0 473 var vc: i64 = 0 474 while vc < nv { 475 var nz: i64 = 0 476 var dom: i64 = 0 477 var tot2: i64 = 0 478 var kk: i64 = 0 479 while kk < MR_INF { 480 let w: i64 = sw[vc*MR_INF+kk] 481 tot2 = tot2 + w 482 if w > 0 { nz = nz + 1 } 483 if w > dom { dom = w } 484 kk = kk + 1 485 } 486 if nz >= 2 { blended = blended + 1 } 487 domsum = domsum + dom 488 if tot2 != MR_Q12 { wsum_bad = wsum_bad + 1 } 489 vc = vc + 1 490 } 491 mr_kv("verts_with_multiple_influences=" as *u8, blended) 492 mr_kv("verts_with_single_influence=" as *u8, nv - blended) 493 mr_kv("mean_dominant_weight_q12=" as *u8, domsum / nv) 494 mr_kv("verts_whose_weights_do_not_sum_to_4096=" as *u8, wsum_bad) 495 if wsum_bad > 0 { 496 mr_refuse("weight lanes do not sum to the format's required 4096 -- refusing to emit a SKIN section the spec would call malformed" as *u8) 497 return 7 498 } 499 500 // ---- emit NXANIM01 ---- 501 let wv: i64 = 1 + 3*nv 502 let wt: i64 = 1 + 3*nt 503 let ws: i64 = 1 + 8*nj 504 let wk: i64 = 1 + 8*nv 505 let tocw: i64 = MR_NSECT * 4 506 let totw: i64 = 4 + tocw + wv + wt + ws + wk 507 let buf: *i64 = sys_mmap(totw*8 + 512) as *i64 508 let bb: *u8 = buf as *u8 509 // magic "NXANIM01" written as its 8 ASCII bytes (read back as one i64 by any reader) 510 bb[0] = 78 as u8 511 bb[1] = 88 as u8 512 bb[2] = 65 as u8 513 bb[3] = 78 as u8 514 bb[4] = 73 as u8 515 bb[5] = 77 as u8 516 bb[6] = 48 as u8 517 bb[7] = 49 as u8 518 buf[1] = 1 519 buf[2] = MR_NSECT 520 // TOC occupies words 4..(4+nsect*4); payloads follow 521 let toc0: i64 = 4 522 var wp: i64 = 4 + MR_NSECT*4 523 // VERT 524 let vert_w: i64 = wp 525 buf[wp] = nv 526 wp = wp + 1 527 var i2: i64 = 0 528 while i2 < nv { 529 buf[wp] = vx[i2*3] 530 buf[wp+1] = vx[i2*3+1] 531 buf[wp+2] = vx[i2*3+2] 532 wp = wp + 3 533 i2 = i2 + 1 534 } 535 // TRIS (triangle soup: corner c of triangle t is vertex 3t+c -- no dedupe, so no seam can open) 536 let tris_w: i64 = wp 537 buf[wp] = nt 538 wp = wp + 1 539 i2 = 0 540 while i2 < nt { 541 buf[wp] = i2*3 542 buf[wp+1] = i2*3 + 1 543 buf[wp+2] = i2*3 + 2 544 wp = wp + 3 545 i2 = i2 + 1 546 } 547 // SKEL: [parent][tx ty tz][qx qy qz qw q12]; rest skeleton carries no authored orientation, so 548 // the bind rotation is the IDENTITY quaternion and that is stated, not smuggled. 549 let skel_w: i64 = wp 550 buf[wp] = nj 551 wp = wp + 1 552 i2 = 0 553 while i2 < nj { 554 var par: i64 = jp[i2] 555 if par < 0 { par = 0 - 1 } 556 buf[wp] = par 557 buf[wp+1] = jx[i2] 558 buf[wp+2] = jy[i2] 559 buf[wp+3] = jz[i2] 560 buf[wp+4] = 0 561 buf[wp+5] = 0 562 buf[wp+6] = 0 563 buf[wp+7] = MR_Q12 564 wp = wp + 8 565 i2 = i2 + 1 566 } 567 // SKIN 568 let skin_w: i64 = wp 569 buf[wp] = nv 570 wp = wp + 1 571 i2 = 0 572 while i2 < nv { 573 buf[wp] = sj[i2*MR_INF] 574 buf[wp+1] = sj[i2*MR_INF+1] 575 buf[wp+2] = sj[i2*MR_INF+2] 576 buf[wp+3] = sj[i2*MR_INF+3] 577 buf[wp+4] = sw[i2*MR_INF] 578 buf[wp+5] = sw[i2*MR_INF+1] 579 buf[wp+6] = sw[i2*MR_INF+2] 580 buf[wp+7] = sw[i2*MR_INF+3] 581 wp = wp + 8 582 i2 = i2 + 1 583 } 584 // TOC entries: [tag][byte_off][word_len][check(payload words)] 585 buf[toc0+0] = mr_tag(86, 69, 82, 84) 586 buf[toc0+1] = vert_w * 8 587 buf[toc0+2] = wv 588 buf[toc0+3] = mr_check(((buf as i64) + vert_w*8) as *i64, wv) 589 buf[toc0+4] = mr_tag(84, 82, 73, 83) 590 buf[toc0+5] = tris_w * 8 591 buf[toc0+6] = wt 592 buf[toc0+7] = mr_check(((buf as i64) + tris_w*8) as *i64, wt) 593 buf[toc0+8] = mr_tag(83, 75, 69, 76) 594 buf[toc0+9] = skel_w * 8 595 buf[toc0+10] = ws 596 buf[toc0+11] = mr_check(((buf as i64) + skel_w*8) as *i64, ws) 597 buf[toc0+12] = mr_tag(83, 75, 73, 78) 598 buf[toc0+13] = skin_w * 8 599 buf[toc0+14] = wk 600 buf[toc0+15] = mr_check(((buf as i64) + skin_w*8) as *i64, wk) 601 buf[3] = mr_check(((buf as i64) + toc0*8) as *i64, MR_NSECT*4) 602 603 let fd: i64 = sys_openat_wr(out_path, MR_MODE) 604 if fd < 0 { mr_refuse("cannot open output for write" as *u8); return 6 } 605 sys_write(fd, buf as *u8, wp*8) 606 sys_close(fd) 607 mr_kv("bytes=" as *u8, wp*8) 608 mr_puts("MESHRIG OK sections=VERT,TRIS,SKEL,SKIN\n" as *u8) 609 return 0 610}