code wiki / (root) / nx_quadruped.nx

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}