reze-engine 0.36.0 → 0.36.1

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.
@@ -13,29 +13,63 @@
13
13
  import { Mat4 } from "../math";
14
14
  import { STOP_ERP } from "./constraint";
15
15
  const BOUNCE_THRESHOLD = 2.0;
16
- // Successive over-relaxation factor on CONTACT rows only (joints untouched).
17
- // Each contact row independently drives the relative velocity at its own point
18
- // to zero; when a dress panel carries up to 43 of them at once, they all
19
- // correct the same motion and the body is over-braked, differently every
20
- // substep. Scaling each row's step damps that without changing the fixed
21
- // point — the accumulated impulse still converges to the same answer, just
22
- // approached rather than overshot.
16
+ // Contact error-reduction parameter btContactSolverInfo::m_erp in Bullet
17
+ // 2.75, the build MMD's own physics runs, at its stock 0.2. PMX rigs are
18
+ // authored against exactly this response.
23
19
  //
24
- // The gain is PER CONTACT, scaled by how contended its two bodies are, not a
25
- // flat constant. A flat factor was measured first and is wrong: it also slows
26
- // the well-conditioned rows, and a body resting on a SINGLE contact can then
27
- // no longer cancel its approach velocity within the iteration budget, so it
28
- // creeps forever instead of settling (托特 peak speed at 15 s: 0.11 at gain
29
- // 1.0, 0.46 at 0.5, 0.72 at 0.3 — a permanent limit cycle on a one-contact
30
- // body). Over-constraint is a local property, so the remedy has to be local.
20
+ // Bullet folds penetration recovery into the CONTACT VELOCITY ROW rather than
21
+ // translating positions (m_splitImpulse defaults to false in 2.75):
31
22
  //
32
- // gain = 1 / max(rows on A, rows on B), floored. One contact → 1.0, exact and
33
- // settling preserved; a dress panel sharing 20 rows → 0.05, damped. This is
34
- // the standard mass-splitting blend toward Jacobi in the contended cluster.
35
- const CONTACT_SOR_MIN = 0.12;
36
- // Per-body contact-row counts for the gain above; grown on demand, refilled
37
- // each substep. Module-level so the solve allocates nothing.
38
- let _rowCount = new Int32Array(0);
23
+ // penetration = cp.getDistance() + m_linearSlop // < 0 when overlapping
24
+ // positionalError = -penetration * m_erp / m_timeStep
25
+ // velocityError = restitution - rel_vel
26
+ // m_rhs = (positionalError + velocityError) * m_jacDiagABInv
27
+ //
28
+ // Our `depth` is the negation of Bullet's distance, so positionalError is just
29
+ // depth·ERP/dt one expression covering both cases. Penetrating (depth > 0) it
30
+ // pushes apart; separated (depth < 0, a speculative row) it ALLOWS approach at
31
+ // the rate that closes the gap, which is what keeps those rows inert until the
32
+ // body would really arrive.
33
+ const CONTACT_ERP = 0.2;
34
+ // btContactSolverInfo's SOLVER_RANDMIZE_ORDER. Gauss-Seidel is order-biased —
35
+ // rows solved first win — which on a cross-linked lattice shows up as a lean or
36
+ // a period-2 chatter. Bullet ships the flag off; it is here to be measured.
37
+ const RANDOMIZE_ORDER = true;
38
+ // btSequentialImpulseConstraintSolver::btRand2, so the shuffle is reproducible
39
+ // and a take replays identically.
40
+ let _seed = 0;
41
+ function rand2() {
42
+ _seed = (1664525 * _seed + 1013904223) & 0xffffffff;
43
+ return _seed >>> 0;
44
+ }
45
+ let _order = new Int32Array(0);
46
+ // btContactSolverInfo::m_warmstartingFactor. Seeded impulses are scaled by
47
+ // this so a stale cache decays instead of compounding.
48
+ const WARMSTARTING_FACTOR = 0.85;
49
+ // btContactSolverInfo::m_restingContactRestitutionThreshold — past this many
50
+ // substeps a contact is "resting" and stops bouncing.
51
+ const RESTING_CONTACT_AGE = 2;
52
+ // btContactSolverInfo::m_linearSlop is 0 in 2.75 — no allowance, the row aims
53
+ // at exactly touching. Kept named so the divergence is visible if it ever moves.
54
+ const LINEAR_SLOP = 0;
55
+ // btContactSolverInfo::m_splitImpulse. False in 2.75 (only the demos turn it
56
+ // on), but TRUE by default from Bullet 2.8x onward — which is what babylon-mmd
57
+ // runs today. MMD is closed-source so its own setting cannot be read, but it is
58
+ // widely reported to enable the flag, and our own measurements agree
59
+ // independently: turning it on lowered the velocity-reversal rate on every rig
60
+ // tested (伊邪那美 0.118 → 0.098, 诗蔻蒂 0.098 → 0.074, 托特 0.054 → 0.028).
61
+ // With it on, the penetration term is moved out of the real
62
+ // velocity row into a pseudo-velocity ("push") channel that is solved
63
+ // separately and integrated straight into the transform, so recovering overlap
64
+ // never adds momentum the joint springs can hand back.
65
+ const SPLIT_IMPULSE = true;
66
+ // btContactSolverInfo::m_splitImpulsePenetrationThreshold. Bullet's sign: only
67
+ // contacts DEEPER than this go to the split channel; shallower ones keep the
68
+ // bias in the real row. Our depth is the negation, hence the flipped compare.
69
+ const SPLIT_PENETRATION_THRESHOLD = 0.02;
70
+ // Pseudo-velocity accumulators for the split channel, zeroed each substep.
71
+ let _pushLin = new Float32Array(0);
72
+ let _pushAng = new Float32Array(0);
39
73
  // Ceilings on limit-correction velocity. In normal operation limit errors are
40
74
  // tiny; a large error only appears after a discontinuity (teleport, stall,
41
75
  // deep penetration), and feeding err·ERP/dt to the solver unclamped then
@@ -57,7 +91,44 @@ const LIMIT_SOFTNESS_ANGULAR = 0.5;
57
91
  // error), so resting chains rang forever — the "static dress slowly boils"
58
92
  // bug. c is derived per row from a fixed damping ratio against the row's
59
93
  // effective mass: c = 2ζ√(k·m_eff).
