code wiki / (root) / nx_nxa_dyna.nx

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}