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}