code wiki / _hdl_build / nx_gltf_anim_gate.nx

nx_gltf_anim_gate.nx source

↩ module page · 139 lines · 8084 B

1// nx_gltf_anim_gate.nx -- export the being as a SKINNED, WALKING .glb. Mesh the skin, bind each vertex to its nearest 2// part's deepest FK bone, export with a walk-cycle animation, self-verify the container + names. Writes knowledge/nishi_being_walk.glb. 3// 4// D001 MIGRATION 2026-08-22: this gate rolled its own verdict (a hand `fails` counter + sys_exit) so /api/promote 5// refused it and nothing outside could read its outcome -- nx_gate_green could not judge it and it wrote no 6// harness.jrnl frame, so flake and erosion were invisible for it. Migrated BY HAND onto per-tooth gv_check, NOT via 7// nx_gate_dry_apply (whose candidate collapses N teeth into ONE boolean: ctr[0]=green; ctr[1]=1 -- a one-state judge 8// cannot see a dropped conjunct). T0..T3 keep their exact semantics. `return gv_verdict(...)` so the EXIT CODE carries 9// the verdict -- /api/gate_run derives its verdict from the exit code. 10// 11// WHY THIS GATE WAS REBUILT AT ALL: a fresh build of the unchanged source measured 88,019 B against a served 139,953 B 12// (375 permil, the same shrink as the matched-vintage control). nx_contentdiff named ONE lost run -- 13// `"animations":[{"name":"walk","samplers":[` -- and this gate's own T3 proved on the STAGED binary's ARTIFACT that the 14// walk animation is still declared, so the run was a refactored literal, not lost capability. That proof is what T3 15// exists for; the promote refusal was about the VERDICT SHAPE, not the content. 16// license_tier: ORIGINAL expect_exit: 0 17import "nx_syscalls.nx" 18import "nx_gate_verdict.nx" 19import "nx_trimesh.nx" 20import "nx_isosurf.nx" 21import "nx_bodyatlas.nx" 22import "nx_gltf_anim.nx" 23 24func ru32(buf: *u8, at: i64) -> i64 { return (buf[at] as i64) + ((buf[at+1] as i64)<<8) + ((buf[at+2] as i64)<<16) + ((buf[at+3] as i64)<<24) } 25func hasstr(buf: *u8, n: i64, needle: *u8) -> i64 { var m: i64=0; while needle[m]!=(0 as u8){m=m+1} var i: i64=0; while i+m<=n { var k: i64=0; while k<m { if buf[i+k]!=needle[k] {k=m+9} else {k=k+1} } if k==m {return 1} i=i+1 } return 0 } 26 27// SDF sampling grid over the body atlas (inherited; the atlas is the canon these cover) 28const GX: i64 = 32 29const GY: i64 = 110 30const GZ: i64 = 20 31const GC: i64 = 18 32const OX: i64 = 0-288 33const OY: i64 = 0-990 34const OZ: i64 = 0-180 35 36// ---- hoisted by purpose (were raw literals in main) ---- 37// a limb counts as BOUND when more than this many vertices map to its bone. INHERITED threshold, named so it is 38// visible; the honest derivation would be a fraction of the limb's vertex share from the atlas, not a count. 39const GA_LIMB_BOUND_MIN_VERTS: i64 = 20 40// packed skin colour r + g*256 + b*65536, used for every vertex and for the unassigned fallback 41const GA_SKIN_R: i64 = 216 42const GA_SKIN_G: i64 = 172 43const GA_SKIN_B: i64 = 152 44const GA_BYTE_RADIX: i64 = 256 45// export buffer. gltf_anim_export takes no cap, so this is a bound the callee cannot announce -- T4 below turns a 46// silent overflow into a RED instead of a silent truncation. 47const GA_OUT_CAP: i64 = 8388608 48// GLB container layout, per glTF 2.0 binary spec: magic 'g'(0x67) at 0, u32 total length at 8, first chunk length at 49// 12; each chunk is 8 header bytes (u32 length, u32 type); BIN chunk type is 'B'(0x42) as the first type byte. 50const GLB_MAGIC_G: i64 = 103 51const GLB_TOTAL_LEN_OFF: i64 = 8 52const GLB_FIRST_CHUNK_LEN_OFF: i64 = 12 53const GLB_HDR_BYTES: i64 = 12 54const GLB_CHUNKHDR_BYTES: i64 = 8 55const GLB_CHUNK_TYPE_BIN: i64 = 66 56const GA_JOINTS: i64 = 9 57const GA_VEC3_WORDS: i64 = 3 58const GA_OUT_PATH: *u8 = "knowledge/nishi_being_walk.glb" 59 60func main() -> i64 { 61 let ctr: *i64 = gv_ctr() 62 gv_head("nx_gltf_anim_gate -- skinned WALKING being -> .glb" as *u8) 63 let skin_rgb: i64 = GA_SKIN_R + GA_SKIN_G*GA_BYTE_RADIX + GA_SKIN_B*GA_BYTE_RADIX*GA_BYTE_RADIX 64 atlas_build(0) 65 let grid: *i64 = sys_mmap((GX+1)*(GY+1)*(GZ+1)*8) as *i64 66 var i: i64 = 0 67 while i <= GX { var jj: i64=0; while jj<=GY { var k: i64=0; while k<=GZ { grid[(i*(GY+1)+jj)*(GZ+1)+k] = ba_sdf(OX+i*GC, OY+jj*GC, OZ+k*GC, 1); k=k+1 } jj=jj+1 } i=i+1 } 68 tm_reset() 69 surface_nets(grid, GX, GY, GZ, OX, OY, OZ, GC, 0, skin_rgb) 70 let NV: i64 = tm_nv() 71 // per-vertex joint + colour, from nearest part 72 let jarr: *i64 = sys_mmap(NV*8) as *i64 73 let vp: *i64 = sys_mmap(GA_VEC3_WORDS*8) as *i64 74 let jc: *i64 = sys_mmap(GA_JOINTS*8) as *i64 75 var b: i64 = 0 76 while b < GA_JOINTS { jc[b]=0; b=b+1 } 77 i = 0 78 while i < NV { 79 tm_vpos(i, vp) 80 let kk: i64 = ba_nearest(vp[0], vp[1], vp[2], 1) 81 var jt: i64 = 0 82 if kk >= 0 { jt = ba_part_bone(kk); tm_vcol(i, ba_part_color(kk)) } else { tm_vcol(i, skin_rgb) } 83 jarr[i] = jt 84 jc[jt] = jc[jt] + 1 85 i = i + 1 86 } 87 gv_puts(" verts=" as *u8); gv_num(NV) 88 gv_puts(" joint spread root=" as *u8); gv_num(jc[0]) 89 gv_puts(" Lsh=" as *u8); gv_num(jc[1]) 90 gv_puts(" Lel=" as *u8); gv_num(jc[2]) 91 gv_puts(" Rsh=" as *u8); gv_num(jc[3]) 92 gv_puts(" Lhip=" as *u8); gv_num(jc[5]) 93 gv_puts(" Lkn=" as *u8); gv_num(jc[6]) 94 gv_puts("\n" as *u8) 95 var t0: i64 = 0 96 if jc[1] > GA_LIMB_BOUND_MIN_VERTS { if jc[5] > GA_LIMB_BOUND_MIN_VERTS { if jc[6] > GA_LIMB_BOUND_MIN_VERTS { t0 = 1 } } } 97 gv_check("T0 vertices bound across the FK bones (arms + legs rigged)" as *u8, t0, ctr) 98 99 let out: *u8 = sys_mmap(GA_OUT_CAP) 100 let total: i64 = gltf_anim_export(out, jarr) 101 gv_puts(" glb bytes=" as *u8); gv_num(total); gv_puts(" cap=" as *u8); gv_num(GA_OUT_CAP); gv_puts("\n" as *u8) 102 // T4 first in ORDER OF TRUST: every later tooth reads `out`, and a total past the cap means the buffer was 103 // overrun before any of them looked. A cap the callee cannot announce must at least be CAUGHT here. 104 var t4: i64 = 0 105 if total > 0 { if total < GA_OUT_CAP { t4 = 1 } } 106 gv_check("T4 export total inside the buffer cap (an overrun would corrupt every tooth below)" as *u8, t4, ctr) 107 108 // self-verify container + rigging/animation presence 109 var t1: i64 = 0 110 if (out[0] as i64)==GLB_MAGIC_G { if ru32(out,GLB_TOTAL_LEN_OFF)==total { t1=1 } } 111 gv_check("T1 glTF header magic + declared total length" as *u8, t1, ctr) 112 let jsonLen: i64 = ru32(out,GLB_FIRST_CHUNK_LEN_OFF) 113 let binLenPos: i64 = GLB_HDR_BYTES+GLB_CHUNKHDR_BYTES+jsonLen 114 let binLen: i64 = ru32(out,binLenPos) 115 var t2: i64 = 0 116 if (GLB_HDR_BYTES+GLB_CHUNKHDR_BYTES+jsonLen+GLB_CHUNKHDR_BYTES+binLen)==total { if (out[binLenPos+4] as i64)==GLB_CHUNK_TYPE_BIN { t2=1 } } 117 gv_check("T2 chunk arithmetic sums to total + BIN chunk type" as *u8, t2, ctr) 118 let jsonEnd: i64 = GLB_HDR_BYTES+GLB_CHUNKHDR_BYTES+jsonLen 119 var t3: i64 = 0 120 if hasstr(out,jsonEnd,"animations"as *u8)==1 { if hasstr(out,jsonEnd,"skins"as *u8)==1 { if hasstr(out,jsonEnd,"JOINTS_0"as *u8)==1 { if hasstr(out,jsonEnd,"\"walk\""as *u8)==1 { t3=1 } } } } 121 gv_check("T3 JSON declares skin + JOINTS_0 + a 'walk' animation" as *u8, t3, ctr) 122 // NEG-CONTROL: T3 is a substring test, and an EMPTY animations array beside a node merely NAMED "walk" would 123 // satisfy every one of its four needles. Assert the empty-array spelling is ABSENT, paired with T3 via gv_bite so 124 // a JSON emitter that declared the key and nothing under it cannot score a pass. 125 var empty_anims: i64 = hasstr(out,jsonEnd,"\"animations\":[]"as *u8) 126 gv_bite("neg-control-walk-animation-is-not-an-empty-array" as *u8, 1 - empty_anims, 1 - t3, ctr) 127 128 let fd: i64 = sys_openat_wr(GA_OUT_PATH, MODE_0644) 129 let wr: i64 = sys_write(fd, out, total) 130 sys_close(fd) 131 gv_puts(" wrote " as *u8); gv_puts(GA_OUT_PATH); gv_puts(" bytes=" as *u8); gv_num(wr); gv_puts(" of=" as *u8); gv_num(total); gv_puts("\n" as *u8) 132 var t5: i64 = 0 133 if wr == total { t5 = 1 } 134 gv_check("T5 artifact written in full (bytes written == export total)" as *u8, t5, ctr) 135 136 let rc: i64 = gv_verdict("GLTF-ANIM-GATE" as *u8, ctr, "the being exports as a SKINNED, WALKING .glb (9-joint FK rig + walk-cycle quaternion keyframes); residual: skinning in our own web viewer" as *u8) 137 sys_exit(rc) 138 return rc 139}