@woosh/meep-engine 3.6.0 → 3.8.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/engine/graphics3/GraphicsEngine3.d.ts +10 -5
- package/src/engine/graphics3/GraphicsEngine3.d.ts.map +1 -1
- package/src/engine/graphics3/GraphicsEngine3.js +10 -5
- package/src/engine/physics/ccd/linear_sweep.d.ts +19 -4
- package/src/engine/physics/ccd/linear_sweep.d.ts.map +1 -1
- package/src/engine/physics/ccd/linear_sweep.js +214 -6
- package/src/engine/physics/ecs/PhysicsSystem.d.ts.map +1 -1
- package/src/engine/physics/ecs/PhysicsSystem.js +2277 -2266
- package/src/engine/physics/queries/shape_cast.d.ts +15 -10
- package/src/engine/physics/queries/shape_cast.d.ts.map +1 -1
- package/src/engine/physics/queries/shape_cast.js +620 -417
- package/src/shade/renderer/global_illumination/brick4/gpu/bake/brick4_bake_basic.js +2 -2
- package/src/shade/renderer/shadow/map/GPUSceneShadowmapContext.js +1 -1
|
@@ -1,417 +1,620 @@
|
|
|
1
|
-
import { assert } from "../../../core/assert.js";
|
|
2
|
-
import { bvh_query_user_data_intersects_aabb } from "../../../core/bvh2/bvh3/query/bvh_query_user_data_intersects_aabb.js";
|
|
3
|
-
import { returnTrue } from "../../../core/function/returnTrue.js";
|
|
4
|
-
import { aabb3_transform_oriented } from "../../../core/geom/3d/aabb/aabb3_transform_oriented.js";
|
|
5
|
-
import { body_id_index } from "../body/BodyStorage.js";
|
|
6
|
-
import { gjk } from "../../../core/geom/3d/gjk/gjk.js";
|
|
7
|
-
import { mpr } from "../../../core/geom/3d/gjk/mpr.js";
|
|
8
|
-
import { PosedShape3D } from "../../../core/geom/3d/shape/PosedShape3D.js";
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* Bisection halts when the search interval is below this fraction of
|
|
12
|
-
* the sweep length. With `MAX_BISECTION = 32` and a unit-length sweep
|
|
13
|
-
* the achieved precision is ≈ 1 / 2³² (well below any practical
|
|
14
|
-
* world-scale tolerance), but we cap on relative tolerance so a
|
|
15
|
-
* 10 m sweep doesn't insist on sub-micron precision.
|
|
16
|
-
* @type {number}
|
|
17
|
-
*/
|
|
18
|
-
const TOI_TOLERANCE = 1e-4;
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* Maximum bisection iterations. `log2(1 / TOI_TOLERANCE)` is enough
|
|
22
|
-
* for any practical sweep; this is the hard upper bound to prevent
|
|
23
|
-
* pathological inputs (NaN ray, etc.) from hanging the query.
|
|
24
|
-
* @type {number}
|
|
25
|
-
*/
|
|
26
|
-
const MAX_BISECTION = 32;
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* Coarse linear-search resolution for finding *any* overlap moment
|
|
30
|
-
* along the per-candidate AABB-overlap interval (NOT the full sweep —
|
|
31
|
-
* see `aabb_overlap_interval` for the analytical narrowing that runs
|
|
32
|
-
* upstream). 32 substeps over the AABB-overlap window is dense enough
|
|
33
|
-
* for any sweep where the shape's smallest extent is on the order of
|
|
34
|
-
* its AABB's extent (true for spheres / boxes / capsules); the slab
|
|
35
|
-
* intersection keeps that interval small even for kilometre-scale
|
|
36
|
-
* sweeps, so this isn't the limit it would otherwise be.
|
|
37
|
-
* @type {number}
|
|
38
|
-
*/
|
|
39
|
-
const COARSE_STEPS = 32;
|
|
40
|
-
|
|
41
|
-
const swept_aabb = new Float64Array(6);
|
|
42
|
-
const local_aabb = new Float64Array(6);
|
|
43
|
-
const initial_aabb = new Float64Array(6);
|
|
44
|
-
const target_local_aabb = new Float64Array(6);
|
|
45
|
-
const target_world_aabb = new Float64Array(6);
|
|
46
|
-
|
|
47
|
-
// GJK simplex buffer (used to detect overlap during the bisection).
|
|
48
|
-
const simplex_buf = new Float64Array(12);
|
|
49
|
-
|
|
50
|
-
// Scratch for MPR's MTV output — same vec3 convention as EPA: direction
|
|
51
|
-
// is "A's interior into B" with the magnitude being the depth. We use
|
|
52
|
-
// MPR for the contact-normal recovery at the kiss point because it
|
|
53
|
-
// converges reliably on smooth-vs-smooth supports where EPA hits its
|
|
54
|
-
// iteration cap before tightening (sphere-vs-sphere shallow overlap).
|
|
55
|
-
const mpr_result = new Float64Array(3);
|
|
56
|
-
|
|
57
|
-
/**
|
|
58
|
-
* Long-lived PosedShape3D adapters for the swept shape and the
|
|
59
|
-
* candidate's collider. Both are re-bound per query — the swept one
|
|
60
|
-
* also gets its position field re-written every bisection step
|
|
61
|
-
* without re-running `setup()`, since shape and rotation are
|
|
62
|
-
* constant through the sweep.
|
|
63
|
-
*/
|
|
64
|
-
const swept_posed = new PosedShape3D();
|
|
65
|
-
const candidate_posed = new PosedShape3D();
|
|
66
|
-
|
|
67
|
-
/**
|
|
68
|
-
* Buffer for the union of static-BVH and dynamic-BVH candidates that
|
|
69
|
-
* overlap the swept AABB. Grows by doubling on the rare overflow.
|
|
70
|
-
* @type {Uint32Array}
|
|
71
|
-
*/
|
|
72
|
-
let scratch_candidates = new Uint32Array(64);
|
|
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
|
-
*
|
|
121
|
-
*
|
|
122
|
-
*
|
|
123
|
-
*
|
|
124
|
-
*
|
|
125
|
-
*
|
|
126
|
-
*
|
|
127
|
-
*
|
|
128
|
-
*
|
|
129
|
-
*
|
|
130
|
-
*
|
|
131
|
-
*
|
|
132
|
-
*
|
|
133
|
-
*
|
|
134
|
-
* @returns {boolean}
|
|
135
|
-
*/
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
if (
|
|
151
|
-
|
|
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
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
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
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
//
|
|
357
|
-
//
|
|
358
|
-
//
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
//
|
|
370
|
-
//
|
|
371
|
-
//
|
|
372
|
-
// `
|
|
373
|
-
//
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
1
|
+
import { assert } from "../../../core/assert.js";
|
|
2
|
+
import { bvh_query_user_data_intersects_aabb } from "../../../core/bvh2/bvh3/query/bvh_query_user_data_intersects_aabb.js";
|
|
3
|
+
import { returnTrue } from "../../../core/function/returnTrue.js";
|
|
4
|
+
import { aabb3_transform_oriented } from "../../../core/geom/3d/aabb/aabb3_transform_oriented.js";
|
|
5
|
+
import { body_id_index } from "../body/BodyStorage.js";
|
|
6
|
+
import { gjk } from "../../../core/geom/3d/gjk/gjk.js";
|
|
7
|
+
import { mpr } from "../../../core/geom/3d/gjk/mpr.js";
|
|
8
|
+
import { PosedShape3D } from "../../../core/geom/3d/shape/PosedShape3D.js";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Bisection halts when the search interval is below this fraction of
|
|
12
|
+
* the sweep length. With `MAX_BISECTION = 32` and a unit-length sweep
|
|
13
|
+
* the achieved precision is ≈ 1 / 2³² (well below any practical
|
|
14
|
+
* world-scale tolerance), but we cap on relative tolerance so a
|
|
15
|
+
* 10 m sweep doesn't insist on sub-micron precision.
|
|
16
|
+
* @type {number}
|
|
17
|
+
*/
|
|
18
|
+
const TOI_TOLERANCE = 1e-4;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Maximum bisection iterations. `log2(1 / TOI_TOLERANCE)` is enough
|
|
22
|
+
* for any practical sweep; this is the hard upper bound to prevent
|
|
23
|
+
* pathological inputs (NaN ray, etc.) from hanging the query.
|
|
24
|
+
* @type {number}
|
|
25
|
+
*/
|
|
26
|
+
const MAX_BISECTION = 32;
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Coarse linear-search resolution for finding *any* overlap moment
|
|
30
|
+
* along the per-candidate AABB-overlap interval (NOT the full sweep —
|
|
31
|
+
* see `aabb_overlap_interval` for the analytical narrowing that runs
|
|
32
|
+
* upstream). 32 substeps over the AABB-overlap window is dense enough
|
|
33
|
+
* for any sweep where the shape's smallest extent is on the order of
|
|
34
|
+
* its AABB's extent (true for spheres / boxes / capsules); the slab
|
|
35
|
+
* intersection keeps that interval small even for kilometre-scale
|
|
36
|
+
* sweeps, so this isn't the limit it would otherwise be.
|
|
37
|
+
* @type {number}
|
|
38
|
+
*/
|
|
39
|
+
const COARSE_STEPS = 32;
|
|
40
|
+
|
|
41
|
+
const swept_aabb = new Float64Array(6);
|
|
42
|
+
const local_aabb = new Float64Array(6);
|
|
43
|
+
const initial_aabb = new Float64Array(6);
|
|
44
|
+
const target_local_aabb = new Float64Array(6);
|
|
45
|
+
const target_world_aabb = new Float64Array(6);
|
|
46
|
+
|
|
47
|
+
// GJK simplex buffer (used to detect overlap during the bisection).
|
|
48
|
+
const simplex_buf = new Float64Array(12);
|
|
49
|
+
|
|
50
|
+
// Scratch for MPR's MTV output — same vec3 convention as EPA: direction
|
|
51
|
+
// is "A's interior into B" with the magnitude being the depth. We use
|
|
52
|
+
// MPR for the contact-normal recovery at the kiss point because it
|
|
53
|
+
// converges reliably on smooth-vs-smooth supports where EPA hits its
|
|
54
|
+
// iteration cap before tightening (sphere-vs-sphere shallow overlap).
|
|
55
|
+
const mpr_result = new Float64Array(3);
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Long-lived PosedShape3D adapters for the swept shape and the
|
|
59
|
+
* candidate's collider. Both are re-bound per query — the swept one
|
|
60
|
+
* also gets its position field re-written every bisection step
|
|
61
|
+
* without re-running `setup()`, since shape and rotation are
|
|
62
|
+
* constant through the sweep.
|
|
63
|
+
*/
|
|
64
|
+
const swept_posed = new PosedShape3D();
|
|
65
|
+
const candidate_posed = new PosedShape3D();
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Buffer for the union of static-BVH and dynamic-BVH candidates that
|
|
69
|
+
* overlap the swept AABB. Grows by doubling on the rare overflow.
|
|
70
|
+
* @type {Uint32Array}
|
|
71
|
+
*/
|
|
72
|
+
let scratch_candidates = new Uint32Array(64);
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Depths at or below this are not a contact. A kissing pair is not always
|
|
76
|
+
* numerically exact — MPR returns a few microns of "overlap" for a sphere
|
|
77
|
+
* resting on a plane where a box-on-box kiss returns a clean zero — so the
|
|
78
|
+
* tangency test needs the same tolerance the narrowphase already applies
|
|
79
|
+
* (`compute_penetration`'s CONTACT_EPSILON, same value and same reasoning).
|
|
80
|
+
* Well below any real overlap a caller would be asked to depenetrate from,
|
|
81
|
+
* and far below the skin widths character controllers work at.
|
|
82
|
+
* @type {number}
|
|
83
|
+
*/
|
|
84
|
+
const CONTACT_EPSILON = 1e-4;
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* How far from parallel a sweep must be before it counts as driving INTO a
|
|
88
|
+
* tangent contact rather than sliding along it — a sine, both inputs being
|
|
89
|
+
* unit vectors, so this is ~0.006°. Comfortably above the angular noise in an
|
|
90
|
+
* MPR-recovered normal and far below any inclination that would move a body
|
|
91
|
+
* meaningfully into a surface over one sweep.
|
|
92
|
+
* @type {number}
|
|
93
|
+
*/
|
|
94
|
+
const GRAZING_SINE_EPSILON = 1e-4;
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Distance past the reported TOI at which the contact normal is sampled.
|
|
98
|
+
* Bisection stops on the just-SEPARATING side, so MPR needs the pair
|
|
99
|
+
* nudged into genuine overlap before it has an MTV to report.
|
|
100
|
+
* @type {number}
|
|
101
|
+
*/
|
|
102
|
+
const NORMAL_PROBE_OFFSET = 0.01;
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Unit contact normal recovered by {@link probe_contact_normal}.
|
|
106
|
+
* @type {Float64Array}
|
|
107
|
+
*/
|
|
108
|
+
const probe_normal = new Float64Array(3);
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Recover the target's outward surface normal by running MPR on
|
|
112
|
+
* `swept_posed` displaced to `(px, py, pz)` against `candidate_posed`
|
|
113
|
+
* (both must already be bound by the caller).
|
|
114
|
+
*
|
|
115
|
+
* Returns `false` — leaving `out` untouched — when MPR reports no depth worth
|
|
116
|
+
* the name (at or below {@link CONTACT_EPSILON}). That is not an MPR failure
|
|
117
|
+
* so much as the honest answer for a pair that is TANGENT at the probe pose:
|
|
118
|
+
* a kiss carries no direction, so there is no normal to read off it. Callers
|
|
119
|
+
* use that verdict both ways — as "no normal here" and as "this is a touch,
|
|
120
|
+
* not an overlap".
|
|
121
|
+
*
|
|
122
|
+
* Sign-validation: MPR's polytope can settle on either side of the origin
|
|
123
|
+
* for highly symmetric configurations, so we dot against the body-centre
|
|
124
|
+
* axis and flip if needed (same pattern as `narrowphase_step`'s EPA path).
|
|
125
|
+
* The stored normal is the target's outward surface normal — `B → A`
|
|
126
|
+
* direction — i.e. the negated (validated) MTV.
|
|
127
|
+
*
|
|
128
|
+
* @param {Float64Array} out receives the unit normal on success
|
|
129
|
+
* @param {number} px swept shape's probe centre
|
|
130
|
+
* @param {number} py
|
|
131
|
+
* @param {number} pz
|
|
132
|
+
* @param {Transform} target_tr the target body's transform (centre for the
|
|
133
|
+
* sign check)
|
|
134
|
+
* @returns {boolean}
|
|
135
|
+
*/
|
|
136
|
+
function probe_contact_normal(out, px, py, pz, target_tr) {
|
|
137
|
+
swept_posed.px = px;
|
|
138
|
+
swept_posed.py = py;
|
|
139
|
+
swept_posed.pz = pz;
|
|
140
|
+
|
|
141
|
+
if (!mpr(mpr_result, 0, swept_posed, candidate_posed)) return false;
|
|
142
|
+
|
|
143
|
+
let ex = mpr_result[0], ey = mpr_result[1], ez = mpr_result[2];
|
|
144
|
+
const depth = Math.sqrt(ex * ex + ey * ey + ez * ez);
|
|
145
|
+
if (!(depth > CONTACT_EPSILON) || !Number.isFinite(depth)) return false;
|
|
146
|
+
|
|
147
|
+
const ab_x = target_tr.position.x - px;
|
|
148
|
+
const ab_y = target_tr.position.y - py;
|
|
149
|
+
const ab_z = target_tr.position.z - pz;
|
|
150
|
+
if (ex * ab_x + ey * ab_y + ez * ab_z < 0) {
|
|
151
|
+
ex = -ex; ey = -ey; ez = -ez;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const inv = 1 / depth;
|
|
155
|
+
out[0] = -ex * inv;
|
|
156
|
+
out[1] = -ey * inv;
|
|
157
|
+
out[2] = -ez * inv;
|
|
158
|
+
return true;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Recover the target's outward surface normal for a contact whose pose is
|
|
163
|
+
* `(cx, cy, cz)` — the swept shape's centre AT the contact — sweeping along
|
|
164
|
+
* unit `(dx, dy, dz)`. `swept_posed` (shape + rotation) and `candidate_posed`
|
|
165
|
+
* must already be bound by the caller; on success the unit normal is written
|
|
166
|
+
* to `out`.
|
|
167
|
+
*
|
|
168
|
+
* MPR needs genuine overlap before it has an MTV to report, and bisection
|
|
169
|
+
* stops on the just-SEPARATING side, so neither the contact pose itself nor
|
|
170
|
+
* the reported `t` is a usable sample point. Two probes displace it:
|
|
171
|
+
*
|
|
172
|
+
* Probe 1 steps `NORMAL_PROBE_OFFSET` ALONG the sweep. That is the whole
|
|
173
|
+
* answer wherever the sweep runs INTO the surface.
|
|
174
|
+
*
|
|
175
|
+
* Probe 2 covers what probe 1 structurally cannot. A GRAZING contact — a body
|
|
176
|
+
* merely touching geometry it is sliding past, level brushes flush against a
|
|
177
|
+
* character, a seam between coplanar faces — has the sweep running PARALLEL to
|
|
178
|
+
* the contact plane, so advancing along it leaves the pair exactly tangent
|
|
179
|
+
* however far it goes. A sweep pointing AWAY from the surface is worse still:
|
|
180
|
+
* probe 1 separates them further. Either way MPR reports zero depth, and zero
|
|
181
|
+
* depth carries no direction.
|
|
182
|
+
*
|
|
183
|
+
* So probe 2 displaces toward the target's CENTRE, which is interior to it
|
|
184
|
+
* (MPR's own precondition) and so closes on it from any contact orientation.
|
|
185
|
+
* It starts from the CONTACT pose rather than probe 1's — anchoring it at
|
|
186
|
+
* probe 1's pose lets a sweep directly away from the surface cancel the two
|
|
187
|
+
* displacements against each other and land back on the tangency it was trying
|
|
188
|
+
* to escape.
|
|
189
|
+
*
|
|
190
|
+
* The distance DOUBLES until the overlap is measurable, because one fixed
|
|
191
|
+
* offset cannot serve both ends of the range. Only the component along the
|
|
192
|
+
* (unknown) contact normal deepens the overlap, and that component shrinks
|
|
193
|
+
* with the target's size: 1 cm toward the centre of a 1 m brush sinks ~1 cm,
|
|
194
|
+
* but toward the centre of a 1 km floor 400 m away it sinks 30 µm — under
|
|
195
|
+
* {@link CONTACT_EPSILON}, and the normal would be lost on exactly the large
|
|
196
|
+
* flat geometry a character spends its life on. Growing from the smallest
|
|
197
|
+
* offset keeps the displacement as shallow as it can be, which is what stops a
|
|
198
|
+
* target thinner than the probe from reporting its FAR face.
|
|
199
|
+
*
|
|
200
|
+
* Getting this right matters more than precision: the alternative is the
|
|
201
|
+
* `-ray.direction` fallback, which opposes the sweep BY CONSTRUCTION. A caller
|
|
202
|
+
* testing `direction · normal < 0` to ask "is this surface in my way?" is then
|
|
203
|
+
* told yes for every direction it could possibly sweep, including straight
|
|
204
|
+
* down a wall it is only brushing — a kinematic character in that state has no
|
|
205
|
+
* escape (see `KinematicMover._slide`).
|
|
206
|
+
*
|
|
207
|
+
* @param {Float64Array} out receives the unit normal on success
|
|
208
|
+
* @param {number} cx swept shape's centre at the contact
|
|
209
|
+
* @param {number} cy
|
|
210
|
+
* @param {number} cz
|
|
211
|
+
* @param {number} dx unit sweep direction
|
|
212
|
+
* @param {number} dy
|
|
213
|
+
* @param {number} dz
|
|
214
|
+
* @param {Transform} target_tr the target body's transform
|
|
215
|
+
* @returns {boolean} false when every probe degenerates — `out` is untouched
|
|
216
|
+
*/
|
|
217
|
+
function recover_contact_normal(out, cx, cy, cz, dx, dy, dz, target_tr) {
|
|
218
|
+
if (probe_contact_normal(
|
|
219
|
+
out,
|
|
220
|
+
cx + dx * NORMAL_PROBE_OFFSET,
|
|
221
|
+
cy + dy * NORMAL_PROBE_OFFSET,
|
|
222
|
+
cz + dz * NORMAL_PROBE_OFFSET,
|
|
223
|
+
target_tr,
|
|
224
|
+
)) return true;
|
|
225
|
+
|
|
226
|
+
const ab_x = target_tr.position.x - cx;
|
|
227
|
+
const ab_y = target_tr.position.y - cy;
|
|
228
|
+
const ab_z = target_tr.position.z - cz;
|
|
229
|
+
const ab_len = Math.sqrt(ab_x * ab_x + ab_y * ab_y + ab_z * ab_z);
|
|
230
|
+
// Coincident centres leave no direction to close along, and the shapes are
|
|
231
|
+
// then as overlapped as they get — probe 1 already had its answer. The
|
|
232
|
+
// finite check also bounds the loop below against a degenerate pose.
|
|
233
|
+
if (!(ab_len > 0) || !Number.isFinite(ab_len)) return false;
|
|
234
|
+
|
|
235
|
+
const ux = ab_x / ab_len, uy = ab_y / ab_len, uz = ab_z / ab_len;
|
|
236
|
+
// Bounded by construction — doubling from a fixed offset reaches `ab_len`
|
|
237
|
+
// in log2(ab_len / NORMAL_PROBE_OFFSET) steps. Stopping there keeps the
|
|
238
|
+
// probe on the near side of the target's centre.
|
|
239
|
+
for (let d = NORMAL_PROBE_OFFSET; d < ab_len; d *= 2) {
|
|
240
|
+
if (probe_contact_normal(out, cx + ux * d, cy + uy * d, cz + uz * d, target_tr)) return true;
|
|
241
|
+
}
|
|
242
|
+
return false;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Sweep a convex shape along a ray and find the first body it would
|
|
247
|
+
* hit. Used by character controllers (sweep a capsule along intended
|
|
248
|
+
* movement to find blockers), high-speed projectile movement (avoid
|
|
249
|
+
* tunnelling without paying for full per-body CCD), and any kinematic
|
|
250
|
+
* "where would this body end up if I tried to move it?" query.
|
|
251
|
+
*
|
|
252
|
+
* Pipeline:
|
|
253
|
+
* 1. Build the swept AABB: the shape's local-frame AABB rotated
|
|
254
|
+
* into world by `rotation`, translated to `ray.origin`, then
|
|
255
|
+
* stretched along `ray.direction * ray.tMax`.
|
|
256
|
+
* 2. Query both broadphase BVHs (static + dynamic) for leaves whose
|
|
257
|
+
* AABB overlaps the swept volume.
|
|
258
|
+
* 3. For each candidate, bisect the [0, best_t] interval for the
|
|
259
|
+
* smallest `t` at which the swept shape overlaps the candidate's
|
|
260
|
+
* collider. GJK on each midpoint.
|
|
261
|
+
*
|
|
262
|
+
* Picking `best_t` (rather than always running the full [0, tMax]
|
|
263
|
+
* bisection) is the early-termination optimisation: once we have a
|
|
264
|
+
* hit at some t, no later candidate can produce a smaller TOI by
|
|
265
|
+
* being checked at its full [0, tMax] interval.
|
|
266
|
+
*
|
|
267
|
+
* Output on hit:
|
|
268
|
+
* - `result.t` — sweep distance to impact (in the same units as
|
|
269
|
+
* `ray.direction`'s magnitude; for a unit-direction ray, this is
|
|
270
|
+
* metres).
|
|
271
|
+
* - `result.position` — `ray.origin + t * ray.direction`, i.e. the
|
|
272
|
+
* centre of the swept shape at the moment of first contact. NOT
|
|
273
|
+
* a point on the target's surface; that would need narrowphase
|
|
274
|
+
* refinement which the broadphase-only architecture doesn't have
|
|
275
|
+
* wired up yet.
|
|
276
|
+
* - `result.normal` — target surface's outward normal at the kiss
|
|
277
|
+
* point (unit length), recovered by running MPR against the
|
|
278
|
+
* just-overlapping configuration at the TOI. A pair that is exactly
|
|
279
|
+
* tangent there carries no MTV to read, so a second probe nudged
|
|
280
|
+
* toward the target's centre supplies one; `-ray.direction` remains
|
|
281
|
+
* the last-resort fallback if both degenerate. See the
|
|
282
|
+
* contact-normal recovery block near the end of the function.
|
|
283
|
+
* - `result.entity` / `result.body_id` — the hit body.
|
|
284
|
+
*
|
|
285
|
+
* Output on miss: untouched; same convention as `raycast`.
|
|
286
|
+
*
|
|
287
|
+
* @param {PhysicsSystem} system
|
|
288
|
+
* @param {Ray3|{origin_x:number,origin_y:number,origin_z:number,direction_x:number,direction_y:number,direction_z:number,tMax:number}} ray
|
|
289
|
+
* origin + unit direction + `tMax`. Only the named accessors are read, so a
|
|
290
|
+
* plain double-precision ray-like works — `Ray3` is Float32Array-backed and
|
|
291
|
+
* quantises large-coordinate sweeps (the CCD pass passes a f64 ray-like)
|
|
292
|
+
* @param {AbstractShape3D} shape shape being swept, in its local frame
|
|
293
|
+
* @param {{x:number,y:number,z:number,w:number}} rotation fixed orientation
|
|
294
|
+
* @param {PhysicsSurfacePoint} result populated on hit; untouched on miss
|
|
295
|
+
* @param {(entity:number, collider:Collider, body_id:number)=>boolean} [filter]
|
|
296
|
+
* mandatory in contract; defaults to {@link returnTrue}. Receives the
|
|
297
|
+
* candidate body's entity, primary collider, and packed body id (the id
|
|
298
|
+
* distinguishes bodies when colliders live on child entities)
|
|
299
|
+
* @param {boolean} [skip_initial_overlaps=false] how a candidate ALREADY
|
|
300
|
+
* overlapping the swept shape at `t = 0` is treated. `false` (kinematic
|
|
301
|
+
* "can I move?" semantics): targets are solid, so the cast is blocked where
|
|
302
|
+
* it stands — report a hit at `t = 0`. The one exception is a convex target
|
|
303
|
+
* the shape merely TOUCHES (no depth beyond {@link CONTACT_EPSILON}): a
|
|
304
|
+
* tangency blocks only a sweep driving into it, and otherwise steps aside so
|
|
305
|
+
* the scan can reach real blockers. `true` (CCD semantics): such a candidate
|
|
306
|
+
* is a resting / sliding contact the discrete solver already owns, NOT a
|
|
307
|
+
* tunnelling threat — skip it and keep scanning the remaining candidates for
|
|
308
|
+
* real blockers along the sweep. Either way, one contact at the start of the
|
|
309
|
+
* sweep must never blind the cast to every other obstacle.
|
|
310
|
+
* @returns {boolean}
|
|
311
|
+
*/
|
|
312
|
+
export function shape_cast(system, ray, shape, rotation, result, filter = returnTrue, skip_initial_overlaps = false) {
|
|
313
|
+
const ox = ray.origin_x;
|
|
314
|
+
const oy = ray.origin_y;
|
|
315
|
+
const oz = ray.origin_z;
|
|
316
|
+
const dx = ray.direction_x;
|
|
317
|
+
const dy = ray.direction_y;
|
|
318
|
+
const dz = ray.direction_z;
|
|
319
|
+
const tMax = ray.tMax;
|
|
320
|
+
// A fresh Ray3 defaults tMax to Infinity, which a sweep cannot use (the
|
|
321
|
+
// swept AABB and the bisection interval would be unbounded). Loud in dev
|
|
322
|
+
// — silently returning false for the DEFAULT ray construction reads as
|
|
323
|
+
// "nothing there" and is a debugging trap; prod degrades to a miss.
|
|
324
|
+
assert.ok(Number.isFinite(tMax) && tMax > 0,
|
|
325
|
+
`shape_cast: ray.tMax must be a positive finite sweep length, got ${tMax}`);
|
|
326
|
+
if (!(tMax > 0) || !Number.isFinite(tMax)) return false;
|
|
327
|
+
|
|
328
|
+
const rx = rotation.x, ry = rotation.y, rz = rotation.z, rw = rotation.w;
|
|
329
|
+
|
|
330
|
+
// ── 1. Swept AABB ──────────────────────────────────────────────
|
|
331
|
+
shape.compute_bounding_box(local_aabb);
|
|
332
|
+
// Rotation around ray.origin gives the shape's initial AABB in
|
|
333
|
+
// world space.
|
|
334
|
+
aabb3_transform_oriented(
|
|
335
|
+
initial_aabb, 0,
|
|
336
|
+
local_aabb[0], local_aabb[1], local_aabb[2],
|
|
337
|
+
local_aabb[3], local_aabb[4], local_aabb[5],
|
|
338
|
+
ox, oy, oz,
|
|
339
|
+
rx, ry, rz, rw
|
|
340
|
+
);
|
|
341
|
+
// Stretch by the sweep displacement. Positive direction components
|
|
342
|
+
// push the +face out; negative components push the −face out.
|
|
343
|
+
swept_aabb[0] = initial_aabb[0];
|
|
344
|
+
swept_aabb[1] = initial_aabb[1];
|
|
345
|
+
swept_aabb[2] = initial_aabb[2];
|
|
346
|
+
swept_aabb[3] = initial_aabb[3];
|
|
347
|
+
swept_aabb[4] = initial_aabb[4];
|
|
348
|
+
swept_aabb[5] = initial_aabb[5];
|
|
349
|
+
if (dx > 0) swept_aabb[3] += dx * tMax; else swept_aabb[0] += dx * tMax;
|
|
350
|
+
if (dy > 0) swept_aabb[4] += dy * tMax; else swept_aabb[1] += dy * tMax;
|
|
351
|
+
if (dz > 0) swept_aabb[5] += dz * tMax; else swept_aabb[2] += dz * tMax;
|
|
352
|
+
|
|
353
|
+
// ── 2. Broadphase: gather candidates from both trees ───────────
|
|
354
|
+
// The BVH query returns the TRUE overlap count and silently drops writes past
|
|
355
|
+
// the buffer end. An undersized buffer would make the narrowphase loop read
|
|
356
|
+
// `undefined` candidates and silently MISS the true nearest hit (tunnelling on
|
|
357
|
+
// a long sweep through a dense scene). Grow by doubling and re-query until
|
|
358
|
+
// both fit (deterministic queries → converges in one resize).
|
|
359
|
+
let n_static, n_dynamic, n_total;
|
|
360
|
+
for (;;) {
|
|
361
|
+
n_static = bvh_query_user_data_intersects_aabb(scratch_candidates, 0, system.staticBvh, swept_aabb);
|
|
362
|
+
n_dynamic = bvh_query_user_data_intersects_aabb(scratch_candidates, n_static, system.dynamicBvh, swept_aabb);
|
|
363
|
+
n_total = n_static + n_dynamic;
|
|
364
|
+
if (n_total <= scratch_candidates.length) break;
|
|
365
|
+
scratch_candidates = new Uint32Array(Math.max(n_total, scratch_candidates.length * 2));
|
|
366
|
+
}
|
|
367
|
+
if (n_total === 0) return false;
|
|
368
|
+
|
|
369
|
+
// ── 3. Narrowphase: per-candidate bisection ────────────────────
|
|
370
|
+
//
|
|
371
|
+
// Re-bind the swept shape's PosedShape3D once for shape + rotation;
|
|
372
|
+
// we update `px/py/pz` per bisection midpoint without rerunning
|
|
373
|
+
// setup (px/py/pz are public fields by contract).
|
|
374
|
+
swept_posed.shape = shape;
|
|
375
|
+
swept_posed.qx = rx; swept_posed.qy = ry; swept_posed.qz = rz; swept_posed.qw = rw;
|
|
376
|
+
|
|
377
|
+
let best_t = tMax;
|
|
378
|
+
let best_body_id = -1;
|
|
379
|
+
let best_entity = -1;
|
|
380
|
+
|
|
381
|
+
for (let i = 0; i < n_total; i++) {
|
|
382
|
+
const body_id = scratch_candidates[i];
|
|
383
|
+
const body_idx = body_id_index(body_id);
|
|
384
|
+
const entity = system.entityOf(body_id);
|
|
385
|
+
if (entity < 0) continue;
|
|
386
|
+
|
|
387
|
+
const collider = system.__primary_collider(body_idx);
|
|
388
|
+
if (collider === null) continue;
|
|
389
|
+
if (!filter(entity, collider, body_id)) continue;
|
|
390
|
+
|
|
391
|
+
const target_tr = system.__transforms[body_idx];
|
|
392
|
+
candidate_posed.setup(collider.shape, target_tr.position, target_tr.rotation);
|
|
393
|
+
|
|
394
|
+
// Already overlapping at t = 0? The bisection's "no overlap at lo,
|
|
395
|
+
// overlap at hi" invariant doesn't hold for this candidate. Under CCD
|
|
396
|
+
// semantics it is a resting contact to step over — skip THIS candidate
|
|
397
|
+
// only, and keep scanning for real blockers.
|
|
398
|
+
//
|
|
399
|
+
// Under kinematic semantics targets are SOLID: a shape that starts
|
|
400
|
+
// genuinely inside one is blocked where it stands, `t = 0`, whatever
|
|
401
|
+
// direction it is trying to sweep. Nothing can beat that, so stop
|
|
402
|
+
// scanning. Getting out is depenetration's job, not the sweep's.
|
|
403
|
+
//
|
|
404
|
+
// Exact TANGENCY is the exception, and it needs one. A zero-depth
|
|
405
|
+
// touch is not "inside" anything — the shape is resting against a
|
|
406
|
+
// face, or flush with a brush seam, and is free to slide along it or
|
|
407
|
+
// leave. It also survives depenetration, which has no depth to push
|
|
408
|
+
// out of, so it persists frame after frame.
|
|
409
|
+
//
|
|
410
|
+
// Reporting it as a `t = 0` hit ends the scan, and that is the whole
|
|
411
|
+
// problem: the caller is handed a contact it correctly judges harmless
|
|
412
|
+
// for this direction and concludes the sweep is clear, while the body
|
|
413
|
+
// it was actually about to hit was never tested. A character flush
|
|
414
|
+
// against a wall falls through the floor that way. So a tangency
|
|
415
|
+
// blocks only when the sweep drives INTO it, and otherwise steps aside
|
|
416
|
+
// and lets the scan continue.
|
|
417
|
+
//
|
|
418
|
+
// MPR's depth at the START pose is what separates the two: positive
|
|
419
|
+
// means genuine overlap, none means tangency. Tangency is also exactly
|
|
420
|
+
// where the normal is hardest to recover, hence the second probe; if
|
|
421
|
+
// even that degenerates there is no normal to judge with and the
|
|
422
|
+
// contact blocks, stopping short being the safe failure.
|
|
423
|
+
swept_posed.px = ox; swept_posed.py = oy; swept_posed.pz = oz;
|
|
424
|
+
if (gjk(simplex_buf, swept_posed, candidate_posed)) {
|
|
425
|
+
if (skip_initial_overlaps) continue;
|
|
426
|
+
|
|
427
|
+
// The tangency exception reads MPR's depth and normal, both of
|
|
428
|
+
// which assume a convex target. This query has never supported
|
|
429
|
+
// concave ones (`overlap_shape` decomposes them per-triangle;
|
|
430
|
+
// nothing here does), so rather than extend a convex-only
|
|
431
|
+
// inference to geometry the module cannot see properly, leave
|
|
432
|
+
// those on the unconditional "solid, blocked at t = 0" path.
|
|
433
|
+
const convex_target = collider.shape.is_convex !== false;
|
|
434
|
+
if (convex_target && !probe_contact_normal(probe_normal, ox, oy, oz, target_tr)) {
|
|
435
|
+
const known = recover_contact_normal(
|
|
436
|
+
probe_normal, ox, oy, oz, dx, dy, dz, target_tr,
|
|
437
|
+
);
|
|
438
|
+
if (known) {
|
|
439
|
+
// Both vectors are unit, so this is the sine of the angle
|
|
440
|
+
// between the sweep and the contact plane. Zero means
|
|
441
|
+
// exactly parallel — sliding along the face — and positive
|
|
442
|
+
// means leaving it; neither blocks. The tolerance matters
|
|
443
|
+
// because the parallel case sits right on the sign change,
|
|
444
|
+
// and an MPR-recovered normal carries ~1e-6 of angular
|
|
445
|
+
// noise: without it a sweep ALONG a face flips to
|
|
446
|
+
// "head-on" on solver noise alone.
|
|
447
|
+
const into = dx * probe_normal[0] + dy * probe_normal[1] + dz * probe_normal[2];
|
|
448
|
+
if (into > -GRAZING_SINE_EPSILON) continue;
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
best_t = 0;
|
|
453
|
+
best_body_id = body_id;
|
|
454
|
+
best_entity = entity;
|
|
455
|
+
break;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
// ── Slab intersection: narrow the search interval analytically ─
|
|
459
|
+
//
|
|
460
|
+
// Compute the body's tight world AABB and intersect it with the
|
|
461
|
+
// swept shape's AABB sliding along the sweep direction. The
|
|
462
|
+
// result is the t-interval `[t_aabb_enter, t_aabb_exit]` during
|
|
463
|
+
// which the two AABBs overlap. Outside this interval the actual
|
|
464
|
+
// shapes can't overlap either (AABB is a superset of the
|
|
465
|
+
// shape), so we get to skip all of [0, t_aabb_enter) and
|
|
466
|
+
// (t_aabb_exit, best_t] without testing.
|
|
467
|
+
//
|
|
468
|
+
// This is the key to making `shapeCast` scale to long sweeps:
|
|
469
|
+
// a 1 km cast of a 1 m sphere against another 1 m sphere has an
|
|
470
|
+
// AABB-overlap window of ≈ 2 m (the sum of diameters along the
|
|
471
|
+
// sweep axis), reducing the coarse-step grid from a 30 m
|
|
472
|
+
// resolution (1000/32) to 6 cm (2/32).
|
|
473
|
+
collider.shape.compute_bounding_box(target_local_aabb);
|
|
474
|
+
aabb3_transform_oriented(
|
|
475
|
+
target_world_aabb, 0,
|
|
476
|
+
target_local_aabb[0], target_local_aabb[1], target_local_aabb[2],
|
|
477
|
+
target_local_aabb[3], target_local_aabb[4], target_local_aabb[5],
|
|
478
|
+
target_tr.position.x, target_tr.position.y, target_tr.position.z,
|
|
479
|
+
target_tr.rotation.x, target_tr.rotation.y, target_tr.rotation.z, target_tr.rotation.w
|
|
480
|
+
);
|
|
481
|
+
|
|
482
|
+
let t_aabb_enter = -Infinity;
|
|
483
|
+
let t_aabb_exit = Infinity;
|
|
484
|
+
let skip_candidate = false;
|
|
485
|
+
for (let ax = 0; ax < 3; ax++) {
|
|
486
|
+
const init_min = initial_aabb[ax];
|
|
487
|
+
const init_max = initial_aabb[ax + 3];
|
|
488
|
+
const body_min = target_world_aabb[ax];
|
|
489
|
+
const body_max = target_world_aabb[ax + 3];
|
|
490
|
+
const d_ax = ax === 0 ? dx : (ax === 1 ? dy : dz);
|
|
491
|
+
if (d_ax === 0) {
|
|
492
|
+
// Static along this axis: AABBs must overlap in their
|
|
493
|
+
// initial position or this candidate is unreachable.
|
|
494
|
+
if (init_max < body_min || init_min > body_max) {
|
|
495
|
+
skip_candidate = true;
|
|
496
|
+
break;
|
|
497
|
+
}
|
|
498
|
+
continue;
|
|
499
|
+
}
|
|
500
|
+
// swept_min(t) = init_min + d·t, swept_max(t) = init_max + d·t.
|
|
501
|
+
// Slab-enter: swept_max(t) = body_min → t = (body_min − init_max) / d
|
|
502
|
+
// Slab-exit: swept_min(t) = body_max → t = (body_max − init_min) / d
|
|
503
|
+
const t1 = (body_min - init_max) / d_ax;
|
|
504
|
+
const t2 = (body_max - init_min) / d_ax;
|
|
505
|
+
const lo = t1 < t2 ? t1 : t2;
|
|
506
|
+
const hi = t1 < t2 ? t2 : t1;
|
|
507
|
+
if (lo > t_aabb_enter) t_aabb_enter = lo;
|
|
508
|
+
if (hi < t_aabb_exit) t_aabb_exit = hi;
|
|
509
|
+
}
|
|
510
|
+
if (skip_candidate) continue;
|
|
511
|
+
if (t_aabb_enter > t_aabb_exit) continue;
|
|
512
|
+
|
|
513
|
+
// Clamp to the relevant t-window: [0, best_t]. Anything beyond
|
|
514
|
+
// best_t can't tighten the answer.
|
|
515
|
+
const t_start = t_aabb_enter > 0 ? t_aabb_enter : 0;
|
|
516
|
+
const t_end = t_aabb_exit < best_t ? t_aabb_exit : best_t;
|
|
517
|
+
if (t_start >= t_end) continue;
|
|
518
|
+
|
|
519
|
+
// Coarse-search within the narrowed interval to find any t for
|
|
520
|
+
// which the shapes overlap (their AABBs do by construction; the
|
|
521
|
+
// shapes themselves may not, e.g. two spheres passing alongside).
|
|
522
|
+
const step_dt = (t_end - t_start) / COARSE_STEPS;
|
|
523
|
+
let t_prev = t_start;
|
|
524
|
+
let t_hi = -1;
|
|
525
|
+
for (let s = 1; s <= COARSE_STEPS; s++) {
|
|
526
|
+
const t = t_start + s * step_dt;
|
|
527
|
+
swept_posed.px = ox + dx * t;
|
|
528
|
+
swept_posed.py = oy + dy * t;
|
|
529
|
+
swept_posed.pz = oz + dz * t;
|
|
530
|
+
if (gjk(simplex_buf, swept_posed, candidate_posed)) {
|
|
531
|
+
t_hi = t;
|
|
532
|
+
break;
|
|
533
|
+
}
|
|
534
|
+
t_prev = t;
|
|
535
|
+
}
|
|
536
|
+
if (t_hi < 0) continue;
|
|
537
|
+
|
|
538
|
+
// Bisect [t_prev, t_hi]: invariant is `gjk(t_lo) === false` and
|
|
539
|
+
// `gjk(t_hi) === true`.
|
|
540
|
+
let t_lo = t_prev;
|
|
541
|
+
const stop_width = TOI_TOLERANCE * tMax;
|
|
542
|
+
for (let bi = 0; bi < MAX_BISECTION && (t_hi - t_lo) > stop_width; bi++) {
|
|
543
|
+
const t_mid = (t_lo + t_hi) * 0.5;
|
|
544
|
+
swept_posed.px = ox + dx * t_mid;
|
|
545
|
+
swept_posed.py = oy + dy * t_mid;
|
|
546
|
+
swept_posed.pz = oz + dz * t_mid;
|
|
547
|
+
if (gjk(simplex_buf, swept_posed, candidate_posed)) {
|
|
548
|
+
t_hi = t_mid;
|
|
549
|
+
} else {
|
|
550
|
+
t_lo = t_mid;
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
// Use t_lo (the just-SEPARATING side of the bisection) rather
|
|
555
|
+
// than t_hi (just-overlapping). With the fixed GJK that no
|
|
556
|
+
// longer flags 5 mm-clear gaps as overlap, the bisection is
|
|
557
|
+
// tight; reporting t_lo gives kinematic callers a conservative
|
|
558
|
+
// TOI — the swept shape at the reported moment is provably NOT
|
|
559
|
+
// overlapping the target. The normal-probe below steps a few
|
|
560
|
+
// mm past best_t to get a valid simplex for MPR.
|
|
561
|
+
if (t_lo < best_t) {
|
|
562
|
+
best_t = t_lo;
|
|
563
|
+
best_body_id = body_id;
|
|
564
|
+
best_entity = entity;
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
if (best_body_id === -1) return false;
|
|
569
|
+
|
|
570
|
+
const rp = result.position;
|
|
571
|
+
const rn = result.normal;
|
|
572
|
+
|
|
573
|
+
const swept_x = ox + dx * best_t;
|
|
574
|
+
const swept_y = oy + dy * best_t;
|
|
575
|
+
const swept_z = oz + dz * best_t;
|
|
576
|
+
|
|
577
|
+
rp[0] = swept_x;
|
|
578
|
+
rp[1] = swept_y;
|
|
579
|
+
rp[2] = swept_z;
|
|
580
|
+
|
|
581
|
+
// ── Contact-normal recovery via MPR ────────────────────────────
|
|
582
|
+
//
|
|
583
|
+
// Bisection converges with `swept_posed` somewhere within
|
|
584
|
+
// `stop_width = TOI_TOLERANCE * tMax` of the true contact. At
|
|
585
|
+
// exactly `best_t` the overlap depth is therefore sub-millimetre
|
|
586
|
+
// on practical sweeps. EPA on smooth-vs-smooth supports at this
|
|
587
|
+
// overlap can't tighten before its iteration cap and returns a
|
|
588
|
+
// direction off by ~30° from truth. MPR is the better tool here:
|
|
589
|
+
// it converges in 5-15 iterations on smooth surfaces, same MTV
|
|
590
|
+
// output convention as EPA (direction = A's interior into B,
|
|
591
|
+
// magnitude = depth).
|
|
592
|
+
//
|
|
593
|
+
// The probe pose is `NORMAL_PROBE_OFFSET` past `best_t` along the sweep
|
|
594
|
+
// (a few mm into the target) — bisection stops on the just-SEPARATING
|
|
595
|
+
// side, so there is no MTV to read exactly at `best_t`. The reported
|
|
596
|
+
// `result.t` stays at `best_t`; only the normal probe moves.
|
|
597
|
+
// `recover_contact_normal` handles the grazing case, where advancing
|
|
598
|
+
// along the sweep never produces overlap at all.
|
|
599
|
+
const contact_x = ox + dx * best_t;
|
|
600
|
+
const contact_y = oy + dy * best_t;
|
|
601
|
+
const contact_z = oz + dz * best_t;
|
|
602
|
+
const best_body_idx = body_id_index(best_body_id);
|
|
603
|
+
const best_collider = system.__primary_collider(best_body_idx);
|
|
604
|
+
let normal_x = -dx, normal_y = -dy, normal_z = -dz;
|
|
605
|
+
if (best_collider !== null) {
|
|
606
|
+
const best_tr = system.__transforms[best_body_idx];
|
|
607
|
+
candidate_posed.setup(best_collider.shape, best_tr.position, best_tr.rotation);
|
|
608
|
+
if (recover_contact_normal(probe_normal, contact_x, contact_y, contact_z, dx, dy, dz, best_tr)) {
|
|
609
|
+
normal_x = probe_normal[0];
|
|
610
|
+
normal_y = probe_normal[1];
|
|
611
|
+
normal_z = probe_normal[2];
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
rn[0] = normal_x; rn[1] = normal_y; rn[2] = normal_z;
|
|
616
|
+
result.t = best_t;
|
|
617
|
+
result.entity = best_entity;
|
|
618
|
+
result.body_id = best_body_id;
|
|
619
|
+
return true;
|
|
620
|
+
}
|