nx_nxa_dyna.nx source
↩ module page · 303 lines · 14208 B
1// nx_nxa_dyna.nx -- WRITE THE DYNA SECTION: per-bone soft-tissue physics DECLARED IN THE ASSET,
2// with the lateral breast-bone pair DERIVED FROM THE MESH'S OWN GEOMETRY.
3//
4// WHY THIS EXISTS. nx_asset_floor_gate measured our shipped character and found 5 midline chest
5// joints and ZERO lateral pair. Real breast bones come in L/R pairs OFF the midline; midline joints
6// at chest height ARE THE SPINE CHAIN, so driving them swings the whole torso -- the wobble the
7// operator kept reporting. With no bone to drive, chest motion had to be faked at the shader vertex
8// band, and the engine's own joint picker (|bind.x| < 9000, i.e. near-midline) was selecting spine
9// joints as stand-ins using a superseded height normalisation (z/171530, which ignores the mesh
10// origin -- the same error that once put the nipples below the legs).
11//
12// THE FIX IS A FORMAT CAPABILITY, NOT ANOTHER SHADER PATCH. FBX and its class carry no physics at
13// all: a rig travels as geometry plus a joint tree, and every consumer re-invents the dynamics in
14// engine code. DYNA makes the ASSET carry its own soft-tissue contract -- anchor, axis, stiffness,
15// damping, travel clamp, influence radius, falloff -- so the engine DRIVES DECLARED DATA instead of
16// hardcoding bands. That is the difference between a mannequin and a body.
17//
18// ANCHORS ARE MEASURED, NEVER GUESSED. For each side we take the CENTROID of the front-hemisphere
19// vertices inside the bust band. Anatomy therefore comes from the mesh in front of us, so a
20// different body type yields different anchors -- and the firm/bouncy RANGE the operator asked for
21// falls out of geometry rather than a table of magic numbers.
22//
23// Physics constants are NOT invented here: they are the banked, in-band values already proven by
24// the chest-motion oracle (2.5 Hz, 3.2 cm travel, 0.0 cm torso, 0.89 coherence) and by nx_softbind
25// (chest K base 60 step 25, C base 100 step 16). Moving them out of code and into the asset is the
26// point -- rule 11, and the recombinator wants them as data.
27//
28// nx_nxa_dyna <in.nxa> <out.nxa>
29//
30// ADDITIVE AND IDEMPOTENT: the input is never modified; an existing DYNA is REPLACED, not doubled,
31// so running twice is identical to running once.
32//
33// DYNA payload (i64 words): [0]=n_bones [1]=stride(12), then per bone:
34// 0 side(-1 L,+1 R,0 mid) 1 anchor_x 2 anchor_y 3 anchor_z
35// 4 k_q8 5 c_q8 6 max_disp 7 influence_radius 8 falloff_q8
36// 9 axis_mask(1=x,2=y,4=z) 10 k_step_q8 11 c_step_q8
37// license_tier: ORIGINAL No hw writes (Rule 26). expect_exit: 0
38import "nx_syscalls.nx"
39import "nx_nxa.nx"
40const ND_MAGIC_1710: i64 = 1710
41const ND_MAGIC_4096: i64 = 4096
42
43const ND_HDR: i64 = 32
44const ND_TOCE: i64 = 32
45const ND_STRIDE: i64 = 12
46const ND_MAXSEC: i64 = 64
47const ND_MODE: i64 = 0x1a4
48const ND_ERRFD: i64 = 2
49const ND_EXIT_USAGE: i64 = 2
50const ND_EXIT_BAD: i64 = 3
51const ND_EXIT_IO: i64 = 1
52const ND_EXIT_NOPAIR: i64 = 4
53// bust band: NIP at 744 permil of stature, derived 2026-08-02 from anthropometric ratios mapped
54// through the asset's own pelvis(580)/head(917) anchors. Half-width 40 permil brackets the band
55// the shader already used (0.085 smoothstep width).
56const ND_NIP_PERMIL: i64 = 744
57const ND_BAND_PERMIL: i64 = 40
58// banked oracle values -- see nx_softbind + gamefeel_oracle.conf. NOT tuned here.
59const ND_K_BASE: i64 = 60
60const ND_K_STEP: i64 = 25
61const ND_C_BASE: i64 = 100
62const ND_C_STEP: i64 = 16
63const ND_FALLOFF_Q8: i64 = 256
64const ND_AXIS_ALL: i64 = 7
65
66func nd_slen(s: *u8) -> i64 { var n: i64 = 0; while s[n] != (0 as u8) { n = n + 1 } return n }
67func nd_werr(s: *u8) -> i64 { sys_write(ND_ERRFD, s, nd_slen(s)); return 0 }
68func nd_out(s: *u8) -> i64 { sys_write(1, s, nd_slen(s)); return 0 }
69func nd_num(v: i64) -> i64 {
70 if v == 0 { sys_write(1, "0" as *u8, 1); return 0 }
71 var m: i64 = v
72 if m < 0 { sys_write(1, "-" as *u8, 1); m = 0 - m }
73 let t: *u8 = sys_mmap(32)
74 var k: i64 = 0
75 while m > 0 { t[k] = (48 + (m % 10)) as u8; m = m / 10; k = k + 1 }
76 let o: *u8 = sys_mmap(32)
77 var i: i64 = 0
78 while i < k { o[i] = t[k - 1 - i]; i = i + 1 }
79 sys_write(1, o, k)
80 return 0
81}
82func nd_rd64(b: *u8, off: i64) -> i64 {
83 var v: i64 = 0
84 var i: i64 = 7
85 while i >= 0 { v = v*256 + ((b[off + i] & 0xff) as i64); i = i - 1 }
86 return v
87}
88func nd_wr64(b: *u8, off: i64, v: i64) -> i64 {
89 var m: i64 = v
90 var i: i64 = 0
91 while i < 8 { b[off + i] = (m & 0xff) as u8; m = m >> 8; i = i + 1 }
92 return 0
93}
94func nd_tageq(b: *u8, off: i64, t: *u8) -> i64 {
95 var i: i64 = 0
96 while i < 4 { if b[off + i] != t[i] { return 0 } i = i + 1 }
97 return 1
98}
99
100func main(argc: i64, argv: *i64) -> i64 {
101 if argc < 3 {
102 nd_werr("usage: nx_nxa_dyna <in.nxa> <out.nxa>\n" as *u8)
103 nd_werr(" derives the lateral soft-tissue bone pair from the mesh and writes a DYNA section.\n" as *u8)
104 nd_werr(" <in.nxa> is never modified; an existing DYNA is replaced, so re-runs are identical.\n" as *u8)
105 sys_exit(ND_EXIT_USAGE)
106 return ND_EXIT_USAGE
107 }
108 let inp: *u8 = argv[1] as *u8
109 let outp: *u8 = argv[2] as *u8
110 let lp: *i64 = sys_mmap(16) as *i64
111 let b: *u8 = sys_read_file(inp, lp)
112 if (b as i64) == 0 { nd_werr("DYNA-RED cannot read input NXA\n" as *u8); sys_exit(ND_EXIT_IO); return ND_EXIT_IO }
113 let flen: i64 = lp[0]
114 if flen < ND_HDR { nd_werr("DYNA-RED file too short to be an NXA\n" as *u8); sys_exit(ND_EXIT_BAD); return ND_EXIT_BAD }
115 if nd_rd64(b, 0) != nxa_magic() { nd_werr("DYNA-RED not an NXA (bad magic)\n" as *u8); sys_exit(ND_EXIT_BAD); return ND_EXIT_BAD }
116 if nd_rd64(b, 8) > NXA_VER { nd_werr("DYNA-RED future version -- refusing\n" as *u8); sys_exit(ND_EXIT_BAD); return ND_EXIT_BAD }
117 let ns: i64 = nd_rd64(b, 16)
118 if ns < 1 { nd_werr("DYNA-RED section count invalid\n" as *u8); sys_exit(ND_EXIT_BAD); return ND_EXIT_BAD }
119 if ns >= ND_MAXSEC { nd_werr("DYNA-RED section table full\n" as *u8); sys_exit(ND_EXIT_BAD); return ND_EXIT_BAD }
120
121 // ---- locate VERT and any existing DYNA ----
122 var vo: i64 = 0 - 1
123 var dyi: i64 = 0 - 1
124 var s: i64 = 0
125 while s < ns {
126 let e: i64 = ND_HDR + s*ND_TOCE
127 if nd_tageq(b, e, "VERT" as *u8) == 1 { vo = nd_rd64(b, e + 8) }
128 if nd_tageq(b, e, "DYNA" as *u8) == 1 { dyi = s }
129 s = s + 1
130 }
131 if vo < 0 { nd_werr("DYNA-RED no VERT section -- cannot derive anatomy\n" as *u8); sys_exit(ND_EXIT_BAD); return ND_EXIT_BAD }
132 let nv: i64 = nd_rd64(b, vo)
133
134 // ---- pass 1: stature from the VERTEX span (the same basis the browser probe uses; a first
135 // version of the floor gate normalised on the JOINT span and the two instruments then
136 // CONTRADICTED each other on the same asset, purely because the joint ladder is shorter than
137 // the mesh -- feet and crown carry no bones). ----
138 var zmin: i64 = 0
139 var zmax: i64 = 0
140 var first: i64 = 1
141 var i: i64 = 0
142 while i < nv {
143 let vz: i64 = nd_rd64(b, vo + 8 + (i*3 + 2)*8)
144 if first == 1 { zmin = vz; zmax = vz; first = 0 }
145 if vz < zmin { zmin = vz }
146 if vz > zmax { zmax = vz }
147 i = i + 1
148 }
149 let span: i64 = zmax - zmin
150 if span <= 0 { nd_werr("DYNA-RED degenerate vertex span\n" as *u8); sys_exit(ND_EXIT_BAD); return ND_EXIT_BAD }
151
152 // ---- pass 2: per-side centroid of FRONT-HEMISPHERE vertices inside the bust band ----
153 // front hemisphere is y<0 in this asset's frame (the floor gate and the shader agree on this).
154 var lx: i64 = 0
155 var ly: i64 = 0
156 var lz: i64 = 0
157 var ln: i64 = 0
158 var rx: i64 = 0
159 var ry: i64 = 0
160 var rz: i64 = 0
161 var rn: i64 = 0
162 let plo: i64 = ND_NIP_PERMIL - ND_BAND_PERMIL
163 let phi: i64 = ND_NIP_PERMIL + ND_BAND_PERMIL
164 var j: i64 = 0
165 while j < nv {
166 let px: i64 = nd_rd64(b, vo + 8 + (j*3)*8)
167 let py: i64 = nd_rd64(b, vo + 8 + (j*3 + 1)*8)
168 let pz: i64 = nd_rd64(b, vo + 8 + (j*3 + 2)*8)
169 let pm: i64 = (pz - zmin)*1000/span
170 if pm >= plo { if pm <= phi { if py < 0 {
171 if px < 0 { lx = lx + px; ly = ly + py; lz = lz + pz; ln = ln + 1 }
172 if px > 0 { rx = rx + px; ry = ry + py; rz = rz + pz; rn = rn + 1 }
173 } } }
174 j = j + 1
175 }
176 // REFUSE rather than emit a fabricated pair. A DYNA section naming anchors we could not measure
177 // would be worse than none: the engine would drive declared-looking data that is actually a
178 // guess, and every downstream reader would treat it as measured.
179 if ln == 0 { nd_werr("DYNA-RED no left-side front vertices in the bust band -- refusing to fabricate an anchor\n" as *u8); sys_exit(ND_EXIT_NOPAIR); return ND_EXIT_NOPAIR }
180 if rn == 0 { nd_werr("DYNA-RED no right-side front vertices in the bust band -- refusing to fabricate an anchor\n" as *u8); sys_exit(ND_EXIT_NOPAIR); return ND_EXIT_NOPAIR }
181 let lax: i64 = lx/ln
182 let lay: i64 = ly/ln
183 let laz: i64 = lz/ln
184 let rax: i64 = rx/rn
185 let ray: i64 = ry/rn
186 let raz: i64 = rz/rn
187 // influence radius = half the gap between the two anchors, so the L and R fields meet at the
188 // midline and neither reaches across it. Derived from the body, not chosen.
189 var gap: i64 = rax - lax
190 if gap < 0 { gap = 0 - gap }
191 let infl: i64 = gap/2
192 // travel clamp from the oracle: 3.2 cm measured, and the asset's stature is `span` units for a
193 // ~1.71 m figure, so the clamp scales with the body instead of being a fixed constant.
194 let maxd: i64 = span*32/ND_MAGIC_1710
195
196 // ---- build the new file ----
197 var nsec: i64 = ns
198 if dyi < 0 { nsec = ns + 1 }
199 let dwords: i64 = 2 + 2*ND_STRIDE
200 let toclen: i64 = ND_HDR + nsec*ND_TOCE
201 var total: i64 = toclen
202 // measure: every kept payload, then ours
203 var s2: i64 = 0
204 while s2 < ns {
205 if s2 != dyi {
206 let e2: i64 = ND_HDR + s2*ND_TOCE
207 total = total + nd_rd64(b, e2 + 16)*8
208 }
209 s2 = s2 + 1
210 }
211 total = total + dwords*8
212 let nb: *u8 = sys_mmap(total + ND_MAGIC_4096)
213 if (nb as i64) == 0 { nd_werr("DYNA-RED cannot allocate output\n" as *u8); sys_exit(ND_EXIT_IO); return ND_EXIT_IO }
214 nd_wr64(nb, 0, nxa_magic())
215 nd_wr64(nb, 8, NXA_VER)
216 nd_wr64(nb, 16, nsec)
217 // copy kept sections, laying payloads out contiguously after the (grown) TOC
218 var wo: i64 = toclen
219 var ti: i64 = 0
220 var s3: i64 = 0
221 while s3 < ns {
222 if s3 != dyi {
223 let e3: i64 = ND_HDR + s3*ND_TOCE
224 let oldoff: i64 = nd_rd64(b, e3 + 8)
225 let wl: i64 = nd_rd64(b, e3 + 16)
226 let te: i64 = ND_HDR + ti*ND_TOCE
227 nd_wr64(nb, te, nd_rd64(b, e3))
228 nd_wr64(nb, te + 8, wo)
229 nd_wr64(nb, te + 16, wl)
230 var k2: i64 = 0
231 while k2 < wl*8 { nb[wo + k2] = b[oldoff + k2]; k2 = k2 + 1 }
232 // recompute rather than copy the old checksum: a copied checksum would still verify if
233 // the copy loop were wrong, so it would validate its own bug.
234 let pw: *i64 = ((nb as i64) + wo) as *i64
235 nd_wr64(nb, te + 24, nxa_check2(1, pw, wl))
236 wo = wo + wl*8
237 ti = ti + 1
238 }
239 s3 = s3 + 1
240 }
241 // ---- the DYNA payload ----
242 let dstart: i64 = wo
243 nd_wr64(nb, wo, 2); wo = wo + 8
244 nd_wr64(nb, wo, ND_STRIDE); wo = wo + 8
245 nd_wr64(nb, wo, 0 - 1); wo = wo + 8
246 nd_wr64(nb, wo, lax); wo = wo + 8
247 nd_wr64(nb, wo, lay); wo = wo + 8
248 nd_wr64(nb, wo, laz); wo = wo + 8
249 nd_wr64(nb, wo, ND_K_BASE); wo = wo + 8
250 nd_wr64(nb, wo, ND_C_BASE); wo = wo + 8
251 nd_wr64(nb, wo, maxd); wo = wo + 8
252 nd_wr64(nb, wo, infl); wo = wo + 8
253 nd_wr64(nb, wo, ND_FALLOFF_Q8); wo = wo + 8
254 nd_wr64(nb, wo, ND_AXIS_ALL); wo = wo + 8
255 nd_wr64(nb, wo, ND_K_STEP); wo = wo + 8
256 nd_wr64(nb, wo, ND_C_STEP); wo = wo + 8
257 nd_wr64(nb, wo, 1); wo = wo + 8
258 nd_wr64(nb, wo, rax); wo = wo + 8
259 nd_wr64(nb, wo, ray); wo = wo + 8
260 nd_wr64(nb, wo, raz); wo = wo + 8
261 nd_wr64(nb, wo, ND_K_BASE); wo = wo + 8
262 nd_wr64(nb, wo, ND_C_BASE); wo = wo + 8
263 nd_wr64(nb, wo, maxd); wo = wo + 8
264 nd_wr64(nb, wo, infl); wo = wo + 8
265 nd_wr64(nb, wo, ND_FALLOFF_Q8); wo = wo + 8
266 nd_wr64(nb, wo, ND_AXIS_ALL); wo = wo + 8
267 nd_wr64(nb, wo, ND_K_STEP); wo = wo + 8
268 nd_wr64(nb, wo, ND_C_STEP); wo = wo + 8
269 let dte: i64 = ND_HDR + ti*ND_TOCE
270 nd_wr64(nb, dte, nxa_tag4("DYNA" as *u8))
271 nd_wr64(nb, dte + 8, dstart)
272 nd_wr64(nb, dte + 16, dwords)
273 let dpw: *i64 = ((nb as i64) + dstart) as *i64
274 nd_wr64(nb, dte + 24, nxa_check2(1, dpw, dwords))
275 // TOC checksum LAST -- it covers the table we just finished writing
276 let tbp: *i64 = ((nb as i64) + ND_HDR) as *i64
277 nd_wr64(nb, 24, nxa_check2(1, tbp, nsec*4))
278
279 let fd: i64 = sys_openat_wr(outp, ND_MODE)
280 if fd < 0 { nd_werr("DYNA-RED cannot open output for write\n" as *u8); sys_exit(ND_EXIT_IO); return ND_EXIT_IO }
281 var w2: i64 = 0
282 while w2 < wo {
283 let kk: i64 = sys_write(fd, ((nb as i64) + w2) as *u8, wo - w2)
284 if kk <= 0 { sys_close(fd); nd_werr("DYNA-RED short write\n" as *u8); sys_exit(ND_EXIT_IO); return ND_EXIT_IO }
285 w2 = w2 + kk
286 }
287 sys_close(fd)
288
289 nd_out("DYNA-GREEN sections " as *u8); nd_num(ns); nd_out(" -> " as *u8); nd_num(nsec)
290 if dyi >= 0 { nd_out(" (replaced existing DYNA)" as *u8) }
291 nd_out("\n stature span=" as *u8); nd_num(span)
292 nd_out(" bust band permil " as *u8); nd_num(plo); nd_out(".." as *u8); nd_num(phi)
293 nd_out("\n LEFT anchor from " as *u8); nd_num(ln); nd_out(" verts: x=" as *u8); nd_num(lax)
294 nd_out(" y=" as *u8); nd_num(lay); nd_out(" z=" as *u8); nd_num(laz)
295 nd_out("\n RIGHT anchor from " as *u8); nd_num(rn); nd_out(" verts: x=" as *u8); nd_num(rax)
296 nd_out(" y=" as *u8); nd_num(ray); nd_out(" z=" as *u8); nd_num(raz)
297 nd_out("\n lateral gap=" as *u8); nd_num(gap); nd_out(" influence=" as *u8); nd_num(infl)
298 nd_out(" travel clamp=" as *u8); nd_num(maxd)
299 nd_out("\n bytes=" as *u8); nd_num(wo)
300 nd_out("\n <- the pair is LATERAL and MEASURED: soft tissue now has a bone that is not the spine.\n" as *u8)
301 sys_exit(0)
302 return 0
303}