code wiki / (root) / nx_skeleton.nx

nx_skeleton.nx source

↩ module page · 216 lines · 10006 B

1// nx_skeleton.nx -- SKELETON + LINEAR-BLEND SKINNING (the character-foundation rung everything Elara needs 2// hangs off: morphs, IK, jiggle-region binding, posing -- VAMX ladder R3, rides nx_mesh3 + nx_itrig per the 3// reuse law). BONES ARE DATA: a rig is rows {parent, local offset, yaw, pitch} a mod can ship. World pose = 4// parent-composed integer 3x3 rotations (fx256) + offsets (fx256). Skinned vertex = sum of per-bone weighted 5// transforms of the vertex's BONE-LOCAL position (LBS, weights fx256 summing 256). 6// ALL INTEGER => deterministic, VM-vettable, wasm-ready (base-relative). license_tier: ORIGINAL 7// 8// --- 2026-08-23 CAPACITY IS NOW DERIVED FROM THE ASSET, NOT A COMPILE-TIME CEILING --------------- 9// MEASURED DEFECT: this lib carried SK_MAXB=32 bones and SK_MAXV=2048 verts as hard constants while 10// the estate's real rigged corpus is 104-370 joints and 14,164-423,919 vertices -- an 11.6x bone and 11// 207x vertex overflow. The only in-engine skinning evaluator was therefore DIMENSIONALLY INCAPABLE 12// of posing a single real character, which is the mechanical reason the served worlds render the 13// visiting cast in static bind pose: there was nothing callable to pose them with. A cap that is 14// smaller than its subject does not truncate loudly, it makes the whole capability unreachable. 15// FIX (the banked law: a buffer cap is not a number to tune -- REMOVE it): there is no ceiling any 16// more. The caller declares capacity from the ASSET'S OWN header counts via sk_init_cap, the arena 17// size comes from sk_bytes_for, and every offset is computed from the stored capacity at runtime. 18// SK_DEF_* below are NOT caps: they are the legacy constructor's defaults, kept so that every 19// existing caller of sk_init/sk_bytes gets byte-identical behaviour (see the stride note). 20// INFLUENCES ARE ALSO DERIVED: v1 hard-wired 2 bones per vertex; real SKIN sections carry their own 21// per-vertex influence count, so the vertex record is now (2*ninf pairs + 3 position) slots. The 22// stride is (2*ninf+4)*8 bytes -- one spare slot -- which at the legacy ninf=2 is EXACTLY the old 23// 64-byte stride, so the legacy arena layout is reproduced bit-for-bit and prior gates are unaffected. 24import "nx_syscalls.nx" 25import "nx_itrig.nx" 26 27// bone record: capacity slots at 256+i*256 28// [0]=parent(-1 root) [1..3]=local offset fx256 [4]=yaw4096 [5]=pitch4096 29// [6..14]=world rotation 3x3 fx256 (row-major) [15..17]=world position fx256 [18]=valid 30// skin vertex record: [0..2*ninf-1]=(bone,weight) pairs [2*ninf..2*ninf+2]=bone-local pos 31// (v1 simplification retained: ONE local pos shared by the influences -- valid when bind poses align) 32// 33// LEGACY DEFAULTS ONLY -- these are not ceilings; sk_init_cap takes the asset's real counts. 34const SK_DEF_B: i64 = 32 35const SK_DEF_V: i64 = 2048 36const SK_DEF_INF: i64 = 2 37 38// header 256B (32 slots): [0]=nbones [1]=nverts [2..10]=rot scratch [11..13]=vec scratch 39// [14]=capacity bones [15]=capacity verts [16]=influences per vertex 40// (in-arena scratch = wasm-ready: sys_mmap does not exist inside the wasm; base-relative organs 41// use ONLY the caller's arena) 42func sk_hdr(base: i64) -> *i64 { return base as *i64 } 43func sk_lr(base: i64) -> *i64 { return (base + 16) as *i64 } 44func sk_vs(base: i64) -> *i64 { return (base + 88) as *i64 } 45func sk_capb(base: i64) -> i64 { let h: *i64 = sk_hdr(base); return h[14] } 46func sk_capv(base: i64) -> i64 { let h: *i64 = sk_hdr(base); return h[15] } 47func sk_ninf(base: i64) -> i64 { let h: *i64 = sk_hdr(base); return h[16] } 48// stride DERIVED from the influence count, not declared: 2*ninf pair slots + 3 position + 1 spare. 49func sk_vstride(base: i64) -> i64 { return (2 * sk_ninf(base) + 4) * 8 } 50func sk_bone(base: i64, i: i64) -> *i64 { return (base + 256 + i * 256) as *i64 } 51func sk_vert(base: i64, i: i64) -> *i64 { return (base + 256 + sk_capb(base) * 256 + i * sk_vstride(base)) as *i64 } 52func sk_out(base: i64, i: i64) -> *i64 { return (base + 256 + sk_capb(base) * 256 + sk_capv(base) * sk_vstride(base) + i * 24) as *i64 } 53// arena bytes for a DECLARED capacity -- the caller sizes from the asset, nothing is assumed. 54func sk_bytes_for(capb: i64, capv: i64, ninf: i64) -> i64 { return 256 + capb * 256 + capv * ((2 * ninf + 4) * 8) + capv * 24 + 64 } 55func sk_bytes() -> i64 { return sk_bytes_for(SK_DEF_B, SK_DEF_V, SK_DEF_INF) } 56 57// capacity-declaring constructor: capb/capv/ninf come from the asset's own SKEL/SKIN counts. 58// REFUSES a non-positive influence count by returning -1: a zero-influence rig would silently 59// produce an unskinned (bind-pose) result, which is the exact failure this lib exists to end. 60func sk_init_cap(base: i64, capb: i64, capv: i64, ninf: i64) -> i64 { 61 if ninf < 1 { return 0 - 1 } 62 if capb < 1 { return 0 - 1 } 63 if capv < 1 { return 0 - 1 } 64 let h: *i64 = sk_hdr(base) 65 h[0] = 0 66 h[1] = 0 67 h[14] = capb 68 h[15] = capv 69 h[16] = ninf 70 return 0 71} 72func sk_init(base: i64) -> i64 { return sk_init_cap(base, SK_DEF_B, SK_DEF_V, SK_DEF_INF) } 73 74func sk_add_bone(base: i64, parent: i64, ox: i64, oy: i64, oz: i64) -> i64 { 75 let h: *i64 = sk_hdr(base) 76 if h[0] >= sk_capb(base) { return 0 - 1 } 77 let b: *i64 = sk_bone(base, h[0]) 78 b[0] = parent 79 b[1] = ox; b[2] = oy; b[3] = oz 80 b[4] = 0; b[5] = 0 81 b[18] = 0 82 h[0] = h[0] + 1 83 return h[0] - 1 84} 85func sk_pose(base: i64, bone: i64, yaw: i64, pitch: i64) -> i64 { 86 let b: *i64 = sk_bone(base, bone) 87 b[4] = yaw 88 b[5] = pitch 89 return 0 90} 91// legacy 2-influence add: unchanged call shape, now writing the generic record (identical at ninf=2). 92func sk_add_vert(base: i64, b0: i64, w0: i64, b1: i64, w1: i64, lx: i64, ly: i64, lz: i64) -> i64 { 93 let h: *i64 = sk_hdr(base) 94 if h[1] >= sk_capv(base) { return 0 - 1 } 95 let v: *i64 = sk_vert(base, h[1]) 96 let po: i64 = 2 * sk_ninf(base) 97 var k: i64 = 0 98 while k < po { v[k] = 0; k = k + 1 } 99 v[0] = b0; v[1] = w0 100 if sk_ninf(base) > 1 { v[2] = b1; v[3] = w1 } 101 v[po] = lx; v[po + 1] = ly; v[po + 2] = lz 102 h[1] = h[1] + 1 103 return h[1] - 1 104} 105// N-influence add: bs/ws are parallel arrays of length n (n <= the declared ninf; the remainder is 106// zero-weighted and therefore contributes nothing to the blend). 107func sk_add_vert_n(base: i64, bs: *i64, ws: *i64, n: i64, lx: i64, ly: i64, lz: i64) -> i64 { 108 let h: *i64 = sk_hdr(base) 109 if h[1] >= sk_capv(base) { return 0 - 1 } 110 let ni: i64 = sk_ninf(base) 111 if n > ni { return 0 - 2 } 112 let v: *i64 = sk_vert(base, h[1]) 113 let po: i64 = 2 * ni 114 var k: i64 = 0 115 while k < po { v[k] = 0; k = k + 1 } 116 var j: i64 = 0 117 while j < n { v[j * 2] = bs[j]; v[j * 2 + 1] = ws[j]; j = j + 1 } 118 v[po] = lx; v[po + 1] = ly; v[po + 2] = lz 119 h[1] = h[1] + 1 120 return h[1] - 1 121} 122 123// local rotation R = Ry(yaw)*Rz(elev), integer fx256 (trig fx4096 -> /16 to fx256). 124// ⚠ELEVATION IS Rz, NOT Rx: bones point along their +x offset, and Rx leaves the x-axis INVARIANT -- a 125// pitch(Rx) bone rotation could never raise a +x bone (caught during IK design). Ry*Rz covers the sphere: 126// +x -> (cy*ce, se, -sy*ce). Yaw-only poses are IDENTICAL to the old basis (Rz(0)=I) -- prior gates unaffected. 127func sk_localrot(yaw: i64, elev: i64, out: *i64) -> i64 { 128 let cy: i64 = it_cos4096(yaw) / 16 129 let sy: i64 = it_sin4096(yaw) / 16 130 let ce: i64 = it_cos4096(elev) / 16 131 let se: i64 = it_sin4096(elev) / 16 132 out[0] = cy * ce / 256 133 out[1] = 0 - cy * se / 256 134 out[2] = sy 135 out[3] = se 136 out[4] = ce 137 out[5] = 0 138 out[6] = 0 - sy * ce / 256 139 out[7] = sy * se / 256 140 out[8] = cy 141 return 0 142} 143func sk_matmul(a: *i64, b: *i64, out: *i64) -> i64 { 144 var r: i64 = 0 145 while r < 3 { 146 var c: i64 = 0 147 while c < 3 { 148 out[r * 3 + c] = (a[r * 3] * b[c] + a[r * 3 + 1] * b[3 + c] + a[r * 3 + 2] * b[6 + c]) / 256 149 c = c + 1 150 } 151 r = r + 1 152 } 153 return 0 154} 155func sk_matvec(m: *i64, x: i64, y: i64, z: i64, out: *i64) -> i64 { 156 out[0] = (m[0] * x + m[1] * y + m[2] * z) / 256 157 out[1] = (m[3] * x + m[4] * y + m[5] * z) / 256 158 out[2] = (m[6] * x + m[7] * y + m[8] * z) / 256 159 return 0 160} 161 162// compose world transforms parent-first (bones MUST be added parent-before-child; enforced by the add order) 163func sk_update(base: i64) -> i64 { 164 let h: *i64 = sk_hdr(base) 165 let scratch: *i64 = sk_vs(base) 166 let lr: *i64 = sk_lr(base) 167 var i: i64 = 0 168 while i < h[0] { 169 let b: *i64 = sk_bone(base, i) 170 sk_localrot(b[4], b[5], lr) 171 if b[0] < 0 { 172 var k: i64 = 0 173 while k < 9 { b[6 + k] = lr[k]; k = k + 1 } 174 b[15] = b[1]; b[16] = b[2]; b[17] = b[3] 175 } else { 176 let p: *i64 = sk_bone(base, b[0]) 177 sk_matmul((p as i64 + 48) as *i64, lr, (b as i64 + 48) as *i64) 178 sk_matvec((p as i64 + 48) as *i64, b[1], b[2], b[3], scratch) 179 b[15] = p[15] + scratch[0] 180 b[16] = p[16] + scratch[1] 181 b[17] = p[17] + scratch[2] 182 } 183 b[18] = 1 184 i = i + 1 185 } 186 return 0 187} 188 189// LBS: out[i] = sum_b w_b * (worldR_b * vlocal + worldpos_b), over the DECLARED influence count. 190func sk_skin(base: i64) -> i64 { 191 let h: *i64 = sk_hdr(base) 192 let tmp: *i64 = sk_vs(base) 193 let ni: i64 = sk_ninf(base) 194 let po: i64 = 2 * ni 195 var i: i64 = 0 196 while i < h[1] { 197 let v: *i64 = sk_vert(base, i) 198 let o: *i64 = sk_out(base, i) 199 o[0] = 0; o[1] = 0; o[2] = 0 200 var k: i64 = 0 201 while k < ni { 202 let bi: i64 = v[k * 2] 203 let w: i64 = v[k * 2 + 1] 204 if w > 0 { 205 let b: *i64 = sk_bone(base, bi) 206 sk_matvec((b as i64 + 48) as *i64, v[po], v[po + 1], v[po + 2], tmp) 207 o[0] = o[0] + w * (tmp[0] + b[15]) / 256 208 o[1] = o[1] + w * (tmp[1] + b[16]) / 256 209 o[2] = o[2] + w * (tmp[2] + b[17]) / 256 210 } 211 k = k + 1 212 } 213 i = i + 1 214 } 215 return 0 216}