reze-engine 0.31.2 → 0.32.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,905 +1,979 @@
1
- // 6DOF spring + contact constraint solver. Sequential-impulse projected
2
- // Gauss-Seidel: per axis, target a relative velocity (limit correction +
3
- // spring), apply the impulse needed to reach it. Friction is two Coulomb
4
- // rows per contact, normal is push-only.
5
- //
6
- // Two passes per substep:
7
- // 1. SETUP — for each constraint and contact, compute every quantity that
8
- // doesn't depend on lv/av (world axes, lever arms, Jacobian denominators,
9
- // target velocities, friction tangent bases, restitution reference).
10
- // These are constant during solve since pos/ori/inertia don't change.
11
- // 2. ITERATE — `iterations` passes that read the cache and apply impulses
12
- // based on the current lv/av. ~2× faster than recomputing per iter.
13
-
14
- import { Mat4 } from "../math"
15
- import type { RigidBodyStore } from "./body"
16
- import type { SixDofSpringConstraint } from "./constraint"
17
- import { STOP_ERP } from "./constraint"
18
- import type { Contact, ContactPool } from "./contact"
19
-
20
- const BOUNCE_THRESHOLD = 2.0
21
-
22
- // Ceilings on limit-correction velocity. In normal operation limit errors are
23
- // tiny; a large error only appears after a discontinuity (teleport, stall,
24
- // deep penetration), and feeding err·ERP/dt to the solver unclamped then
25
- // injects explosion-scale impulses into the chain.
26
- const MAX_LINEAR_CORRECTION_VEL = 120 // units/s
27
- const MAX_ANGULAR_CORRECTION_VEL = 30 // rad/s
28
-
29
- // Bullet's limit-motor softness defaults (0.7 translational, 0.5 rotational):
30
- // scale each iteration's limit impulse so the stop engages progressively
31
- // instead of as a hard velocity snap.
32
- const LIMIT_SOFTNESS_LINEAR = 0.7
33
- const LIMIT_SOFTNESS_ANGULAR = 0.5
34
-
35
- // Spring rows are IMPLICIT spring-dampers (Spring2/ODE-style soft
36
- // constraints): each axis solves
37
- // relVel⁺ + (k/γ)·err + s·λ = 0, γ = c + h·k, s = 1/(h·γ)
38
- // which is the backward-Euler update of that axis's spring-damper —
39
- // unconditionally stable for ANY authored k (no deadbeat clamp, no force
40
- // clamp). The previous clamped velocity-drive could inject velocity far from
41
- // equilibrium but not absorb it near equilibrium (its clamp shrank with the
42
- // error), so resting chains rang forever — the "static dress slowly boils"
43
- // bug. c is derived per row from a fixed damping ratio against the row's
44
- // effective mass: c = 2ζ√(k·m_eff).
45
- const SPRING_DAMPING_ZETA = 0.7
46
-
47
- // ERP scale for loop-closing constraints (see buildConstraints: joints that
48
- // close a cycle in the joint graph, e.g. the horizontal ring welds of
49
- // cross-linked skirt lattices). A loop over-determines positions — when
50
- // contacts push the lattice, the ring's errors cannot all reach zero, and
51
- // full-rate corrections chase each other around the cycle as violent
52
- // chatter. Loop edges keep shape at a fraction of the correction rate while
53
- // the spanning-tree chains stay stiff.
54
- const LOOP_ERP_SCALE = 1.0
55
- // Loop-edge LOCKED axes are converted to force-clamped springs instead of
56
- // weld rows: an equality row on a cycle fights the other cycle edges at any
57
- // ERP (the velocity system is over-determined too). A spring bounded by its
58
- // real force k·|err|·dt holds the ring's shape elastically without fighting.
59
- const LOOP_SPRING_K = 900
60
- // Angular limit violations below this switch to per-axis euler rows; above
61
- // it, the single geodesic row takes over (see setupConstraint).
62
- const GEODESIC_THRESHOLD = 0.5 // rad
63
-
64
- // Module-level scratch (no per-iter allocations).
65
- const _TA = new Float32Array(16)
66
- const _TB = new Float32Array(16)
67
- const _bodyMatA = new Float32Array(16)
68
- const _bodyMatB = new Float32Array(16)
69
- const _angDiffScratch = new Float32Array(3)
70
- const _quatScratchA = new Float32Array(4)
71
- const _quatScratchB = new Float32Array(4)
72
-
73
- export function solveConstraints(
74
- store: RigidBodyStore,
75
- constraints: SixDofSpringConstraint[],
76
- contacts: ContactPool,
77
- dt: number,
78
- iterations: number,
79
- ): void {
80
- if (dt <= 0) return
81
- if (constraints.length === 0 && contacts.count === 0) return
82
-
83
- const invDt = 1 / dt
84
- const lv = store.linearVelocities
85
- const av = store.angularVelocities
86
- const invMass = store.invMass
87
-
88
- // World-space inverse inertia tensors for this substep's poses.
89
- store.updateInvInertiaWorld()
90
- const W = store.invInertiaWorld
91
-
92
- for (let c = 0; c < constraints.length; c++) {
93
- setupConstraint(constraints[c], store, dt, invDt)
94
- }
95
- for (let ci = 0; ci < contacts.count; ci++) {
96
- setupContactRow(contacts.get(ci), lv, av, invMass, W)
97
- }
98
-
99
- for (let iter = 0; iter < iterations; iter++) {
100
- for (let c = 0; c < constraints.length; c++) {
101
- iterateConstraint(constraints[c], lv, av, invMass)
102
- }
103
- for (let ci = 0; ci < contacts.count; ci++) {
104
- iterateContactRow(contacts.get(ci), lv, av, invMass)
105
- }
106
- }
107
- }
108
-
109
- // SETUP: compute everything that doesn't depend on velocities. Caller
110
- // guarantees pos/ori don't change between this and the iter loop.
111
- function setupConstraint(
112
- con: SixDofSpringConstraint,
113
- store: RigidBodyStore,
114
- dt: number,
115
- invDt: number,
116
- ): void {
117
- const a = con.bodyA
118
- const b = con.bodyB
119
- const imA = store.invMass[a]
120
- const imB = store.invMass[b]
121
- const W = store.invInertiaWorld
122
- const a9 = a * 9
123
- const b9 = b * 9
124
-
125
- con.cacheSkip = imA === 0 && imB === 0
126
- if (con.cacheSkip) return
127
-
128
- const erpScale = con.isLoop ? LOOP_ERP_SCALE : 1.0
129
-
130
- buildBodyMat(store, a, _bodyMatA)
131
- buildBodyMat(store, b, _bodyMatB)
132
- Mat4.multiplyArrays(_bodyMatA, 0, con.frameA, 0, _TA, 0)
133
- Mat4.multiplyArrays(_bodyMatB, 0, con.frameB, 0, _TB, 0)
134
-
135
- // Per-body pivots at each body's own joint-frame origin (Spring2-style).
136
- // Bullet 2.7x's shared mass-weighted anchor (m_AnchorPos) degenerates when
137
- // the joint is violated by a large distance: the midpoint sits far from
138
- // both bodies, the lever arms grow with the separation, the Jacobian
139
- // denominator blows up as err²·invInertia, and the row applies torque
140
- // instead of closing velocity — the joint "breaks" and the error runs
141
- // away. Per-body pivots keep the levers bounded by the frame offsets, so
142
- // the row stays effective no matter how large the violation is.
143
- const pos = store.positions
144
- const ai = a * 3
145
- const bi = b * 3
146
- const rAx = _TA[12] - pos[ai + 0]
147
- const rAy = _TA[13] - pos[ai + 1]
148
- const rAz = _TA[14] - pos[ai + 2]
149
- const rBx = _TB[12] - pos[bi + 0]
150
- const rBy = _TB[13] - pos[bi + 1]
151
- const rBz = _TB[14] - pos[bi + 2]
152
- const lA = con.cacheLeverA
153
- const lB = con.cacheLeverB
154
- lA[0] = rAx; lA[1] = rAy; lA[2] = rAz
155
- lB[0] = rBx; lB[1] = rBy; lB[2] = rBz
156
-
157
- // linearDiff = TA.basis^T · (TB.origin TA.origin); axes = TA columns 0/1/2.
158
- const dxw = _TB[12] - _TA[12]
159
- const dyw = _TB[13] - _TA[13]
160
- const dzw = _TB[14] - _TA[14]
161
- const linDiff0 = _TA[0] * dxw + _TA[1] * dyw + _TA[2] * dzw
162
- const linDiff1 = _TA[4] * dxw + _TA[5] * dyw + _TA[6] * dzw
163
- const linDiff2 = _TA[8] * dxw + _TA[9] * dyw + _TA[10] * dzw
164
-
165
- const axes = con.cacheLinAxes
166
- const cA = con.cacheLinCrossA
167
- const cB = con.cacheLinCrossB
168
- const jac = con.cacheLinJacInv
169
- const tgt = con.cacheLinTargetVel
170
- const act = con.cacheLinActive
171
-
172
- for (let i = 0; i < 3; i++) {
173
- const o = i * 3
174
- const axx = i === 0 ? _TA[0] : i === 1 ? _TA[4] : _TA[8]
175
- const axy = i === 0 ? _TA[1] : i === 1 ? _TA[5] : _TA[9]
176
- const axz = i === 0 ? _TA[2] : i === 1 ? _TA[6] : _TA[10]
177
- axes[o + 0] = axx
178
- axes[o + 1] = axy
179
- axes[o + 2] = axz
180
-
181
- const cAx = rAy * axz - rAz * axy
182
- const cAy = rAz * axx - rAx * axz
183
- const cAz = rAx * axy - rAy * axx
184
- const cBx = rBy * axz - rBz * axy
185
- const cBy = rBz * axx - rBx * axz
186
- const cBz = rBx * axy - rBy * axx
187
- // Tensor-multiplied lever crosses: cache I⁻¹·(r×ax) for application;
188
- // denominator = (r×ax)ᵀ·I⁻¹·(r×ax).
189
- const wAx = W[a9 + 0] * cAx + W[a9 + 1] * cAy + W[a9 + 2] * cAz
190
- const wAy = W[a9 + 3] * cAx + W[a9 + 4] * cAy + W[a9 + 5] * cAz
191
- const wAz = W[a9 + 6] * cAx + W[a9 + 7] * cAy + W[a9 + 8] * cAz
192
- const wBx = W[b9 + 0] * cBx + W[b9 + 1] * cBy + W[b9 + 2] * cBz
193
- const wBy = W[b9 + 3] * cBx + W[b9 + 4] * cBy + W[b9 + 5] * cBz
194
- const wBz = W[b9 + 6] * cBx + W[b9 + 7] * cBy + W[b9 + 8] * cBz
195
- cA[o + 0] = wAx; cA[o + 1] = wAy; cA[o + 2] = wAz
196
- cB[o + 0] = wBx; cB[o + 1] = wBy; cB[o + 2] = wBz
197
-
198
- const denom = imA + imB +
199
- (cAx * wAx + cAy * wAy + cAz * wAz) +
200
- (cBx * wBx + cBy * wBy + cBz * wBz)
201
- jac[i] = denom > 0 ? 1 / denom : 0
202
-
203
- const lo = con.linearMin[i]
204
- const hi = con.linearMax[i]
205
- const curr = i === 0 ? linDiff0 : i === 1 ? linDiff1 : linDiff2
206
- // active: 1 = bilateral equality (locked axis a joint, always on),
207
- // 2 = unilateral stop (ranged axis in violation).
208
- let target = 0
209
- let active = 0
210
- if (lo <= hi) {
211
- let err = 0
212
- if (curr < lo) err = curr - lo
213
- else if (curr > hi) err = curr - hi
214
- if (lo === hi) active = 1
215
- else if (err !== 0) active = 2
216
- if (err !== 0) {
217
- target = -err * STOP_ERP * erpScale * invDt
218
- if (target > MAX_LINEAR_CORRECTION_VEL) target = MAX_LINEAR_CORRECTION_VEL
219
- else if (target < -MAX_LINEAR_CORRECTION_VEL) target = -MAX_LINEAR_CORRECTION_VEL
220
- }
221
- }
222
- tgt[i] = target
223
- act[i] = denom > 0 ? active : 0
224
- con.cacheLinLimitImp[i] = 0
225
- // A spring on a locked axis is redundant — the bilateral limit row
226
- // already welds the DOF, and driving it twice overshoots every
227
- // iteration (PMX rigs routinely put k=100000 springs on locked axes,
228
- // which turned welded weight-bodies into energy pumps).
229
- if (con.springEnabled[i] && denom > 0 && lo !== hi) {
230
- // Implicit spring-damper (see SPRING_DAMPING_ZETA). Stored per axis:
231
- // cacheLinSpringTarget = −(k/γ)·err (the bias, in velocity units) and
232
- // cacheLinSpringMaxImp = s (the CFM softness — NOT a clamp anymore).
233
- const k = con.springStiffness[i]
234
- const serr = curr - con.equilibriumPoint[i]
235
- const meff = 1 / denom
236
- const c = 2 * SPRING_DAMPING_ZETA * Math.sqrt(k * meff)
237
- const gamma = c + dt * k
238
- con.cacheLinSpringTarget[i] = -(k / gamma) * serr
239
- con.cacheLinSpringMaxImp[i] = 1 / (dt * gamma)
240
- con.cacheLinSpringImp[i] = 0
241
- con.cacheLinSpringActive[i] = 1
242
- } else if (!(lo === hi && con.isLoop)) {
243
- con.cacheLinSpringActive[i] = 0
244
- }
245
- }
246
-
247
- // Angular: TA^T · TB → Euler XYZ; axes from TA.col2 × TB.col0.
248
- const r00 = _TA[0]*_TB[0] + _TA[1]*_TB[1] + _TA[2]*_TB[2]
249
- const r01 = _TA[0]*_TB[4] + _TA[1]*_TB[5] + _TA[2]*_TB[6]
250
- const r10 = _TA[4]*_TB[0] + _TA[5]*_TB[1] + _TA[6]*_TB[2]
251
- const r11 = _TA[4]*_TB[4] + _TA[5]*_TB[5] + _TA[6]*_TB[6]
252
- const r20 = _TA[8]*_TB[0] + _TA[9]*_TB[1] + _TA[10]*_TB[2]
253
- const r21 = _TA[8]*_TB[4] + _TA[9]*_TB[5] + _TA[10]*_TB[6]
254
- const r22 = _TA[8]*_TB[8] + _TA[9]*_TB[9] + _TA[10]*_TB[10]
255
- matrixToEulerXYZ(r00, r01, r10, r11, r20, r21, r22, _angDiffScratch)
256
-
257
- const a2x = _TA[8], a2y = _TA[9], a2z = _TA[10]
258
- const b0x = _TB[0], b0y = _TB[1], b0z = _TB[2]
259
- let yx = a2y * b0z - a2z * b0y
260
- let yy = a2z * b0x - a2x * b0z
261
- let yz = a2x * b0y - a2y * b0x
262
- let l = Math.hypot(yx, yy, yz)
263
- if (l > 1e-8) { const inv = 1/l; yx*=inv; yy*=inv; yz*=inv }
264
- let xx = yy * a2z - yz * a2y
265
- let xy = yz * a2x - yx * a2z
266
- let xz = yx * a2y - yy * a2x
267
- l = Math.hypot(xx, xy, xz)
268
- if (l > 1e-8) { const inv = 1/l; xx*=inv; xy*=inv; xz*=inv }
269
- let zx = b0y * yz - b0z * yy
270
- let zy = b0z * yx - b0x * yz
271
- let zz = b0x * yy - b0y * yx
272
- l = Math.hypot(zx, zy, zz)
273
- if (l > 1e-8) { const inv = 1/l; zx*=inv; zy*=inv; zz*=inv }
274
-
275
- const angAxes = con.cacheAngAxes
276
- angAxes[0] = xx; angAxes[1] = xy; angAxes[2] = xz
277
- angAxes[3] = yx; angAxes[4] = yy; angAxes[5] = yz
278
- angAxes[6] = zx; angAxes[7] = zy; angAxes[8] = zz
279
-
280
- // Per-axis angular Jacobians with the full tensors: cache I⁻¹·axis per
281
- // body plus 1/(axᵀ(I⁻¹A+I⁻¹B)ax) per axis.
282
- const angJac = con.cacheAngJacInv
283
- const angWAs = con.cacheAngWA
284
- const angWBs = con.cacheAngWB
285
- for (let i = 0; i < 3; i++) {
286
- const o = i * 3
287
- const axx = angAxes[o + 0], axy = angAxes[o + 1], axz = angAxes[o + 2]
288
- const wAx = W[a9 + 0] * axx + W[a9 + 1] * axy + W[a9 + 2] * axz
289
- const wAy = W[a9 + 3] * axx + W[a9 + 4] * axy + W[a9 + 5] * axz
290
- const wAz = W[a9 + 6] * axx + W[a9 + 7] * axy + W[a9 + 8] * axz
291
- const wBx = W[b9 + 0] * axx + W[b9 + 1] * axy + W[b9 + 2] * axz
292
- const wBy = W[b9 + 3] * axx + W[b9 + 4] * axy + W[b9 + 5] * axz
293
- const wBz = W[b9 + 6] * axx + W[b9 + 7] * axy + W[b9 + 8] * axz
294
- angWAs[o + 0] = wAx; angWAs[o + 1] = wAy; angWAs[o + 2] = wAz
295
- angWBs[o + 0] = wBx; angWBs[o + 1] = wBy; angWBs[o + 2] = wBz
296
- const denom = axx * (wAx + wBx) + axy * (wAy + wBy) + axz * (wAz + wBz)
297
- angJac[i] = denom > 0 ? 1 / denom : 0
298
- }
299
-
300
- // Per-axis rows carry only the springs. Sign flip vs linear:
301
- // d(angDiff)/dt = −(ω_B ω_A)·ax.
302
- const angTgt = con.cacheAngTargetVel
303
- const angAct = con.cacheAngActive
304
- for (let i = 0; i < 3; i++) {
305
- const idx = i + 3
306
- // Springs on locked axes are skipped the limit row welds those, and
307
- // double-driving a DOF overshoots every iteration (see the linear loop).
308
- if (con.springEnabled[idx] && angJac[i] > 0 && con.angularMin[i] !== con.angularMax[i]) {
309
- // Implicit spring-damper, angular flavor. angJac[i] = 1/denom IS the
310
- // row's effective inertia. Sign flip vs linear: d(err)/dt = −relAv, so
311
- // the bias is positive for positive error. cacheAngSpringMaxImp holds
312
- // the CFM softness s (not a clamp).
313
- const k = con.springStiffness[idx]
314
- const serr = _angDiffScratch[i] - con.equilibriumPoint[idx]
315
- const c = 2 * SPRING_DAMPING_ZETA * Math.sqrt(k * angJac[i])
316
- const gamma = c + dt * k
317
- angTgt[i] = (k / gamma) * serr
318
- con.cacheAngSpringMaxImp[i] = 1 / (dt * gamma)
319
- angAct[i] = 1
320
- } else {
321
- angTgt[i] = 0
322
- angAct[i] = 0
323
- }
324
- con.cacheAngSpringImp[i] = 0
325
- }
326
-
327
- // Angular limit handling is hybrid. Small violations (the resting-cloth
328
- // regime) use per-axis euler rows they converge cleanly and keep resting
329
- // cloth dead still. Large violations switch to a single geodesic row toward
330
- // the euler-clamped target: per-axis euler rows (the Bullet-2.7x approach
331
- // this port used) become geometrically inconsistent for large errors — near
332
- // the asin singularity they chase phantom errors and pump angular velocity
333
- // into the chain instead of converging.
334
- con.cacheAngLimActive = 0
335
- con.cacheAngPAActive[0] = 0
336
- con.cacheAngPAActive[1] = 0
337
- con.cacheAngPAActive[2] = 0
338
- if (imA > 0 || imB > 0) {
339
- const ex = _angDiffScratch[0], ey = _angDiffScratch[1], ez = _angDiffScratch[2]
340
- // Free axes (min > max) follow the current angle, i.e. no correction.
341
- let tx = ex, ty = ey, tz = ez
342
- if (con.angularMin[0] <= con.angularMax[0]) tx = ex < con.angularMin[0] ? con.angularMin[0] : ex > con.angularMax[0] ? con.angularMax[0] : ex
343
- if (con.angularMin[1] <= con.angularMax[1]) ty = ey < con.angularMin[1] ? con.angularMin[1] : ey > con.angularMax[1] ? con.angularMax[1] : ey
344
- if (con.angularMin[2] <= con.angularMax[2]) tz = ez < con.angularMin[2] ? con.angularMin[2] : ez > con.angularMax[2] ? con.angularMax[2] : ez
345
- const errX = ex - tx, errY = ey - ty, errZ = ez - tz
346
- const maxErr = Math.max(Math.abs(errX), Math.abs(errY), Math.abs(errZ))
347
- if (maxErr > 0 && maxErr < GEODESIC_THRESHOLD) {
348
- // Per-axis euler limit rows. Locked axes are bilateral joints; ranged
349
- // axes are unilateral stops (sign-clamped accumulation) — a bilateral
350
- // row on a ranged axis brakes natural recovery every substep and
351
- // pumps energy into swinging cloth, and the pump grows WITH solver
352
- // convergence (more iterations enforce the brake harder).
353
- for (let i = 0; i < 3; i++) {
354
- const err = i === 0 ? errX : i === 1 ? errY : errZ
355
- con.cacheAngPAImp[i] = 0
356
- if (err === 0) {
357
- con.cacheAngPAActive[i] = 0
358
- continue
359
- }
360
- let target = err * STOP_ERP * erpScale * invDt
361
- if (target > MAX_ANGULAR_CORRECTION_VEL) target = MAX_ANGULAR_CORRECTION_VEL
362
- else if (target < -MAX_ANGULAR_CORRECTION_VEL) target = -MAX_ANGULAR_CORRECTION_VEL
363
- con.cacheAngPATarget[i] = target
364
- con.cacheAngPAActive[i] = con.angularMin[i] === con.angularMax[i] ? 1 : 2
365
- }
366
- } else if (maxErr > 0) {
367
- // Bilateral (equality) if any violated axis is locked a locked axis
368
- // is a joint, not a stop. Unilateral otherwise.
369
- const bilateral =
370
- (tx !== ex && con.angularMin[0] === con.angularMax[0]) ||
371
- (ty !== ey && con.angularMin[1] === con.angularMax[1]) ||
372
- (tz !== ez && con.angularMin[2] === con.angularMax[2])
373
- // The decomposition above satisfies R_rel^T = Rx(x)·Ry(y)·Rz(z), so
374
- // u = qx·qy·qz is conj(q_rel) and the error rotation (current →
375
- // clamped target, expressed in TA's frame) is q_E = conj(u_t) ⊗ u.
376
- eulerXYZQuatInto(ex, ey, ez, _quatScratchA)
377
- eulerXYZQuatInto(tx, ty, tz, _quatScratchB)
378
- const ux = _quatScratchA[0], uy = _quatScratchA[1], uz = _quatScratchA[2], uw = _quatScratchA[3]
379
- const vx = _quatScratchB[0], vy = _quatScratchB[1], vz = _quatScratchB[2], vw = _quatScratchB[3]
380
- // q_E = conj(v) ⊗ u
381
- let qex = vw * ux - vx * uw - vy * uz + vz * uy
382
- let qey = vw * uy + vx * uz - vy * uw - vz * ux
383
- let qez = vw * uz - vx * uy + vy * ux - vz * uw
384
- let qew = vw * uw + vx * ux + vy * uy + vz * uz
385
- if (qew < 0) { qex = -qex; qey = -qey; qez = -qez; qew = -qew }
386
- const sinHalf = Math.sqrt(qex * qex + qey * qey + qez * qez)
387
- if (sinHalf > 1e-6) {
388
- const angle = 2 * Math.atan2(sinHalf, qew)
389
- const invS = 1 / sinHalf
390
- const axx = qex * invS, axy = qey * invS, axz = qez * invS
391
- // Axis lives in TA's frame; TA's basis columns map it to world.
392
- const lim = con.cacheAngLimAxis
393
- lim[0] = _TA[0] * axx + _TA[4] * axy + _TA[8] * axz
394
- lim[1] = _TA[1] * axx + _TA[5] * axy + _TA[9] * axz
395
- lim[2] = _TA[2] * axx + _TA[6] * axy + _TA[10] * axz
396
- const gWA = con.cacheAngLimWA
397
- const gWB = con.cacheAngLimWB
398
- gWA[0] = W[a9 + 0] * lim[0] + W[a9 + 1] * lim[1] + W[a9 + 2] * lim[2]
399
- gWA[1] = W[a9 + 3] * lim[0] + W[a9 + 4] * lim[1] + W[a9 + 5] * lim[2]
400
- gWA[2] = W[a9 + 6] * lim[0] + W[a9 + 7] * lim[1] + W[a9 + 8] * lim[2]
401
- gWB[0] = W[b9 + 0] * lim[0] + W[b9 + 1] * lim[1] + W[b9 + 2] * lim[2]
402
- gWB[1] = W[b9 + 3] * lim[0] + W[b9 + 4] * lim[1] + W[b9 + 5] * lim[2]
403
- gWB[2] = W[b9 + 6] * lim[0] + W[b9 + 7] * lim[1] + W[b9 + 8] * lim[2]
404
- const gDenom = lim[0] * (gWA[0] + gWB[0]) + lim[1] * (gWA[1] + gWB[1]) + lim[2] * (gWA[2] + gWB[2])
405
- con.cacheAngLimJacInv = gDenom > 0 ? 1 / gDenom : 0
406
- let target = angle * STOP_ERP * erpScale * invDt
407
- if (target > MAX_ANGULAR_CORRECTION_VEL) target = MAX_ANGULAR_CORRECTION_VEL
408
- con.cacheAngLimTarget = target
409
- con.cacheAngLimActive = bilateral ? 1 : 2
410
- }
411
- }
412
- }
413
- con.cacheAngLimImp = 0
414
- }
415
-
416
- // ITER: read cache, compute relVel from current lv/av, apply impulse.
417
- function iterateConstraint(
418
- con: SixDofSpringConstraint,
419
- lv: Float32Array,
420
- av: Float32Array,
421
- invMass: Float32Array,
422
- ): void {
423
- if (con.cacheSkip) return
424
- const a = con.bodyA
425
- const b = con.bodyB
426
- const ai = a * 3
427
- const bi = b * 3
428
- const imA = invMass[a]
429
- const imB = invMass[b]
430
-
431
- // Linear axes relVel at the offset point: v_pivot = v_CG + ω × r.
432
- const lA = con.cacheLeverA
433
- const lB = con.cacheLeverB
434
- const rAx = lA[0], rAy = lA[1], rAz = lA[2]
435
- const rBx = lB[0], rBy = lB[1], rBz = lB[2]
436
- const axes = con.cacheLinAxes
437
- const cA = con.cacheLinCrossA
438
- const cB = con.cacheLinCrossB
439
- const jac = con.cacheLinJacInv
440
- const tgt = con.cacheLinTargetVel
441
- const act = con.cacheLinActive
442
-
443
- const vAx = lv[ai + 0] + av[ai + 1] * rAz - av[ai + 2] * rAy
444
- const vAy = lv[ai + 1] + av[ai + 2] * rAx - av[ai + 0] * rAz
445
- const vAz = lv[ai + 2] + av[ai + 0] * rAy - av[ai + 1] * rAx
446
- const vBx = lv[bi + 0] + av[bi + 1] * rBz - av[bi + 2] * rBy
447
- const vBy = lv[bi + 1] + av[bi + 2] * rBx - av[bi + 0] * rBz
448
- const vBz = lv[bi + 2] + av[bi + 0] * rBy - av[bi + 1] * rBx
449
- const dvx = vBx - vAx
450
- const dvy = vBy - vAy
451
- const dvz = vBz - vAz
452
-
453
- const sprAct = con.cacheLinSpringActive
454
- const sprTgt = con.cacheLinSpringTarget
455
- const sprMax = con.cacheLinSpringMaxImp
456
- const sprImp = con.cacheLinSpringImp
457
- const limImp = con.cacheLinLimitImp
458
- for (let i = 0; i < 3; i++) {
459
- if (!act[i] && !sprAct[i]) continue
460
- const o = i * 3
461
- const axx = axes[o + 0], axy = axes[o + 1], axz = axes[o + 2]
462
- const relVel = dvx * axx + dvy * axy + dvz * axz
463
- let j = 0
464
-
465
- // Limit row. Locked axes (act 1) are bilateral equality joints; ranged
466
- // axes in violation (act 2) are unilateral stops accumulated impulse
467
- // clamped to the corrective sign, so the stop pushes back into range but
468
- // never pulls deeper or brakes natural recovery (a bilateral stop acts
469
- // as a motor and pumps energy into swinging cloth).
470
- if (act[i]) {
471
- const target = tgt[i]
472
- let dImp = LIMIT_SOFTNESS_LINEAR * (target - relVel) * jac[i]
473
- if (act[i] === 2) {
474
- const old = limImp[i]
475
- let next = old + dImp
476
- if (target > 0 ? next < 0 : next > 0) next = 0
477
- dImp = next - old
478
- limImp[i] = next
479
- }
480
- j += dImp
481
- }
482
-
483
- // Implicit spring-damper row (soft constraint, see setup): CFM-softened
484
- // with accumulated λ, dissipative by construction. relVel is refreshed
485
- // with the limit impulse applied just above (j·denom = j / jac) — driving
486
- // the spring off the stale value double-corrects the DOF.
487
- if (sprAct[i]) {
488
- const relVelNow = j !== 0 ? relVel + j / jac[i] : relVel
489
- const s = sprMax[i] // CFM softness
490
- const dImp = (sprTgt[i] - relVelNow - s * sprImp[i]) / (1 / jac[i] + s)
491
- sprImp[i] += dImp
492
- j += dImp
493
- }
494
-
495
- if (j === 0) continue
496
- if (imA > 0) {
497
- lv[ai + 0] -= j * imA * axx
498
- lv[ai + 1] -= j * imA * axy
499
- lv[ai + 2] -= j * imA * axz
500
- av[ai + 0] -= j * cA[o + 0]
501
- av[ai + 1] -= j * cA[o + 1]
502
- av[ai + 2] -= j * cA[o + 2]
503
- }
504
- if (imB > 0) {
505
- lv[bi + 0] += j * imB * axx
506
- lv[bi + 1] += j * imB * axy
507
- lv[bi + 2] += j * imB * axz
508
- av[bi + 0] += j * cB[o + 0]
509
- av[bi + 1] += j * cB[o + 1]
510
- av[bi + 2] += j * cB[o + 2]
511
- }
512
- }
513
-
514
- // Angular axes — relAv = ω_B ω_A.
515
- const angAxes = con.cacheAngAxes
516
- const angJac = con.cacheAngJacInv
517
- const angWAs = con.cacheAngWA
518
- const angWBs = con.cacheAngWB
519
- const angTgt = con.cacheAngTargetVel
520
- const angAct = con.cacheAngActive
521
- const dax = av[bi + 0] - av[ai + 0]
522
- const day = av[bi + 1] - av[ai + 1]
523
- const daz = av[bi + 2] - av[ai + 2]
524
- const angSprMax = con.cacheAngSpringMaxImp
525
- const angSprImp = con.cacheAngSpringImp
526
- for (let i = 0; i < 3; i++) {
527
- if (!angAct[i]) continue
528
- const o = i * 3
529
- const axx = angAxes[o + 0], axy = angAxes[o + 1], axz = angAxes[o + 2]
530
- const relAv = dax * axx + day * axy + daz * axz
531
- // Implicit spring-damper row (soft constraint, see setup).
532
- const s = angSprMax[i] // CFM softness
533
- const j = (angTgt[i] - relAv - s * angSprImp[i]) / (1 / angJac[i] + s)
534
- angSprImp[i] += j
535
- if (j === 0) continue
536
- if (imA > 0) {
537
- av[ai + 0] -= j * angWAs[o + 0]
538
- av[ai + 1] -= j * angWAs[o + 1]
539
- av[ai + 2] -= j * angWAs[o + 2]
540
- }
541
- if (imB > 0) {
542
- av[bi + 0] += j * angWBs[o + 0]
543
- av[bi + 1] += j * angWBs[o + 1]
544
- av[bi + 2] += j * angWBs[o + 2]
545
- }
546
- }
547
-
548
- // Per-axis angular limit rows (small-violation regime), on the derived
549
- // euler axes. Sign convention matches the springs: positive target reduces
550
- // positive error via d(angDiff)/dt = −(ω_B ω_A)·ax.
551
- const paAct = con.cacheAngPAActive
552
- if (paAct[0] || paAct[1] || paAct[2]) {
553
- const paTgt = con.cacheAngPATarget
554
- const paImp = con.cacheAngPAImp
555
- for (let i = 0; i < 3; i++) {
556
- if (!paAct[i]) continue
557
- const o = i * 3
558
- const axx = angAxes[o + 0], axy = angAxes[o + 1], axz = angAxes[o + 2]
559
- const relAv =
560
- (av[bi + 0] - av[ai + 0]) * axx +
561
- (av[bi + 1] - av[ai + 1]) * axy +
562
- (av[bi + 2] - av[ai + 2]) * axz
563
- const target = paTgt[i]
564
- // Locked axes (act 1) are welds full gain, like the 0.16.3 fold;
565
- // softness only tempers the unilateral stops.
566
- const soft = paAct[i] === 2 ? LIMIT_SOFTNESS_ANGULAR : 1.0
567
- let j = soft * (target - relAv) * angJac[i]
568
- if (paAct[i] === 2) {
569
- const old = paImp[i]
570
- let next = old + j
571
- if (target > 0 ? next < 0 : next > 0) next = 0
572
- j = next - old
573
- paImp[i] = next
574
- }
575
- if (j === 0) continue
576
- if (imA > 0) {
577
- av[ai + 0] -= j * angWAs[o + 0]
578
- av[ai + 1] -= j * angWAs[o + 1]
579
- av[ai + 2] -= j * angWAs[o + 2]
580
- }
581
- if (imB > 0) {
582
- av[bi + 0] += j * angWBs[o + 0]
583
- av[bi + 1] += j * angWBs[o + 1]
584
- av[bi + 2] += j * angWBs[o + 2]
585
- }
586
- }
587
- }
588
-
589
- // Geodesic limit row: drive (ω_B − ω_A)·axis toward the correction target.
590
- // Unilateral the accumulated impulse can only push toward the legal
591
- // region (target is always ≥ 0 along the corrective axis).
592
- if (con.cacheAngLimActive) {
593
- const lim = con.cacheAngLimAxis
594
- const nx = lim[0], ny = lim[1], nz = lim[2]
595
- // Re-read relAv the spring rows above may have changed av.
596
- const relAv =
597
- (av[bi + 0] - av[ai + 0]) * nx +
598
- (av[bi + 1] - av[ai + 1]) * ny +
599
- (av[bi + 2] - av[ai + 2]) * nz
600
- let j = LIMIT_SOFTNESS_ANGULAR * (con.cacheAngLimTarget - relAv) * con.cacheAngLimJacInv
601
- if (con.cacheAngLimActive === 2) {
602
- const old = con.cacheAngLimImp
603
- let next = old + j
604
- if (next < 0) next = 0
605
- j = next - old
606
- con.cacheAngLimImp = next
607
- }
608
- if (j !== 0) {
609
- const gWA = con.cacheAngLimWA
610
- const gWB = con.cacheAngLimWB
611
- if (imA > 0) {
612
- av[ai + 0] -= j * gWA[0]
613
- av[ai + 1] -= j * gWA[1]
614
- av[ai + 2] -= j * gWA[2]
615
- }
616
- if (imB > 0) {
617
- av[bi + 0] += j * gWB[0]
618
- av[bi + 1] += j * gWB[1]
619
- av[bi + 2] += j * gWB[2]
620
- }
621
- }
622
- }
623
- }
624
-
625
- // SETUP: pre-compute Jacobians, friction basis, and the bounce reference
626
- // from the *initial* closing velocity (Bullet's pattern captures restitution
627
- // before iter 1 zeroes out the approach).
628
- function setupContactRow(
629
- c: Contact,
630
- lv: Float32Array,
631
- av: Float32Array,
632
- invMass: Float32Array,
633
- W: Float32Array,
634
- ): void {
635
- const ai = c.bodyA * 3
636
- const bi = c.bodyB * 3
637
- const a9 = c.bodyA * 9
638
- const b9 = c.bodyB * 9
639
- const imA = invMass[c.bodyA]
640
- const imB = invMass[c.bodyB]
641
- const rAx = c.rAx, rAy = c.rAy, rAz = c.rAz
642
- const rBx = c.rBx, rBy = c.rBy, rBz = c.rBz
643
- const nx = c.nx, ny = c.ny, nz = c.nz
644
-
645
- // Normal Jacobian. Cached vectors are tensor-multiplied I⁻¹·(r×n).
646
- const cAxN = rAy * nz - rAz * ny
647
- const cAyN = rAz * nx - rAx * nz
648
- const cAzN = rAx * ny - rAy * nx
649
- const cBxN = rBy * nz - rBz * ny
650
- const cByN = rBz * nx - rBx * nz
651
- const cBzN = rBx * ny - rBy * nx
652
- const wAxN = W[a9 + 0] * cAxN + W[a9 + 1] * cAyN + W[a9 + 2] * cAzN
653
- const wAyN = W[a9 + 3] * cAxN + W[a9 + 4] * cAyN + W[a9 + 5] * cAzN
654
- const wAzN = W[a9 + 6] * cAxN + W[a9 + 7] * cAyN + W[a9 + 8] * cAzN
655
- const wBxN = W[b9 + 0] * cBxN + W[b9 + 1] * cByN + W[b9 + 2] * cBzN
656
- const wByN = W[b9 + 3] * cBxN + W[b9 + 4] * cByN + W[b9 + 5] * cBzN
657
- const wBzN = W[b9 + 6] * cBxN + W[b9 + 7] * cByN + W[b9 + 8] * cBzN
658
- const denomN = imA + imB +
659
- (cAxN * wAxN + cAyN * wAyN + cAzN * wAzN) +
660
- (cBxN * wBxN + cByN * wByN + cBzN * wBzN)
661
- c.cAxN = wAxN; c.cAyN = wAyN; c.cAzN = wAzN
662
- c.cBxN = wBxN; c.cByN = wByN; c.cBzN = wBzN
663
- c.jacInvN = denomN > 0 ? 1 / denomN : 0
664
-
665
- // Restitution reference, captured from initial relVelN.
666
- const vAx = lv[ai + 0] + av[ai + 1] * rAz - av[ai + 2] * rAy
667
- const vAy = lv[ai + 1] + av[ai + 2] * rAx - av[ai + 0] * rAz
668
- const vAz = lv[ai + 2] + av[ai + 0] * rAy - av[ai + 1] * rAx
669
- const vBx = lv[bi + 0] + av[bi + 1] * rBz - av[bi + 2] * rBy
670
- const vBy = lv[bi + 1] + av[bi + 2] * rBx - av[bi + 0] * rBz
671
- const vBz = lv[bi + 2] + av[bi + 0] * rBy - av[bi + 1] * rBx
672
- const relVelN0 = (vBx - vAx) * nx + (vBy - vAy) * ny + (vBz - vAz) * nz
673
- c.bounceVel = c.restitution > 0 && relVelN0 < -BOUNCE_THRESHOLD
674
- ? -c.restitution * relVelN0
675
- : 0
676
-
677
- // Friction tangent basis. Pick the axis least aligned with n.
678
- let t1x: number, t1y: number, t1z: number
679
- if (Math.abs(nx) < 0.7071) { t1x = 0; t1y = -nz; t1z = ny }
680
- else { t1x = nz; t1y = 0; t1z = -nx }
681
- const tl = Math.hypot(t1x, t1y, t1z)
682
- if (tl > 1e-8) {
683
- const tInv = 1 / tl
684
- t1x *= tInv; t1y *= tInv; t1z *= tInv
685
- } else {
686
- c.jacInvT1 = 0; c.jacInvT2 = 0
687
- return
688
- }
689
- const t2x = ny * t1z - nz * t1y
690
- const t2y = nz * t1x - nx * t1z
691
- const t2z = nx * t1y - ny * t1x
692
- c.t1x = t1x; c.t1y = t1y; c.t1z = t1z
693
- c.t2x = t2x; c.t2y = t2y; c.t2z = t2z
694
-
695
- // Friction Jacobians.
696
- const cAxT1 = rAy * t1z - rAz * t1y
697
- const cAyT1 = rAz * t1x - rAx * t1z
698
- const cAzT1 = rAx * t1y - rAy * t1x
699
- const cBxT1 = rBy * t1z - rBz * t1y
700
- const cByT1 = rBz * t1x - rBx * t1z
701
- const cBzT1 = rBx * t1y - rBy * t1x
702
- const wAxT1 = W[a9 + 0] * cAxT1 + W[a9 + 1] * cAyT1 + W[a9 + 2] * cAzT1
703
- const wAyT1 = W[a9 + 3] * cAxT1 + W[a9 + 4] * cAyT1 + W[a9 + 5] * cAzT1
704
- const wAzT1 = W[a9 + 6] * cAxT1 + W[a9 + 7] * cAyT1 + W[a9 + 8] * cAzT1
705
- const wBxT1 = W[b9 + 0] * cBxT1 + W[b9 + 1] * cByT1 + W[b9 + 2] * cBzT1
706
- const wByT1 = W[b9 + 3] * cBxT1 + W[b9 + 4] * cByT1 + W[b9 + 5] * cBzT1
707
- const wBzT1 = W[b9 + 6] * cBxT1 + W[b9 + 7] * cByT1 + W[b9 + 8] * cBzT1
708
- const denomT1 = imA + imB +
709
- (cAxT1 * wAxT1 + cAyT1 * wAyT1 + cAzT1 * wAzT1) +
710
- (cBxT1 * wBxT1 + cByT1 * wByT1 + cBzT1 * wBzT1)
711
- c.cAxT1 = wAxT1; c.cAyT1 = wAyT1; c.cAzT1 = wAzT1
712
- c.cBxT1 = wBxT1; c.cByT1 = wByT1; c.cBzT1 = wBzT1
713
- c.jacInvT1 = denomT1 > 0 ? 1 / denomT1 : 0
714
-
715
- const cAxT2 = rAy * t2z - rAz * t2y
716
- const cAyT2 = rAz * t2x - rAx * t2z
717
- const cAzT2 = rAx * t2y - rAy * t2x
718
- const cBxT2 = rBy * t2z - rBz * t2y
719
- const cByT2 = rBz * t2x - rBx * t2z
720
- const cBzT2 = rBx * t2y - rBy * t2x
721
- const wAxT2 = W[a9 + 0] * cAxT2 + W[a9 + 1] * cAyT2 + W[a9 + 2] * cAzT2
722
- const wAyT2 = W[a9 + 3] * cAxT2 + W[a9 + 4] * cAyT2 + W[a9 + 5] * cAzT2
723
- const wAzT2 = W[a9 + 6] * cAxT2 + W[a9 + 7] * cAyT2 + W[a9 + 8] * cAzT2
724
- const wBxT2 = W[b9 + 0] * cBxT2 + W[b9 + 1] * cByT2 + W[b9 + 2] * cBzT2
725
- const wByT2 = W[b9 + 3] * cBxT2 + W[b9 + 4] * cByT2 + W[b9 + 5] * cBzT2
726
- const wBzT2 = W[b9 + 6] * cBxT2 + W[b9 + 7] * cByT2 + W[b9 + 8] * cBzT2
727
- const denomT2 = imA + imB +
728
- (cAxT2 * wAxT2 + cAyT2 * wAyT2 + cAzT2 * wAzT2) +
729
- (cBxT2 * wBxT2 + cByT2 * wByT2 + cBzT2 * wBzT2)
730
- c.cAxT2 = wAxT2; c.cAyT2 = wAyT2; c.cAzT2 = wAzT2
731
- c.cBxT2 = wBxT2; c.cByT2 = wByT2; c.cBzT2 = wBzT2
732
- c.jacInvT2 = denomT2 > 0 ? 1 / denomT2 : 0
733
- }
734
-
735
- // ITER: one push-only normal row + two Coulomb friction rows. Friction
736
- // bound depends on the *current* applied normal impulse, so it tightens
737
- // as the normal row converges.
738
- function iterateContactRow(
739
- c: Contact,
740
- lv: Float32Array,
741
- av: Float32Array,
742
- invMass: Float32Array,
743
- ): void {
744
- const imA = invMass[c.bodyA]
745
- const imB = invMass[c.bodyB]
746
- if (imA === 0 && imB === 0) return
747
- const ai = c.bodyA * 3, bi = c.bodyB * 3
748
- const rAx = c.rAx, rAy = c.rAy, rAz = c.rAz
749
- const rBx = c.rBx, rBy = c.rBy, rBz = c.rBz
750
-
751
- const vAx = lv[ai + 0] + av[ai + 1] * rAz - av[ai + 2] * rAy
752
- const vAy = lv[ai + 1] + av[ai + 2] * rAx - av[ai + 0] * rAz
753
- const vAz = lv[ai + 2] + av[ai + 0] * rAy - av[ai + 1] * rAx
754
- const vBx = lv[bi + 0] + av[bi + 1] * rBz - av[bi + 2] * rBy
755
- const vBy = lv[bi + 1] + av[bi + 2] * rBx - av[bi + 0] * rBz
756
- const vBz = lv[bi + 2] + av[bi + 0] * rBy - av[bi + 1] * rBx
757
- const dvx = vBx - vAx
758
- const dvy = vBy - vAy
759
- const dvz = vBz - vAz
760
-
761
- // Normal row.
762
- const jacInvN = c.jacInvN
763
- if (jacInvN > 0) {
764
- const nx = c.nx, ny = c.ny, nz = c.nz
765
- const relVelN = dvx * nx + dvy * ny + dvz * nz
766
- let dImpN = (c.bounceVel - relVelN) * jacInvN
767
- const oldN = c.appliedNormalImpulse
768
- let newN = oldN + dImpN
769
- if (newN < 0) { newN = 0; dImpN = -oldN }
770
- c.appliedNormalImpulse = newN
771
- if (dImpN !== 0) {
772
- const cAxN = c.cAxN, cAyN = c.cAyN, cAzN = c.cAzN
773
- const cBxN = c.cBxN, cByN = c.cByN, cBzN = c.cBzN
774
- if (imA > 0) {
775
- lv[ai + 0] -= dImpN * imA * nx
776
- lv[ai + 1] -= dImpN * imA * ny
777
- lv[ai + 2] -= dImpN * imA * nz
778
- av[ai + 0] -= dImpN * cAxN
779
- av[ai + 1] -= dImpN * cAyN
780
- av[ai + 2] -= dImpN * cAzN
781
- }
782
- if (imB > 0) {
783
- lv[bi + 0] += dImpN * imB * nx
784
- lv[bi + 1] += dImpN * imB * ny
785
- lv[bi + 2] += dImpN * imB * nz
786
- av[bi + 0] += dImpN * cBxN
787
- av[bi + 1] += dImpN * cByN
788
- av[bi + 2] += dImpN * cBzN
789
- }
790
- }
791
- }
792
-
793
- // Friction. Bound = ±μ · current normal impulse.
794
- const muNormal = c.friction * c.appliedNormalImpulse
795
- if (muNormal <= 0) return
796
-
797
- // Re-read dv after the normal impulse possibly changed lv/av.
798
- const vAx2 = lv[ai + 0] + av[ai + 1] * rAz - av[ai + 2] * rAy
799
- const vAy2 = lv[ai + 1] + av[ai + 2] * rAx - av[ai + 0] * rAz
800
- const vAz2 = lv[ai + 2] + av[ai + 0] * rAy - av[ai + 1] * rAx
801
- const vBx2 = lv[bi + 0] + av[bi + 1] * rBz - av[bi + 2] * rBy
802
- const vBy2 = lv[bi + 1] + av[bi + 2] * rBx - av[bi + 0] * rBz
803
- const vBz2 = lv[bi + 2] + av[bi + 0] * rBy - av[bi + 1] * rBx
804
- const dvx2 = vBx2 - vAx2
805
- const dvy2 = vBy2 - vAy2
806
- const dvz2 = vBz2 - vAz2
807
-
808
- applyFrictionTangent(
809
- c, ai, bi, dvx2, dvy2, dvz2,
810
- c.t1x, c.t1y, c.t1z,
811
- c.cAxT1, c.cAyT1, c.cAzT1, c.cBxT1, c.cByT1, c.cBzT1,
812
- c.jacInvT1, muNormal, imA, imB, lv, av, 1,
813
- )
814
- applyFrictionTangent(
815
- c, ai, bi, dvx2, dvy2, dvz2,
816
- c.t2x, c.t2y, c.t2z,
817
- c.cAxT2, c.cAyT2, c.cAzT2, c.cBxT2, c.cByT2, c.cBzT2,
818
- c.jacInvT2, muNormal, imA, imB, lv, av, 2,
819
- )
820
- }
821
-
822
- function applyFrictionTangent(
823
- c: Contact,
824
- ai: number, bi: number,
825
- dvx: number, dvy: number, dvz: number,
826
- tx: number, ty: number, tz: number,
827
- cAx: number, cAy: number, cAz: number,
828
- cBx: number, cBy: number, cBz: number,
829
- jacInv: number, muNormal: number,
830
- imA: number, imB: number,
831
- lv: Float32Array, av: Float32Array,
832
- slot: 1 | 2,
833
- ): void {
834
- if (jacInv <= 0) return
835
- const relVel = dvx * tx + dvy * ty + dvz * tz
836
- let dImp = -relVel * jacInv
837
- const old = slot === 1 ? c.appliedFrictionImpulse1 : c.appliedFrictionImpulse2
838
- let next = old + dImp
839
- if (next < -muNormal) { next = -muNormal; dImp = next - old }
840
- else if (next > muNormal) { next = muNormal; dImp = next - old }
841
- if (slot === 1) c.appliedFrictionImpulse1 = next
842
- else c.appliedFrictionImpulse2 = next
843
-
844
- if (dImp === 0) return
845
- if (imA > 0) {
846
- lv[ai + 0] -= dImp * imA * tx
847
- lv[ai + 1] -= dImp * imA * ty
848
- lv[ai + 2] -= dImp * imA * tz
849
- av[ai + 0] -= dImp * cAx
850
- av[ai + 1] -= dImp * cAy
851
- av[ai + 2] -= dImp * cAz
852
- }
853
- if (imB > 0) {
854
- lv[bi + 0] += dImp * imB * tx
855
- lv[bi + 1] += dImp * imB * ty
856
- lv[bi + 2] += dImp * imB * tz
857
- av[bi + 0] += dImp * cBx
858
- av[bi + 1] += dImp * cBy
859
- av[bi + 2] += dImp * cBz
860
- }
861
- }
862
-
863
- function buildBodyMat(store: RigidBodyStore, i: number, out: Float32Array): void {
864
- const i3 = i * 3, i4 = i * 4
865
- Mat4.fromPositionRotationInto(
866
- store.positions[i3 + 0], store.positions[i3 + 1], store.positions[i3 + 2],
867
- store.orientations[i4 + 0], store.orientations[i4 + 1], store.orientations[i4 + 2], store.orientations[i4 + 3],
868
- out,
869
- )
870
- }
871
-
872
- // Quaternion of qx(x) qy(y) qz(z) (three.js 'XYZ' order).
873
- function eulerXYZQuatInto(x: number, y: number, z: number, out: Float32Array): void {
874
- const sx = Math.sin(x * 0.5), cx = Math.cos(x * 0.5)
875
- const sy = Math.sin(y * 0.5), cy = Math.cos(y * 0.5)
876
- const sz = Math.sin(z * 0.5), cz = Math.cos(z * 0.5)
877
- out[0] = sx * cy * cz + cx * sy * sz
878
- out[1] = cx * sy * cz - sx * cy * sz
879
- out[2] = cx * cy * sz + sx * sy * cz
880
- out[3] = cx * cy * cz - sx * sy * sz
881
- }
882
-
883
- // Euler XYZ from a 3×3 rotation matrix (row-major elements).
884
- function matrixToEulerXYZ(
885
- r00: number, r01: number,
886
- r10: number, r11: number,
887
- r20: number, r21: number, r22: number,
888
- out: Float32Array,
889
- ): void {
890
- if (r20 < 1) {
891
- if (r20 > -1) {
892
- out[0] = Math.atan2(-r21, r22)
893
- out[1] = Math.asin(r20)
894
- out[2] = Math.atan2(-r10, r00)
895
- } else {
896
- out[0] = -Math.atan2(r01, r11)
897
- out[1] = -Math.PI * 0.5
898
- out[2] = 0
899
- }
900
- } else {
901
- out[0] = Math.atan2(r01, r11)
902
- out[1] = Math.PI * 0.5
903
- out[2] = 0
904
- }
905
- }
1
+ // 6DOF spring + contact constraint solver. Sequential-impulse projected
2
+ // Gauss-Seidel: per axis, target a relative velocity (limit correction +
3
+ // spring), apply the impulse needed to reach it. Friction is two Coulomb
4
+ // rows per contact, normal is push-only.
5
+ //
6
+ // Two passes per substep:
7
+ // 1. SETUP — for each constraint and contact, compute every quantity that
8
+ // doesn't depend on lv/av (world axes, lever arms, Jacobian denominators,
9
+ // target velocities, friction tangent bases, restitution reference).
10
+ // These are constant during solve since pos/ori/inertia don't change.
11
+ // 2. ITERATE — `iterations` passes that read the cache and apply impulses
12
+ // based on the current lv/av. ~2× faster than recomputing per iter.
13
+
14
+ import { Mat4 } from "../math"
15
+ import type { RigidBodyStore } from "./body"
16
+ import type { SixDofSpringConstraint } from "./constraint"
17
+ import { STOP_ERP } from "./constraint"
18
+ import type { Contact, ContactPool } from "./contact"
19
+
20
+ const BOUNCE_THRESHOLD = 2.0
21
+
22
+ // Ceilings on limit-correction velocity. In normal operation limit errors are
23
+ // tiny; a large error only appears after a discontinuity (teleport, stall,
24
+ // deep penetration), and feeding err·ERP/dt to the solver unclamped then
25
+ // injects explosion-scale impulses into the chain.
26
+ const MAX_LINEAR_CORRECTION_VEL = 120 // units/s
27
+ const MAX_ANGULAR_CORRECTION_VEL = 30 // rad/s
28
+
29
+ // Bullet's limit-motor softness defaults (0.7 translational, 0.5 rotational):
30
+ // scale each iteration's limit impulse so the stop engages progressively
31
+ // instead of as a hard velocity snap.
32
+ const LIMIT_SOFTNESS_LINEAR = 0.7
33
+ const LIMIT_SOFTNESS_ANGULAR = 0.5
34
+
35
+ // Spring rows are IMPLICIT spring-dampers (Spring2/ODE-style soft
36
+ // constraints): each axis solves
37
+ // relVel⁺ + (k/γ)·err + s·λ = 0, γ = c + h·k, s = 1/(h·γ)
38
+ // which is the backward-Euler update of that axis's spring-damper —
39
+ // unconditionally stable for ANY authored k (no deadbeat clamp, no force
40
+ // clamp). The previous clamped velocity-drive could inject velocity far from
41
+ // equilibrium but not absorb it near equilibrium (its clamp shrank with the
42
+ // error), so resting chains rang forever — the "static dress slowly boils"
43
+ // bug. c is derived per row from a fixed damping ratio against the row's
44
+ // effective mass: c = 2ζ√(k·m_eff).
45
+ const SPRING_DAMPING_ZETA = 0.7
46
+
47
+ // ERP scale for loop-closing constraints (see buildConstraints: joints that
48
+ // close a cycle in the joint graph, e.g. the horizontal ring welds of
49
+ // cross-linked skirt lattices). A loop over-determines positions — when
50
+ // contacts push the lattice, the ring's errors cannot all reach zero, and
51
+ // full-rate corrections chase each other around the cycle as violent
52
+ // chatter. Loop edges keep shape at a fraction of the correction rate while
53
+ // the spanning-tree chains stay stiff.
54
+ const LOOP_ERP_SCALE = 1.0
55
+ // Loop-edge LOCKED axes are converted to force-clamped springs instead of
56
+ // weld rows: an equality row on a cycle fights the other cycle edges at any
57
+ // ERP (the velocity system is over-determined too). A spring bounded by its
58
+ // real force k·|err|·dt holds the ring's shape elastically without fighting.
59
+ const LOOP_SPRING_K = 900
60
+ // Angular limit violations below this switch to per-axis euler rows; above
61
+ // it, the single geodesic row takes over (see setupConstraint).
62
+ const GEODESIC_THRESHOLD = 0.5 // rad
63
+
64
+ // Module-level scratch (no per-iter allocations).
65
+ const _TA = new Float32Array(16)
66
+ const _TB = new Float32Array(16)
67
+ const _bodyMatA = new Float32Array(16)
68
+ const _bodyMatB = new Float32Array(16)
69
+ const _angDiffScratch = new Float32Array(3)
70
+ const _quatScratchA = new Float32Array(4)
71
+ const _quatScratchB = new Float32Array(4)
72
+
73
+ export function solveConstraints(
74
+ store: RigidBodyStore,
75
+ constraints: SixDofSpringConstraint[],
76
+ cache: SolverCache,
77
+ contacts: ContactPool,
78
+ dt: number,
79
+ iterations: number,
80
+ ): void {
81
+ if (dt <= 0) return
82
+ if (constraints.length === 0 && contacts.count === 0) return
83
+
84
+ const invDt = 1 / dt
85
+ const lv = store.linearVelocities
86
+ const av = store.angularVelocities
87
+ const invMass = store.invMass
88
+
89
+ // World-space inverse inertia tensors for this substep's poses.
90
+ store.updateInvInertiaWorld()
91
+ const W = store.invInertiaWorld
92
+
93
+ for (let c = 0; c < constraints.length; c++) {
94
+ setupConstraint(constraints[c], c, cache, store, dt, invDt)
95
+ }
96
+ for (let ci = 0; ci < contacts.count; ci++) {
97
+ setupContactRow(contacts.get(ci), lv, av, invMass, W)
98
+ }
99
+
100
+ for (let iter = 0; iter < iterations; iter++) {
101
+ for (let c = 0; c < constraints.length; c++) {
102
+ iterateConstraint(c, cache, lv, av, invMass)
103
+ }
104
+ for (let ci = 0; ci < contacts.count; ci++) {
105
+ iterateContactRow(contacts.get(ci), lv, av, invMass)
106
+ }
107
+ }
108
+ }
109
+
110
+ // ── Flat solver cache (SoA) ──────────────────────────────────────────────────
111
+ // One typed-array block instead of a dozen small Float32Arrays per constraint:
112
+ // the 10-iteration hot loop walks memory linearly off a single base pointer
113
+ // instead of pointer-chasing ~1000 scattered objects. Layout below mirrors the
114
+ // old cache fields one-to-one; the math is untouched and bit-identical.
115
+ const F_STRIDE = 108
116
+ const LEVER_A = 0 // 3
117
+ const LEVER_B = 3 // 3
118
+ const LIN_AXES = 6 // 9
119
+ const LIN_CA = 15 // 9
120
+ const LIN_CB = 24 // 9
121
+ const LIN_JAC = 33 // 3
122
+ const LIN_TGT = 36 // 3
123
+ const LIN_LIMIT_IMP = 39 // 3
124
+ const LIN_SPR_TGT = 42 // 3
125
+ const LIN_SPR_MAX = 45 // 3
126
+ const LIN_SPR_IMP = 48 // 3
127
+ const ANG_AXES = 51 // 9
128
+ const ANG_TGT = 60 // 3
129
+ const ANG_JAC = 63 // 3
130
+ const ANG_SPR_MAX = 66 // 3
131
+ const ANG_SPR_IMP = 69 // 3
132
+ const ANG_WA = 72 // 9
133
+ const ANG_WB = 81 // 9
134
+ const ANG_LIM_AXIS = 90 // 3
135
+ const ANG_LIM_WA = 93 // 3
136
+ const ANG_LIM_WB = 96 // 3
137
+ const ANG_PA_TGT = 99 // 3
138
+ const ANG_PA_IMP = 102 // 3
139
+ const I_STRIDE = 16
140
+ const I_BODY_A = 0
141
+ const I_BODY_B = 1
142
+ const I_SKIP = 2
143
+ const I_LIN_ACT = 3 // 3
144
+ const I_LIN_SPR_ACT = 6 // 3
145
+ const I_ANG_ACT = 9 // 3
146
+ const I_ANG_PA_ACT = 12 // 3
147
+ const I_ANG_LIM_ACT = 15
148
+
149
+ export class SolverCache {
150
+ readonly F: Float32Array
151
+ readonly I: Int32Array
152
+ /** f64 lane for the geodesic row's scalars — they were plain number fields,
153
+ * and demoting them to f32 would break bit-identical results. */
154
+ readonly D: Float64Array
155
+ constructor(constraints: SixDofSpringConstraint[]) {
156
+ const n = constraints.length
157
+ this.F = new Float32Array(n * F_STRIDE)
158
+ this.I = new Int32Array(n * I_STRIDE)
159
+ this.D = new Float64Array(n * 3)
160
+ for (let i = 0; i < n; i++) {
161
+ this.I[i * I_STRIDE + I_BODY_A] = constraints[i].bodyA
162
+ this.I[i * I_STRIDE + I_BODY_B] = constraints[i].bodyB
163
+ }
164
+ }
165
+ }
166
+
167
+ // SETUP: compute everything that doesn't depend on velocities. Caller
168
+ // guarantees pos/ori don't change between this and the iter loop.
169
+ function setupConstraint(
170
+ con: SixDofSpringConstraint,
171
+ ci: number,
172
+ cache: SolverCache,
173
+ store: RigidBodyStore,
174
+ dt: number,
175
+ invDt: number,
176
+ ): void {
177
+ const F = cache.F
178
+ const I = cache.I
179
+ const D = cache.D
180
+ const base = ci * F_STRIDE
181
+ const ib = ci * I_STRIDE
182
+ const db = ci * 3
183
+ const a = con.bodyA
184
+ const b = con.bodyB
185
+ const imA = store.invMass[a]
186
+ const imB = store.invMass[b]
187
+ const W = store.invInertiaWorld
188
+ const a9 = a * 9
189
+ const b9 = b * 9
190
+
191
+ const skip = imA === 0 && imB === 0
192
+ I[ib + I_SKIP] = skip ? 1 : 0
193
+ if (skip) return
194
+
195
+ const erpScale = con.isLoop ? LOOP_ERP_SCALE : 1.0
196
+
197
+ buildBodyMat(store, a, _bodyMatA)
198
+ buildBodyMat(store, b, _bodyMatB)
199
+ Mat4.multiplyArrays(_bodyMatA, 0, con.frameA, 0, _TA, 0)
200
+ Mat4.multiplyArrays(_bodyMatB, 0, con.frameB, 0, _TB, 0)
201
+
202
+ // Per-body pivots at each body's own joint-frame origin (Spring2-style).
203
+ // Bullet 2.7x's shared mass-weighted anchor (m_AnchorPos) degenerates when
204
+ // the joint is violated by a large distance: the midpoint sits far from
205
+ // both bodies, the lever arms grow with the separation, the Jacobian
206
+ // denominator blows up as err²·invInertia, and the row applies torque
207
+ // instead of closing velocity the joint "breaks" and the error runs
208
+ // away. Per-body pivots keep the levers bounded by the frame offsets, so
209
+ // the row stays effective no matter how large the violation is.
210
+ const pos = store.positions
211
+ const ai = a * 3
212
+ const bi = b * 3
213
+ const rAx = _TA[12] - pos[ai + 0]
214
+ const rAy = _TA[13] - pos[ai + 1]
215
+ const rAz = _TA[14] - pos[ai + 2]
216
+ const rBx = _TB[12] - pos[bi + 0]
217
+ const rBy = _TB[13] - pos[bi + 1]
218
+ const rBz = _TB[14] - pos[bi + 2]
219
+ const lA = base + LEVER_A
220
+ const lB = base + LEVER_B
221
+ F[lA + 0] = rAx; F[lA + 1] = rAy; F[lA + 2] = rAz
222
+ F[lB + 0] = rBx; F[lB + 1] = rBy; F[lB + 2] = rBz
223
+
224
+ // linearDiff = TA.basis^T · (TB.origin − TA.origin); axes = TA columns 0/1/2.
225
+ const dxw = _TB[12] - _TA[12]
226
+ const dyw = _TB[13] - _TA[13]
227
+ const dzw = _TB[14] - _TA[14]
228
+ const linDiff0 = _TA[0] * dxw + _TA[1] * dyw + _TA[2] * dzw
229
+ const linDiff1 = _TA[4] * dxw + _TA[5] * dyw + _TA[6] * dzw
230
+ const linDiff2 = _TA[8] * dxw + _TA[9] * dyw + _TA[10] * dzw
231
+
232
+ const axes = base + LIN_AXES
233
+ const cA = base + LIN_CA
234
+ const cB = base + LIN_CB
235
+ const jac = base + LIN_JAC
236
+ const tgt = base + LIN_TGT
237
+ const act = ib + I_LIN_ACT
238
+
239
+ for (let i = 0; i < 3; i++) {
240
+ const o = i * 3
241
+ const axx = i === 0 ? _TA[0] : i === 1 ? _TA[4] : _TA[8]
242
+ const axy = i === 0 ? _TA[1] : i === 1 ? _TA[5] : _TA[9]
243
+ const axz = i === 0 ? _TA[2] : i === 1 ? _TA[6] : _TA[10]
244
+ F[axes + o + 0] = axx
245
+ F[axes + o + 1] = axy
246
+ F[axes + o + 2] = axz
247
+
248
+ const cAx = rAy * axz - rAz * axy
249
+ const cAy = rAz * axx - rAx * axz
250
+ const cAz = rAx * axy - rAy * axx
251
+ const cBx = rBy * axz - rBz * axy
252
+ const cBy = rBz * axx - rBx * axz
253
+ const cBz = rBx * axy - rBy * axx
254
+ // Tensor-multiplied lever crosses: cache I⁻¹·(r×ax) for application;
255
+ // denominator = (r×ax)ᵀ·I⁻¹·(r×ax).
256
+ const wAx = W[a9 + 0] * cAx + W[a9 + 1] * cAy + W[a9 + 2] * cAz
257
+ const wAy = W[a9 + 3] * cAx + W[a9 + 4] * cAy + W[a9 + 5] * cAz
258
+ const wAz = W[a9 + 6] * cAx + W[a9 + 7] * cAy + W[a9 + 8] * cAz
259
+ const wBx = W[b9 + 0] * cBx + W[b9 + 1] * cBy + W[b9 + 2] * cBz
260
+ const wBy = W[b9 + 3] * cBx + W[b9 + 4] * cBy + W[b9 + 5] * cBz
261
+ const wBz = W[b9 + 6] * cBx + W[b9 + 7] * cBy + W[b9 + 8] * cBz
262
+ F[cA + o + 0] = wAx; F[cA + o + 1] = wAy; F[cA + o + 2] = wAz
263
+ F[cB + o + 0] = wBx; F[cB + o + 1] = wBy; F[cB + o + 2] = wBz
264
+
265
+ const denom = imA + imB +
266
+ (cAx * wAx + cAy * wAy + cAz * wAz) +
267
+ (cBx * wBx + cBy * wBy + cBz * wBz)
268
+ F[jac + i] = denom > 0 ? 1 / denom : 0
269
+
270
+ const lo = con.linearMin[i]
271
+ const hi = con.linearMax[i]
272
+ const curr = i === 0 ? linDiff0 : i === 1 ? linDiff1 : linDiff2
273
+ // active: 1 = bilateral equality (locked axis a joint, always on),
274
+ // 2 = unilateral stop (ranged axis in violation).
275
+ let target = 0
276
+ let active = 0
277
+ if (lo <= hi) {
278
+ let err = 0
279
+ if (curr < lo) err = curr - lo
280
+ else if (curr > hi) err = curr - hi
281
+ if (lo === hi) active = 1
282
+ else if (err !== 0) active = 2
283
+ if (err !== 0) {
284
+ target = -err * STOP_ERP * erpScale * invDt
285
+ if (target > MAX_LINEAR_CORRECTION_VEL) target = MAX_LINEAR_CORRECTION_VEL
286
+ else if (target < -MAX_LINEAR_CORRECTION_VEL) target = -MAX_LINEAR_CORRECTION_VEL
287
+ }
288
+ }
289
+ F[tgt + i] = target
290
+ I[act + i] = denom > 0 ? active : 0
291
+ F[base + LIN_LIMIT_IMP + i] = 0
292
+ // A spring on a locked axis is redundant the bilateral limit row
293
+ // already welds the DOF, and driving it twice overshoots every
294
+ // iteration (PMX rigs routinely put k=100000 springs on locked axes,
295
+ // which turned welded weight-bodies into energy pumps).
296
+ if (con.springEnabled[i] && denom > 0 && lo !== hi) {
297
+ // Implicit spring-damper (see SPRING_DAMPING_ZETA). Stored per axis:
298
+ // cacheLinSpringTarget = −(k/γ)·err (the bias, in velocity units) and
299
+ // cacheLinSpringMaxImp = s (the CFM softness — NOT a clamp anymore).
300
+ const k = con.springStiffness[i]
301
+ const serr = curr - con.equilibriumPoint[i]
302
+ const meff = 1 / denom
303
+ const c = 2 * SPRING_DAMPING_ZETA * Math.sqrt(k * meff)
304
+ const gamma = c + dt * k
305
+ F[base + LIN_SPR_TGT + i] = -(k / gamma) * serr
306
+ F[base + LIN_SPR_MAX + i] = 1 / (dt * gamma)
307
+ F[base + LIN_SPR_IMP + i] = 0
308
+ I[ib + I_LIN_SPR_ACT + i] = 1
309
+ } else if (!(lo === hi && con.isLoop)) {
310
+ I[ib + I_LIN_SPR_ACT + i] = 0
311
+ }
312
+ }
313
+
314
+ // Angular: TA^T · TB → Euler XYZ; axes from TA.col2 × TB.col0.
315
+ const r00 = _TA[0]*_TB[0] + _TA[1]*_TB[1] + _TA[2]*_TB[2]
316
+ const r01 = _TA[0]*_TB[4] + _TA[1]*_TB[5] + _TA[2]*_TB[6]
317
+ const r10 = _TA[4]*_TB[0] + _TA[5]*_TB[1] + _TA[6]*_TB[2]
318
+ const r11 = _TA[4]*_TB[4] + _TA[5]*_TB[5] + _TA[6]*_TB[6]
319
+ const r20 = _TA[8]*_TB[0] + _TA[9]*_TB[1] + _TA[10]*_TB[2]
320
+ const r21 = _TA[8]*_TB[4] + _TA[9]*_TB[5] + _TA[10]*_TB[6]
321
+ const r22 = _TA[8]*_TB[8] + _TA[9]*_TB[9] + _TA[10]*_TB[10]
322
+ matrixToEulerXYZ(r00, r01, r10, r11, r20, r21, r22, _angDiffScratch)
323
+
324
+ const a2x = _TA[8], a2y = _TA[9], a2z = _TA[10]
325
+ const b0x = _TB[0], b0y = _TB[1], b0z = _TB[2]
326
+ let yx = a2y * b0z - a2z * b0y
327
+ let yy = a2z * b0x - a2x * b0z
328
+ let yz = a2x * b0y - a2y * b0x
329
+ let l = Math.hypot(yx, yy, yz)
330
+ if (l > 1e-8) { const inv = 1/l; yx*=inv; yy*=inv; yz*=inv }
331
+ let xx = yy * a2z - yz * a2y
332
+ let xy = yz * a2x - yx * a2z
333
+ let xz = yx * a2y - yy * a2x
334
+ l = Math.hypot(xx, xy, xz)
335
+ if (l > 1e-8) { const inv = 1/l; xx*=inv; xy*=inv; xz*=inv }
336
+ let zx = b0y * yz - b0z * yy
337
+ let zy = b0z * yx - b0x * yz
338
+ let zz = b0x * yy - b0y * yx
339
+ l = Math.hypot(zx, zy, zz)
340
+ if (l > 1e-8) { const inv = 1/l; zx*=inv; zy*=inv; zz*=inv }
341
+
342
+ const angAxes = base + ANG_AXES
343
+ F[angAxes + 0] = xx; F[angAxes + 1] = xy; F[angAxes + 2] = xz
344
+ F[angAxes + 3] = yx; F[angAxes + 4] = yy; F[angAxes + 5] = yz
345
+ F[angAxes + 6] = zx; F[angAxes + 7] = zy; F[angAxes + 8] = zz
346
+
347
+ // Per-axis angular Jacobians with the full tensors: cache I⁻¹·axis per
348
+ // body plus 1/(axᵀ(I⁻¹A+I⁻¹B)ax) per axis.
349
+ const angJac = base + ANG_JAC
350
+ const angWAs = base + ANG_WA
351
+ const angWBs = base + ANG_WB
352
+ for (let i = 0; i < 3; i++) {
353
+ const o = i * 3
354
+ const axx = F[angAxes + o + 0], axy = F[angAxes + o + 1], axz = F[angAxes + o + 2]
355
+ const wAx = W[a9 + 0] * axx + W[a9 + 1] * axy + W[a9 + 2] * axz
356
+ const wAy = W[a9 + 3] * axx + W[a9 + 4] * axy + W[a9 + 5] * axz
357
+ const wAz = W[a9 + 6] * axx + W[a9 + 7] * axy + W[a9 + 8] * axz
358
+ const wBx = W[b9 + 0] * axx + W[b9 + 1] * axy + W[b9 + 2] * axz
359
+ const wBy = W[b9 + 3] * axx + W[b9 + 4] * axy + W[b9 + 5] * axz
360
+ const wBz = W[b9 + 6] * axx + W[b9 + 7] * axy + W[b9 + 8] * axz
361
+ F[angWAs + o + 0] = wAx; F[angWAs + o + 1] = wAy; F[angWAs + o + 2] = wAz
362
+ F[angWBs + o + 0] = wBx; F[angWBs + o + 1] = wBy; F[angWBs + o + 2] = wBz
363
+ const denom = axx * (wAx + wBx) + axy * (wAy + wBy) + axz * (wAz + wBz)
364
+ F[angJac + i] = denom > 0 ? 1 / denom : 0
365
+ }
366
+
367
+ // Per-axis rows carry only the springs. Sign flip vs linear:
368
+ // d(angDiff)/dt = −(ω_B ω_A)·ax.
369
+ const angTgt = base + ANG_TGT
370
+ const angAct = ib + I_ANG_ACT
371
+ for (let i = 0; i < 3; i++) {
372
+ const idx = i + 3
373
+ // Springs on locked axes are skipped — the limit row welds those, and
374
+ // double-driving a DOF overshoots every iteration (see the linear loop).
375
+ if (con.springEnabled[idx] && F[angJac + i] > 0 && con.angularMin[i] !== con.angularMax[i]) {
376
+ // Implicit spring-damper, angular flavor. F[angJac + i] = 1/denom IS the
377
+ // row's effective inertia. Sign flip vs linear: d(err)/dt = −relAv, so
378
+ // the bias is positive for positive error. cacheAngSpringMaxImp holds
379
+ // the CFM softness s (not a clamp).
380
+ const k = con.springStiffness[idx]
381
+ const serr = _angDiffScratch[i] - con.equilibriumPoint[idx]
382
+ const c = 2 * SPRING_DAMPING_ZETA * Math.sqrt(k * F[angJac + i])
383
+ const gamma = c + dt * k
384
+ F[angTgt + i] = (k / gamma) * serr
385
+ F[base + ANG_SPR_MAX + i] = 1 / (dt * gamma)
386
+ I[angAct + i] = 1
387
+ } else {
388
+ F[angTgt + i] = 0
389
+ I[angAct + i] = 0
390
+ }
391
+ F[base + ANG_SPR_IMP + i] = 0
392
+ }
393
+
394
+ // Angular limit handling is hybrid. Small violations (the resting-cloth
395
+ // regime) use per-axis euler rows they converge cleanly and keep resting
396
+ // cloth dead still. Large violations switch to a single geodesic row toward
397
+ // the euler-clamped target: per-axis euler rows (the Bullet-2.7x approach
398
+ // this port used) become geometrically inconsistent for large errors near
399
+ // the asin singularity they chase phantom errors and pump angular velocity
400
+ // into the chain instead of converging.
401
+ I[ib + I_ANG_LIM_ACT] = 0
402
+ I[ib + I_ANG_PA_ACT + 0] = 0
403
+ I[ib + I_ANG_PA_ACT + 1] = 0
404
+ I[ib + I_ANG_PA_ACT + 2] = 0
405
+ if (imA > 0 || imB > 0) {
406
+ const ex = _angDiffScratch[0], ey = _angDiffScratch[1], ez = _angDiffScratch[2]
407
+ // Free axes (min > max) follow the current angle, i.e. no correction.
408
+ let tx = ex, ty = ey, tz = ez
409
+ if (con.angularMin[0] <= con.angularMax[0]) tx = ex < con.angularMin[0] ? con.angularMin[0] : ex > con.angularMax[0] ? con.angularMax[0] : ex
410
+ if (con.angularMin[1] <= con.angularMax[1]) ty = ey < con.angularMin[1] ? con.angularMin[1] : ey > con.angularMax[1] ? con.angularMax[1] : ey
411
+ if (con.angularMin[2] <= con.angularMax[2]) tz = ez < con.angularMin[2] ? con.angularMin[2] : ez > con.angularMax[2] ? con.angularMax[2] : ez
412
+ const errX = ex - tx, errY = ey - ty, errZ = ez - tz
413
+ const maxErr = Math.max(Math.abs(errX), Math.abs(errY), Math.abs(errZ))
414
+ if (maxErr > 0 && maxErr < GEODESIC_THRESHOLD) {
415
+ // Per-axis euler limit rows. Locked axes are bilateral joints; ranged
416
+ // axes are unilateral stops (sign-clamped accumulation) a bilateral
417
+ // row on a ranged axis brakes natural recovery every substep and
418
+ // pumps energy into swinging cloth, and the pump grows WITH solver
419
+ // convergence (more iterations enforce the brake harder).
420
+ for (let i = 0; i < 3; i++) {
421
+ const err = i === 0 ? errX : i === 1 ? errY : errZ
422
+ F[base + ANG_PA_IMP + i] = 0
423
+ if (err === 0) {
424
+ I[ib + I_ANG_PA_ACT + i] = 0
425
+ continue
426
+ }
427
+ let target = err * STOP_ERP * erpScale * invDt
428
+ if (target > MAX_ANGULAR_CORRECTION_VEL) target = MAX_ANGULAR_CORRECTION_VEL
429
+ else if (target < -MAX_ANGULAR_CORRECTION_VEL) target = -MAX_ANGULAR_CORRECTION_VEL
430
+ F[base + ANG_PA_TGT + i] = target
431
+ I[ib + I_ANG_PA_ACT + i] = con.angularMin[i] === con.angularMax[i] ? 1 : 2
432
+ }
433
+ } else if (maxErr > 0) {
434
+ // Bilateral (equality) if any violated axis is locked — a locked axis
435
+ // is a joint, not a stop. Unilateral otherwise.
436
+ const bilateral =
437
+ (tx !== ex && con.angularMin[0] === con.angularMax[0]) ||
438
+ (ty !== ey && con.angularMin[1] === con.angularMax[1]) ||
439
+ (tz !== ez && con.angularMin[2] === con.angularMax[2])
440
+ // The decomposition above satisfies R_rel^T = Rx(x)·Ry(y)·Rz(z), so
441
+ // u = qx·qy·qz is conj(q_rel) and the error rotation (current →
442
+ // clamped target, expressed in TA's frame) is q_E = conj(u_t) ⊗ u.
443
+ eulerXYZQuatInto(ex, ey, ez, _quatScratchA)
444
+ eulerXYZQuatInto(tx, ty, tz, _quatScratchB)
445
+ const ux = _quatScratchA[0], uy = _quatScratchA[1], uz = _quatScratchA[2], uw = _quatScratchA[3]
446
+ const vx = _quatScratchB[0], vy = _quatScratchB[1], vz = _quatScratchB[2], vw = _quatScratchB[3]
447
+ // q_E = conj(v) u
448
+ let qex = vw * ux - vx * uw - vy * uz + vz * uy
449
+ let qey = vw * uy + vx * uz - vy * uw - vz * ux
450
+ let qez = vw * uz - vx * uy + vy * ux - vz * uw
451
+ let qew = vw * uw + vx * ux + vy * uy + vz * uz
452
+ if (qew < 0) { qex = -qex; qey = -qey; qez = -qez; qew = -qew }
453
+ const sinHalf = Math.sqrt(qex * qex + qey * qey + qez * qez)
454
+ if (sinHalf > 1e-6) {
455
+ const angle = 2 * Math.atan2(sinHalf, qew)
456
+ const invS = 1 / sinHalf
457
+ const axx = qex * invS, axy = qey * invS, axz = qez * invS
458
+ // Axis lives in TA's frame; TA's basis columns map it to world.
459
+ const lim = base + ANG_LIM_AXIS
460
+ F[lim + 0] = _TA[0] * axx + _TA[4] * axy + _TA[8] * axz
461
+ F[lim + 1] = _TA[1] * axx + _TA[5] * axy + _TA[9] * axz
462
+ F[lim + 2] = _TA[2] * axx + _TA[6] * axy + _TA[10] * axz
463
+ const gWA = base + ANG_LIM_WA
464
+ const gWB = base + ANG_LIM_WB
465
+ F[gWA + 0] = W[a9 + 0] * F[lim + 0] + W[a9 + 1] * F[lim + 1] + W[a9 + 2] * F[lim + 2]
466
+ F[gWA + 1] = W[a9 + 3] * F[lim + 0] + W[a9 + 4] * F[lim + 1] + W[a9 + 5] * F[lim + 2]
467
+ F[gWA + 2] = W[a9 + 6] * F[lim + 0] + W[a9 + 7] * F[lim + 1] + W[a9 + 8] * F[lim + 2]
468
+ F[gWB + 0] = W[b9 + 0] * F[lim + 0] + W[b9 + 1] * F[lim + 1] + W[b9 + 2] * F[lim + 2]
469
+ F[gWB + 1] = W[b9 + 3] * F[lim + 0] + W[b9 + 4] * F[lim + 1] + W[b9 + 5] * F[lim + 2]
470
+ F[gWB + 2] = W[b9 + 6] * F[lim + 0] + W[b9 + 7] * F[lim + 1] + W[b9 + 8] * F[lim + 2]
471
+ const gDenom = F[lim + 0] * (F[gWA + 0] + F[gWB + 0]) + F[lim + 1] * (F[gWA + 1] + F[gWB + 1]) + F[lim + 2] * (F[gWA + 2] + F[gWB + 2])
472
+ D[db + 0] = gDenom > 0 ? 1 / gDenom : 0
473
+ let target = angle * STOP_ERP * erpScale * invDt
474
+ if (target > MAX_ANGULAR_CORRECTION_VEL) target = MAX_ANGULAR_CORRECTION_VEL
475
+ D[db + 1] = target
476
+ I[ib + I_ANG_LIM_ACT] = bilateral ? 1 : 2
477
+ }
478
+ }
479
+ }
480
+ D[db + 2] = 0
481
+ }
482
+
483
+ // ITER: read cache, compute relVel from current lv/av, apply impulse.
484
+ function iterateConstraint(
485
+ ci: number,
486
+ cache: SolverCache,
487
+ lv: Float32Array,
488
+ av: Float32Array,
489
+ invMass: Float32Array,
490
+ ): void {
491
+ const F = cache.F
492
+ const I = cache.I
493
+ const D = cache.D
494
+ const base = ci * F_STRIDE
495
+ const ib = ci * I_STRIDE
496
+ const db = ci * 3
497
+ if (I[ib + I_SKIP] === 1) return
498
+ const a = I[ib + I_BODY_A]
499
+ const b = I[ib + I_BODY_B]
500
+ const ai = a * 3
501
+ const bi = b * 3
502
+ const imA = invMass[a]
503
+ const imB = invMass[b]
504
+
505
+ // Linear axes relVel at the offset point: v_pivot = v_CG + ω × r.
506
+ const lA = base + LEVER_A
507
+ const lB = base + LEVER_B
508
+ const rAx = F[lA + 0], rAy = F[lA + 1], rAz = F[lA + 2]
509
+ const rBx = F[lB + 0], rBy = F[lB + 1], rBz = F[lB + 2]
510
+ const axes = base + LIN_AXES
511
+ const cA = base + LIN_CA
512
+ const cB = base + LIN_CB
513
+ const jac = base + LIN_JAC
514
+ const tgt = base + LIN_TGT
515
+ const act = ib + I_LIN_ACT
516
+
517
+ const vAx = lv[ai + 0] + av[ai + 1] * rAz - av[ai + 2] * rAy
518
+ const vAy = lv[ai + 1] + av[ai + 2] * rAx - av[ai + 0] * rAz
519
+ const vAz = lv[ai + 2] + av[ai + 0] * rAy - av[ai + 1] * rAx
520
+ const vBx = lv[bi + 0] + av[bi + 1] * rBz - av[bi + 2] * rBy
521
+ const vBy = lv[bi + 1] + av[bi + 2] * rBx - av[bi + 0] * rBz
522
+ const vBz = lv[bi + 2] + av[bi + 0] * rBy - av[bi + 1] * rBx
523
+ const dvx = vBx - vAx
524
+ const dvy = vBy - vAy
525
+ const dvz = vBz - vAz
526
+
527
+ const sprAct = ib + I_LIN_SPR_ACT
528
+ const sprTgt = base + LIN_SPR_TGT
529
+ const sprMax = base + LIN_SPR_MAX
530
+ const sprImp = base + LIN_SPR_IMP
531
+ const limImp = base + LIN_LIMIT_IMP
532
+ for (let i = 0; i < 3; i++) {
533
+ if (!I[act + i] && !I[sprAct + i]) continue
534
+ const o = i * 3
535
+ const axx = F[axes + o + 0], axy = F[axes + o + 1], axz = F[axes + o + 2]
536
+ const relVel = dvx * axx + dvy * axy + dvz * axz
537
+ let j = 0
538
+
539
+ // Limit row. Locked axes (act 1) are bilateral equality joints; ranged
540
+ // axes in violation (act 2) are unilateral stops — accumulated impulse
541
+ // clamped to the corrective sign, so the stop pushes back into range but
542
+ // never pulls deeper or brakes natural recovery (a bilateral stop acts
543
+ // as a motor and pumps energy into swinging cloth).
544
+ if (I[act + i]) {
545
+ const target = F[tgt + i]
546
+ let dImp = LIMIT_SOFTNESS_LINEAR * (target - relVel) * F[jac + i]
547
+ if (I[act + i] === 2) {
548
+ const old = F[limImp + i]
549
+ let next = old + dImp
550
+ if (target > 0 ? next < 0 : next > 0) next = 0
551
+ dImp = next - old
552
+ F[limImp + i] = next
553
+ }
554
+ j += dImp
555
+ }
556
+
557
+ // Implicit spring-damper row (soft constraint, see setup): CFM-softened
558
+ // with accumulated λ, dissipative by construction. relVel is refreshed
559
+ // with the limit impulse applied just above (j·denom = j / jac) — driving
560
+ // the spring off the stale value double-corrects the DOF.
561
+ if (I[sprAct + i]) {
562
+ const relVelNow = j !== 0 ? relVel + j / F[jac + i] : relVel
563
+ const s = F[sprMax + i] // CFM softness
564
+ const dImp = (F[sprTgt + i] - relVelNow - s * F[sprImp + i]) / (1 / F[jac + i] + s)
565
+ F[sprImp + i] += dImp
566
+ j += dImp
567
+ }
568
+
569
+ if (j === 0) continue
570
+ if (imA > 0) {
571
+ lv[ai + 0] -= j * imA * axx
572
+ lv[ai + 1] -= j * imA * axy
573
+ lv[ai + 2] -= j * imA * axz
574
+ av[ai + 0] -= j * F[cA + o + 0]
575
+ av[ai + 1] -= j * F[cA + o + 1]
576
+ av[ai + 2] -= j * F[cA + o + 2]
577
+ }
578
+ if (imB > 0) {
579
+ lv[bi + 0] += j * imB * axx
580
+ lv[bi + 1] += j * imB * axy
581
+ lv[bi + 2] += j * imB * axz
582
+ av[bi + 0] += j * F[cB + o + 0]
583
+ av[bi + 1] += j * F[cB + o + 1]
584
+ av[bi + 2] += j * F[cB + o + 2]
585
+ }
586
+ }
587
+
588
+ // Angular axes — relAv = ω_B − ω_A.
589
+ const angAxes = base + ANG_AXES
590
+ const angJac = base + ANG_JAC
591
+ const angWAs = base + ANG_WA
592
+ const angWBs = base + ANG_WB
593
+ const angTgt = base + ANG_TGT
594
+ const angAct = ib + I_ANG_ACT
595
+ const dax = av[bi + 0] - av[ai + 0]
596
+ const day = av[bi + 1] - av[ai + 1]
597
+ const daz = av[bi + 2] - av[ai + 2]
598
+ const angSprMax = base + ANG_SPR_MAX
599
+ const angSprImp = base + ANG_SPR_IMP
600
+ for (let i = 0; i < 3; i++) {
601
+ if (!I[angAct + i]) continue
602
+ const o = i * 3
603
+ const axx = F[angAxes + o + 0], axy = F[angAxes + o + 1], axz = F[angAxes + o + 2]
604
+ const relAv = dax * axx + day * axy + daz * axz
605
+ // Implicit spring-damper row (soft constraint, see setup).
606
+ const s = F[angSprMax + i] // CFM softness
607
+ const j = (F[angTgt + i] - relAv - s * F[angSprImp + i]) / (1 / F[angJac + i] + s)
608
+ F[angSprImp + i] += j
609
+ if (j === 0) continue
610
+ if (imA > 0) {
611
+ av[ai + 0] -= j * F[angWAs + o + 0]
612
+ av[ai + 1] -= j * F[angWAs + o + 1]
613
+ av[ai + 2] -= j * F[angWAs + o + 2]
614
+ }
615
+ if (imB > 0) {
616
+ av[bi + 0] += j * F[angWBs + o + 0]
617
+ av[bi + 1] += j * F[angWBs + o + 1]
618
+ av[bi + 2] += j * F[angWBs + o + 2]
619
+ }
620
+ }
621
+
622
+ // Per-axis angular limit rows (small-violation regime), on the derived
623
+ // euler axes. Sign convention matches the springs: positive target reduces
624
+ // positive error via d(angDiff)/dt = −(ω_B − ω_A)·ax.
625
+ const paAct = ib + I_ANG_PA_ACT
626
+ if (I[paAct + 0] || I[paAct + 1] || I[paAct + 2]) {
627
+ const paTgt = base + ANG_PA_TGT
628
+ const paImp = base + ANG_PA_IMP
629
+ for (let i = 0; i < 3; i++) {
630
+ if (!I[paAct + i]) continue
631
+ const o = i * 3
632
+ const axx = F[angAxes + o + 0], axy = F[angAxes + o + 1], axz = F[angAxes + o + 2]
633
+ const relAv =
634
+ (av[bi + 0] - av[ai + 0]) * axx +
635
+ (av[bi + 1] - av[ai + 1]) * axy +
636
+ (av[bi + 2] - av[ai + 2]) * axz
637
+ const target = F[paTgt + i]
638
+ // Locked axes (act 1) are welds — full gain, like the 0.16.3 fold;
639
+ // softness only tempers the unilateral stops.
640
+ const soft = I[paAct + i] === 2 ? LIMIT_SOFTNESS_ANGULAR : 1.0
641
+ let j = soft * (target - relAv) * F[angJac + i]
642
+ if (I[paAct + i] === 2) {
643
+ const old = F[paImp + i]
644
+ let next = old + j
645
+ if (target > 0 ? next < 0 : next > 0) next = 0
646
+ j = next - old
647
+ F[paImp + i] = next
648
+ }
649
+ if (j === 0) continue
650
+ if (imA > 0) {
651
+ av[ai + 0] -= j * F[angWAs + o + 0]
652
+ av[ai + 1] -= j * F[angWAs + o + 1]
653
+ av[ai + 2] -= j * F[angWAs + o + 2]
654
+ }
655
+ if (imB > 0) {
656
+ av[bi + 0] += j * F[angWBs + o + 0]
657
+ av[bi + 1] += j * F[angWBs + o + 1]
658
+ av[bi + 2] += j * F[angWBs + o + 2]
659
+ }
660
+ }
661
+ }
662
+
663
+ // Geodesic limit row: drive (ω_B ω_A)·axis toward the correction target.
664
+ // Unilateral — the accumulated impulse can only push toward the legal
665
+ // region (target is always 0 along the corrective axis).
666
+ if (I[ib + I_ANG_LIM_ACT] !== 0) {
667
+ const lim = base + ANG_LIM_AXIS
668
+ const nx = F[lim + 0], ny = F[lim + 1], nz = F[lim + 2]
669
+ // Re-read relAv the spring rows above may have changed av.
670
+ const relAv =
671
+ (av[bi + 0] - av[ai + 0]) * nx +
672
+ (av[bi + 1] - av[ai + 1]) * ny +
673
+ (av[bi + 2] - av[ai + 2]) * nz
674
+ let j = LIMIT_SOFTNESS_ANGULAR * (D[db + 1] - relAv) * D[db + 0]
675
+ if (I[ib + I_ANG_LIM_ACT] === 2) {
676
+ const old = D[db + 2]
677
+ let next = old + j
678
+ if (next < 0) next = 0
679
+ j = next - old
680
+ D[db + 2] = next
681
+ }
682
+ if (j !== 0) {
683
+ const gWA = base + ANG_LIM_WA
684
+ const gWB = base + ANG_LIM_WB
685
+ if (imA > 0) {
686
+ av[ai + 0] -= j * F[gWA + 0]
687
+ av[ai + 1] -= j * F[gWA + 1]
688
+ av[ai + 2] -= j * F[gWA + 2]
689
+ }
690
+ if (imB > 0) {
691
+ av[bi + 0] += j * F[gWB + 0]
692
+ av[bi + 1] += j * F[gWB + 1]
693
+ av[bi + 2] += j * F[gWB + 2]
694
+ }
695
+ }
696
+ }
697
+ }
698
+
699
+ // SETUP: pre-compute Jacobians, friction basis, and the bounce reference
700
+ // from the *initial* closing velocity (Bullet's pattern — captures restitution
701
+ // before iter 1 zeroes out the approach).
702
+ function setupContactRow(
703
+ c: Contact,
704
+ lv: Float32Array,
705
+ av: Float32Array,
706
+ invMass: Float32Array,
707
+ W: Float32Array,
708
+ ): void {
709
+ const ai = c.bodyA * 3
710
+ const bi = c.bodyB * 3
711
+ const a9 = c.bodyA * 9
712
+ const b9 = c.bodyB * 9
713
+ const imA = invMass[c.bodyA]
714
+ const imB = invMass[c.bodyB]
715
+ const rAx = c.rAx, rAy = c.rAy, rAz = c.rAz
716
+ const rBx = c.rBx, rBy = c.rBy, rBz = c.rBz
717
+ const nx = c.nx, ny = c.ny, nz = c.nz
718
+
719
+ // Normal Jacobian. Cached vectors are tensor-multiplied I⁻¹·(r×n).
720
+ const cAxN = rAy * nz - rAz * ny
721
+ const cAyN = rAz * nx - rAx * nz
722
+ const cAzN = rAx * ny - rAy * nx
723
+ const cBxN = rBy * nz - rBz * ny
724
+ const cByN = rBz * nx - rBx * nz
725
+ const cBzN = rBx * ny - rBy * nx
726
+ const wAxN = W[a9 + 0] * cAxN + W[a9 + 1] * cAyN + W[a9 + 2] * cAzN
727
+ const wAyN = W[a9 + 3] * cAxN + W[a9 + 4] * cAyN + W[a9 + 5] * cAzN
728
+ const wAzN = W[a9 + 6] * cAxN + W[a9 + 7] * cAyN + W[a9 + 8] * cAzN
729
+ const wBxN = W[b9 + 0] * cBxN + W[b9 + 1] * cByN + W[b9 + 2] * cBzN
730
+ const wByN = W[b9 + 3] * cBxN + W[b9 + 4] * cByN + W[b9 + 5] * cBzN
731
+ const wBzN = W[b9 + 6] * cBxN + W[b9 + 7] * cByN + W[b9 + 8] * cBzN
732
+ const denomN = imA + imB +
733
+ (cAxN * wAxN + cAyN * wAyN + cAzN * wAzN) +
734
+ (cBxN * wBxN + cByN * wByN + cBzN * wBzN)
735
+ c.cAxN = wAxN; c.cAyN = wAyN; c.cAzN = wAzN
736
+ c.cBxN = wBxN; c.cByN = wByN; c.cBzN = wBzN
737
+ c.jacInvN = denomN > 0 ? 1 / denomN : 0
738
+
739
+ // Restitution reference, captured from initial relVelN.
740
+ const vAx = lv[ai + 0] + av[ai + 1] * rAz - av[ai + 2] * rAy
741
+ const vAy = lv[ai + 1] + av[ai + 2] * rAx - av[ai + 0] * rAz
742
+ const vAz = lv[ai + 2] + av[ai + 0] * rAy - av[ai + 1] * rAx
743
+ const vBx = lv[bi + 0] + av[bi + 1] * rBz - av[bi + 2] * rBy
744
+ const vBy = lv[bi + 1] + av[bi + 2] * rBx - av[bi + 0] * rBz
745
+ const vBz = lv[bi + 2] + av[bi + 0] * rBy - av[bi + 1] * rBx
746
+ const relVelN0 = (vBx - vAx) * nx + (vBy - vAy) * ny + (vBz - vAz) * nz
747
+ c.bounceVel = c.restitution > 0 && relVelN0 < -BOUNCE_THRESHOLD
748
+ ? -c.restitution * relVelN0
749
+ : 0
750
+
751
+ // Friction tangent basis. Pick the axis least aligned with n.
752
+ let t1x: number, t1y: number, t1z: number
753
+ if (Math.abs(nx) < 0.7071) { t1x = 0; t1y = -nz; t1z = ny }
754
+ else { t1x = nz; t1y = 0; t1z = -nx }
755
+ const tl = Math.hypot(t1x, t1y, t1z)
756
+ if (tl > 1e-8) {
757
+ const tInv = 1 / tl
758
+ t1x *= tInv; t1y *= tInv; t1z *= tInv
759
+ } else {
760
+ c.jacInvT1 = 0; c.jacInvT2 = 0
761
+ return
762
+ }
763
+ const t2x = ny * t1z - nz * t1y
764
+ const t2y = nz * t1x - nx * t1z
765
+ const t2z = nx * t1y - ny * t1x
766
+ c.t1x = t1x; c.t1y = t1y; c.t1z = t1z
767
+ c.t2x = t2x; c.t2y = t2y; c.t2z = t2z
768
+
769
+ // Friction Jacobians.
770
+ const cAxT1 = rAy * t1z - rAz * t1y
771
+ const cAyT1 = rAz * t1x - rAx * t1z
772
+ const cAzT1 = rAx * t1y - rAy * t1x
773
+ const cBxT1 = rBy * t1z - rBz * t1y
774
+ const cByT1 = rBz * t1x - rBx * t1z
775
+ const cBzT1 = rBx * t1y - rBy * t1x
776
+ const wAxT1 = W[a9 + 0] * cAxT1 + W[a9 + 1] * cAyT1 + W[a9 + 2] * cAzT1
777
+ const wAyT1 = W[a9 + 3] * cAxT1 + W[a9 + 4] * cAyT1 + W[a9 + 5] * cAzT1
778
+ const wAzT1 = W[a9 + 6] * cAxT1 + W[a9 + 7] * cAyT1 + W[a9 + 8] * cAzT1
779
+ const wBxT1 = W[b9 + 0] * cBxT1 + W[b9 + 1] * cByT1 + W[b9 + 2] * cBzT1
780
+ const wByT1 = W[b9 + 3] * cBxT1 + W[b9 + 4] * cByT1 + W[b9 + 5] * cBzT1
781
+ const wBzT1 = W[b9 + 6] * cBxT1 + W[b9 + 7] * cByT1 + W[b9 + 8] * cBzT1
782
+ const denomT1 = imA + imB +
783
+ (cAxT1 * wAxT1 + cAyT1 * wAyT1 + cAzT1 * wAzT1) +
784
+ (cBxT1 * wBxT1 + cByT1 * wByT1 + cBzT1 * wBzT1)
785
+ c.cAxT1 = wAxT1; c.cAyT1 = wAyT1; c.cAzT1 = wAzT1
786
+ c.cBxT1 = wBxT1; c.cByT1 = wByT1; c.cBzT1 = wBzT1
787
+ c.jacInvT1 = denomT1 > 0 ? 1 / denomT1 : 0
788
+
789
+ const cAxT2 = rAy * t2z - rAz * t2y
790
+ const cAyT2 = rAz * t2x - rAx * t2z
791
+ const cAzT2 = rAx * t2y - rAy * t2x
792
+ const cBxT2 = rBy * t2z - rBz * t2y
793
+ const cByT2 = rBz * t2x - rBx * t2z
794
+ const cBzT2 = rBx * t2y - rBy * t2x
795
+ const wAxT2 = W[a9 + 0] * cAxT2 + W[a9 + 1] * cAyT2 + W[a9 + 2] * cAzT2
796
+ const wAyT2 = W[a9 + 3] * cAxT2 + W[a9 + 4] * cAyT2 + W[a9 + 5] * cAzT2
797
+ const wAzT2 = W[a9 + 6] * cAxT2 + W[a9 + 7] * cAyT2 + W[a9 + 8] * cAzT2
798
+ const wBxT2 = W[b9 + 0] * cBxT2 + W[b9 + 1] * cByT2 + W[b9 + 2] * cBzT2
799
+ const wByT2 = W[b9 + 3] * cBxT2 + W[b9 + 4] * cByT2 + W[b9 + 5] * cBzT2
800
+ const wBzT2 = W[b9 + 6] * cBxT2 + W[b9 + 7] * cByT2 + W[b9 + 8] * cBzT2
801
+ const denomT2 = imA + imB +
802
+ (cAxT2 * wAxT2 + cAyT2 * wAyT2 + cAzT2 * wAzT2) +
803
+ (cBxT2 * wBxT2 + cByT2 * wByT2 + cBzT2 * wBzT2)
804
+ c.cAxT2 = wAxT2; c.cAyT2 = wAyT2; c.cAzT2 = wAzT2
805
+ c.cBxT2 = wBxT2; c.cByT2 = wByT2; c.cBzT2 = wBzT2
806
+ c.jacInvT2 = denomT2 > 0 ? 1 / denomT2 : 0
807
+ }
808
+
809
+ // ITER: one push-only normal row + two Coulomb friction rows. Friction
810
+ // bound depends on the *current* applied normal impulse, so it tightens
811
+ // as the normal row converges.
812
+ function iterateContactRow(
813
+ c: Contact,
814
+ lv: Float32Array,
815
+ av: Float32Array,
816
+ invMass: Float32Array,
817
+ ): void {
818
+ const imA = invMass[c.bodyA]
819
+ const imB = invMass[c.bodyB]
820
+ if (imA === 0 && imB === 0) return
821
+ const ai = c.bodyA * 3, bi = c.bodyB * 3
822
+ const rAx = c.rAx, rAy = c.rAy, rAz = c.rAz
823
+ const rBx = c.rBx, rBy = c.rBy, rBz = c.rBz
824
+
825
+ const vAx = lv[ai + 0] + av[ai + 1] * rAz - av[ai + 2] * rAy
826
+ const vAy = lv[ai + 1] + av[ai + 2] * rAx - av[ai + 0] * rAz
827
+ const vAz = lv[ai + 2] + av[ai + 0] * rAy - av[ai + 1] * rAx
828
+ const vBx = lv[bi + 0] + av[bi + 1] * rBz - av[bi + 2] * rBy
829
+ const vBy = lv[bi + 1] + av[bi + 2] * rBx - av[bi + 0] * rBz
830
+ const vBz = lv[bi + 2] + av[bi + 0] * rBy - av[bi + 1] * rBx
831
+ const dvx = vBx - vAx
832
+ const dvy = vBy - vAy
833
+ const dvz = vBz - vAz
834
+
835
+ // Normal row.
836
+ const jacInvN = c.jacInvN
837
+ if (jacInvN > 0) {
838
+ const nx = c.nx, ny = c.ny, nz = c.nz
839
+ const relVelN = dvx * nx + dvy * ny + dvz * nz
840
+ let dImpN = (c.bounceVel - relVelN) * jacInvN
841
+ const oldN = c.appliedNormalImpulse
842
+ let newN = oldN + dImpN
843
+ if (newN < 0) { newN = 0; dImpN = -oldN }
844
+ c.appliedNormalImpulse = newN
845
+ if (dImpN !== 0) {
846
+ const cAxN = c.cAxN, cAyN = c.cAyN, cAzN = c.cAzN
847
+ const cBxN = c.cBxN, cByN = c.cByN, cBzN = c.cBzN
848
+ if (imA > 0) {
849
+ lv[ai + 0] -= dImpN * imA * nx
850
+ lv[ai + 1] -= dImpN * imA * ny
851
+ lv[ai + 2] -= dImpN * imA * nz
852
+ av[ai + 0] -= dImpN * cAxN
853
+ av[ai + 1] -= dImpN * cAyN
854
+ av[ai + 2] -= dImpN * cAzN
855
+ }
856
+ if (imB > 0) {
857
+ lv[bi + 0] += dImpN * imB * nx
858
+ lv[bi + 1] += dImpN * imB * ny
859
+ lv[bi + 2] += dImpN * imB * nz
860
+ av[bi + 0] += dImpN * cBxN
861
+ av[bi + 1] += dImpN * cByN
862
+ av[bi + 2] += dImpN * cBzN
863
+ }
864
+ }
865
+ }
866
+
867
+ // Friction. Bound = ±μ · current normal impulse.
868
+ const muNormal = c.friction * c.appliedNormalImpulse
869
+ if (muNormal <= 0) return
870
+
871
+ // Re-read dv after the normal impulse possibly changed lv/av.
872
+ const vAx2 = lv[ai + 0] + av[ai + 1] * rAz - av[ai + 2] * rAy
873
+ const vAy2 = lv[ai + 1] + av[ai + 2] * rAx - av[ai + 0] * rAz
874
+ const vAz2 = lv[ai + 2] + av[ai + 0] * rAy - av[ai + 1] * rAx
875
+ const vBx2 = lv[bi + 0] + av[bi + 1] * rBz - av[bi + 2] * rBy
876
+ const vBy2 = lv[bi + 1] + av[bi + 2] * rBx - av[bi + 0] * rBz
877
+ const vBz2 = lv[bi + 2] + av[bi + 0] * rBy - av[bi + 1] * rBx
878
+ const dvx2 = vBx2 - vAx2
879
+ const dvy2 = vBy2 - vAy2
880
+ const dvz2 = vBz2 - vAz2
881
+
882
+ applyFrictionTangent(
883
+ c, ai, bi, dvx2, dvy2, dvz2,
884
+ c.t1x, c.t1y, c.t1z,
885
+ c.cAxT1, c.cAyT1, c.cAzT1, c.cBxT1, c.cByT1, c.cBzT1,
886
+ c.jacInvT1, muNormal, imA, imB, lv, av, 1,
887
+ )
888
+ applyFrictionTangent(
889
+ c, ai, bi, dvx2, dvy2, dvz2,
890
+ c.t2x, c.t2y, c.t2z,
891
+ c.cAxT2, c.cAyT2, c.cAzT2, c.cBxT2, c.cByT2, c.cBzT2,
892
+ c.jacInvT2, muNormal, imA, imB, lv, av, 2,
893
+ )
894
+ }
895
+
896
+ function applyFrictionTangent(
897
+ c: Contact,
898
+ ai: number, bi: number,
899
+ dvx: number, dvy: number, dvz: number,
900
+ tx: number, ty: number, tz: number,
901
+ cAx: number, cAy: number, cAz: number,
902
+ cBx: number, cBy: number, cBz: number,
903
+ jacInv: number, muNormal: number,
904
+ imA: number, imB: number,
905
+ lv: Float32Array, av: Float32Array,
906
+ slot: 1 | 2,
907
+ ): void {
908
+ if (jacInv <= 0) return
909
+ const relVel = dvx * tx + dvy * ty + dvz * tz
910
+ let dImp = -relVel * jacInv
911
+ const old = slot === 1 ? c.appliedFrictionImpulse1 : c.appliedFrictionImpulse2
912
+ let next = old + dImp
913
+ if (next < -muNormal) { next = -muNormal; dImp = next - old }
914
+ else if (next > muNormal) { next = muNormal; dImp = next - old }
915
+ if (slot === 1) c.appliedFrictionImpulse1 = next
916
+ else c.appliedFrictionImpulse2 = next
917
+
918
+ if (dImp === 0) return
919
+ if (imA > 0) {
920
+ lv[ai + 0] -= dImp * imA * tx
921
+ lv[ai + 1] -= dImp * imA * ty
922
+ lv[ai + 2] -= dImp * imA * tz
923
+ av[ai + 0] -= dImp * cAx
924
+ av[ai + 1] -= dImp * cAy
925
+ av[ai + 2] -= dImp * cAz
926
+ }
927
+ if (imB > 0) {
928
+ lv[bi + 0] += dImp * imB * tx
929
+ lv[bi + 1] += dImp * imB * ty
930
+ lv[bi + 2] += dImp * imB * tz
931
+ av[bi + 0] += dImp * cBx
932
+ av[bi + 1] += dImp * cBy
933
+ av[bi + 2] += dImp * cBz
934
+ }
935
+ }
936
+
937
+ function buildBodyMat(store: RigidBodyStore, i: number, out: Float32Array): void {
938
+ const i3 = i * 3, i4 = i * 4
939
+ Mat4.fromPositionRotationInto(
940
+ store.positions[i3 + 0], store.positions[i3 + 1], store.positions[i3 + 2],
941
+ store.orientations[i4 + 0], store.orientations[i4 + 1], store.orientations[i4 + 2], store.orientations[i4 + 3],
942
+ out,
943
+ )
944
+ }
945
+
946
+ // Quaternion of qx(x) ⊗ qy(y) ⊗ qz(z) (three.js 'XYZ' order).
947
+ function eulerXYZQuatInto(x: number, y: number, z: number, out: Float32Array): void {
948
+ const sx = Math.sin(x * 0.5), cx = Math.cos(x * 0.5)
949
+ const sy = Math.sin(y * 0.5), cy = Math.cos(y * 0.5)
950
+ const sz = Math.sin(z * 0.5), cz = Math.cos(z * 0.5)
951
+ out[0] = sx * cy * cz + cx * sy * sz
952
+ out[1] = cx * sy * cz - sx * cy * sz
953
+ out[2] = cx * cy * sz + sx * sy * cz
954
+ out[3] = cx * cy * cz - sx * sy * sz
955
+ }
956
+
957
+ // Euler XYZ from a 3×3 rotation matrix (row-major elements).
958
+ function matrixToEulerXYZ(
959
+ r00: number, r01: number,
960
+ r10: number, r11: number,
961
+ r20: number, r21: number, r22: number,
962
+ out: Float32Array,
963
+ ): void {
964
+ if (r20 < 1) {
965
+ if (r20 > -1) {
966
+ out[0] = Math.atan2(-r21, r22)
967
+ out[1] = Math.asin(r20)
968
+ out[2] = Math.atan2(-r10, r00)
969
+ } else {
970
+ out[0] = -Math.atan2(r01, r11)
971
+ out[1] = -Math.PI * 0.5
972
+ out[2] = 0
973
+ }
974
+ } else {
975
+ out[0] = Math.atan2(r01, r11)
976
+ out[1] = Math.PI * 0.5
977
+ out[2] = 0
978
+ }
979
+ }