nx_nxa_pose.nx source
↩ module page · 296 lines · 11805 B
1// nx_nxa_pose.nx -- POSE section compiler: freezes ANIM channel-2 keyframes into the NXA
2// pose library (spec: knowledge/nxa_format_spec.md POSE). A pose entry is one frozen
3// channel-2 keyframe per joint -- [joint][qx qy qz qw q12][dtx dty dtz mm], world-delta
4// about bind, root horizontal motion removed -- so a player applies it with the exact LBS
5// the ANIM path already proves (M x = D(x-b)+b+dt), no FK reconstruction (which would
6// re-introduce the dead-chain-adoption shard class: adopted joints' dt is donor-relative,
7// not parent-relative).
8// freeze picks its frame BY MEASUREMENT when t=-1: the sample minimizing horizontal foot
9// separation (feet passing = legs together = reads as standing), never by taste.
10// usage: nx_nxa_pose freeze <in.nxa> <out.nxa> <pose_id> <t_ms|-1>
11// nx_nxa_pose list <in.nxa>
12// rc: 0 ok, 2 usage, 4 missing SKEL/ANIM/tracks, 5 corrupt, 6 future-version, 9 io
13// license_tier: ORIGINAL
14import "nx_syscalls.nx"
15import "nx_nxa.nx"
16import "nx_nxa_fk.nx"
17import "nx_nxa_posewrite_lib.nx"
18
19const NP_MAGIC_65535: i64 = 65535
20const NP_MAGIC_32767: i64 = 32767
21const NP_MAGIC_65536: i64 = 65536
22const NP_MAGIC_4096: i64 = 4096
23const NP_MAGIC_1000: i64 = 1000
24const NP_MAGIC_33: i64 = 33
25const NP_MAGIC_52: i64 = 52
26const NP_MAGIC_100: i64 = 100
27const NP_MAGIC_256: i64 = 256
28const NP_MAGIC_1000000000: i64 = 1000000000
29
30func npw(s: *u8) -> i64 { var n: i64=0; while s[n]!=(0 as u8){n=n+1} sys_write(1,s,n); return 0 }
31func npn(v: i64) -> i64 {
32 let t: *u8 = sys_mmap(32) as *u8
33 var m: i64 = v; var w: i64 = 0
34 if m<0 { t[w]=45 as u8; w=w+1; m=0-m }
35 if m==0 { t[w]=48 as u8; sys_write(1,t,w+1); return 0 }
36 let d: *u8 = sys_mmap(32) as *u8
37 var k: i64=0
38 while m>0 { d[k]=(48+(m%10)) as u8; m=m/10; k=k+1 }
39 var j: i64=0
40 while j<k { t[w]=d[k-1-j]; w=w+1; j=j+1 }
41 sys_write(1,t,w); return 0
42}
43func np_atoi(s: *u8) -> i64 {
44 var v: i64 = 0
45 var i: i64 = 0
46 var neg: i64 = 0
47 if s[0]==(45 as u8) { neg=1; i=1 }
48 while s[i]>=(48 as u8) {
49 if s[i]>(57 as u8) { return v }
50 v = v*10 + ((s[i] as i64) - 48)
51 i = i + 1
52 }
53 if neg==1 { v = 0-v }
54 return v
55}
56func np_seq(a: *u8, b: *u8) -> i64 {
57 var i: i64 = 0
58 while a[i]!=(0 as u8) {
59 if a[i]!=b[i] { return 0 }
60 i = i + 1
61 }
62 if b[i]!=(0 as u8) { return 0 }
63 return 1
64}
65// sign-extend a 16-bit lane out of an i64 word (channel-2 packed keys)
66func np_i16(w: i64, sh: i64) -> i64 {
67 var v: i64 = (w >> sh) & NP_MAGIC_65535
68 if v > NP_MAGIC_32767 { v = v - NP_MAGIC_65536 }
69 return v
70}
71// sample one channel-2 track at t ms: shortest-path integer nlerp dq + lerp dt.
72// out7 = [qx qy qz qw dtx dty dtz]; koff = word offset of first key in h
73func np_sample(h: *i64, koff: i64, nk: i64, t: i64, out7: *i64) -> i64 {
74 var k: i64 = 0
75 var scan: i64 = 1
76 while scan == 1 {
77 scan = 0
78 if k+1 < nk {
79 let tn: i64 = h[koff+(k+1)*2] & NP_MAGIC_65535
80 if tn <= t { k = k + 1; scan = 1 }
81 }
82 }
83 var k1: i64 = k + 1
84 if k1 >= nk { k1 = k }
85 let w0a: i64 = h[koff+k*2]
86 let w1a: i64 = h[koff+k*2+1]
87 let w0b: i64 = h[koff+k1*2]
88 let w1b: i64 = h[koff+k1*2+1]
89 let t0: i64 = w0a & NP_MAGIC_65535
90 let t1: i64 = w0b & NP_MAGIC_65535
91 var u: i64 = 0
92 if t1 > t0 { u = (t - t0)*NP_MAGIC_1000/(t1 - t0) }
93 if u < 0 { u = 0 }
94 if u > NP_MAGIC_1000 { u = NP_MAGIC_1000 }
95 let ax: i64 = np_i16(w0a,16)
96 let ay: i64 = np_i16(w0a,32)
97 let az: i64 = np_i16(w0a,48)
98 let aq: i64 = np_i16(w1a,0)
99 var bx: i64 = np_i16(w0b,16)
100 var by: i64 = np_i16(w0b,32)
101 var bz: i64 = np_i16(w0b,48)
102 var bq: i64 = np_i16(w1b,0)
103 if ax*bx + ay*by + az*bz + aq*bq < 0 { bx=0-bx; by=0-by; bz=0-bz; bq=0-bq }
104 out7[0] = ax + (bx-ax)*u/NP_MAGIC_1000
105 out7[1] = ay + (by-ay)*u/NP_MAGIC_1000
106 out7[2] = az + (bz-az)*u/NP_MAGIC_1000
107 out7[3] = aq + (bq-aq)*u/NP_MAGIC_1000
108 nf_qnorm(out7)
109 let dxa: i64 = np_i16(w1a,16)
110 let dya: i64 = np_i16(w1a,32)
111 let dza: i64 = np_i16(w1a,48)
112 let dxb: i64 = np_i16(w1b,16)
113 let dyb: i64 = np_i16(w1b,32)
114 let dzb: i64 = np_i16(w1b,48)
115 out7[4] = dxa + (dxb-dxa)*u/NP_MAGIC_1000
116 out7[5] = dya + (dyb-dya)*u/NP_MAGIC_1000
117 out7[6] = dza + (dzb-dza)*u/NP_MAGIC_1000
118 return 0
119}
120
121func main(argc: i64, argv: *i64) -> i64 {
122 if argc < 3 { npw("usage: nx_nxa_pose freeze <in.nxa> <out.nxa> <pose_id> <t_ms|-1> | list <in.nxa>\n" as *u8); return 2 }
123 let verb: *u8 = argv[1] as *u8
124 let lp: *i64 = sys_mmap(16) as *i64
125 let b: *u8 = sys_map_file(argv[2] as *u8, lp)
126 let flen: i64 = lp[0]
127 if flen < 96 { npw("unreadable\n" as *u8); return 5 }
128 let h: *i64 = b as *i64
129 let swo: i64 = nxa_find(b, flen, nxa_tag4("SKEL" as *u8))
130 if swo == 0 - 2 { npw("NXA-FUTURE-VERSION\n" as *u8); return 6 }
131 if swo == 0 - 3 { npw("NXA-CORRUPT skel\n" as *u8); return 5 }
132 if swo < 0 { npw("no SKEL\n" as *u8); return 4 }
133 let nj: i64 = h[swo]
134 let sk: *i64 = ((b as i64) + swo*8 + 8) as *i64
135 let awo: i64 = nxa_find(b, flen, nxa_tag4("ANIM" as *u8))
136 if awo == 0 - 3 { npw("NXA-CORRUPT anim\n" as *u8); return 5 }
137 let pwo: i64 = nxa_find(b, flen, nxa_tag4("POSE" as *u8))
138 if pwo == 0 - 3 { npw("NXA-CORRUPT pose\n" as *u8); return 5 }
139
140 if np_seq(verb, "list" as *u8) == 1 {
141 if pwo < 0 { npw("poses=0\n" as *u8); return 0 }
142 let npz: i64 = h[pwo]
143 npw("poses=" as *u8); npn(npz); npw("\n" as *u8)
144 var rp: i64 = pwo + 1
145 var pi: i64 = 0
146 while pi < npz {
147 npw(" pose id=" as *u8); npn(h[rp])
148 npw(" entries=" as *u8); npn(h[rp+1]); npw("\n" as *u8)
149 rp = rp + 2 + h[rp+1]*8
150 pi = pi + 1
151 }
152 return 0
153 }
154 if np_seq(verb, "freeze" as *u8) == 0 { npw("unknown verb\n" as *u8); return 2 }
155 if argc < 6 { npw("usage: nx_nxa_pose freeze <in.nxa> <out.nxa> <pose_id> <t_ms|-1>\n" as *u8); return 2 }
156 if awo < 0 { npw("no ANIM\n" as *u8); return 4 }
157 let a4: *u8 = argv[4] as *u8
158 let a5: *u8 = argv[5] as *u8
159 let pid: i64 = np_atoi(a4)
160 var targ: i64 = np_atoi(a5)
161
162 // index channel-2 tracks by joint: koff/nk per joint (-1 = untracked)
163 let tko: *i64 = sys_mmap(nj*8 + 64) as *i64
164 let tkn: *i64 = sys_mmap(nj*8 + 64) as *i64
165 var j0: i64 = 0
166 while j0 < nj { tko[j0] = 0 - 1; tkn[j0] = 0; j0 = j0 + 1 }
167 let ntr: i64 = h[awo]
168 var rp2: i64 = awo + 1
169 var dur: i64 = 0
170 var ti: i64 = 0
171 var tracked: i64 = 0
172 while ti < ntr {
173 let jt: i64 = h[rp2]
174 let ch: i64 = h[rp2+1]
175 let nk: i64 = h[rp2+2]
176 if ch == 2 {
177 if jt >= 0 { if jt < nj { if nk > 0 {
178 tko[jt] = rp2 + 3
179 tkn[jt] = nk
180 tracked = tracked + 1
181 let lastt: i64 = h[rp2+3+(nk-1)*2] & NP_MAGIC_65535
182 if lastt > dur { dur = lastt }
183 } } }
184 rp2 = rp2 + 3 + nk*2
185 }
186 if ch == 0 { rp2 = rp2 + 3 + nk*4 }
187 if ch == 1 { rp2 = rp2 + 3 + nk*5 }
188 ti = ti + 1
189 }
190 if tracked == 0 { npw("no channel-2 tracks\n" as *u8); return 4 }
191
192 // structural picks from binds (no names in NXA): H = max bind z;
193 // root = nearest (0,0,.52H); feet = per side (bind x sign) the lowest-z TRACKED joint
194 var hmax: i64 = 0
195 var j1: i64 = 0
196 while j1 < nj { if sk[j1*8+3] > hmax { hmax = sk[j1*8+3] } j1 = j1 + 1 }
197 var rj: i64 = 0 - 1
198 var rsc: i64 = NP_MAGIC_1000000000
199 var fl: i64 = 0 - 1
200 var flz: i64 = NP_MAGIC_1000000000
201 var fr: i64 = 0 - 1
202 var frz: i64 = NP_MAGIC_1000000000
203 var j2: i64 = 0
204 while j2 < nj {
205 let bxj: i64 = sk[j2*8+1]
206 let bzj: i64 = sk[j2*8+3]
207 var ds: i64 = bzj - hmax*NP_MAGIC_52/NP_MAGIC_100
208 if ds < 0 { ds = 0-ds }
209 var axj: i64 = bxj
210 if axj < 0 { axj = 0-axj }
211 let sc: i64 = ds + 2*axj
212 if sc < rsc { rsc = sc; rj = j2 }
213 if tko[j2] >= 0 {
214 if bxj < 0-NP_MAGIC_1000 { if bzj < flz { flz = bzj; fl = j2 } }
215 if bxj > NP_MAGIC_1000 { if bzj < frz { frz = bzj; fr = j2 } }
216 }
217 j2 = j2 + 1
218 }
219
220 let ev: *i64 = sys_mmap(64) as *i64
221 let ev2: *i64 = sys_mmap(64) as *i64
222 // auto frame pick: minimize horizontal foot separation across the clip (33ms grid).
223 // dt lanes are MILLIMETERS (the bake divides by 100 to fit i16); binds are 0.01mm
224 // units -- scale dt x100 before mixing (the unit-mismatch class MEASURED 2026-07-29:
225 // consumers applying raw lanes rendered every baked translation at 1%)
226 if targ < 0 {
227 if fl < 0 { npw("no left foot track\n" as *u8); return 4 }
228 if fr < 0 { npw("no right foot track\n" as *u8); return 4 }
229 var bt: i64 = 0
230 var bsep: i64 = NP_MAGIC_1000000000*NP_MAGIC_1000
231 var t: i64 = 0
232 while t <= dur {
233 np_sample(h, tko[fl], tkn[fl], t, ev)
234 np_sample(h, tko[fr], tkn[fr], t, ev2)
235 let px: i64 = (sk[fl*8+1] + ev[4]*NP_MAGIC_100) - (sk[fr*8+1] + ev2[4]*NP_MAGIC_100)
236 let py: i64 = (sk[fl*8+2] + ev[5]*NP_MAGIC_100) - (sk[fr*8+2] + ev2[5]*NP_MAGIC_100)
237 let sep: i64 = px*px + py*py
238 if sep < bsep { bsep = sep; bt = t }
239 t = t + NP_MAGIC_33
240 }
241 targ = bt
242 npw("auto t_ms=" as *u8); npn(bt)
243 npw(" footsep_units=" as *u8); npn(nf_isqrt(bsep)); npw("\n" as *u8)
244 }
245
246 // sample every tracked joint at targ; POSE entries store dt in VERT units (mm lanes
247 // x100) so players compose them with binds directly, no scale knowledge needed
248 let pq: *i64 = sys_mmap(nj*32 + 64) as *i64
249 let pd: *i64 = sys_mmap(nj*24 + 64) as *i64
250 var j3: i64 = 0
251 while j3 < nj {
252 if tko[j3] >= 0 {
253 np_sample(h, tko[j3], tkn[j3], targ, ev)
254 pq[j3*4] = ev[0]
255 pq[j3*4+1] = ev[1]
256 pq[j3*4+2] = ev[2]
257 pq[j3*4+3] = ev[3]
258 pd[j3*3] = ev[4]*NP_MAGIC_100
259 pd[j3*3+1] = ev[5]*NP_MAGIC_100
260 pd[j3*3+2] = ev[6]*NP_MAGIC_100
261 }
262 j3 = j3 + 1
263 }
264 // root horizontal motion removal (same convention the runtime applies to the clip)
265 var rdx: i64 = 0
266 var rdy: i64 = 0
267 if rj >= 0 { if tko[rj] >= 0 { rdx = pd[rj*3]; rdy = pd[rj*3+1] } }
268 var j4: i64 = 0
269 while j4 < nj {
270 if tko[j4] >= 0 { pd[j4*3] = pd[j4*3] - rdx; pd[j4*3+1] = pd[j4*3+1] - rdy }
271 j4 = j4 + 1
272 }
273
274 // POSE emission goes through the estate's ONE POSE-section writer (nx_nxa_posewrite_lib, 2026-09-05): kept poses
275 // first, the frozen one LAST, the same id REPLACES, TOC rebuilt. This organ used to carry its own inline copy of
276 // that writer beside nx_nxa_retarget's -- the duplicate-ruler defect; now there is exactly one.
277 let jl: *i64 = sys_mmap(nj*8 + 64) as *i64
278 var ntr: i64 = 0
279 var j5: i64 = 0
280 while j5 < nj {
281 if tko[j5] >= 0 { jl[ntr] = j5; ntr = ntr + 1 }
282 j5 = j5 + 1
283 }
284 // a partition that does not sum is a leak: the joint list must hold exactly the tracked count
285 if ntr != tracked { npw("NXA-POSE-REFUSE tracked joint list does not sum to the tracked count\n" as *u8); return 5 }
286 let pst: *i64 = sys_mmap(64) as *i64
287 let wrote: i64 = npw_write(b, flen, pid, jl, pq, pd, ntr, argv[3] as *u8, pst)
288 if wrote < 0 { npw("open out failed posewrite_code=" as *u8); npn(wrote); npw("\n" as *u8); return 9 }
289 npw("POSE written id=" as *u8); npn(pid)
290 npw(" t_ms=" as *u8); npn(targ)
291 npw(" entries=" as *u8); npn(ntr)
292 npw(" kept=" as *u8); npn(pst[NPW_ST_KEPT])
293 npw(" sections=" as *u8); npn(pst[NPW_ST_SECTIONS])
294 npw(" bytes=" as *u8); npn(wrote); npw("\n" as *u8)
295 return 0
296}