code wiki / (root) / nx_softbody_region.nx

nx_softbody_region.nx source

↩ module page · 288 lines · 12601 B

1// nx_softbody_region.nx -- BODY-REGION soft physics with the OCCLUSION LAW (operator 2026-07-03: TittyMagic- 2// class "main physics parameters" for breast/ass/body regions + "when an object enters another object and is 3// occluded the physics should be more than visible area"). A REGION = an anchor + a ring of surface nodes 4// (the cross-section of a breast/glute/belly profile; full 3D shell = next rung) + a TittyMagic-class 5// PARAMETER BLOCK AS DATA: mass, spring, damper, per-axis gravity multipliers, collider softness, and the 6// VOLUME-REDISTRIBUTION gain. THE OCCLUSION LAW: when a capsule penetrates, each contact node's displacement 7// is measured and REDISTRIBUTED as outward push on the NON-contact nodes -- the flesh bulges where the 8// collider ISN'T (displaced volume goes somewhere; physics beyond the visible contact area). 9// ALL INTEGER (pos fx256, vel fx65536, 60Hz -- the ecosystem law) => byte-identical replays, VM-vettable. 10// Base-relative. license_tier: ORIGINAL 11import "nx_syscalls.nx" 12 13// header: [0]=nnodes [1]=ncaps [2]=anchor_x [3]=anchor_y [4]=anchor_z 14// param block (DATA -- a mod ships these): [5]=P_MASS(fx256, 256=1.0) [6]=P_SPRING [7]=P_DAMP(0..256) 15// [8]=P_GMULX [9]=P_GMULY [10]=P_GMULZ (fx256 gravity multipliers) [11]=P_COLSOFT(0..256: 256=hard pushout, 16// lower=squishier) [12]=P_VOLDIST(fx256: occlusion redistribution gain) [13]=spare 17// node: 16 slots at 64+i*128: [0..2] pos [3..5] vel [6..8] rest offset (from anchor) [9] displaced-this-tick 18// capsule: 8 slots after nodes: p0xyz p1xyz r spare 19const SR_MAXN: i64 = 48 20const SR_MAXC: i64 = 8 21const SR_G: i64 = 178 22 23func sr_hdr(base: i64) -> *i64 { return base as *i64 } 24func sr_node(base: i64, i: i64) -> *i64 { return (base + 128 + i * 128) as *i64 } 25func sr_cap(base: i64, i: i64) -> *i64 { return (base + 128 + SR_MAXN * 128 + i * 64) as *i64 } 26func sr_bytes() -> i64 { return 128 + SR_MAXN * 128 + SR_MAXC * 64 + 64 } 27 28func sr_isqrt(v: i64) -> i64 { 29 if v <= 0 { return 0 } 30 var x: i64 = v 31 var y: i64 = (x + 1) / 2 32 while y < x { x = y; y = (x + v / x) / 2 } 33 return x 34} 35func sr_iabs(v: i64) -> i64 { if v < 0 { return 0 - v } return v } 36 37func sr_init(base: i64, ax: i64, ay: i64, az: i64) -> i64 { 38 let h: *i64 = sr_hdr(base) 39 h[0] = 0 40 h[1] = 0 41 h[2] = ax; h[3] = ay; h[4] = az 42 h[5] = 256; h[6] = 20; h[7] = 30 43 h[8] = 256; h[9] = 256; h[10] = 256 44 h[11] = 256; h[12] = 256 45 return 0 46} 47func sr_param(base: i64, idx: i64, v: i64) -> i64 { 48 let h: *i64 = sr_hdr(base) 49 if idx < 5 { return 0 - 1 } 50 if idx > 12 { return 0 - 1 } 51 h[idx] = v 52 return 0 53} 54func sr_add_node(base: i64, rx: i64, ry: i64, rz: i64) -> i64 { 55 let h: *i64 = sr_hdr(base) 56 if h[0] >= SR_MAXN { return 0 - 1 } 57 let n: *i64 = sr_node(base, h[0]) 58 n[0] = h[2] + rx; n[1] = h[3] + ry; n[2] = h[4] + rz 59 n[3] = 0; n[4] = 0; n[5] = 0 60 n[6] = rx; n[7] = ry; n[8] = rz 61 n[9] = 0 62 h[0] = h[0] + 1 63 return h[0] - 1 64} 65// a RING region (the 2D cross-section profile) of nn nodes, radius r, in the XY plane -- the v1 region shape. 66// Uses an integer 16-point unit circle table (fx256) stepped around; nn must divide 16 evenly at v1 (8 or 16). 67func sr_ring(base: i64, nn: i64, r: i64) -> i64 { 68 // 16-point circle, fx256: cos,sin pairs (exact integers from 256*cos(k*22.5deg)) 69 let tab: *i64 = sys_mmap(32 * 8) as *i64 70 tab[0] = 256; tab[1] = 0 71 tab[2] = 237; tab[3] = 98 72 tab[4] = 181; tab[5] = 181 73 tab[6] = 98; tab[7] = 237 74 tab[8] = 0; tab[9] = 256 75 tab[10] = 0 - 98; tab[11] = 237 76 tab[12] = 0 - 181; tab[13] = 181 77 tab[14] = 0 - 237; tab[15] = 98 78 tab[16] = 0 - 256; tab[17] = 0 79 tab[18] = 0 - 237; tab[19] = 0 - 98 80 tab[20] = 0 - 181; tab[21] = 0 - 181 81 tab[22] = 0 - 98; tab[23] = 0 - 237 82 tab[24] = 0; tab[25] = 0 - 256 83 tab[26] = 98; tab[27] = 0 - 237 84 tab[28] = 181; tab[29] = 0 - 181 85 tab[30] = 237; tab[31] = 0 - 98 86 let step: i64 = 16 / nn 87 var k: i64 = 0 88 while k < nn { 89 let c: i64 = tab[k * step * 2] 90 let s: i64 = tab[k * step * 2 + 1] 91 sr_add_node(base, r * c / 256, r * s / 256, 0) 92 k = k + 1 93 } 94 // MEASURE the rest chord (node0->node1 distance) into header[13]: the neighbor springs target THIS 95 // length. (A midpoint-target spring contracts the ring -- the chord midpoint lies INSIDE the circle; 96 // gate-proven: 31-unit rest shrink that masked gravity and swallowed the bulge.) 97 let h: *i64 = sr_hdr(base) 98 let n0: *i64 = sr_node(base, 0) 99 let n1: *i64 = sr_node(base, 1) 100 let cdx: i64 = n1[0] - n0[0] 101 let cdy: i64 = n1[1] - n0[1] 102 let cdz: i64 = n1[2] - n0[2] 103 h[13] = sr_isqrt(cdx * cdx + cdy * cdy + cdz * cdz) 104 return nn 105} 106func sr_add_capsule(base: i64, x0: i64, y0: i64, z0: i64, x1: i64, y1: i64, z1: i64, r: i64) -> i64 { 107 let h: *i64 = sr_hdr(base) 108 if h[1] >= SR_MAXC { return 0 - 1 } 109 let c: *i64 = sr_cap(base, h[1]) 110 c[0] = x0; c[1] = y0; c[2] = z0 111 c[3] = x1; c[4] = y1; c[5] = z1 112 c[6] = r 113 h[1] = h[1] + 1 114 return h[1] - 1 115} 116func sr_clear_capsules(base: i64) -> i64 { 117 let h: *i64 = sr_hdr(base) 118 h[1] = 0 119 return 0 120} 121func sr_anchor(base: i64, ax: i64, ay: i64, az: i64) -> i64 { 122 let h: *i64 = sr_hdr(base) 123 h[2] = ax; h[3] = ay; h[4] = az 124 return 0 125} 126 127func sr_step(base: i64) -> i64 { 128 let h: *i64 = sr_hdr(base) 129 let nn: i64 = h[0] 130 let mass: i64 = h[5] 131 let k: i64 = h[6] 132 let damp: i64 = h[7] 133 var i: i64 = 0 134 // pass 1: springs (anchor-rest + ring neighbors), gravity, damping, integrate, collide, measure displacement 135 while i < nn { 136 let n: *i64 = sr_node(base, i) 137 // spring toward the rest position in the anchor frame; response scaled by 1/mass 138 let tx: i64 = h[2] + n[6] 139 let ty: i64 = h[3] + n[7] 140 let tz: i64 = h[4] + n[8] 141 n[3] = n[3] + (tx - n[0]) * k * 256 / mass 142 n[4] = n[4] + (ty - n[1]) * k * 256 / mass 143 n[5] = n[5] + (tz - n[2]) * k * 256 / mass 144 // ring neighbor EDGE spring at rest chord length h[13] (action-reaction to the RIGHT neighbor once 145 // per edge; momentum-conserving; zero force at rest -- no contraction) 146 let R: *i64 = sr_node(base, (i + 1) % nn) 147 let edx: i64 = R[0] - n[0] 148 let edy: i64 = R[1] - n[1] 149 let edz: i64 = R[2] - n[2] 150 let edist: i64 = sr_isqrt(edx * edx + edy * edy + edz * edz) 151 if edist > 0 { 152 let eerr: i64 = edist - h[13] 153 let k2: i64 = k / 2 154 n[3] = n[3] + edx * eerr * k2 * 256 / (edist * mass) 155 n[4] = n[4] + edy * eerr * k2 * 256 / (edist * mass) 156 n[5] = n[5] + edz * eerr * k2 * 256 / (edist * mass) 157 R[3] = R[3] - edx * eerr * k2 * 256 / (edist * mass) 158 R[4] = R[4] - edy * eerr * k2 * 256 / (edist * mass) 159 R[5] = R[5] - edz * eerr * k2 * 256 / (edist * mass) 160 } 161 // per-axis gravity multipliers (TittyMagic gravity physics) 162 n[4] = n[4] - SR_G * h[9] / 256 163 n[3] = n[3] - SR_G * (h[8] - 256) / 256 // gmulx/z express DIRECTIONAL lean as deviation from 256 164 n[5] = n[5] - SR_G * (h[10] - 256) / 256 165 // damping 166 n[3] = n[3] - n[3] * damp / 256 167 n[4] = n[4] - n[4] * damp / 256 168 n[5] = n[5] - n[5] * damp / 256 169 // integrate 170 n[0] = n[0] + n[3] / 256 171 n[1] = n[1] + n[4] / 256 172 n[2] = n[2] + n[5] / 256 173 // colliders: soft pushout (colsoft fraction per tick) + displacement measured for redistribution 174 n[9] = 0 175 var ci: i64 = 0 176 while ci < h[1] { 177 let cp: *i64 = sr_cap(base, ci) 178 let dx: i64 = cp[3] - cp[0] 179 let dy: i64 = cp[4] - cp[1] 180 let dz: i64 = cp[5] - cp[2] 181 let den: i64 = dx * dx + dy * dy + dz * dz 182 var t256: i64 = 0 183 if den > 0 { 184 let num: i64 = (n[0] - cp[0]) * dx + (n[1] - cp[1]) * dy + (n[2] - cp[2]) * dz 185 t256 = num * 256 / den 186 if t256 < 0 { t256 = 0 } 187 if t256 > 256 { t256 = 256 } 188 } 189 let cx: i64 = cp[0] + dx * t256 / 256 190 let cy: i64 = cp[1] + dy * t256 / 256 191 let cz: i64 = cp[2] + dz * t256 / 256 192 let ex: i64 = n[0] - cx 193 let ey: i64 = n[1] - cy 194 let ez: i64 = n[2] - cz 195 let d2: i64 = ex * ex + ey * ey + ez * ez 196 let r: i64 = cp[6] 197 // contact detected with a SKIN (r+24): a node resting ON the surface is still IN CONTACT -- 198 // without the skin, sustained occlusion reads zero at steady state and the bulge would decay 199 // while the object is still embedded. 200 if d2 < (r + 24) * (r + 24) { 201 var dist: i64 = sr_isqrt(d2) 202 var ux: i64 = 256 203 var uy: i64 = 0 204 var uz: i64 = 0 205 if dist > 0 { ux = ex * 256 / dist; uy = ey * 256 / dist; uz = ez * 256 / dist } 206 if dist < r { 207 let want: i64 = r - dist // pushout depth 208 let push: i64 = want * h[11] / 256 // colsoft: fraction applied per tick (squish) 209 n[0] = n[0] + ux * push / 256 210 n[1] = n[1] + uy * push / 256 211 n[2] = n[2] + uz * push / 256 212 let vr: i64 = (n[3] * ux + n[4] * uy + n[5] * uz) / 256 213 if vr < 0 { 214 n[3] = n[3] - ux * vr / 256 215 n[4] = n[4] - uy * vr / 256 216 n[5] = n[5] - uz * vr / 256 217 } 218 } 219 // sustained OCCLUSION = how far the collider holds this node off its rest pose 220 let ox2: i64 = n[0] - (h[2] + n[6]) 221 let oy2: i64 = n[1] - (h[3] + n[7]) 222 let oz2: i64 = n[2] - (h[4] + n[8]) 223 n[9] = sr_isqrt(ox2 * ox2 + oy2 * oy2 + oz2 * oz2) 224 } 225 ci = ci + 1 226 } 227 i = i + 1 228 } 229 // pass 2: THE OCCLUSION LAW -- total displaced amount redistributes as OUTWARD push on non-contact nodes 230 var total: i64 = 0 231 var freecount: i64 = 0 232 i = 0 233 while i < nn { 234 let n2: *i64 = sr_node(base, i) 235 total = total + n2[9] 236 if n2[9] == 0 { freecount = freecount + 1 } 237 i = i + 1 238 } 239 if total > 0 { if freecount > 0 { 240 // per-tick feed + a PHYSICAL CAP: a free node's outward bulge beyond its rest radius cannot exceed 241 // its share of the displaced volume (cap = 2*total*voldist/256/freecount). Without the cap the 242 // position feed grows unbounded against the soft spring; with it, nodes HOLD a sustained bulge while 243 // the collider is embedded and relax when it leaves (total -> 0 -> cap -> 0). 244 // full feed; the PHYSICAL CAP is the safety (an arbitrary /8 damper here strangled the bulge to 6 245 // units -- gate-measured; the cap alone bounds it) 246 let share: i64 = total * h[12] / 256 / freecount 247 let cap: i64 = total * h[12] / 256 * 2 / freecount 248 i = 0 249 while i < nn { 250 let n3: *i64 = sr_node(base, i) 251 if n3[9] == 0 { 252 let ox: i64 = n3[0] - h[2] 253 let oy: i64 = n3[1] - h[3] 254 let oz: i64 = n3[2] - h[4] 255 let om: i64 = sr_isqrt(ox * ox + oy * oy + oz * oz) 256 let rl: i64 = sr_isqrt(n3[6] * n3[6] + n3[7] * n3[7] + n3[8] * n3[8]) 257 if om > 0 { 258 var excess: i64 = om + share - rl 259 var newlen: i64 = om + share 260 if excess > cap { newlen = rl + cap } 261 n3[0] = h[2] + ox * newlen / om 262 n3[1] = h[3] + oy * newlen / om 263 n3[2] = h[4] + oz * newlen / om 264 } 265 } 266 i = i + 1 267 } 268 } } 269 return 0 270} 271 272// diagnostics for gates/HUDs 273func sr_pos(base: i64, i: i64, axis: i64) -> i64 { let n: *i64 = sr_node(base, i); return n[axis] } 274// polygon area proxy (shoelace, fx256^2 halved) of the ring cross-section -- the volume-conservation measure 275func sr_area(base: i64) -> i64 { 276 let h: *i64 = sr_hdr(base) 277 let nn: i64 = h[0] 278 var acc: i64 = 0 279 var i: i64 = 0 280 while i < nn { 281 let a: *i64 = sr_node(base, i) 282 let b: *i64 = sr_node(base, (i + 1) % nn) 283 acc = acc + (a[0] - h[2]) * (b[1] - h[3]) - (b[0] - h[2]) * (a[1] - h[3]) 284 i = i + 1 285 } 286 if acc < 0 { acc = 0 - acc } 287 return acc / 2 288}