@woosh/meep-engine 2.143.0 → 2.144.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/package.json +1 -1
- package/src/core/geom/3d/shape/PointShape3D.d.ts +1 -0
- package/src/core/geom/3d/shape/PointShape3D.d.ts.map +1 -1
- package/src/core/geom/3d/shape/PointShape3D.js +11 -0
- package/src/core/geom/3d/shape/SphereShape3D.d.ts +1 -0
- package/src/core/geom/3d/shape/SphereShape3D.d.ts.map +1 -1
- package/src/core/geom/3d/shape/SphereShape3D.js +4 -0
- package/src/engine/control/first-person/DESIGN_COLLISION.md +264 -217
- package/src/engine/control/first-person/FirstPersonPlayerControllerSystem.d.ts +91 -58
- package/src/engine/control/first-person/FirstPersonPlayerControllerSystem.d.ts.map +1 -1
- package/src/engine/control/first-person/FirstPersonPlayerControllerSystem.js +1814 -1789
- package/src/engine/control/first-person/TODO.md +17 -32
- package/src/engine/control/first-person/collision/KinematicMover.d.ts +176 -0
- package/src/engine/control/first-person/collision/KinematicMover.d.ts.map +1 -0
- package/src/engine/control/first-person/collision/KinematicMover.js +424 -0
- package/src/engine/control/first-person/prototype_first_person_controller.js +65 -0
- package/src/engine/graphics/render/buffer/simple-fx/ao/SAOShader.js +18 -9
- package/src/engine/physics/PLAN.md +94 -32
- package/src/engine/physics/contact/ManifoldStore.d.ts +28 -2
- package/src/engine/physics/contact/ManifoldStore.d.ts.map +1 -1
- package/src/engine/physics/contact/ManifoldStore.js +37 -3
- package/src/engine/physics/contact/combine_material.d.ts +30 -0
- package/src/engine/physics/contact/combine_material.d.ts.map +1 -0
- package/src/engine/physics/contact/combine_material.js +35 -0
- package/src/engine/physics/ecs/Collider.d.ts +15 -0
- package/src/engine/physics/ecs/Collider.d.ts.map +1 -1
- package/src/engine/physics/ecs/Collider.js +34 -0
- package/src/engine/physics/ecs/Joint.d.ts +18 -0
- package/src/engine/physics/ecs/Joint.d.ts.map +1 -1
- package/src/engine/physics/ecs/Joint.js +70 -0
- package/src/engine/physics/ecs/PhysicsSystem.d.ts +9 -4
- package/src/engine/physics/ecs/PhysicsSystem.d.ts.map +1 -1
- package/src/engine/physics/ecs/PhysicsSystem.js +9 -4
- package/src/engine/physics/ecs/RigidBody.d.ts +15 -0
- package/src/engine/physics/ecs/RigidBody.d.ts.map +1 -1
- package/src/engine/physics/ecs/RigidBody.js +46 -0
- package/src/engine/physics/narrowphase/compute_penetration.d.ts +41 -41
- package/src/engine/physics/narrowphase/compute_penetration.d.ts.map +1 -1
- package/src/engine/physics/narrowphase/compute_penetration.js +96 -169
- package/src/engine/physics/narrowphase/narrowphase_step.d.ts +52 -0
- package/src/engine/physics/narrowphase/narrowphase_step.d.ts.map +1 -1
- package/src/engine/physics/narrowphase/narrowphase_step.js +130 -3
- package/src/engine/physics/solver/solve_contacts.d.ts.map +1 -1
- package/src/engine/physics/solver/solve_contacts.js +10 -21
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { aabb3_transform_oriented } from "../../../core/geom/3d/aabb/aabb3_transform_oriented.js";
|
|
2
2
|
import { v3_quat3_apply } from "../../../core/geom/vec3/v3_quat3_apply.js";
|
|
3
|
-
import { mpr } from "../gjk/mpr.js";
|
|
4
3
|
import { aabb_world_to_local } from "./decomposition/aabb_world_to_local.js";
|
|
5
4
|
import { decompose_to_triangles } from "./decomposition/decompose_to_triangles.js";
|
|
6
5
|
import { TRIANGLE_FLOAT_STRIDE } from "./decomposition/triangle_buffer_layout.js";
|
|
6
|
+
import { deepest_pair_penetration } from "./narrowphase_step.js";
|
|
7
7
|
import { PosedShape } from "./PosedShape.js";
|
|
8
8
|
|
|
9
9
|
/**
|
|
@@ -11,54 +11,45 @@ import { PosedShape } from "./PosedShape.js";
|
|
|
11
11
|
* the other narrowphase utilities. Safe because PhysicsSystem queries
|
|
12
12
|
* (and gameplay code calling this) run on the main thread.
|
|
13
13
|
*/
|
|
14
|
-
const posed_a = new PosedShape();
|
|
15
14
|
const posed_b = new PosedShape();
|
|
16
15
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
16
|
+
/**
|
|
17
|
+
* Scratch normal written by the primary {@link deepest_pair_penetration} query.
|
|
18
|
+
* Copied to the caller's `out_direction` only when the depth clears
|
|
19
|
+
* {@link CONTACT_EPSILON}, preserving the "untouched on no overlap" contract.
|
|
20
|
+
* @type {Float64Array}
|
|
21
|
+
*/
|
|
22
|
+
const primary_normal = new Float64Array(3);
|
|
22
23
|
|
|
23
24
|
/**
|
|
24
|
-
* Scratch buffers for the convex-vs-concave path (see
|
|
25
|
-
* {@link
|
|
25
|
+
* Scratch buffers for the convex-vs-concave recovery path (see
|
|
26
|
+
* {@link concave_recovery_penetration}).
|
|
26
27
|
*/
|
|
27
28
|
const local_aabb = new Float64Array(6);
|
|
28
29
|
const world_aabb = new Float64Array(6);
|
|
29
30
|
const concave_query_aabb = new Float64Array(6);
|
|
30
31
|
const scratch_v3 = new Float64Array(3);
|
|
31
|
-
// Dedicated scratch for the per-triangle q · v · q* rotations below
|
|
32
|
-
// (face normal + centroid). Distinct from `scratch_v3` (used by the
|
|
33
|
-
// support-function call later in the same iteration) to keep the
|
|
34
|
-
// data flow obvious.
|
|
35
32
|
const scratch_rot = new Float64Array(3);
|
|
36
33
|
|
|
37
34
|
/**
|
|
38
|
-
* Per-pair triangle decomposition cap. Same rationale as
|
|
39
|
-
* `narrowphase_step.MAX_TRIANGLES_PER_PAIR
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
* yields tens of triangles. Excess triangles are dropped by the
|
|
43
|
-
* enumerator's bounds check.
|
|
35
|
+
* Per-pair triangle decomposition cap for the recovery path. Same rationale as
|
|
36
|
+
* `narrowphase_step.MAX_TRIANGLES_PER_PAIR`: the query AABB is bounded by the
|
|
37
|
+
* convex shape's envelope, so a single pair yields tens of triangles. Excess
|
|
38
|
+
* is dropped by the enumerator's bounds check.
|
|
44
39
|
* @type {number}
|
|
45
40
|
*/
|
|
46
41
|
const MAX_TRIANGLES_PER_PAIR = 1024;
|
|
47
42
|
const triangle_buffer = new Float64Array(MAX_TRIANGLES_PER_PAIR * TRIANGLE_FLOAT_STRIDE);
|
|
48
43
|
|
|
49
44
|
/**
|
|
50
|
-
* Penetration depths below this are treated as no contact.
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
* "0 means no overlap" contract is more useful when small-noise hits
|
|
56
|
-
* are filtered out at the source.
|
|
45
|
+
* Penetration depths below this are treated as no contact. The narrowphase
|
|
46
|
+
* dispatch can report sub-micron "overlap" at exact tangent (GJK/EPA noise,
|
|
47
|
+
* or a closed-form solver returning a hair of depth on a kissing contact); the
|
|
48
|
+
* "0 means no overlap" contract is more useful when that noise is filtered out
|
|
49
|
+
* at the source.
|
|
57
50
|
*
|
|
58
|
-
* 1e-4 m (100 µm)
|
|
59
|
-
*
|
|
60
|
-
* residuals at exact tangent. Smaller values would let near-tangent
|
|
61
|
-
* floating-point noise leak through as "tiny positive depth".
|
|
51
|
+
* 1e-4 m (100 µm) is well below any practical world-scale tolerance while
|
|
52
|
+
* still larger than typical convergence residuals at exact tangent.
|
|
62
53
|
*
|
|
63
54
|
* @type {number}
|
|
64
55
|
*/
|
|
@@ -76,56 +67,56 @@ const CONTACT_EPSILON = 1e-4;
|
|
|
76
67
|
* `-out_direction * return_value` to `position_b`) is the minimum
|
|
77
68
|
* translation that produces separation.
|
|
78
69
|
*
|
|
79
|
-
* If the shapes do not overlap
|
|
80
|
-
*
|
|
81
|
-
*
|
|
70
|
+
* If the shapes do not overlap, the return value is `0` and
|
|
71
|
+
* `out_direction` is left untouched. Callers should treat 0 as "no
|
|
72
|
+
* penetration".
|
|
82
73
|
*
|
|
83
74
|
* Sign convention matches the narrowphase's stored contact normal:
|
|
84
|
-
*
|
|
85
|
-
*
|
|
86
|
-
* - The outward normal of B's surface at the contact, pointing
|
|
87
|
-
* toward A
|
|
75
|
+
* `out_direction` ≡ the "B → A" direction (B's outward surface normal at the
|
|
76
|
+
* contact, pointing toward A).
|
|
88
77
|
*
|
|
89
|
-
*
|
|
90
|
-
* precision characteristics (essentially exact for sign-based supports
|
|
91
|
-
* like cubes; asymptotic on curved supports like spheres near tangent,
|
|
92
|
-
* where the polytope iteration cap leaves a small angular residual on
|
|
93
|
-
* the direction).
|
|
78
|
+
* ## How it is computed (hardened)
|
|
94
79
|
*
|
|
95
|
-
*
|
|
80
|
+
* The query routes through the **same narrowphase contact dispatch the solver
|
|
81
|
+
* consumes** ({@link deepest_pair_penetration} → `dispatch_pair`) and reports
|
|
82
|
+
* the deepest contact. That makes it correct — not "correct sometimes" — for
|
|
83
|
+
* every shape pair the engine can build:
|
|
96
84
|
*
|
|
97
|
-
*
|
|
98
|
-
*
|
|
99
|
-
*
|
|
100
|
-
*
|
|
101
|
-
*
|
|
102
|
-
*
|
|
103
|
-
*
|
|
85
|
+
* - **sphere / box / capsule pairs** → exact closed-form solvers
|
|
86
|
+
* (`sphere_sphere`, `sphere_box`, `box_box` via SAT, `capsule_*`). Box-box
|
|
87
|
+
* in particular uses the true minimum-translation axis, so a small body
|
|
88
|
+
* resting on a large box reports the few-cm overlap through the near face
|
|
89
|
+
* rather than the metres-deep "exit through the far side" a centroid-seeded
|
|
90
|
+
* MPR portal used to return.
|
|
91
|
+
* - **general convex pairs** (anything without a closed form) → GJK + EPA,
|
|
92
|
+
* which is exact for polytopes and is only ever reached by polytope-like
|
|
93
|
+
* shapes here, since every curved primitive (sphere, capsule) has a closed
|
|
94
|
+
* form above.
|
|
95
|
+
* - **convex vs concave** (one of heightmap / mesh) → triangle decomposition
|
|
96
|
+
* over the convex AABB + the closed-form per-triangle solvers
|
|
97
|
+
* (`sphere_triangle`, `box_triangle`, `capsule_triangle`), deepest wins.
|
|
98
|
+
* These are bounded to each triangle's true 2-D extent, so the historical
|
|
99
|
+
* over-report on closed-mesh side faces (infinite-plane extrapolation) is
|
|
100
|
+
* gone.
|
|
104
101
|
*
|
|
105
|
-
* ##
|
|
102
|
+
* ## Convex-vs-concave recovery (fully-tunnelled bodies)
|
|
106
103
|
*
|
|
107
|
-
* The
|
|
108
|
-
*
|
|
109
|
-
*
|
|
110
|
-
*
|
|
104
|
+
* The per-triangle closed-form solvers are intentionally one-sided: a convex
|
|
105
|
+
* shape that has crossed to the *inner* side of a surface produces no
|
|
106
|
+
* from-outside contact (the narrowphase won't shove a body deeper into the
|
|
107
|
+
* solid mid-step). For a standalone penetration / depenetration query that is
|
|
108
|
+
* the wrong answer — the shape *is* overlapping the solid and must be pushed
|
|
109
|
+
* back out. When the primary dispatch finds no contact for a convex-vs-concave
|
|
110
|
+
* pair, this function falls back to a half-space test
|
|
111
|
+
* ({@link concave_recovery_penetration}) that reports the outward push-out
|
|
112
|
+
* vector. This is exact for heightmap terrain and a valid (if not strictly
|
|
113
|
+
* minimal) recovery direction for closed meshes.
|
|
111
114
|
*
|
|
112
|
-
*
|
|
113
|
-
*
|
|
114
|
-
* when the convex shape extends past a face's 2D extent. The
|
|
115
|
-
* deepest-wins aggregation then picks a "false-deepest" face whose
|
|
116
|
-
* direction may not be the geometrically optimal one. A closed-form
|
|
117
|
-
* triangle-vs-X solver per primitive shape would fix this; until
|
|
118
|
-
* then, the function reports *some* outward direction with positive
|
|
119
|
-
* depth, which still resolves penetration over multiple iterations.
|
|
115
|
+
* Concave-vs-concave throws — the M×N triangle-pair cost is out of scope (and
|
|
116
|
+
* is also refused by the narrowphase for dynamic pairs).
|
|
120
117
|
*
|
|
121
|
-
*
|
|
122
|
-
*
|
|
123
|
-
* support point lands on the inward side, so the half-space test
|
|
124
|
-
* fires, and the deepest face wins. The reported direction pushes
|
|
125
|
-
* the body outward through that face.
|
|
126
|
-
*
|
|
127
|
-
* @param {Float64Array|number[]} out_direction length ≥ 3; receives
|
|
128
|
-
* the unit separation direction (B → A) on penetration
|
|
118
|
+
* @param {Float64Array|number[]} out_direction length ≥ 3; receives the unit
|
|
119
|
+
* separation direction (B → A) on penetration
|
|
129
120
|
* @param {AbstractShape3D} shape_a in shape_a's local frame; may be concave
|
|
130
121
|
* @param {{x:number,y:number,z:number}} position_a world position of A
|
|
131
122
|
* @param {{x:number,y:number,z:number,w:number}} rotation_a world rotation of A
|
|
@@ -147,121 +138,59 @@ export function compute_penetration(
|
|
|
147
138
|
throw new Error("compute_penetration: at most one shape may be non-convex (concave-vs-concave triangle-pair cost is out of scope)");
|
|
148
139
|
}
|
|
149
140
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
shape_a, position_a, rotation_a,
|
|
154
|
-
shape_b, position_b, rotation_b
|
|
155
|
-
);
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
return compute_penetration_convex(
|
|
159
|
-
out_direction,
|
|
141
|
+
// ── Primary: the exact narrowphase dispatch, deepest contact = MTV. ──
|
|
142
|
+
const depth = deepest_pair_penetration(
|
|
143
|
+
primary_normal,
|
|
160
144
|
shape_a, position_a, rotation_a,
|
|
161
145
|
shape_b, position_b, rotation_b
|
|
162
146
|
);
|
|
163
|
-
}
|
|
164
147
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
* supports where the previous GJK + EPA path struggled (sphere-vs-
|
|
171
|
-
* sphere shallow overlap, sphere-vs-box near-tangent — the closest-
|
|
172
|
-
* face direction would noise out by 20-30° before EPA hit its
|
|
173
|
-
* iteration cap). MPR converges in 5-15 iterations on those same
|
|
174
|
-
* configurations.
|
|
175
|
-
* @private
|
|
176
|
-
*/
|
|
177
|
-
function compute_penetration_convex(
|
|
178
|
-
out_direction,
|
|
179
|
-
shape_a, position_a, rotation_a,
|
|
180
|
-
shape_b, position_b, rotation_b
|
|
181
|
-
) {
|
|
182
|
-
posed_a.setup(shape_a, position_a, rotation_a);
|
|
183
|
-
posed_b.setup(shape_b, position_b, rotation_b);
|
|
184
|
-
|
|
185
|
-
if (!mpr(mpr_result, 0, posed_a, posed_b)) return 0;
|
|
186
|
-
|
|
187
|
-
let ex = mpr_result[0], ey = mpr_result[1], ez = mpr_result[2];
|
|
188
|
-
const depth = Math.sqrt(ex * ex + ey * ey + ez * ez);
|
|
189
|
-
if (!(depth > CONTACT_EPSILON) || !Number.isFinite(depth)) return 0;
|
|
190
|
-
|
|
191
|
-
// MTV direction sanity check: should point from A's centre toward
|
|
192
|
-
// B's centre. Same trick as in `narrowphase_step` — even MPR can
|
|
193
|
-
// settle on either side of the origin for symmetric configurations
|
|
194
|
-
// (axis-aligned cubes), so dot against the body-centre axis and
|
|
195
|
-
// flip if needed.
|
|
196
|
-
const ab_x = position_b.x - position_a.x;
|
|
197
|
-
const ab_y = position_b.y - position_a.y;
|
|
198
|
-
const ab_z = position_b.z - position_a.z;
|
|
199
|
-
if (ex * ab_x + ey * ab_y + ez * ab_z < 0) {
|
|
200
|
-
ex = -ex; ey = -ey; ez = -ez;
|
|
148
|
+
if (depth > CONTACT_EPSILON && Number.isFinite(depth)) {
|
|
149
|
+
out_direction[0] = primary_normal[0];
|
|
150
|
+
out_direction[1] = primary_normal[1];
|
|
151
|
+
out_direction[2] = primary_normal[2];
|
|
152
|
+
return depth;
|
|
201
153
|
}
|
|
202
154
|
|
|
203
|
-
//
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
155
|
+
// ── Recovery: a convex shape fully crossed to the inner side of a concave
|
|
156
|
+
// surface produces no from-outside contact above. Push it back out. ──
|
|
157
|
+
if (isConcaveA !== isConcaveB) {
|
|
158
|
+
return concave_recovery_penetration(
|
|
159
|
+
out_direction, isConcaveA,
|
|
160
|
+
shape_a, position_a, rotation_a,
|
|
161
|
+
shape_b, position_b, rotation_b
|
|
162
|
+
);
|
|
163
|
+
}
|
|
208
164
|
|
|
209
|
-
return
|
|
165
|
+
return 0;
|
|
210
166
|
}
|
|
211
167
|
|
|
212
168
|
/**
|
|
213
|
-
*
|
|
214
|
-
*
|
|
215
|
-
*
|
|
216
|
-
* Why the half-space test instead of per-triangle GJK + EPA:
|
|
217
|
-
*
|
|
218
|
-
* GJK + EPA on `Triangle3D` (a flat 2D shape) has a known degeneracy.
|
|
219
|
-
* `Triangle3D.support` along the triangle's face-normal axis returns
|
|
220
|
-
* the SAME vertex regardless of sign (all three vertices have equal
|
|
221
|
-
* projection along that axis). GJK can't converge from this degenerate
|
|
222
|
-
* support and returns false positives — sphere clearly above a flat
|
|
223
|
-
* surface gets reported as overlapping, with EPA producing a non-zero
|
|
224
|
-
* depth in some arbitrary direction.
|
|
225
|
-
*
|
|
226
|
-
* The half-space test is closed-form and exact for this case:
|
|
227
|
-
*
|
|
228
|
-
* 1. Compute the triangle's outward face normal in world frame
|
|
229
|
-
* (winding gives outward-CCW per the enumerator's contract).
|
|
230
|
-
* 2. Query `convex.support(-face_normal)` — the deepest point of the
|
|
231
|
-
* convex shape along the face-normal axis in the inward direction.
|
|
232
|
-
* 3. Project that point onto the face normal relative to the
|
|
233
|
-
* triangle's centroid plane.
|
|
234
|
-
* 4. If the projection is positive, the convex shape is entirely on
|
|
235
|
-
* the outward side — no penetration. Skip.
|
|
236
|
-
* 5. Otherwise the magnitude is the penetration depth, and the
|
|
237
|
-
* face normal IS the contact direction.
|
|
238
|
-
*
|
|
239
|
-
* For continuous concave surfaces (heightmaps with adjacent triangles
|
|
240
|
-
* sharing edges) the half-space test gives exact results: any penetrating
|
|
241
|
-
* convex either crosses a triangle's plane within the triangle's 2D
|
|
242
|
-
* extent (correct depth reported) or hits an adjacent triangle.
|
|
169
|
+
* Recovery fallback for a convex shape that has tunnelled to the inner side of
|
|
170
|
+
* a concave surface (heightmap / mesh), where the one-sided per-triangle
|
|
171
|
+
* solvers report no contact.
|
|
243
172
|
*
|
|
244
|
-
*
|
|
245
|
-
*
|
|
246
|
-
* convex
|
|
247
|
-
*
|
|
248
|
-
*
|
|
249
|
-
*
|
|
173
|
+
* Per-triangle half-space test, deepest-wins:
|
|
174
|
+
* 1. Face normal of the triangle in world frame (outward by winding).
|
|
175
|
+
* 2. `convex.support(-face_normal)` — the convex's deepest point along the
|
|
176
|
+
* inward face axis.
|
|
177
|
+
* 3. Signed distance of that point to the triangle's plane. If positive the
|
|
178
|
+
* convex is fully outside this face → skip. If negative, its magnitude is
|
|
179
|
+
* the depth and the face normal is the contact axis.
|
|
250
180
|
*
|
|
251
|
-
*
|
|
252
|
-
*
|
|
253
|
-
*
|
|
254
|
-
*
|
|
181
|
+
* Exact for heightmaps (adjacent triangles cover the boundary); for closed
|
|
182
|
+
* meshes the infinite-plane extrapolation can over-report on side faces, but
|
|
183
|
+
* deepest-wins gives a valid outward push that resolves over iterations — and
|
|
184
|
+
* this path only runs once a body is already inside the solid, where any
|
|
185
|
+
* outward direction is progress.
|
|
255
186
|
*
|
|
256
187
|
* @private
|
|
257
188
|
*/
|
|
258
|
-
function
|
|
189
|
+
function concave_recovery_penetration(
|
|
259
190
|
out_direction, isConcaveA,
|
|
260
191
|
shape_a, position_a, rotation_a,
|
|
261
192
|
shape_b, position_b, rotation_b
|
|
262
193
|
) {
|
|
263
|
-
// Internally normalise: "concave" side is what we decompose;
|
|
264
|
-
// "convex" side is wrapped in PosedShape and queried via support.
|
|
265
194
|
const concave_shape = isConcaveA ? shape_a : shape_b;
|
|
266
195
|
const concave_pos = isConcaveA ? position_a : position_b;
|
|
267
196
|
const concave_rot = isConcaveA ? rotation_a : rotation_b;
|
|
@@ -376,8 +305,6 @@ function compute_penetration_concave(
|
|
|
376
305
|
if (best_depth === 0) return 0;
|
|
377
306
|
|
|
378
307
|
// ── 6. Write out_direction in the user's "B → A" convention ────
|
|
379
|
-
//
|
|
380
|
-
// The face normal points OUTWARD from the concave's solid.
|
|
381
308
|
// - isConcaveA: original A = concave. "B → A" = convex → concave
|
|
382
309
|
// = INTO the solid = −face_normal.
|
|
383
310
|
// - isConcaveB: original A = convex. "B → A" = concave → convex
|
|
@@ -1,3 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Single-pair penetration query: the depth and world normal of the DEEPEST
|
|
3
|
+
* contact the narrowphase would generate for one posed shape pair.
|
|
4
|
+
*
|
|
5
|
+
* Routes through the exact same {@link dispatch_pair} the contact solver
|
|
6
|
+
* consumes — closed-form for every sphere / box / capsule pair (box-box via
|
|
7
|
+
* SAT, so the true minimum-translation axis is found rather than the
|
|
8
|
+
* centroid-seeded portal MPR would pick), triangle decomposition + closed-form
|
|
9
|
+
* per triangle for convex-vs-concave, and GJK + EPA (+ MPR) for any other
|
|
10
|
+
* convex pair. The deepest contact's depth is the minimum-translation distance
|
|
11
|
+
* and its normal is the MTV axis, so the result is correct for every shape pair
|
|
12
|
+
* the engine can build and agrees bit-for-bit with what the solver acts on.
|
|
13
|
+
*
|
|
14
|
+
* The normal follows the narrowphase's stored convention: a unit vector
|
|
15
|
+
* pointing from B toward A — the direction to translate A to separate it.
|
|
16
|
+
*
|
|
17
|
+
* Concave-vs-concave is not dispatched (the narrowphase skips it) and returns
|
|
18
|
+
* 0; callers needing to reject that case must check `is_convex` themselves.
|
|
19
|
+
*
|
|
20
|
+
* Not re-entrant: shares the module-level candidate / scratch buffers with
|
|
21
|
+
* {@link narrowphase_step}. Intended for main-thread queries run outside a
|
|
22
|
+
* step (depenetration, overlap depth, tooling) — never from inside one.
|
|
23
|
+
*
|
|
24
|
+
* @param {Float64Array|number[]} out_normal length ≥ 3; receives the unit B→A
|
|
25
|
+
* normal on penetration (untouched when the return value is 0)
|
|
26
|
+
* @param {AbstractShape3D} shapeA
|
|
27
|
+
* @param {{x:number,y:number,z:number}} posA
|
|
28
|
+
* @param {{x:number,y:number,z:number,w:number}} rotA
|
|
29
|
+
* @param {AbstractShape3D} shapeB
|
|
30
|
+
* @param {{x:number,y:number,z:number}} posB
|
|
31
|
+
* @param {{x:number,y:number,z:number,w:number}} rotB
|
|
32
|
+
* @returns {number} deepest penetration depth (> 0) or 0 if separated
|
|
33
|
+
*/
|
|
34
|
+
export function deepest_pair_penetration(out_normal: Float64Array | number[], shapeA: AbstractShape3D, posA: {
|
|
35
|
+
x: number;
|
|
36
|
+
y: number;
|
|
37
|
+
z: number;
|
|
38
|
+
}, rotA: {
|
|
39
|
+
x: number;
|
|
40
|
+
y: number;
|
|
41
|
+
z: number;
|
|
42
|
+
w: number;
|
|
43
|
+
}, shapeB: AbstractShape3D, posB: {
|
|
44
|
+
x: number;
|
|
45
|
+
y: number;
|
|
46
|
+
z: number;
|
|
47
|
+
}, rotB: {
|
|
48
|
+
x: number;
|
|
49
|
+
y: number;
|
|
50
|
+
z: number;
|
|
51
|
+
w: number;
|
|
52
|
+
}): number;
|
|
1
53
|
/**
|
|
2
54
|
* For every pair in `pair_list`, do a cross-product over A's collider list ×
|
|
3
55
|
* B's collider list, accumulate candidate contacts, reduce to ≤4, and write
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"narrowphase_step.d.ts","sourceRoot":"","sources":["../../../../../src/engine/physics/narrowphase/narrowphase_step.js"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"narrowphase_step.d.ts","sourceRoot":"","sources":["../../../../../src/engine/physics/narrowphase/narrowphase_step.js"],"names":[],"mappings":"AAwyCA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AACH,qDAVW,YAAY,GAAC,MAAM,EAAE,iCAGrB;IAAC,CAAC,EAAC,MAAM,CAAC;IAAA,CAAC,EAAC,MAAM,CAAC;IAAA,CAAC,EAAC,MAAM,CAAA;CAAC,QAC5B;IAAC,CAAC,EAAC,MAAM,CAAC;IAAA,CAAC,EAAC,MAAM,CAAC;IAAA,CAAC,EAAC,MAAM,CAAC;IAAA,CAAC,EAAC,MAAM,CAAA;CAAC,iCAErC;IAAC,CAAC,EAAC,MAAM,CAAC;IAAA,CAAC,EAAC,MAAM,CAAC;IAAA,CAAC,EAAC,MAAM,CAAA;CAAC,QAC5B;IAAC,CAAC,EAAC,MAAM,CAAC;IAAA,CAAC,EAAC,MAAM,CAAC;IAAA,CAAC,EAAC,MAAM,CAAC;IAAA,CAAC,EAAC,MAAM,CAAA;CAAC,GACnC,MAAM,CAyClB;AAED;;;;;;;;;;;GAWG;AACH,uFALW,MAAM,MAAM;IAAC,QAAQ,WAAW;IAAC,SAAS,YAAW;CAAC,CAAC,CAAC,QA0JlE;AAED;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,uEAJW,MAAM,UACN,MAAM;IAAC,QAAQ,WAAW;IAAC,SAAS,YAAW;CAAC,CAAC,UACjD,MAAM;IAAC,QAAQ,WAAW;IAAC,SAAS,YAAW;CAAC,CAAC,QAiD3D"}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { aabb3_transform_oriented } from "../../../core/geom/3d/aabb/aabb3_transform_oriented.js";
|
|
2
2
|
import { Triangle3D } from "../../../core/geom/3d/shape/Triangle3D.js";
|
|
3
3
|
import { body_id_index } from "../body/BodyStorage.js";
|
|
4
|
+
import { combine_friction, combine_restitution } from "../contact/combine_material.js";
|
|
4
5
|
import { CONTACT_STRIDE, MAX_CONTACTS_PER_MANIFOLD } from "../contact/ManifoldStore.js";
|
|
5
6
|
import { expanding_polytope_algorithm } from "../gjk/expanding_polytope_algorithm.js";
|
|
6
7
|
import { gjk_with_axis } from "../gjk/gjk.js";
|
|
@@ -52,7 +53,7 @@ const capsule_box_multi_result = new Float64Array(CAPSULE_BOX_MAX_CONTACTS * CAP
|
|
|
52
53
|
|
|
53
54
|
/**
|
|
54
55
|
* Candidate-contact stride: wax, way, waz, wbx, wby, wbz, nx, ny, nz, depth,
|
|
55
|
-
* feature_id.
|
|
56
|
+
* feature_id, friction, restitution.
|
|
56
57
|
*
|
|
57
58
|
* The `feature_id` (offset 10) is a stable cross-frame identifier of the
|
|
58
59
|
* geometric feature pair that produced this contact — used by the
|
|
@@ -61,9 +62,26 @@ const capsule_box_multi_result = new Float64Array(CAPSULE_BOX_MAX_CONTACTS * CAP
|
|
|
61
62
|
* corresponds to the same physical contact. A value of 0 means
|
|
62
63
|
* "no feature info, fall back to position matching".
|
|
63
64
|
*
|
|
65
|
+
* `friction` (offset 11) and `restitution` (offset 12) are the COMBINED
|
|
66
|
+
* coefficients for the specific (colliderA, colliderB) pair that produced this
|
|
67
|
+
* contact, combined here (the only place that knows the exact source collider
|
|
68
|
+
* on each side) and carried into the manifold so a compound body's per-collider
|
|
69
|
+
* materials are honoured per-contact.
|
|
70
|
+
*
|
|
71
|
+
* @type {number}
|
|
72
|
+
*/
|
|
73
|
+
const CANDIDATE_STRIDE = 13;
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Combined friction / restitution for the collider pair currently being
|
|
77
|
+
* dispatched. Set once at the top of {@link dispatch_pair} (which is called
|
|
78
|
+
* per collider pair) and written into every contact that call appends, so
|
|
79
|
+
* each contact carries the material of its actual source colliders. Module
|
|
80
|
+
* scratch rather than threaded through every `append_contact` call site.
|
|
64
81
|
* @type {number}
|
|
65
82
|
*/
|
|
66
|
-
|
|
83
|
+
let g_pair_friction = 0;
|
|
84
|
+
let g_pair_restitution = 0;
|
|
67
85
|
|
|
68
86
|
/**
|
|
69
87
|
* Maximum number of contacts emitted into the per-pair manifold after the
|
|
@@ -194,6 +212,8 @@ function append_contact(count, wax, way, waz, wbx, wby, wbz, nx, ny, nz, depth,
|
|
|
194
212
|
candidates[off + 6] = nx; candidates[off + 7] = ny; candidates[off + 8] = nz;
|
|
195
213
|
candidates[off + 9] = depth;
|
|
196
214
|
candidates[off + 10] = feature_id;
|
|
215
|
+
candidates[off + 11] = g_pair_friction;
|
|
216
|
+
candidates[off + 12] = g_pair_restitution;
|
|
197
217
|
|
|
198
218
|
return count + 1;
|
|
199
219
|
}
|
|
@@ -358,6 +378,20 @@ function dispatch_pair(count, colA, trA, colB, trB, gjk_axis_buf = null, gjk_axi
|
|
|
358
378
|
const shapeA = colA.shape;
|
|
359
379
|
const shapeB = colB.shape;
|
|
360
380
|
|
|
381
|
+
// Per-contact materials: combine the two source colliders' coefficients
|
|
382
|
+
// once here (this is the only place that knows the exact collider on each
|
|
383
|
+
// side) and stamp them onto every contact this dispatch appends. The
|
|
384
|
+
// `deepest_pair_penetration` query passes bare `{shape}` adapters with no
|
|
385
|
+
// material fields — it never writes to a manifold, so 0 is fine there.
|
|
386
|
+
const fa = colA.friction, fb = colB.friction;
|
|
387
|
+
if (fa !== undefined && fb !== undefined) {
|
|
388
|
+
g_pair_friction = combine_friction(fa, fb);
|
|
389
|
+
g_pair_restitution = combine_restitution(colA.restitution, colB.restitution);
|
|
390
|
+
} else {
|
|
391
|
+
g_pair_friction = 0;
|
|
392
|
+
g_pair_restitution = 0;
|
|
393
|
+
}
|
|
394
|
+
|
|
361
395
|
// isSphereShape3D covers both UnitSphereShape3D (fixed radius 1) and
|
|
362
396
|
// SphereShape3D (arbitrary radius). Both expose `radius`.
|
|
363
397
|
const isSphereA = shapeA.isSphereShape3D === true;
|
|
@@ -1266,6 +1300,98 @@ function dispatch_pair(count, colA, trA, colB, trB, gjk_axis_buf = null, gjk_axi
|
|
|
1266
1300
|
);
|
|
1267
1301
|
}
|
|
1268
1302
|
|
|
1303
|
+
// Reusable single-pair adapters for the penetration query below — no per-call
|
|
1304
|
+
// allocation. dispatch_pair only reads `.shape` off a collider and
|
|
1305
|
+
// `.position` / `.rotation` off a transform, so these minimal stand-ins are
|
|
1306
|
+
// all it needs.
|
|
1307
|
+
const _pp_colA = { shape: null };
|
|
1308
|
+
const _pp_colB = { shape: null };
|
|
1309
|
+
const _pp_trA = { position: null, rotation: null };
|
|
1310
|
+
const _pp_trB = { position: null, rotation: null };
|
|
1311
|
+
|
|
1312
|
+
/**
|
|
1313
|
+
* Cold-start GJK seed for the one-shot penetration query. Re-zeroed before each
|
|
1314
|
+
* call so every query is independent (gjk_with_axis treats a zero vector as a
|
|
1315
|
+
* cold start) — no warm-start leakage between unrelated queries, which keeps
|
|
1316
|
+
* the result a pure function of its inputs.
|
|
1317
|
+
* @type {Float64Array}
|
|
1318
|
+
*/
|
|
1319
|
+
const _pp_axis = new Float64Array(3);
|
|
1320
|
+
|
|
1321
|
+
/**
|
|
1322
|
+
* Single-pair penetration query: the depth and world normal of the DEEPEST
|
|
1323
|
+
* contact the narrowphase would generate for one posed shape pair.
|
|
1324
|
+
*
|
|
1325
|
+
* Routes through the exact same {@link dispatch_pair} the contact solver
|
|
1326
|
+
* consumes — closed-form for every sphere / box / capsule pair (box-box via
|
|
1327
|
+
* SAT, so the true minimum-translation axis is found rather than the
|
|
1328
|
+
* centroid-seeded portal MPR would pick), triangle decomposition + closed-form
|
|
1329
|
+
* per triangle for convex-vs-concave, and GJK + EPA (+ MPR) for any other
|
|
1330
|
+
* convex pair. The deepest contact's depth is the minimum-translation distance
|
|
1331
|
+
* and its normal is the MTV axis, so the result is correct for every shape pair
|
|
1332
|
+
* the engine can build and agrees bit-for-bit with what the solver acts on.
|
|
1333
|
+
*
|
|
1334
|
+
* The normal follows the narrowphase's stored convention: a unit vector
|
|
1335
|
+
* pointing from B toward A — the direction to translate A to separate it.
|
|
1336
|
+
*
|
|
1337
|
+
* Concave-vs-concave is not dispatched (the narrowphase skips it) and returns
|
|
1338
|
+
* 0; callers needing to reject that case must check `is_convex` themselves.
|
|
1339
|
+
*
|
|
1340
|
+
* Not re-entrant: shares the module-level candidate / scratch buffers with
|
|
1341
|
+
* {@link narrowphase_step}. Intended for main-thread queries run outside a
|
|
1342
|
+
* step (depenetration, overlap depth, tooling) — never from inside one.
|
|
1343
|
+
*
|
|
1344
|
+
* @param {Float64Array|number[]} out_normal length ≥ 3; receives the unit B→A
|
|
1345
|
+
* normal on penetration (untouched when the return value is 0)
|
|
1346
|
+
* @param {AbstractShape3D} shapeA
|
|
1347
|
+
* @param {{x:number,y:number,z:number}} posA
|
|
1348
|
+
* @param {{x:number,y:number,z:number,w:number}} rotA
|
|
1349
|
+
* @param {AbstractShape3D} shapeB
|
|
1350
|
+
* @param {{x:number,y:number,z:number}} posB
|
|
1351
|
+
* @param {{x:number,y:number,z:number,w:number}} rotB
|
|
1352
|
+
* @returns {number} deepest penetration depth (> 0) or 0 if separated
|
|
1353
|
+
*/
|
|
1354
|
+
export function deepest_pair_penetration(out_normal, shapeA, posA, rotA, shapeB, posB, rotB) {
|
|
1355
|
+
_pp_colA.shape = shapeA;
|
|
1356
|
+
_pp_trA.position = posA;
|
|
1357
|
+
_pp_trA.rotation = rotA;
|
|
1358
|
+
_pp_colB.shape = shapeB;
|
|
1359
|
+
_pp_trB.position = posB;
|
|
1360
|
+
_pp_trB.rotation = rotB;
|
|
1361
|
+
|
|
1362
|
+
// Cold GJK seed — one-shot query, not a warm-started per-frame manifold.
|
|
1363
|
+
_pp_axis[0] = 0; _pp_axis[1] = 0; _pp_axis[2] = 0;
|
|
1364
|
+
const n = dispatch_pair(0, _pp_colA, _pp_trA, _pp_colB, _pp_trB, _pp_axis, 0);
|
|
1365
|
+
if (n === 0) {
|
|
1366
|
+
return 0;
|
|
1367
|
+
}
|
|
1368
|
+
|
|
1369
|
+
// Deepest contact = the minimum-translation depth; its stored normal is the
|
|
1370
|
+
// separation axis. (For multi-point manifolds — box-box, capsule-box, a
|
|
1371
|
+
// convex straddling several mesh triangles — every point shares the
|
|
1372
|
+
// separating axis, so the max depth along it is the distance to separate.)
|
|
1373
|
+
let best_depth = -1;
|
|
1374
|
+
let best_off = 0;
|
|
1375
|
+
for (let i = 0; i < n; i++) {
|
|
1376
|
+
const off = i * CANDIDATE_STRIDE;
|
|
1377
|
+
const d = candidates[off + 9];
|
|
1378
|
+
if (d > best_depth) {
|
|
1379
|
+
best_depth = d;
|
|
1380
|
+
best_off = off;
|
|
1381
|
+
}
|
|
1382
|
+
}
|
|
1383
|
+
|
|
1384
|
+
if (!(best_depth > 0) || !Number.isFinite(best_depth)) {
|
|
1385
|
+
return 0;
|
|
1386
|
+
}
|
|
1387
|
+
|
|
1388
|
+
out_normal[0] = candidates[best_off + 6];
|
|
1389
|
+
out_normal[1] = candidates[best_off + 7];
|
|
1390
|
+
out_normal[2] = candidates[best_off + 8];
|
|
1391
|
+
|
|
1392
|
+
return best_depth;
|
|
1393
|
+
}
|
|
1394
|
+
|
|
1269
1395
|
/**
|
|
1270
1396
|
* For every pair in `pair_list`, do a cross-product over A's collider list ×
|
|
1271
1397
|
* B's collider list, accumulate candidate contacts, reduce to ≤4, and write
|
|
@@ -1408,7 +1534,8 @@ export function narrowphase_step(pair_list, manifolds, lists) {
|
|
|
1408
1534
|
candidates[off + 3], candidates[off + 4], candidates[off + 5],
|
|
1409
1535
|
candidates[off + 6], candidates[off + 7], candidates[off + 8],
|
|
1410
1536
|
candidates[off + 9],
|
|
1411
|
-
candidates[off + 10]
|
|
1537
|
+
candidates[off + 10],
|
|
1538
|
+
candidates[off + 11], candidates[off + 12]
|
|
1412
1539
|
);
|
|
1413
1540
|
const prev_j = cand_to_prev[k];
|
|
1414
1541
|
if (prev_j !== -1) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"solve_contacts.d.ts","sourceRoot":"","sources":["../../../../../src/engine/physics/solver/solve_contacts.js"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"solve_contacts.d.ts","sourceRoot":"","sources":["../../../../../src/engine/physics/solver/solve_contacts.js"],"names":[],"mappings":"AAkbA;;;;;;;;;;;;;GAaG;AACH,0FAHW,MAAM,GACJ,MAAM,CA+JlB;AAED;;;;;;;;;;;;;;;GAeG;AACH,2FAuCC;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wFAgEC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,iGAmFC;AAED;;;;;;;;GAQG;AACH,uFAFW,MAAM,QA2GhB;AAED;;;;;;;;;;;GAWG;AACH,yFA4DC;AAED;;;;;;;;;;;;;;GAcG;AACH,2FAFW,MAAM,QA4EhB;AAED;;;;;;;;;;;;;;;GAeG;AACH,oFAJW,MAAM,UACN,MAAM,cACN,MAAM,QAahB;AA7kCD;;;;;GAKG;AACH,0CAFU,MAAM,CAEuB;AAEvC;;;GAGG;AACH,0CAFU,MAAM,CAEsB"}
|