nx_quadruped.nx source
↩ module page · 298 lines · 17398 B
1// nx_quadruped.nx -- QUADRUPED rig + walk gait + body-fill: the bestiary's ANIMALS move too (operator thesis:
2// reuse synthetic assets "from animals to people to monsters" + give them motion). An 18-joint quadruped rig on
3// the SAME nx_skeleton FK substrate (root barrel -> chest/head + hips/tail + 4 three-bone legs), a DIAGONAL
4// WALK gait (FL+HR swing in phase, FR+HL antiphase -- the real quadruped walk coordination; legs point -y so a
5// pitch(Rz) swing moves the foot along the body axis x), and quadpose_fill = the wolf body as bone-anchored
6// ellipsoids + sphere-chain legs/tail on the CURRENT pose, rendering on the production shader like everything
7// else. Gait parameters are data (amplitudes/period per call). license_tier: ORIGINAL
8import "nx_syscalls.nx"
9import "nx_bodypose.nx" // bp_part/bp_chain/bp_ball + skeleton/sdfrender/figure_render transitively
10import "nx_itrig.nx"
11const QR_MAGIC_12868: i64 = 12868
12const QR_MAGIC_25736: i64 = 25736
13// ONE GAIT CYCLE in phase units. Split out from the old QR_MAGIC_4096, which was doing double duty as
14// both this AND the trig output scale below -- numerically equal, conceptually unrelated, and therefore
15// impossible to change independently. Named for purpose, as the ratchet's own refusal text demands.
16const QR_PHASE_FULL: i64 = 4096
17// Output scale of it_sin4096/it_cos4096. NOT a tunable: it is the unit those functions return in.
18const QR_TRIG_Q12: i64 = 4096
19// Quarter of a cycle. A four-beat gait places its feet at successive quarters, so this is the step,
20// not an arbitrary offset -- which is why the walk offsets below are multiples of it rather than
21// literals. QR_MAGIC_2048 (a half cycle) survives only where a two-beat gait genuinely means one.
22const QR_PHASE_QUARTER: i64 = 1024
23// A quarter turn in the angle units `a` is measured in (QR_MAGIC_25736 is a full turn). Its purpose
24// was already written in the tail-sway comment below -- "quarter offset" -- and never in its name.
25const QR_ANGLE_QUARTER: i64 = 6434
26const QR_MAGIC_2048: i64 = 2048
27const QR_MAGIC_2300: i64 = 2300
28const QR_MAGIC_2900: i64 = 2900
29
30const QR_ROOT: i64 = 0
31const QR_CHEST: i64 = 1
32const QR_HEAD: i64 = 2
33const QR_HIPS: i64 = 3
34const QR_FLHIP: i64 = 4
35const QR_FLKNEE: i64 = 5
36const QR_FLFOOT: i64 = 6
37const QR_FRHIP: i64 = 7
38const QR_FRKNEE: i64 = 8
39const QR_FRFOOT: i64 = 9
40const QR_HLHIP: i64 = 10
41const QR_HLKNEE: i64 = 11
42const QR_HLFOOT: i64 = 12
43const QR_HRHIP: i64 = 13
44const QR_HRKNEE: i64 = 14
45const QR_HRFOOT: i64 = 15
46const QR_TAIL1: i64 = 16
47const QR_TAIL2: i64 = 17
48
49func qdeg(d: i64) -> i64 { return d * QR_MAGIC_12868 / 180 }
50
51// ===== THE DIAGONAL-PHASE RULER (extracted here 2026-08-25) ================================
52// WHAT A GAIT ACTUALLY IS. A quadruped gait is defined by WHICH LEGS MOVE TOGETHER, not by how far any
53// one leg reaches. A trot is 2-beat with the diagonal pairs in phase; a walk is a 4-beat lateral sequence
54// where they are not. Measured on this rig 2026-08-25: the front-right foot's peak swing reads walk=-146
55// trot=-156 -- TEN APART -- so a single-leg amplitude test cannot tell those two gaits apart at all, while
56// the diagonal relationship separates them by construction.
57// WHY IT LIVES IN THE RIG. TWO gates independently needed this exact question: nx_wolf_walk_gate T3 already
58// had it ("FL and FR displace in OPPOSITE x directions, and FL matches HR") and nx_quad_gaits_gate was
59// about to re-derive it as a private T5. That is one ruler with two owners, which is how they drift. The
60// rig owns its own gait semantics; a gate asks the rig rather than each re-deciding what a trot is.
61func quad_leg_swing(sk: i64, foot: i64, hip: i64) -> i64 {
62 let bf: *i64 = sk_bone(sk, foot)
63 let bh: *i64 = sk_bone(sk, hip)
64 return bf[15] - bh[15]
65}
66// Sign of the FR/HL diagonal coupling at the CURRENT pose. Call after quad_gait_pose_g + sk_update.
67// +1 the diagonal swings TOGETHER (trot-like) -1 OPPOSED (walk-like) 0 one leg is not swinging,
68// which is a real third answer and not a failure: at some phases a foot is planted and has no swing, so
69// collapsing it into either sign would invent a coupling the pose does not show.
70func quad_diag_phase(sk: i64) -> i64 {
71 let fr: i64 = quad_leg_swing(sk, QR_FRFOOT, QR_FRHIP)
72 let hl: i64 = quad_leg_swing(sk, QR_HLFOOT, QR_HLHIP)
73 let p: i64 = fr*hl
74 if p > 0 { return 1 }
75 if p < 0 { return 0 - 1 }
76 return 0
77}
78
79// Permil of a full gait cycle in which the front-right foot and its DIAGONAL partner, the hind-left,
80// are swinging in the SAME direction.
81//
82// WHY THIS EXISTS ALONGSIDE quad_diag_phase, WHICH IT DOES NOT REPLACE. quad_diag_phase answers about
83// ONE POSE, and a single pose CANNOT express a phase relationship: a walk and a trot both pass through
84// instants where the diagonal agrees, so sampling either at one moment returns +1 for both. Measured
85// 2026-08-25 -- nx_quad_gaits_gate T5 read trot=1 walk=1 and correctly went RED, because the RULER was
86// under-specified, not because the gaits were identical. What separates the two gaits is the FRACTION
87// of the cycle the diagonal spends coupled: a trot is a two-beat gait whose diagonals move as pairs,
88// while a four-beat walk lands each foot separately.
89//
90// This is the same shape as the level-versus-derivative law: an instantaneous reading cannot express a
91// relationship that is defined over time, and the fix is to integrate rather than to sample harder.
92// nsamp is the caller's, not a constant here -- the rig has no opinion on how finely a gait is walked.
93func quad_diag_sync_permil(sk: i64, gait: i64, nsamp: i64) -> i64 {
94 if nsamp <= 0 { return 0 - 1 }
95 var together: i64 = 0
96 var moving: i64 = 0
97 var i: i64 = 0
98 while i < nsamp {
99 quad_gait_pose_g(sk, i*QR_PHASE_FULL/nsamp, gait)
100 sk_update(sk)
101 let d: i64 = quad_diag_phase(sk)
102 if d != 0 {
103 moving = moving + 1
104 if d > 0 { together = together + 1 }
105 }
106 i = i + 1
107 }
108 // Samples where NEITHER leg is swinging carry no phase information at all, and counting them would
109 // dilute both gaits toward the same number. The denominator is the samples that could answer.
110 if moving == 0 { return 0 - 1 }
111 return together*1000/moving
112}
113
114// build the rest rig (parent-first). Head end is -x (matches av_base_wolf's layout).
115func quad_build(sk: i64) -> i64 {
116 sk_init(sk)
117 sk_add_bone(sk, 0 - 1, 0, 0 - 100, 0) // 0 root (barrel center)
118 sk_add_bone(sk, QR_ROOT, 0 - 560, 120, 0) // 1 chest/shoulders
119 sk_add_bone(sk, QR_CHEST, 0 - 380, 300, 0) // 2 head
120 sk_add_bone(sk, QR_ROOT, 560, 60, 0) // 3 hips/haunches
121 sk_add_bone(sk, QR_CHEST, 0, 0 - 240, 90) // 4 FL hip
122 sk_add_bone(sk, QR_FLHIP, 0, 0 - 320, 0) // 5 FL knee
123 sk_add_bone(sk, QR_FLKNEE, 0, 0 - 320, 0) // 6 FL foot
124 sk_add_bone(sk, QR_CHEST, 0, 0 - 240, 0 - 90) // 7 FR hip
125 sk_add_bone(sk, QR_FRHIP, 0, 0 - 320, 0) // 8 FR knee
126 sk_add_bone(sk, QR_FRKNEE, 0, 0 - 320, 0) // 9 FR foot
127 sk_add_bone(sk, QR_HIPS, 0, 0 - 180, 90) // 10 HL hip
128 sk_add_bone(sk, QR_HLHIP, 0, 0 - 320, 0) // 11 HL knee
129 sk_add_bone(sk, QR_HLKNEE, 0, 0 - 320, 0) // 12 HL foot
130 sk_add_bone(sk, QR_HIPS, 0, 0 - 180, 0 - 90) // 13 HR hip
131 sk_add_bone(sk, QR_HRHIP, 0, 0 - 320, 0) // 14 HR knee
132 sk_add_bone(sk, QR_HRKNEE, 0, 0 - 320, 0) // 15 HR foot
133 sk_add_bone(sk, QR_HIPS, 340, 240, 0) // 16 tail base
134 sk_add_bone(sk, QR_TAIL1, 160, 160, 0) // 17 tail tip
135 return 0
136}
137
138// pose the WALK at phase t4096 in [0,4096) (one full stride). Diagonal pairs: FL+HR in phase, FR+HL antiphase.
139// swing_deg = leg swing amplitude (deg); call sk_update after.
140func quad_gait_pose(sk: i64, t4096: i64, swing_deg: i64) -> i64 {
141 let a: i64 = t4096 * QR_MAGIC_25736 / QR_PHASE_FULL // phase angle (2*IT_PI = QR_MAGIC_25736)
142 let s: i64 = it_sin4096(a) // -QR_MAGIC_4096..QR_MAGIC_4096
143 let amp: i64 = qdeg(swing_deg)
144 // FOUR-BEAT LATERAL WALK. Until 2026-08-25 this function drove FL+HR at +swing and FR+HL at
145 // -swing, which is a TWO-BEAT DIAGONAL gait -- a TROT. The organ that consumes it is called
146 // nx_wolf_walk_gate and it writes synth_wolfwalk.png, so the estate was rendering a trotting wolf,
147 // labelling it a walk, and its gate was ENFORCING the trot as correct.
148 // THE FOOTFALL PATTERN IS WHAT NAMES A GAIT, so this is a definitional correction and not a tuning
149 // choice: a lateral-sequence walk lands hind-left, fore-left, hind-right, fore-right at successive
150 // quarter cycles, giving four DISTINCT leg phases instead of two coincident pairs.
151 // ⚠SCOPE: phase pattern only. Stride FREQUENCY remains uncited (softbody rung SB8) and this does
152 // not close it.
153 let swing: i64 = amp * s / QR_TRIG_Q12
154 let swingHR: i64 = amp * it_sin4096(a + QR_ANGLE_QUARTER) / QR_TRIG_Q12
155 let swingFR: i64 = amp * it_sin4096(a + QR_ANGLE_QUARTER*2) / QR_TRIG_Q12
156 let swingHL: i64 = amp * it_sin4096(a + QR_ANGLE_QUARTER*3) / QR_TRIG_Q12
157 var kmag: i64 = swing
158 if kmag < 0 { kmag = 0 - kmag }
159 let kb: i64 = qdeg(9) + kmag / 2 // knees bend more mid-swing
160 // Each leg's knee follows ITS OWN swing -- sharing one knee bend across four independently phased
161 // legs would put every knee at the same bend while the feet are at four different points of the
162 // stride, which is the visible tell of a fake four-beat gait.
163 var kHR: i64 = swingHR
164 if kHR < 0 { kHR = 0 - kHR }
165 var kFR: i64 = swingFR
166 if kFR < 0 { kFR = 0 - kFR }
167 var kHL: i64 = swingHL
168 if kHL < 0 { kHL = 0 - kHL }
169 let s2: i64 = it_sin4096(a * 2)
170 let bob: i64 = qdeg(3) * s2 / QR_TRIG_Q12 // body/head double-frequency bob
171 let tsw: i64 = qdeg(14) * it_sin4096(a + QR_ANGLE_QUARTER) / QR_TRIG_Q12 // tail sway, quarter offset
172 sk_pose(sk, QR_ROOT, 0, bob)
173 sk_pose(sk, QR_HEAD, 0, 0 - bob)
174 sk_pose(sk, QR_FLHIP, 0, swing)
175 sk_pose(sk, QR_FLKNEE, 0, kb)
176 sk_pose(sk, QR_HRHIP, 0, swingHR)
177 sk_pose(sk, QR_HRKNEE, 0, qdeg(9) + kHR / 2)
178 sk_pose(sk, QR_FRHIP, 0, swingFR)
179 sk_pose(sk, QR_FRKNEE, 0, qdeg(9) + kFR / 2)
180 sk_pose(sk, QR_HLHIP, 0, swingHL)
181 sk_pose(sk, QR_HLKNEE, 0, qdeg(9) + kHL / 2)
182 sk_pose(sk, QR_TAIL1, tsw, qdeg(10))
183 sk_pose(sk, QR_TAIL2, tsw, qdeg(8))
184 return 0
185}
186
187// one leg's hip swing at phase a with a per-leg cycle offset (off in [0,4096) = fraction of the stride).
188func qleg(a: i64, ampfx: i64, off: i64) -> i64 { let p: i64 = a + off * QR_MAGIC_25736 / QR_TRIG_Q12; return ampfx * it_sin4096(p) / QR_TRIG_Q12 }
189func qabs(v: i64) -> i64 { if v < 0 { return 0 - v } return v }
190// GAIT-AS-DATA: pose at phase t4096 for gait 0=walk (4-beat diagonal), 1=trot (2-beat diagonal, suspension
191// bounce), 2=gallop (rotary, asymmetric leg phases + big spine gather/extend). Amplitudes + per-leg phase
192// offsets are the gait's data. call sk_update after.
193func quad_gait_pose_g(sk: i64, t4096: i64, gait: i64) -> i64 {
194 let a: i64 = t4096 * QR_MAGIC_25736 / QR_PHASE_FULL
195 var swing: i64 = qdeg(24)
196 // WALK IS FOUR-BEAT LATERAL, TROT IS TWO-BEAT DIAGONAL, AND UNTIL 2026-08-25 THIS FUNCTION GAVE
197 // THEM IDENTICAL FOOT PHASING. gait 1 changed only swing amplitude, bob, pitch and knee base, so
198 // the "walk" was a small-amplitude trot and the two were the same gait wearing two names. Found by
199 // nx_quad_gaits_gate T5, which measured the diagonal coupling of both as +1 and correctly went RED.
200 //
201 // This is a DEFINITIONAL correction, not a tuned one: a two-beat diagonal pattern IS a trot, so a
202 // walk that runs it is mislabelled by its own footfall order. The lateral-sequence walk lands
203 // hind-left, fore-left, hind-right, fore-right at successive quarter cycles; expressed relative to
204 // the fore-left that gives FR a half cycle, HL three quarters and HR one quarter.
205 // ⚠SCOPE: this fixes the PHASE PATTERN, which is definitional. It says nothing about stride
206 // FREQUENCY, which is empirical and still has no cited band in gamefeel_oracle.conf -- that gap is
207 // softbody rung SB8 and is NOT closed by this change.
208 var offFR: i64 = QR_PHASE_QUARTER * 2
209 var offHL: i64 = QR_PHASE_QUARTER * 3
210 var offHR: i64 = QR_PHASE_QUARTER
211 var bobA: i64 = qdeg(3); var pitchA: i64 = 0; var kbase: i64 = qdeg(9)
212 // TROT: two-beat, DIAGONAL PAIRS TOGETHER. The offsets are the point of the gait, not the bounce.
213 if gait == 1 {
214 offFR = QR_PHASE_QUARTER * 2
215 offHL = QR_PHASE_QUARTER * 2
216 offHR = 0
217 swing = qdeg(31); bobA = qdeg(7); pitchA = qdeg(3); kbase = qdeg(14)
218 }
219 if gait == 2 { swing = qdeg(36); offFR = 600; offHL = QR_MAGIC_2300; offHR = QR_MAGIC_2900; bobA = qdeg(4); pitchA = qdeg(11); kbase = qdeg(16) } // gallop: rotary + spine flex
220 let sFL: i64 = qleg(a, swing, 0)
221 let sFR: i64 = qleg(a, swing, offFR)
222 let sHL: i64 = qleg(a, swing, offHL)
223 let sHR: i64 = qleg(a, swing, offHR)
224 let s2: i64 = it_sin4096(a * 2)
225 let bob: i64 = bobA * s2 / QR_TRIG_Q12
226 let pitch: i64 = pitchA * it_sin4096(a) / QR_TRIG_Q12 // 1x-freq gather/extend (gallop)
227 let tsw: i64 = qdeg(14) * it_sin4096(a + QR_ANGLE_QUARTER) / QR_TRIG_Q12
228 sk_pose(sk, QR_ROOT, 0, bob + pitch)
229 sk_pose(sk, QR_CHEST, 0, 0 - pitch / 2) // spine flexes on gallop
230 sk_pose(sk, QR_HEAD, 0, 0 - bob)
231 sk_pose(sk, QR_HIPS, 0, pitch / 2)
232 sk_pose(sk, QR_FLHIP, 0, sFL); sk_pose(sk, QR_FLKNEE, 0, kbase + qabs(sFL) / 2)
233 sk_pose(sk, QR_FRHIP, 0, sFR); sk_pose(sk, QR_FRKNEE, 0, kbase + qabs(sFR) / 2)
234 sk_pose(sk, QR_HLHIP, 0, sHL); sk_pose(sk, QR_HLKNEE, 0, kbase + qabs(sHL) / 2)
235 sk_pose(sk, QR_HRHIP, 0, sHR); sk_pose(sk, QR_HRKNEE, 0, kbase + qabs(sHR) / 2)
236 sk_pose(sk, QR_TAIL1, tsw, qdeg(10)); sk_pose(sk, QR_TAIL2, tsw, qdeg(8))
237 return 0
238}
239// add HORNS to the current quadruped body (call after quadpose_fill; n = its returned part count). Returns new n.
240func quad_add_horns(base: i64, n0: i64, sk: i64) -> i64 {
241 let orr: *i64 = sys_mmap(6 * 8) as *i64
242 var n: i64 = n0
243 orr[0]=40; orr[1]=190; orr[2]=95; orr[3]=55; orr[4]=150; orr[5]=48
244 bp_anchor(base, n, sk, QR_HEAD, orr); n = n + 1 // horn L (up+forward off the head)
245 orr[0]=40; orr[1]=190; orr[2]=0-95; orr[3]=55; orr[4]=150; orr[5]=48
246 bp_anchor(base, n, sk, QR_HEAD, orr); n = n + 1 // horn R
247 let np: *i64 = (base + O_NPART) as *i64; np[0] = n
248 sdf_clear_ops(base, n)
249 return n
250}
251
252// fill the sdfrender base with the wolf body ON the current pose. Returns part count.
253func quadpose_fill(base: i64, sk: i64) -> i64 {
254 let orr: *i64 = sys_mmap(6 * 8) as *i64
255 let o4: *i64 = sys_mmap(4 * 8) as *i64
256 var n: i64 = 0
257 // torso/head ellipsoids anchored to bones
258 orr[0]=0; orr[1]=0; orr[2]=0; orr[3]=780; orr[4]=300; orr[5]=280
259 bp_anchor(base, n, sk, QR_ROOT, orr); n = n + 1 // barrel
260 orr[0]=0; orr[1]=0; orr[2]=0; orr[3]=330; orr[4]=330; orr[5]=300
261 bp_anchor(base, n, sk, QR_CHEST, orr); n = n + 1 // chest
262 orr[0]=0; orr[1]=0; orr[2]=0; orr[3]=230; orr[4]=220; orr[5]=210
263 bp_anchor(base, n, sk, QR_HEAD, orr); n = n + 1 // head
264 orr[0]=0-260; orr[1]=0-40; orr[2]=0; orr[3]=190; orr[4]=120; orr[5]=130
265 bp_anchor(base, n, sk, QR_HEAD, orr); n = n + 1 // snout (forward -x)
266 orr[0]=0; orr[1]=0; orr[2]=0; orr[3]=300; orr[4]=270; orr[5]=260
267 bp_anchor(base, n, sk, QR_HIPS, orr); n = n + 1 // haunches
268 orr[0]=60; orr[1]=230; orr[2]=110; orr[3]=65; orr[4]=170; orr[5]=50
269 bp_anchor(base, n, sk, QR_HEAD, orr); n = n + 1 // ear L
270 orr[0]=60; orr[1]=230; orr[2]=0-110; orr[3]=65; orr[4]=170; orr[5]=50
271 bp_anchor(base, n, sk, QR_HEAD, orr); n = n + 1 // ear R
272 // legs: sphere-chains along the POSED segments (upper + lower + foot ball)
273 n = bp_chain(base, n, sk, QR_FLHIP, QR_FLKNEE, 105, 92)
274 n = bp_chain(base, n, sk, QR_FLKNEE, QR_FLFOOT, 90, 74)
275 o4[0]=0; o4[1]=0-40; o4[2]=0; o4[3]=88
276 n = bp_ball(base, n, sk, QR_FLFOOT, o4)
277 n = bp_chain(base, n, sk, QR_FRHIP, QR_FRKNEE, 105, 92)
278 n = bp_chain(base, n, sk, QR_FRKNEE, QR_FRFOOT, 90, 74)
279 o4[0]=0; o4[1]=0-40; o4[2]=0; o4[3]=88
280 n = bp_ball(base, n, sk, QR_FRFOOT, o4)
281 n = bp_chain(base, n, sk, QR_HLHIP, QR_HLKNEE, 110, 95)
282 n = bp_chain(base, n, sk, QR_HLKNEE, QR_HLFOOT, 92, 76)
283 o4[0]=0; o4[1]=0-40; o4[2]=0; o4[3]=90
284 n = bp_ball(base, n, sk, QR_HLFOOT, o4)
285 n = bp_chain(base, n, sk, QR_HRHIP, QR_HRKNEE, 110, 95)
286 n = bp_chain(base, n, sk, QR_HRKNEE, QR_HRFOOT, 92, 76)
287 o4[0]=0; o4[1]=0-40; o4[2]=0; o4[3]=90
288 n = bp_ball(base, n, sk, QR_HRFOOT, o4)
289 // tail: chain hips->tail1->tail2 + tip
290 n = bp_chain(base, n, sk, QR_HIPS, QR_TAIL1, 95, 72)
291 n = bp_chain(base, n, sk, QR_TAIL1, QR_TAIL2, 70, 56)
292 o4[0]=0; o4[1]=0; o4[2]=0; o4[3]=54
293 n = bp_ball(base, n, sk, QR_TAIL2, o4)
294 let np: *i64 = (base + O_NPART) as *i64; np[0] = n
295 let kb2: *i64 = (base + O_KBLEND) as *i64; kb2[0] = 130
296 sdf_clear_ops(base, n)
297 return n
298}