reze-engine 0.15.0 → 0.15.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.
@@ -2,6 +2,14 @@
2
2
  // Gauss-Seidel: per axis, target a relative velocity (limit correction +
3
3
  // spring), apply the impulse needed to reach it. Friction is two Coulomb
4
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.
5
13
  import { Mat4 } from "../math";
6
14
  import { STOP_ERP } from "./constraint";
7
15
  const BOUNCE_THRESHOLD = 2.0;
@@ -10,12 +18,7 @@ const _TA = new Float32Array(16);
10
18
  const _TB = new Float32Array(16);
11
19
  const _bodyMatA = new Float32Array(16);
12
20
  const _bodyMatB = new Float32Array(16);
13
- const _linAxes = new Float32Array(9); // 3 linear axes × xyz
14
- const _angAxes = new Float32Array(9); // 3 angular axes × xyz
15
- const _linDiff = new Float32Array(3);
16
- const _angDiff = new Float32Array(3);
17
- const _rA = new Float32Array(3);
18
- const _rB = new Float32Array(3);
21
+ const _angDiffScratch = new Float32Array(3);
19
22
  export function solveConstraints(store, constraints, contacts, dt, iterations) {
20
23
  if (dt <= 0)
21
24
  return;
@@ -24,250 +27,307 @@ export function solveConstraints(store, constraints, contacts, dt, iterations) {
24
27
  const invDt = 1 / dt;
25
28
  const lv = store.linearVelocities;
26
29
  const av = store.angularVelocities;
27
- const pos = store.positions;
28
30
  const invMass = store.invMass;
29
31
  const invInertia = store.invInertia;
32
+ for (let c = 0; c < constraints.length; c++) {
33
+ setupConstraint(constraints[c], store, dt, invDt);
34
+ }
35
+ for (let ci = 0; ci < contacts.count; ci++) {
36
+ setupContactRow(contacts.get(ci), lv, av, invMass, invInertia);
37
+ }
30
38
  for (let iter = 0; iter < iterations; iter++) {
31
39
  for (let c = 0; c < constraints.length; c++) {
32
- const con = constraints[c];
33
- const a = con.bodyA;
34
- const b = con.bodyB;
35
- const imA = invMass[a];
36
- const imB = invMass[b];
37
- const iiA = invInertia[a];
38
- const iiB = invInertia[b];
39
- if (imA === 0 && imB === 0)
40
- continue;
41
- buildBodyMat(store, a, _bodyMatA);
42
- buildBodyMat(store, b, _bodyMatB);
43
- Mat4.multiplyArrays(_bodyMatA, 0, con.frameA, 0, _TA, 0);
44
- Mat4.multiplyArrays(_bodyMatB, 0, con.frameB, 0, _TB, 0);
45
- // Mass-weighted shared anchor: kinematic partner gets weight 1 so
46
- // the anchor sits exactly on it; two dynamic bodies blend by inverse
47
- // mass. Lever arms are measured from each CG to that shared anchor.
48
- const ai = a * 3;
49
- const bi = b * 3;
50
- const weightA = imB === 0 ? 1 : imA / (imA + imB);
51
- const weightB = 1 - weightA;
52
- const anchorX = _TA[12] * weightA + _TB[12] * weightB;
53
- const anchorY = _TA[13] * weightA + _TB[13] * weightB;
54
- const anchorZ = _TA[14] * weightA + _TB[14] * weightB;
55
- _rA[0] = anchorX - pos[ai + 0];
56
- _rA[1] = anchorY - pos[ai + 1];
57
- _rA[2] = anchorZ - pos[ai + 2];
58
- _rB[0] = anchorX - pos[bi + 0];
59
- _rB[1] = anchorY - pos[bi + 1];
60
- _rB[2] = anchorZ - pos[bi + 2];
61
- // Linear part: linearDiff = TA.basis^T · (TB.origin TA.origin),
62
- // axes = TA's columns 0/1/2 in world space.
63
- const dxw = _TB[12] - _TA[12];
64
- const dyw = _TB[13] - _TA[13];
65
- const dzw = _TB[14] - _TA[14];
66
- _linDiff[0] = _TA[0] * dxw + _TA[1] * dyw + _TA[2] * dzw;
67
- _linDiff[1] = _TA[4] * dxw + _TA[5] * dyw + _TA[6] * dzw;
68
- _linDiff[2] = _TA[8] * dxw + _TA[9] * dyw + _TA[10] * dzw;
69
- _linAxes[0] = _TA[0];
70
- _linAxes[1] = _TA[1];
71
- _linAxes[2] = _TA[2];
72
- _linAxes[3] = _TA[4];
73
- _linAxes[4] = _TA[5];
74
- _linAxes[5] = _TA[6];
75
- _linAxes[6] = _TA[8];
76
- _linAxes[7] = _TA[9];
77
- _linAxes[8] = _TA[10];
78
- for (let i = 0; i < 3; i++) {
79
- const lo = con.linearMin[i];
80
- const hi = con.linearMax[i];
81
- const curr = _linDiff[i];
82
- const off = i * 3;
83
- const axx = _linAxes[off + 0];
84
- const axy = _linAxes[off + 1];
85
- const axz = _linAxes[off + 2];
86
- // (rA × axis), (rB × axis): angular components of the linear Jacobian.
87
- const cAx = _rA[1] * axz - _rA[2] * axy;
88
- const cAy = _rA[2] * axx - _rA[0] * axz;
89
- const cAz = _rA[0] * axy - _rA[1] * axx;
90
- const cBx = _rB[1] * axz - _rB[2] * axy;
91
- const cBy = _rB[2] * axx - _rB[0] * axz;
92
- const cBz = _rB[0] * axy - _rB[1] * axx;
93
- const cA2 = cAx * cAx + cAy * cAy + cAz * cAz;
94
- const cB2 = cBx * cBx + cBy * cBy + cBz * cBz;
95
- const denom = imA + imB + cA2 * iiA + cB2 * iiB;
96
- if (denom <= 0)
97
- continue;
98
- const jacInv = 1 / denom;
99
- // v_pivot = v_CG + ω × r.
100
- const vAx = lv[ai + 0] + av[ai + 1] * _rA[2] - av[ai + 2] * _rA[1];
101
- const vAy = lv[ai + 1] + av[ai + 2] * _rA[0] - av[ai + 0] * _rA[2];
102
- const vAz = lv[ai + 2] + av[ai + 0] * _rA[1] - av[ai + 1] * _rA[0];
103
- const vBx = lv[bi + 0] + av[bi + 1] * _rB[2] - av[bi + 2] * _rB[1];
104
- const vBy = lv[bi + 1] + av[bi + 2] * _rB[0] - av[bi + 0] * _rB[2];
105
- const vBz = lv[bi + 2] + av[bi + 0] * _rB[1] - av[bi + 1] * _rB[0];
106
- const relVel = (vBx - vAx) * axx + (vBy - vAy) * axy + (vBz - vAz) * axz;
107
- let targetVel = 0;
108
- let active = false;
109
- if (lo <= hi) {
110
- let err = 0;
111
- if (curr < lo)
112
- err = curr - lo;
113
- else if (curr > hi)
114
- err = curr - hi;
115
- if (err !== 0) {
116
- targetVel = -err * STOP_ERP * invDt;
117
- active = true;
118
- }
119
- }
120
- if (con.springEnabled[i]) {
121
- targetVel += -con.springStiffness[i] * (curr - con.equilibriumPoint[i]) * dt;
122
- active = true;
123
- }
124
- if (active) {
125
- const j = (targetVel - relVel) * jacInv;
126
- if (imA > 0) {
127
- lv[ai + 0] -= j * imA * axx;
128
- lv[ai + 1] -= j * imA * axy;
129
- lv[ai + 2] -= j * imA * axz;
130
- av[ai + 0] -= j * iiA * cAx;
131
- av[ai + 1] -= j * iiA * cAy;
132
- av[ai + 2] -= j * iiA * cAz;
133
- }
134
- if (imB > 0) {
135
- lv[bi + 0] += j * imB * axx;
136
- lv[bi + 1] += j * imB * axy;
137
- lv[bi + 2] += j * imB * axz;
138
- av[bi + 0] += j * iiB * cBx;
139
- av[bi + 1] += j * iiB * cBy;
140
- av[bi + 2] += j * iiB * cBz;
141
- }
142
- }
143
- }
144
- // Angular part: relative rotation TA^T·TB → Euler XYZ; axes from
145
- // TA.col2 × TB.col0 (as Bullet's calculatedAxis derivation).
146
- const r00 = _TA[0] * _TB[0] + _TA[1] * _TB[1] + _TA[2] * _TB[2];
147
- const r01 = _TA[0] * _TB[4] + _TA[1] * _TB[5] + _TA[2] * _TB[6];
148
- const r10 = _TA[4] * _TB[0] + _TA[5] * _TB[1] + _TA[6] * _TB[2];
149
- const r11 = _TA[4] * _TB[4] + _TA[5] * _TB[5] + _TA[6] * _TB[6];
150
- const r20 = _TA[8] * _TB[0] + _TA[9] * _TB[1] + _TA[10] * _TB[2];
151
- const r21 = _TA[8] * _TB[4] + _TA[9] * _TB[5] + _TA[10] * _TB[6];
152
- const r22 = _TA[8] * _TB[8] + _TA[9] * _TB[9] + _TA[10] * _TB[10];
153
- matrixToEulerXYZ(r00, r01, r10, r11, r20, r21, r22, _angDiff);
154
- const a2x = _TA[8], a2y = _TA[9], a2z = _TA[10];
155
- const b0x = _TB[0], b0y = _TB[1], b0z = _TB[2];
156
- // ax[1] = a2 × b0; ax[0] = ax[1] × a2; ax[2] = b0 × ax[1].
157
- let yx = a2y * b0z - a2z * b0y;
158
- let yy = a2z * b0x - a2x * b0z;
159
- let yz = a2x * b0y - a2y * b0x;
160
- let l = Math.hypot(yx, yy, yz);
161
- if (l > 1e-8) {
162
- const inv = 1 / l;
163
- yx *= inv;
164
- yy *= inv;
165
- yz *= inv;
166
- }
167
- _angAxes[3] = yx;
168
- _angAxes[4] = yy;
169
- _angAxes[5] = yz;
170
- let xx = yy * a2z - yz * a2y;
171
- let xy = yz * a2x - yx * a2z;
172
- let xz = yx * a2y - yy * a2x;
173
- l = Math.hypot(xx, xy, xz);
174
- if (l > 1e-8) {
175
- const inv = 1 / l;
176
- xx *= inv;
177
- xy *= inv;
178
- xz *= inv;
179
- }
180
- _angAxes[0] = xx;
181
- _angAxes[1] = xy;
182
- _angAxes[2] = xz;
183
- let zx = b0y * yz - b0z * yy;
184
- let zy = b0z * yx - b0x * yz;
185
- let zz = b0x * yy - b0y * yx;
186
- l = Math.hypot(zx, zy, zz);
187
- if (l > 1e-8) {
188
- const inv = 1 / l;
189
- zx *= inv;
190
- zy *= inv;
191
- zz *= inv;
40
+ iterateConstraint(constraints[c], lv, av, invMass, invInertia);
41
+ }
42
+ for (let ci = 0; ci < contacts.count; ci++) {
43
+ iterateContactRow(contacts.get(ci), lv, av, invMass, invInertia);
44
+ }
45
+ }
46
+ }
47
+ // SETUP: compute everything that doesn't depend on velocities. Caller
48
+ // guarantees pos/ori don't change between this and the iter loop.
49
+ function setupConstraint(con, store, dt, invDt) {
50
+ const a = con.bodyA;
51
+ const b = con.bodyB;
52
+ const imA = store.invMass[a];
53
+ const imB = store.invMass[b];
54
+ const iiA = store.invInertia[a];
55
+ const iiB = store.invInertia[b];
56
+ con.cacheSkip = imA === 0 && imB === 0;
57
+ if (con.cacheSkip)
58
+ return;
59
+ buildBodyMat(store, a, _bodyMatA);
60
+ buildBodyMat(store, b, _bodyMatB);
61
+ Mat4.multiplyArrays(_bodyMatA, 0, con.frameA, 0, _TA, 0);
62
+ Mat4.multiplyArrays(_bodyMatB, 0, con.frameB, 0, _TB, 0);
63
+ // Mass-weighted shared anchor (Bullet 2.75 m_AnchorPos).
64
+ const pos = store.positions;
65
+ const ai = a * 3;
66
+ const bi = b * 3;
67
+ const weightA = imB === 0 ? 1 : imA / (imA + imB);
68
+ const weightB = 1 - weightA;
69
+ const anchorX = _TA[12] * weightA + _TB[12] * weightB;
70
+ const anchorY = _TA[13] * weightA + _TB[13] * weightB;
71
+ const anchorZ = _TA[14] * weightA + _TB[14] * weightB;
72
+ const rAx = anchorX - pos[ai + 0];
73
+ const rAy = anchorY - pos[ai + 1];
74
+ const rAz = anchorZ - pos[ai + 2];
75
+ const rBx = anchorX - pos[bi + 0];
76
+ const rBy = anchorY - pos[bi + 1];
77
+ const rBz = anchorZ - pos[bi + 2];
78
+ const lA = con.cacheLeverA;
79
+ const lB = con.cacheLeverB;
80
+ lA[0] = rAx;
81
+ lA[1] = rAy;
82
+ lA[2] = rAz;
83
+ lB[0] = rBx;
84
+ lB[1] = rBy;
85
+ lB[2] = rBz;
86
+ // linearDiff = TA.basis^T · (TB.origin TA.origin); axes = TA columns 0/1/2.
87
+ const dxw = _TB[12] - _TA[12];
88
+ const dyw = _TB[13] - _TA[13];
89
+ const dzw = _TB[14] - _TA[14];
90
+ const linDiff0 = _TA[0] * dxw + _TA[1] * dyw + _TA[2] * dzw;
91
+ const linDiff1 = _TA[4] * dxw + _TA[5] * dyw + _TA[6] * dzw;
92
+ const linDiff2 = _TA[8] * dxw + _TA[9] * dyw + _TA[10] * dzw;
93
+ const axes = con.cacheLinAxes;
94
+ const cA = con.cacheLinCrossA;
95
+ const cB = con.cacheLinCrossB;
96
+ const jac = con.cacheLinJacInv;
97
+ const tgt = con.cacheLinTargetVel;
98
+ const act = con.cacheLinActive;
99
+ for (let i = 0; i < 3; i++) {
100
+ const o = i * 3;
101
+ const axx = i === 0 ? _TA[0] : i === 1 ? _TA[4] : _TA[8];
102
+ const axy = i === 0 ? _TA[1] : i === 1 ? _TA[5] : _TA[9];
103
+ const axz = i === 0 ? _TA[2] : i === 1 ? _TA[6] : _TA[10];
104
+ axes[o + 0] = axx;
105
+ axes[o + 1] = axy;
106
+ axes[o + 2] = axz;
107
+ const cAx = rAy * axz - rAz * axy;
108
+ const cAy = rAz * axx - rAx * axz;
109
+ const cAz = rAx * axy - rAy * axx;
110
+ const cBx = rBy * axz - rBz * axy;
111
+ const cBy = rBz * axx - rBx * axz;
112
+ const cBz = rBx * axy - rBy * axx;
113
+ cA[o + 0] = cAx;
114
+ cA[o + 1] = cAy;
115
+ cA[o + 2] = cAz;
116
+ cB[o + 0] = cBx;
117
+ cB[o + 1] = cBy;
118
+ cB[o + 2] = cBz;
119
+ const denom = imA + imB +
120
+ (cAx * cAx + cAy * cAy + cAz * cAz) * iiA +
121
+ (cBx * cBx + cBy * cBy + cBz * cBz) * iiB;
122
+ jac[i] = denom > 0 ? 1 / denom : 0;
123
+ const lo = con.linearMin[i];
124
+ const hi = con.linearMax[i];
125
+ const curr = i === 0 ? linDiff0 : i === 1 ? linDiff1 : linDiff2;
126
+ let target = 0;
127
+ let active = 0;
128
+ if (lo <= hi) {
129
+ let err = 0;
130
+ if (curr < lo)
131
+ err = curr - lo;
132
+ else if (curr > hi)
133
+ err = curr - hi;
134
+ if (err !== 0) {
135
+ target = -err * STOP_ERP * invDt;
136
+ active = 1;
192
137
  }
193
- _angAxes[6] = zx;
194
- _angAxes[7] = zy;
195
- _angAxes[8] = zz;
196
- const angDenom = iiA + iiB;
197
- if (angDenom > 0) {
198
- const angJacInv = 1 / angDenom;
199
- for (let i = 0; i < 3; i++) {
200
- const idx = i + 3;
201
- const lo = con.angularMin[i];
202
- const hi = con.angularMax[i];
203
- const curr = _angDiff[i];
204
- const off = i * 3;
205
- const axx = _angAxes[off + 0];
206
- const axy = _angAxes[off + 1];
207
- const axz = _angAxes[off + 2];
208
- const relAv = (av[bi + 0] - av[ai + 0]) * axx +
209
- (av[bi + 1] - av[ai + 1]) * axy +
210
- (av[bi + 2] - av[ai + 2]) * axz;
211
- // Sign flip vs linear: d(angDiff)/dt = −(ω_B ω_A)·ax.
212
- let targetVel = 0;
213
- let active = false;
214
- if (lo <= hi) {
215
- let err = 0;
216
- if (curr < lo)
217
- err = curr - lo;
218
- else if (curr > hi)
219
- err = curr - hi;
220
- if (err !== 0) {
221
- targetVel = err * STOP_ERP * invDt;
222
- active = true;
223
- }
224
- }
225
- if (con.springEnabled[idx]) {
226
- targetVel += con.springStiffness[idx] * (curr - con.equilibriumPoint[idx]) * dt;
227
- active = true;
228
- }
229
- if (active) {
230
- const j = (targetVel - relAv) * angJacInv;
231
- if (iiA > 0) {
232
- av[ai + 0] -= j * iiA * axx;
233
- av[ai + 1] -= j * iiA * axy;
234
- av[ai + 2] -= j * iiA * axz;
235
- }
236
- if (iiB > 0) {
237
- av[bi + 0] += j * iiB * axx;
238
- av[bi + 1] += j * iiB * axy;
239
- av[bi + 2] += j * iiB * axz;
240
- }
241
- }
242
- }
138
+ }
139
+ if (con.springEnabled[i]) {
140
+ target += -con.springStiffness[i] * (curr - con.equilibriumPoint[i]) * dt;
141
+ active = 1;
142
+ }
143
+ tgt[i] = target;
144
+ act[i] = denom > 0 ? active : 0;
145
+ }
146
+ // Angular: TA^T · TB → Euler XYZ; axes from TA.col2 × TB.col0.
147
+ const r00 = _TA[0] * _TB[0] + _TA[1] * _TB[1] + _TA[2] * _TB[2];
148
+ const r01 = _TA[0] * _TB[4] + _TA[1] * _TB[5] + _TA[2] * _TB[6];
149
+ const r10 = _TA[4] * _TB[0] + _TA[5] * _TB[1] + _TA[6] * _TB[2];
150
+ const r11 = _TA[4] * _TB[4] + _TA[5] * _TB[5] + _TA[6] * _TB[6];
151
+ const r20 = _TA[8] * _TB[0] + _TA[9] * _TB[1] + _TA[10] * _TB[2];
152
+ const r21 = _TA[8] * _TB[4] + _TA[9] * _TB[5] + _TA[10] * _TB[6];
153
+ const r22 = _TA[8] * _TB[8] + _TA[9] * _TB[9] + _TA[10] * _TB[10];
154
+ matrixToEulerXYZ(r00, r01, r10, r11, r20, r21, r22, _angDiffScratch);
155
+ const a2x = _TA[8], a2y = _TA[9], a2z = _TA[10];
156
+ const b0x = _TB[0], b0y = _TB[1], b0z = _TB[2];
157
+ let yx = a2y * b0z - a2z * b0y;
158
+ let yy = a2z * b0x - a2x * b0z;
159
+ let yz = a2x * b0y - a2y * b0x;
160
+ let l = Math.hypot(yx, yy, yz);
161
+ if (l > 1e-8) {
162
+ const inv = 1 / l;
163
+ yx *= inv;
164
+ yy *= inv;
165
+ yz *= inv;
166
+ }
167
+ let xx = yy * a2z - yz * a2y;
168
+ let xy = yz * a2x - yx * a2z;
169
+ let xz = yx * a2y - yy * a2x;
170
+ l = Math.hypot(xx, xy, xz);
171
+ if (l > 1e-8) {
172
+ const inv = 1 / l;
173
+ xx *= inv;
174
+ xy *= inv;
175
+ xz *= inv;
176
+ }
177
+ let zx = b0y * yz - b0z * yy;
178
+ let zy = b0z * yx - b0x * yz;
179
+ let zz = b0x * yy - b0y * yx;
180
+ l = Math.hypot(zx, zy, zz);
181
+ if (l > 1e-8) {
182
+ const inv = 1 / l;
183
+ zx *= inv;
184
+ zy *= inv;
185
+ zz *= inv;
186
+ }
187
+ const angAxes = con.cacheAngAxes;
188
+ angAxes[0] = xx;
189
+ angAxes[1] = xy;
190
+ angAxes[2] = xz;
191
+ angAxes[3] = yx;
192
+ angAxes[4] = yy;
193
+ angAxes[5] = yz;
194
+ angAxes[6] = zx;
195
+ angAxes[7] = zy;
196
+ angAxes[8] = zz;
197
+ const angDenom = iiA + iiB;
198
+ con.cacheAngJacInv = angDenom > 0 ? 1 / angDenom : 0;
199
+ const angTgt = con.cacheAngTargetVel;
200
+ const angAct = con.cacheAngActive;
201
+ for (let i = 0; i < 3; i++) {
202
+ const idx = i + 3;
203
+ const lo = con.angularMin[i];
204
+ const hi = con.angularMax[i];
205
+ const curr = _angDiffScratch[i];
206
+ let target = 0;
207
+ let active = 0;
208
+ if (lo <= hi) {
209
+ let err = 0;
210
+ if (curr < lo)
211
+ err = curr - lo;
212
+ else if (curr > hi)
213
+ err = curr - hi;
214
+ // Sign flip vs linear: d(angDiff)/dt = −(ω_B − ω_A)·ax.
215
+ if (err !== 0) {
216
+ target = err * STOP_ERP * invDt;
217
+ active = 1;
243
218
  }
244
219
  }
245
- for (let ci = 0; ci < contacts.count; ci++) {
246
- solveContactRow(contacts.get(ci), lv, av, invMass, invInertia);
220
+ if (con.springEnabled[idx]) {
221
+ target += con.springStiffness[idx] * (curr - con.equilibriumPoint[idx]) * dt;
222
+ active = 1;
247
223
  }
224
+ angTgt[i] = target;
225
+ angAct[i] = angDenom > 0 ? active : 0;
248
226
  }
249
227
  }
250
- // Per-contact: one push-only normal row + two Coulomb friction rows
251
- // (impulse bound = ±μ·appliedNormalImpulse).
252
- function solveContactRow(c, lv, av, invMass, invInertia) {
253
- const ai = c.bodyA * 3, bi = c.bodyB * 3;
254
- const imA = invMass[c.bodyA], imB = invMass[c.bodyB];
255
- const iiA = invInertia[c.bodyA], iiB = invInertia[c.bodyB];
256
- if (imA === 0 && imB === 0)
228
+ // ITER: read cache, compute relVel from current lv/av, apply impulse.
229
+ function iterateConstraint(con, lv, av, invMass, invInertia) {
230
+ if (con.cacheSkip)
257
231
  return;
258
- const rAx = c.rAx, rAy = c.rAy, rAz = c.rAz;
259
- const rBx = c.rBx, rBy = c.rBy, rBz = c.rBz;
260
- const nx = c.nx, ny = c.ny, nz = c.nz;
232
+ const a = con.bodyA;
233
+ const b = con.bodyB;
234
+ const ai = a * 3;
235
+ const bi = b * 3;
236
+ const imA = invMass[a];
237
+ const imB = invMass[b];
238
+ const iiA = invInertia[a];
239
+ const iiB = invInertia[b];
240
+ // Linear axes — relVel at the offset point: v_pivot = v_CG + ω × r.
241
+ const lA = con.cacheLeverA;
242
+ const lB = con.cacheLeverB;
243
+ const rAx = lA[0], rAy = lA[1], rAz = lA[2];
244
+ const rBx = lB[0], rBy = lB[1], rBz = lB[2];
245
+ const axes = con.cacheLinAxes;
246
+ const cA = con.cacheLinCrossA;
247
+ const cB = con.cacheLinCrossB;
248
+ const jac = con.cacheLinJacInv;
249
+ const tgt = con.cacheLinTargetVel;
250
+ const act = con.cacheLinActive;
261
251
  const vAx = lv[ai + 0] + av[ai + 1] * rAz - av[ai + 2] * rAy;
262
252
  const vAy = lv[ai + 1] + av[ai + 2] * rAx - av[ai + 0] * rAz;
263
253
  const vAz = lv[ai + 2] + av[ai + 0] * rAy - av[ai + 1] * rAx;
264
254
  const vBx = lv[bi + 0] + av[bi + 1] * rBz - av[bi + 2] * rBy;
265
255
  const vBy = lv[bi + 1] + av[bi + 2] * rBx - av[bi + 0] * rBz;
266
256
  const vBz = lv[bi + 2] + av[bi + 0] * rBy - av[bi + 1] * rBx;
267
- const dvX = vBx - vAx;
268
- const dvY = vBy - vAy;
269
- const dvZ = vBz - vAz;
270
- // Normal row.
257
+ const dvx = vBx - vAx;
258
+ const dvy = vBy - vAy;
259
+ const dvz = vBz - vAz;
260
+ for (let i = 0; i < 3; i++) {
261
+ if (!act[i])
262
+ continue;
263
+ const o = i * 3;
264
+ const axx = axes[o + 0], axy = axes[o + 1], axz = axes[o + 2];
265
+ const relVel = dvx * axx + dvy * axy + dvz * axz;
266
+ const j = (tgt[i] - relVel) * jac[i];
267
+ if (j === 0)
268
+ continue;
269
+ if (imA > 0) {
270
+ lv[ai + 0] -= j * imA * axx;
271
+ lv[ai + 1] -= j * imA * axy;
272
+ lv[ai + 2] -= j * imA * axz;
273
+ av[ai + 0] -= j * iiA * cA[o + 0];
274
+ av[ai + 1] -= j * iiA * cA[o + 1];
275
+ av[ai + 2] -= j * iiA * cA[o + 2];
276
+ }
277
+ if (imB > 0) {
278
+ lv[bi + 0] += j * imB * axx;
279
+ lv[bi + 1] += j * imB * axy;
280
+ lv[bi + 2] += j * imB * axz;
281
+ av[bi + 0] += j * iiB * cB[o + 0];
282
+ av[bi + 1] += j * iiB * cB[o + 1];
283
+ av[bi + 2] += j * iiB * cB[o + 2];
284
+ }
285
+ }
286
+ // Angular axes — relAv = ω_B − ω_A.
287
+ const angJacInv = con.cacheAngJacInv;
288
+ if (angJacInv === 0)
289
+ return;
290
+ const angAxes = con.cacheAngAxes;
291
+ const angTgt = con.cacheAngTargetVel;
292
+ const angAct = con.cacheAngActive;
293
+ const dax = av[bi + 0] - av[ai + 0];
294
+ const day = av[bi + 1] - av[ai + 1];
295
+ const daz = av[bi + 2] - av[ai + 2];
296
+ for (let i = 0; i < 3; i++) {
297
+ if (!angAct[i])
298
+ continue;
299
+ const o = i * 3;
300
+ const axx = angAxes[o + 0], axy = angAxes[o + 1], axz = angAxes[o + 2];
301
+ const relAv = dax * axx + day * axy + daz * axz;
302
+ const j = (angTgt[i] - relAv) * angJacInv;
303
+ if (j === 0)
304
+ continue;
305
+ if (iiA > 0) {
306
+ av[ai + 0] -= j * iiA * axx;
307
+ av[ai + 1] -= j * iiA * axy;
308
+ av[ai + 2] -= j * iiA * axz;
309
+ }
310
+ if (iiB > 0) {
311
+ av[bi + 0] += j * iiB * axx;
312
+ av[bi + 1] += j * iiB * axy;
313
+ av[bi + 2] += j * iiB * axz;
314
+ }
315
+ }
316
+ }
317
+ // SETUP: pre-compute Jacobians, friction basis, and the bounce reference
318
+ // from the *initial* closing velocity (Bullet's pattern — captures restitution
319
+ // before iter 1 zeroes out the approach).
320
+ function setupContactRow(c, lv, av, invMass, invInertia) {
321
+ const ai = c.bodyA * 3;
322
+ const bi = c.bodyB * 3;
323
+ const imA = invMass[c.bodyA];
324
+ const imB = invMass[c.bodyB];
325
+ const iiA = invInertia[c.bodyA];
326
+ const iiB = invInertia[c.bodyB];
327
+ const rAx = c.rAx, rAy = c.rAy, rAz = c.rAz;
328
+ const rBx = c.rBx, rBy = c.rBy, rBz = c.rBz;
329
+ const nx = c.nx, ny = c.ny, nz = c.nz;
330
+ // Normal Jacobian.
271
331
  const cAxN = rAy * nz - rAz * ny;
272
332
  const cAyN = rAz * nx - rAx * nz;
273
333
  const cAzN = rAx * ny - rAy * nx;
@@ -277,44 +337,25 @@ function solveContactRow(c, lv, av, invMass, invInertia) {
277
337
  const denomN = imA + imB +
278
338
  (cAxN * cAxN + cAyN * cAyN + cAzN * cAzN) * iiA +
279
339
  (cBxN * cBxN + cByN * cByN + cBzN * cBzN) * iiB;
280
- if (denomN <= 0)
281
- return;
282
- const jacInvN = 1 / denomN;
283
- const relVelN = dvX * nx + dvY * ny + dvZ * nz;
284
- // Position correction is handled directly in world.ts (split impulse).
285
- // Velocity row only removes approach + applies restitution above bounce.
286
- let bounce = 0;
287
- if (c.restitution > 0 && relVelN < -BOUNCE_THRESHOLD) {
288
- bounce = -c.restitution * relVelN;
289
- }
290
- let dImpN = (bounce - relVelN) * jacInvN;
291
- const oldN = c.appliedNormalImpulse;
292
- let newN = oldN + dImpN;
293
- if (newN < 0) {
294
- newN = 0;
295
- dImpN = -oldN;
296
- }
297
- c.appliedNormalImpulse = newN;
298
- if (dImpN !== 0) {
299
- if (imA > 0) {
300
- lv[ai + 0] -= dImpN * imA * nx;
301
- lv[ai + 1] -= dImpN * imA * ny;
302
- lv[ai + 2] -= dImpN * imA * nz;
303
- av[ai + 0] -= dImpN * iiA * cAxN;
304
- av[ai + 1] -= dImpN * iiA * cAyN;
305
- av[ai + 2] -= dImpN * iiA * cAzN;
306
- }
307
- if (imB > 0) {
308
- lv[bi + 0] += dImpN * imB * nx;
309
- lv[bi + 1] += dImpN * imB * ny;
310
- lv[bi + 2] += dImpN * imB * nz;
311
- av[bi + 0] += dImpN * iiB * cBxN;
312
- av[bi + 1] += dImpN * iiB * cByN;
313
- av[bi + 2] += dImpN * iiB * cBzN;
314
- }
315
- }
316
- // Friction tangent basis. Pick the axis least aligned with n to avoid a
317
- // near-zero cross product.
340
+ c.cAxN = cAxN;
341
+ c.cAyN = cAyN;
342
+ c.cAzN = cAzN;
343
+ c.cBxN = cBxN;
344
+ c.cByN = cByN;
345
+ c.cBzN = cBzN;
346
+ c.jacInvN = denomN > 0 ? 1 / denomN : 0;
347
+ // Restitution reference, captured from initial relVelN.
348
+ const vAx = lv[ai + 0] + av[ai + 1] * rAz - av[ai + 2] * rAy;
349
+ const vAy = lv[ai + 1] + av[ai + 2] * rAx - av[ai + 0] * rAz;
350
+ const vAz = lv[ai + 2] + av[ai + 0] * rAy - av[ai + 1] * rAx;
351
+ const vBx = lv[bi + 0] + av[bi + 1] * rBz - av[bi + 2] * rBy;
352
+ const vBy = lv[bi + 1] + av[bi + 2] * rBx - av[bi + 0] * rBz;
353
+ const vBz = lv[bi + 2] + av[bi + 0] * rBy - av[bi + 1] * rBx;
354
+ const relVelN0 = (vBx - vAx) * nx + (vBy - vAy) * ny + (vBz - vAz) * nz;
355
+ c.bounceVel = c.restitution > 0 && relVelN0 < -BOUNCE_THRESHOLD
356
+ ? -c.restitution * relVelN0
357
+ : 0;
358
+ // Friction tangent basis. Pick the axis least aligned with n.
318
359
  let t1x, t1y, t1z;
319
360
  if (Math.abs(nx) < 0.7071) {
320
361
  t1x = 0;
@@ -326,36 +367,138 @@ function solveContactRow(c, lv, av, invMass, invInertia) {
326
367
  t1y = 0;
327
368
  t1z = -nx;
328
369
  }
329
- const l = Math.hypot(t1x, t1y, t1z);
330
- if (l < 1e-8)
370
+ const tl = Math.hypot(t1x, t1y, t1z);
371
+ if (tl > 1e-8) {
372
+ const tInv = 1 / tl;
373
+ t1x *= tInv;
374
+ t1y *= tInv;
375
+ t1z *= tInv;
376
+ }
377
+ else {
378
+ c.jacInvT1 = 0;
379
+ c.jacInvT2 = 0;
331
380
  return;
332
- const tInv = 1 / l;
333
- t1x *= tInv;
334
- t1y *= tInv;
335
- t1z *= tInv;
381
+ }
336
382
  const t2x = ny * t1z - nz * t1y;
337
383
  const t2y = nz * t1x - nx * t1z;
338
384
  const t2z = nx * t1y - ny * t1x;
339
- const muNormal = c.friction * c.appliedNormalImpulse;
340
- applyFrictionRow(c, ai, bi, t1x, t1y, t1z, rAx, rAy, rAz, rBx, rBy, rBz, dvX, dvY, dvZ, imA, imB, iiA, iiB, lv, av, muNormal, 1);
341
- applyFrictionRow(c, ai, bi, t2x, t2y, t2z, rAx, rAy, rAz, rBx, rBy, rBz, dvX, dvY, dvZ, imA, imB, iiA, iiB, lv, av, muNormal, 2);
385
+ c.t1x = t1x;
386
+ c.t1y = t1y;
387
+ c.t1z = t1z;
388
+ c.t2x = t2x;
389
+ c.t2y = t2y;
390
+ c.t2z = t2z;
391
+ // Friction Jacobians.
392
+ const cAxT1 = rAy * t1z - rAz * t1y;
393
+ const cAyT1 = rAz * t1x - rAx * t1z;
394
+ const cAzT1 = rAx * t1y - rAy * t1x;
395
+ const cBxT1 = rBy * t1z - rBz * t1y;
396
+ const cByT1 = rBz * t1x - rBx * t1z;
397
+ const cBzT1 = rBx * t1y - rBy * t1x;
398
+ const denomT1 = imA + imB +
399
+ (cAxT1 * cAxT1 + cAyT1 * cAyT1 + cAzT1 * cAzT1) * iiA +
400
+ (cBxT1 * cBxT1 + cByT1 * cByT1 + cBzT1 * cBzT1) * iiB;
401
+ c.cAxT1 = cAxT1;
402
+ c.cAyT1 = cAyT1;
403
+ c.cAzT1 = cAzT1;
404
+ c.cBxT1 = cBxT1;
405
+ c.cByT1 = cByT1;
406
+ c.cBzT1 = cBzT1;
407
+ c.jacInvT1 = denomT1 > 0 ? 1 / denomT1 : 0;
408
+ const cAxT2 = rAy * t2z - rAz * t2y;
409
+ const cAyT2 = rAz * t2x - rAx * t2z;
410
+ const cAzT2 = rAx * t2y - rAy * t2x;
411
+ const cBxT2 = rBy * t2z - rBz * t2y;
412
+ const cByT2 = rBz * t2x - rBx * t2z;
413
+ const cBzT2 = rBx * t2y - rBy * t2x;
414
+ const denomT2 = imA + imB +
415
+ (cAxT2 * cAxT2 + cAyT2 * cAyT2 + cAzT2 * cAzT2) * iiA +
416
+ (cBxT2 * cBxT2 + cByT2 * cByT2 + cBzT2 * cBzT2) * iiB;
417
+ c.cAxT2 = cAxT2;
418
+ c.cAyT2 = cAyT2;
419
+ c.cAzT2 = cAzT2;
420
+ c.cBxT2 = cBxT2;
421
+ c.cByT2 = cByT2;
422
+ c.cBzT2 = cBzT2;
423
+ c.jacInvT2 = denomT2 > 0 ? 1 / denomT2 : 0;
342
424
  }
343
- function applyFrictionRow(c, ai, bi, tx, ty, tz, rAx, rAy, rAz, rBx, rBy, rBz, dvX, dvY, dvZ, imA, imB, iiA, iiB, lv, av, muNormal, slot) {
425
+ // ITER: one push-only normal row + two Coulomb friction rows. Friction
426
+ // bound depends on the *current* applied normal impulse, so it tightens
427
+ // as the normal row converges.
428
+ function iterateContactRow(c, lv, av, invMass, invInertia) {
429
+ const imA = invMass[c.bodyA];
430
+ const imB = invMass[c.bodyB];
431
+ if (imA === 0 && imB === 0)
432
+ return;
433
+ const iiA = invInertia[c.bodyA];
434
+ const iiB = invInertia[c.bodyB];
435
+ const ai = c.bodyA * 3, bi = c.bodyB * 3;
436
+ const rAx = c.rAx, rAy = c.rAy, rAz = c.rAz;
437
+ const rBx = c.rBx, rBy = c.rBy, rBz = c.rBz;
438
+ const vAx = lv[ai + 0] + av[ai + 1] * rAz - av[ai + 2] * rAy;
439
+ const vAy = lv[ai + 1] + av[ai + 2] * rAx - av[ai + 0] * rAz;
440
+ const vAz = lv[ai + 2] + av[ai + 0] * rAy - av[ai + 1] * rAx;
441
+ const vBx = lv[bi + 0] + av[bi + 1] * rBz - av[bi + 2] * rBy;
442
+ const vBy = lv[bi + 1] + av[bi + 2] * rBx - av[bi + 0] * rBz;
443
+ const vBz = lv[bi + 2] + av[bi + 0] * rBy - av[bi + 1] * rBx;
444
+ const dvx = vBx - vAx;
445
+ const dvy = vBy - vAy;
446
+ const dvz = vBz - vAz;
447
+ // Normal row.
448
+ const jacInvN = c.jacInvN;
449
+ if (jacInvN > 0) {
450
+ const nx = c.nx, ny = c.ny, nz = c.nz;
451
+ const relVelN = dvx * nx + dvy * ny + dvz * nz;
452
+ let dImpN = (c.bounceVel - relVelN) * jacInvN;
453
+ const oldN = c.appliedNormalImpulse;
454
+ let newN = oldN + dImpN;
455
+ if (newN < 0) {
456
+ newN = 0;
457
+ dImpN = -oldN;
458
+ }
459
+ c.appliedNormalImpulse = newN;
460
+ if (dImpN !== 0) {
461
+ const cAxN = c.cAxN, cAyN = c.cAyN, cAzN = c.cAzN;
462
+ const cBxN = c.cBxN, cByN = c.cByN, cBzN = c.cBzN;
463
+ if (imA > 0) {
464
+ lv[ai + 0] -= dImpN * imA * nx;
465
+ lv[ai + 1] -= dImpN * imA * ny;
466
+ lv[ai + 2] -= dImpN * imA * nz;
467
+ av[ai + 0] -= dImpN * iiA * cAxN;
468
+ av[ai + 1] -= dImpN * iiA * cAyN;
469
+ av[ai + 2] -= dImpN * iiA * cAzN;
470
+ }
471
+ if (imB > 0) {
472
+ lv[bi + 0] += dImpN * imB * nx;
473
+ lv[bi + 1] += dImpN * imB * ny;
474
+ lv[bi + 2] += dImpN * imB * nz;
475
+ av[bi + 0] += dImpN * iiB * cBxN;
476
+ av[bi + 1] += dImpN * iiB * cByN;
477
+ av[bi + 2] += dImpN * iiB * cBzN;
478
+ }
479
+ }
480
+ }
481
+ // Friction. Bound = ±μ · current normal impulse.
482
+ const muNormal = c.friction * c.appliedNormalImpulse;
344
483
  if (muNormal <= 0)
345
484
  return;
346
- const cAx = rAy * tz - rAz * ty;
347
- const cAy = rAz * tx - rAx * tz;
348
- const cAz = rAx * ty - rAy * tx;
349
- const cBx = rBy * tz - rBz * ty;
350
- const cBy = rBz * tx - rBx * tz;
351
- const cBz = rBx * ty - rBy * tx;
352
- const denom = imA + imB +
353
- (cAx * cAx + cAy * cAy + cAz * cAz) * iiA +
354
- (cBx * cBx + cBy * cBy + cBz * cBz) * iiB;
355
- if (denom <= 0)
485
+ // Re-read dv after the normal impulse possibly changed lv/av.
486
+ const vAx2 = lv[ai + 0] + av[ai + 1] * rAz - av[ai + 2] * rAy;
487
+ const vAy2 = lv[ai + 1] + av[ai + 2] * rAx - av[ai + 0] * rAz;
488
+ const vAz2 = lv[ai + 2] + av[ai + 0] * rAy - av[ai + 1] * rAx;
489
+ const vBx2 = lv[bi + 0] + av[bi + 1] * rBz - av[bi + 2] * rBy;
490
+ const vBy2 = lv[bi + 1] + av[bi + 2] * rBx - av[bi + 0] * rBz;
491
+ const vBz2 = lv[bi + 2] + av[bi + 0] * rBy - av[bi + 1] * rBx;
492
+ const dvx2 = vBx2 - vAx2;
493
+ const dvy2 = vBy2 - vAy2;
494
+ const dvz2 = vBz2 - vAz2;
495
+ 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, iiA, iiB, lv, av, 1);
496
+ 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, iiA, iiB, lv, av, 2);
497
+ }
498
+ function applyFrictionTangent(c, ai, bi, dvx, dvy, dvz, tx, ty, tz, cAx, cAy, cAz, cBx, cBy, cBz, jacInv, muNormal, imA, imB, iiA, iiB, lv, av, slot) {
499
+ if (jacInv <= 0)
356
500
  return;
357
- const jacInv = 1 / denom;
358
- const relVel = dvX * tx + dvY * ty + dvZ * tz;
501
+ const relVel = dvx * tx + dvy * ty + dvz * tz;
359
502
  let dImp = -relVel * jacInv;
360
503
  const old = slot === 1 ? c.appliedFrictionImpulse1 : c.appliedFrictionImpulse2;
361
504
  let next = old + dImp;