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