nx_skeleton.nx source
↩ module page · 154 lines · 6111 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 with 2 influences, weights fx256 summing 256).
6// ALL INTEGER => deterministic, VM-vettable, wasm-ready (base-relative). license_tier: ORIGINAL
7import "nx_syscalls.nx"
8import "nx_itrig.nx"
9
10// bone record: 32 slots at 64+i*256
11// [0]=parent(-1 root) [1..3]=local offset fx256 [4]=yaw4096 [5]=pitch4096
12// [6..14]=world rotation 3x3 fx256 (row-major) [15..17]=world position fx256 [18]=valid
13const SK_MAXB: i64 = 32
14// skin vertex record: 8 slots: [0]=bone0 [1]=w0 [2]=bone1 [3]=w1 [4..6]=bone-local pos (in bone0/bone1 local
15// frames -- v1 authors verts in EACH bone's local frame identically: local-of-bone = offset from that bone)
16// (v1 simplification: ONE local pos used for both bones -- valid when bind poses align, as in tube limbs)
17const SK_MAXV: i64 = 2048
18
19// header 256B (32 slots): [0]=nbones [1]=nverts [2..10]=rot scratch [11..13]=vec scratch (in-arena scratch =
20// wasm-ready: sys_mmap does not exist inside the wasm; base-relative organs use ONLY the caller's arena)
21func sk_hdr(base: i64) -> *i64 { return base as *i64 }
22func sk_lr(base: i64) -> *i64 { return (base + 16) as *i64 }
23func sk_vs(base: i64) -> *i64 { return (base + 88) as *i64 }
24func sk_bone(base: i64, i: i64) -> *i64 { return (base + 256 + i * 256) as *i64 }
25func sk_vert(base: i64, i: i64) -> *i64 { return (base + 256 + SK_MAXB * 256 + i * 64) as *i64 }
26func sk_out(base: i64, i: i64) -> *i64 { return (base + 256 + SK_MAXB * 256 + SK_MAXV * 64 + i * 24) as *i64 }
27func sk_bytes() -> i64 { return 256 + SK_MAXB * 256 + SK_MAXV * 64 + SK_MAXV * 24 + 64 }
28
29func sk_init(base: i64) -> i64 {
30 let h: *i64 = sk_hdr(base)
31 h[0] = 0
32 h[1] = 0
33 return 0
34}
35func sk_add_bone(base: i64, parent: i64, ox: i64, oy: i64, oz: i64) -> i64 {
36 let h: *i64 = sk_hdr(base)
37 if h[0] >= SK_MAXB { return 0 - 1 }
38 let b: *i64 = sk_bone(base, h[0])
39 b[0] = parent
40 b[1] = ox; b[2] = oy; b[3] = oz
41 b[4] = 0; b[5] = 0
42 b[18] = 0
43 h[0] = h[0] + 1
44 return h[0] - 1
45}
46func sk_pose(base: i64, bone: i64, yaw: i64, pitch: i64) -> i64 {
47 let b: *i64 = sk_bone(base, bone)
48 b[4] = yaw
49 b[5] = pitch
50 return 0
51}
52func sk_add_vert(base: i64, b0: i64, w0: i64, b1: i64, w1: i64, lx: i64, ly: i64, lz: i64) -> i64 {
53 let h: *i64 = sk_hdr(base)
54 if h[1] >= SK_MAXV { return 0 - 1 }
55 let v: *i64 = sk_vert(base, h[1])
56 v[0] = b0; v[1] = w0; v[2] = b1; v[3] = w1
57 v[4] = lx; v[5] = ly; v[6] = lz
58 h[1] = h[1] + 1
59 return h[1] - 1
60}
61
62// local rotation R = Ry(yaw)*Rz(elev), integer fx256 (trig fx4096 -> /16 to fx256).
63// ⚠ELEVATION IS Rz, NOT Rx: bones point along their +x offset, and Rx leaves the x-axis INVARIANT -- a
64// pitch(Rx) bone rotation could never raise a +x bone (caught during IK design). Ry*Rz covers the sphere:
65// +x -> (cy*ce, se, -sy*ce). Yaw-only poses are IDENTICAL to the old basis (Rz(0)=I) -- prior gates unaffected.
66func sk_localrot(yaw: i64, elev: i64, out: *i64) -> i64 {
67 let cy: i64 = it_cos4096(yaw) / 16
68 let sy: i64 = it_sin4096(yaw) / 16
69 let ce: i64 = it_cos4096(elev) / 16
70 let se: i64 = it_sin4096(elev) / 16
71 out[0] = cy * ce / 256
72 out[1] = 0 - cy * se / 256
73 out[2] = sy
74 out[3] = se
75 out[4] = ce
76 out[5] = 0
77 out[6] = 0 - sy * ce / 256
78 out[7] = sy * se / 256
79 out[8] = cy
80 return 0
81}
82func sk_matmul(a: *i64, b: *i64, out: *i64) -> i64 {
83 var r: i64 = 0
84 while r < 3 {
85 var c: i64 = 0
86 while c < 3 {
87 out[r * 3 + c] = (a[r * 3] * b[c] + a[r * 3 + 1] * b[3 + c] + a[r * 3 + 2] * b[6 + c]) / 256
88 c = c + 1
89 }
90 r = r + 1
91 }
92 return 0
93}
94func sk_matvec(m: *i64, x: i64, y: i64, z: i64, out: *i64) -> i64 {
95 out[0] = (m[0] * x + m[1] * y + m[2] * z) / 256
96 out[1] = (m[3] * x + m[4] * y + m[5] * z) / 256
97 out[2] = (m[6] * x + m[7] * y + m[8] * z) / 256
98 return 0
99}
100
101// compose world transforms parent-first (bones MUST be added parent-before-child; enforced by the add order)
102func sk_update(base: i64) -> i64 {
103 let h: *i64 = sk_hdr(base)
104 let scratch: *i64 = sk_vs(base)
105 let lr: *i64 = sk_lr(base)
106 var i: i64 = 0
107 while i < h[0] {
108 let b: *i64 = sk_bone(base, i)
109 sk_localrot(b[4], b[5], lr)
110 if b[0] < 0 {
111 var k: i64 = 0
112 while k < 9 { b[6 + k] = lr[k]; k = k + 1 }
113 b[15] = b[1]; b[16] = b[2]; b[17] = b[3]
114 } else {
115 let p: *i64 = sk_bone(base, b[0])
116 sk_matmul((p as i64 + 48) as *i64, lr, (b as i64 + 48) as *i64)
117 sk_matvec((p as i64 + 48) as *i64, b[1], b[2], b[3], scratch)
118 b[15] = p[15] + scratch[0]
119 b[16] = p[16] + scratch[1]
120 b[17] = p[17] + scratch[2]
121 }
122 b[18] = 1
123 i = i + 1
124 }
125 return 0
126}
127
128// LBS: out[i] = sum_b w_b * (worldR_b * vlocal + worldpos_b)
129func sk_skin(base: i64) -> i64 {
130 let h: *i64 = sk_hdr(base)
131 let tmp: *i64 = sk_vs(base)
132 var i: i64 = 0
133 while i < h[1] {
134 let v: *i64 = sk_vert(base, i)
135 let o: *i64 = sk_out(base, i)
136 o[0] = 0; o[1] = 0; o[2] = 0
137 var k: i64 = 0
138 while k < 2 {
139 var bi: i64 = v[0]
140 var w: i64 = v[1]
141 if k == 1 { bi = v[2]; w = v[3] }
142 if w > 0 {
143 let b: *i64 = sk_bone(base, bi)
144 sk_matvec((b as i64 + 48) as *i64, v[4], v[5], v[6], tmp)
145 o[0] = o[0] + w * (tmp[0] + b[15]) / 256
146 o[1] = o[1] + w * (tmp[1] + b[16]) / 256
147 o[2] = o[2] + w * (tmp[2] + b[17]) / 256
148 }
149 k = k + 1
150 }
151 i = i + 1
152 }
153 return 0
154}