reze-engine 0.14.0 → 0.15.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 (93) hide show
  1. package/README.md +81 -108
  2. package/dist/engine.d.ts +1 -7
  3. package/dist/engine.d.ts.map +1 -1
  4. package/dist/engine.js +4 -7
  5. package/dist/index.d.ts +1 -1
  6. package/dist/index.d.ts.map +1 -1
  7. package/dist/index.js +1 -1
  8. package/dist/physics/body.d.ts +30 -0
  9. package/dist/physics/body.d.ts.map +1 -0
  10. package/dist/physics/body.js +215 -0
  11. package/dist/physics/constraint.d.ts +17 -0
  12. package/dist/physics/constraint.d.ts.map +1 -0
  13. package/dist/physics/constraint.js +102 -0
  14. package/dist/physics/contact.d.ts +32 -0
  15. package/dist/physics/contact.d.ts.map +1 -0
  16. package/dist/physics/contact.js +728 -0
  17. package/dist/physics/index.d.ts +4 -0
  18. package/dist/physics/index.d.ts.map +1 -0
  19. package/dist/physics/index.js +3 -0
  20. package/dist/physics/physics.d.ts +31 -0
  21. package/dist/physics/physics.d.ts.map +1 -0
  22. package/dist/physics/physics.js +211 -0
  23. package/dist/physics/solver.d.ts +5 -0
  24. package/dist/physics/solver.d.ts.map +1 -0
  25. package/dist/physics/solver.js +416 -0
  26. package/dist/physics/types.d.ts +46 -0
  27. package/dist/physics/types.d.ts.map +1 -0
  28. package/dist/physics/types.js +12 -0
  29. package/dist/physics/world.d.ts +12 -0
  30. package/dist/physics/world.d.ts.map +1 -0
  31. package/dist/physics/world.js +146 -0
  32. package/dist/physics-debug.d.ts +30 -0
  33. package/dist/physics-debug.d.ts.map +1 -0
  34. package/dist/physics-debug.js +526 -0
  35. package/dist/shaders/materials/hair.d.ts +1 -1
  36. package/dist/shaders/materials/hair.d.ts.map +1 -1
  37. package/dist/shaders/materials/hair.js +2 -2
  38. package/dist/shaders/passes/physics-debug.d.ts +2 -0
  39. package/dist/shaders/passes/physics-debug.d.ts.map +1 -0
  40. package/dist/shaders/passes/physics-debug.js +69 -0
  41. package/package.json +3 -6
  42. package/src/engine.ts +5 -9
  43. package/src/index.ts +1 -1
  44. package/src/physics/body.ts +305 -0
  45. package/src/physics/constraint.ts +151 -0
  46. package/src/physics/contact.ts +983 -0
  47. package/src/physics/index.ts +8 -0
  48. package/src/physics/physics.ts +255 -0
  49. package/src/physics/solver.ts +430 -0
  50. package/src/physics/types.ts +50 -0
  51. package/src/physics/world.ts +152 -0
  52. package/src/shaders/materials/hair.ts +2 -2
  53. package/dist/ammo-loader.d.ts +0 -3
  54. package/dist/ammo-loader.d.ts.map +0 -1
  55. package/dist/ammo-loader.js +0 -26
  56. package/dist/physics.d.ts +0 -86
  57. package/dist/physics.d.ts.map +0 -1
  58. package/dist/physics.js +0 -527
  59. package/dist/shaders/body.d.ts +0 -2
  60. package/dist/shaders/body.d.ts.map +0 -1
  61. package/dist/shaders/body.js +0 -199
  62. package/dist/shaders/classify.d.ts +0 -4
  63. package/dist/shaders/classify.d.ts.map +0 -1
  64. package/dist/shaders/classify.js +0 -12
  65. package/dist/shaders/cloth_rough.d.ts +0 -2
  66. package/dist/shaders/cloth_rough.d.ts.map +0 -1
  67. package/dist/shaders/cloth_rough.js +0 -178
  68. package/dist/shaders/cloth_smooth.d.ts +0 -2
  69. package/dist/shaders/cloth_smooth.d.ts.map +0 -1
  70. package/dist/shaders/cloth_smooth.js +0 -174
  71. package/dist/shaders/default.d.ts +0 -2
  72. package/dist/shaders/default.d.ts.map +0 -1
  73. package/dist/shaders/default.js +0 -171
  74. package/dist/shaders/eye.d.ts +0 -2
  75. package/dist/shaders/eye.d.ts.map +0 -1
  76. package/dist/shaders/eye.js +0 -146
  77. package/dist/shaders/face.d.ts +0 -2
  78. package/dist/shaders/face.d.ts.map +0 -1
  79. package/dist/shaders/face.js +0 -199
  80. package/dist/shaders/hair.d.ts +0 -2
  81. package/dist/shaders/hair.d.ts.map +0 -1
  82. package/dist/shaders/hair.js +0 -176
  83. package/dist/shaders/metal.d.ts +0 -2
  84. package/dist/shaders/metal.d.ts.map +0 -1
  85. package/dist/shaders/metal.js +0 -174
  86. package/dist/shaders/nodes.d.ts +0 -2
  87. package/dist/shaders/nodes.d.ts.map +0 -1
  88. package/dist/shaders/nodes.js +0 -456
  89. package/dist/shaders/stockings.d.ts +0 -2
  90. package/dist/shaders/stockings.d.ts.map +0 -1
  91. package/dist/shaders/stockings.js +0 -244
  92. package/src/ammo-loader.ts +0 -31
  93. package/src/physics.ts +0 -706
