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,983 @@
1
+ // Narrowphase contact generation for sphere/box/capsule pairs.
2
+ //
3
+ // Contact convention: `normal` points from body A toward body B, so a
4
+ // positive normal impulse pushes B away from A. `rA` / `rB` are world-space
5
+ // lever arms from each CG to the contact point. Depth is positive when
6
+ // shapes overlap, ≤ 0 for speculative contacts inside the margin band.
7
+ // Box-box not implemented (PMX rigs rarely use it and it needs SAT + clipping).
8
+
9
+ import { RigidbodyShape } from "./types"
10
+ import type { RigidBodyStore } from "./body"
11
+
12
+ // Speculative contact range. Depth is reported relative to the un-inflated
13
+ // surface, so values 0 ≥ depth ≥ −CONTACT_MARGIN cover the "near touch but
14
+ // not overlapping yet" case. The push-only impulse clamp keeps these inert
15
+ // until actual overlap, but they prevent fast bodies from crossing a thin
16
+ // surface in one substep without ever generating a contact.
17
+ export const CONTACT_MARGIN = 0.04
18
+
19
+ export interface Contact {
20
+ bodyA: number
21
+ bodyB: number
22
+ // Lever arms (world-space) from each body's CG to the contact point.
23
+ rAx: number
24
+ rAy: number
25
+ rAz: number
26
+ rBx: number
27
+ rBy: number
28
+ rBz: number
29
+ // Unit normal pointing A → B.
30
+ nx: number
31
+ ny: number
32
+ nz: number
33
+ depth: number
34
+ friction: number
35
+ restitution: number
36
+ // SI-row state, written by the solver each iter.
37
+ appliedNormalImpulse: number
38
+ appliedFrictionImpulse1: number
39
+ appliedFrictionImpulse2: number
40
+ }
41
+
42
+ function makeContact(): Contact {
43
+ return {
44
+ bodyA: 0,
45
+ bodyB: 0,
46
+ rAx: 0,
47
+ rAy: 0,
48
+ rAz: 0,
49
+ rBx: 0,
50
+ rBy: 0,
51
+ rBz: 0,
52
+ nx: 0,
53
+ ny: 0,
54
+ nz: 0,
55
+ depth: 0,
56
+ friction: 0,
57
+ restitution: 0,
58
+ appliedNormalImpulse: 0,
59
+ appliedFrictionImpulse1: 0,
60
+ appliedFrictionImpulse2: 0,
61
+ }
62
+ }
63
+
64
+ // Pool of reusable Contact objects.
65
+ export class ContactPool {
66
+ private pool: Contact[] = []
67
+ count = 0
68
+
69
+ acquire(): Contact {
70
+ if (this.count < this.pool.length) {
71
+ const c = this.pool[this.count]
72
+ c.appliedNormalImpulse = 0
73
+ c.appliedFrictionImpulse1 = 0
74
+ c.appliedFrictionImpulse2 = 0
75
+ this.count++
76
+ return c
77
+ }
78
+ const c = makeContact()
79
+ this.pool.push(c)
80
+ this.count++
81
+ return c
82
+ }
83
+
84
+ reset(): void {
85
+ this.count = 0
86
+ }
87
+ get(i: number): Contact {
88
+ return this.pool[i]
89
+ }
90
+ }
91
+
92
+ // Geometric mean for friction, arithmetic for restitution.
93
+ function combineMaterials(store: RigidBodyStore, a: number, b: number, out: Contact): void {
94
+ out.friction = Math.sqrt(store.friction[a] * store.friction[b])
95
+ out.restitution = (store.restitution[a] + store.restitution[b]) * 0.5
96
+ }
97
+
98
+ // --- AABB overlap (broadphase reuses this) ---------------------------------
99
+ export function aabbOverlap(store: RigidBodyStore, a: number, b: number): boolean {
100
+ const a3 = a * 3,
101
+ b3 = b * 3
102
+ const minA = store.aabbMin,
103
+ maxA = store.aabbMax
104
+ return (
105
+ minA[a3 + 0] <= maxA[b3 + 0] &&
106
+ maxA[a3 + 0] >= minA[b3 + 0] &&
107
+ minA[a3 + 1] <= maxA[b3 + 1] &&
108
+ maxA[a3 + 1] >= minA[b3 + 1] &&
109
+ minA[a3 + 2] <= maxA[b3 + 2] &&
110
+ maxA[a3 + 2] >= minA[b3 + 2]
111
+ )
112
+ }
113
+
114
+ // --- Sphere–sphere ---------------------------------------------------------
115
+ function detectSphereSphere(store: RigidBodyStore, a: number, b: number, pool: ContactPool): void {
116
+ const ai = a * 3,
117
+ bi = b * 3
118
+ const pos = store.positions,
119
+ sz = store.size
120
+ const dx = pos[bi + 0] - pos[ai + 0]
121
+ const dy = pos[bi + 1] - pos[ai + 1]
122
+ const dz = pos[bi + 2] - pos[ai + 2]
123
+ const rA = sz[ai + 0]
124
+ const rB = sz[bi + 0]
125
+ const sumR = rA + rB
126
+ const sumExt = sumR + CONTACT_MARGIN
127
+ const d2 = dx * dx + dy * dy + dz * dz
128
+ if (d2 > sumExt * sumExt) return
129
+ const d = Math.sqrt(d2)
130
+ let nx: number, ny: number, nz: number
131
+ if (d > 1e-6) {
132
+ nx = dx / d
133
+ ny = dy / d
134
+ nz = dz / d
135
+ } else {
136
+ nx = 0
137
+ ny = 1
138
+ nz = 0
139
+ } // arbitrary axis when fully co-located
140
+ const c = pool.acquire()
141
+ c.bodyA = a
142
+ c.bodyB = b
143
+ c.nx = nx
144
+ c.ny = ny
145
+ c.nz = nz
146
+ c.depth = sumR - d // signed: > 0 overlapping, ≤ 0 within margin
147
+ c.rAx = nx * rA
148
+ c.rAy = ny * rA
149
+ c.rAz = nz * rA
150
+ c.rBx = -nx * rB
151
+ c.rBy = -ny * rB
152
+ c.rBz = -nz * rB
153
+ combineMaterials(store, a, b, c)
154
+ }
155
+
156
+ // --- Sphere–capsule helper -------------------------------------------------
157
+ // Returns closest point on capsule's line segment (centered at cBody, axis=R·ŷ,
158
+ // half-height halfH) to the sphere center sx,sy,sz. Out is (cx,cy,cz).
159
+ function closestPointOnCapsuleSegment(
160
+ cx: number,
161
+ cy: number,
162
+ cz: number,
163
+ ax: number,
164
+ ay: number,
165
+ az: number,
166
+ halfH: number,
167
+ sx: number,
168
+ sy: number,
169
+ sz: number,
170
+ out: Float32Array,
171
+ ): void {
172
+ const dx = sx - cx,
173
+ dy = sy - cy,
174
+ dz = sz - cz
175
+ let t = dx * ax + dy * ay + dz * az
176
+ if (t > halfH) t = halfH
177
+ else if (t < -halfH) t = -halfH
178
+ out[0] = cx + ax * t
179
+ out[1] = cy + ay * t
180
+ out[2] = cz + az * t
181
+ }
182
+
183
+ const _capPoint = new Float32Array(3)
184
+ const _capPointB = new Float32Array(3)
185
+
186
+ function capsuleAxis(store: RigidBodyStore, i: number, out: Float32Array): void {
187
+ const i4 = i * 4
188
+ const qx = store.orientations[i4 + 0]
189
+ const qy = store.orientations[i4 + 1]
190
+ const qz = store.orientations[i4 + 2]
191
+ const qw = store.orientations[i4 + 3]
192
+ // R · (0,1,0)
193
+ out[0] = 2 * (qx * qy - qw * qz)
194
+ out[1] = 1 - 2 * (qx * qx + qz * qz)
195
+ out[2] = 2 * (qy * qz + qw * qx)
196
+ }
197
+
198
+ // --- Sphere–capsule (sphere = a, capsule = b) ------------------------------
199
+ function detectSphereCapsule(store: RigidBodyStore, a: number, b: number, pool: ContactPool): void {
200
+ const pos = store.positions,
201
+ sz = store.size
202
+ const ai = a * 3,
203
+ bi = b * 3
204
+ const sx = pos[ai + 0],
205
+ sy = pos[ai + 1],
206
+ sz_ = pos[ai + 2]
207
+ const cx = pos[bi + 0],
208
+ cy = pos[bi + 1],
209
+ cz = pos[bi + 2]
210
+ const rA = sz[ai + 0]
211
+ const rB = sz[bi + 0]
212
+ const halfH = sz[bi + 1] * 0.5
213
+ const axis = _capPoint
214
+ capsuleAxis(store, b, axis)
215
+ const closest = _capPointB
216
+ closestPointOnCapsuleSegment(cx, cy, cz, axis[0], axis[1], axis[2], halfH, sx, sy, sz_, closest)
217
+ const dx = closest[0] - sx
218
+ const dy = closest[1] - sy
219
+ const dz = closest[2] - sz_
220
+ const sumR = rA + rB
221
+ const sumExt = sumR + CONTACT_MARGIN
222
+ const d2 = dx * dx + dy * dy + dz * dz
223
+ if (d2 > sumExt * sumExt) return
224
+ const d = Math.sqrt(d2)
225
+ let nx: number, ny: number, nz: number
226
+ if (d > 1e-6) {
227
+ nx = dx / d
228
+ ny = dy / d
229
+ nz = dz / d
230
+ } else {
231
+ nx = 0
232
+ ny = 1
233
+ nz = 0
234
+ }
235
+ const c = pool.acquire()
236
+ c.bodyA = a
237
+ c.bodyB = b
238
+ c.nx = nx
239
+ c.ny = ny
240
+ c.nz = nz
241
+ c.depth = sumR - d
242
+ // Contact point on A's surface: sphere center + n * rA. Lever arm rA = that
243
+ // offset since A's CG = sphere center.
244
+ c.rAx = nx * rA
245
+ c.rAy = ny * rA
246
+ c.rAz = nz * rA
247
+ // Contact point on B's surface: closest_on_segment − n * rB, lever from B's CG.
248
+ c.rBx = closest[0] - nx * rB - cx
249
+ c.rBy = closest[1] - ny * rB - cy
250
+ c.rBz = closest[2] - nz * rB - cz
251
+ combineMaterials(store, a, b, c)
252
+ }
253
+
254
+ // --- Capsule–capsule -------------------------------------------------------
255
+ const _cpA = new Float32Array(3)
256
+ const _cpB = new Float32Array(3)
257
+
258
+ // Closest pair on two segments. Adapted from Real-Time Collision Detection §5.1.9.
259
+ function closestPointsTwoSegments(
260
+ p1x: number,
261
+ p1y: number,
262
+ p1z: number,
263
+ q1x: number,
264
+ q1y: number,
265
+ q1z: number,
266
+ p2x: number,
267
+ p2y: number,
268
+ p2z: number,
269
+ q2x: number,
270
+ q2y: number,
271
+ q2z: number,
272
+ outA: Float32Array,
273
+ outB: Float32Array,
274
+ ): void {
275
+ const d1x = q1x - p1x,
276
+ d1y = q1y - p1y,
277
+ d1z = q1z - p1z
278
+ const d2x = q2x - p2x,
279
+ d2y = q2y - p2y,
280
+ d2z = q2z - p2z
281
+ const rx = p1x - p2x,
282
+ ry = p1y - p2y,
283
+ rz = p1z - p2z
284
+ const a = d1x * d1x + d1y * d1y + d1z * d1z
285
+ const e = d2x * d2x + d2y * d2y + d2z * d2z
286
+ const f = d2x * rx + d2y * ry + d2z * rz
287
+ let s = 0,
288
+ t = 0
289
+ const EPS = 1e-8
290
+ if (a <= EPS && e <= EPS) {
291
+ outA[0] = p1x
292
+ outA[1] = p1y
293
+ outA[2] = p1z
294
+ outB[0] = p2x
295
+ outB[1] = p2y
296
+ outB[2] = p2z
297
+ return
298
+ }
299
+ if (a <= EPS) {
300
+ s = 0
301
+ t = clamp01(f / e)
302
+ } else {
303
+ const c = d1x * rx + d1y * ry + d1z * rz
304
+ if (e <= EPS) {
305
+ t = 0
306
+ s = clamp01(-c / a)
307
+ } else {
308
+ const b = d1x * d2x + d1y * d2y + d1z * d2z
309
+ const denom = a * e - b * b
310
+ if (denom !== 0) s = clamp01((b * f - c * e) / denom)
311
+ t = (b * s + f) / e
312
+ if (t < 0) {
313
+ t = 0
314
+ s = clamp01(-c / a)
315
+ } else if (t > 1) {
316
+ t = 1
317
+ s = clamp01((b - c) / a)
318
+ }
319
+ }
320
+ }
321
+ outA[0] = p1x + d1x * s
322
+ outA[1] = p1y + d1y * s
323
+ outA[2] = p1z + d1z * s
324
+ outB[0] = p2x + d2x * t
325
+ outB[1] = p2y + d2y * t
326
+ outB[2] = p2z + d2z * t
327
+ }
328
+
329
+ function clamp01(x: number): number {
330
+ return x < 0 ? 0 : x > 1 ? 1 : x
331
+ }
332
+
333
+ // Closest point on segment p1→q1 to a free point (sx,sy,sz). Out gets the
334
+ // projected point clamped to the segment.
335
+ function closestPointOnSegment(
336
+ p1x: number,
337
+ p1y: number,
338
+ p1z: number,
339
+ q1x: number,
340
+ q1y: number,
341
+ q1z: number,
342
+ sx: number,
343
+ sy: number,
344
+ sz: number,
345
+ out: Float32Array,
346
+ ): void {
347
+ const dx = q1x - p1x,
348
+ dy = q1y - p1y,
349
+ dz = q1z - p1z
350
+ const segLen2 = dx * dx + dy * dy + dz * dz
351
+ let t = 0
352
+ if (segLen2 > 1e-8) {
353
+ t = ((sx - p1x) * dx + (sy - p1y) * dy + (sz - p1z) * dz) / segLen2
354
+ if (t < 0) t = 0
355
+ else if (t > 1) t = 1
356
+ }
357
+ out[0] = p1x + dx * t
358
+ out[1] = p1y + dy * t
359
+ out[2] = p1z + dz * t
360
+ }
361
+
362
+ // Emit one capsule-vs-capsule contact given a pair of points (pA on A's
363
+ // segment, pB on B's segment). Skips silently if outside speculative range.
364
+ function emitCapsuleContact(
365
+ store: RigidBodyStore,
366
+ a: number,
367
+ b: number,
368
+ pool: ContactPool,
369
+ pAx: number,
370
+ pAy: number,
371
+ pAz: number,
372
+ pBx: number,
373
+ pBy: number,
374
+ pBz: number,
375
+ rA: number,
376
+ rB: number,
377
+ sumR: number,
378
+ sumExt: number,
379
+ cAx: number,
380
+ cAy: number,
381
+ cAz: number,
382
+ cBx: number,
383
+ cBy: number,
384
+ cBz: number,
385
+ ): void {
386
+ const dx = pBx - pAx,
387
+ dy = pBy - pAy,
388
+ dz = pBz - pAz
389
+ const d2 = dx * dx + dy * dy + dz * dz
390
+ if (d2 > sumExt * sumExt) return
391
+ const d = Math.sqrt(d2)
392
+ let nx: number, ny: number, nz: number
393
+ if (d > 1e-6) {
394
+ nx = dx / d
395
+ ny = dy / d
396
+ nz = dz / d
397
+ } else {
398
+ nx = 0
399
+ ny = 1
400
+ nz = 0
401
+ }
402
+ const c = pool.acquire()
403
+ c.bodyA = a
404
+ c.bodyB = b
405
+ c.nx = nx
406
+ c.ny = ny
407
+ c.nz = nz
408
+ c.depth = sumR - d
409
+ c.rAx = pAx + nx * rA - cAx
410
+ c.rAy = pAy + ny * rA - cAy
411
+ c.rAz = pAz + nz * rA - cAz
412
+ c.rBx = pBx - nx * rB - cBx
413
+ c.rBy = pBy - ny * rB - cBy
414
+ c.rBz = pBz - nz * rB - cBz
415
+ combineMaterials(store, a, b, c)
416
+ }
417
+
418
+ function detectCapsuleCapsule(store: RigidBodyStore, a: number, b: number, pool: ContactPool): void {
419
+ const pos = store.positions,
420
+ sz = store.size
421
+ const ai = a * 3,
422
+ bi = b * 3
423
+ const cAx = pos[ai + 0],
424
+ cAy = pos[ai + 1],
425
+ cAz = pos[ai + 2]
426
+ const cBx = pos[bi + 0],
427
+ cBy = pos[bi + 1],
428
+ cBz = pos[bi + 2]
429
+ const rA = sz[ai + 0],
430
+ hA = sz[ai + 1] * 0.5
431
+ const rB = sz[bi + 0],
432
+ hB = sz[bi + 1] * 0.5
433
+ const aAx = _capPoint
434
+ const aBx = _capPointB
435
+ capsuleAxis(store, a, aAx)
436
+ capsuleAxis(store, b, aBx)
437
+ const p1x = cAx - aAx[0] * hA,
438
+ p1y = cAy - aAx[1] * hA,
439
+ p1z = cAz - aAx[2] * hA
440
+ const q1x = cAx + aAx[0] * hA,
441
+ q1y = cAy + aAx[1] * hA,
442
+ q1z = cAz + aAx[2] * hA
443
+ const p2x = cBx - aBx[0] * hB,
444
+ p2y = cBy - aBx[1] * hB,
445
+ p2z = cBz - aBx[2] * hB
446
+ const q2x = cBx + aBx[0] * hB,
447
+ q2y = cBy + aBx[1] * hB,
448
+ q2z = cBz + aBx[2] * hB
449
+
450
+ const sumR = rA + rB
451
+ const sumExt = sumR + CONTACT_MARGIN
452
+
453
+ // Primary contact: closest-pair on the two segments.
454
+ closestPointsTwoSegments(p1x, p1y, p1z, q1x, q1y, q1z, p2x, p2y, p2z, q2x, q2y, q2z, _cpA, _cpB)
455
+ emitCapsuleContact(
456
+ store,
457
+ a,
458
+ b,
459
+ pool,
460
+ _cpA[0],
461
+ _cpA[1],
462
+ _cpA[2],
463
+ _cpB[0],
464
+ _cpB[1],
465
+ _cpB[2],
466
+ rA,
467
+ rB,
468
+ sumR,
469
+ sumExt,
470
+ cAx,
471
+ cAy,
472
+ cAz,
473
+ cBx,
474
+ cBy,
475
+ cBz,
476
+ )
477
+
478
+ // For nearly-parallel axes the closest-pair algorithm is degenerate
479
+ // (denom = a·e − b² ≈ 0) and returns one arbitrary point. Sampling A's
480
+ // endpoints adds two contacts that pin both rotation and length-wise push.
481
+ const cosA = Math.abs(aAx[0] * aBx[0] + aAx[1] * aBx[1] + aAx[2] * aBx[2])
482
+ if (cosA > 0.9) {
483
+ closestPointOnSegment(p2x, p2y, p2z, q2x, q2y, q2z, p1x, p1y, p1z, _cpB)
484
+ emitCapsuleContact(
485
+ store,
486
+ a,
487
+ b,
488
+ pool,
489
+ p1x,
490
+ p1y,
491
+ p1z,
492
+ _cpB[0],
493
+ _cpB[1],
494
+ _cpB[2],
495
+ rA,
496
+ rB,
497
+ sumR,
498
+ sumExt,
499
+ cAx,
500
+ cAy,
501
+ cAz,
502
+ cBx,
503
+ cBy,
504
+ cBz,
505
+ )
506
+ closestPointOnSegment(p2x, p2y, p2z, q2x, q2y, q2z, q1x, q1y, q1z, _cpB)
507
+ emitCapsuleContact(
508
+ store,
509
+ a,
510
+ b,
511
+ pool,
512
+ q1x,
513
+ q1y,
514
+ q1z,
515
+ _cpB[0],
516
+ _cpB[1],
517
+ _cpB[2],
518
+ rA,
519
+ rB,
520
+ sumR,
521
+ sumExt,
522
+ cAx,
523
+ cAy,
524
+ cAz,
525
+ cBx,
526
+ cBy,
527
+ cBz,
528
+ )
529
+ }
530
+ }
531
+
532
+ // --- Sphere–box (sphere = a, box = b) --------------------------------------
533
+ const _localPt = new Float32Array(3)
534
+
535
+ // 3×3 row-major rotation matrix for body i (xx = 2·qx·qx etc.).
536
+ const _rot = new Float32Array(9)
537
+ function loadBodyRot(store: RigidBodyStore, i: number): void {
538
+ const i4 = i * 4
539
+ const qx = store.orientations[i4 + 0]
540
+ const qy = store.orientations[i4 + 1]
541
+ const qz = store.orientations[i4 + 2]
542
+ const qw = store.orientations[i4 + 3]
543
+ const x2 = qx + qx,
544
+ y2 = qy + qy,
545
+ z2 = qz + qz
546
+ const xx = qx * x2,
547
+ yy = qy * y2,
548
+ zz = qz * z2
549
+ const xy = qx * y2,
550
+ xz = qx * z2,
551
+ yz = qy * z2
552
+ const wx = qw * x2,
553
+ wy = qw * y2,
554
+ wz = qw * z2
555
+ _rot[0] = 1 - (yy + zz)
556
+ _rot[1] = xy - wz
557
+ _rot[2] = xz + wy
558
+ _rot[3] = xy + wz
559
+ _rot[4] = 1 - (xx + zz)
560
+ _rot[5] = yz - wx
561
+ _rot[6] = xz - wy
562
+ _rot[7] = yz + wx
563
+ _rot[8] = 1 - (xx + yy)
564
+ }
565
+
566
+ // Transform world point into body i's local frame: v_local = R^T · (p − bodyPos).
567
+ function worldToBodyLocal(
568
+ store: RigidBodyStore,
569
+ i: number,
570
+ px: number,
571
+ py: number,
572
+ pz: number,
573
+ out: Float32Array,
574
+ ): void {
575
+ const i3 = i * 3
576
+ const dx = px - store.positions[i3 + 0]
577
+ const dy = py - store.positions[i3 + 1]
578
+ const dz = pz - store.positions[i3 + 2]
579
+ loadBodyRot(store, i)
580
+ // R^T · v = (col k of R) · v.
581
+ out[0] = _rot[0] * dx + _rot[3] * dy + _rot[6] * dz
582
+ out[1] = _rot[1] * dx + _rot[4] * dy + _rot[7] * dz
583
+ out[2] = _rot[2] * dx + _rot[5] * dy + _rot[8] * dz
584
+ }
585
+
586
+ // Rotate a body-local direction into world space: v_world = R · v_local.
587
+ function bodyLocalToWorldDir(
588
+ store: RigidBodyStore,
589
+ i: number,
590
+ lx: number,
591
+ ly: number,
592
+ lz: number,
593
+ out: Float32Array,
594
+ ): void {
595
+ loadBodyRot(store, i)
596
+ out[0] = _rot[0] * lx + _rot[1] * ly + _rot[2] * lz
597
+ out[1] = _rot[3] * lx + _rot[4] * ly + _rot[5] * lz
598
+ out[2] = _rot[6] * lx + _rot[7] * ly + _rot[8] * lz
599
+ }
600
+
601
+ function detectSphereBox(store: RigidBodyStore, a: number, b: number, pool: ContactPool): void {
602
+ const ai = a * 3,
603
+ bi = b * 3
604
+ const sx = store.positions[ai + 0]
605
+ const sy = store.positions[ai + 1]
606
+ const sz_ = store.positions[ai + 2]
607
+ const rA = store.size[ai + 0]
608
+ const hx = store.size[bi + 0]
609
+ const hy = store.size[bi + 1]
610
+ const hz = store.size[bi + 2]
611
+
612
+ // Sphere center in box-local frame.
613
+ worldToBodyLocal(store, b, sx, sy, sz_, _localPt)
614
+ const lx = _localPt[0],
615
+ ly = _localPt[1],
616
+ lz = _localPt[2]
617
+
618
+ // Closest point on box (clamp to half-extents).
619
+ let qx = lx,
620
+ qy = ly,
621
+ qz = lz
622
+ if (qx > hx) qx = hx
623
+ else if (qx < -hx) qx = -hx
624
+ if (qy > hy) qy = hy
625
+ else if (qy < -hy) qy = -hy
626
+ if (qz > hz) qz = hz
627
+ else if (qz < -hz) qz = -hz
628
+
629
+ let dx = lx - qx,
630
+ dy = ly - qy,
631
+ dz = lz - qz
632
+ let d2 = dx * dx + dy * dy + dz * dz
633
+
634
+ let nLocalX: number, nLocalY: number, nLocalZ: number
635
+ let depth: number
636
+
637
+ const rExt = rA + CONTACT_MARGIN
638
+ if (d2 > rExt * rExt) return // outside speculative range
639
+
640
+ if (d2 > 1e-12) {
641
+ const d = Math.sqrt(d2)
642
+ nLocalX = dx / d
643
+ nLocalY = dy / d
644
+ nLocalZ = dz / d
645
+ depth = rA - d // signed: > 0 overlapping, ≤ 0 within margin
646
+ } else {
647
+ // Sphere center inside box — pick shortest axis to escape.
648
+ const px = hx - Math.abs(lx),
649
+ py = hy - Math.abs(ly),
650
+ pz = hz - Math.abs(lz)
651
+ if (px < py && px < pz) {
652
+ nLocalX = lx > 0 ? 1 : -1
653
+ nLocalY = 0
654
+ nLocalZ = 0
655
+ depth = rA + px
656
+ qx = lx > 0 ? hx : -hx
657
+ qy = ly
658
+ qz = lz
659
+ } else if (py < pz) {
660
+ nLocalX = 0
661
+ nLocalY = ly > 0 ? 1 : -1
662
+ nLocalZ = 0
663
+ depth = rA + py
664
+ qx = lx
665
+ qy = ly > 0 ? hy : -hy
666
+ qz = lz
667
+ } else {
668
+ nLocalX = 0
669
+ nLocalY = 0
670
+ nLocalZ = lz > 0 ? 1 : -1
671
+ depth = rA + pz
672
+ qx = lx
673
+ qy = ly
674
+ qz = lz > 0 ? hz : -hz
675
+ }
676
+ }
677
+
678
+ // Rotate local normal back to world. Convention: normal points A→B, but we
679
+ // computed n = (lx − qx) which goes "from box surface toward sphere center"
680
+ // (= B → A). Flip sign.
681
+ const out = _capPoint
682
+ bodyLocalToWorldDir(store, b, -nLocalX, -nLocalY, -nLocalZ, out)
683
+ const nx = out[0],
684
+ ny = out[1],
685
+ nz = out[2]
686
+
687
+ // Box's contact point in world: rotate (qx,qy,qz) and translate by box pos.
688
+ const bp = _capPointB
689
+ bodyLocalToWorldDir(store, b, qx, qy, qz, bp)
690
+ const bpx = bp[0] + store.positions[bi + 0]
691
+ const bpy = bp[1] + store.positions[bi + 1]
692
+ const bpz = bp[2] + store.positions[bi + 2]
693
+
694
+ const c = pool.acquire()
695
+ c.bodyA = a
696
+ c.bodyB = b
697
+ c.nx = nx
698
+ c.ny = ny
699
+ c.nz = nz
700
+ c.depth = depth
701
+ c.rAx = nx * rA
702
+ c.rAy = ny * rA
703
+ c.rAz = nz * rA
704
+ c.rBx = bpx - store.positions[bi + 0]
705
+ c.rBy = bpy - store.positions[bi + 1]
706
+ c.rBz = bpz - store.positions[bi + 2]
707
+ combineMaterials(store, a, b, c)
708
+ }
709
+
710
+ // --- Capsule–box -----------------------------------------------------------
711
+ // Walk the capsule's segment (in box-local space) toward the box, sample
712
+ // sphere-box at the converged parameter plus both endpoints. Endpoint
713
+ // samples catch caps grazing a face when the closest-point parameter sits
714
+ // at one end of the segment.
715
+ function detectCapsuleBox(store: RigidBodyStore, a: number, b: number, pool: ContactPool): void {
716
+ const pos = store.positions,
717
+ sz = store.size
718
+ const ai = a * 3,
719
+ bi = b * 3
720
+ const cx = pos[ai + 0],
721
+ cy = pos[ai + 1],
722
+ cz = pos[ai + 2]
723
+ const rA = sz[ai + 0]
724
+ const hA = sz[ai + 1] * 0.5
725
+ const ax = _capPoint
726
+ capsuleAxis(store, a, ax)
727
+
728
+ // Endpoints in world space.
729
+ const p1wx = cx - ax[0] * hA,
730
+ p1wy = cy - ax[1] * hA,
731
+ p1wz = cz - ax[2] * hA
732
+ const p2wx = cx + ax[0] * hA,
733
+ p2wy = cy + ax[1] * hA,
734
+ p2wz = cz + ax[2] * hA
735
+
736
+ // Endpoints in box-local space.
737
+ worldToBodyLocal(store, b, p1wx, p1wy, p1wz, _localPt)
738
+ const p1lx = _localPt[0],
739
+ p1ly = _localPt[1],
740
+ p1lz = _localPt[2]
741
+ worldToBodyLocal(store, b, p2wx, p2wy, p2wz, _localPt)
742
+ const p2lx = _localPt[0],
743
+ p2ly = _localPt[1],
744
+ p2lz = _localPt[2]
745
+
746
+ const hx = sz[bi + 0],
747
+ hy = sz[bi + 1],
748
+ hz = sz[bi + 2]
749
+
750
+ // Closest point on segment to box (in box-local). Iterate a few times to
751
+ // converge — clamp each component, recompute t, repeat. Two passes is
752
+ // enough for our use case (capsule modestly larger than box).
753
+ let t = 0.5
754
+ for (let iter = 0; iter < 4; iter++) {
755
+ const px = p1lx + (p2lx - p1lx) * t
756
+ const py = p1ly + (p2ly - p1ly) * t
757
+ const pz = p1lz + (p2lz - p1lz) * t
758
+ let qx = px,
759
+ qy = py,
760
+ qz = pz
761
+ if (qx > hx) qx = hx
762
+ else if (qx < -hx) qx = -hx
763
+ if (qy > hy) qy = hy
764
+ else if (qy < -hy) qy = -hy
765
+ if (qz > hz) qz = hz
766
+ else if (qz < -hz) qz = -hz
767
+ // Project clamped point back onto the segment to refine t.
768
+ const dx = p2lx - p1lx,
769
+ dy = p2ly - p1ly,
770
+ dz = p2lz - p1lz
771
+ const segLen2 = dx * dx + dy * dy + dz * dz
772
+ if (segLen2 < 1e-8) break
773
+ t = ((qx - p1lx) * dx + (qy - p1ly) * dy + (qz - p1lz) * dz) / segLen2
774
+ if (t < 0) {
775
+ t = 0
776
+ break
777
+ }
778
+ if (t > 1) {
779
+ t = 1
780
+ break
781
+ }
782
+ }
783
+
784
+ // Sample at the converged t plus both endpoints — endpoints catch capsule
785
+ // caps grazing the box surface where the closest-point loop sits at one
786
+ // segment end.
787
+ let bestDepth = -Infinity
788
+ let bestNX = 0,
789
+ bestNY = 0,
790
+ bestNZ = 0
791
+ let bestRAX = 0,
792
+ bestRAY = 0,
793
+ bestRAZ = 0
794
+ let bestRBX = 0,
795
+ bestRBY = 0,
796
+ bestRBZ = 0
797
+ let found = false
798
+
799
+ const samples = [t, 0, 1]
800
+ for (const s of samples) {
801
+ const sx = p1wx + (p2wx - p1wx) * s
802
+ const sy = p1wy + (p2wy - p1wy) * s
803
+ const sz_ = p1wz + (p2wz - p1wz) * s
804
+ worldToBodyLocal(store, b, sx, sy, sz_, _localPt)
805
+ const lx = _localPt[0],
806
+ ly = _localPt[1],
807
+ lz = _localPt[2]
808
+ let qx = lx,
809
+ qy = ly,
810
+ qz = lz
811
+ if (qx > hx) qx = hx
812
+ else if (qx < -hx) qx = -hx
813
+ if (qy > hy) qy = hy
814
+ else if (qy < -hy) qy = -hy
815
+ if (qz > hz) qz = hz
816
+ else if (qz < -hz) qz = -hz
817
+ const dx = lx - qx,
818
+ dy = ly - qy,
819
+ dz = lz - qz
820
+ const d2 = dx * dx + dy * dy + dz * dz
821
+ const rExt = rA + CONTACT_MARGIN
822
+ if (d2 > rExt * rExt) continue
823
+ let nLocalX = 0,
824
+ nLocalY = 0,
825
+ nLocalZ = 0
826
+ let depth: number
827
+ if (d2 > 1e-12) {
828
+ const d = Math.sqrt(d2)
829
+ nLocalX = dx / d
830
+ nLocalY = dy / d
831
+ nLocalZ = dz / d
832
+ depth = rA - d // signed: > 0 overlapping, ≤ 0 within margin
833
+ } else {
834
+ const px = hx - Math.abs(lx),
835
+ py = hy - Math.abs(ly),
836
+ pz = hz - Math.abs(lz)
837
+ if (px < py && px < pz) {
838
+ nLocalX = lx > 0 ? 1 : -1
839
+ depth = rA + px
840
+ qx = lx > 0 ? hx : -hx
841
+ qy = ly
842
+ qz = lz
843
+ } else if (py < pz) {
844
+ nLocalY = ly > 0 ? 1 : -1
845
+ depth = rA + py
846
+ qx = lx
847
+ qy = ly > 0 ? hy : -hy
848
+ qz = lz
849
+ } else {
850
+ nLocalZ = lz > 0 ? 1 : -1
851
+ depth = rA + pz
852
+ qx = lx
853
+ qy = ly
854
+ qz = lz > 0 ? hz : -hz
855
+ }
856
+ }
857
+ if (depth <= bestDepth) continue
858
+ bestDepth = depth
859
+ found = true
860
+ const dirOut = _localPt
861
+ bodyLocalToWorldDir(store, b, -nLocalX, -nLocalY, -nLocalZ, dirOut)
862
+ bestNX = dirOut[0]
863
+ bestNY = dirOut[1]
864
+ bestNZ = dirOut[2]
865
+ const bpOut = _localPt
866
+ bodyLocalToWorldDir(store, b, qx, qy, qz, bpOut)
867
+ const bpx = bpOut[0] + pos[bi + 0]
868
+ const bpy = bpOut[1] + pos[bi + 1]
869
+ const bpz = bpOut[2] + pos[bi + 2]
870
+ bestRAX = sx + bestNX * rA - cx
871
+ bestRAY = sy + bestNY * rA - cy
872
+ bestRAZ = sz_ + bestNZ * rA - cz
873
+ bestRBX = bpx - pos[bi + 0]
874
+ bestRBY = bpy - pos[bi + 1]
875
+ bestRBZ = bpz - pos[bi + 2]
876
+ }
877
+
878
+ if (!found) return
879
+ const c = pool.acquire()
880
+ c.bodyA = a
881
+ c.bodyB = b
882
+ c.nx = bestNX
883
+ c.ny = bestNY
884
+ c.nz = bestNZ
885
+ c.depth = bestDepth
886
+ c.rAx = bestRAX
887
+ c.rAy = bestRAY
888
+ c.rAz = bestRAZ
889
+ c.rBx = bestRBX
890
+ c.rBy = bestRBY
891
+ c.rBz = bestRBZ
892
+ combineMaterials(store, a, b, c)
893
+ }
894
+
895
+ // Dispatch a pair to the matching narrowphase. Caller has already done
896
+ // broadphase + group/mask filtering. Some shape pairs (sphere-A capsule-B
897
+ // etc.) reuse a canonical implementation via swap + flipLastNormal.
898
+ export function generateContacts(store: RigidBodyStore, a: number, b: number, pool: ContactPool): void {
899
+ const sA = store.shape[a]
900
+ const sB = store.shape[b]
901
+ if (sA === RigidbodyShape.Sphere && sB === RigidbodyShape.Sphere) {
902
+ detectSphereSphere(store, a, b, pool)
903
+ return
904
+ }
905
+ if (sA === RigidbodyShape.Sphere && sB === RigidbodyShape.Capsule) {
906
+ detectSphereCapsule(store, a, b, pool)
907
+ return
908
+ }
909
+ if (sA === RigidbodyShape.Capsule && sB === RigidbodyShape.Sphere) {
910
+ detectSphereCapsule(store, b, a, pool)
911
+ flipLastNormal(pool)
912
+ return
913
+ }
914
+ if (sA === RigidbodyShape.Capsule && sB === RigidbodyShape.Capsule) {
915
+ detectCapsuleCapsule(store, a, b, pool)
916
+ return
917
+ }
918
+ if (sA === RigidbodyShape.Sphere && sB === RigidbodyShape.Box) {
919
+ detectSphereBox(store, a, b, pool)
920
+ return
921
+ }
922
+ if (sA === RigidbodyShape.Box && sB === RigidbodyShape.Sphere) {
923
+ detectSphereBox(store, b, a, pool)
924
+ flipLastNormal(pool)
925
+ return
926
+ }
927
+ if (sA === RigidbodyShape.Capsule && sB === RigidbodyShape.Box) {
928
+ detectCapsuleBox(store, a, b, pool)
929
+ return
930
+ }
931
+ if (sA === RigidbodyShape.Box && sB === RigidbodyShape.Capsule) {
932
+ detectCapsuleBox(store, b, a, pool)
933
+ flipLastNormal(pool)
934
+ return
935
+ }
936
+ // Box-box left unimplemented.
937
+ }
938
+
939
+ // After a swapped detect* call, the last contact's normal points the wrong
940
+ // way and lever arms are mismatched. Flip and re-anchor.
941
+ function flipLastNormal(pool: ContactPool): void {
942
+ if (pool.count === 0) return
943
+ const c = pool.get(pool.count - 1)
944
+ const ta = c.bodyA
945
+ c.bodyA = c.bodyB
946
+ c.bodyB = ta
947
+ const trAx = c.rAx,
948
+ trAy = c.rAy,
949
+ trAz = c.rAz
950
+ c.rAx = c.rBx
951
+ c.rAy = c.rBy
952
+ c.rAz = c.rBz
953
+ c.rBx = trAx
954
+ c.rBy = trAy
955
+ c.rBz = trAz
956
+ c.nx = -c.nx
957
+ c.ny = -c.ny
958
+ c.nz = -c.nz
959
+ }
960
+
961
+ // O(N²) AABB pair test. For 30–100 PMX bodies that's <5000 checks per
962
+ // step; SAP / dynamic AABB tree pay off above ~500 bodies. The pair filter
963
+ // uses the AND form `(maskA & groupB) && (maskB & groupA)` — both sides
964
+ // must agree, which is what PMX rigs are tuned against.
965
+ export function findContacts(store: RigidBodyStore, pool: ContactPool): void {
966
+ store.updateAabbs()
967
+ const N = store.count
968
+ const invMass = store.invMass
969
+ const group = store.collisionGroup
970
+ const mask = store.willCollideMask
971
+
972
+ for (let i = 0; i < N; i++) {
973
+ const gi = group[i]
974
+ const mi = mask[i]
975
+ const dynA = invMass[i] > 0
976
+ for (let j = i + 1; j < N; j++) {
977
+ if (!dynA && invMass[j] === 0) continue // both static — no resolution
978
+ if ((mi & group[j]) === 0 || (mask[j] & gi) === 0) continue
979
+ if (!aabbOverlap(store, i, j)) continue
980
+ generateContacts(store, i, j, pool)
981
+ }
982
+ }
983
+ }