code wiki / (root) / nx_softbind.nx

nx_softbind.nx source

↩ module page · 285 lines · 18187 B

1// nx_softbind.nx -- LIB: BINDS nx_softdyn to character geometry (roadmap rung A4, 2026-08-01). 2// The solver was proven 7/7 in isolation but bound to NOTHING -- no body part consumed it, so 3// nothing in the game visibly jiggled. This lib is the missing joint-anchor contract: each mob 4// owns SB_PTS spring points (hair / chest / skirt class), each seated at a fixed local offset 5// from the mob ROOT, stepped once per sim tick from root motion ONLY (organ truth -- the same 6// source as the mobdx/mobdz/mobdy doors), yielding signed Q8 offsets a renderer ADDS to its 7// designated joints on top of the animated pose (spring-decomposed-skinning pattern: helper 8// offsets over the animation, never replacing it). Pure function of the root trajectory => 9// bit-deterministic, so replay / netsync / save-transparency hold by construction. 10// license_tier: ORIGINAL No hw writes (Rule 26). 11import "nx_syscalls.nx" 12import "nx_softdyn.nx" 13const SB_MAGIC_5381: i64 = 5381 14const SB_MAGIC_1000000007: i64 = 1000000007 15 16const SB_PTS: i64 = 6 17const SB_HAIR: i64 = 0 18const SB_CHEST: i64 = 1 19const SB_SKIRT: i64 = 2 20// R7 regions (2026-08-26): the asset now DECLARES these bones (nx_nxa_dyna bones=6); the engine 21// ticks them on the same solver. fn: belly/glutes at the tissue band's LOW edge (softer mass), 22// thigh at midband -- all from the pinned conf rows, never tasted. 23const SB_BELLY: i64 = 3 24const SB_GLUTE: i64 = 4 25const SB_THIGH: i64 = 5 26// per-kind spring params (per-1024). ALL underdamped -- discrete envelope: underdamped while 27// C < 64*sqrt(K) (zeta = C / (2*sqrt(1024*K))), so every row below VISIBLY oscillates: 28// hair K=200 -> bound ~905, C=60 29// skirt K=300 -> bound ~1108, C=80 30// CHEST IS NOT A ROW HERE. It is derived from the oracle bands below, because it is the only one 31// of the three with published human free-vibration, damping, asymmetry and anisotropy to derive 32// FROM. hair and skirt keep their cited rows (hair_k_q10 120..420) until the same exists for them. 33// ===== HAIR: THE SAME TWO INTEGERS, NOW SAYING WHAT THEY MEAN (2026-08-25) ===== 34// SB_K_HAIR and SB_C_HAIR were hand-picked: two per-1024 integers sitting inside hair_k_q10's 35// banked band by inspection, never derived from any frequency, and driving a SINGLE point. They 36// are UNCHANGED here -- 200 and 60, byte-for-byte the same plant -- because there is no citable 37// hair band to move them toward: knowledge/gamefeel_oracle.conf's hair_k_q10 source slug 38// "secondary-motion-hair-2to5hz" resolves to NOTHING (proven with complete coverage; the 39// correction record sits beside that row), and swapping one uncited number for another would be 40// the same defect wearing a newer date. 41// WHAT CHANGES IS THAT THE PLANT CAN NOW BE READ. The two rows below express the SAME plant in 42// physical units -- a natural frequency and a damping ratio -- so that a future citation has 43// something to bind to, and so a reader can see what the game actually does instead of two opaque 44// integers. They are EXACT, not approximate: nx_hairdyn_gate asserts sb_k_sym(SB_HAIR_FN_MHZ) == 45// SB_K_HAIR and sb_c_for_zeta(SB_K_HAIR, SB_HAIR_ZETA_PERMIL) == SB_C_HAIR, so if anyone edits 46// either side the gate goes RED instead of the two drifting apart in silence. 47// MEASURED CONSEQUENCE, and note the ratio below is the REQUESTED one, not the DELIVERED one. 48// nx_hairdyn_gate ran the ringdown on this exact plant and read back fn 4285 mhz, zeta 22 permil 49// -- a THIRD of the 67 the coefficient implies. That is not an error in either number: sd_step's 50// damping term is (v*C)/SD_G and it TRUNCATES, so at C=60 no damping is applied at all until |v| 51// reaches 17 q8 per tick, and the shortfall grows as the swing decays into that dead zone. In this 52// solver a requested zeta is an UPPER BOUND on the delivered one, never an equality. 53// So shipped hair has a quality factor of about 23 and rings for tens of periods -- far longer 54// than the chest, whose MEASURED ratio is 480 permil. Whether that is right for hair is exactly 55// the question no citation in this estate can currently answer. 56const SB_HAIR_FN_MHZ: i64 = 4221 // == sb_k_sym -> SB_K_HAIR exactly (gate-pinned) 57const SB_HAIR_ZETA_PERMIL: i64 = 67 // == sb_c_for_zeta -> SB_C_HAIR exactly (gate-pinned) 58const SB_K_HAIR: i64 = 200 59const SB_C_HAIR: i64 = 60 60const SB_K_SKIRT: i64 = 300 61const SB_C_SKIRT: i64 = 80 62 63// ===== CHEST TISSUE: DERIVED, NEVER TUNED ================================================== 64// The four hand-picked rows that used to live here (K 60/85/110/135, C 100..148) were chosen to 65// hit a self-declared "2.3-3.4Hz, zeta 0.17-0.20" band. MEASUREMENT retired them on two counts, 66// and both are recorded because the second one is the defect nobody could see: 67// 1. OUT OF BAND. knowledge/gamefeel_oracle.conf tissue_fn_mhz is 4000..5200 (free-vibration 68// modes) and tissue_zeta_permil is 350..600 (haake-scurr-2010, zeta 0.475 +- 0.062). The 69// tuned plant sat BELOW both -- roughly half the cited damping. 70// 2. TUNED INTO ITS OWN DRIVE. tissue_drive_mhz records our own beach corpus at 2000..3100 71// mhz (n=5). The tuned plant's 2310..3470 mhz sits INSIDE that, so the tissue was driven AT 72// RESONANCE with quality factor Q = 1/(2*zeta) ~ 2.5. A plant tuned onto its own drive 73// cannot read as tissue: it wobbles at walking frequency with amplified, slow-decaying 74// gain instead of taking one footfall thump and dying. Real tissue at 4000..5200 mhz is 75// driven well BELOW resonance, which is what makes it look like flesh and not like jelly. 76// AUTHORITY FOR THE NUMBERS BELOW. The conf forbids comparing a solver tunable to a subject row 77// until the ringdown probe reports ("UNTIL THAT PROBE REPORTS, NO TUNABLE MAY BE COMPARED TO A 78// SUBJECT ROW"). nx_dyna_bind_skin ringdown was built and run for the first time on 2026-08-25 79// and our XPBD plant measured fn=4137 mhz zeta=480 permil at the linear limit -- INSIDE both 80// human bands. So the reduced-order spring below is identified FROM a reference solver that is 81// itself validated against the literature, which is the whole reason it may carry these numbers. 82const SB_FN_LO_MHZ: i64 = 4000 // conf tissue_fn_mhz lo 83const SB_FN_HI_MHZ: i64 = 5200 // conf tissue_fn_mhz hi 84const SB_ZETA_PERMIL: i64 = 480 // MEASURED: our XPBD plant at the linear limit 85const SB_KUP_REF: i64 = 74 // conf tissue_k_up_n_per_m midband (cai2018 k_a 73.5) 86const SB_KDN_REF: i64 = 660 // conf tissue_k_down_n_per_m midband (cai2018 k_b 658) 87const SB_ANISO_AP_PERMIL: i64 = 610 // conf tissue_aniso_ap_permil (mills2025 AP/SI) 88const SB_ANISO_ML_PERMIL: i64 = 600 // conf tissue_aniso_ml_permil (mills2025 ML/SI) 89const SB_FIRM_MAX: i64 = 3 // genome bits 16-17 span four bred firmness levels 90// 1024*4*pi^2 and 1024*pi^2 -- the two pure constants that turn a frequency into a per-1024 91// stiffness, and (1000 mhz-per-hz * 60 ticks-per-second)^2 which carries the units. 92const SB_K4PI2_Q10: i64 = 40426 93const SB_KPI2_Q10: i64 = 10106 94const SB_MHZ_TICK_SQ: i64 = 3600000000 95 96// firmness 0..3 spans the cited free-vibration band. Firmness is BRED -- genome bits 16-17, 97// field-crossed at birth like every other trait: a population of bodies, not a preset list. 98func sb_fn_mhz(firm: i64) -> i64 { 99 return SB_FN_LO_MHZ + firm*(SB_FN_HI_MHZ - SB_FN_LO_MHZ)/SB_FIRM_MAX 100} 101// symmetric-equivalent stiffness for a target natural frequency: K = 1024*(2*pi*f/rate)^2 102func sb_k_sym(fn_mhz: i64) -> i64 { return SB_K4PI2_Q10*fn_mhz*fn_mhz/SB_MHZ_TICK_SQ } 103// A piecewise oscillator's free-vibration PERIOD is the sum of its two half-periods, so the soft 104// branch is scaled by (1 + 1/sqrt(R)) to put the MEASURED frequency on target. Using the mean of 105// the two stiffnesses instead would miss the frequency the probe actually reads. 106func sb_branch_permil() -> i64 { 107 let r100: i64 = sd_isqrt(SB_KDN_REF*10000/SB_KUP_REF) 108 if r100 < 1 { return 1000 } 109 return 1000 + 100000/r100 110} 111func sb_k_up(fn_mhz: i64) -> i64 { 112 let p: i64 = sb_branch_permil() 113 var v: i64 = SB_KPI2_Q10*fn_mhz*fn_mhz 114 v = v*p/1000 115 v = v*p/1000 116 return v/SB_MHZ_TICK_SQ 117} 118func sb_k_dn(fn_mhz: i64) -> i64 { return sb_k_up(fn_mhz)*SB_KDN_REF/SB_KUP_REF } 119// horizontal axes are sign-symmetric (cai2018's asymmetry is about the VERTICAL static 120// equilibrium) but they are NOT as compliant as the vertical: amplitude ratio = compliance 121// ratio, so a smaller cited excursion means a proportionally STIFFER axis. 122func sb_k_lat(fn_mhz: i64) -> i64 { return sb_k_sym(fn_mhz)*1000/SB_ANISO_ML_PERMIL } 123func sb_k_ant(fn_mhz: i64) -> i64 { return sb_k_sym(fn_mhz)*1000/SB_ANISO_AP_PERMIL } 124// damping coefficient for the target ratio: zeta = C/(2*sqrt(1024*K)) => C = zeta*2*sqrt(1024*K) 125// ZETA-GENERAL FORM. The chest path fixes zeta at the MEASURED tissue value, but hair (nx_hairdyn) 126// needs the SAME arithmetic at a different ratio, and a second copy of it in the hair lane would be 127// exactly the duplicate-ruler defect this file already retired once for the chest. sb_c_for is this 128// function at SB_ZETA_PERMIL, so every existing chest caller is arithmetically unchanged BY 129// CONSTRUCTION -- there is one damping ruler in the estate and both consumers read it. 130func sb_c_for_zeta(K: i64, zeta_permil: i64) -> i64 { return zeta_permil*2*sd_isqrt(SD_G*K)/1000 } 131func sb_c_for(K: i64) -> i64 { return sb_c_for_zeta(K, SB_ZETA_PERMIL) } 132 133// ===== EVERY GIRL HER OWN BODY ============================================================= 134// The cast used to tick on TWO GENOME BITS -- four firmness levels shared across sixteen girls, 135// so four bodies moved and the rest were copies of them. The estate already had the cure and it 136// was simply out of reach: nx_softtissue's st_profile_seeded draws a whole body from a seed on a 137// single FULLNESS axis, and states why it must be a single axis -- fields drawn independently 138// produce implausible combinations (a tiny body with maximal fat compliance is not a person). 139// The hash below mirrors st_hash EXACTLY -- same constants, same mixing, SAME SALT 101 -- so a 140// girl's fullness here and her tissue profile there are ONE DRAW, not two families that happen 141// to resemble each other. Fullness moves size and compliance together, so a fuller body sits 142// LOWER in the cited free-vibration band (more mass on a more compliant suspension) and a 143// smaller, firmer one sits higher. That is 1001 distinct plants where there were 4. 144const SB_MAGIC_2654435761: i64 = 2654435761 145const SB_MAGIC_1013904223: i64 = 1013904223 146const SB_MAGIC_1442695040888963407: i64 = 1442695040888963407 147const SB_MAGIC_2246822519: i64 = 2246822519 148const SB_MAGIC_288230376151711743: i64 = 288230376151711743 149const SB_FULL_N: i64 = 1001 // fullness axis 0..1000, the axis st_profile_seeded uses 150const SB_FULL_SALT: i64 = 101 // st_profile_seeded's own fullness salt -- do not diverge 151 152func sb_hash(seed: i64, salt: i64) -> i64 { 153 var h: i64 = seed * SB_MAGIC_2654435761 + salt * SB_MAGIC_1013904223 + SB_MAGIC_1442695040888963407 154 h = h ^ (h >> 15) 155 h = h * SB_MAGIC_2246822519 156 h = h ^ (h >> 13) 157 h = h & SB_MAGIC_288230376151711743 158 if h < 0 { h = 0 - h } 159 return h 160} 161func sb_full_from_gene(gene: i64) -> i64 { return sb_hash(gene, SB_FULL_SALT) % SB_FULL_N } 162func sb_fn_mhz_gene(gene: i64) -> i64 { 163 return SB_FN_HI_MHZ - sb_full_from_gene(gene)*(SB_FN_HI_MHZ - SB_FN_LO_MHZ)/(SB_FULL_N - 1) 164} 165// hard displacement clamps (model units) -- tissue can never leave the body 166const SB_MAXD_HAIR: i64 = 6 167const SB_MAXD_CHEST: i64 = 2 // +-3.1cm: tissue-vs-bone p2p <= 6.2cm, the cited band 168const SB_MAXD_SKIRT: i64 = 5 169// local anchor heights above the mob root (model units): hair tip / chest / skirt hem class 170const SB_OFFY_HAIR: i64 = 17 171const SB_OFFY_CHEST: i64 = 12 172const SB_OFFY_SKIRT: i64 = 7 173 174// chest rows are DERIVED at the softest bred firmness so there is exactly one chest ruler: these 175// accessors and the per-body path below cannot disagree, because both read the same derivation. 176func sb_k(kind: i64) -> i64 { if kind == SB_HAIR { return SB_K_HAIR } if kind == SB_CHEST { return sb_k_up(sb_fn_mhz(0)) } return SB_K_SKIRT } 177func sb_c(kind: i64) -> i64 { if kind == SB_HAIR { return SB_C_HAIR } if kind == SB_CHEST { return sb_c_for(sb_k_up(sb_fn_mhz(0))) } return SB_C_SKIRT } 178func sb_maxd(kind: i64) -> i64 { if kind == SB_HAIR { return SB_MAXD_HAIR } if kind == SB_CHEST { return SB_MAXD_CHEST } return SB_MAXD_SKIRT } 179// R7: belly/glute/thigh reuse the hip-height offset (they are hip-adjacent); a mesh-derived 180// per-region offset is the refinement, and reusing a named anchor beats inventing one. 181func sb_offy(kind: i64) -> i64 { if kind == SB_HAIR { return SB_OFFY_HAIR } if kind == SB_CHEST { return SB_OFFY_CHEST } return SB_OFFY_SKIRT } 182 183// state for n mobs (n*SB_PTS softdyn points, softdyn owns the layout) 184func sb_alloc(nmobs: i64) -> *i64 { return sd_alloc(nmobs*SB_PTS) } 185 186// seat every point of one mob exactly on its anchor (no startup transient) 187func sb_seat_mob(st: *i64, mobi: i64, rx: i64, ry: i64, rz: i64) -> i64 { 188 var k: i64 = 0 189 while k < SB_PTS { sd_seat(st, mobi*SB_PTS + k, rx, ry + sb_offy(k), rz); k = k + 1 } 190 return 0 191} 192 193// one sim tick for one mob against its CURRENT root position (organ truth) 194// The un-bred tick is the bred one at the softest firmness -- delegated, not duplicated, so a 195// caller that does not carry a genome still gets the asymmetric chest rather than a second model. 196func sb_tick_mob(st: *i64, mobi: i64, rx: i64, ry: i64, rz: i64) -> i64 { 197 return sb_tick_mob_firm(st, mobi, rx, ry, rz, 0) 198} 199// single point with explicit params (the per-body range's primitive) 200func sb_tick_pt(st: *i64, mobi: i64, kind: i64, rx: i64, ry: i64, rz: i64, K: i64, C: i64, maxd: i64) -> i64 { 201 sd_step(st, mobi*SB_PTS + kind, rx, ry + sb_offy(kind), rz, K, C, maxd) 202 return 0 203} 204// one tick with a BRED body: firmness 0..3 selects the chest band; hair/skirt ride the defaults 205func sb_tick_mob_firm(st: *i64, mobi: i64, rx: i64, ry: i64, rz: i64, firm: i64) -> i64 { 206 var f: i64 = firm 207 if f < 0 { f = 0 } 208 if f > SB_FIRM_MAX { f = SB_FIRM_MAX } 209 return sb_tick_fn(st, mobi, rx, ry, rz, sb_fn_mhz(f)) 210} 211// one tick with HER OWN body: the whole genome picks her plant, not two bits of it. 212func sb_tick_mob_gene(st: *i64, mobi: i64, rx: i64, ry: i64, rz: i64, gene: i64) -> i64 { 213 return sb_tick_fn(st, mobi, rx, ry, rz, sb_fn_mhz_gene(gene)) 214} 215// THE one chest path. Both callers above land here, so a bred body and a genome body can never 216// be two different models -- there is only one, taking a natural frequency. 217func sb_tick_fn(st: *i64, mobi: i64, rx: i64, ry: i64, rz: i64, fn_mhz: i64) -> i64 { 218 sb_tick_pt(st, mobi, SB_HAIR, rx, ry, rz, SB_K_HAIR, SB_C_HAIR, SB_MAXD_HAIR) 219 // chest rides the anisotropic + sign-asymmetric integrator: vertical carries the cited 220 // 9x up/down split, the two horizontal axes carry the cited AP/ML compliance ratios. 221 let kup: i64 = sb_k_up(fn_mhz) 222 let kdn: i64 = sb_k_dn(fn_mhz) 223 let klat: i64 = sb_k_lat(fn_mhz) 224 let kant: i64 = sb_k_ant(fn_mhz) 225 sd_step_ax(st, mobi*SB_PTS + SB_CHEST, rx, ry + sb_offy(SB_CHEST), rz, 226 klat, kup, kdn, kant, 227 sb_c_for(klat), sb_c_for(kup), sb_c_for(kdn), sb_c_for(kant), SB_MAXD_CHEST) 228 sb_tick_pt(st, mobi, SB_SKIRT, rx, ry, rz, SB_K_SKIRT, SB_C_SKIRT, SB_MAXD_SKIRT) 229 // R7: region points. dyna_bind_skin -- the binding the charsim/graphics watch declares: 230 // asset-declared tissue bones driven by the one solver, consumed by the renderer's fields. 231 dyna_bind_skin(st, mobi, rx, ry, rz) 232 return 0 233} 234 235// dyna_bind_skin: tick the asset-declared soft-tissue regions (belly, glutes, thigh) on the one 236// solver. Named for the R7/GR13 watch contract: this IS the binding the boards declare. 237// K from the pinned tissue band via the same frequency->stiffness ruler as the chest. 238func dyna_bind_skin(st: *i64, mobi: i64, rx: i64, ry: i64, rz: i64) -> i64 { 239 let klo: i64 = sb_k_sym(SB_FN_LO_MHZ) 240 let kmid: i64 = sb_k_sym((SB_FN_LO_MHZ + SB_FN_HI_MHZ)/2) 241 sb_tick_pt(st, mobi, SB_BELLY, rx, ry, rz, klo, sb_c_for(klo), SB_MAXD_CHEST) 242 sb_tick_pt(st, mobi, SB_GLUTE, rx, ry, rz, klo, sb_c_for(klo), SB_MAXD_CHEST) 243 sb_tick_pt(st, mobi, SB_THIGH, rx, ry, rz, kmid, sb_c_for(kmid), SB_MAXD_CHEST) 244 return 0 245} 246 247// a hit / footfall / landing kicks every point of the mob (recoil inheritance, same solver as firearms) 248func sb_impulse_mob(st: *i64, mobi: i64, ix: i64, iy: i64, iz: i64) -> i64 { 249 var k: i64 = 0 250 while k < SB_PTS { sd_impulse(st, mobi*SB_PTS + k, ix, iy, iz); k = k + 1 } 251 return 0 252} 253 254// ---- renderer-facing accessors: signed Q8 offsets FROM the anchor, per axis -------------------- 255// The consumer adds these to its designated joint translations. Q8 sub-unit resolution is 256// deliberate: jiggle amplitude is often a fraction of a model unit (softdyn gate T2 lesson). 257func sb_off_x_q8(st: *i64, mobi: i64, kind: i64, rx: i64) -> i64 { 258 return st[(mobi*SB_PTS + kind)*SD_STRIDE] - rx*SD_Q8 259} 260func sb_off_y_q8(st: *i64, mobi: i64, kind: i64, ry: i64) -> i64 { 261 return st[(mobi*SB_PTS + kind)*SD_STRIDE + 1] - (ry + sb_offy(kind))*SD_Q8 262} 263func sb_off_z_q8(st: *i64, mobi: i64, kind: i64, rz: i64) -> i64 { 264 return st[(mobi*SB_PTS + kind)*SD_STRIDE + 2] - rz*SD_Q8 265} 266// magnitude of displacement from anchor in Q8 (clamp verification) 267func sb_disp_q8(st: *i64, mobi: i64, kind: i64, rx: i64, ry: i64, rz: i64) -> i64 { 268 return sd_disp_q8(st, mobi*SB_PTS + kind, rx, ry + sb_offy(kind), rz) 269} 270// kinetic energy proxy of one point (decay proof) 271func sb_energy(st: *i64, mobi: i64, kind: i64) -> i64 { return sd_energy(st, mobi*SB_PTS + kind) } 272 273// deterministic rolling checksum over all state words of n mobs (replay / netsync proof) 274func sb_ck(st: *i64, nmobs: i64) -> i64 { 275 var h: i64 = SB_MAGIC_5381 276 var i: i64 = 0 277 let n: i64 = nmobs*SB_PTS*SD_STRIDE 278 while i < n { 279 var w: i64 = st[i] 280 if w < 0 { w = (0 - w)*2 + 1 } else { w = w*2 } 281 h = (h*33 + w) % SB_MAGIC_1000000007 282 i = i + 1 283 } 284 return h 285}