@@ -0,0 +1,8 @@
1
+ export { RezePhysics } from "./physics"
2
+ export { RigidBodyStore } from "./body"
3
+ export {
4
+ RigidbodyShape,
5
+ RigidbodyType,
6
+ type Rigidbody,
7
+ type Joint,
8
+ } from "./types"
@@ -0,0 +1,255 @@
1
+ import { Vec3, Quat, Mat4 } from "../math"
2
+ import type { Rigidbody, Joint } from "./types"
3
+ import { RigidbodyType } from "./types"
4
+ import { RigidBodyStore } from "./body"
5
+ import { World } from "./world"
6
+ import { buildConstraints, type SixDofSpringConstraint } from "./constraint"
7
+ import { ContactPool } from "./contact"
8
+
9
+ const _bodyMat = new Float32Array(16)
10
+ const _boneMat = new Float32Array(16)
11
+ const _scratchQuat = new Quat(0, 0, 0, 1)
12
+
13
+ // Static / kinematic bodies follow their bone via boneWorld × bodyOffset;
14
+ // dynamic bodies integrate under gravity + constraints and write their pose
15
+ // back via bodyWorld × bodyOffsetInverse.
16
+ export class RezePhysics {
17
+ private rigidbodies: Rigidbody[]
18
+ private joints: Joint[]
19
+ private store: RigidBodyStore
20
+ private world: World
21
+ private constraints: SixDofSpringConstraint[]
22
+ private contacts: ContactPool
23
+ private firstFrame = true
24
+ // Fixed-timestep accumulator: physics runs at 75 Hz regardless of render
25
+ // rate, so spring impulse, damping, and integration are deterministic.
26
+ private timeAccum = 0
27
+ private readonly fixedTimeStep = 1 / 75
28
+ private readonly maxSubSteps = 10
29
+
30
+ constructor(rigidbodies: Rigidbody[], joints: Joint[] = []) {
31
+ this.rigidbodies = rigidbodies
32
+ this.joints = joints
33
+ this.store = new RigidBodyStore(rigidbodies)
34
+ this.world = new World(new Vec3(0, -98, 0))
35
+ this.constraints = buildConstraints(rigidbodies, joints)
36
+ this.contacts = new ContactPool()
37
+ }
38
+
39
+ setGravity(gravity: Vec3): void {
40
+ this.world.setGravity(gravity)
41
+ }
42
+ getGravity(): Vec3 {
43
+ return this.world.gravity
44
+ }
45
+ getRigidbodies(): Rigidbody[] {
46
+ return this.rigidbodies
47
+ }
48
+ getJoints(): Joint[] {
49
+ return this.joints
50
+ }
51
+ getStore(): RigidBodyStore {
52
+ return this.store
53
+ }
54
+
55
+ getRigidbodyTransforms(): Array<{ position: Vec3; rotation: Quat }> {
56
+ const out: Array<{ position: Vec3; rotation: Quat }> = []
57
+ const pos = this.store.positions
58
+ const ori = this.store.orientations
59
+ for (let i = 0; i < this.store.count; i++) {
60
+ const i3 = i * 3
61
+ const i4 = i * 4
62
+ out.push({
63
+ position: new Vec3(pos[i3 + 0], pos[i3 + 1], pos[i3 + 2]),
64
+ rotation: new Quat(ori[i4 + 0], ori[i4 + 1], ori[i4 + 2], ori[i4 + 3]),
65
+ })
66
+ }
67
+ return out
68
+ }
69
+
70
+ // Snap dynamic bodies back to their bone-driven pose, zero velocities.
71
+ // Used when the simulation diverged or the user scrubbed the timeline.
72
+ reset(boneWorldMatrices: Mat4[]): void {
73
+ if (this.firstFrame) return
74
+ this.snapBodiesToBones(boneWorldMatrices)
75
+ }
76
+
77
+ step(dt: number, boneWorldMatrices: Mat4[], boneInverseBindMatrices: Float32Array): void {
78
+ if (this.firstFrame) {
79
+ this.store.computeBoneOffsets(boneInverseBindMatrices)
80
+ // Start at current bone pose, not the PMX bind pose, so animations
81
+ // that skip frame 0 don't pop bodies on first step.
82
+ this.snapBodiesToBones(boneWorldMatrices)
83
+ this.firstFrame = false
84
+ }
85
+
86
+ // Sync once per render frame; kinematic targets don't change between
87
+ // substeps. Render dt is used to derive kinematic velocities from the
88
+ // bone-pose delta so joints feel the kinematic motion.
89
+ this.syncFromBones(boneWorldMatrices, dt)
90
+
91
+ // Fixed-timestep substeps. The maxSubSteps cap prevents runaway after
92
+ // a long stall (tab backgrounded, etc.).
93
+ this.timeAccum += dt
94
+ let sub = 0
95
+ while (this.timeAccum >= this.fixedTimeStep && sub < this.maxSubSteps) {
96
+ this.world.step(this.store, this.constraints, this.contacts, this.fixedTimeStep)
97
+ this.timeAccum -= this.fixedTimeStep
98
+ sub++
99
+ }
100
+ if (sub === this.maxSubSteps) this.timeAccum = 0
101
+
102
+ this.applyDynamicsToBones(boneWorldMatrices)
103
+ }
104
+
105
+ // Snap all bone-bound bodies to boneWorld × bodyOffset, zero velocities.
106
+ private snapBodiesToBones(boneWorldMatrices: Mat4[]): void {
107
+ const N = this.store.count
108
+ const offsets = this.store.bodyOffsetMatrix
109
+ const positions = this.store.positions
110
+ const orientations = this.store.orientations
111
+ const lv = this.store.linearVelocities
112
+ const av = this.store.angularVelocities
113
+ const boneIdx = this.store.boneIndex
114
+
115
+ for (let i = 0; i < N; i++) {
116
+ const b = boneIdx[i]
117
+ if (b < 0 || b >= boneWorldMatrices.length) continue
118
+
119
+ Mat4.multiplyArrays(boneWorldMatrices[b].values, 0, offsets, i * 16, _bodyMat, 0)
120
+
121
+ const i3 = i * 3
122
+ const i4 = i * 4
123
+ positions[i3 + 0] = _bodyMat[12]
124
+ positions[i3 + 1] = _bodyMat[13]
125
+ positions[i3 + 2] = _bodyMat[14]
126
+ Mat4.toQuatFromArrayInto(_bodyMat, 0, _scratchQuat)
127
+ orientations[i4 + 0] = _scratchQuat.x
128
+ orientations[i4 + 1] = _scratchQuat.y
129
+ orientations[i4 + 2] = _scratchQuat.z
130
+ orientations[i4 + 3] = _scratchQuat.w
131
+
132
+ lv[i3 + 0] = 0
133
+ lv[i3 + 1] = 0
134
+ lv[i3 + 2] = 0
135
+ av[i3 + 0] = 0
136
+ av[i3 + 1] = 0
137
+ av[i3 + 2] = 0
138
+ }
139
+ }
140
+
141
+ // Pull Static / Kinematic bodies to their bones and derive velocities
142
+ // from the bone-pose delta — joints attached to fast limbs need to see
143
+ // the kinematic motion, not just the position jump, or dependent cloth
144
+ // bodies lag behind quick movement.
145
+ private syncFromBones(boneWorldMatrices: Mat4[], dt: number): void {
146
+ const N = this.store.count
147
+ const offsets = this.store.bodyOffsetMatrix
148
+ const positions = this.store.positions
149
+ const orientations = this.store.orientations
150
+ const lv = this.store.linearVelocities
151
+ const av = this.store.angularVelocities
152
+ const types = this.store.type
153
+ const boneIdx = this.store.boneIndex
154
+ const invDt = dt > 0 ? 1 / dt : 0
155
+
156
+ for (let i = 0; i < N; i++) {
157
+ const t = types[i]
158
+ if (t !== RigidbodyType.Static && t !== RigidbodyType.Kinematic) continue
159
+ const b = boneIdx[i]
160
+ if (b < 0 || b >= boneWorldMatrices.length) continue
161
+
162
+ Mat4.multiplyArrays(boneWorldMatrices[b].values, 0, offsets, i * 16, _bodyMat, 0)
163
+
164
+ const i3 = i * 3
165
+ const i4 = i * 4
166
+
167
+ // Save previous transform for the velocity diff. invDt = 0 (first
168
+ // frame / reset) skips the diff and zeros velocities.
169
+ const oldPx = positions[i3 + 0],
170
+ oldPy = positions[i3 + 1],
171
+ oldPz = positions[i3 + 2]
172
+ const oldOx = orientations[i4 + 0],
173
+ oldOy = orientations[i4 + 1]
174
+ const oldOz = orientations[i4 + 2],
175
+ oldOw = orientations[i4 + 3]
176
+
177
+ positions[i3 + 0] = _bodyMat[12]
178
+ positions[i3 + 1] = _bodyMat[13]
179
+ positions[i3 + 2] = _bodyMat[14]
180
+ Mat4.toQuatFromArrayInto(_bodyMat, 0, _scratchQuat)
181
+ const newOx = _scratchQuat.x,
182
+ newOy = _scratchQuat.y
183
+ const newOz = _scratchQuat.z,
184
+ newOw = _scratchQuat.w
185
+ orientations[i4 + 0] = newOx
186
+ orientations[i4 + 1] = newOy
187
+ orientations[i4 + 2] = newOz
188
+ orientations[i4 + 3] = newOw
189
+
190
+ if (invDt === 0) {
191
+ lv[i3 + 0] = 0
192
+ lv[i3 + 1] = 0
193
+ lv[i3 + 2] = 0
194
+ av[i3 + 0] = 0
195
+ av[i3 + 1] = 0
196
+ av[i3 + 2] = 0
197
+ } else {
198
+ lv[i3 + 0] = (_bodyMat[12] - oldPx) * invDt
199
+ lv[i3 + 1] = (_bodyMat[13] - oldPy) * invDt
200
+ lv[i3 + 2] = (_bodyMat[14] - oldPz) * invDt
201
+
202
+ // ω ≈ 2 · qDiff.xyz / dt with qDiff = qNew · conj(qOld). Shortest-
203
+ // arc sign keeps qDiff and −qDiff (same rotation) from doubling ω.
204
+ const cox = -oldOx,
205
+ coy = -oldOy,
206
+ coz = -oldOz,
207
+ cow = oldOw
208
+ const dx = newOw * cox + newOx * cow + newOy * coz - newOz * coy
209
+ const dy = newOw * coy - newOx * coz + newOy * cow + newOz * cox
210
+ const dz = newOw * coz + newOx * coy - newOy * cox + newOz * cow
211
+ const dw = newOw * cow - newOx * cox - newOy * coy - newOz * coz
212
+ const sign = dw < 0 ? -1 : 1
213
+ av[i3 + 0] = 2 * sign * dx * invDt
214
+ av[i3 + 1] = 2 * sign * dy * invDt
215
+ av[i3 + 2] = 2 * sign * dz * invDt
216
+ }
217
+ }
218
+ }
219
+
220
+ // Dynamic bodies write their transform back to the bone matrix:
221
+ // boneWorld = bodyWorld × bodyOffsetInverse.
222
+ private applyDynamicsToBones(boneWorldMatrices: Mat4[]): void {
223
+ const N = this.store.count
224
+ const inv = this.store.bodyOffsetInverse
225
+ const positions = this.store.positions
226
+ const orientations = this.store.orientations
227
+ const types = this.store.type
228
+ const boneIdx = this.store.boneIndex
229
+
230
+ for (let i = 0; i < N; i++) {
231
+ if (types[i] !== RigidbodyType.Dynamic) continue
232
+ const b = boneIdx[i]
233
+ if (b < 0 || b >= boneWorldMatrices.length) continue
234
+
235
+ const i3 = i * 3
236
+ const i4 = i * 4
237
+ Mat4.fromPositionRotationInto(
238
+ positions[i3 + 0],
239
+ positions[i3 + 1],
240
+ positions[i3 + 2],
241
+ orientations[i4 + 0],
242
+ orientations[i4 + 1],
243
+ orientations[i4 + 2],
244
+ orientations[i4 + 3],
245
+ _bodyMat,
246
+ )
247
+ Mat4.multiplyArrays(_bodyMat, 0, inv, i * 16, _boneMat, 0)
248
+
249
+ // Sanity gate against NaN / extreme values — silently drop the update.
250
+ if (Number.isFinite(_boneMat[0]) && Math.abs(_boneMat[0]) < 1e6) {
251
+ boneWorldMatrices[b].values.set(_boneMat)
252
+ }
253
+ }
254
+ }
255
+ }
@@ -0,0 +1,430 @@
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
+ import { Mat4 } from "../math"
7
+ import type { RigidBodyStore } from "./body"
8
+ import type { SixDofSpringConstraint } from "./constraint"
9
+ import { STOP_ERP } from "./constraint"
10
+ import type { Contact, ContactPool } from "./contact"
11
+
12
+ const BOUNCE_THRESHOLD = 2.0
13
+
14
+ // Module-level scratch (no per-iter allocations).
15
+ const _TA = new Float32Array(16)
16
+ const _TB = new Float32Array(16)
17
+ const _bodyMatA = new Float32Array(16)
18
+ const _bodyMatB = new Float32Array(16)
19
+ const _linAxes = new Float32Array(9) // 3 linear axes × xyz
20
+ const _angAxes = new Float32Array(9) // 3 angular axes × xyz
21
+ const _linDiff = new Float32Array(3)
22
+ const _angDiff = new Float32Array(3)
23
+ const _rA = new Float32Array(3)
24
+ const _rB = new Float32Array(3)
25
+
26
+ export function solveConstraints(
27
+ store: RigidBodyStore,
28
+ constraints: SixDofSpringConstraint[],
29
+ contacts: ContactPool,
30
+ dt: number,
31
+ iterations: number,
32
+ ): void {
33
+ if (dt <= 0) return
34
+ if (constraints.length === 0 && contacts.count === 0) return
35
+
36
+ const invDt = 1 / dt
37
+ const lv = store.linearVelocities
38
+ const av = store.angularVelocities
39
+ const pos = store.positions
40
+ const invMass = store.invMass
41
+ const invInertia = store.invInertia
42
+
43
+ for (let iter = 0; iter < iterations; iter++) {
44
+ for (let c = 0; c < constraints.length; c++) {
45
+ const con = constraints[c]
46
+ const a = con.bodyA
47
+ const b = con.bodyB
48
+ const imA = invMass[a]
49
+ const imB = invMass[b]
50
+ const iiA = invInertia[a]
51
+ const iiB = invInertia[b]
52
+ if (imA === 0 && imB === 0) continue
53
+
54
+ buildBodyMat(store, a, _bodyMatA)
55
+ buildBodyMat(store, b, _bodyMatB)
56
+ Mat4.multiplyArrays(_bodyMatA, 0, con.frameA, 0, _TA, 0)
57
+ Mat4.multiplyArrays(_bodyMatB, 0, con.frameB, 0, _TB, 0)
58
+
59
+ // Mass-weighted shared anchor: kinematic partner gets weight 1 so
60
+ // the anchor sits exactly on it; two dynamic bodies blend by inverse
61
+ // mass. Lever arms are measured from each CG to that shared anchor.
62
+ const ai = a * 3
63
+ const bi = b * 3
64
+ const weightA = imB === 0 ? 1 : imA / (imA + imB)
65
+ const weightB = 1 - weightA
66
+ const anchorX = _TA[12] * weightA + _TB[12] * weightB
67
+ const anchorY = _TA[13] * weightA + _TB[13] * weightB
68
+ const anchorZ = _TA[14] * weightA + _TB[14] * weightB
69
+ _rA[0] = anchorX - pos[ai + 0]
70
+ _rA[1] = anchorY - pos[ai + 1]
71
+ _rA[2] = anchorZ - pos[ai + 2]
72
+ _rB[0] = anchorX - pos[bi + 0]
73
+ _rB[1] = anchorY - pos[bi + 1]
74
+ _rB[2] = anchorZ - pos[bi + 2]
75
+
76
+ // Linear part: linearDiff = TA.basis^T · (TB.origin − TA.origin),
77
+ // axes = TA's columns 0/1/2 in world space.
78
+ const dxw = _TB[12] - _TA[12]
79
+ const dyw = _TB[13] - _TA[13]
80
+ const dzw = _TB[14] - _TA[14]
81
+ _linDiff[0] = _TA[0] * dxw + _TA[1] * dyw + _TA[2] * dzw
82
+ _linDiff[1] = _TA[4] * dxw + _TA[5] * dyw + _TA[6] * dzw
83
+ _linDiff[2] = _TA[8] * dxw + _TA[9] * dyw + _TA[10] * dzw
84
+
85
+ _linAxes[0] = _TA[0]; _linAxes[1] = _TA[1]; _linAxes[2] = _TA[2]
86
+ _linAxes[3] = _TA[4]; _linAxes[4] = _TA[5]; _linAxes[5] = _TA[6]
87
+ _linAxes[6] = _TA[8]; _linAxes[7] = _TA[9]; _linAxes[8] = _TA[10]
88
+
89
+ for (let i = 0; i < 3; i++) {
90
+ const lo = con.linearMin[i]
91
+ const hi = con.linearMax[i]
92
+ const curr = _linDiff[i]
93
+ const off = i * 3
94
+ const axx = _linAxes[off + 0]
95
+ const axy = _linAxes[off + 1]
96
+ const axz = _linAxes[off + 2]
97
+
98
+ // (rA × axis), (rB × axis): angular components of the linear Jacobian.
99
+ const cAx = _rA[1] * axz - _rA[2] * axy
100
+ const cAy = _rA[2] * axx - _rA[0] * axz
101
+ const cAz = _rA[0] * axy - _rA[1] * axx
102
+ const cBx = _rB[1] * axz - _rB[2] * axy
103
+ const cBy = _rB[2] * axx - _rB[0] * axz
104
+ const cBz = _rB[0] * axy - _rB[1] * axx
105
+ const cA2 = cAx * cAx + cAy * cAy + cAz * cAz
106
+ const cB2 = cBx * cBx + cBy * cBy + cBz * cBz
107
+ const denom = imA + imB + cA2 * iiA + cB2 * iiB
108
+ if (denom <= 0) continue
109
+ const jacInv = 1 / denom
110
+
111
+ // v_pivot = v_CG + ω × r.
112
+ const vAx = lv[ai + 0] + av[ai + 1] * _rA[2] - av[ai + 2] * _rA[1]
113
+ const vAy = lv[ai + 1] + av[ai + 2] * _rA[0] - av[ai + 0] * _rA[2]
114
+ const vAz = lv[ai + 2] + av[ai + 0] * _rA[1] - av[ai + 1] * _rA[0]
115
+ const vBx = lv[bi + 0] + av[bi + 1] * _rB[2] - av[bi + 2] * _rB[1]
116
+ const vBy = lv[bi + 1] + av[bi + 2] * _rB[0] - av[bi + 0] * _rB[2]
117
+ const vBz = lv[bi + 2] + av[bi + 0] * _rB[1] - av[bi + 1] * _rB[0]
118
+ const relVel = (vBx - vAx) * axx + (vBy - vAy) * axy + (vBz - vAz) * axz
119
+
120
+ let targetVel = 0
121
+ let active = false
122
+
123
+ if (lo <= hi) {
124
+ let err = 0
125
+ if (curr < lo) err = curr - lo
126
+ else if (curr > hi) err = curr - hi
127
+ if (err !== 0) {
128
+ targetVel = -err * STOP_ERP * invDt
129
+ active = true
130
+ }
131
+ }
132
+ if (con.springEnabled[i]) {
133
+ targetVel += -con.springStiffness[i] * (curr - con.equilibriumPoint[i]) * dt
134
+ active = true
135
+ }
136
+
137
+ if (active) {
138
+ const j = (targetVel - relVel) * jacInv
139
+ if (imA > 0) {
140
+ lv[ai + 0] -= j * imA * axx
141
+ lv[ai + 1] -= j * imA * axy
142
+ lv[ai + 2] -= j * imA * axz
143
+ av[ai + 0] -= j * iiA * cAx
144
+ av[ai + 1] -= j * iiA * cAy
145
+ av[ai + 2] -= j * iiA * cAz
146
+ }
147
+ if (imB > 0) {
148
+ lv[bi + 0] += j * imB * axx
149
+ lv[bi + 1] += j * imB * axy
150
+ lv[bi + 2] += j * imB * axz
151
+ av[bi + 0] += j * iiB * cBx
152
+ av[bi + 1] += j * iiB * cBy
153
+ av[bi + 2] += j * iiB * cBz
154
+ }
155
+ }
156
+ }
157
+
158
+ // Angular part: relative rotation TA^T·TB → Euler XYZ; axes from
159
+ // TA.col2 × TB.col0 (as Bullet's calculatedAxis derivation).
160
+ const r00 = _TA[0]*_TB[0] + _TA[1]*_TB[1] + _TA[2]*_TB[2]
161
+ const r01 = _TA[0]*_TB[4] + _TA[1]*_TB[5] + _TA[2]*_TB[6]
162
+ const r10 = _TA[4]*_TB[0] + _TA[5]*_TB[1] + _TA[6]*_TB[2]
163
+ const r11 = _TA[4]*_TB[4] + _TA[5]*_TB[5] + _TA[6]*_TB[6]
164
+ const r20 = _TA[8]*_TB[0] + _TA[9]*_TB[1] + _TA[10]*_TB[2]
165
+ const r21 = _TA[8]*_TB[4] + _TA[9]*_TB[5] + _TA[10]*_TB[6]
166
+ const r22 = _TA[8]*_TB[8] + _TA[9]*_TB[9] + _TA[10]*_TB[10]
167
+ matrixToEulerXYZ(r00, r01, r10, r11, r20, r21, r22, _angDiff)
168
+
169
+ const a2x = _TA[8], a2y = _TA[9], a2z = _TA[10]
170
+ const b0x = _TB[0], b0y = _TB[1], b0z = _TB[2]
171
+ // ax[1] = a2 × b0; ax[0] = ax[1] × a2; ax[2] = b0 × ax[1].
172
+ let yx = a2y * b0z - a2z * b0y
173
+ let yy = a2z * b0x - a2x * b0z
174
+ let yz = a2x * b0y - a2y * b0x
175
+ let l = Math.hypot(yx, yy, yz)
176
+ if (l > 1e-8) { const inv = 1/l; yx*=inv; yy*=inv; yz*=inv }
177
+ _angAxes[3] = yx; _angAxes[4] = yy; _angAxes[5] = yz
178
+ let xx = yy * a2z - yz * a2y
179
+ let xy = yz * a2x - yx * a2z
180
+ let xz = yx * a2y - yy * a2x
181
+ l = Math.hypot(xx, xy, xz)
182
+ if (l > 1e-8) { const inv = 1/l; xx*=inv; xy*=inv; xz*=inv }
183
+ _angAxes[0] = xx; _angAxes[1] = xy; _angAxes[2] = xz
184
+ let zx = b0y * yz - b0z * yy
185
+ let zy = b0z * yx - b0x * yz
186
+ let zz = b0x * yy - b0y * yx
187
+ l = Math.hypot(zx, zy, zz)
188
+ if (l > 1e-8) { const inv = 1/l; zx*=inv; zy*=inv; zz*=inv }
189
+ _angAxes[6] = zx; _angAxes[7] = zy; _angAxes[8] = zz
190
+
191
+ const angDenom = iiA + iiB
192
+ if (angDenom > 0) {
193
+ const angJacInv = 1 / angDenom
194
+
195
+ for (let i = 0; i < 3; i++) {
196
+ const idx = i + 3
197
+ const lo = con.angularMin[i]
198
+ const hi = con.angularMax[i]
199
+ const curr = _angDiff[i]
200
+ const off = i * 3
201
+ const axx = _angAxes[off + 0]
202
+ const axy = _angAxes[off + 1]
203
+ const axz = _angAxes[off + 2]
204
+
205
+ const relAv =
206
+ (av[bi + 0] - av[ai + 0]) * axx +
207
+ (av[bi + 1] - av[ai + 1]) * axy +
208
+ (av[bi + 2] - av[ai + 2]) * axz
209
+
210
+ // Sign flip vs linear: d(angDiff)/dt = −(ω_B − ω_A)·ax.
211
+ let targetVel = 0
212
+ let active = false
213
+
214
+ if (lo <= hi) {
215
+ let err = 0
216
+ if (curr < lo) err = curr - lo
217
+ else if (curr > hi) err = curr - hi
218
+ if (err !== 0) {
219
+ targetVel = err * STOP_ERP * invDt
220
+ active = true
221
+ }
222
+ }
223
+ if (con.springEnabled[idx]) {
224
+ targetVel += con.springStiffness[idx] * (curr - con.equilibriumPoint[idx]) * dt
225
+ active = true
226
+ }
227
+
228
+ if (active) {
229
+ const j = (targetVel - relAv) * angJacInv
230
+ if (iiA > 0) {
231
+ av[ai + 0] -= j * iiA * axx
232
+ av[ai + 1] -= j * iiA * axy
233
+ av[ai + 2] -= j * iiA * axz
234
+ }
235
+ if (iiB > 0) {
236
+ av[bi + 0] += j * iiB * axx
237
+ av[bi + 1] += j * iiB * axy
238
+ av[bi + 2] += j * iiB * axz
239
+ }
240
+ }
241
+ }
242
+ }
243
+ }
244
+
245
+ for (let ci = 0; ci < contacts.count; ci++) {
246
+ solveContactRow(contacts.get(ci), lv, av, invMass, invInertia)
247
+ }
248
+ }
249
+ }
250
+
251
+ // Per-contact: one push-only normal row + two Coulomb friction rows
252
+ // (impulse bound = ±μ·appliedNormalImpulse).
253
+ function solveContactRow(
254
+ c: Contact,
255
+ lv: Float32Array,
256
+ av: Float32Array,
257
+ invMass: Float32Array,
258
+ invInertia: Float32Array,
259
+ ): void {
260
+ const ai = c.bodyA * 3, bi = c.bodyB * 3
261
+ const imA = invMass[c.bodyA], imB = invMass[c.bodyB]
262
+ const iiA = invInertia[c.bodyA], iiB = invInertia[c.bodyB]
263
+ if (imA === 0 && imB === 0) return
264
+ const rAx = c.rAx, rAy = c.rAy, rAz = c.rAz
265
+ const rBx = c.rBx, rBy = c.rBy, rBz = c.rBz
266
+ const nx = c.nx, ny = c.ny, nz = c.nz
267
+
268
+ const vAx = lv[ai + 0] + av[ai + 1] * rAz - av[ai + 2] * rAy
269
+ const vAy = lv[ai + 1] + av[ai + 2] * rAx - av[ai + 0] * rAz
270
+ const vAz = lv[ai + 2] + av[ai + 0] * rAy - av[ai + 1] * rAx
271
+ const vBx = lv[bi + 0] + av[bi + 1] * rBz - av[bi + 2] * rBy
272
+ const vBy = lv[bi + 1] + av[bi + 2] * rBx - av[bi + 0] * rBz
273
+ const vBz = lv[bi + 2] + av[bi + 0] * rBy - av[bi + 1] * rBx
274
+ const dvX = vBx - vAx
275
+ const dvY = vBy - vAy
276
+ const dvZ = vBz - vAz
277
+
278
+ // Normal row.
279
+ const cAxN = rAy * nz - rAz * ny
280
+ const cAyN = rAz * nx - rAx * nz
281
+ const cAzN = rAx * ny - rAy * nx
282
+ const cBxN = rBy * nz - rBz * ny
283
+ const cByN = rBz * nx - rBx * nz
284
+ const cBzN = rBx * ny - rBy * nx
285
+ const denomN = imA + imB +
286
+ (cAxN * cAxN + cAyN * cAyN + cAzN * cAzN) * iiA +
287
+ (cBxN * cBxN + cByN * cByN + cBzN * cBzN) * iiB
288
+ if (denomN <= 0) return
289
+ const jacInvN = 1 / denomN
290
+
291
+ const relVelN = dvX * nx + dvY * ny + dvZ * nz
292
+ // Position correction is handled directly in world.ts (split impulse).
293
+ // Velocity row only removes approach + applies restitution above bounce.
294
+ let bounce = 0
295
+ if (c.restitution > 0 && relVelN < -BOUNCE_THRESHOLD) {
296
+ bounce = -c.restitution * relVelN
297
+ }
298
+ let dImpN = (bounce - relVelN) * jacInvN
299
+ const oldN = c.appliedNormalImpulse
300
+ let newN = oldN + dImpN
301
+ if (newN < 0) { newN = 0; dImpN = -oldN }
302
+ c.appliedNormalImpulse = newN
303
+
304
+ if (dImpN !== 0) {
305
+ if (imA > 0) {
306
+ lv[ai + 0] -= dImpN * imA * nx
307
+ lv[ai + 1] -= dImpN * imA * ny
308
+ lv[ai + 2] -= dImpN * imA * nz
309
+ av[ai + 0] -= dImpN * iiA * cAxN
310
+ av[ai + 1] -= dImpN * iiA * cAyN
311
+ av[ai + 2] -= dImpN * iiA * cAzN
312
+ }
313
+ if (imB > 0) {
314
+ lv[bi + 0] += dImpN * imB * nx
315
+ lv[bi + 1] += dImpN * imB * ny
316
+ lv[bi + 2] += dImpN * imB * nz
317
+ av[bi + 0] += dImpN * iiB * cBxN
318
+ av[bi + 1] += dImpN * iiB * cByN
319
+ av[bi + 2] += dImpN * iiB * cBzN
320
+ }
321
+ }
322
+
323
+ // Friction tangent basis. Pick the axis least aligned with n to avoid a
324
+ // near-zero cross product.
325
+ let t1x: number, t1y: number, t1z: number
326
+ if (Math.abs(nx) < 0.7071) { t1x = 0; t1y = -nz; t1z = ny }
327
+ else { t1x = nz; t1y = 0; t1z = -nx }
328
+ const l = Math.hypot(t1x, t1y, t1z)
329
+ if (l < 1e-8) return
330
+ const tInv = 1 / l
331
+ t1x *= tInv; t1y *= tInv; t1z *= tInv
332
+ const t2x = ny * t1z - nz * t1y
333
+ const t2y = nz * t1x - nx * t1z
334
+ const t2z = nx * t1y - ny * t1x
335
+
336
+ const muNormal = c.friction * c.appliedNormalImpulse
337
+
338
+ applyFrictionRow(c, ai, bi, t1x, t1y, t1z,
339
+ rAx, rAy, rAz, rBx, rBy, rBz, dvX, dvY, dvZ,
340
+ imA, imB, iiA, iiB, lv, av, muNormal, 1)
341
+ applyFrictionRow(c, ai, bi, t2x, t2y, t2z,
342
+ rAx, rAy, rAz, rBx, rBy, rBz, dvX, dvY, dvZ,
343
+ imA, imB, iiA, iiB, lv, av, muNormal, 2)
344
+ }
345
+
346
+ function applyFrictionRow(
347
+ c: Contact,
348
+ ai: number, bi: number,
349
+ tx: number, ty: number, tz: number,
350
+ rAx: number, rAy: number, rAz: number,
351
+ rBx: number, rBy: number, rBz: number,
352
+ dvX: number, dvY: number, dvZ: number,
353
+ imA: number, imB: number, iiA: number, iiB: number,
354
+ lv: Float32Array, av: Float32Array,
355
+ muNormal: number,
356
+ slot: 1 | 2,
357
+ ): void {
358
+ if (muNormal <= 0) return
359
+ const cAx = rAy * tz - rAz * ty
360
+ const cAy = rAz * tx - rAx * tz
361
+ const cAz = rAx * ty - rAy * tx
362
+ const cBx = rBy * tz - rBz * ty
363
+ const cBy = rBz * tx - rBx * tz
364
+ const cBz = rBx * ty - rBy * tx
365
+ const denom = imA + imB +
366
+ (cAx * cAx + cAy * cAy + cAz * cAz) * iiA +
367
+ (cBx * cBx + cBy * cBy + cBz * cBz) * iiB
368
+ if (denom <= 0) return
369
+ const jacInv = 1 / denom
370
+
371
+ const relVel = dvX * tx + dvY * ty + dvZ * tz
372
+ let dImp = -relVel * jacInv
373
+ const old = slot === 1 ? c.appliedFrictionImpulse1 : c.appliedFrictionImpulse2
374
+ let next = old + dImp
375
+ if (next < -muNormal) { next = -muNormal; dImp = next - old }
376
+ else if (next > muNormal) { next = muNormal; dImp = next - old }
377
+ if (slot === 1) c.appliedFrictionImpulse1 = next
378
+ else c.appliedFrictionImpulse2 = next
379
+
380
+ if (dImp === 0) return
381
+ if (imA > 0) {
382
+ lv[ai + 0] -= dImp * imA * tx
383
+ lv[ai + 1] -= dImp * imA * ty
384
+ lv[ai + 2] -= dImp * imA * tz
385
+ av[ai + 0] -= dImp * iiA * cAx
386
+ av[ai + 1] -= dImp * iiA * cAy
387
+ av[ai + 2] -= dImp * iiA * cAz
388
+ }
389
+ if (imB > 0) {
390
+ lv[bi + 0] += dImp * imB * tx
391
+ lv[bi + 1] += dImp * imB * ty
392
+ lv[bi + 2] += dImp * imB * tz
393
+ av[bi + 0] += dImp * iiB * cBx
394
+ av[bi + 1] += dImp * iiB * cBy
395
+ av[bi + 2] += dImp * iiB * cBz
396
+ }
397
+ }
398
+
399
+ function buildBodyMat(store: RigidBodyStore, i: number, out: Float32Array): void {
400
+ const i3 = i * 3, i4 = i * 4
401
+ Mat4.fromPositionRotationInto(
402
+ store.positions[i3 + 0], store.positions[i3 + 1], store.positions[i3 + 2],
403
+ store.orientations[i4 + 0], store.orientations[i4 + 1], store.orientations[i4 + 2], store.orientations[i4 + 3],
404
+ out,
405
+ )
406
+ }
407
+
408
+ // Euler XYZ from a 3×3 rotation matrix (row-major elements).
409
+ function matrixToEulerXYZ(
410
+ r00: number, r01: number,
411
+ r10: number, r11: number,
412
+ r20: number, r21: number, r22: number,
413
+ out: Float32Array,
414
+ ): void {
415
+ if (r20 < 1) {
416
+ if (r20 > -1) {
417
+ out[0] = Math.atan2(-r21, r22)
418
+ out[1] = Math.asin(r20)
419
+ out[2] = Math.atan2(-r10, r00)
420
+ } else {
421
+ out[0] = -Math.atan2(r01, r11)
422
+ out[1] = -Math.PI * 0.5
423
+ out[2] = 0
424
+ }
425
+ } else {
426
+ out[0] = Math.atan2(r01, r11)
427
+ out[1] = Math.PI * 0.5
428
+ out[2] = 0
429
+ }
430
+ }