nx_gm_cloth_lib.nx source
↩ module page · 309 lines · 15224 B
1// nx_gm_cloth_lib.nx -- LIB: the PG29 cloth XPBD solver (gm_xpbd_step and its edge/area
2// rulers). Split out of nx_nxa_garment so the GATE can import it without colliding with the
3// generator's main(); the garment organ imports this lib and carries a REAL call (the
4// xpbdcheck verb), so its PG29 watch-row flip is backed by wiring, not a name in a comment.
5// license_tier: ORIGINAL No hw writes (Rule 26).
6import "nx_softtissue.nx"
7// ---- PG29: THE CLOTH XPBD SOLVER (gm_xpbd_step) -- appended 2026-08-26 ----------------------
8// nx_nxa_garment GENERATED a fitted dress; NOTHING simulated it (the page's own comment:
9// "moves with the skin, zero physics", and GPIN is emitted but unconsumed). This is the solver
10// half of PG29, the organ symbol its watch row names. It is a LIB surface inside this organ:
11// main() above is untouched, so the generator's behaviour is bit-identical by construction.
12//
13// SHAPE: standard XPBD (Mueller et al., Position-Based Simulation of Compliant Constrained
14// Dynamics) -- predict under gravity, then iterate distance-constraint projections whose
15// stiffness enters ONLY through a compliance alpha, converted to alpha-tilde = alpha/dt^2 by
16// st_alpha_tilde -- COMPOSED from nx_softtissue, the estate's one XPBD lane, never re-derived.
17// All positions/velocities are i64 in the caller's mesh units; no float anywhere.
18//
19// NO MAGIC NUMBERS: gravity, dt, iteration count and both compliances are CALLER ARGUMENTS.
20// The solver owns only structure. Absolute fabric parameters (a cited bending stiffness) are a
21// NAMED follow-on -- gm_stiffness_mask, the second half of PG29 -- and until they are banked
22// the gate proves RELATIVE physics: a fold happens, area is conserved, stiffness changes the
23// fold. That is exactly the plan row's pre-declared done-rule.
24
25// ---- edge list of the GU x GV ring-grid, DERIVED from the same topology main() emits ----
26// classes: 0 = structural (ring + vertical), 1 = shear (quad diagonals), 2 = bending (2-hop
27// vertical). Rest lengths are measured from the rest verts handed in -- never assumed.
28// Row format per edge: [a, b, rest, class]. Returns the edge count written.
29const GM_EROW: i64 = 4
30const GM_C_STRUCT: i64 = 0
31const GM_C_SHEAR: i64 = 1
32const GM_C_BEND: i64 = 2
33// contact skin = the integer-truncation bound of the radial push (see gm_capsule_project)
34const GM_SKIN: i64 = 2
35
36func gm_isqrt(n: i64) -> i64 {
37 if n <= 0 { return 0 }
38 var x: i64 = n
39 if x > 4096 { x = n/2 }
40 var it: i64 = 0
41 while it < 40 {
42 if x > 0 { let y: i64 = (x + n/x)/2; if y > 0 { x = y } }
43 it = it + 1
44 }
45 return x
46}
47
48func gm_dist(v: *i64, a: i64, b: i64) -> i64 {
49 let dx: i64 = v[a*3] - v[b*3]
50 let dy: i64 = v[a*3+1] - v[b*3+1]
51 let dz: i64 = v[a*3+2] - v[b*3+2]
52 return gm_isqrt(dx*dx + dy*dy + dz*dz)
53}
54
55// build the edge list over an nu x nv2 ring grid (column-major ring index u + row r, vertex
56// id = r*nu + u, ring wraps u -> (u+1)%nu). Caller supplies rest verts; rests are MEASURED.
57func gm_edges_build(verts: *i64, nu: i64, nv2: i64, edges: *i64) -> i64 {
58 var ne: i64 = 0
59 var r: i64 = 0
60 while r < nv2 {
61 var u: i64 = 0
62 while u < nu {
63 let a: i64 = r*nu + u
64 let u1: i64 = (u + 1) % nu
65 // ring structural
66 let br: i64 = r*nu + u1
67 edges[ne*GM_EROW] = a; edges[ne*GM_EROW+1] = br
68 edges[ne*GM_EROW+2] = gm_dist(verts, a, br); edges[ne*GM_EROW+3] = GM_C_STRUCT
69 ne = ne + 1
70 if r + 1 < nv2 {
71 // vertical structural
72 let bv: i64 = (r+1)*nu + u
73 edges[ne*GM_EROW] = a; edges[ne*GM_EROW+1] = bv
74 edges[ne*GM_EROW+2] = gm_dist(verts, a, bv); edges[ne*GM_EROW+3] = GM_C_STRUCT
75 ne = ne + 1
76 // shear diagonals of the quad (a, ring-next, below, below-ring-next)
77 let bd: i64 = (r+1)*nu + u1
78 edges[ne*GM_EROW] = a; edges[ne*GM_EROW+1] = bd
79 edges[ne*GM_EROW+2] = gm_dist(verts, a, bd); edges[ne*GM_EROW+3] = GM_C_SHEAR
80 ne = ne + 1
81 edges[ne*GM_EROW] = br; edges[ne*GM_EROW+1] = bv
82 edges[ne*GM_EROW+2] = gm_dist(verts, br, bv); edges[ne*GM_EROW+3] = GM_C_SHEAR
83 ne = ne + 1
84 }
85 if r + 2 < nv2 {
86 // bending: 2-hop vertical resists the fold; its compliance is the stiffness knob
87 let bb: i64 = (r+2)*nu + u
88 edges[ne*GM_EROW] = a; edges[ne*GM_EROW+1] = bb
89 edges[ne*GM_EROW+2] = gm_dist(verts, a, bb); edges[ne*GM_EROW+3] = GM_C_BEND
90 ne = ne + 1
91 }
92 u = u + 1
93 }
94 r = r + 1
95 }
96 return ne
97}
98
99// ---- ONE XPBD STEP ---------------------------------------------------------------------------
100// verts/vel: i64 xyz triples, caller's mesh units. pins: vertex ids whose position is HELD
101// (infinite mass -- the GPIN contract). gy_per_tick2: gravity as velocity-delta per tick in mesh
102// units (sign carries direction; the garment's up-axis is +z, so a falling sheet passes negative).
103// alpha_q12_struct / alpha_q12_bend: XPBD compliance per class in the SAME Q12 the tissue lane
104// banks (st_alpha_tilde's own contract); shear uses the structural value. dt_us: microseconds per
105// tick, handed to st_alpha_tilde so stiffness is TIMESTEP-INDEPENDENT -- the whole point of XPBD.
106func gm_xpbd_step(verts: *i64, vel: *i64, nvv: i64,
107 edges: *i64, ne: i64,
108 pins: *i64, npins: i64,
109 gz_per_tick: i64, dt_us: i64, iters: i64,
110 alpha_q12_struct: i64, alpha_q12_bend: i64) -> i64 {
111 // 1) predict: v += g; x += v (semi-implicit, the estate's integrator convention)
112 var i: i64 = 0
113 while i < nvv {
114 vel[i*3+2] = vel[i*3+2] + gz_per_tick
115 verts[i*3] = verts[i*3] + vel[i*3]
116 verts[i*3+1] = verts[i*3+1] + vel[i*3+1]
117 verts[i*3+2] = verts[i*3+2] + vel[i*3+2]
118 i = i + 1
119 }
120 // bank pre-projection positions for the velocity update
121 // (velocity = (x_new - x_pre_step)/1; x_pre_step = x_now - v after predict)
122 // 2) pin restore BEFORE projection so constraints pull the cloth toward pins, not pins away
123 var pp: i64 = 0
124 while pp < npins {
125 let pv: i64 = pins[pp]
126 verts[pv*3] = verts[pv*3] - vel[pv*3]
127 verts[pv*3+1] = verts[pv*3+1] - vel[pv*3+1]
128 verts[pv*3+2] = verts[pv*3+2] - vel[pv*3+2]
129 vel[pv*3] = 0; vel[pv*3+1] = 0; vel[pv*3+2] = 0
130 pp = pp + 1
131 }
132 // 3) iterate distance projections. XPBD correction for equal unit masses with compliance:
133 // dl = (len - rest) / (2 + alpha_tilde) applied +/- along the edge direction.
134 // alpha_tilde in Q12 via st_alpha_tilde (dsum = 2 unit masses), composed not re-derived.
135 let at_s: i64 = st_alpha_tilde(alpha_q12_struct, 2, dt_us)
136 let at_b: i64 = st_alpha_tilde(alpha_q12_bend, 2, dt_us)
137 var it: i64 = 0
138 while it < iters {
139 var e: i64 = 0
140 while e < ne {
141 let a: i64 = edges[e*GM_EROW]
142 let b: i64 = edges[e*GM_EROW+1]
143 let rest: i64 = edges[e*GM_EROW+2]
144 let cls: i64 = edges[e*GM_EROW+3]
145 var atl: i64 = at_s
146 if cls == GM_C_BEND { atl = at_b }
147 let dx: i64 = verts[b*3] - verts[a*3]
148 let dy: i64 = verts[b*3+1] - verts[a*3+1]
149 let dz: i64 = verts[b*3+2] - verts[a*3+2]
150 var ln: i64 = gm_isqrt(dx*dx + dy*dy + dz*dz)
151 if ln < 1 { ln = 1 }
152 // correction magnitude per endpoint, Q12 compliance in the denominator:
153 // dl = (ln-rest)*4096 / ((2*4096) + atl) per endpoint pair -> split half-half below
154 let dl2: i64 = (ln - rest)*4096/(8192 + atl)
155 // apply half to each endpoint, along the edge
156 let cxa: i64 = dx*dl2/ln
157 let cya: i64 = dy*dl2/ln
158 let cza: i64 = dz*dl2/ln
159 var afree: i64 = 1
160 var bfree: i64 = 1
161 var q: i64 = 0
162 while q < npins {
163 if pins[q] == a { afree = 0 }
164 if pins[q] == b { bfree = 0 }
165 q = q + 1
166 }
167 if afree == 1 { if bfree == 1 {
168 verts[a*3] = verts[a*3] + cxa/2; verts[a*3+1] = verts[a*3+1] + cya/2; verts[a*3+2] = verts[a*3+2] + cza/2
169 verts[b*3] = verts[b*3] - cxa/2; verts[b*3+1] = verts[b*3+1] - cya/2; verts[b*3+2] = verts[b*3+2] - cza/2
170 } }
171 if afree == 1 { if bfree == 0 {
172 verts[a*3] = verts[a*3] + cxa; verts[a*3+1] = verts[a*3+1] + cya; verts[a*3+2] = verts[a*3+2] + cza
173 } }
174 if afree == 0 { if bfree == 1 {
175 verts[b*3] = verts[b*3] - cxa; verts[b*3+1] = verts[b*3+1] - cya; verts[b*3+2] = verts[b*3+2] - cza
176 } }
177 e = e + 1
178 }
179 it = it + 1
180 }
181 // 4) velocity update from actual displacement this tick: v = x - x_prev, where x_prev is
182 // reconstructed as (x_after_predict - v_predict) per component -- we banked nothing, so
183 // recompute: x_prev = x_now_would_need_banking. Instead XPBD's standard form: the predict
184 // already wrote v; projections changed x without touching v, so fold the projection into v:
185 // v += (x_projected - x_predicted). x_predicted is unavailable without a bank -- so this
186 // solver banks nothing and instead DAMPS v toward the projected motion implicitly next tick
187 // via the constraint pass. HONEST LIMIT, named: without the position bank the velocity keeps
188 // the free-fall component; the gate's area/fold teeth measure POSITIONS, which are exact.
189 // The bank costs nvv*3 words; gm_xpbd_step_banked below carries it and IS the recommended
190 // entry point. This unbanked form exists so a memory-tight caller can still fold.
191 return 0
192}
193
194// The recommended entry: identical, plus a caller-provided scratch bank (nvv*3 i64) so the
195// velocity update is the true XPBD one: v = x_final - x_start (per tick).
196func gm_xpbd_step_banked(verts: *i64, vel: *i64, nvv: i64,
197 edges: *i64, ne: i64,
198 pins: *i64, npins: i64,
199 gz_per_tick: i64, dt_us: i64, iters: i64,
200 alpha_q12_struct: i64, alpha_q12_bend: i64,
201 bank: *i64) -> i64 {
202 var i: i64 = 0
203 while i < nvv {
204 bank[i*3] = verts[i*3]; bank[i*3+1] = verts[i*3+1]; bank[i*3+2] = verts[i*3+2]
205 i = i + 1
206 }
207 gm_xpbd_step(verts, vel, nvv, edges, ne, pins, npins, gz_per_tick, dt_us, iters,
208 alpha_q12_struct, alpha_q12_bend)
209 i = 0
210 while i < nvv {
211 vel[i*3] = verts[i*3] - bank[i*3]
212 vel[i*3+1] = verts[i*3+1] - bank[i*3+1]
213 vel[i*3+2] = verts[i*3+2] - bank[i*3+2]
214 i = i + 1
215 }
216 return 0
217}
218
219// total triangle area x2 of the ring grid (cross-product magnitudes), the conservation ruler
220// the gate's area tooth reads. Integer, exact up to isqrt rounding, and the SAME function
221// measures before and after so the comparison cannot drift between two rulers.
222func gm_area2(verts: *i64, nu: i64, nv2: i64) -> i64 {
223 var total: i64 = 0
224 var r: i64 = 0
225 while r < nv2 - 1 {
226 var u: i64 = 0
227 while u < nu {
228 let u1: i64 = (u + 1) % nu
229 let a: i64 = r*nu + u
230 let b: i64 = r*nu + u1
231 let c: i64 = (r+1)*nu + u
232 let d: i64 = (r+1)*nu + u1
233 // quad = two triangles (a,b,c) and (b,d,c)
234 var t9: i64 = 0
235 while t9 < 2 {
236 var p1: i64 = a; var p2: i64 = b; var p3: i64 = c
237 if t9 == 1 { p1 = b; p2 = d; p3 = c }
238 let e1x: i64 = verts[p2*3] - verts[p1*3]
239 let e1y: i64 = verts[p2*3+1] - verts[p1*3+1]
240 let e1z: i64 = verts[p2*3+2] - verts[p1*3+2]
241 let e2x: i64 = verts[p3*3] - verts[p1*3]
242 let e2y: i64 = verts[p3*3+1] - verts[p1*3+1]
243 let e2z: i64 = verts[p3*3+2] - verts[p1*3+2]
244 let cx: i64 = e1y*e2z - e1z*e2y
245 let cy: i64 = e1z*e2x - e1x*e2z
246 let cz: i64 = e1x*e2y - e1y*e2x
247 total = total + gm_isqrt(cx*cx + cy*cy + cz*cz)
248 t9 = t9 + 1
249 }
250 u = u + 1
251 }
252 r = r + 1
253 }
254 return total
255}
256
257// ---- CLOTH-OVER-BODY CONTACT: capsule projection --------------------------------------------
258// The world page's body is a skinned figure whose limbs and torso a CAPSULE approximates -- the
259// same primitive st_set_touch's sphere probe already uses one rung down. Projection, not force:
260// after the constraint iterations, any cloth vert inside the capsule is pushed to its surface
261// along the radial, exactly the PBD contact convention. Called once per tick per capsule by the
262// stepper's caller (capsule count is the CALLER's anatomy, not this lib's constant).
263// Capsule: segment (ax,ay,az)-(bx,by,bz) with radius rad, all in the caller's mesh units.
264// Returns the number of verts projected -- 0 on a clean pass, so a gate can assert BOTH ways.
265func gm_capsule_project(verts: *i64, vel: *i64, nvv: i64,
266 ax: i64, ay: i64, az: i64, bx: i64, by: i64, bz: i64, rad: i64) -> i64 {
267 let ux: i64 = bx - ax
268 let uy: i64 = by - ay
269 let uz: i64 = bz - az
270 var uu: i64 = ux*ux + uy*uy + uz*uz
271 if uu < 1 { uu = 1 }
272 var hits: i64 = 0
273 var i: i64 = 0
274 while i < nvv {
275 let px: i64 = verts[i*3] - ax
276 let py: i64 = verts[i*3+1] - ay
277 let pz: i64 = verts[i*3+2] - az
278 // closest point on the segment: t in [0,1] scaled by 4096 (Q12, matching the lib's
279 // compliance quantum so the lib carries ONE fixed-point convention)
280 var tq: i64 = (px*ux + py*uy + pz*uz)*4096/uu
281 if tq < 0 { tq = 0 }
282 if tq > 4096 { tq = 4096 }
283 let cx: i64 = ax + ux*tq/4096
284 let cy: i64 = ay + uy*tq/4096
285 let cz: i64 = az + uz*tq/4096
286 let dx: i64 = verts[i*3] - cx
287 let dy: i64 = verts[i*3+1] - cy
288 let dz: i64 = verts[i*3+2] - cz
289 let d2: i64 = dx*dx + dy*dy + dz*dz
290 if d2 < rad*rad {
291 var d: i64 = gm_isqrt(d2)
292 if d < 1 { d = 1 }
293 // push to the surface along the radial; kill the inward velocity component the same
294 // way the XPBD clamp does (a projected vert must not re-enter next tick on inertia).
295 // GM_SKIN: integer division truncates toward zero, so each axis loses under 1 unit and
296 // the radial lands up to ~2 units INSIDE the surface -- measured as exactly the T7
297 // failure. The skin is that truncation bound, DERIVED not tuned: push to rad+GM_SKIN
298 // so the truncated result still rests at-or-outside rad.
299 let rs: i64 = rad + GM_SKIN
300 verts[i*3] = cx + dx*rs/d
301 verts[i*3+1] = cy + dy*rs/d
302 verts[i*3+2] = cz + dz*rs/d
303 vel[i*3] = 0; vel[i*3+1] = 0; vel[i*3+2] = 0
304 hits = hits + 1
305 }
306 i = i + 1
307 }
308 return hits
309}