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,305 @@
1
+ import { Mat4, Quat } from "../math"
2
+ import { RigidbodyType, RigidbodyShape, type Rigidbody } from "./types"
3
+
4
+ // SoA storage for all rigid bodies. Per-body state, constants, bone-coupling
5
+ // matrices, and a per-step AABB.
6
+ export class RigidBodyStore {
7
+ readonly count: number
8
+
9
+ readonly positions: Float32Array // 3*N
10
+ readonly orientations: Float32Array // 4*N (xyzw)
11
+ readonly linearVelocities: Float32Array // 3*N
12
+ readonly angularVelocities: Float32Array // 3*N
13
+
14
+ readonly invMass: Float32Array // N (0 for static / kinematic)
15
+ // Scalar isotropic inverse inertia. The full 3×3 tensor would be more
16
+ // accurate but PMX shapes are roughly compact, and a single I⁻¹ is far
17
+ // better than reusing invMass (which under-rotates by 100–1000×).
18
+ readonly invInertia: Float32Array
19
+ readonly linearDamping: Float32Array
20
+ readonly angularDamping: Float32Array
21
+ readonly type: Uint8Array
22
+ readonly boneIndex: Int32Array
23
+ readonly friction: Float32Array
24
+ readonly restitution: Float32Array
25
+
26
+ // PMX has 16 collision groups. `collisionGroup[i]` is a single-bit set;
27
+ // `willCollideMask[i]` is the 16-bit set of groups body i collides with.
28
+ readonly collisionGroup: Uint16Array
29
+ readonly willCollideMask: Uint16Array
30
+
31
+ readonly shape: Uint8Array
32
+ readonly size: Float32Array // 3*N (semantics depend on shape)
33
+
34
+ readonly aabbMin: Float32Array // 3*N
35
+ readonly aabbMax: Float32Array // 3*N
36
+
37
+ // bodyOffsetMatrix[i] = boneInverseBind · shapeWorldBind.
38
+ // bodyWorld = boneWorld · bodyOffsetMatrix; boneWorld = bodyWorld · bodyOffsetInverse.
39
+ readonly bodyOffsetMatrix: Float32Array // 16*N column-major
40
+ readonly bodyOffsetInverse: Float32Array // 16*N column-major
41
+ private boneOffsetsReady = false
42
+
43
+ constructor(rigidbodies: Rigidbody[]) {
44
+ const N = rigidbodies.length
45
+ this.count = N
46
+
47
+ this.positions = new Float32Array(N * 3)
48
+ this.orientations = new Float32Array(N * 4)
49
+ this.linearVelocities = new Float32Array(N * 3)
50
+ this.angularVelocities = new Float32Array(N * 3)
51
+ this.invMass = new Float32Array(N)
52
+ this.invInertia = new Float32Array(N)
53
+ this.linearDamping = new Float32Array(N)
54
+ this.angularDamping = new Float32Array(N)
55
+ this.type = new Uint8Array(N)
56
+ this.boneIndex = new Int32Array(N)
57
+ this.bodyOffsetMatrix = new Float32Array(N * 16)
58
+ this.bodyOffsetInverse = new Float32Array(N * 16)
59
+ this.friction = new Float32Array(N)
60
+ this.restitution = new Float32Array(N)
61
+ this.collisionGroup = new Uint16Array(N)
62
+ this.willCollideMask = new Uint16Array(N)
63
+ this.shape = new Uint8Array(N)
64
+ this.size = new Float32Array(N * 3)
65
+ this.aabbMin = new Float32Array(N * 3)
66
+ this.aabbMax = new Float32Array(N * 3)
67
+
68
+ for (let i = 0; i < N; i++) {
69
+ const rb = rigidbodies[i]
70
+ const i3 = i * 3
71
+ const i4 = i * 4
72
+
73
+ this.positions[i3 + 0] = rb.shapePosition.x
74
+ this.positions[i3 + 1] = rb.shapePosition.y
75
+ this.positions[i3 + 2] = rb.shapePosition.z
76
+
77
+ const q = Quat.fromEuler(rb.shapeRotation.x, rb.shapeRotation.y, rb.shapeRotation.z)
78
+ this.orientations[i4 + 0] = q.x
79
+ this.orientations[i4 + 1] = q.y
80
+ this.orientations[i4 + 2] = q.z
81
+ this.orientations[i4 + 3] = q.w
82
+
83
+ const dynamic = rb.type === RigidbodyType.Dynamic && rb.mass > 0
84
+ this.invMass[i] = dynamic ? 1 / rb.mass : 0
85
+ this.invInertia[i] = dynamic ? computeInvInertia(rb) : 0
86
+ this.linearDamping[i] = rb.linearDamping
87
+ this.angularDamping[i] = rb.angularDamping
88
+ this.type[i] = rb.type
89
+ this.boneIndex[i] = rb.boneIndex
90
+ this.friction[i] = rb.friction
91
+ this.restitution[i] = rb.restitution
92
+ this.collisionGroup[i] = 1 << (rb.group & 0xf)
93
+ this.willCollideMask[i] = rb.collisionMask & 0xffff
94
+ this.shape[i] = rb.shape
95
+ this.size[i * 3 + 0] = rb.size.x
96
+ this.size[i * 3 + 1] = rb.size.y
97
+ this.size[i * 3 + 2] = rb.size.z
98
+ }
99
+ }
100
+
101
+ // World-space AABBs for every body. Inflated by margin so contacts stay
102
+ // paired across small velocity jitter without recomputing per iteration.
103
+ updateAabbs(margin = 0.5): void {
104
+ const N = this.count
105
+ const pos = this.positions
106
+ const ori = this.orientations
107
+ const shapes = this.shape
108
+ const sz = this.size
109
+ const minA = this.aabbMin
110
+ const maxA = this.aabbMax
111
+
112
+ for (let i = 0; i < N; i++) {
113
+ const i3 = i * 3
114
+ const i4 = i * 4
115
+ const px = pos[i3 + 0],
116
+ py = pos[i3 + 1],
117
+ pz = pos[i3 + 2]
118
+ let hx = 0,
119
+ hy = 0,
120
+ hz = 0
121
+
122
+ switch (shapes[i]) {
123
+ case RigidbodyShape.Sphere: {
124
+ const r = sz[i3 + 0]
125
+ hx = hy = hz = r
126
+ break
127
+ }
128
+ case RigidbodyShape.Box: {
129
+ // OBB AABB: half-extents projected by |R|·size.
130
+ const qx = ori[i4 + 0],
131
+ qy = ori[i4 + 1],
132
+ qz = ori[i4 + 2],
133
+ qw = ori[i4 + 3]
134
+ const x2 = qx + qx,
135
+ y2 = qy + qy,
136
+ z2 = qz + qz
137
+ const xx = qx * x2,
138
+ yy = qy * y2,
139
+ zz = qz * z2
140
+ const xy = qx * y2,
141
+ xz = qx * z2,
142
+ yz = qy * z2
143
+ const wx = qw * x2,
144
+ wy = qw * y2,
145
+ wz = qw * z2
146
+ const m00 = Math.abs(1 - (yy + zz)),
147
+ m01 = Math.abs(xy + wz),
148
+ m02 = Math.abs(xz - wy)
149
+ const m10 = Math.abs(xy - wz),
150
+ m11 = Math.abs(1 - (xx + zz)),
151
+ m12 = Math.abs(yz + wx)
152
+ const m20 = Math.abs(xz + wy),
153
+ m21 = Math.abs(yz - wx),
154
+ m22 = Math.abs(1 - (xx + yy))
155
+ const sx = sz[i3 + 0],
156
+ sy = sz[i3 + 1],
157
+ szz = sz[i3 + 2]
158
+ hx = m00 * sx + m01 * sy + m02 * szz
159
+ hy = m10 * sx + m11 * sy + m12 * szz
160
+ hz = m20 * sx + m21 * sy + m22 * szz
161
+ break
162
+ }
163
+ case RigidbodyShape.Capsule: {
164
+ // After rotation, cap offsets are ±halfH · R·ŷ, so AABB half-
165
+ // extents = |R·ŷ|·halfH + radius.
166
+ const r = sz[i3 + 0]
167
+ const halfH = sz[i3 + 1] * 0.5
168
+ const qx = ori[i4 + 0],
169
+ qy = ori[i4 + 1],
170
+ qz = ori[i4 + 2],
171
+ qw = ori[i4 + 3]
172
+ // R · (0,1,0) = (2(xy − wz), 1 − 2(xx + zz), 2(yz + wx))
173
+ const rx = 2 * (qx * qy - qw * qz)
174
+ const ry = 1 - 2 * (qx * qx + qz * qz)
175
+ const rz = 2 * (qy * qz + qw * qx)
176
+ hx = Math.abs(rx) * halfH + r
177
+ hy = Math.abs(ry) * halfH + r
178
+ hz = Math.abs(rz) * halfH + r
179
+ break
180
+ }
181
+ }
182
+
183
+ minA[i3 + 0] = px - hx - margin
184
+ minA[i3 + 1] = py - hy - margin
185
+ minA[i3 + 2] = pz - hz - margin
186
+ maxA[i3 + 0] = px + hx + margin
187
+ maxA[i3 + 1] = py + hy + margin
188
+ maxA[i3 + 2] = pz + hz + margin
189
+ }
190
+ }
191
+
192
+ // Compute bone-coupling matrices once, on the first step. Bodies with
193
+ // boneIndex < 0 get identity offsets.
194
+ computeBoneOffsets(boneInverseBindMatrices: Float32Array): void {
195
+ const N = this.count
196
+ const offsets = this.bodyOffsetMatrix
197
+ const inverses = this.bodyOffsetInverse
198
+ const ori = this.orientations
199
+ const pos = this.positions
200
+ const boneIdx = this.boneIndex
201
+ const totalBones = boneInverseBindMatrices.length / 16
202
+
203
+ const shapeWorldBind = _scratchA
204
+ const offsetMat = _scratchB
205
+
206
+ for (let i = 0; i < N; i++) {
207
+ const dst = i * 16
208
+ const b = boneIdx[i]
209
+
210
+ if (b < 0 || b >= totalBones) {
211
+ identity16(offsets, dst)
212
+ identity16(inverses, dst)
213
+ continue
214
+ }
215
+
216
+ // shapeWorldBind = T(shapePosition) · R(shapeRotation)
217
+ const i3 = i * 3
218
+ const i4 = i * 4
219
+ Mat4.fromPositionRotationInto(
220
+ pos[i3 + 0],
221
+ pos[i3 + 1],
222
+ pos[i3 + 2],
223
+ ori[i4 + 0],
224
+ ori[i4 + 1],
225
+ ori[i4 + 2],
226
+ ori[i4 + 3],
227
+ shapeWorldBind,
228
+ )
229
+
230
+ // bodyOffset = boneInverseBind × shapeWorldBind
231
+ Mat4.multiplyArrays(boneInverseBindMatrices, b * 16, shapeWorldBind, 0, offsetMat, 0)
232
+
233
+ // Copy into offsets[dst] and invert into inverses[dst].
234
+ offsets.set(offsetMat, dst)
235
+ const inverseTmp = _scratchC
236
+ const ok = Mat4.inverseInto(offsetMat, inverseTmp)
237
+ if (ok) {
238
+ inverses.set(inverseTmp, dst)
239
+ } else {
240
+ identity16(inverses, dst)
241
+ }
242
+ }
243
+
244
+ this.boneOffsetsReady = true
245
+ }
246
+
247
+ isBoneOffsetsReady(): boolean {
248
+ return this.boneOffsetsReady
249
+ }
250
+ }
251
+
252
+ const _scratchA = new Float32Array(16)
253
+ const _scratchB = new Float32Array(16)
254
+ const _scratchC = new Float32Array(16)
255
+
256
+ // Scalar isotropic inverse inertia. Returns 0 for static bodies (mass = 0).
257
+ // Sphere: I = (2/5)·m·r²
258
+ // Box: I = (1/3)·m·max(a,b,c)²
259
+ // Capsule: I = (1/12)·m·(3r² + h²) (cylinder transverse axis)
260
+ function computeInvInertia(rb: Rigidbody): number {
261
+ const m = rb.mass
262
+ if (m <= 0) return 0
263
+ let I: number
264
+ switch (rb.shape) {
265
+ case RigidbodyShape.Sphere: {
266
+ const r = rb.size.x
267
+ I = 0.4 * m * r * r
268
+ break
269
+ }
270
+ case RigidbodyShape.Box: {
271
+ // Largest half-extent so the long axis isn't under-rotated.
272
+ const a = Math.max(rb.size.x, rb.size.y, rb.size.z)
273
+ I = (1 / 3) * m * a * a
274
+ break
275
+ }
276
+ case RigidbodyShape.Capsule: {
277
+ const r = rb.size.x
278
+ const h = rb.size.y
279
+ I = (1 / 12) * m * (3 * r * r + h * h)
280
+ break
281
+ }
282
+ default:
283
+ I = m
284
+ }
285
+ return I > 0 ? 1 / I : 0
286
+ }
287
+
288
+ function identity16(out: Float32Array, offset: number): void {
289
+ out[offset + 0] = 1
290
+ out[offset + 1] = 0
291
+ out[offset + 2] = 0
292
+ out[offset + 3] = 0
293
+ out[offset + 4] = 0
294
+ out[offset + 5] = 1
295
+ out[offset + 6] = 0
296
+ out[offset + 7] = 0
297
+ out[offset + 8] = 0
298
+ out[offset + 9] = 0
299
+ out[offset + 10] = 1
300
+ out[offset + 11] = 0
301
+ out[offset + 12] = 0
302
+ out[offset + 13] = 0
303
+ out[offset + 14] = 0
304
+ out[offset + 15] = 1
305
+ }
@@ -0,0 +1,151 @@
1
+ import { Mat4 } from "../math"
2
+ import type { Joint, Rigidbody } from "./types"
3
+
4
+ // 6DOF spring constraint trimmed to what MMD uses. Connects bodyA and bodyB
5
+ // via local-space anchor frames; at simulate time the world frames are
6
+ // TA = worldA · frameA, TB = worldB · frameB. The 6 DOFs are the linear
7
+ // diff in TA's basis (axes 0..2) and the Euler-XYZ angular diff between
8
+ // TA's and TB's basis (axes 3..5).
9
+ //
10
+ // Springs (when enabled) drive each DOF toward equilibriumPoint[i] with
11
+ // stiffness[i]. Per-axis stop ERP is 0.475 — PMX joint limits are tuned
12
+ // against this softness.
13
+ export interface SixDofSpringConstraint {
14
+ bodyA: number
15
+ bodyB: number
16
+ // Local 4x4 (column-major) anchor frames on each body.
17
+ frameA: Float32Array
18
+ frameB: Float32Array
19
+ // Per-axis limits. For each i: when min[i] > max[i] the axis is free
20
+ // (Bullet's "free" convention); when min[i] === max[i] the axis is locked.
21
+ linearMin: Float32Array // length 3
22
+ linearMax: Float32Array
23
+ angularMin: Float32Array // length 3, radians
24
+ angularMax: Float32Array
25
+ // Springs.
26
+ springEnabled: Uint8Array // length 6
27
+ springStiffness: Float32Array // length 6 (k)
28
+ equilibriumPoint: Float32Array// length 6, baked at setup time
29
+ }
30
+
31
+ export const STOP_ERP = 0.475
32
+
33
+ // Build per-joint constraints from PMX data:
34
+ // frameA = (bodyA_worldBind)^-1 · jointWorldBind
35
+ // frameB = (bodyB_worldBind)^-1 · jointWorldBind
36
+ // Equilibrium is zero on every axis (both frames coincide at bind pose).
37
+ export function buildConstraints(
38
+ rigidbodies: Rigidbody[],
39
+ joints: Joint[],
40
+ ): SixDofSpringConstraint[] {
41
+ const out: SixDofSpringConstraint[] = []
42
+ const jointWorld = new Float32Array(16)
43
+ const bodyWorld = new Float32Array(16)
44
+ const bodyInv = new Float32Array(16)
45
+
46
+ for (let j = 0; j < joints.length; j++) {
47
+ const joint = joints[j]
48
+ const a = joint.rigidbodyIndexA
49
+ const b = joint.rigidbodyIndexB
50
+ if (a < 0 || b < 0 || a >= rigidbodies.length || b >= rigidbodies.length) continue
51
+ if (a === b) continue
52
+ const rbA = rigidbodies[a]
53
+ const rbB = rigidbodies[b]
54
+
55
+ // jointWorldBind from PMX (Euler XYZ as written by saba reference).
56
+ const jq = eulerToQuat(joint.rotation.x, joint.rotation.y, joint.rotation.z)
57
+ Mat4.fromPositionRotationInto(
58
+ joint.position.x, joint.position.y, joint.position.z,
59
+ jq.x, jq.y, jq.z, jq.w,
60
+ jointWorld,
61
+ )
62
+
63
+ const frameA = new Float32Array(16)
64
+ const frameB = new Float32Array(16)
65
+ if (!buildLocalFrame(rbA, jointWorld, bodyWorld, bodyInv, frameA)) continue
66
+ if (!buildLocalFrame(rbB, jointWorld, bodyWorld, bodyInv, frameB)) continue
67
+
68
+ const linearMin = new Float32Array([joint.positionMin.x, joint.positionMin.y, joint.positionMin.z])
69
+ const linearMax = new Float32Array([joint.positionMax.x, joint.positionMax.y, joint.positionMax.z])
70
+ // Some PMX rigs encode "free" angular axes as ±π·N which wraps badly
71
+ // in limit comparisons — normalize to [-π, π] up front.
72
+ const angularMin = new Float32Array([
73
+ normalizeAngle(joint.rotationMin.x),
74
+ normalizeAngle(joint.rotationMin.y),
75
+ normalizeAngle(joint.rotationMin.z),
76
+ ])
77
+ const angularMax = new Float32Array([
78
+ normalizeAngle(joint.rotationMax.x),
79
+ normalizeAngle(joint.rotationMax.y),
80
+ normalizeAngle(joint.rotationMax.z),
81
+ ])
82
+
83
+ const springEnabled = new Uint8Array(6)
84
+ const springStiffness = new Float32Array(6)
85
+ springStiffness[0] = joint.springPosition.x
86
+ springStiffness[1] = joint.springPosition.y
87
+ springStiffness[2] = joint.springPosition.z
88
+ springStiffness[3] = joint.springRotation.x
89
+ springStiffness[4] = joint.springRotation.y
90
+ springStiffness[5] = joint.springRotation.z
91
+ for (let i = 0; i < 6; i++) springEnabled[i] = springStiffness[i] !== 0 ? 1 : 0
92
+
93
+ out.push({
94
+ bodyA: a,
95
+ bodyB: b,
96
+ frameA,
97
+ frameB,
98
+ linearMin,
99
+ linearMax,
100
+ angularMin,
101
+ angularMax,
102
+ springEnabled,
103
+ springStiffness,
104
+ equilibriumPoint: new Float32Array(6),
105
+ })
106
+ }
107
+
108
+ return out
109
+ }
110
+
111
+ // frame = bodyWorldBind^-1 · jointWorld. False if bodyWorldBind is singular.
112
+ function buildLocalFrame(
113
+ rb: Rigidbody,
114
+ jointWorld: Float32Array,
115
+ bodyWorld: Float32Array,
116
+ bodyInv: Float32Array,
117
+ out: Float32Array,
118
+ ): boolean {
119
+ const q = eulerToQuat(rb.shapeRotation.x, rb.shapeRotation.y, rb.shapeRotation.z)
120
+ Mat4.fromPositionRotationInto(
121
+ rb.shapePosition.x, rb.shapePosition.y, rb.shapePosition.z,
122
+ q.x, q.y, q.z, q.w,
123
+ bodyWorld,
124
+ )
125
+ if (!Mat4.inverseInto(bodyWorld, bodyInv)) return false
126
+ Mat4.multiplyArrays(bodyInv, 0, jointWorld, 0, out, 0)
127
+ return true
128
+ }
129
+
130
+ function normalizeAngle(a: number): number {
131
+ const twoPi = Math.PI * 2
132
+ a = a % twoPi
133
+ if (a < -Math.PI) a += twoPi
134
+ else if (a > Math.PI) a -= twoPi
135
+ return a
136
+ }
137
+
138
+ // ZXY left-handed Euler → quat (matches Quat.fromEuler), inlined to skip
139
+ // the allocation per joint at build time.
140
+ function eulerToQuat(rx: number, ry: number, rz: number) {
141
+ const cx = Math.cos(rx * 0.5), sx = Math.sin(rx * 0.5)
142
+ const cy = Math.cos(ry * 0.5), sy = Math.sin(ry * 0.5)
143
+ const cz = Math.cos(rz * 0.5), sz = Math.sin(rz * 0.5)
144
+ const w = cy * cx * cz + sy * sx * sz
145
+ const x = cy * sx * cz + sy * cx * sz
146
+ const y = sy * cx * cz - cy * sx * sz
147
+ const z = cy * cx * sz - sy * sx * cz
148
+ const len = Math.hypot(x, y, z, w) || 1
149
+ const inv = 1 / len
150
+ return { x: x * inv, y: y * inv, z: z * inv, w: w * inv }
151
+ }