@woosh/meep-engine 2.153.0 → 2.154.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/ConvexHullShape3D.d.ts +112 -0
- package/src/core/geom/3d/shape/ConvexHullShape3D.d.ts.map +1 -0
- package/src/core/geom/3d/shape/ConvexHullShape3D.js +325 -0
- package/src/engine/graphics/ecs/trail2d/Trail2D.d.ts +4 -0
- package/src/engine/graphics/ecs/trail2d/Trail2D.d.ts.map +1 -1
- package/src/engine/graphics/ecs/trail2d/Trail2D.js +21 -0
- package/src/engine/physics/PLAN.md +4 -4
- package/src/engine/physics/body/BodyStorage.d.ts +3 -1
- package/src/engine/physics/body/BodyStorage.d.ts.map +1 -1
- package/src/engine/physics/body/BodyStorage.js +452 -450
- package/src/engine/physics/body/SolverBodyState.d.ts.map +1 -1
- package/src/engine/physics/body/SolverBodyState.js +6 -5
- package/src/engine/physics/broadphase/generate_pairs.d.ts.map +1 -1
- package/src/engine/physics/broadphase/generate_pairs.js +9 -1
- package/src/engine/physics/ccd/linear_sweep.d.ts.map +1 -1
- package/src/engine/physics/ccd/linear_sweep.js +237 -238
- package/src/engine/physics/computeInterceptPoint.d.ts.map +1 -1
- package/src/engine/physics/computeInterceptPoint.js +8 -3
- package/src/engine/physics/contact/ManifoldStore.d.ts +0 -16
- package/src/engine/physics/contact/ManifoldStore.d.ts.map +1 -1
- package/src/engine/physics/contact/ManifoldStore.js +1 -38
- package/src/engine/physics/ecs/BodyKind.d.ts +3 -2
- package/src/engine/physics/ecs/BodyKind.d.ts.map +1 -1
- package/src/engine/physics/ecs/BodyKind.js +25 -24
- package/src/engine/physics/ecs/PhysicsEvents.d.ts +4 -5
- package/src/engine/physics/ecs/PhysicsEvents.d.ts.map +1 -1
- package/src/engine/physics/ecs/PhysicsEvents.js +15 -16
- package/src/engine/physics/ecs/PhysicsSystem.d.ts +5 -30
- package/src/engine/physics/ecs/PhysicsSystem.d.ts.map +1 -1
- package/src/engine/physics/ecs/PhysicsSystem.js +13 -45
- package/src/engine/physics/ecs/RigidBodySerializationAdapter.d.ts.map +1 -1
- package/src/engine/physics/ecs/RigidBodySerializationAdapter.js +85 -81
- package/src/engine/physics/ecs/is_sensor.d.ts +18 -0
- package/src/engine/physics/ecs/is_sensor.d.ts.map +1 -0
- package/src/engine/physics/ecs/is_sensor.js +27 -0
- package/src/engine/physics/events/ContactEventBuffer.d.ts +2 -1
- package/src/engine/physics/events/ContactEventBuffer.d.ts.map +1 -1
- package/src/engine/physics/events/ContactEventBuffer.js +84 -83
- package/src/engine/physics/gjk/gjk.d.ts +0 -26
- package/src/engine/physics/gjk/gjk.d.ts.map +1 -1
- package/src/engine/physics/gjk/gjk.js +3 -52
- package/src/engine/physics/gjk/gjk_epa_penetration.d.ts +16 -0
- package/src/engine/physics/gjk/gjk_epa_penetration.d.ts.map +1 -0
- package/src/engine/physics/gjk/gjk_epa_penetration.js +255 -0
- package/src/engine/physics/gjk/minkowski_support.d.ts +4 -9
- package/src/engine/physics/gjk/minkowski_support.d.ts.map +1 -1
- package/src/engine/physics/gjk/minkowski_support.js +70 -75
- package/src/engine/physics/gjk/mpr.d.ts +1 -1
- package/src/engine/physics/gjk/mpr.d.ts.map +1 -1
- package/src/engine/physics/gjk/mpr.js +362 -344
- package/src/engine/physics/island/IslandBuilder.d.ts.map +1 -1
- package/src/engine/physics/island/IslandBuilder.js +431 -428
- package/src/engine/physics/narrowphase/box_box_manifold.d.ts.map +1 -1
- package/src/engine/physics/narrowphase/box_box_manifold.js +4 -81
- package/src/engine/physics/narrowphase/box_triangle_contact.d.ts.map +1 -1
- package/src/engine/physics/narrowphase/box_triangle_contact.js +4 -39
- package/src/engine/physics/narrowphase/capsule_contacts.d.ts.map +1 -1
- package/src/engine/physics/narrowphase/capsule_contacts.js +459 -462
- package/src/engine/physics/narrowphase/clip_against_axis_uv.d.ts.map +1 -1
- package/src/engine/physics/narrowphase/clip_against_axis_uv.js +4 -1
- package/src/engine/physics/narrowphase/convex_convex_manifold.d.ts +83 -0
- package/src/engine/physics/narrowphase/convex_convex_manifold.d.ts.map +1 -0
- package/src/engine/physics/narrowphase/convex_convex_manifold.js +425 -0
- package/src/engine/physics/narrowphase/convex_decomposition.d.ts +32 -0
- package/src/engine/physics/narrowphase/convex_decomposition.d.ts.map +1 -0
- package/src/engine/physics/narrowphase/convex_decomposition.js +293 -0
- package/src/engine/physics/narrowphase/mesh_convex_hull.d.ts +41 -0
- package/src/engine/physics/narrowphase/mesh_convex_hull.d.ts.map +1 -0
- package/src/engine/physics/narrowphase/mesh_convex_hull.js +106 -0
- package/src/engine/physics/narrowphase/mesh_mesh_tet_manifold.d.ts +8 -0
- package/src/engine/physics/narrowphase/mesh_mesh_tet_manifold.d.ts.map +1 -0
- package/src/engine/physics/narrowphase/mesh_mesh_tet_manifold.js +117 -0
- package/src/engine/physics/narrowphase/narrowphase_step.d.ts.map +1 -1
- package/src/engine/physics/narrowphase/narrowphase_step.js +105 -102
- package/src/engine/physics/narrowphase/reduce_manifold_contacts.d.ts +29 -0
- package/src/engine/physics/narrowphase/reduce_manifold_contacts.d.ts.map +1 -0
- package/src/engine/physics/narrowphase/reduce_manifold_contacts.js +69 -0
- package/src/engine/physics/narrowphase/refine_ray_concave.d.ts.map +1 -1
- package/src/engine/physics/narrowphase/refine_ray_concave.js +152 -145
- package/src/engine/physics/narrowphase/sphere_box_contact.d.ts.map +1 -1
- package/src/engine/physics/narrowphase/sphere_box_contact.js +132 -123
- package/src/engine/physics/queries/overlap_shape.d.ts.map +1 -1
- package/src/engine/physics/queries/overlap_shape.js +16 -17
- package/src/engine/physics/queries/raycast.d.ts +5 -0
- package/src/engine/physics/queries/raycast.d.ts.map +1 -1
- package/src/engine/physics/queries/raycast.js +16 -8
- package/src/engine/physics/queries/shape_cast.d.ts.map +1 -1
- package/src/engine/physics/queries/shape_cast.js +13 -7
- package/src/engine/physics/solver/solve_contacts.d.ts.map +1 -1
- package/src/engine/physics/solver/solve_contacts.js +8 -11
- package/src/engine/physics/vehicle/RaycastVehicle.d.ts.map +1 -1
- package/src/engine/physics/vehicle/RaycastVehicle.js +339 -333
- package/src/engine/physics/gjk/expanding_polytope_algorithm.d.ts +0 -13
- package/src/engine/physics/gjk/expanding_polytope_algorithm.d.ts.map +0 -1
- package/src/engine/physics/gjk/expanding_polytope_algorithm.js +0 -399
|
@@ -1,83 +1,84 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Event kinds emitted by the manifold diff pass.
|
|
3
|
-
* @readonly
|
|
4
|
-
* @enum {number}
|
|
5
|
-
*/
|
|
6
|
-
export const ContactEventKind = {
|
|
7
|
-
Begin: 0,
|
|
8
|
-
Stay: 1,
|
|
9
|
-
End: 2,
|
|
10
|
-
};
|
|
11
|
-
|
|
12
|
-
const DEFAULT_INITIAL_CAPACITY = 32;
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* Per-event stride in the
|
|
16
|
-
* 0 : kind (ContactEventKind)
|
|
17
|
-
* 1 : entityA
|
|
18
|
-
* 2 : entityB
|
|
19
|
-
* 3 : slot
|
|
20
|
-
* @type {number}
|
|
21
|
-
*/
|
|
22
|
-
const EVENT_STRIDE = 4;
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* Append-only buffer of contact events produced during one fixedUpdate, ready
|
|
26
|
-
* to be dispatched (and then cleared) at end-of-step.
|
|
27
|
-
*
|
|
28
|
-
* Storage is flat
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
* @
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
this.
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
* @param {number}
|
|
60
|
-
* @param {number}
|
|
61
|
-
* @param {number}
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
const
|
|
67
|
-
next
|
|
68
|
-
this.__data
|
|
69
|
-
this.
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
this.__data[off
|
|
74
|
-
this.__data[off +
|
|
75
|
-
this.__data[off +
|
|
76
|
-
this.
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Event kinds emitted by the manifold diff pass.
|
|
3
|
+
* @readonly
|
|
4
|
+
* @enum {number}
|
|
5
|
+
*/
|
|
6
|
+
export const ContactEventKind = {
|
|
7
|
+
Begin: 0,
|
|
8
|
+
Stay: 1,
|
|
9
|
+
End: 2,
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
const DEFAULT_INITIAL_CAPACITY = 32;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Per-event stride in the Int32 backing array:
|
|
16
|
+
* 0 : kind (ContactEventKind)
|
|
17
|
+
* 1 : entityA
|
|
18
|
+
* 2 : entityB
|
|
19
|
+
* 3 : slot
|
|
20
|
+
* @type {number}
|
|
21
|
+
*/
|
|
22
|
+
const EVENT_STRIDE = 4;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Append-only buffer of contact events produced during one fixedUpdate, ready
|
|
26
|
+
* to be dispatched (and then cleared) at end-of-step.
|
|
27
|
+
*
|
|
28
|
+
* Storage is flat Int32Array (signed: entity ids may legitimately be -1 for a
|
|
29
|
+
* stale body after free) — keeping the metadata side allocation-free; the
|
|
30
|
+
* actual contact payload (normal, depth, world point) is read on demand from
|
|
31
|
+
* the {@link ManifoldStore} by the dispatch code.
|
|
32
|
+
*
|
|
33
|
+
* @author Alex Goldring
|
|
34
|
+
* @copyright Company Named Limited (c) 2026
|
|
35
|
+
*/
|
|
36
|
+
export class ContactEventBuffer {
|
|
37
|
+
/**
|
|
38
|
+
* @param {number} [initial_capacity] events
|
|
39
|
+
*/
|
|
40
|
+
constructor(initial_capacity = DEFAULT_INITIAL_CAPACITY) {
|
|
41
|
+
this.__capacity = Math.max(1, initial_capacity);
|
|
42
|
+
this.__count = 0;
|
|
43
|
+
// Int32 so entity ids may legitimately store -1 (stale body after free).
|
|
44
|
+
this.__data = new Int32Array(this.__capacity * EVENT_STRIDE);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* @returns {number}
|
|
49
|
+
*/
|
|
50
|
+
get count() {
|
|
51
|
+
return this.__count;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
clear() {
|
|
55
|
+
this.__count = 0;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* @param {ContactEventKind|number} kind
|
|
60
|
+
* @param {number} entityA
|
|
61
|
+
* @param {number} entityB
|
|
62
|
+
* @param {number} slot manifold slot id
|
|
63
|
+
*/
|
|
64
|
+
push(kind, entityA, entityB, slot) {
|
|
65
|
+
if (this.__count === this.__capacity) {
|
|
66
|
+
const new_cap = this.__capacity << 1;
|
|
67
|
+
const next = new Int32Array(new_cap * EVENT_STRIDE);
|
|
68
|
+
next.set(this.__data);
|
|
69
|
+
this.__data = next;
|
|
70
|
+
this.__capacity = new_cap;
|
|
71
|
+
}
|
|
72
|
+
const off = this.__count * EVENT_STRIDE;
|
|
73
|
+
this.__data[off] = kind;
|
|
74
|
+
this.__data[off + 1] = entityA;
|
|
75
|
+
this.__data[off + 2] = entityB;
|
|
76
|
+
this.__data[off + 3] = slot;
|
|
77
|
+
this.__count++;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
kind_at(i) { return this.__data[i * EVENT_STRIDE]; }
|
|
81
|
+
entityA_at(i) { return this.__data[i * EVENT_STRIDE + 1]; }
|
|
82
|
+
entityB_at(i) { return this.__data[i * EVENT_STRIDE + 2]; }
|
|
83
|
+
slot_at(i) { return this.__data[i * EVENT_STRIDE + 3]; }
|
|
84
|
+
}
|
|
@@ -13,30 +13,4 @@
|
|
|
13
13
|
* @returns {boolean} true if the shapes intersect
|
|
14
14
|
*/
|
|
15
15
|
export function gjk(simplex: number[] | Float64Array, shape_a: AbstractShape3D, shape_b: AbstractShape3D): boolean;
|
|
16
|
-
/**
|
|
17
|
-
* Separating-axis-cached variant of {@link gjk}. The 3 floats at
|
|
18
|
-
* `axis_buffer[axis_offset..+2]` are used as the initial search
|
|
19
|
-
* direction (seed) AND are overwritten with the final search direction
|
|
20
|
-
* on exit. The narrowphase keeps a per-manifold-slot cache of these
|
|
21
|
-
* three floats so quiescent contacts converge in 1–2 iterations next
|
|
22
|
-
* frame instead of ~6–10 from a cold `(1, 0, 0)` start (Bullet's
|
|
23
|
-
* `m_cachedSeparatingAxis` and Jolt's `ioV` use the same pattern).
|
|
24
|
-
*
|
|
25
|
-
* The same primitive doubles as a separating-axis cache for the
|
|
26
|
-
* "no-overlap" path: when GJK returns false, the final direction is
|
|
27
|
-
* roughly the separating axis, and next frame's first iteration
|
|
28
|
-
* detects separation immediately.
|
|
29
|
-
*
|
|
30
|
-
* Writeback is automatic — `gjk_core` mutates a `subarray` view of
|
|
31
|
-
* `axis_buffer`, so on return `axis_buffer[axis_offset..+2]` holds the
|
|
32
|
-
* final direction. No try/finally needed.
|
|
33
|
-
*
|
|
34
|
-
* @param {number[]|Float64Array} simplex
|
|
35
|
-
* @param {AbstractShape3D} shape_a
|
|
36
|
-
* @param {AbstractShape3D} shape_b
|
|
37
|
-
* @param {Float64Array} axis_buffer
|
|
38
|
-
* @param {number} axis_offset
|
|
39
|
-
* @returns {boolean}
|
|
40
|
-
*/
|
|
41
|
-
export function gjk_with_axis(simplex: number[] | Float64Array, shape_a: AbstractShape3D, shape_b: AbstractShape3D, axis_buffer: Float64Array, axis_offset: number): boolean;
|
|
42
16
|
//# sourceMappingURL=gjk.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"gjk.d.ts","sourceRoot":"","sources":["../../../../../src/engine/physics/gjk/gjk.js"],"names":[],"mappings":"AAiBA;;;;;;;;;;;;;GAaG;AACH,6BALW,MAAM,EAAE,GAAC,YAAY,uDAGnB,OAAO,CAWnB
|
|
1
|
+
{"version":3,"file":"gjk.d.ts","sourceRoot":"","sources":["../../../../../src/engine/physics/gjk/gjk.js"],"names":[],"mappings":"AAiBA;;;;;;;;;;;;;GAaG;AACH,6BALW,MAAM,EAAE,GAAC,YAAY,uDAGnB,OAAO,CAWnB"}
|
|
@@ -40,60 +40,11 @@ export function gjk(simplex, shape_a, shape_b) {
|
|
|
40
40
|
return gjk_core(simplex, shape_a, shape_b, scratch_dir);
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
-
/**
|
|
44
|
-
* Separating-axis-cached variant of {@link gjk}. The 3 floats at
|
|
45
|
-
* `axis_buffer[axis_offset..+2]` are used as the initial search
|
|
46
|
-
* direction (seed) AND are overwritten with the final search direction
|
|
47
|
-
* on exit. The narrowphase keeps a per-manifold-slot cache of these
|
|
48
|
-
* three floats so quiescent contacts converge in 1–2 iterations next
|
|
49
|
-
* frame instead of ~6–10 from a cold `(1, 0, 0)` start (Bullet's
|
|
50
|
-
* `m_cachedSeparatingAxis` and Jolt's `ioV` use the same pattern).
|
|
51
|
-
*
|
|
52
|
-
* The same primitive doubles as a separating-axis cache for the
|
|
53
|
-
* "no-overlap" path: when GJK returns false, the final direction is
|
|
54
|
-
* roughly the separating axis, and next frame's first iteration
|
|
55
|
-
* detects separation immediately.
|
|
56
|
-
*
|
|
57
|
-
* Writeback is automatic — `gjk_core` mutates a `subarray` view of
|
|
58
|
-
* `axis_buffer`, so on return `axis_buffer[axis_offset..+2]` holds the
|
|
59
|
-
* final direction. No try/finally needed.
|
|
60
|
-
*
|
|
61
|
-
* @param {number[]|Float64Array} simplex
|
|
62
|
-
* @param {AbstractShape3D} shape_a
|
|
63
|
-
* @param {AbstractShape3D} shape_b
|
|
64
|
-
* @param {Float64Array} axis_buffer
|
|
65
|
-
* @param {number} axis_offset
|
|
66
|
-
* @returns {boolean}
|
|
67
|
-
*/
|
|
68
|
-
export function gjk_with_axis(simplex, shape_a, shape_b, axis_buffer, axis_offset) {
|
|
69
|
-
// Subarray view: zero-copy in V8 once the JIT realises both views
|
|
70
|
-
// back the same buffer; the in-place writes inside gjk_core
|
|
71
|
-
// propagate to axis_buffer automatically.
|
|
72
|
-
const dir_view = axis_buffer.subarray(axis_offset, axis_offset + 3);
|
|
73
|
-
|
|
74
|
-
// Guard against a stale or degenerate cached axis. A zero vector
|
|
75
|
-
// (first-frame initial state, or a stuck cache) and NaN / Inf
|
|
76
|
-
// would crash the first support-point query; the (1, 0, 0)
|
|
77
|
-
// fallback is the cold-start default.
|
|
78
|
-
const x = dir_view[0], y = dir_view[1], z = dir_view[2];
|
|
79
|
-
if (!Number.isFinite(x) || !Number.isFinite(y) || !Number.isFinite(z)
|
|
80
|
-
|| (x === 0 && y === 0 && z === 0)) {
|
|
81
|
-
dir_view[0] = 1;
|
|
82
|
-
dir_view[1] = 0;
|
|
83
|
-
dir_view[2] = 0;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
return gjk_core(simplex, shape_a, shape_b, dir_view);
|
|
87
|
-
}
|
|
88
|
-
|
|
89
43
|
/**
|
|
90
44
|
* Core GJK routine — parameterised on the search-direction buffer.
|
|
91
|
-
* The caller seeds `dir[0..2]` with an initial direction (
|
|
92
|
-
*
|
|
93
|
-
*
|
|
94
|
-
* direction the algorithm settled on. The narrowphase separating-
|
|
95
|
-
* axis cache (P2.1) uses this to thread per-manifold axes through
|
|
96
|
-
* `gjk_with_axis` without a try/finally writeback dance.
|
|
45
|
+
* The caller seeds `dir[0..2]` with an initial direction (`(1, 0, 0)`
|
|
46
|
+
* for the {@link gjk} cold-start wrapper), and on return `dir` holds the
|
|
47
|
+
* FINAL search direction the algorithm settled on.
|
|
97
48
|
*
|
|
98
49
|
* @param {number[]|Float64Array} simplex
|
|
99
50
|
* @param {AbstractShape3D} shape_a
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Penetration depth + normal between two convex support providers at world pose.
|
|
3
|
+
* Writes the unit separation normal (B→A) into `out` and returns the depth
|
|
4
|
+
* (positive on overlap, 0 if separated — `out` untouched on 0).
|
|
5
|
+
*
|
|
6
|
+
* @param {Float64Array|number[]} out length ≥ 3
|
|
7
|
+
* @param {{support:Function}} A world-space support provider (e.g. PosedShape)
|
|
8
|
+
* @param {{support:Function}} B
|
|
9
|
+
* @returns {number} penetration depth, or 0 if separated
|
|
10
|
+
*/
|
|
11
|
+
export function gjk_epa_penetration(out: Float64Array | number[], A: {
|
|
12
|
+
support: Function;
|
|
13
|
+
}, B: {
|
|
14
|
+
support: Function;
|
|
15
|
+
}): number;
|
|
16
|
+
//# sourceMappingURL=gjk_epa_penetration.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"gjk_epa_penetration.d.ts","sourceRoot":"","sources":["../../../../../src/engine/physics/gjk/gjk_epa_penetration.js"],"names":[],"mappings":"AA+OA;;;;;;;;;GASG;AACH,yCALW,YAAY,GAAC,MAAM,EAAE,KACrB;IAAC,OAAO,WAAS;CAAC,KAClB;IAAC,OAAO,WAAS;CAAC,GAChB,MAAM,CAOlB"}
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Robust GJK + EPA penetration for convex shapes — the narrowphase's
|
|
3
|
+
* convex-fallback and concave-per-triangle penetration query. Replaced the
|
|
4
|
+
* `gjk.js` `gjk_with_axis` + `expanding_polytope_algorithm.js` pair, which
|
|
5
|
+
* returned a non-minimal axis for polytopes because its GJK handed EPA a
|
|
6
|
+
* degenerate (origin-on-face) simplex (see memory: feedback_epa_unreliable_polytopes).
|
|
7
|
+
*
|
|
8
|
+
* Structure follows Bullet's btGjkEpa2: GJK produces a rank-4 simplex that
|
|
9
|
+
* strictly encloses the origin; EPA builds outward-oriented faces from it and
|
|
10
|
+
* expands toward the closest face, rebuilding the horizon each step. Operates on
|
|
11
|
+
* any support provider with `support(out, off, dx, dy, dz)` in WORLD space (e.g.
|
|
12
|
+
* {@link PosedShape}); the penetration normal + depth come from the Minkowski
|
|
13
|
+
* difference's closest face to the origin. No cross-frame state → reset-and-
|
|
14
|
+
* resimulate determinism preserved.
|
|
15
|
+
*
|
|
16
|
+
* Even with per-call allocation (plain arrays) it measured ~2.1× FASTER than the
|
|
17
|
+
* replaced pair, which wasted all 64 iterations diverging on the degenerate
|
|
18
|
+
* simplex. KNOWN FOLLOW-UP: port the hot path to typed-array pools to remove the
|
|
19
|
+
* per-call allocation; the convex/concave EPA fallbacks are rarely hit today
|
|
20
|
+
* (closed-form solvers + SAT cover the common pairs), so it has not been urgent.
|
|
21
|
+
*
|
|
22
|
+
* @author Alex Goldring
|
|
23
|
+
* @copyright Company Named Limited (c) 2026
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
const GJK_MAX_ITER = 64;
|
|
27
|
+
const EPA_MAX_ITER = 64;
|
|
28
|
+
const EPA_EPS = 1e-8;
|
|
29
|
+
|
|
30
|
+
const _sA = new Float64Array(3);
|
|
31
|
+
const _sB = new Float64Array(3);
|
|
32
|
+
|
|
33
|
+
function sub(a, b) { return [a[0] - b[0], a[1] - b[1], a[2] - b[2]]; }
|
|
34
|
+
function dot(a, b) { return a[0] * b[0] + a[1] * b[1] + a[2] * b[2]; }
|
|
35
|
+
function cross(a, b) {
|
|
36
|
+
return [a[1] * b[2] - a[2] * b[1], a[2] * b[0] - a[0] * b[2], a[0] * b[1] - a[1] * b[0]];
|
|
37
|
+
}
|
|
38
|
+
function neg(a) { return [-a[0], -a[1], -a[2]]; }
|
|
39
|
+
function lensq(a) { return a[0] * a[0] + a[1] * a[1] + a[2] * a[2]; }
|
|
40
|
+
// triple product (a×b)×c = b(a·c) − a(b·c)
|
|
41
|
+
function tripleProduct(a, b, c) {
|
|
42
|
+
const ac = dot(a, c), bc = dot(b, c);
|
|
43
|
+
return [b[0] * ac - a[0] * bc, b[1] * ac - a[1] * bc, b[2] * ac - a[2] * bc];
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Minkowski-difference support: support_A(d) − support_B(−d), world space.
|
|
48
|
+
*
|
|
49
|
+
* The direction is normalised first: the support contract assumes a UNIT vector
|
|
50
|
+
* — `SphereShape3D`/`CapsuleShape3D` scale the raw direction by their radius
|
|
51
|
+
* with no internal renormalisation — but GJK/EPA hand us arbitrary-magnitude
|
|
52
|
+
* search directions (the seed `−s` and the cross/triple products from the
|
|
53
|
+
* simplex evolution). Polytope supports are scale-invariant (vertex argmax), so
|
|
54
|
+
* this only matters for curved providers, but it must hold for them. Matches the
|
|
55
|
+
* sibling `minkowski_support.js`, which normalises for the same reason.
|
|
56
|
+
*/
|
|
57
|
+
function csoSupport(A, B, d) {
|
|
58
|
+
let dx = d[0], dy = d[1], dz = d[2];
|
|
59
|
+
const len2 = dx * dx + dy * dy + dz * dz;
|
|
60
|
+
if (len2 > 1e-30) {
|
|
61
|
+
const inv = 1 / Math.sqrt(len2);
|
|
62
|
+
dx *= inv; dy *= inv; dz *= inv;
|
|
63
|
+
}
|
|
64
|
+
A.support(_sA, 0, dx, dy, dz);
|
|
65
|
+
B.support(_sB, 0, -dx, -dy, -dz);
|
|
66
|
+
return [_sA[0] - _sB[0], _sA[1] - _sB[1], _sA[2] - _sB[2]];
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* GJK intersection. Returns a rank-4 simplex (array of 4 CSO points) enclosing
|
|
71
|
+
* the origin on overlap, or null if separated. `simplex[0]` is the most-recent
|
|
72
|
+
* point throughout (Muratori convention).
|
|
73
|
+
*/
|
|
74
|
+
function gjk(A, B) {
|
|
75
|
+
let d = [1, 0, 0];
|
|
76
|
+
let s = csoSupport(A, B, d);
|
|
77
|
+
if (lensq(s) < 1e-18) { d = [0, 1, 0]; s = csoSupport(A, B, d); }
|
|
78
|
+
const simplex = [s];
|
|
79
|
+
d = neg(s);
|
|
80
|
+
|
|
81
|
+
for (let iter = 0; iter < GJK_MAX_ITER; iter++) {
|
|
82
|
+
if (lensq(d) < 1e-18) return null; // search direction collapsed → touching/sep
|
|
83
|
+
const a = csoSupport(A, B, d);
|
|
84
|
+
if (dot(a, d) < 0) return null; // farthest point short of origin → separated
|
|
85
|
+
simplex.unshift(a);
|
|
86
|
+
if (doSimplex(simplex, d)) return simplex.slice(0, 4);
|
|
87
|
+
}
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Evolve the simplex toward the origin. Mutates `simplex` (in place) and writes
|
|
93
|
+
* the next search direction into `dOut`. Returns true when a tetrahedron
|
|
94
|
+
* encloses the origin.
|
|
95
|
+
*/
|
|
96
|
+
function doSimplex(simplex, dOut) {
|
|
97
|
+
if (simplex.length === 2) return lineCase(simplex, dOut);
|
|
98
|
+
if (simplex.length === 3) return triangleCase(simplex, dOut);
|
|
99
|
+
return tetraCase(simplex, dOut);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function setDir(dOut, v) { dOut[0] = v[0]; dOut[1] = v[1]; dOut[2] = v[2]; }
|
|
103
|
+
|
|
104
|
+
function lineCase(simplex, dOut) {
|
|
105
|
+
const a = simplex[0], b = simplex[1];
|
|
106
|
+
const ab = sub(b, a), ao = neg(a);
|
|
107
|
+
if (dot(ab, ao) > 0) {
|
|
108
|
+
const perp = tripleProduct(ab, ao, ab);
|
|
109
|
+
if (lensq(perp) < 1e-18) {
|
|
110
|
+
// origin on the line — pick any perpendicular
|
|
111
|
+
const ref = Math.abs(ab[0]) < 0.9 ? [1, 0, 0] : [0, 1, 0];
|
|
112
|
+
setDir(dOut, cross(ab, ref));
|
|
113
|
+
} else setDir(dOut, perp);
|
|
114
|
+
} else {
|
|
115
|
+
simplex.length = 1; // keep [a]
|
|
116
|
+
setDir(dOut, ao);
|
|
117
|
+
}
|
|
118
|
+
return false;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function triangleCase(simplex, dOut) {
|
|
122
|
+
const a = simplex[0], b = simplex[1], c = simplex[2];
|
|
123
|
+
const ab = sub(b, a), ac = sub(c, a), ao = neg(a);
|
|
124
|
+
const abc = cross(ab, ac);
|
|
125
|
+
|
|
126
|
+
if (dot(cross(abc, ac), ao) > 0) {
|
|
127
|
+
if (dot(ac, ao) > 0) { // outside edge ac
|
|
128
|
+
simplex.length = 0; simplex.push(a, c);
|
|
129
|
+
setDir(dOut, tripleProduct(ac, ao, ac));
|
|
130
|
+
return false;
|
|
131
|
+
}
|
|
132
|
+
return starCase(simplex, a, b, ao, dOut);
|
|
133
|
+
}
|
|
134
|
+
if (dot(cross(ab, abc), ao) > 0) return starCase(simplex, a, b, ao, dOut);
|
|
135
|
+
|
|
136
|
+
// origin within the triangle's prism — above or below
|
|
137
|
+
if (dot(abc, ao) > 0) { simplex.length = 0; simplex.push(a, b, c); setDir(dOut, abc); }
|
|
138
|
+
else { simplex.length = 0; simplex.push(a, c, b); setDir(dOut, neg(abc)); }
|
|
139
|
+
return false;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function starCase(simplex, a, b, ao, dOut) {
|
|
143
|
+
const ab = sub(b, a);
|
|
144
|
+
if (dot(ab, ao) > 0) {
|
|
145
|
+
simplex.length = 0; simplex.push(a, b);
|
|
146
|
+
setDir(dOut, tripleProduct(ab, ao, ab));
|
|
147
|
+
} else {
|
|
148
|
+
simplex.length = 0; simplex.push(a);
|
|
149
|
+
setDir(dOut, ao);
|
|
150
|
+
}
|
|
151
|
+
return false;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function tetraCase(simplex, dOut) {
|
|
155
|
+
const a = simplex[0], b = simplex[1], c = simplex[2], d = simplex[3];
|
|
156
|
+
const ab = sub(b, a), ac = sub(c, a), ad = sub(d, a), ao = neg(a);
|
|
157
|
+
const abc = cross(ab, ac);
|
|
158
|
+
const acd = cross(ac, ad);
|
|
159
|
+
const adb = cross(ad, ab);
|
|
160
|
+
|
|
161
|
+
if (dot(abc, ao) > 0) { simplex.length = 0; simplex.push(a, b, c); return triangleCase(simplex, dOut); }
|
|
162
|
+
if (dot(acd, ao) > 0) { simplex.length = 0; simplex.push(a, c, d); return triangleCase(simplex, dOut); }
|
|
163
|
+
if (dot(adb, ao) > 0) { simplex.length = 0; simplex.push(a, d, b); return triangleCase(simplex, dOut); }
|
|
164
|
+
return true; // origin inside the tetrahedron
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* EPA over the Minkowski difference, seeded by a rank-4 enclosing simplex.
|
|
169
|
+
* Writes the unit penetration normal (B→A) into `out` and returns the depth.
|
|
170
|
+
*/
|
|
171
|
+
function epa(A, B, simplex, out) {
|
|
172
|
+
const verts = simplex.map((p) => [p[0], p[1], p[2]]);
|
|
173
|
+
// centroid for outward orientation of the initial faces
|
|
174
|
+
const cen = [(verts[0][0] + verts[1][0] + verts[2][0] + verts[3][0]) / 4,
|
|
175
|
+
(verts[0][1] + verts[1][1] + verts[2][1] + verts[3][1]) / 4,
|
|
176
|
+
(verts[0][2] + verts[1][2] + verts[2][2] + verts[3][2]) / 4];
|
|
177
|
+
|
|
178
|
+
let faces = [[0, 1, 2], [0, 2, 3], [0, 3, 1], [1, 3, 2]].map((f) => makeFace(verts, f, cen));
|
|
179
|
+
|
|
180
|
+
for (let iter = 0; iter < EPA_MAX_ITER; iter++) {
|
|
181
|
+
// closest face to the origin
|
|
182
|
+
let best = faces[0];
|
|
183
|
+
for (let i = 1; i < faces.length; i++) if (faces[i].dist < best.dist) best = faces[i];
|
|
184
|
+
|
|
185
|
+
const w = csoSupport(A, B, best.n);
|
|
186
|
+
const wdist = dot(best.n, w) - best.dist;
|
|
187
|
+
if (wdist < EPA_EPS) break; // converged onto the closest face
|
|
188
|
+
|
|
189
|
+
const wi = verts.length;
|
|
190
|
+
verts.push(w);
|
|
191
|
+
|
|
192
|
+
// remove faces visible from w; collect horizon edges (those on exactly one removed face)
|
|
193
|
+
const edges = [];
|
|
194
|
+
const kept = [];
|
|
195
|
+
for (const f of faces) {
|
|
196
|
+
if (dot(f.n, w) - f.dist > EPA_EPS) {
|
|
197
|
+
addEdge(edges, f.v[0], f.v[1]);
|
|
198
|
+
addEdge(edges, f.v[1], f.v[2]);
|
|
199
|
+
addEdge(edges, f.v[2], f.v[0]);
|
|
200
|
+
} else kept.push(f);
|
|
201
|
+
}
|
|
202
|
+
faces = kept;
|
|
203
|
+
for (const [i, j] of edges) faces.push(makeFace(verts, [i, j, wi], cen));
|
|
204
|
+
if (faces.length === 0) break; // degenerate polytope (all faces visible + horizon cancelled)
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// recompute the closest face (faces may have changed on the last iteration)
|
|
208
|
+
if (faces.length === 0) {
|
|
209
|
+
// Degenerate: no face survived. Report no usable axis (caller treats a
|
|
210
|
+
// non-positive depth as a miss) rather than dereferencing faces[0] and
|
|
211
|
+
// throwing a TypeError out of the narrowphase step.
|
|
212
|
+
out[0] = 0; out[1] = 0; out[2] = 0;
|
|
213
|
+
return 0;
|
|
214
|
+
}
|
|
215
|
+
let best = faces[0];
|
|
216
|
+
for (let i = 1; i < faces.length; i++) if (faces[i].dist < best.dist) best = faces[i];
|
|
217
|
+
out[0] = best.n[0]; out[1] = best.n[1]; out[2] = best.n[2];
|
|
218
|
+
return best.dist;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/** Build a face with an OUTWARD unit normal (away from the polytope centroid). */
|
|
222
|
+
function makeFace(verts, idx, cen) {
|
|
223
|
+
const a = verts[idx[0]], b = verts[idx[1]], c = verts[idx[2]];
|
|
224
|
+
let n = cross(sub(b, a), sub(c, a));
|
|
225
|
+
const l = Math.sqrt(lensq(n)) || 1;
|
|
226
|
+
n = [n[0] / l, n[1] / l, n[2] / l];
|
|
227
|
+
// orient outward: normal should point away from the polytope interior (centroid)
|
|
228
|
+
if (dot(n, sub(a, cen)) < 0) { n = neg(n); const t = idx[1]; idx = [idx[0], idx[2], t]; }
|
|
229
|
+
return { v: idx, n, dist: dot(n, a) }; // perpendicular distance from origin (≥0 once outward)
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/** Add an edge with horizon cancellation: if the reverse edge is present, drop both. */
|
|
233
|
+
function addEdge(edges, i, j) {
|
|
234
|
+
for (let k = 0; k < edges.length; k++) {
|
|
235
|
+
if (edges[k][0] === j && edges[k][1] === i) { edges.splice(k, 1); return; }
|
|
236
|
+
}
|
|
237
|
+
edges.push([i, j]);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Penetration depth + normal between two convex support providers at world pose.
|
|
242
|
+
* Writes the unit separation normal (B→A) into `out` and returns the depth
|
|
243
|
+
* (positive on overlap, 0 if separated — `out` untouched on 0).
|
|
244
|
+
*
|
|
245
|
+
* @param {Float64Array|number[]} out length ≥ 3
|
|
246
|
+
* @param {{support:Function}} A world-space support provider (e.g. PosedShape)
|
|
247
|
+
* @param {{support:Function}} B
|
|
248
|
+
* @returns {number} penetration depth, or 0 if separated
|
|
249
|
+
*/
|
|
250
|
+
export function gjk_epa_penetration(out, A, B) {
|
|
251
|
+
const simplex = gjk(A, B);
|
|
252
|
+
if (simplex === null || simplex.length < 4) return 0;
|
|
253
|
+
const depth = epa(A, B, simplex, out);
|
|
254
|
+
return depth > 0 && Number.isFinite(depth) ? depth : 0;
|
|
255
|
+
}
|
|
@@ -15,15 +15,10 @@
|
|
|
15
15
|
* and shape support implementations (sphere especially) don't bother
|
|
16
16
|
* to renormalise internally.
|
|
17
17
|
*
|
|
18
|
-
* NOTE:
|
|
19
|
-
* support inline rather than calling this
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
* already unit-length. The narrowphase consumer of EPA's output then
|
|
23
|
-
* negates the result to get the contact normal in the "B → A" stored
|
|
24
|
-
* convention. Folding EPA onto this helper would require both flipping
|
|
25
|
-
* its sign convention and changing the narrowphase call site — not
|
|
26
|
-
* worth the churn for one call site.
|
|
18
|
+
* NOTE: the robust penetration query (`gjk_epa_penetration.js`) computes
|
|
19
|
+
* its own Minkowski-difference support inline rather than calling this
|
|
20
|
+
* helper — it is self-contained (its own GJK + EPA) and works on world-
|
|
21
|
+
* space support providers, so it does not share this body-space path.
|
|
27
22
|
*
|
|
28
23
|
* @param {Float64Array|number[]} result 3-vector destination
|
|
29
24
|
* @param {number} result_offset
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"minkowski_support.d.ts","sourceRoot":"","sources":["../../../../../src/engine/physics/gjk/minkowski_support.js"],"names":[],"mappings":"AAEA
|
|
1
|
+
{"version":3,"file":"minkowski_support.d.ts","sourceRoot":"","sources":["../../../../../src/engine/physics/gjk/minkowski_support.js"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,0CARW,YAAY,GAAC,MAAM,EAAE,iBACrB,MAAM,6DAGN,MAAM,SACN,MAAM,SACN,MAAM,QA2BhB"}
|