code wiki / (root) / nx_phys3d.nx

nx_phys3d.nx source

↩ module page · 294 lines · 12766 B

1// nx_phys3d.nx -- the UNIFIED sovereign 3D rigid-body physics core (the 07-01 census's named gap; operator 2// 2026-07-03: "real 3d s class exceed physics"). ONE engine for games + sim + CAD-motion: semi-implicit Euler, 3// sphere/AABB contacts, sequential impulse solver (restitution + Coulomb friction + positional correction), 4// sleeping. ALL INTEGER (deterministic law: fixed-point pos=1/256 m, vel=1/65536 m-per-tick; 60Hz implied) -- 5// byte-identical replays by construction, no float, no libm. Base-relative so the same code runs native AND 6// in-wasm (mineworld pattern). Broadphase is O(n^2) pairs at this rung (BVH = ladder R4); shapes = sphere + 7// axis-aligned box (GJK convex = R5; rotation/orientation = R6 with joints -- stated, not hidden). 8// license_tier: ORIGINAL 9import "nx_syscalls.nx" 10 11// body record: 16 i64 slots 12// [0]shape 0=sphere 1=box [1]x [2]y [3]z (fx256) [4]vx [5]vy [6]vz (fx65536) 13// [7]hx|radius [8]hy [9]hz (fx256) [10]inv_mass (fx256; 0=STATIC) [11]restitution(0..256) 14// [12]friction(0..256) [13]still_frames [14]awake(1/0) [15]spare 15const PH_MAXB: i64 = 64 16const PH_G: i64 = 178 // ~9.8 m/s^2 at 60Hz in vel-units (0.00272 m/tick * 65536) 17const PH_SLOP: i64 = 4 // penetration allowance (fx256) 18const PH_POSPCT: i64 = 128 // positional correction percent (fx256 of penetration beyond slop) 19const PH_RESTV: i64 = 400 // |vn| below this -> restitution treated as 0 (rest, standard trick) 20const PH_SLEEPV: i64 = 80 // per-axis |v| threshold for stillness 21const PH_SLEEPN: i64 = 30 // still frames before sleep 22const PH_ITERS: i64 = 6 // solver iterations per tick 23 24func ph_hdr(base: i64) -> *i64 { return base as *i64 } // [0]=nbodies [1]=tick [2..7]=spare 25func ph_body(base: i64, id: i64) -> *i64 { return (base + 64 + id * 128) as *i64 } 26// WARM-START tables (the Box2D-standard robustness kit; the cure for the parked stack rest-pen rung): 27// ACC = this tick's accumulated normal impulse per pair, WARM = last tick's (pre-loaded at first touch). 28// key = ia*PH_MAXB+ib. Copied ACC->WARM (and ACC zeroed) at the end of every step, so separated pairs 29// carry NO stale impulse. 30func ph_acc(base: i64) -> *i64 { return (base + 64 + PH_MAXB * 128) as *i64 } 31func ph_warm(base: i64) -> *i64 { return (base + 64 + PH_MAXB * 128 + PH_MAXB * PH_MAXB * 8) as *i64 } 32// per-pair TARGET VELOCITY for this tick (restitution + Baumgarte bias, captured at sweep 0). The 33// accumulator drives vn TOWARD the target, never toward 0 -- driving toward 0 clawed the bounce back 34// (sweeps 1..5 erased the restitution while overlap persisted: gate-measured apex=0) and let the warm 35// pre-load self-cancel the bias (rest sank to 42). 36func ph_tgt(base: i64) -> *i64 { return (base + 64 + PH_MAXB * 128 + PH_MAXB * PH_MAXB * 16) as *i64 } 37func ph_bytes() -> i64 { return 64 + PH_MAXB * 128 + PH_MAXB * PH_MAXB * 24 } 38 39func ph_init(base: i64) -> i64 { 40 let h: *i64 = ph_hdr(base) 41 h[0] = 0 42 h[1] = 0 43 let ac: *i64 = ph_acc(base) 44 let wm: *i64 = ph_warm(base) 45 let tg: *i64 = ph_tgt(base) 46 var i: i64 = 0 47 while i < PH_MAXB * PH_MAXB { ac[i] = 0; wm[i] = 0; tg[i] = 0; i = i + 1 } 48 return 0 49} 50 51func ph_add(base: i64, shape: i64, x: i64, y: i64, z: i64, hx: i64, hy: i64, hz: i64, invm: i64, rest: i64, fric: i64) -> i64 { 52 let h: *i64 = ph_hdr(base) 53 if h[0] >= PH_MAXB { return 0 - 1 } 54 let id: i64 = h[0] 55 let b: *i64 = ph_body(base, id) 56 b[0] = shape 57 b[1] = x; b[2] = y; b[3] = z 58 b[4] = 0; b[5] = 0; b[6] = 0 59 b[7] = hx; b[8] = hy; b[9] = hz 60 b[10] = invm 61 b[11] = rest 62 b[12] = fric 63 b[13] = 0 64 b[14] = 1 65 b[15] = 0 66 h[0] = h[0] + 1 67 return id 68} 69 70func ph_set_vel(base: i64, id: i64, vx: i64, vy: i64, vz: i64) -> i64 { 71 let b: *i64 = ph_body(base, id) 72 b[4] = vx; b[5] = vy; b[6] = vz 73 b[14] = 1 74 b[13] = 0 75 return 0 76} 77 78func ph_iabs(v: i64) -> i64 { if v < 0 { return 0 - v } return v } 79func ph_min(a: i64, b: i64) -> i64 { if a < b { return a } return b } 80// deterministic integer sqrt (Newton), for sphere-sphere normals 81func ph_isqrt(v: i64) -> i64 { 82 if v <= 0 { return 0 } 83 var x: i64 = v 84 var y: i64 = (x + 1) / 2 85 while y < x { x = y; y = (x + v / x) / 2 } 86 return x 87} 88 89// resolve ONE contact: bodies a,b; unit normal scaled fx256 pointing a->b; penetration fx256 (>0 = overlap). 90// SCALE LAW (audited): v fx65536, invm fx256, n fx256. j = (256+e)*(-vn)/imsum is momentum in fx65536. 91// dv = j*n*invm => stored: j * n/256 * im/256 (exactly TWO /256 -- one per fx256 factor). 92// SLEEP LAW: a resting contact (pen<=slop AND |vn| below rest threshold) must apply NOTHING and wake NO ONE, 93// or stacks would jitter awake forever. 94func ph_contact(base: i64, ia: i64, ib: i64, nx: i64, ny: i64, nz: i64, pen: i64, docorr: i64) -> i64 { 95 let a: *i64 = ph_body(base, ia) 96 let b: *i64 = ph_body(base, ib) 97 let ima: i64 = a[10] 98 let imb: i64 = b[10] 99 let imsum: i64 = ima + imb 100 if imsum == 0 { return 0 } 101 let pc: i64 = pen - PH_SLOP 102 var rvx: i64 = b[4] - a[4] 103 var rvy: i64 = b[5] - a[5] 104 var rvz: i64 = b[6] - a[6] 105 var vn: i64 = (rvx * nx + rvy * ny + rvz * nz) / 256 106 if pc <= 0 { if vn >= 0 - PH_SLEEPV { return 0 } } // settled resting touch: no action, no wake 107 if pc <= 0 { if vn >= 0 { return 0 } } // separating with no penetration debt 108 a[14] = 1 109 b[14] = 1 110 let key: i64 = ia * PH_MAXB + ib 111 let ac: *i64 = ph_acc(base) 112 // WARM-START (first sweep): pre-apply last tick's accumulated impulse along the CURRENT normal -- the 113 // solver starts from yesterday's answer instead of re-fighting the column weight from zero. This is the 114 // literature cure for the deep-stack rest-pen (the previously parked rung). 115 if docorr == 1 { 116 let wm: *i64 = ph_warm(base) 117 let w: i64 = wm[key] 118 if w > 0 { 119 a[4] = a[4] - w * nx / 256 * ima / 256 120 a[5] = a[5] - w * ny / 256 * ima / 256 121 a[6] = a[6] - w * nz / 256 * ima / 256 122 b[4] = b[4] + w * nx / 256 * imb / 256 123 b[5] = b[5] + w * ny / 256 * imb / 256 124 b[6] = b[6] + w * nz / 256 * imb / 256 125 ac[key] = w 126 rvx = b[4] - a[4] 127 rvy = b[5] - a[5] 128 rvz = b[6] - a[6] 129 vn = (rvx * nx + rvy * ny + rvz * nz) / 256 130 } 131 } 132 // TARGET VELOCITY captured at sweep 0: restitution (+e*|vn0| on fast approach, mix=MAX) + Baumgarte 133 // bias (resting regime). Sweeps drive vn TOWARD tgt via signed accumulated deltas (clamp on the 134 // ACCUMULATOR only) -- the bounce survives the later sweeps and the bias survives the pre-load. 135 let tg: *i64 = ph_tgt(base) 136 if docorr == 1 { 137 var tv: i64 = 0 138 if vn < 0 - PH_RESTV { 139 let e: i64 = ph_imax(a[11], b[11]) 140 tv = e * (0 - vn) / 256 141 } else { 142 // bias gain 77 (was 51): rest-pen equilibrium is gravity/gain ~ 178/77 ~ 2.3 beyond slop -- 143 // inside the gate's r+-7 rest tolerance (51 rested at pen ~12 under the warm-started solver) 144 if pc > 0 { tv = pc * 77 } 145 } 146 tg[key] = tv 147 } 148 var jstop: i64 = (tg[key] - vn) * 256 / imsum 149 let oldacc: i64 = ac[key] 150 var newacc: i64 = oldacc + jstop 151 if newacc < 0 { newacc = 0 } 152 let dj: i64 = newacc - oldacc 153 ac[key] = newacc 154 a[4] = a[4] - dj * nx / 256 * ima / 256 155 a[5] = a[5] - dj * ny / 256 * ima / 256 156 a[6] = a[6] - dj * nz / 256 * ima / 256 157 b[4] = b[4] + dj * nx / 256 * imb / 256 158 b[5] = b[5] + dj * ny / 256 * imb / 256 159 b[6] = b[6] + dj * nz / 256 * imb / 256 160 // Coulomb friction: clamped by mu * the ACCUMULATED normal impulse (the physical cone, not per-sweep |j|) 161 let tvx: i64 = rvx - vn * nx / 256 162 let tvy: i64 = rvy - vn * ny / 256 163 let tvz: i64 = rvz - vn * nz / 256 164 let tmag: i64 = ph_isqrt(tvx * tvx + tvy * tvy + tvz * tvz) 165 if tmag > 16 { 166 let mu: i64 = ph_min(a[12], b[12]) 167 var jt: i64 = tmag * 256 / imsum 168 let jmax: i64 = newacc * mu / 256 169 if jt > jmax { jt = jmax } 170 let tx: i64 = tvx * 256 / tmag 171 let ty: i64 = tvy * 256 / tmag 172 let tz: i64 = tvz * 256 / tmag 173 a[4] = a[4] + jt * tx / 256 * ima / 256 174 a[5] = a[5] + jt * ty / 256 * ima / 256 175 a[6] = a[6] + jt * tz / 256 * ima / 256 176 b[4] = b[4] - jt * tx / 256 * imb / 256 177 b[5] = b[5] - jt * ty / 256 * imb / 256 178 b[6] = b[6] - jt * tz / 256 * imb / 256 179 } 180 return 0 181} 182 183// narrowphase dispatch for a pair; generates + resolves the contact if overlapping 184func ph_pair(base: i64, ia: i64, ib: i64, docorr: i64) -> i64 { 185 let a: *i64 = ph_body(base, ia) 186 let b: *i64 = ph_body(base, ib) 187 if a[10] == 0 { if b[10] == 0 { return 0 } } 188 if a[0] == 0 { if b[0] == 0 { 189 // sphere-sphere 190 let dx: i64 = b[1] - a[1] 191 let dy: i64 = b[2] - a[2] 192 let dz: i64 = b[3] - a[3] 193 let rsum: i64 = a[7] + b[7] 194 let d2: i64 = dx * dx + dy * dy + dz * dz 195 if d2 >= rsum * rsum { return 0 } 196 var d: i64 = ph_isqrt(d2) 197 var nx: i64 = 256 198 var ny: i64 = 0 199 var nz: i64 = 0 200 if d > 0 { nx = dx * 256 / d; ny = dy * 256 / d; nz = dz * 256 / d } 201 return ph_contact(base, ia, ib, nx, ny, nz, rsum - d, docorr) 202 } } 203 // box-box AABB (and sphere treated as its bounding box vs box -- exact for axis contacts) 204 let ax0: i64 = a[1] - a[7] 205 let ax1: i64 = a[1] + a[7] 206 let ay0: i64 = a[2] - a[8] 207 let ay1: i64 = a[2] + a[8] 208 let az0: i64 = a[3] - a[9] 209 let az1: i64 = a[3] + a[9] 210 let bx0: i64 = b[1] - b[7] 211 let bx1: i64 = b[1] + b[7] 212 let by0: i64 = b[2] - b[8] 213 let by1: i64 = b[2] + b[8] 214 let bz0: i64 = b[3] - b[9] 215 let bz1: i64 = b[3] + b[9] 216 let ox: i64 = ph_min(ax1, bx1) - ph_imax(ax0, bx0) 217 let oy: i64 = ph_min(ay1, by1) - ph_imax(ay0, by0) 218 let oz: i64 = ph_min(az1, bz1) - ph_imax(az0, bz0) 219 if ox <= 0 { return 0 } 220 if oy <= 0 { return 0 } 221 if oz <= 0 { return 0 } 222 // least-penetration axis, sign from center delta (axis-aligned normal = integer-exact) 223 var nx2: i64 = 0 224 var ny2: i64 = 0 225 var nz2: i64 = 0 226 var pen: i64 = ox 227 if b[1] >= a[1] { nx2 = 256 } else { nx2 = 0 - 256 } 228 if oy < pen { pen = oy; nx2 = 0; nz2 = 0; if b[2] >= a[2] { ny2 = 256 } else { ny2 = 0 - 256 } } 229 if oz < pen { pen = oz; nx2 = 0; ny2 = 0; if b[3] >= a[3] { nz2 = 256 } else { nz2 = 0 - 256 } } 230 return ph_contact(base, ia, ib, nx2, ny2, nz2, pen, docorr) 231} 232func ph_imax(a: i64, b: i64) -> i64 { if a > b { return a } return b } 233 234func ph_step(base: i64) -> i64 { 235 let h: *i64 = ph_hdr(base) 236 let n: i64 = h[0] 237 // integrate (semi-implicit: v then x), dynamic + awake only 238 var i: i64 = 0 239 while i < n { 240 let b: *i64 = ph_body(base, i) 241 if b[10] > 0 { if b[14] == 1 { 242 b[5] = b[5] - PH_G 243 b[1] = b[1] + b[4] / 256 244 b[2] = b[2] + b[5] / 256 245 b[3] = b[3] + b[6] / 256 246 } } 247 i = i + 1 248 } 249 // solve contacts, fixed order, PH_ITERS sweeps (sequential impulses) 250 var it: i64 = 0 251 while it < PH_ITERS { 252 var docorr: i64 = 0 253 if it == 0 { docorr = 1 } 254 var p: i64 = 0 255 while p < n { 256 var q: i64 = p + 1 257 while q < n { 258 ph_pair(base, p, q, docorr) 259 q = q + 1 260 } 261 p = p + 1 262 } 263 it = it + 1 264 } 265 // sleeping on POSITION stillness: integer positions FREEZE at rest (|v|<256 moves 0 cells/tick), so a 266 // frozen pos-hash for PH_SLEEPN frames = asleep. Velocity-based stillness failed: solver rounding jitter 267 // (+-tens) around the gravity/impulse balance kept resetting the counter (gate diag: top boxes awake 268 // at |vy|~38 forever). Slot [15] holds the previous position hash. 269 i = 0 270 while i < n { 271 let b2: *i64 = ph_body(base, i) 272 if b2[10] > 0 { 273 let posh: i64 = b2[1] * 31 + b2[2] * 7 + b2[3] 274 if posh == b2[15] { b2[13] = b2[13] + 1 } else { b2[13] = 0 } 275 b2[15] = posh 276 if b2[13] >= PH_SLEEPN { b2[14] = 0; b2[4] = 0; b2[5] = 0; b2[6] = 0 } 277 } 278 i = i + 1 279 } 280 // warm-start bookkeeping: this tick's accumulated impulses become next tick's pre-load; ACC zeroes so 281 // pairs that separate carry NO stale impulse. 282 let ac2: *i64 = ph_acc(base) 283 let wm2: *i64 = ph_warm(base) 284 let tg2: *i64 = ph_tgt(base) 285 var wk: i64 = 0 286 while wk < PH_MAXB * PH_MAXB { 287 wm2[wk] = ac2[wk] 288 ac2[wk] = 0 289 tg2[wk] = 0 290 wk = wk + 1 291 } 292 h[1] = h[1] + 1 293 return 0 294}