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}