code wiki / (root) / nx_bvhfk.nx

nx_bvhfk.nx source

↩ module page · 418 lines · 18534 B

1// nx_bvhfk.nx -- BVH FORWARD KINEMATICS: joint tree + channel rotations -> WORLD-SPACE positions. 2// 3// WHY THIS IS THE RUNG THAT MATTERS: parsing a BVH gets you a hierarchy; PLAYING it is what 4// DanceXR does. Our exceed is not playback -- it is that nx_pose_retarget already inverts our 5// sovereign FK (joint POSITIONS -> our rig's FK angles), and nx_pose_coach / nx_pose_naturalness 6// already SCORE a pose track. The one missing link was world-space positions out of a BVH. This 7// organ is that link, so third-party mocap can drive OUR rig -- including VRM, which appears 8// nowhere in DanceXR's format set. 9// 10// THE FK CORE (fk_eval + the integer matrix substrate) LIVES IN nx_bvhfk_lib.nx since 2026-08-13 11// -- extracted on the second consumer (nx_clipcheck's bvh self-clearance control). The KAT below 12// is the extraction's value-identity proof: bone-length isometry on real third-party data. 13// 14// *THE HARDEST TOOTH IS BONE-LENGTH INVARIANCE. A rotation is an isometry: |child - parent| MUST 15// equal |offset|, at every frame, for every joint. A rest-pose test passes even if the rotation 16// path is a no-op; a length test fails the moment a matrix is mis-scaled, mis-composed, or drifts 17// in fixed point. It is the implied hidden quantity that physics demands, and it is checked here 18// against REAL third-party data, not only a fixture. 19// 20// UNITS: positions in file units x B_SCALE (1000) -- BVH does not state its unit; the convention 21// is centimetres, so we do not claim mm. Angles arrive as millidegrees and are converted to the 22// it_* integer-trig circle (25736 units) -- never to floats. 23// 24// usage: nx_bvhfk pos <file.bvh> <frame> world x y z per joint 25// nx_bvhfk lens <file.bvh> <frame> measured bone length vs declared |offset| per joint 26// nx_bvhfk --kat selftest: 6 teeth, 3 of them anti-vacuity 27// license_tier: ORIGINAL No hw writes (Rule 26). expect_exit: 0 28import "nx_gate_verdict.nx" 29import "nx_bvh_lib.nx" 30import "nx_itrig.nx" 31import "nx_bvhfk_lib.nx" 32const FK_MAGIC_8388607: i64 = 8388607 33const FK_MAGIC_4096: i64 = 4096 34const FK_MAGIC_5000: i64 = 5000 35const FK_MAGIC_4900: i64 = 4900 36const FK_MAGIC_5100: i64 = 5100 37const FK_MAGIC_4000: i64 = 4000 38const FK_MAGIC_1500: i64 = 1500 39const FK_MAGIC_7500: i64 = 7500 40const FK_MAGIC_3500: i64 = 3500 41 42const FK_KATBUF: i64 = 4096 43const FK_LENTOL: i64 = 40 // permitted |measured-declared| in file-units x1000 (fixed-point) 44 45// ===== NXMSH2 EMISSION: the picture the numbers were always standing in for ======= 46// 47// WHY (operator, 2026-08-07): "published the actual results of each of these models working, not 48// hand waving it works". FK produced 19 joints x 455 frames of real third-party mocap in world 49// space and NOTHING WAS EVER DRAWN. The estate already had nx_meshview / nx_mesh2glb / nx_render3d 50// registered -- so this was an omission, not a missing capability, which is the worse kind. 51// 52// Emits one NXMSH2 box per bone spanning parent->child world position. NXMSH2 layout, read from the 53// consumer (nx_mesh2glb) rather than assumed: magic at 0, nlay u32 at 8, ntri u32 at 12, header is 54// 16 + nlay*24, then ntri records of 84 bytes; corner c axis ax sits at t*84 + c*12 + ax*4, normals 55// at 36..71, per-tri colour at 72/76/80. 56// 57// UNITS: NXMSH2 f32 positions are MILLIMETRES (banked law -- a metres-scale donor once decoded to a 58// 2px sliver whose self-checks still read 1000). BVH offsets are file units x1000, conventionally 59// centimetres, so mm = units / FK_MM_DIV. 60const FK_MM_DIV: i64 = 100 61const FK_BONE_R: i64 = 14 // bone half-width in mm 62const FK_TRI_REC: i64 = 84 63const FK_HDR: i64 = 16 64const FK_COL_R: i64 = 900 // per-mille colour, per the heatmap convention 65const FK_COL_G: i64 = 620 66const FK_COL_B: i64 = 300 67 68// integer -> IEEE-754 binary32 bit pattern. Exact for |v| < 2^24, which every millimetre value here is. 69func fk_f32bits(v: i64) -> i64 { 70 if v == 0 { return 0 } 71 var neg: i64 = 0 72 var m: i64 = v 73 if m < 0 { neg = 1; m = 0 - m } 74 var e: i64 = 0 75 var x: i64 = m 76 while x > 1 { x = x / 2; e = e + 1 } 77 var mant: i64 = 0 78 if e >= 23 { mant = m >> (e - 23) } else { mant = m << (23 - e) } 79 mant = mant & FK_MAGIC_8388607 80 var bits: i64 = ((e + 127) << 23) | mant 81 if neg == 1 { bits = bits | (1 << 31) } 82 return bits 83} 84 85func fk_w32(b: *u8, o: i64, v: i64) -> i64 { 86 b[o] = (v & 255) as u8 87 b[o+1] = ((v >> 8) & 255) as u8 88 b[o+2] = ((v >> 16) & 255) as u8 89 b[o+3] = ((v >> 24) & 255) as u8 90 return o + 4 91} 92 93// write one triangle record: 3 corners (mm), a shared face normal, a per-tri colour 94func fk_tri(b: *u8, t: i64, p: *i64, nx: i64, ny: i64, nz: i64) -> i64 { 95 let base: i64 = FK_HDR + t * FK_TRI_REC 96 var c: i64 = 0 97 while c < 3 { 98 var a: i64 = 0 99 while a < 3 { fk_w32(b, base + c*12 + a*4, fk_f32bits(p[c*3 + a])); a = a + 1 } 100 c = c + 1 101 } 102 var k: i64 = 0 103 while k < 3 { 104 fk_w32(b, base + 36 + k*12, fk_f32bits(nx)) 105 fk_w32(b, base + 36 + k*12 + 4, fk_f32bits(ny)) 106 fk_w32(b, base + 36 + k*12 + 8, fk_f32bits(nz)) 107 k = k + 1 108 } 109 fk_w32(b, base + 72, fk_f32bits(FK_COL_R)) 110 fk_w32(b, base + 76, fk_f32bits(FK_COL_G)) 111 fk_w32(b, base + 80, fk_f32bits(FK_COL_B)) 112 return t + 1 113} 114 115// A box from A to B. Cross-section expands in the two axes LEAST aligned with the bone direction, 116// so a bone never degenerates to a zero-width sliver whichever way it points. 117func fk_bone_box(b: *u8, t0: i64, ax: i64, ay: i64, az: i64, bx: i64, by: i64, bz: i64) -> i64 { 118 var dx: i64 = bx - ax 119 var dy: i64 = by - ay 120 var dz: i64 = bz - az 121 if dx < 0 { dx = 0 - dx } 122 if dy < 0 { dy = 0 - dy } 123 if dz < 0 { dz = 0 - dz } 124 var ex: i64 = FK_BONE_R 125 var ey: i64 = FK_BONE_R 126 var ez: i64 = FK_BONE_R 127 if dy >= dx { if dy >= dz { ey = 0 } } 128 if dx > dy { if dx >= dz { ex = 0 } } 129 if dz > dx { if dz > dy { ez = 0 } } 130 let v: *i64 = sys_mmap(8 * 3 * 8) as *i64 131 var i: i64 = 0 132 while i < 8 { 133 var sx: i64 = 0 - 1 134 var sz: i64 = 0 - 1 135 if (i & 1) == 1 { sx = 1 } 136 if (i & 2) == 2 { sz = 1 } 137 var px: i64 = ax 138 var py: i64 = ay 139 var pz: i64 = az 140 if (i & 4) == 4 { px = bx; py = by; pz = bz } 141 v[i*3] = px + sx * ex 142 v[i*3 + 1] = py + sx * ey 143 v[i*3 + 2] = pz + sz * ez 144 i = i + 1 145 } 146 let p: *i64 = sys_mmap(9 * 8) as *i64 147 let q: *i64 = sys_mmap(12 * 6 * 8) as *i64 148 // 12 triangles as vertex-index triples over the 8 corners 149 q[0]=0;q[1]=1;q[2]=5; q[3]=0;q[4]=5;q[5]=4 150 q[6]=1;q[7]=3;q[8]=7; q[9]=1;q[10]=7;q[11]=5 151 q[12]=3;q[13]=2;q[14]=6; q[15]=3;q[16]=6;q[17]=7 152 q[18]=2;q[19]=0;q[20]=4; q[21]=2;q[22]=4;q[23]=6 153 q[24]=4;q[25]=5;q[26]=7; q[27]=4;q[28]=7;q[29]=6 154 q[30]=2;q[31]=3;q[32]=1; q[33]=2;q[34]=1;q[35]=0 155 var t: i64 = t0 156 var f: i64 = 0 157 while f < 12 { 158 var c: i64 = 0 159 while c < 3 { 160 let vi: i64 = q[f*3 + c] 161 p[c*3] = v[vi*3] 162 p[c*3 + 1] = v[vi*3 + 1] 163 p[c*3 + 2] = v[vi*3 + 2] 164 c = c + 1 165 } 166 t = fk_tri(b, t, p, 0, 1000, 0) 167 f = f + 1 168 } 169 return t 170} 171 172func fk_emit_mesh(H: *i64, parent: *i64, pos: *i64, outp: *u8) -> i64 { 173 let nj: i64 = H[B_H_NJOINT] 174 let cap: i64 = FK_HDR + nj * 12 * FK_TRI_REC + FK_MAGIC_4096 175 let b: *u8 = sys_mmap(cap) 176 b[0] = 78 as u8; b[1] = 88 as u8; b[2] = 77 as u8 177 b[3] = 83 as u8; b[4] = 72 as u8; b[5] = 50 as u8 178 fk_w32(b, 8, 0) 179 var t: i64 = 0 180 var j: i64 = 0 181 while j < nj { 182 let pj: i64 = parent[j] 183 if pj >= 0 { 184 t = fk_bone_box(b, t, 185 pos[pj*3] / FK_MM_DIV, pos[pj*3+1] / FK_MM_DIV, pos[pj*3+2] / FK_MM_DIV, 186 pos[j*3] / FK_MM_DIV, pos[j*3+1] / FK_MM_DIV, pos[j*3+2] / FK_MM_DIV) 187 } 188 j = j + 1 189 } 190 fk_w32(b, 12, t) 191 let fd: i64 = sys_openat_wr(outp, MODE_0644) 192 if fd < 0 { return 0 - 1 } 193 sys_write(fd, b, FK_HDR + t * FK_TRI_REC) 194 sys_close(fd) 195 gv_puts("NXMSH2 written bones=" as *u8); gv_num(nj - 1) 196 gv_puts(" triangles=" as *u8); gv_num(t) 197 gv_puts(" bytes=" as *u8); gv_num(FK_HDR + t * FK_TRI_REC) 198 gv_puts(" units=mm\n" as *u8) 199 return t 200} 201 202// ===== KAT ======================================================== 203func fk_cat(b: *u8, o: i64, s: *u8) -> i64 { 204 var i: i64 = 0 205 while s[i] != (0 as u8) { b[o + i] = s[i]; i = i + 1 } 206 return o + i 207} 208 209// Hips -> Chest offset (0, 5.00, 0); root has 6 channels ZXY, Chest 3 channels ZXY. 210func fk_fixture(b: *u8, row: *u8) -> i64 { 211 var o: i64 = 0 212 o = fk_cat(b, o, "HIERARCHY\nROOT Hips\n{\n" as *u8) 213 o = fk_cat(b, o, "OFFSET 0.00 0.00 0.00\n" as *u8) 214 o = fk_cat(b, o, "CHANNELS 6 Xposition Yposition Zposition Zrotation Xrotation Yrotation\n" as *u8) 215 o = fk_cat(b, o, "JOINT Chest\n{\n" as *u8) 216 o = fk_cat(b, o, "OFFSET 0.00 5.00 0.00\n" as *u8) 217 o = fk_cat(b, o, "CHANNELS 3 Zrotation Xrotation Yrotation\n" as *u8) 218 o = fk_cat(b, o, "End Site\n{\nOFFSET 0.00 5.00 0.00\n}\n}\n}\n" as *u8) 219 o = fk_cat(b, o, "MOTION\nFrames: 1\nFrame Time: 0.0333333\n" as *u8) 220 o = fk_cat(b, o, row) 221 b[o] = 0 as u8 222 return o 223} 224 225func fk_kat() -> i64 { 226 let ctr: *i64 = gv_ctr() 227 gv_head("nx_bvhfk KAT -- BVH forward kinematics; bone length is the invariant that cannot be faked" as *u8) 228 229 let b: *u8 = sys_mmap(FK_KATBUF) 230 let H: *i64 = sys_mmap(B_H_WORDS * 8) as *i64 231 let names: *u8 = sys_mmap(B_MAX_JOINTS * B_NAMEW) 232 let parent: *i64 = sys_mmap(B_MAX_JOINTS * 8) as *i64 233 let nchan: *i64 = sys_mmap(B_MAX_JOINTS * 8) as *i64 234 let chan0: *i64 = sys_mmap(B_MAX_JOINTS * 8) as *i64 235 let off: *i64 = sys_mmap(B_MAX_JOINTS * 24) as *i64 236 let chtype: *i64 = sys_mmap(B_MAX_CHAN * 8) as *i64 237 let vals: *i64 = sys_mmap(B_MAX_CHAN * 8) as *i64 238 let rot: *i64 = sys_mmap(B_MAX_JOINTS * 9 * 8) as *i64 239 let pos: *i64 = sys_mmap(B_MAX_JOINTS * 3 * 8) as *i64 240 241 // ---- T1 REST POSE: all rotations zero -> world position is the cumulative offset 242 var n: i64 = fk_fixture(b, "0 0 0 0 0 0 0 0 0\n" as *u8) 243 var ok: i64 = 0 244 if b_parse(b, n, H, names, parent, nchan, chan0, off, chtype) == 0 { 245 if b_frame_values(b, n, H, 0, vals) == 0 { 246 fk_eval(H, parent, nchan, chan0, off, chtype, vals, rot, pos) 247 if pos[3] == 0 { if pos[4] == FK_MAGIC_5000 { if pos[5] == 0 { ok = 1 } } } 248 } 249 } 250 gv_check("T1 rest pose puts Chest at (0,5000,0)" as *u8, ok, ctr) 251 252 // ---- T2 root Zrotation 90 deg: a +Y offset must swing to -X. This pins the axis AND the sign 253 // convention; a transposed matrix lands at +X and passes every length test. 254 n = fk_fixture(b, "0 0 0 90 0 0 0 0 0\n" as *u8) 255 var t2: i64 = 0 256 var gx: i64 = 0 257 var gy: i64 = 0 258 if b_parse(b, n, H, names, parent, nchan, chan0, off, chtype) == 0 { 259 if b_frame_values(b, n, H, 0, vals) == 0 { 260 fk_eval(H, parent, nchan, chan0, off, chtype, vals, rot, pos) 261 gx = pos[3] 262 gy = pos[4] 263 if gx < 0 - FK_MAGIC_4900 { if gx > 0 - FK_MAGIC_5100 { if gy > 0 - 100 { if gy < 100 { t2 = 1 } } } } 264 } 265 } 266 gv_check("T2 Rz(90) swings +Y offset to -X" as *u8, t2, ctr) 267 268 // ---- T3 ANTI-VACUITY: rotation must CHANGE the pose. A no-op FK passes T1 perfectly. 269 var t3: i64 = 0 270 if gy < FK_MAGIC_4000 { t3 = 1 } 271 gv_check("T3 ANTI-VACUITY rotated pose differs from rest" as *u8, t3, ctr) 272 273 // ---- T4 root translation carries every descendant 274 n = fk_fixture(b, "1.5 2.5 -3.5 0 0 0 0 0 0\n" as *u8) 275 var t4: i64 = 0 276 if b_parse(b, n, H, names, parent, nchan, chan0, off, chtype) == 0 { 277 if b_frame_values(b, n, H, 0, vals) == 0 { 278 fk_eval(H, parent, nchan, chan0, off, chtype, vals, rot, pos) 279 if pos[3] == FK_MAGIC_1500 { if pos[4] == FK_MAGIC_7500 { if pos[5] == 0 - FK_MAGIC_3500 { t4 = 1 } } } 280 } 281 } 282 gv_check("T4 root translation carries descendants" as *u8, t4, ctr) 283 284 // ---- T5 ANTI-VACUITY: BONE LENGTH INVARIANCE under an arbitrary 3-axis rotation. 285 // A rotation is an isometry. Any mis-scaled or mis-composed matrix breaks this while 286 // still producing plausible-looking coordinates. 287 n = fk_fixture(b, "0 0 0 37 -52 113 0 0 0\n" as *u8) 288 var t5: i64 = 0 289 if b_parse(b, n, H, names, parent, nchan, chan0, off, chtype) == 0 { 290 if b_frame_values(b, n, H, 0, vals) == 0 { 291 fk_eval(H, parent, nchan, chan0, off, chtype, vals, rot, pos) 292 let d: i64 = fk_dist(pos[3], pos[4], pos[5], pos[0], pos[1], pos[2]) 293 var e: i64 = d - FK_MAGIC_5000 294 if e < 0 { e = 0 - e } 295 if e <= FK_LENTOL { t5 = 1 } 296 } 297 } 298 gv_check("T5 ANTI-VACUITY bone length invariant under arbitrary rotation" as *u8, t5, ctr) 299 300 // ---- T6 THE REAL-DATA TOOTH: every bone of a third-party 19-joint mocap file must keep its 301 // declared |OFFSET| at a mid-motion frame. 18 independent length checks on data we did 302 // not author. This is the file validating the kinematics. 303 let fx: *u8 = "knowledge/fixture_mocapbank.bvh" as *u8 304 let lenp: *i64 = sys_mmap(16) as *i64 305 let rb: *u8 = sys_read_file(fx, lenp) 306 var present: i64 = 0 307 if (rb as i64) != 0 { present = 1 } 308 if gv_need("knowledge/fixture_mocapbank.bvh" as *u8, present, ctr) == 1 { 309 var t6: i64 = 0 310 var worst: i64 = 0 311 if b_parse(rb, lenp[0], H, names, parent, nchan, chan0, off, chtype) == 0 { 312 if b_frame_values(rb, lenp[0], H, 200, vals) == 0 { 313 fk_eval(H, parent, nchan, chan0, off, chtype, vals, rot, pos) 314 t6 = 1 315 var j: i64 = 1 316 while j < H[B_H_NJOINT] { 317 let p: i64 = parent[j] 318 let want: i64 = fk_isqrt(off[j*3]*off[j*3] + off[j*3+1]*off[j*3+1] + off[j*3+2]*off[j*3+2]) 319 let got: i64 = fk_dist(pos[j*3], pos[j*3+1], pos[j*3+2], pos[p*3], pos[p*3+1], pos[p*3+2]) 320 var e2: i64 = got - want 321 if e2 < 0 { e2 = 0 - e2 } 322 if e2 > worst { worst = e2 } 323 if e2 > FK_LENTOL { t6 = 0 } 324 j = j + 1 325 } 326 } 327 } 328 gv_check("T6 REAL DATA all 18 bone lengths hold at frame 200" as *u8, t6, ctr) 329 gv_puts(" worst bone-length error (file units x1000): " as *u8); gv_num(worst); gv_puts("\n" as *u8) 330 } 331 332 return gv_verdict("BVHFK-GATE" as *u8, ctr, "FK is an isometry and real data proves it" as *u8) 333} 334 335func fk_load(path: *u8, H: *i64, names: *u8, parent: *i64, nchan: *i64, chan0: *i64, 336 off: *i64, chtype: *i64, frame: i64, vals: *i64, rot: *i64, pos: *i64) -> i64 { 337 let lenp: *i64 = sys_mmap(16) as *i64 338 let buf: *u8 = sys_read_file(path, lenp) 339 if (buf as i64) == 0 { gv_puts("nx_bvhfk unreadable\n" as *u8); return 2 } 340 let r: i64 = b_parse(buf, lenp[0], H, names, parent, nchan, chan0, off, chtype) 341 if r < 0 { return b_refuse(r) } 342 let fr: i64 = b_frame_values(buf, lenp[0], H, frame, vals) 343 if fr < 0 { return b_refuse(fr) } 344 fk_eval(H, parent, nchan, chan0, off, chtype, vals, rot, pos) 345 return 0 346} 347 348func main(argc: i64, argv: *i64) -> i64 { 349 if argc < 2 { gv_puts("usage: nx_bvhfk pos|lens <file.bvh> <frame> | --kat\n" as *u8); return 3 } 350 let verb: *u8 = argv[1] as *u8 351 var vl: i64 = 0 352 while verb[vl] != (0 as u8) { vl = vl + 1 } 353 if vl == 5 { if verb[0] == (45 as u8) { let r: i64 = fk_kat(); sys_exit(r); return r } } 354 if argc < 3 { gv_puts("usage: nx_bvhfk pos|lens <file.bvh> <frame> | --kat\n" as *u8); return 3 } 355 356 let H: *i64 = sys_mmap(B_H_WORDS * 8) as *i64 357 let names: *u8 = sys_mmap(B_MAX_JOINTS * B_NAMEW) 358 let parent: *i64 = sys_mmap(B_MAX_JOINTS * 8) as *i64 359 let nchan: *i64 = sys_mmap(B_MAX_JOINTS * 8) as *i64 360 let chan0: *i64 = sys_mmap(B_MAX_JOINTS * 8) as *i64 361 let off: *i64 = sys_mmap(B_MAX_JOINTS * 24) as *i64 362 let chtype: *i64 = sys_mmap(B_MAX_CHAN * 8) as *i64 363 let vals: *i64 = sys_mmap(B_MAX_CHAN * 8) as *i64 364 let rot: *i64 = sys_mmap(B_MAX_JOINTS * 9 * 8) as *i64 365 let pos: *i64 = sys_mmap(B_MAX_JOINTS * 3 * 8) as *i64 366 367 var frame: i64 = 0 368 if argc > 3 { 369 let s: *u8 = argv[3] as *u8 370 var sl: i64 = 0 371 while s[sl] != (0 as u8) { sl = sl + 1 } 372 frame = b_parse_fx(s, sl, 1) 373 } 374 let rc: i64 = fk_load(argv[2] as *u8, H, names, parent, nchan, chan0, off, chtype, frame, vals, rot, pos) 375 if rc != 0 { return rc } 376 377 if verb[0] == (112 as u8) { 378 gv_puts("idx\tparent\twx\twy\twz\tname\n" as *u8) 379 var j: i64 = 0 380 while j < H[B_H_NJOINT] { 381 gv_num(j); gv_puts("\t" as *u8); gv_num(parent[j]); gv_puts("\t" as *u8) 382 gv_num(pos[j*3]); gv_puts("\t" as *u8) 383 gv_num(pos[j*3+1]); gv_puts("\t" as *u8) 384 gv_num(pos[j*3+2]); gv_puts("\t" as *u8) 385 gv_puts(((names as i64) + j * B_NAMEW) as *u8) 386 gv_puts("\n" as *u8) 387 j = j + 1 388 } 389 return 0 390 } 391 if verb[0] == (108 as u8) { 392 gv_puts("idx\tdeclared\tmeasured\terr\tname\n" as *u8) 393 var worst: i64 = 0 394 var j2: i64 = 1 395 while j2 < H[B_H_NJOINT] { 396 let p: i64 = parent[j2] 397 let want: i64 = fk_isqrt(off[j2*3]*off[j2*3] + off[j2*3+1]*off[j2*3+1] + off[j2*3+2]*off[j2*3+2]) 398 let got: i64 = fk_dist(pos[j2*3], pos[j2*3+1], pos[j2*3+2], pos[p*3], pos[p*3+1], pos[p*3+2]) 399 var e: i64 = got - want 400 if e < 0 { e = 0 - e } 401 if e > worst { worst = e } 402 gv_num(j2); gv_puts("\t" as *u8); gv_num(want); gv_puts("\t" as *u8) 403 gv_num(got); gv_puts("\t" as *u8); gv_num(e); gv_puts("\t" as *u8) 404 gv_puts(((names as i64) + j2 * B_NAMEW) as *u8); gv_puts("\n" as *u8) 405 j2 = j2 + 1 406 } 407 gv_puts("worst_err=" as *u8); gv_num(worst) 408 gv_puts(" tol=" as *u8); gv_num(FK_LENTOL); gv_puts("\n" as *u8) 409 return 0 410 } 411 if verb[0] == (109 as u8) { 412 if argc < 5 { gv_puts("usage: nx_bvhfk mesh <file.bvh> <frame> <out.nxmesh>\n" as *u8); return 3 } 413 if fk_emit_mesh(H, parent, pos, argv[4] as *u8) < 0 { gv_puts("nx_bvhfk mesh write failed\n" as *u8); return 9 } 414 return 0 415 } 416 gv_puts("nx_bvhfk unknown verb\n" as *u8) 417 return 3 418}