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}