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}