code wiki / (root) / nx_softbody_region.nx

nx_softbody_region.nx source

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