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}