60
- const SPRING_DAMPING_ZETA = 0.7;
94
+ // btGeneric6DofSpringConstraint drives a spring as a MOTOR, not as a
95
+ // spring-damper: stiffness becomes a velocity target with a force cap.
96
+ //
97
+ // velFactor = fps · damping / numIterations
98
+ // targetVelocity = velFactor · force (force = err · k)
99
+ // maxMotorForce = |force| / fps
100
+ //
101
+ // m_springDamping defaults to 1.0 and PMX carries no damping field, so every
102
+ // MMD spring runs at exactly that — which is what these rigs were authored
103
+ // against. This replaces an implicit spring-damper with an invented damping
104
+ // ratio (ζ = 0.7); ported in isolation the motor form measured 0.0015 mean
105
+ // reversal at jerk 0.5, the calmest joint-side result recorded.
106
+ const SPRING_DAMPING = 1.0;
107
+ // btConstraintInfo2::erp, from infoGlobal.m_erp — used only by getMotorFactor.
108
+ const INFO_ERP = 0.2;
109
+ // btTypedConstraint::getMotorFactor. Scales a motor down as it approaches the
110
+ // limit it is driving toward, so a stiff spring cannot drive straight through
111
+ // its own stop and then be fought by the limit row. That fight is a candidate
112
+ // explanation for the period-2 joint chatter seen on bodies carrying NO
113
+ // contacts at all (qh_15_5: accumulated joint impulse alternating 8.4 / 14.7).
114
+ function getMotorFactor(pos, lowLim, uppLim, vel, timeFact) {
115
+ if (lowLim > uppLim)
116
+ return 1;
117
+ if (lowLim === uppLim)
118
+ return 0;
119
+ const deltaMax = vel / timeFact;
120
+ if (deltaMax < 0) {
121
+ if (pos >= lowLim && pos < lowLim - deltaMax)
122
+ return (lowLim - pos) / deltaMax;
123
+ return pos < lowLim ? 0 : 1;
124
+ }
125
+ if (deltaMax > 0) {
126
+ if (pos <= uppLim && pos > uppLim - deltaMax)
127
+ return (uppLim - pos) / deltaMax;
128
+ return pos > uppLim ? 0 : 1;
129
+ }
130
+ return 0;
131
+ }
61
132
  // ERP scale for loop-closing constraints (see buildConstraints: joints that
62
133
  // close a cycle in the joint graph, e.g. the horizontal ring welds of
63
134
  // cross-linked skirt lattices). A loop over-determines positions — when
@@ -66,11 +137,6 @@ const SPRING_DAMPING_ZETA = 0.7;
66
137
  // chatter. Loop edges keep shape at a fraction of the correction rate while
67
138
  // the spanning-tree chains stay stiff.
68
139
  const LOOP_ERP_SCALE = 1.0;
69
- // Loop-edge LOCKED axes are converted to force-clamped springs instead of
70
- // weld rows: an equality row on a cycle fights the other cycle edges at any
71
- // ERP (the velocity system is over-determined too). A spring bounded by its
72
- // real force k·|err|·dt holds the ring's shape elastically without fighting.
73
- const LOOP_SPRING_K = 900;
74
140
  // Angular limit violations below this switch to per-axis euler rows; above
75
141
  // it, the single geodesic row takes over (see setupConstraint).
76
142
  const GEODESIC_THRESHOLD = 0.5; // rad
@@ -82,7 +148,7 @@ const _bodyMatB = new Float32Array(16);
82
148
  const _angDiffScratch = new Float32Array(3);
83
149
  const _quatScratchA = new Float32Array(4);
84
150
  const _quatScratchB = new Float32Array(4);
