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