@woosh/meep-engine 2.144.0 → 2.146.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/bvh2/bvh3/BVH.d.ts.map +1 -1
- package/src/core/bvh2/bvh3/BVH.js +158 -4
- package/src/core/geom/3d/shape/CylinderShape3D.d.ts +56 -0
- package/src/core/geom/3d/shape/CylinderShape3D.d.ts.map +1 -0
- package/src/core/geom/3d/shape/CylinderShape3D.js +223 -0
- package/src/core/geom/3d/shape/HeightMapShape3D.d.ts +33 -3
- package/src/core/geom/3d/shape/HeightMapShape3D.d.ts.map +1 -1
- package/src/core/geom/3d/shape/HeightMapShape3D.js +486 -451
- package/src/core/geom/3d/shape/json/shape_to_type.d.ts.map +1 -1
- package/src/core/geom/3d/shape/json/shape_to_type.js +3 -0
- package/src/core/geom/3d/shape/json/type_adapters.d.ts +15 -0
- package/src/core/geom/3d/shape/json/type_adapters.d.ts.map +1 -1
- package/src/core/geom/3d/shape/json/type_adapters.js +16 -0
- package/src/engine/control/first-person/DESIGN_COLLISION.md +365 -302
- package/src/engine/control/first-person/FirstPersonPlayerController.d.ts +1 -3
- package/src/engine/control/first-person/FirstPersonPlayerController.d.ts.map +1 -1
- package/src/engine/control/first-person/FirstPersonPlayerControllerConfig.d.ts +12 -2
- package/src/engine/control/first-person/FirstPersonPlayerControllerConfig.d.ts.map +1 -1
- package/src/engine/control/first-person/FirstPersonPlayerControllerConfig.js +7 -2
- package/src/engine/control/first-person/FirstPersonPlayerControllerSystem.d.ts +13 -0
- package/src/engine/control/first-person/FirstPersonPlayerControllerSystem.d.ts.map +1 -1
- package/src/engine/control/first-person/FirstPersonPlayerControllerSystem.js +16 -2
- package/src/engine/control/first-person/TODO.md +13 -11
- package/src/engine/control/first-person/abilities/WallJump.d.ts.map +1 -1
- package/src/engine/control/first-person/abilities/WallJump.js +11 -3
- package/src/engine/control/first-person/abilities/WallRun.d.ts.map +1 -1
- package/src/engine/control/first-person/abilities/WallRun.js +30 -35
- package/src/engine/control/first-person/collision/KinematicMover.d.ts +35 -5
- package/src/engine/control/first-person/collision/KinematicMover.d.ts.map +1 -1
- package/src/engine/control/first-person/collision/KinematicMover.js +634 -424
- package/src/engine/control/first-person/prototype_first_person_controller.js +1003 -901
- package/src/engine/physics/PLAN.md +943 -767
- package/src/engine/physics/body/BodyStorage.d.ts +9 -0
- package/src/engine/physics/body/BodyStorage.d.ts.map +1 -1
- package/src/engine/physics/body/BodyStorage.js +23 -0
- package/src/engine/physics/broadphase/generate_pairs.d.ts.map +1 -1
- package/src/engine/physics/broadphase/generate_pairs.js +7 -0
- package/src/engine/physics/ccd/linear_sweep.d.ts +97 -0
- package/src/engine/physics/ccd/linear_sweep.d.ts.map +1 -0
- package/src/engine/physics/ccd/linear_sweep.js +238 -0
- package/src/engine/physics/ecs/PhysicsSystem.d.ts +18 -3
- package/src/engine/physics/ecs/PhysicsSystem.d.ts.map +1 -1
- package/src/engine/physics/ecs/PhysicsSystem.js +59 -8
- package/src/engine/physics/ecs/RigidBodyFlags.d.ts +6 -0
- package/src/engine/physics/ecs/RigidBodyFlags.d.ts.map +1 -1
- package/src/engine/physics/ecs/RigidBodyFlags.js +6 -0
- package/src/engine/physics/narrowphase/box_triangle_contact.js +811 -811
- package/src/engine/physics/narrowphase/compute_penetration.d.ts.map +1 -1
- package/src/engine/physics/narrowphase/compute_penetration.js +325 -323
- package/src/engine/physics/narrowphase/decomposition/heightmap_enumerate_triangles.d.ts +27 -8
- package/src/engine/physics/narrowphase/decomposition/heightmap_enumerate_triangles.d.ts.map +1 -1
- package/src/engine/physics/narrowphase/decomposition/heightmap_enumerate_triangles.js +235 -204
- package/src/engine/physics/narrowphase/narrowphase_step.d.ts.map +1 -1
- package/src/engine/physics/narrowphase/narrowphase_step.js +70 -13
- package/src/engine/physics/queries/overlap_shape.d.ts.map +1 -1
- package/src/engine/physics/queries/overlap_shape.js +185 -183
- package/src/engine/simulation/Ticker.d.ts +14 -0
- package/src/engine/simulation/Ticker.d.ts.map +1 -1
- package/src/engine/simulation/Ticker.js +136 -1
|
@@ -1,323 +1,325 @@
|
|
|
1
|
-
import { aabb3_transform_oriented } from "../../../core/geom/3d/aabb/aabb3_transform_oriented.js";
|
|
2
|
-
import { v3_quat3_apply } from "../../../core/geom/vec3/v3_quat3_apply.js";
|
|
3
|
-
import { aabb_world_to_local } from "./decomposition/aabb_world_to_local.js";
|
|
4
|
-
import { decompose_to_triangles } from "./decomposition/decompose_to_triangles.js";
|
|
5
|
-
import { TRIANGLE_FLOAT_STRIDE } from "./decomposition/triangle_buffer_layout.js";
|
|
6
|
-
import { deepest_pair_penetration } from "./narrowphase_step.js";
|
|
7
|
-
import { PosedShape } from "./PosedShape.js";
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* Module-scoped scratch — same single-thread re-entrancy assumption as
|
|
11
|
-
* the other narrowphase utilities. Safe because PhysicsSystem queries
|
|
12
|
-
* (and gameplay code calling this) run on the main thread.
|
|
13
|
-
*/
|
|
14
|
-
const posed_b = new PosedShape();
|
|
15
|
-
|
|
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);
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* Scratch buffers for the convex-vs-concave recovery path (see
|
|
26
|
-
* {@link concave_recovery_penetration}).
|
|
27
|
-
*/
|
|
28
|
-
const local_aabb = new Float64Array(6);
|
|
29
|
-
const world_aabb = new Float64Array(6);
|
|
30
|
-
const concave_query_aabb = new Float64Array(6);
|
|
31
|
-
const scratch_v3 = new Float64Array(3);
|
|
32
|
-
const scratch_rot = new Float64Array(3);
|
|
33
|
-
|
|
34
|
-
/**
|
|
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
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
-
*
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
*
|
|
72
|
-
*
|
|
73
|
-
*
|
|
74
|
-
*
|
|
75
|
-
*
|
|
76
|
-
*
|
|
77
|
-
*
|
|
78
|
-
*
|
|
79
|
-
*
|
|
80
|
-
*
|
|
81
|
-
*
|
|
82
|
-
*
|
|
83
|
-
*
|
|
84
|
-
*
|
|
85
|
-
*
|
|
86
|
-
*
|
|
87
|
-
*
|
|
88
|
-
*
|
|
89
|
-
*
|
|
90
|
-
*
|
|
91
|
-
*
|
|
92
|
-
*
|
|
93
|
-
*
|
|
94
|
-
*
|
|
95
|
-
*
|
|
96
|
-
*
|
|
97
|
-
*
|
|
98
|
-
*
|
|
99
|
-
*
|
|
100
|
-
*
|
|
101
|
-
*
|
|
102
|
-
*
|
|
103
|
-
*
|
|
104
|
-
*
|
|
105
|
-
*
|
|
106
|
-
*
|
|
107
|
-
*
|
|
108
|
-
*
|
|
109
|
-
*
|
|
110
|
-
*
|
|
111
|
-
*
|
|
112
|
-
*
|
|
113
|
-
*
|
|
114
|
-
*
|
|
115
|
-
*
|
|
116
|
-
*
|
|
117
|
-
*
|
|
118
|
-
*
|
|
119
|
-
*
|
|
120
|
-
* @param {
|
|
121
|
-
*
|
|
122
|
-
* @param {
|
|
123
|
-
* @param {
|
|
124
|
-
* @param {{x:number,y:number,z:number}}
|
|
125
|
-
* @param {
|
|
126
|
-
* @
|
|
127
|
-
* @
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
out_direction[
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
*
|
|
172
|
-
*
|
|
173
|
-
*
|
|
174
|
-
*
|
|
175
|
-
*
|
|
176
|
-
*
|
|
177
|
-
*
|
|
178
|
-
*
|
|
179
|
-
*
|
|
180
|
-
*
|
|
181
|
-
*
|
|
182
|
-
*
|
|
183
|
-
*
|
|
184
|
-
*
|
|
185
|
-
* outward
|
|
186
|
-
*
|
|
187
|
-
*
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
const
|
|
197
|
-
const
|
|
198
|
-
const
|
|
199
|
-
const
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
const
|
|
233
|
-
const
|
|
234
|
-
const
|
|
235
|
-
const
|
|
236
|
-
const
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
const
|
|
246
|
-
|
|
247
|
-
const
|
|
248
|
-
const
|
|
249
|
-
const
|
|
250
|
-
const
|
|
251
|
-
const
|
|
252
|
-
const
|
|
253
|
-
const
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
let
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
const
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
const
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
const
|
|
276
|
-
|
|
277
|
-
const
|
|
278
|
-
|
|
279
|
-
const
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
//
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
if (depth
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
//
|
|
310
|
-
// -
|
|
311
|
-
// =
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
out_direction[
|
|
316
|
-
|
|
317
|
-
out_direction[
|
|
318
|
-
|
|
319
|
-
out_direction[
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
1
|
+
import { aabb3_transform_oriented } from "../../../core/geom/3d/aabb/aabb3_transform_oriented.js";
|
|
2
|
+
import { v3_quat3_apply } from "../../../core/geom/vec3/v3_quat3_apply.js";
|
|
3
|
+
import { aabb_world_to_local } from "./decomposition/aabb_world_to_local.js";
|
|
4
|
+
import { decompose_to_triangles } from "./decomposition/decompose_to_triangles.js";
|
|
5
|
+
import { TRIANGLE_FLOAT_STRIDE } from "./decomposition/triangle_buffer_layout.js";
|
|
6
|
+
import { deepest_pair_penetration } from "./narrowphase_step.js";
|
|
7
|
+
import { PosedShape } from "./PosedShape.js";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Module-scoped scratch — same single-thread re-entrancy assumption as
|
|
11
|
+
* the other narrowphase utilities. Safe because PhysicsSystem queries
|
|
12
|
+
* (and gameplay code calling this) run on the main thread.
|
|
13
|
+
*/
|
|
14
|
+
const posed_b = new PosedShape();
|
|
15
|
+
|
|
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);
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Scratch buffers for the convex-vs-concave recovery path (see
|
|
26
|
+
* {@link concave_recovery_penetration}).
|
|
27
|
+
*/
|
|
28
|
+
const local_aabb = new Float64Array(6);
|
|
29
|
+
const world_aabb = new Float64Array(6);
|
|
30
|
+
const concave_query_aabb = new Float64Array(6);
|
|
31
|
+
const scratch_v3 = new Float64Array(3);
|
|
32
|
+
const scratch_rot = new Float64Array(3);
|
|
33
|
+
|
|
34
|
+
/**
|
|
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 — though
|
|
38
|
+
* a heightmap's count scales O(N²) with its `tessellation` (still inside the
|
|
39
|
+
* buffer for a bounded query at moderate tessellation). Excess is dropped by
|
|
40
|
+
* the enumerator's bounds check.
|
|
41
|
+
* @type {number}
|
|
42
|
+
*/
|
|
43
|
+
const MAX_TRIANGLES_PER_PAIR = 1024;
|
|
44
|
+
const triangle_buffer = new Float64Array(MAX_TRIANGLES_PER_PAIR * TRIANGLE_FLOAT_STRIDE);
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Penetration depths below this are treated as no contact. The narrowphase
|
|
48
|
+
* dispatch can report sub-micron "overlap" at exact tangent (GJK/EPA noise,
|
|
49
|
+
* or a closed-form solver returning a hair of depth on a kissing contact); the
|
|
50
|
+
* "0 means no overlap" contract is more useful when that noise is filtered out
|
|
51
|
+
* at the source.
|
|
52
|
+
*
|
|
53
|
+
* 1e-4 m (100 µm) is well below any practical world-scale tolerance while
|
|
54
|
+
* still larger than typical convergence residuals at exact tangent.
|
|
55
|
+
*
|
|
56
|
+
* @type {number}
|
|
57
|
+
*/
|
|
58
|
+
const CONTACT_EPSILON = 1e-4;
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Compute the penetration depth between two shapes at world poses,
|
|
62
|
+
* returning the depth and writing the separation direction.
|
|
63
|
+
*
|
|
64
|
+
* If the shapes overlap, the return value is the positive distance
|
|
65
|
+
* along `out_direction` you would have to translate `shape_a` to
|
|
66
|
+
* separate it from `shape_b`. `out_direction` is the unit vector
|
|
67
|
+
* pointing from `shape_b` toward `shape_a` — applying
|
|
68
|
+
* `out_direction * return_value` to `position_a` (or equivalently
|
|
69
|
+
* `-out_direction * return_value` to `position_b`) is the minimum
|
|
70
|
+
* translation that produces separation.
|
|
71
|
+
*
|
|
72
|
+
* If the shapes do not overlap, the return value is `0` and
|
|
73
|
+
* `out_direction` is left untouched. Callers should treat 0 as "no
|
|
74
|
+
* penetration".
|
|
75
|
+
*
|
|
76
|
+
* Sign convention matches the narrowphase's stored contact normal:
|
|
77
|
+
* `out_direction` ≡ the "B → A" direction (B's outward surface normal at the
|
|
78
|
+
* contact, pointing toward A).
|
|
79
|
+
*
|
|
80
|
+
* ## How it is computed (hardened)
|
|
81
|
+
*
|
|
82
|
+
* The query routes through the **same narrowphase contact dispatch the solver
|
|
83
|
+
* consumes** ({@link deepest_pair_penetration} → `dispatch_pair`) and reports
|
|
84
|
+
* the deepest contact. That makes it correct — not "correct sometimes" — for
|
|
85
|
+
* every shape pair the engine can build:
|
|
86
|
+
*
|
|
87
|
+
* - **sphere / box / capsule pairs** → exact closed-form solvers
|
|
88
|
+
* (`sphere_sphere`, `sphere_box`, `box_box` via SAT, `capsule_*`). Box-box
|
|
89
|
+
* in particular uses the true minimum-translation axis, so a small body
|
|
90
|
+
* resting on a large box reports the few-cm overlap through the near face
|
|
91
|
+
* rather than the metres-deep "exit through the far side" a centroid-seeded
|
|
92
|
+
* MPR portal used to return.
|
|
93
|
+
* - **general convex pairs** (anything without a closed form) → GJK + EPA,
|
|
94
|
+
* which is exact for polytopes and is only ever reached by polytope-like
|
|
95
|
+
* shapes here, since every curved primitive (sphere, capsule) has a closed
|
|
96
|
+
* form above.
|
|
97
|
+
* - **convex vs concave** (one of heightmap / mesh) → triangle decomposition
|
|
98
|
+
* over the convex AABB + the closed-form per-triangle solvers
|
|
99
|
+
* (`sphere_triangle`, `box_triangle`, `capsule_triangle`), deepest wins.
|
|
100
|
+
* These are bounded to each triangle's true 2-D extent, so the historical
|
|
101
|
+
* over-report on closed-mesh side faces (infinite-plane extrapolation) is
|
|
102
|
+
* gone.
|
|
103
|
+
*
|
|
104
|
+
* ## Convex-vs-concave recovery (fully-tunnelled bodies)
|
|
105
|
+
*
|
|
106
|
+
* The per-triangle closed-form solvers are intentionally one-sided: a convex
|
|
107
|
+
* shape that has crossed to the *inner* side of a surface produces no
|
|
108
|
+
* from-outside contact (the narrowphase won't shove a body deeper into the
|
|
109
|
+
* solid mid-step). For a standalone penetration / depenetration query that is
|
|
110
|
+
* the wrong answer — the shape *is* overlapping the solid and must be pushed
|
|
111
|
+
* back out. When the primary dispatch finds no contact for a convex-vs-concave
|
|
112
|
+
* pair, this function falls back to a half-space test
|
|
113
|
+
* ({@link concave_recovery_penetration}) that reports the outward push-out
|
|
114
|
+
* vector. This is exact for heightmap terrain and a valid (if not strictly
|
|
115
|
+
* minimal) recovery direction for closed meshes.
|
|
116
|
+
*
|
|
117
|
+
* Concave-vs-concave throws — the M×N triangle-pair cost is out of scope (and
|
|
118
|
+
* is also refused by the narrowphase for dynamic pairs).
|
|
119
|
+
*
|
|
120
|
+
* @param {Float64Array|number[]} out_direction length ≥ 3; receives the unit
|
|
121
|
+
* separation direction (B → A) on penetration
|
|
122
|
+
* @param {AbstractShape3D} shape_a in shape_a's local frame; may be concave
|
|
123
|
+
* @param {{x:number,y:number,z:number}} position_a world position of A
|
|
124
|
+
* @param {{x:number,y:number,z:number,w:number}} rotation_a world rotation of A
|
|
125
|
+
* @param {AbstractShape3D} shape_b in shape_b's local frame; may be concave
|
|
126
|
+
* @param {{x:number,y:number,z:number}} position_b world position of B
|
|
127
|
+
* @param {{x:number,y:number,z:number,w:number}} rotation_b world rotation of B
|
|
128
|
+
* @returns {number} penetration depth (positive) on overlap, 0 otherwise
|
|
129
|
+
* @throws {Error} if both shapes have `is_convex === false`
|
|
130
|
+
*/
|
|
131
|
+
export function compute_penetration(
|
|
132
|
+
out_direction,
|
|
133
|
+
shape_a, position_a, rotation_a,
|
|
134
|
+
shape_b, position_b, rotation_b
|
|
135
|
+
) {
|
|
136
|
+
const isConcaveA = shape_a.is_convex === false;
|
|
137
|
+
const isConcaveB = shape_b.is_convex === false;
|
|
138
|
+
|
|
139
|
+
if (isConcaveA && isConcaveB) {
|
|
140
|
+
throw new Error("compute_penetration: at most one shape may be non-convex (concave-vs-concave triangle-pair cost is out of scope)");
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// ── Primary: the exact narrowphase dispatch, deepest contact = MTV. ──
|
|
144
|
+
const depth = deepest_pair_penetration(
|
|
145
|
+
primary_normal,
|
|
146
|
+
shape_a, position_a, rotation_a,
|
|
147
|
+
shape_b, position_b, rotation_b
|
|
148
|
+
);
|
|
149
|
+
|
|
150
|
+
if (depth > CONTACT_EPSILON && Number.isFinite(depth)) {
|
|
151
|
+
out_direction[0] = primary_normal[0];
|
|
152
|
+
out_direction[1] = primary_normal[1];
|
|
153
|
+
out_direction[2] = primary_normal[2];
|
|
154
|
+
return depth;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// ── Recovery: a convex shape fully crossed to the inner side of a concave
|
|
158
|
+
// surface produces no from-outside contact above. Push it back out. ──
|
|
159
|
+
if (isConcaveA !== isConcaveB) {
|
|
160
|
+
return concave_recovery_penetration(
|
|
161
|
+
out_direction, isConcaveA,
|
|
162
|
+
shape_a, position_a, rotation_a,
|
|
163
|
+
shape_b, position_b, rotation_b
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return 0;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Recovery fallback for a convex shape that has tunnelled to the inner side of
|
|
172
|
+
* a concave surface (heightmap / mesh), where the one-sided per-triangle
|
|
173
|
+
* solvers report no contact.
|
|
174
|
+
*
|
|
175
|
+
* Per-triangle half-space test, deepest-wins:
|
|
176
|
+
* 1. Face normal of the triangle in world frame (outward by winding).
|
|
177
|
+
* 2. `convex.support(-face_normal)` — the convex's deepest point along the
|
|
178
|
+
* inward face axis.
|
|
179
|
+
* 3. Signed distance of that point to the triangle's plane. If positive the
|
|
180
|
+
* convex is fully outside this face → skip. If negative, its magnitude is
|
|
181
|
+
* the depth and the face normal is the contact axis.
|
|
182
|
+
*
|
|
183
|
+
* Exact for heightmaps (adjacent triangles cover the boundary); for closed
|
|
184
|
+
* meshes the infinite-plane extrapolation can over-report on side faces, but
|
|
185
|
+
* deepest-wins gives a valid outward push that resolves over iterations — and
|
|
186
|
+
* this path only runs once a body is already inside the solid, where any
|
|
187
|
+
* outward direction is progress.
|
|
188
|
+
*
|
|
189
|
+
* @private
|
|
190
|
+
*/
|
|
191
|
+
function concave_recovery_penetration(
|
|
192
|
+
out_direction, isConcaveA,
|
|
193
|
+
shape_a, position_a, rotation_a,
|
|
194
|
+
shape_b, position_b, rotation_b
|
|
195
|
+
) {
|
|
196
|
+
const concave_shape = isConcaveA ? shape_a : shape_b;
|
|
197
|
+
const concave_pos = isConcaveA ? position_a : position_b;
|
|
198
|
+
const concave_rot = isConcaveA ? rotation_a : rotation_b;
|
|
199
|
+
const convex_shape = isConcaveA ? shape_b : shape_a;
|
|
200
|
+
const convex_pos = isConcaveA ? position_b : position_a;
|
|
201
|
+
const convex_rot = isConcaveA ? rotation_b : rotation_a;
|
|
202
|
+
|
|
203
|
+
// ── 1. Convex shape's world AABB ───────────────────────────────
|
|
204
|
+
convex_shape.compute_bounding_box(local_aabb);
|
|
205
|
+
aabb3_transform_oriented(
|
|
206
|
+
world_aabb, 0,
|
|
207
|
+
local_aabb[0], local_aabb[1], local_aabb[2],
|
|
208
|
+
local_aabb[3], local_aabb[4], local_aabb[5],
|
|
209
|
+
convex_pos.x, convex_pos.y, convex_pos.z,
|
|
210
|
+
convex_rot.x, convex_rot.y, convex_rot.z, convex_rot.w
|
|
211
|
+
);
|
|
212
|
+
|
|
213
|
+
// ── 2. Project into concave's body-local frame ─────────────────
|
|
214
|
+
aabb_world_to_local(
|
|
215
|
+
concave_query_aabb, 0,
|
|
216
|
+
world_aabb,
|
|
217
|
+
concave_pos.x, concave_pos.y, concave_pos.z,
|
|
218
|
+
concave_rot.x, concave_rot.y, concave_rot.z, concave_rot.w
|
|
219
|
+
);
|
|
220
|
+
|
|
221
|
+
// ── 3. Decompose to triangles ──────────────────────────────────
|
|
222
|
+
const tri_count = decompose_to_triangles(
|
|
223
|
+
triangle_buffer, 0, concave_shape,
|
|
224
|
+
concave_query_aabb[0], concave_query_aabb[1], concave_query_aabb[2],
|
|
225
|
+
concave_query_aabb[3], concave_query_aabb[4], concave_query_aabb[5]
|
|
226
|
+
);
|
|
227
|
+
if (tri_count === 0) return 0;
|
|
228
|
+
|
|
229
|
+
// ── 4. Setup convex PosedShape (we'll call support on it per tri) ─
|
|
230
|
+
posed_b.setup(convex_shape, convex_pos, convex_rot);
|
|
231
|
+
|
|
232
|
+
const cqx = concave_rot.x;
|
|
233
|
+
const cqy = concave_rot.y;
|
|
234
|
+
const cqz = concave_rot.z;
|
|
235
|
+
const cqw = concave_rot.w;
|
|
236
|
+
const c_pos_x = concave_pos.x;
|
|
237
|
+
const c_pos_y = concave_pos.y;
|
|
238
|
+
const c_pos_z = concave_pos.z;
|
|
239
|
+
|
|
240
|
+
// ── 5. Per-triangle half-space test, deepest-wins ──────────────
|
|
241
|
+
let best_depth = 0;
|
|
242
|
+
let best_fnx_w = 0, best_fny_w = 0, best_fnz_w = 0;
|
|
243
|
+
|
|
244
|
+
for (let i = 0; i < tri_count; i++) {
|
|
245
|
+
const tri_offset = i * TRIANGLE_FLOAT_STRIDE;
|
|
246
|
+
|
|
247
|
+
const ax = triangle_buffer[tri_offset ];
|
|
248
|
+
const ay = triangle_buffer[tri_offset + 1];
|
|
249
|
+
const az = triangle_buffer[tri_offset + 2];
|
|
250
|
+
const bx = triangle_buffer[tri_offset + 3];
|
|
251
|
+
const by = triangle_buffer[tri_offset + 4];
|
|
252
|
+
const bz = triangle_buffer[tri_offset + 5];
|
|
253
|
+
const cx_v = triangle_buffer[tri_offset + 6];
|
|
254
|
+
const cy_v = triangle_buffer[tri_offset + 7];
|
|
255
|
+
const cz_v = triangle_buffer[tri_offset + 8];
|
|
256
|
+
|
|
257
|
+
// Face normal in body-local: (B − A) × (C − A), then normalise.
|
|
258
|
+
const e1x_l = bx - ax, e1y_l = by - ay, e1z_l = bz - az;
|
|
259
|
+
const e2x_l = cx_v - ax, e2y_l = cy_v - ay, e2z_l = cz_v - az;
|
|
260
|
+
let fnx_l = e1y_l * e2z_l - e1z_l * e2y_l;
|
|
261
|
+
let fny_l = e1z_l * e2x_l - e1x_l * e2z_l;
|
|
262
|
+
let fnz_l = e1x_l * e2y_l - e1y_l * e2x_l;
|
|
263
|
+
const fn_mag = Math.sqrt(fnx_l * fnx_l + fny_l * fny_l + fnz_l * fnz_l);
|
|
264
|
+
if (fn_mag < 1e-12) continue; // degenerate triangle
|
|
265
|
+
const fn_inv = 1 / fn_mag;
|
|
266
|
+
fnx_l *= fn_inv; fny_l *= fn_inv; fnz_l *= fn_inv;
|
|
267
|
+
|
|
268
|
+
// Rotate face normal to world via the concave body's quaternion.
|
|
269
|
+
v3_quat3_apply(scratch_rot, 0, fnx_l, fny_l, fnz_l, cqx, cqy, cqz, cqw);
|
|
270
|
+
const fnx_w = scratch_rot[0];
|
|
271
|
+
const fny_w = scratch_rot[1];
|
|
272
|
+
const fnz_w = scratch_rot[2];
|
|
273
|
+
|
|
274
|
+
// Centroid in body-local → world (one point on the triangle's plane).
|
|
275
|
+
const cent_lx = (ax + bx + cx_v) / 3;
|
|
276
|
+
const cent_ly = (ay + by + cy_v) / 3;
|
|
277
|
+
const cent_lz = (az + bz + cz_v) / 3;
|
|
278
|
+
v3_quat3_apply(scratch_rot, 0, cent_lx, cent_ly, cent_lz, cqx, cqy, cqz, cqw);
|
|
279
|
+
const cent_wx = scratch_rot[0] + c_pos_x;
|
|
280
|
+
const cent_wy = scratch_rot[1] + c_pos_y;
|
|
281
|
+
const cent_wz = scratch_rot[2] + c_pos_z;
|
|
282
|
+
|
|
283
|
+
// Deepest inward point of convex along -face_normal.
|
|
284
|
+
posed_b.support(scratch_v3, 0, -fnx_w, -fny_w, -fnz_w);
|
|
285
|
+
|
|
286
|
+
// Signed distance from that point to the triangle plane along
|
|
287
|
+
// the face normal: > 0 ⇒ convex is fully on outward side,
|
|
288
|
+
// ≤ 0 ⇒ convex extends |dist| into the solid through this face.
|
|
289
|
+
const signed_dist =
|
|
290
|
+
(scratch_v3[0] - cent_wx) * fnx_w
|
|
291
|
+
+ (scratch_v3[1] - cent_wy) * fny_w
|
|
292
|
+
+ (scratch_v3[2] - cent_wz) * fnz_w;
|
|
293
|
+
|
|
294
|
+
if (signed_dist >= 0) continue;
|
|
295
|
+
|
|
296
|
+
const depth = -signed_dist;
|
|
297
|
+
if (depth < CONTACT_EPSILON) continue;
|
|
298
|
+
|
|
299
|
+
if (depth > best_depth) {
|
|
300
|
+
best_depth = depth;
|
|
301
|
+
best_fnx_w = fnx_w;
|
|
302
|
+
best_fny_w = fny_w;
|
|
303
|
+
best_fnz_w = fnz_w;
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
if (best_depth === 0) return 0;
|
|
308
|
+
|
|
309
|
+
// ── 6. Write out_direction in the user's "B → A" convention ────
|
|
310
|
+
// - isConcaveA: original A = concave. "B → A" = convex → concave
|
|
311
|
+
// = INTO the solid = −face_normal.
|
|
312
|
+
// - isConcaveB: original A = convex. "B → A" = concave → convex
|
|
313
|
+
// = AWAY from the solid = +face_normal.
|
|
314
|
+
if (isConcaveA) {
|
|
315
|
+
out_direction[0] = -best_fnx_w;
|
|
316
|
+
out_direction[1] = -best_fny_w;
|
|
317
|
+
out_direction[2] = -best_fnz_w;
|
|
318
|
+
} else {
|
|
319
|
+
out_direction[0] = best_fnx_w;
|
|
320
|
+
out_direction[1] = best_fny_w;
|
|
321
|
+
out_direction[2] = best_fnz_w;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
return best_depth;
|
|
325
|
+
}
|