nx_gltf_anim.nx source
↩ module page · 234 lines · 18843 B
1// nx_gltf_anim.nx -- ★SKINNED + ANIMATED glTF export: the being as a rigged, WALKING .glb (plays in any glTF viewer).
2// Reuses our FK rig: each mesh vertex is rigidly bound to its nearest part's deepest bone (joints supplied by the driver).
3// 9 joints (root + L/R shoulder,elbow,hip,knee) in a 2-level hierarchy; inverse-bind = translate(-pivot); a walk-cycle
4// animation = per-bone quaternion rotation keyframes about X (sagittal). Includes an integer float32 + fixed-point encoder.
5// GENERALIZED 2026-08-10 (ws=dance-motion): the writer is now gltf_anim_export_qtrack (caller-supplied full-quaternion
6// track, any key count, any animation name) and gltf_anim_export is the walk-table wrapper over it -- ONE writer,
7// two callers, so real mocap (nx_dance_emit) and the synthetic walk share every byte of container logic.
8// license_tier: ORIGINAL
9import "nx_syscalls.nx"
10import "nx_trimesh.nx"
11import "nx_itrig.nx"
12import "nx_gltf_mesh.nx"
13import "nx_glbnorm_lib.nx"
14const GA_MAGIC_3217: i64 = 3217
15const GA_MAGIC_25736: i64 = 25736
16const GA_MAGIC_4096: i64 = 4096
17const GA_MAGIC_1400: i64 = 1400
18const GA_MAGIC_1700: i64 = 1700
19const GA_MAGIC_2400: i64 = 2400
20const GA_MAGIC_8192: i64 = 8192
21const GA_MAGIC_2000000000: i64 = 2000000000
22const GA_MAGIC_32767: i64 = 32767
23const GA_MAGIC_65535: i64 = 65535
24const GA_MAGIC_65536: i64 = 65536
25
26// encode a fixed-point value n/2^fb as float32 bits (n integer). Divides by 2^fb = subtract fb from the exponent field.
27func ga_f32fx(n: i64, fb: i64) -> i64 { if n == 0 { return 0 } return gl_f32bits(n) - (fb<<23) }
28
29const GA_K: i64 = 9 // walk keyframes (dt = 1/8 s -> exact float times, 1.0 s cycle)
30const GA_NJ: i64 = 11 // joints: 0 root, 1 Lsh,2 Lel,3 Rsh,4 Rel,5 Lhip,6 Lkn,7 Rhip,8 Rkn, 9 Lank,10 Rank (ankle = the calf landmark, unanimated; debt 1785900993)
31
32// absolute bind pivots per joint (joint 0 = root at origin).
33// ★ROOT FIX 2026-08-04 (caught by the R3 closing measurement, debt 1785900993): this table was a BAKED
34// COPY of the atlas's base pivots, so a parametrically-reshaped body exported with the ORIGINAL skeleton --
35// mesh moved, rig did not, and the retarget convergence test correctly refused to converge. The atlas is
36// the SSOT of its own rig: read ba_piv(). The literals below remain ONLY as the fallback for a caller that
37// exports before any atlas_build (pivots unallocated), and they are the atlas's own defaults.
38func ga_piv(j: i64, axis: i64) -> i64 {
39 if j > 0 { if ba_piv_ready() == 1 { return ba_piv(j, axis) } }
40 let px: *i64 = sys_mmap(GA_NJ*8) as *i64; let py: *i64 = sys_mmap(GA_NJ*8) as *i64; let pz: *i64 = sys_mmap(GA_NJ*8) as *i64
41 px[0]=0; py[0]=0; pz[0]=0
42 px[1]=0-185; py[1]=520; pz[1]=15
43 px[2]=0-205; py[2]=300; pz[2]=20
44 px[3]=185; py[3]=520; pz[3]=15
45 px[4]=205; py[4]=300; pz[4]=20
46 px[5]=0-88; py[5]=0-40; pz[5]=10
47 px[6]=0-84; py[6]=0-430; pz[6]=20
48 px[7]=88; py[7]=0-40; pz[7]=10
49 px[8]=84; py[8]=0-430; pz[8]=20
50 px[9]=0-84; py[9]=0-830; pz[9]=20
51 px[10]=84; py[10]=0-830; pz[10]=20
52 if axis == 0 { return px[j] }
53 if axis == 1 { return py[j] }
54 return pz[j]
55}
56// walk-cycle angle (it4096) for bone j (1..8) at keyframe k (0..K-1)
57func ga_angle(j: i64, k: i64) -> i64 {
58 let phase: i64 = k * GA_MAGIC_3217 // k * 2PI4096/(K-1) = k*GA_MAGIC_25736/8
59 let s: i64 = it_sin4096(phase) // [-GA_MAGIC_4096,GA_MAGIC_4096]
60 let c: i64 = it_cos4096(phase)
61 if j == 1 { return GA_MAGIC_1400*s/GA_MAGIC_4096 } // L shoulder (+arm swing)
62 if j == 3 { return 0-GA_MAGIC_1400*s/GA_MAGIC_4096 } // R shoulder (opposite)
63 if j == 2 { return 700 } // L elbow (slight constant bend)
64 if j == 4 { return 700 } // R elbow
65 if j == 5 { return 0-GA_MAGIC_1700*s/GA_MAGIC_4096 } // L hip (opposite the L arm)
66 if j == 7 { return GA_MAGIC_1700*s/GA_MAGIC_4096 } // R hip
67 if j == 6 { return GA_MAGIC_2400*(GA_MAGIC_4096-c)/GA_MAGIC_8192 } // L knee (bends 0..GA_MAGIC_2400 over the cycle)
68 if j == 8 { return GA_MAGIC_2400*(GA_MAGIC_4096+c)/GA_MAGIC_8192 } // R knee (opposite phase)
69 return 0
70}
71
72// THE writer: export the current trimesh as a skinned+animated .glb with a CALLER-SUPPLIED rotation track.
73// jarr[i] = joint index (0..8) vertex i is bound to. qtrk[((bn-1)*nk + k)*4 + c] = quaternion component c
74// (x,y,z,w) fx12 (x4096) for animated bone bn (1..8) at key k. nk = key count (times = k/8 s, exact f32).
75// aname = animation name bytes. -> total .glb length.
76func gltf_anim_export_qtrack(out: *u8, jarr: *i64, qtrk: *i64, nk: i64, aname: *u8) -> i64 {
77 let NV: i64 = tm_nv(); let NT: i64 = tm_nt(); let NI: i64 = NT*3
78 // ---- BIN layout ----
79 let posOff: i64 = 0; let posLen: i64 = NV*12
80 let nrmOff: i64 = posLen; let nrmLen: i64 = NV*GL_VEC3_F32_BYTES
81 let colOff: i64 = gl_align4(nrmOff+nrmLen);let colLen: i64 = NV*4
82 let joOff: i64 = colOff+colLen; let joLen: i64 = NV*4
83 let weOff: i64 = joOff+joLen; let weLen: i64 = NV*4
84 let idxOff: i64 = weOff+weLen; let idxLen: i64 = NI*4
85 let ibmOff: i64 = idxOff+idxLen; let ibmLen: i64 = GA_NJ*64
86 let inOff: i64 = ibmOff+ibmLen; let inLen: i64 = nk*4
87 let outOff: i64 = inOff+inLen; let outLen1: i64 = nk*16 // per animated bone (8 bones)
88 let binLen: i64 = gl_align4(outOff + outLen1*8)
89 let bin: *u8 = sys_mmap(binLen + 64)
90 let vp: *i64 = sys_mmap(24) as *i64
91 let tt: *i64 = sys_mmap(24) as *i64
92 var minx: i64=GA_MAGIC_2000000000; var miny: i64=GA_MAGIC_2000000000; var minz: i64=GA_MAGIC_2000000000
93 var maxx: i64=0-GA_MAGIC_2000000000; var maxy: i64=0-GA_MAGIC_2000000000; var maxz: i64=0-GA_MAGIC_2000000000
94 var a: i64 = 0; var at: i64 = posOff
95 while a < NV { tm_vpos(a,vp); at=gl_f32(bin,at,vp[0]); at=gl_f32(bin,at,vp[1]); at=gl_f32(bin,at,vp[2]); if vp[0]<minx{minx=vp[0]} if vp[0]>maxx{maxx=vp[0]} if vp[1]<miny{miny=vp[1]} if vp[1]>maxy{maxy=vp[1]} if vp[2]<minz{minz=vp[2]} if vp[2]>maxz{maxz=vp[2]} a=a+1 }
96 // normals: {VEC3, FLOAT}, unit -- THE SHARED EMITTER, not a copy. This line used to be a
97 // character-for-character duplicate of nx_gltf_mesh's normalized-int16 loop, which is why the same two
98 // validator errors appeared in both exporters' output and had to be fixed twice. One owner now.
99 at = gl_emit_normals(bin, nrmOff, NV)
100 a=0; at=colOff
101 while a < NV { let col: i64=tm_getvcol(a); at=gl_u8(bin,at,col&255); at=gl_u8(bin,at,(col>>8)&255); at=gl_u8(bin,at,(col>>16)&255); at=gl_u8(bin,at,255); a=a+1 }
102 a=0; at=joOff
103 while a < NV { at=gl_u8(bin,at,jarr[a]); at=gl_u8(bin,at,0); at=gl_u8(bin,at,0); at=gl_u8(bin,at,0); a=a+1 }
104 a=0; at=weOff
105 while a < NV { at=gl_u8(bin,at,255); at=gl_u8(bin,at,0); at=gl_u8(bin,at,0); at=gl_u8(bin,at,0); a=a+1 } // normalized ubyte weight [1,0,0,0]
106 var tix: i64=0; at=idxOff
107 while tix < NT { tm_tget(tix,tt); at=gl_u32(bin,at,tt[0]); at=gl_u32(bin,at,tt[1]); at=gl_u32(bin,at,tt[2]); tix=tix+1 }
108 // inverse-bind matrices (column-major translate(-pivot))
109 at=ibmOff; var j: i64=0
110 while j < GA_NJ {
111 at=gl_f32(bin,at,1); at=gl_f32(bin,at,0); at=gl_f32(bin,at,0); at=gl_f32(bin,at,0)
112 at=gl_f32(bin,at,0); at=gl_f32(bin,at,1); at=gl_f32(bin,at,0); at=gl_f32(bin,at,0)
113 at=gl_f32(bin,at,0); at=gl_f32(bin,at,0); at=gl_f32(bin,at,1); at=gl_f32(bin,at,0)
114 at=gl_f32(bin,at,0-ga_piv(j,0)); at=gl_f32(bin,at,0-ga_piv(j,1)); at=gl_f32(bin,at,0-ga_piv(j,2)); at=gl_f32(bin,at,1)
115 j=j+1
116 }
117 // animation input (times: k/8 seconds, exact)
118 at=inOff; var k: i64=0
119 while k < nk { at=gl_u32(bin,at,ga_f32fx(k,3)); k=k+1 }
120 // animation outputs: 8 bones (joints 1..8), nk quaternions each (full xyzw, fx12)
121 var bn: i64=1
122 while bn <= 8 {
123 k=0
124 while k < nk {
125 let qb: i64 = ((bn-1)*nk + k)*4
126 at=gl_u32(bin,at,ga_f32fx(qtrk[qb],12))
127 at=gl_u32(bin,at,ga_f32fx(qtrk[qb+1],12))
128 at=gl_u32(bin,at,ga_f32fx(qtrk[qb+2],12))
129 at=gl_u32(bin,at,ga_f32fx(qtrk[qb+3],12))
130 k=k+1
131 }
132 bn=bn+1
133 }
134 // ---- JSON ----
135 let json: *u8 = sys_mmap(GA_MAGIC_65536)
136 var jj: i64 = 0
137 jj=jcat(json,jj,"{\"asset\":{\"version\":\"2.0\",\"generator\":\"nishi nx_gltf_anim\"},\"scene\":0,\"scenes\":[{\"nodes\":[0,1]}]," as *u8)
138 // nodes: 0 mesh(skin), 1 root, 2 Lsh,3 Lel,4 Rsh,5 Rel,6 Lhip,7 Lkn,8 Rhip,9 Rkn,10 Lank,11 Rank
139 jj=jcat(json,jj,"\"nodes\":[{\"mesh\":0,\"skin\":0},{\"name\":\"root\",\"children\":[2,4,6,8]}," as *u8)
140 // helper to write a joint node with translation (local = pivot - parentPivot) + optional child
141 // Lsh (parent root): T=pivot1 ; child Lel(3)
142 jj=jcat(json,jj,"{\"name\":\"Lsh\",\"children\":[3],\"translation\":["as *u8); jj=jnum(json,jj,ga_piv(1,0)); jj=jcat(json,jj,","as *u8); jj=jnum(json,jj,ga_piv(1,1)); jj=jcat(json,jj,","as *u8); jj=jnum(json,jj,ga_piv(1,2)); jj=jcat(json,jj,"]},"as *u8)
143 jj=jcat(json,jj,"{\"name\":\"Lel\",\"translation\":["as *u8); jj=jnum(json,jj,ga_piv(2,0)-ga_piv(1,0)); jj=jcat(json,jj,","as *u8); jj=jnum(json,jj,ga_piv(2,1)-ga_piv(1,1)); jj=jcat(json,jj,","as *u8); jj=jnum(json,jj,ga_piv(2,2)-ga_piv(1,2)); jj=jcat(json,jj,"]},"as *u8)
144 jj=jcat(json,jj,"{\"name\":\"Rsh\",\"children\":[5],\"translation\":["as *u8); jj=jnum(json,jj,ga_piv(3,0)); jj=jcat(json,jj,","as *u8); jj=jnum(json,jj,ga_piv(3,1)); jj=jcat(json,jj,","as *u8); jj=jnum(json,jj,ga_piv(3,2)); jj=jcat(json,jj,"]},"as *u8)
145 jj=jcat(json,jj,"{\"name\":\"Rel\",\"translation\":["as *u8); jj=jnum(json,jj,ga_piv(4,0)-ga_piv(3,0)); jj=jcat(json,jj,","as *u8); jj=jnum(json,jj,ga_piv(4,1)-ga_piv(3,1)); jj=jcat(json,jj,","as *u8); jj=jnum(json,jj,ga_piv(4,2)-ga_piv(3,2)); jj=jcat(json,jj,"]},"as *u8)
146 jj=jcat(json,jj,"{\"name\":\"Lhip\",\"children\":[7],\"translation\":["as *u8); jj=jnum(json,jj,ga_piv(5,0)); jj=jcat(json,jj,","as *u8); jj=jnum(json,jj,ga_piv(5,1)); jj=jcat(json,jj,","as *u8); jj=jnum(json,jj,ga_piv(5,2)); jj=jcat(json,jj,"]},"as *u8)
147 jj=jcat(json,jj,"{\"name\":\"Lkn\",\"children\":[10],\"translation\":["as *u8); jj=jnum(json,jj,ga_piv(6,0)-ga_piv(5,0)); jj=jcat(json,jj,","as *u8); jj=jnum(json,jj,ga_piv(6,1)-ga_piv(5,1)); jj=jcat(json,jj,","as *u8); jj=jnum(json,jj,ga_piv(6,2)-ga_piv(5,2)); jj=jcat(json,jj,"]},"as *u8)
148 jj=jcat(json,jj,"{\"name\":\"Rhip\",\"children\":[9],\"translation\":["as *u8); jj=jnum(json,jj,ga_piv(7,0)); jj=jcat(json,jj,","as *u8); jj=jnum(json,jj,ga_piv(7,1)); jj=jcat(json,jj,","as *u8); jj=jnum(json,jj,ga_piv(7,2)); jj=jcat(json,jj,"]},"as *u8)
149 jj=jcat(json,jj,"{\"name\":\"Rkn\",\"children\":[11],\"translation\":["as *u8); jj=jnum(json,jj,ga_piv(8,0)-ga_piv(7,0)); jj=jcat(json,jj,","as *u8); jj=jnum(json,jj,ga_piv(8,1)-ga_piv(7,1)); jj=jcat(json,jj,","as *u8); jj=jnum(json,jj,ga_piv(8,2)-ga_piv(7,2)); jj=jcat(json,jj,"]},"as *u8)
150 jj=jcat(json,jj,"{\"name\":\"Lank\",\"translation\":["as *u8); jj=jnum(json,jj,ga_piv(9,0)-ga_piv(6,0)); jj=jcat(json,jj,","as *u8); jj=jnum(json,jj,ga_piv(9,1)-ga_piv(6,1)); jj=jcat(json,jj,","as *u8); jj=jnum(json,jj,ga_piv(9,2)-ga_piv(6,2)); jj=jcat(json,jj,"]},"as *u8)
151 jj=jcat(json,jj,"{\"name\":\"Rank\",\"translation\":["as *u8); jj=jnum(json,jj,ga_piv(10,0)-ga_piv(8,0)); jj=jcat(json,jj,","as *u8); jj=jnum(json,jj,ga_piv(10,1)-ga_piv(8,1)); jj=jcat(json,jj,","as *u8); jj=jnum(json,jj,ga_piv(10,2)-ga_piv(8,2)); jj=jcat(json,jj,"]}],"as *u8)
152 jj=jcat(json,jj,"\"skins\":[{\"inverseBindMatrices\":6,\"joints\":[1,2,3,4,5,6,7,8,9,10,11]}]," as *u8)
153 jj=jcat(json,jj,"\"meshes\":[{\"primitives\":[{\"attributes\":{\"POSITION\":0,\"NORMAL\":1,\"COLOR_0\":2,\"JOINTS_0\":3,\"WEIGHTS_0\":4},\"indices\":5,\"material\":0}]}]," as *u8)
154 jj=jcat(json,jj,"\"materials\":[{\"pbrMetallicRoughness\":{\"metallicFactor\":0,\"roughnessFactor\":1},\"doubleSided\":true}]," as *u8)
155 // accessors 0..15
156 jj=jcat(json,jj,"\"accessors\":[{\"bufferView\":0,\"componentType\":5126,\"count\":"as *u8); jj=jnum(json,jj,NV); jj=jcat(json,jj,",\"type\":\"VEC3\",\"min\":["as *u8); jj=jnum(json,jj,minx); jj=jcat(json,jj,","as *u8); jj=jnum(json,jj,miny); jj=jcat(json,jj,","as *u8); jj=jnum(json,jj,minz); jj=jcat(json,jj,"],\"max\":["as *u8); jj=jnum(json,jj,maxx); jj=jcat(json,jj,","as *u8); jj=jnum(json,jj,maxy); jj=jcat(json,jj,","as *u8); jj=jnum(json,jj,maxz); jj=jcat(json,jj,"]},"as *u8)
157 jj=jcat(json,jj,"{\"bufferView\":1,\"componentType\":5126,\"count\":"as *u8); jj=jnum(json,jj,NV); jj=jcat(json,jj,",\"type\":\"VEC3\"},"as *u8)
158 jj=jcat(json,jj,"{\"bufferView\":2,\"componentType\":5121,\"normalized\":true,\"count\":"as *u8); jj=jnum(json,jj,NV); jj=jcat(json,jj,",\"type\":\"VEC4\"},"as *u8)
159 jj=jcat(json,jj,"{\"bufferView\":3,\"componentType\":5121,\"count\":"as *u8); jj=jnum(json,jj,NV); jj=jcat(json,jj,",\"type\":\"VEC4\"},"as *u8)
160 jj=jcat(json,jj,"{\"bufferView\":4,\"componentType\":5121,\"normalized\":true,\"count\":"as *u8); jj=jnum(json,jj,NV); jj=jcat(json,jj,",\"type\":\"VEC4\"},"as *u8)
161 jj=jcat(json,jj,"{\"bufferView\":5,\"componentType\":5125,\"count\":"as *u8); jj=jnum(json,jj,NI); jj=jcat(json,jj,",\"type\":\"SCALAR\"},"as *u8)
162 jj=jcat(json,jj,"{\"bufferView\":6,\"componentType\":5126,\"count\":"as *u8); jj=jnum(json,jj,GA_NJ); jj=jcat(json,jj,",\"type\":\"MAT4\"},"as *u8)
163 // SPEC FIX 2026-08-25: the animation input accessor declared "max":[nk-1] -- the KEY INDEX -- while the
164 // values written are ga_f32fx(k,3), i.e. k/8 SECONDS. Every animated file therefore carried
165 // ACCESSOR_MAX_MISMATCH: the walk declared 8 against an actual 1, the dance declared 127 against 15.875.
166 // The bound is now rendered FROM THE VERY BITS THAT WERE WRITTEN, by the same expression that writes
167 // them, so it cannot drift again -- a declared bound recomputed in a different domain is exactly how
168 // this defect happened.
169 jj=jcat(json,jj,"{\"bufferView\":7,\"componentType\":5126,\"count\":"as *u8); jj=jnum(json,jj,nk); jj=jcat(json,jj,",\"type\":\"SCALAR\",\"min\":["as *u8); jj=gn_dec_f32(ga_f32fx(0,3), json, jj); jj=jcat(json,jj,"],\"max\":["as *u8); jj=gn_dec_f32(ga_f32fx(nk-1,3), json, jj); jj=jcat(json,jj,"]},"as *u8)
170 var acn: i64 = 8
171 while acn < 16 { if acn>8 { } jj=jcat(json,jj,"{\"bufferView\":"as *u8); jj=jnum(json,jj,acn); jj=jcat(json,jj,",\"componentType\":5126,\"count\":"as *u8); jj=jnum(json,jj,nk); jj=jcat(json,jj,",\"type\":\"VEC4\"}"as *u8); if acn<15 { jj=jcat(json,jj,","as *u8) } acn=acn+1 }
172 jj=jcat(json,jj,"]," as *u8)
173 // bufferViews 0..15
174 jj=jcat(json,jj,"\"bufferViews\":[" as *u8)
175 jj=jcat(json,jj,"{\"buffer\":0,\"byteOffset\":0,\"byteLength\":"as *u8); jj=jnum(json,jj,posLen); jj=jcat(json,jj,"},"as *u8)
176 jj=jcat(json,jj,"{\"buffer\":0,\"byteOffset\":"as *u8); jj=jnum(json,jj,nrmOff); jj=jcat(json,jj,",\"byteLength\":"as *u8); jj=jnum(json,jj,nrmLen); jj=jcat(json,jj,"},"as *u8)
177 jj=jcat(json,jj,"{\"buffer\":0,\"byteOffset\":"as *u8); jj=jnum(json,jj,colOff); jj=jcat(json,jj,",\"byteLength\":"as *u8); jj=jnum(json,jj,colLen); jj=jcat(json,jj,"},"as *u8)
178 jj=jcat(json,jj,"{\"buffer\":0,\"byteOffset\":"as *u8); jj=jnum(json,jj,joOff); jj=jcat(json,jj,",\"byteLength\":"as *u8); jj=jnum(json,jj,joLen); jj=jcat(json,jj,"},"as *u8)
179 jj=jcat(json,jj,"{\"buffer\":0,\"byteOffset\":"as *u8); jj=jnum(json,jj,weOff); jj=jcat(json,jj,",\"byteLength\":"as *u8); jj=jnum(json,jj,weLen); jj=jcat(json,jj,"},"as *u8)
180 jj=jcat(json,jj,"{\"buffer\":0,\"byteOffset\":"as *u8); jj=jnum(json,jj,idxOff); jj=jcat(json,jj,",\"byteLength\":"as *u8); jj=jnum(json,jj,idxLen); jj=jcat(json,jj,"},"as *u8)
181 jj=jcat(json,jj,"{\"buffer\":0,\"byteOffset\":"as *u8); jj=jnum(json,jj,ibmOff); jj=jcat(json,jj,",\"byteLength\":"as *u8); jj=jnum(json,jj,ibmLen); jj=jcat(json,jj,"},"as *u8)
182 jj=jcat(json,jj,"{\"buffer\":0,\"byteOffset\":"as *u8); jj=jnum(json,jj,inOff); jj=jcat(json,jj,",\"byteLength\":"as *u8); jj=jnum(json,jj,inLen); jj=jcat(json,jj,"}"as *u8)
183 var bvn: i64 = 0
184 while bvn < 8 { jj=jcat(json,jj,",{\"buffer\":0,\"byteOffset\":"as *u8); jj=jnum(json,jj,outOff+bvn*outLen1); jj=jcat(json,jj,",\"byteLength\":"as *u8); jj=jnum(json,jj,outLen1); jj=jcat(json,jj,"}"as *u8); bvn=bvn+1 }
185 jj=jcat(json,jj,"]," as *u8)
186 // animation: 8 samplers + 8 channels (bone j -> node j+1, path rotation)
187 jj=jcat(json,jj,"\"animations\":[{\"name\":\"" as *u8)
188 jj=jcat(json,jj,aname)
189 jj=jcat(json,jj,"\",\"samplers\":[" as *u8)
190 var sn: i64 = 0
191 while sn < 8 { if sn>0 { jj=jcat(json,jj,","as *u8) } jj=jcat(json,jj,"{\"input\":7,\"interpolation\":\"LINEAR\",\"output\":"as *u8); jj=jnum(json,jj,8+sn); jj=jcat(json,jj,"}"as *u8); sn=sn+1 }
192 jj=jcat(json,jj,"],\"channels\":[" as *u8)
193 sn = 0
194 while sn < 8 { if sn>0 { jj=jcat(json,jj,","as *u8) } jj=jcat(json,jj,"{\"sampler\":"as *u8); jj=jnum(json,jj,sn); jj=jcat(json,jj,",\"target\":{\"node\":"as *u8); jj=jnum(json,jj,sn+2); jj=jcat(json,jj,",\"path\":\"rotation\"}}"as *u8); sn=sn+1 }
195 jj=jcat(json,jj,"]}]," as *u8)
196 jj=jcat(json,jj,"\"buffers\":[{\"byteLength\":" as *u8); jj=jnum(json,jj,binLen); jj=jcat(json,jj,"}]}" as *u8)
197 let jsonLen: i64 = jj
198 let jsonPad: i64 = gl_align4(jsonLen)
199 // ---- GLB ----
200 var o: i64 = 0
201 o=gl_str(out,o,"glTF"as *u8); o=gl_u32(out,o,2)
202 let total: i64 = 12+8+jsonPad+8+binLen
203 o=gl_u32(out,o,total); o=gl_u32(out,o,jsonPad); o=gl_str(out,o,"JSON"as *u8)
204 var q: i64=0
205 while q<jsonLen { out[o]=json[q]; o=o+1; q=q+1 }
206 while q<jsonPad { out[o]=32 as u8; o=o+1; q=q+1 }
207 o=gl_u32(out,o,binLen); o=gl_u8(out,o,66); o=gl_u8(out,o,73); o=gl_u8(out,o,78); o=gl_u8(out,o,0)
208 q=0
209 while q<binLen { out[o]=bin[q]; o=o+1; q=q+1 }
210 return o
211}
212
213// the walk export, unchanged contract: builds the walk-cycle quaternion table from ga_angle and hands it
214// to the shared writer. Values are the exact ones the pre-refactor writer computed inline (sin/cos of the
215// same half-angles), so the emitted .glb is byte-identical -- proven by the gate artifact hash.
216func gltf_anim_export(out: *u8, jarr: *i64) -> i64 {
217 let qtrk: *i64 = sys_mmap(8*GA_K*4*8) as *i64
218 var bn: i64 = 1
219 while bn <= 8 {
220 var k: i64 = 0
221 while k < GA_K {
222 let ang: i64 = ga_angle(bn,k)
223 let half: i64 = ang/2
224 let qb: i64 = ((bn-1)*GA_K + k)*4
225 qtrk[qb] = it_sin4096(half)
226 qtrk[qb+1] = 0
227 qtrk[qb+2] = 0
228 qtrk[qb+3] = it_cos4096(half)
229 k=k+1
230 }
231 bn=bn+1
232 }
233 return gltf_anim_export_qtrack(out, jarr, qtrk, GA_K, "walk" as *u8)
234}