85
- export function solveConstraints(store, constraints, cache, contacts, dt, iterations) {
151
+ export function solveConstraints(store, constraints, cache, contacts, dt, iterations, manifolds = null) {
86
152
  if (dt <= 0)
87
153
  return;
88
154
  if (constraints.length === 0 && contacts.count === 0)
@@ -95,36 +161,189 @@ export function solveConstraints(store, constraints, cache, contacts, dt, iterat
95
161
  store.updateInvInertiaWorld();
96
162
  const W = store.invInertiaWorld;
97
163
  for (let c = 0; c < constraints.length; c++) {
98
- setupConstraint(constraints[c], c, cache, store, dt, invDt);
99
- }
100
- // How many contact rows each body carries this substep — the per-contact
101
- // relaxation gain below is a function of the more contended of its two
102
- // bodies. Counting is O(contacts), done once, before any setup.
103
- if (_rowCount.length < store.count)
104
- _rowCount = new Int32Array(store.count);
105
- else
106
- _rowCount.fill(0, 0, store.count);
107
- // Only DYNAMIC bodies are counted. A static body — above all the ground —
108
- // collects a row from every body resting on it, and letting that inflate the
109
- // count crushes the gain of each of those contacts to the floor, so nothing
110
- // resting on the ground can build enough impulse and it creeps instead of
111
- // settling. A body with invMass 0 cannot be over-braked in the first place.
112
- for (let ci = 0; ci < contacts.count; ci++) {
113
- const c = contacts.get(ci);
114
- if (invMass[c.bodyA] > 0)
115
- _rowCount[c.bodyA]++;
116
- if (invMass[c.bodyB] > 0)
117
- _rowCount[c.bodyB]++;
164
+ setupConstraint(constraints[c], c, cache, store, dt, invDt, iterations);
118
165
  }
119
166
  for (let ci = 0; ci < contacts.count; ci++) {
120
167
  setupContactRow(contacts.get(ci), lv, av, invMass, W, invDt);
121
168
  }
169
+ // Warm start: seed each row from what the same point converged to last
170
+ // substep and APPLY it before iterating, exactly as Bullet does at setup.
171
+ if (manifolds !== null) {
172
+ for (let ci = 0; ci < contacts.count; ci++) {
173
+ warmStartContact(contacts.get(ci), store, manifolds, lv, av, invMass);
174
+ }
175
+ }
176
+ // Bullet 2.75 solves each iteration in three separate passes, in this order:
177
+ // all joint rows, then all contact normal rows, then all friction rows. The
178
+ // friction bound is read from the contact's accumulated impulse AS IT STANDS
179
+ // after the normal pass, so friction tightens as the normal converges. Doing
180
+ // friction inline per contact (our previous shape) bounds it against a normal
181
+ // impulse that half the manifold has not contributed to yet.
182
+ if (RANDOMIZE_ORDER) {
183
+ // Reset per substep. Bullet keeps the seed on the solver instance; ours is
184
+ // module-level, so without this two worlds in one process share a stream
185
+ // and the same scene stops replaying identically — which the determinism
186
+ // test catches. Re-seeding costs nothing: the point is breaking the order
187
+ // bias BETWEEN iterations, not being unpredictable across frames.
188
+ _seed = 0;
189
+ if (_order.length < contacts.count)
190
+ _order = new Int32Array(contacts.count);
191
+ for (let i = 0; i < contacts.count; i++)
192
+ _order[i] = i;
193
+ }
122
194
  for (let iter = 0; iter < iterations; iter++) {
195
+ if (RANDOMIZE_ORDER) {
196
+ // Bullet reshuffles between iterations, not once per substep.
197
+ for (let i = 1; i < contacts.count; i++) {
198
+ const j = rand2() % (i + 1);
199
+ const tmp = _order[i];
200
+ _order[i] = _order[j];
201
+ _order[j] = tmp;
202
+ }
203
+ }
123
204
  for (let c = 0; c < constraints.length; c++) {
124
205
  iterateConstraint(c, cache, lv, av, invMass);
125
206
  }
126
207
  for (let ci = 0; ci < contacts.count; ci++) {
127
- iterateContactRow(contacts.get(ci), lv, av, invMass);
208
+ iterateContactNormal(contacts.get(RANDOMIZE_ORDER ? _order[ci] : ci), lv, av, invMass);
209
+ }
210
+ for (let ci = 0; ci < contacts.count; ci++) {
211
+ iterateContactFriction(contacts.get(ci), lv, av, invMass);
212
+ }
213
+ }
214
+ // Split-impulse pass: its OWN full set of iterations over the penetration
215
+ // channel only, exactly as Bullet runs it after the main loop.
216
+ if (SPLIT_IMPULSE) {
217
+ const n3 = store.count * 3;
218
+ if (_pushLin.length < n3) {
219
+ _pushLin = new Float32Array(n3);
220
+ _pushAng = new Float32Array(n3);
221
+ }
222
+ else {
223
+ _pushLin.fill(0, 0, n3);
224
+ _pushAng.fill(0, 0, n3);
225
+ }
226
+ let any = false;
227
+ for (let ci = 0; ci < contacts.count; ci++)
228
+ if (contacts.get(ci).rhsPenetration !== 0) {
229
+ any = true;
230
+ break;
231
+ }
232
+ if (any) {
233
+ if (RANDOMIZE_ORDER) {
234
+ // Reset per substep. Bullet keeps the seed on the solver instance; ours is
235
+ // module-level, so without this two worlds in one process share a stream
236
+ // and the same scene stops replaying identically — which the determinism
237
+ // test catches. Re-seeding costs nothing: the point is breaking the order
238
+ // bias BETWEEN iterations, not being unpredictable across frames.
239
+ _seed = 0;
240
+ if (_order.length < contacts.count)
241
+ _order = new Int32Array(contacts.count);
242
+ for (let i = 0; i < contacts.count; i++)
243
+ _order[i] = i;
244
+ }
245
+ for (let iter = 0; iter < iterations; iter++) {
246
+ if (RANDOMIZE_ORDER) {
247
+ // Bullet reshuffles between iterations, not once per substep.
248
+ for (let i = 1; i < contacts.count; i++) {
249
+ const j = rand2() % (i + 1);
250
+ const tmp = _order[i];
251
+ _order[i] = _order[j];
252
+ _order[j] = tmp;
253
+ }
254
+ }
255
+ for (let ci = 0; ci < contacts.count; ci++) {
256
+ resolveSplitPenetration(contacts.get(ci), invMass);
257
+ }
258
+ }
259
+ }
260
+ }
261
+ }
262
+ // Bullet's resolveSplitPenetrationImpulse: same Jacobian, but reading and
263
+ // writing the pseudo-velocity channel, with its own push-only accumulator.
264
+ function resolveSplitPenetration(c, invMass) {
265
+ if (c.rhsPenetration === 0)
266
+ return;
267
+ const jacInvN = c.jacInvN;
268
+ if (jacInvN <= 0)
269
+ return;
270
+ const imA = invMass[c.bodyA];
271
+ const imB = invMass[c.bodyB];
272
+ if (imA === 0 && imB === 0)
273
+ return;
274
+ const ai = c.bodyA * 3, bi = c.bodyB * 3;
275
+ const nx = c.nx, ny = c.ny, nz = c.nz;
276
+ const rAx = c.rAx, rAy = c.rAy, rAz = c.rAz;
277
+ const rBx = c.rBx, rBy = c.rBy, rBz = c.rBz;
278
+ const vAx = _pushLin[ai + 0] + _pushAng[ai + 1] * rAz - _pushAng[ai + 2] * rAy;
279
+ const vAy = _pushLin[ai + 1] + _pushAng[ai + 2] * rAx - _pushAng[ai + 0] * rAz;
280
+ const vAz = _pushLin[ai + 2] + _pushAng[ai + 0] * rAy - _pushAng[ai + 1] * rAx;
281
+ const vBx = _pushLin[bi + 0] + _pushAng[bi + 1] * rBz - _pushAng[bi + 2] * rBy;
282
+ const vBy = _pushLin[bi + 1] + _pushAng[bi + 2] * rBx - _pushAng[bi + 0] * rBz;
283
+ const vBz = _pushLin[bi + 2] + _pushAng[bi + 0] * rBy - _pushAng[bi + 1] * rBx;
284
+ const relVel = (vBx - vAx) * nx + (vBy - vAy) * ny + (vBz - vAz) * nz;
285
+ let d = (c.rhsPenetration - relVel) * jacInvN;
286
+ const old = c.appliedPushImpulse;
287
+ let sum = old + d;
288
+ if (sum < 0) {
289
+ sum = 0;
290
+ d = -old;
291
+ }
292
+ c.appliedPushImpulse = sum;
293
+ if (d === 0)
294
+ return;
295
+ if (imA > 0) {
296
+ _pushLin[ai + 0] -= d * imA * nx;
297
+ _pushLin[ai + 1] -= d * imA * ny;
298
+ _pushLin[ai + 2] -= d * imA * nz;
299
+ _pushAng[ai + 0] -= d * c.cAxN;
300
+ _pushAng[ai + 1] -= d * c.cAyN;
301
+ _pushAng[ai + 2] -= d * c.cAzN;
302
+ }
303
+ if (imB > 0) {
304
+ _pushLin[bi + 0] += d * imB * nx;
305
+ _pushLin[bi + 1] += d * imB * ny;
306
+ _pushLin[bi + 2] += d * imB * nz;
307
+ _pushAng[bi + 0] += d * c.cBxN;
308
+ _pushAng[bi + 1] += d * c.cByN;
309
+ _pushAng[bi + 2] += d * c.cBzN;
310
+ }
311
+ }
312
+ /** Integrate the accumulated push/turn velocity straight into the transform —
313
+ * btSolverBody::writebackVelocity(timeStep). Never touches real momentum. */
314
+ export function applySplitImpulsePush(store, dt) {
315
+ if (!SPLIT_IMPULSE || _pushLin.length === 0)
316
+ return;
317
+ const pos = store.positions;
318
+ const ori = store.orientations;
319
+ const invMass = store.invMass;
320
+ for (let i = 0; i < store.count; i++) {
321
+ if (invMass[i] <= 0)
322
+ continue;
323
+ const i3 = i * 3, i4 = i * 4;
324
+ const px = _pushLin[i3 + 0], py = _pushLin[i3 + 1], pz = _pushLin[i3 + 2];
325
+ const wx = _pushAng[i3 + 0], wy = _pushAng[i3 + 1], wz = _pushAng[i3 + 2];
326
+ if (px === 0 && py === 0 && pz === 0 && wx === 0 && wy === 0 && wz === 0)
327
+ continue;
328
+ pos[i3 + 0] += px * dt;
329
+ pos[i3 + 1] += py * dt;
330
+ pos[i3 + 2] += pz * dt;
331
+ if (wx !== 0 || wy !== 0 || wz !== 0) {
332
+ const qx = ori[i4 + 0], qy = ori[i4 + 1], qz = ori[i4 + 2], qw = ori[i4 + 3];
333
+ const dx = qw * wx + wy * qz - wz * qy;
334
+ const dy = qw * wy + wz * qx - wx * qz;
335
+ const dz = qw * wz + wx * qy - wy * qx;
336
+ const dw = -(wx * qx + wy * qy + wz * qz);
337
+ const h = 0.5 * dt;
338
+ let nx2 = qx + dx * h, ny2 = qy + dy * h, nz2 = qz + dz * h, nw2 = qw + dw * h;
339
+ const l2 = nx2 * nx2 + ny2 * ny2 + nz2 * nz2 + nw2 * nw2;
340
+ if (l2 > 0) {
341
+ const inv = 1 / Math.sqrt(l2);
342
+ ori[i4 + 0] = nx2 * inv;
343
+ ori[i4 + 1] = ny2 * inv;
344
+ ori[i4 + 2] = nz2 * inv;
345
+ ori[i4 + 3] = nw2 * inv;
346
+ }
128
347
  }
129
348
  }
130
349
  }
@@ -180,7 +399,7 @@ export class SolverCache {
180
399
  }
181
400
  // SETUP: compute everything that doesn't depend on velocities. Caller
182
401
  // guarantees pos/ori don't change between this and the iter loop.
183
- function setupConstraint(con, ci, cache, store, dt, invDt) {
402
+ function setupConstraint(con, ci, cache, store, dt, invDt, iterations) {
184
403
  const F = cache.F;
185
404
  const I = cache.I;
186
405
  const D = cache.D;
@@ -306,16 +525,20 @@ function setupConstraint(con, ci, cache, store, dt, invDt) {
306
525
  // iteration (PMX rigs routinely put k=100000 springs on locked axes,
307
526
  // which turned welded weight-bodies into energy pumps).
308
527
  if (con.springEnabled[i] && denom > 0 && lo !== hi) {
309
- // Implicit spring-damper (see SPRING_DAMPING_ZETA). Stored per axis:
310
- // cacheLinSpringTarget = −(k/γ)·err (the bias, in velocity units) and
311
- // cacheLinSpringMaxImp = s (the CFM softness NOT a clamp anymore).
528
+ // Bullet motor form. Our relVel is (vB − vA)·axis where Bullet's is the
529
+ // negation, so the target carries the opposite sign to Bullet's — which
530
+ // is the same sign the old spring-damper produced, so behaviour keeps its
531
+ // direction and only its magnitude law changes.
312
532
  const k = con.springStiffness[i];
313
533
  const serr = curr - con.equilibriumPoint[i];
314
- const meff = 1 / denom;
315
- const c = 2 * SPRING_DAMPING_ZETA * Math.sqrt(k * meff);
316
- const gamma = c + dt * k;
317
- F[base + LIN_SPR_TGT + i] = -(k / gamma) * serr;
318
- F[base + LIN_SPR_MAX + i] = 1 / (dt * gamma);
534
+ const force = serr * k;
535
+ const velFactor = (invDt * SPRING_DAMPING) / iterations;
536
+ const target = -velFactor * force;
537
+ // getMotorFactor's `vel` is Bullet's tag_vel: −targetVelocity for a
538
+ // linear axis, which in our sign convention is the target itself.
539
+ const motFact = getMotorFactor(curr, lo, hi, target, invDt * INFO_ERP);
540
+ F[base + LIN_SPR_TGT + i] = motFact * target;
541
+ F[base + LIN_SPR_MAX + i] = Math.abs(force) * dt;
319
542
  F[base + LIN_SPR_IMP + i] = 0;
320
543
  I[ib + I_LIN_SPR_ACT + i] = 1;
321
544
  }
@@ -406,16 +629,18 @@ function setupConstraint(con, ci, cache, store, dt, invDt) {
406
629
  // Springs on locked axes are skipped — the limit row welds those, and
407
630
  // double-driving a DOF overshoots every iteration (see the linear loop).
408
631
  if (con.springEnabled[idx] && F[angJac + i] > 0 && con.angularMin[i] !== con.angularMax[i]) {
409
- // Implicit spring-damper, angular flavor. F[angJac + i] = 1/denom IS the
410
- // row's effective inertia. Sign flip vs linear: d(err)/dt = −relAv, so
411
- // the bias is positive for positive error. cacheAngSpringMaxImp holds
412
- // the CFM softness s (not a clamp).
632
+ // Bullet motor form, angular flavour. Bullet's angular force is −err·k
633
+ // and its rel_vel is the negation of our relAv, so the two flips cancel
634
+ // and our target stays positive for positive error, as before.
413
635
  const k = con.springStiffness[idx];
414
636
  const serr = _angDiffScratch[i] - con.equilibriumPoint[idx];
415
- const c = 2 * SPRING_DAMPING_ZETA * Math.sqrt(k * F[angJac + i]);
416
- const gamma = c + dt * k;
417
- F[angTgt + i] = (k / gamma) * serr;
418
- F[base + ANG_SPR_MAX + i] = 1 / (dt * gamma);
637
+ const force = -serr * k;
638
+ const velFactor = (invDt * SPRING_DAMPING) / iterations;
639
+ const target = -velFactor * force;
640
+ // tag_vel for a rotational axis is Bullet's targetVelocity, i.e. −ours.
641
+ const motFact = getMotorFactor(_angDiffScratch[i], con.angularMin[i], con.angularMax[i], -target, invDt * INFO_ERP);
642
+ F[angTgt + i] = motFact * target;
643
+ F[base + ANG_SPR_MAX + i] = Math.abs(force) * dt;
419
644
  I[angAct + i] = 1;
420
645
  }
421
646
  else {
@@ -595,9 +820,18 @@ function iterateConstraint(ci, cache, lv, av, invMass) {
595
820
  // the spring off the stale value double-corrects the DOF.
596
821
  if (I[sprAct + i]) {
597
822
  const relVelNow = j !== 0 ? relVel + j / F[jac + i] : relVel;
598
- const s = F[sprMax + i]; // CFM softness
599
- const dImp = (F[sprTgt + i] - relVelNow - s * F[sprImp + i]) / (1 / F[jac + i] + s);
600
- F[sprImp + i] += dImp;
823
+ // Motor row: drive toward the target, accumulated impulse clamped to the
824
+ // spring's real force over the step (Bullet's m_lowerLimit/m_upperLimit).
825
+ let dImp = (F[sprTgt + i] - relVelNow) * F[jac + i];
826
+ const maxF = F[sprMax + i];
827
+ const oldS = F[sprImp + i];
828
+ let nextS = oldS + dImp;
829
+ if (nextS < -maxF)
830
+ nextS = -maxF;
831
+ else if (nextS > maxF)
832
+ nextS = maxF;
833
+ dImp = nextS - oldS;
834
+ F[sprImp + i] = nextS;
601
835
  j += dImp;
602
836
  }
603
837
  if (j === 0)
@@ -638,9 +872,16 @@ function iterateConstraint(ci, cache, lv, av, invMass) {
638
872
  const axx = F[angAxes + o + 0], axy = F[angAxes + o + 1], axz = F[angAxes + o + 2];
639
873
  const relAv = dax * axx + day * axy + daz * axz;
640
874
  // Implicit spring-damper row (soft constraint, see setup).
641
- const s = F[angSprMax + i]; // CFM softness
642
- const j = (F[angTgt + i] - relAv - s * F[angSprImp + i]) / (1 / F[angJac + i] + s);
643
- F[angSprImp + i] += j;
875
+ let j = (F[angTgt + i] - relAv) * F[angJac + i];
876
+ const maxF = F[angSprMax + i];
877
+ const oldS = F[angSprImp + i];
878
+ let nextS = oldS + j;
879
+ if (nextS < -maxF)
880
+ nextS = -maxF;
881
+ else if (nextS > maxF)
882
+ nextS = maxF;
883
+ j = nextS - oldS;
884
+ F[angSprImp + i] = nextS;
644
885
  if (j === 0)
645
886
  continue;
646
887
  if (imA > 0) {
@@ -774,8 +1015,14 @@ function setupContactRow(c, lv, av, invMass, W, invDt) {
774
1015
  const vBx = lv[bi + 0] + av[bi + 1] * rBz - av[bi + 2] * rBy;
775
1016
  const vBy = lv[bi + 1] + av[bi + 2] * rBx - av[bi + 0] * rBz;
776
1017
  const vBz = lv[bi + 2] + av[bi + 0] * rBy - av[bi + 1] * rBx;
777
- const relVelN0 = (vBx - vAx) * nx + (vBy - vAy) * ny + (vBz - vAz) * nz;
778
- c.bounceVel = c.restitution > 0 && relVelN0 < -BOUNCE_THRESHOLD
1018
+ const dvx = vBx - vAx;
1019
+ const dvy = vBy - vAy;
1020
+ const dvz = vBz - vAz;
1021
+ const relVelN0 = dvx * nx + dvy * ny + dvz * nz;
1022
+ // Restitution is switched off once a point has been in contact for more than
1023
+ // a couple of substeps (m_restingContactRestitutionThreshold), or resting
1024
+ // cloth keeps being handed a little bounce forever.
1025
+ c.bounceVel = c.restitution > 0 && relVelN0 < -BOUNCE_THRESHOLD && c.age <= RESTING_CONTACT_AGE
779
1026
  ? -c.restitution * relVelN0
780
1027
  : 0;
781
1028
  // Speculative rows (depth < 0 — the shapes are inside the margin band but
@@ -790,39 +1037,66 @@ function setupContactRow(c, lv, av, invMass, W, invDt) {
790
1037
  // (pulling) impulse, not a large positive one on a body in mid-air. On a
791
1038
  // dress rig half of all contact rows are speculative and 88% of them fire,
792
1039
  // which is the field of invisible brakes the cloth was shaking against.
793
- c.allowedApproachVel = c.depth < 0 ? -c.depth * invDt : 0;
794
- // Relaxation gain, from the more contended of the two bodies (see
795
- // CONTACT_SOR_MIN). A lone contact keeps gain 1.0 and stays exact.
796
- const contended = _rowCount[c.bodyA] > _rowCount[c.bodyB] ? _rowCount[c.bodyA] : _rowCount[c.bodyB];
797
- const gain = contended > 1 ? 1 / contended : 1;
798
- c.sorGain = gain < CONTACT_SOR_MIN ? CONTACT_SOR_MIN : gain;
799
- // Friction tangent basis. Pick the axis least aligned with n.
800
- let t1x, t1y, t1z;
801
- if (Math.abs(nx) < 0.7071) {
802
- t1x = 0;
803
- t1y = -nz;
804
- t1z = ny;
1040
+ const positionalError = (c.depth - LINEAR_SLOP) * CONTACT_ERP * invDt;
1041
+ if (SPLIT_IMPULSE && c.depth > SPLIT_PENETRATION_THRESHOLD) {
1042
+ c.biasVel = 0;
1043
+ c.rhsPenetration = positionalError;
805
1044
  }
806
1045
  else {
807
- t1x = nz;
808
- t1y = 0;
809
- t1z = -nx;
1046
+ c.biasVel = positionalError;
1047
+ c.rhsPenetration = 0;
810
1048
  }
811
- const tl = Math.hypot(t1x, t1y, t1z);
812
- if (tl > 1e-8) {
813
- const tInv = 1 / tl;
814
- t1x *= tInv;
815
- t1y *= tInv;
816
- t1z *= tInv;
1049
+ // Friction direction, Bullet 2.75 style. The stock solverMode is
1050
+ // SOLVER_USE_WARMSTARTING | SOLVER_SIMD — SOLVER_USE_2_FRICTION_DIRECTIONS is
1051
+ // NOT set, so there is exactly ONE friction row, aligned with the direction
1052
+ // the contact is actually sliding:
1053
+ //
1054
+ // m_lateralFrictionDir1 = vel - normalWorldOnB * rel_vel
1055
+ //
1056
+ // with vel = vA − vB. Our dv is vB − vA, so that direction is the negation of
1057
+ // dv's tangential part; sign is irrelevant to a row with symmetric ±μN bounds.
1058
+ // Below SIMD_EPSILON of sliding Bullet falls back to btPlaneSpace1, ported
1059
+ // exactly. Direction is recomputed every substep — friction-direction caching
1060
+ // is off in the stock mode.
1061
+ let t1x = dvx - nx * relVelN0;
1062
+ let t1y = dvy - ny * relVelN0;
1063
+ let t1z = dvz - nz * relVelN0;
1064
+ let t2x, t2y, t2z;
1065
+ const lat2 = t1x * t1x + t1y * t1y + t1z * t1z;
1066
+ if (lat2 > 1.192092896e-07) {
1067
+ const inv = 1 / Math.sqrt(lat2);
1068
+ t1x *= inv;
1069
+ t1y *= inv;
1070
+ t1z *= inv;
1071
+ // Second tangent is unused (single-direction mode) but kept orthonormal so
1072
+ // the cached Jacobian slots stay well-defined.
1073
+ t2x = ny * t1z - nz * t1y;
1074
+ t2y = nz * t1x - nx * t1z;
1075
+ t2z = nx * t1y - ny * t1x;
817
1076
  }
818
1077
  else {
819
- c.jacInvT1 = 0;
820
- c.jacInvT2 = 0;
821
- return;
1078
+ // btPlaneSpace1, verbatim.
1079
+ if (Math.abs(nz) > 0.7071067811865475) {
1080
+ const a = ny * ny + nz * nz;
1081
+ const k = 1 / Math.sqrt(a);
1082
+ t1x = 0;
1083
+ t1y = -nz * k;
1084
+ t1z = ny * k;
1085
+ t2x = a * k;
1086
+ t2y = -nx * t1z;
1087
+ t2z = nx * t1y;
1088
+ }
1089
+ else {
1090
+ const a = nx * nx + ny * ny;
1091
+ const k = 1 / Math.sqrt(a);
1092
+ t1x = -ny * k;
1093
+ t1y = nx * k;
1094
+ t1z = 0;
1095
+ t2x = -nz * t1y;
1096
+ t2y = nz * t1x;
1097
+ t2z = a * k;
1098
+ }
822
1099
  }
823
- const t2x = ny * t1z - nz * t1y;
824
- const t2y = nz * t1x - nx * t1z;
825
- const t2z = nx * t1y - ny * t1x;
826
1100
  c.t1x = t1x;
827
1101
  c.t1y = t1y;
828
1102
  c.t1z = t1z;
@@ -875,10 +1149,13 @@ function setupContactRow(c, lv, av, invMass, W, invDt) {
875
1149
  c.cBzT2 = wBzT2;
876
1150
  c.jacInvT2 = denomT2 > 0 ? 1 / denomT2 : 0;
877
1151
  }
878
- // ITER: one push-only normal row + two Coulomb friction rows. Friction
879
- // bound depends on the *current* applied normal impulse, so it tightens
880
- // as the normal row converges.
881
- function iterateContactRow(c, lv, av, invMass) {
1152
+ // ITER, normal row only Bullet resolveSingleConstraintRowLowerLimit:
1153
+ // deltaImpulse = (rhs - relVel) * jacDiagABInv, accumulated impulse clamped at
1154
+ // the lower limit of 0 (push-only). `rhs` here is restitution + Baumgarte bias.
1155
+ function iterateContactNormal(c, lv, av, invMass) {
1156
+ const jacInvN = c.jacInvN;
1157
+ if (jacInvN <= 0)
1158
+ return;
882
1159
  const imA = invMass[c.bodyA];
883
1160
  const imB = invMass[c.bodyB];
884
1161
  if (imA === 0 && imB === 0)
@@ -886,102 +1163,92 @@ function iterateContactRow(c, lv, av, invMass) {
886
1163
  const ai = c.bodyA * 3, bi = c.bodyB * 3;
887
1164
  const rAx = c.rAx, rAy = c.rAy, rAz = c.rAz;
888
1165
  const rBx = c.rBx, rBy = c.rBy, rBz = c.rBz;
1166
+ const nx = c.nx, ny = c.ny, nz = c.nz;
889
1167
  const vAx = lv[ai + 0] + av[ai + 1] * rAz - av[ai + 2] * rAy;
890
1168
  const vAy = lv[ai + 1] + av[ai + 2] * rAx - av[ai + 0] * rAz;
891
1169
  const vAz = lv[ai + 2] + av[ai + 0] * rAy - av[ai + 1] * rAx;
892
1170
  const vBx = lv[bi + 0] + av[bi + 1] * rBz - av[bi + 2] * rBy;
893
1171
  const vBy = lv[bi + 1] + av[bi + 2] * rBx - av[bi + 0] * rBz;
894
1172
  const vBz = lv[bi + 2] + av[bi + 0] * rBy - av[bi + 1] * rBx;
895
- const dvx = vBx - vAx;
896
- const dvy = vBy - vAy;
897
- const dvz = vBz - vAz;
898
- // Normal row.
899
- const jacInvN = c.jacInvN;
900
- if (jacInvN > 0) {
901
- const nx = c.nx, ny = c.ny, nz = c.nz;
902
- const relVelN = dvx * nx + dvy * ny + dvz * nz;
903
- let dImpN = (c.bounceVel - c.allowedApproachVel - relVelN) * jacInvN * c.sorGain;
904
- const oldN = c.appliedNormalImpulse;
905
- let newN = oldN + dImpN;
906
- if (newN < 0) {
907
- newN = 0;
908
- dImpN = -oldN;
909
- }
910
- c.appliedNormalImpulse = newN;
911
- if (dImpN !== 0) {
912
- const cAxN = c.cAxN, cAyN = c.cAyN, cAzN = c.cAzN;
913
- const cBxN = c.cBxN, cByN = c.cByN, cBzN = c.cBzN;
914
- if (imA > 0) {
915
- lv[ai + 0] -= dImpN * imA * nx;
916
- lv[ai + 1] -= dImpN * imA * ny;
917
- lv[ai + 2] -= dImpN * imA * nz;
918
- av[ai + 0] -= dImpN * cAxN;
919
- av[ai + 1] -= dImpN * cAyN;
920
- av[ai + 2] -= dImpN * cAzN;
921
- }
922
- if (imB > 0) {
923
- lv[bi + 0] += dImpN * imB * nx;
924
- lv[bi + 1] += dImpN * imB * ny;
925
- lv[bi + 2] += dImpN * imB * nz;
926
- av[bi + 0] += dImpN * cBxN;
927
- av[bi + 1] += dImpN * cByN;
928
- av[bi + 2] += dImpN * cBzN;
929
- }
930
- }
1173
+ const relVelN = (vBx - vAx) * nx + (vBy - vAy) * ny + (vBz - vAz) * nz;
1174
+ let dImpN = (c.bounceVel + c.biasVel - relVelN) * jacInvN;
1175
+ const oldN = c.appliedNormalImpulse;
1176
+ let newN = oldN + dImpN;
1177
+ if (newN < 0) {
1178
+ newN = 0;
1179
+ dImpN = -oldN;
1180
+ }
1181
+ c.appliedNormalImpulse = newN;
1182
+ if (dImpN === 0)
1183
+ return;
1184
+ if (imA > 0) {
1185
+ lv[ai + 0] -= dImpN * imA * nx;
1186
+ lv[ai + 1] -= dImpN * imA * ny;
1187
+ lv[ai + 2] -= dImpN * imA * nz;
1188
+ av[ai + 0] -= dImpN * c.cAxN;
1189
+ av[ai + 1] -= dImpN * c.cAyN;
1190
+ av[ai + 2] -= dImpN * c.cAzN;
931
1191
  }
932
- // Friction. Bound = ±μ · current normal impulse.
1192
+ if (imB > 0) {
1193
+ lv[bi + 0] += dImpN * imB * nx;
1194
+ lv[bi + 1] += dImpN * imB * ny;
1195
+ lv[bi + 2] += dImpN * imB * nz;
1196
+ av[bi + 0] += dImpN * c.cBxN;
1197
+ av[bi + 1] += dImpN * c.cByN;
1198
+ av[bi + 2] += dImpN * c.cBzN;
1199
+ }
1200
+ }
1201
+ // ITER, friction pass. One Coulomb row (stock Bullet has
1202
+ // SOLVER_USE_2_FRICTION_DIRECTIONS off), bounded by ±friction · the normal
1203
+ // impulse accumulated so far. Bullet skips the row entirely while that is
1204
+ // still zero.
1205
+ function iterateContactFriction(c, lv, av, invMass) {
933
1206
  const muNormal = c.friction * c.appliedNormalImpulse;
934
1207
  if (muNormal <= 0)
935
1208
  return;
936
- // Re-read dv after the normal impulse possibly changed lv/av.
937
- const vAx2 = lv[ai + 0] + av[ai + 1] * rAz - av[ai + 2] * rAy;
938
- const vAy2 = lv[ai + 1] + av[ai + 2] * rAx - av[ai + 0] * rAz;
939
- const vAz2 = lv[ai + 2] + av[ai + 0] * rAy - av[ai + 1] * rAx;
940
- const vBx2 = lv[bi + 0] + av[bi + 1] * rBz - av[bi + 2] * rBy;
941
- const vBy2 = lv[bi + 1] + av[bi + 2] * rBx - av[bi + 0] * rBz;
942
- const vBz2 = lv[bi + 2] + av[bi + 0] * rBy - av[bi + 1] * rBx;
943
- const dvx2 = vBx2 - vAx2;
944
- const dvy2 = vBy2 - vAy2;
945
- const dvz2 = vBz2 - vAz2;
946
- applyFrictionTangent(c, ai, bi, dvx2, dvy2, dvz2, c.t1x, c.t1y, c.t1z, c.cAxT1, c.cAyT1, c.cAzT1, c.cBxT1, c.cByT1, c.cBzT1, c.jacInvT1, muNormal, imA, imB, lv, av, 1);
947
- applyFrictionTangent(c, ai, bi, dvx2, dvy2, dvz2, c.t2x, c.t2y, c.t2z, c.cAxT2, c.cAyT2, c.cAzT2, c.cBxT2, c.cByT2, c.cBzT2, c.jacInvT2, muNormal, imA, imB, lv, av, 2);
948
- }
949
- function applyFrictionTangent(c, ai, bi, dvx, dvy, dvz, tx, ty, tz, cAx, cAy, cAz, cBx, cBy, cBz, jacInv, muNormal, imA, imB, lv, av, slot) {
950
- if (jacInv <= 0)
1209
+ if (c.jacInvT1 <= 0)
951
1210
  return;
952
- const relVel = dvx * tx + dvy * ty + dvz * tz;
953
- let dImp = -relVel * jacInv * c.sorGain;
954
- const old = slot === 1 ? c.appliedFrictionImpulse1 : c.appliedFrictionImpulse2;
1211
+ const imA = invMass[c.bodyA];
1212
+ const imB = invMass[c.bodyB];
1213
+ if (imA === 0 && imB === 0)
1214
+ return;
1215
+ const ai = c.bodyA * 3, bi = c.bodyB * 3;
1216
+ const rAx = c.rAx, rAy = c.rAy, rAz = c.rAz;
1217
+ const rBx = c.rBx, rBy = c.rBy, rBz = c.rBz;
1218
+ const vAx = lv[ai + 0] + av[ai + 1] * rAz - av[ai + 2] * rAy;
1219
+ const vAy = lv[ai + 1] + av[ai + 2] * rAx - av[ai + 0] * rAz;
1220
+ const vAz = lv[ai + 2] + av[ai + 0] * rAy - av[ai + 1] * rAx;
1221
+ const vBx = lv[bi + 0] + av[bi + 1] * rBz - av[bi + 2] * rBy;
1222
+ const vBy = lv[bi + 1] + av[bi + 2] * rBx - av[bi + 0] * rBz;
1223
+ const vBz = lv[bi + 2] + av[bi + 0] * rBy - av[bi + 1] * rBx;
1224
+ const dvx = vBx - vAx, dvy = vBy - vAy, dvz = vBz - vAz;
1225
+ const relVel = dvx * c.t1x + dvy * c.t1y + dvz * c.t1z;
1226
+ let dImp = -relVel * c.jacInvT1;
1227
+ const old = c.appliedFrictionImpulse1;
955
1228
  let next = old + dImp;
956
- if (next < -muNormal) {
1229
+ if (next < -muNormal)
957
1230
  next = -muNormal;
958
- dImp = next - old;
959
- }
960
- else if (next > muNormal) {
1231
+ else if (next > muNormal)
961
1232
  next = muNormal;
962
- dImp = next - old;
963
- }
964
- if (slot === 1)
965
- c.appliedFrictionImpulse1 = next;
966
- else
967
- c.appliedFrictionImpulse2 = next;
1233
+ dImp = next - old;
1234
+ c.appliedFrictionImpulse1 = next;
968
1235
  if (dImp === 0)
969
1236
  return;
970
1237
  if (imA > 0) {
971
- lv[ai + 0] -= dImp * imA * tx;
972
- lv[ai + 1] -= dImp * imA * ty;
973
- lv[ai + 2] -= dImp * imA * tz;
974
- av[ai + 0] -= dImp * cAx;
975
- av[ai + 1] -= dImp * cAy;
976
- av[ai + 2] -= dImp * cAz;
1238
+ lv[ai + 0] -= dImp * imA * c.t1x;
1239
+ lv[ai + 1] -= dImp * imA * c.t1y;
1240
+ lv[ai + 2] -= dImp * imA * c.t1z;
1241
+ av[ai + 0] -= dImp * c.cAxT1;
1242
+ av[ai + 1] -= dImp * c.cAyT1;
1243
+ av[ai + 2] -= dImp * c.cAzT1;
977
1244
  }
978
1245
  if (imB > 0) {
979
- lv[bi + 0] += dImp * imB * tx;
980
- lv[bi + 1] += dImp * imB * ty;
981
- lv[bi + 2] += dImp * imB * tz;
982
- av[bi + 0] += dImp * cBx;
983
- av[bi + 1] += dImp * cBy;
984
- av[bi + 2] += dImp * cBz;
1246
+ lv[bi + 0] += dImp * imB * c.t1x;
1247
+ lv[bi + 1] += dImp * imB * c.t1y;
1248
+ lv[bi + 2] += dImp * imB * c.t1z;
1249
+ av[bi + 0] += dImp * c.cBxT1;
1250
+ av[bi + 1] += dImp * c.cByT1;
1251
+ av[bi + 2] += dImp * c.cBzT1;
985
1252
  }
986
1253
  }
987
1254
  function buildBodyMat(store, i, out) {
@@ -999,6 +1266,19 @@ function eulerXYZQuatInto(x, y, z, out) {
999
1266
  out[3] = cx * cy * cz - sx * sy * sz;
1000
1267
  }
1001
1268
  // Euler XYZ from a 3×3 rotation matrix (row-major elements).
1269
+ //
1270
+ // ⚠ TRANSPOSED RELATIVE TO BULLET. Bullet's matrixToEulerXYZ (btGeneric6DofConstraint.cpp)
1271
+ // reads asin(r02) and atan2(-r12, r22); this reads asin(r20) and atan2(-r21, r22).
1272
+ // Every angular DOF here is therefore the NEGATION of Bullet's, and the code
1273
+ // compensates by driving angular rows with +k where Bullet uses −k, and by
1274
+ // treating a low violation as Bullet's high one. The two flips cancel, so
1275
+ // nothing is wrong — but they must be flipped TOGETHER.
1276
+ //
1277
+ // Porting Bullet's sign without also swapping the violation enum makes every
1278
+ // limit drive its joint further out of range: measured mean velocity-reversal
1279
+ // 0.59 with jerk 1912 (an explosion), improving to 0.40 and then 0.137 as each
1280
+ // half was corrected. If you ever port more Bullet joint code, either flip both
1281
+ // or change this function to Bullet's convention and flip every consumer.
1002
1282
  function matrixToEulerXYZ(r00, r01, r10, r11, r20, r21, r22, out) {
1003
1283
  if (r20 < 1) {
1004
1284
  if (r20 > -1) {
@@ -1018,3 +1298,77 @@ function matrixToEulerXYZ(r00, r01, r10, r11, r20, r21, r22, out) {
1018
1298
  out[2] = 0;
1019
1299
  }
1020
1300
  }
1301
+ // Seed a row from the persistent manifold and apply that impulse up front —
1302
+ // Bullet's setup does this inline (m_appliedImpulse = cp.m_appliedImpulse *
1303
+ // m_warmstartingFactor, then applyImpulse). Contact points are matched in body
1304
+ // A's local frame so the identity survives the bodies moving.
1305
+ function warmStartContact(c, store, manifolds, lv, av, invMass) {
1306
+ const imA = invMass[c.bodyA];
1307
+ const imB = invMass[c.bodyB];
1308
+ if (imA === 0 && imB === 0)
1309
+ return;
1310
+ worldToLocal(store, c.bodyA, c.rAx, c.rAy, c.rAz, _mfA);
1311
+ const prev = manifolds.find(c.bodyA, c.bodyB, _mfA[0], _mfA[1], _mfA[2]);
1312
+ if (prev === null) {
1313
+ c.age = 0;
1314
+ return;
1315
+ }
1316
+ c.age = prev.age + 1;
1317
+ const n = prev.normalImpulse * WARMSTARTING_FACTOR;
1318
+ const f1 = prev.frictionImpulse1 * WARMSTARTING_FACTOR;
1319
+ const f2 = prev.frictionImpulse2 * WARMSTARTING_FACTOR;
1320
+ c.appliedNormalImpulse = n;
1321
+ c.appliedFrictionImpulse1 = f1;
1322
+ c.appliedFrictionImpulse2 = f2;
1323
+ applyContactImpulse(c, lv, av, imA, imB, c.nx, c.ny, c.nz, c.cAxN, c.cAyN, c.cAzN, c.cBxN, c.cByN, c.cBzN, n);
1324
+ if (c.jacInvT1 > 0)
1325
+ applyContactImpulse(c, lv, av, imA, imB, c.t1x, c.t1y, c.t1z, c.cAxT1, c.cAyT1, c.cAzT1, c.cBxT1, c.cByT1, c.cBzT1, f1);
1326
+ if (c.jacInvT2 > 0)
1327
+ applyContactImpulse(c, lv, av, imA, imB, c.t2x, c.t2y, c.t2z, c.cAxT2, c.cAyT2, c.cAzT2, c.cBxT2, c.cByT2, c.cBzT2, f2);
1328
+ }
1329
+ function applyContactImpulse(c, lv, av, imA, imB, dx, dy, dz, cAx, cAy, cAz, cBx, cBy, cBz, j) {
1330
+ if (j === 0)
1331
+ return;
1332
+ const ai = c.bodyA * 3, bi = c.bodyB * 3;
1333
+ if (imA > 0) {
1334
+ lv[ai + 0] -= j * imA * dx;
1335
+ lv[ai + 1] -= j * imA * dy;
1336
+ lv[ai + 2] -= j * imA * dz;
1337
+ av[ai + 0] -= j * cAx;
1338
+ av[ai + 1] -= j * cAy;
1339
+ av[ai + 2] -= j * cAz;
1340
+ }
1341
+ if (imB > 0) {
1342
+ lv[bi + 0] += j * imB * dx;
1343
+ lv[bi + 1] += j * imB * dy;
1344
+ lv[bi + 2] += j * imB * dz;
1345
+ av[bi + 0] += j * cBx;
1346
+ av[bi + 1] += j * cBy;
1347
+ av[bi + 2] += j * cBz;
1348
+ }
1349
+ }
1350
+ /** Store every solved row back into the manifold for the next substep. */
1351
+ export function saveContactImpulses(store, contacts, manifolds) {
1352
+ for (let ci = 0; ci < contacts.count; ci++) {
1353
+ const c = contacts.get(ci);
1354
+ worldToLocal(store, c.bodyA, c.rAx, c.rAy, c.rAz, _mfA);
1355
+ worldToLocal(store, c.bodyB, c.rBx, c.rBy, c.rBz, _mfB);
1356
+ manifolds.store(c.bodyA, c.bodyB, _mfA[0], _mfA[1], _mfA[2], _mfB[0], _mfB[1], _mfB[2], c.appliedNormalImpulse, c.appliedFrictionImpulse1, c.appliedFrictionImpulse2, c.age);
1357
+ }
1358
+ manifolds.endStep();
1359
+ }
1360
+ const _mfA = new Float32Array(3);
1361
+ const _mfB = new Float32Array(3);
1362
+ // Rotate a world-space lever arm into the body's local frame (R^T · v).
1363
+ function worldToLocal(store, i, x, y, z, out) {
1364
+ const i4 = i * 4;
1365
+ const qx = store.orientations[i4 + 0], qy = store.orientations[i4 + 1];
1366
+ const qz = store.orientations[i4 + 2], qw = store.orientations[i4 + 3];
1367
+ // v' = conj(q) * v * q
1368
+ const tx = 2 * (qy * z - qz * y);
1369
+ const ty = 2 * (qz * x - qx * z);
1370
+ const tz = 2 * (qx * y - qy * x);
1371
+ out[0] = x - qw * tx + (qy * tz - qz * ty);
1372
+ out[1] = y - qw * ty + (qz * tx - qx * tz);
1373
+ out[2] = z - qw * tz + (qx * ty - qy * tx);
1374
+ }