Commit e7d1a27
authored
Rework Contact Pair Management (#683)
# Objective
Currently, contact pair management works as follows:
1. Collect all pairs of entities with intersecting AABBs in the `BroadCollisionPairs` resource.
2. Reset collision statuses `during_previous_frame` and `during_current_frame` of each existing contact to `true` and `false` respectively.
3. Iterate over all `BroadCollisionPairs` in parallel. For each pair:
1. Get the previous contacts from `Collisions`, if they exist, and set `during_previous_frame` accordingly.
2. Compute new contact manifolds.
3. Match contacts.
4. Insert the new contact data to `Collisions`.
4. Generate contact constraints.
5. After the solver, report contacts (send collision events and update `CollidingEntities`).
6. Remove contact pairs for which `during_current_frame` is `false`.
There are a lot of inefficiencies here.
- We collect all broad phase pairs from scratch to a `Vec` every frame.
- We iterate through all collisions at least *three* separate times every frame for handling contact status changes (reset statuses, report contacts, remove ended contacts).
- We have to do a lookup for previous contacts for every contact pair.
- The logic for resetting collision statuses involves ECS queries and confusing state management.
- The parallel loop over `BroadCollisionPairs` collects collisions into new vectors every time.
Overall, there is an excessive amount of iteration and allocations, and the logic for managing contact statuses is very confusing.
In addition, the `Collisions` resource itself is not efficient for our purposes. There are many cases where you may need to iterate over contacts associated with a specific entity, but this currently requires iterating through *all* collisions because collisions are just stored in an `IndexMap`. To resolve this, we need a more graph-like structure.
## Solution
Change `Collisions` to a `ContactGraph`, and rework contact pair management to look like the following:
1. Find new broad phase pairs and add them to the `ContactGraph` directly. Duplicate pairs are avoided with fast lookups into a `HashSet<PairKey>`.
2. Iterate over all pairs in the `ContactGraph` in parallel, maintaining thread-local bit vectors to track contact status changes. For each contact pair:
1. Test if AABBs still overlap.
2. If the AABBs are disjoint, set `ContactPairFlags::DISJOINT_AABB` and the status change bit for this contact pair. Continue to the next pair.
3. Otherwise, update the contact manifolds.
4. Match contacts.
5. Set flags for whether the contact is touching, and whether it started or stopped touching.
3. Combine thread-local bit vectors into a global bit vector with bitwise OR.
4. Serially iterate through set bits using the [count trailing zeros](https://lemire.me/blog/2018/02/21/iterating-over-set-bits-quickly/) method. For each contact pair with a changed status:
1. If the AABBs are disjoint, send the `CollisionEnded` event (if events are enabled), update `CollidingEntities`, and remove the pair from `Collisions`.
2. If the colliders started touching, send the `CollisionStared` event (if events are enabled) and update `CollidingEntities`.
3. If the AABBs stopped touching, send the `CollisionEnded` event (if events are enabled) and update `CollidingEntities`.
5. Generate contact constraints.
Contact removal for removed or disabled colliders is now also handled with observers.
This improves several aspects:
- The broad phase only adds new pairs, and adds them to the `ContactGraph` directly.
- We only iterate through collisions *once* every frame for handling contact status changes, using bit scanning intrinsics to only iterate over pairs that actually changed.
- We are mutably iterating over the `ContactGraph` directly, and don't need to do separate lookups for previous contacts or do any extra allocations.
As you may have noticed, a contact pair now exists between two colliders if their AABBs are touching, even if the actual shapes aren't. This is important for the pair management logic, though it does mean that the `ContactGraph` can now have a lot more contact pairs in some cases.
### Contact Graph
Previously, `Collisions` used an `IndexMap` to store collisions, keyed by `(Entity, Entity)`. The motivation was that we get vec-like iteration speed, with preserved insertion order and fast lookups by entity pairs.
However, there are scenarios where you may need to iterate over the entities colliding with a given entity, such as for simulation islands or even gameplay logic. With just an `IndexMap`, this requires iterating over all pairs.
This PR adds an undirected graph data structure called `UnGraph`, based on [petgraph](https://github.com/petgraph/petgraph/), simplified and tailored for our use cases.
```rust
#[derive(Clone, Debug)]
pub struct UnGraph<N, E> {
nodes: Vec<Node<N>>,
edges: Vec<Edge<E>>,
}
```
This is used for the new `ContactGraph` to provide faster and more powerful queries over contact pairs. The following methods are available:
- `get(&self, entity1: Entity, entity2: Entity)`
- `get_mut(&mut self, entity1: Entity, entity2: Entity)`
- `contains(&self, entity1: Entity, entity2: Entity)`
- `contains_key(&self, pair_key: &PairKey)`
- `iter(&self)`
- `iter_touching(&self)`
- `iter_mut(&mut self)`
- `iter_touching_mut(&mut self)`
- `collisions_with(&self, entity: Entity)`
- `collisions_with_mut(&mut self, entity: Entity)`
- `entities_colliding_with(&self, entity: Entity)`
and a few ones primarily for internals:
- `add_pair(&mut self, contacts: Contacts)`
- `add_pair_with_key(&mut self, contacts: Contacts, pair_key: PairKey)`
- `insert_pair(&mut self, contacts: Contacts)`
- `insert_pair_with_key(&mut self, contacts: Contacts, pair_key: PairKey)`
- `remove_pair(&mut self, entity1: Entity, entity2: Entity)`
- `remove_collider_with(&mut self, entity: Entity, pair_callback: F)`
The graph doesn't let us directly get nodes or edges by `Entity` ID. However, a new `EntityDataIndex` is used to map `Entity` IDs to graph nodes.
```rust
/// A container for data associated with entities in a generational arena.
#[derive(Clone, Debug, Default)]
pub struct EntityDataIndex<T> {
data: Vec<(u32, T)>,
}
```
This is modeled after Rapier's [`Coarena`](https://docs.rs/rapier3d/0.23.1/rapier3d/data/struct.Coarena.html).
### `Collisions` System Parameter
The `ContactGraph` resource contains both touching and non-touching contacts. This may be inconvenient and confusing for new users.
To provide a simpler, more user-friendly API, a `Collisions` `SystemParam` has been added. It is similar to the old `Collisions` resource, and only provides access to touching contacts. It doesn't allow mutation, as contact modification and filtering should typically be handled via `CollisionHooks`.
```rust
#[derive(Component)]
struct PressurePlate;
fn activate_pressure_plates(mut query: Query<Entity, With<PressurePlate>>, collisions: Collisions) {
for pressure_plate in &query {
// Compute the total impulse applied to the pressure plate.
let mut total_impulse = 0.0;
for contact_pair in collisions.collisions_with(pressure_plate) {
total_impulse += contact_pair.total_normal_impulse_magnitude();
}
if total_impulse > 5.0 {
println!("Pressure plate activated!");
}
}
}
```
### Contact Reporting
Previously, the `ContactReportingPlugin` sent the `CollisionStarted`, `CollisionEnded`, and `Collision` events and updated `CollidingEntities` for all contact pairs after the solver. This required iterating through all contacts and performing lots of queries, which had meaningful overhead, even for apps that don't need collision events, or only need them for a few entities.
Very few applications actually need collision events for *all* entities. In most engines, contact reporting/monitoring is entirely optional, and typically opt-in. Thus, a new `CollisionEventsEnabled` component has been added. Collision events are only sent if either entity in a collision has the component.
The `ContactReportingPlugin` has also been entirely removed, and contact reporting is now handled directly by the narrow phase when processing contact status changes. This removes the need for extra iteration or queries.
Finally, the `Collision` event has been removed. It was largely unnecessary, as the collision data can be accessed through `Collisions` directly. And semantically, it didn't feel like an "event" as it was sent every frame during continuous contact.
## Performance
In the new `pyramid_2d` example, with a pyramid that has a base of 50 boxes, 1276 total colliders, and 6 substeps, the old timings with the `parallel` feature looked like the following after 500 steps:
<img alt="Old multi-threaded" src="https://github.com/user-attachments/assets/185f68ef-3fe9-4571-9619-a4254d02f4e2" width="300" />
Now, they look like this:
<img alt="New multi-threaded" src="https://github.com/user-attachments/assets/64280006-b129-4ada-9a0a-23e0782a6288" width="300" />
Notably:
- **Narrow Phase**: 4.5x as fast, despite also handling collision events
- **Collision Events**: This whole separate step is gone, and handled by the narrow phase
- **Store Impulses**: From 0.12 ms down to 0.03 ms, because fetching contacts is handled more efficiently
- **Other**: From 0.97 ms down to 0.18 ms, largely due to `wake_on_collision_ended` being removed in favor of much more efficient logic integrated into the narrow phase
The total step time in this scene is reduced by 1.76 ms. The difference should be larger the more collisions there are.
Single-threaded performance is also improved, though not quite as much. In the same test scene, the old timings looked like this:
<img alt="Old single-threaded" src="https://github.com/user-attachments/assets/a9b6ad1f-e887-4b7f-b2fd-6ce9b4fa3fa8" width="300" />
Now, they look like this:
<img alt="New single-threaded" src="https://github.com/user-attachments/assets/08e902a0-0a6b-463a-b7a2-54f25703747b" width="300" />
reducing the total step time in this scene by 0.7 ms.
The changes in this PR also unlock many future optimizations:
- Generate contact constraints in parallel directly in the contact pair update loop
- Persistent simulation islands (benefits from a contact graph)
It is worth noting that we are currently using Bevy's built-in `ComputeTaskPool` for parallelism, which limits the number of available threads. Using it, we are still slightly behind Rapier's narrow phase performance. However, I have measured that if we manually increase the size of the thread pool, or use rayon, we now match or even slightly outperform Rapier's narrow phase.
## Future Work
- Persistent simulation islands
- Improved broad phase using [OBVHS](https://github.com/DGriffin91/obvhs)
- Rename `Contacts` to `ContactPair`
- Reuse contact manifolds and match contacts more effectively (likely not possible with Parry due to different `ContactManifold` types)
---
## Migration Guide
### `PostProcessCollisions`
The `PostProcessCollisions` schedule and `NarrowPhaseSet::PostProcess` system set have been removed, as it is incompatible with new optimizations to narrow phase collision detection. Instead, use `CollisionHooks` for contact modification.
### Contact Reporting
The `ContactReportingPlugin` and `PhysicsStepSet` have been removed. Contact reporting is now handled by the `NarrowPhasePlugin` directly.
The `Collision` event no longer exists. Instead, use `Collisions` directly, or get colliding entities using the `CollidingEntities` component.
The `CollisionStarted` and `CollisionEnded` events are now only sent if either entity in the collision has the `CollisionEventsEnabled` component. If you'd like to revert to the old behavior of having collision events for all entities, consider making `CollisionEventsEnabled` a required component for `Collider`:
```rust
app.register_required_components::<Collider, CollisionEventsEnabled>();
```
### `Collisions`
The `Collisions` resource is now a `SystemParam`.
```rust
// Old
fn iter_collisions(collisions: Res<Collisions>) {
todo!()
}
// New
fn iter_collisions(collisions: Collisions) {
todo!()
}
```
Internally, `Collisions` now stores a `ContactGraph` that stores both touching and non-touching contact pairs. The `Collisions` system parameter is just a wrapper that provides a simpler API and only returns touching contacts.
The `collisions_with_entity` method has also been renamed to `collisions_with`, and all methods that mutatate, add, or remove contact pairs have been removed from `Collisions`. However, the following mutating methods are available on `ContactGraph`:
- `get_mut`
- `iter_mut`
- `iter_touching_mut`
- `collisions_with_mut`
- `add_pair`/`add_pair_with_key`
- `insert_pair`/`insert_pair_with_key`
- `remove_pair`
- `remove_collider_with`
For most scenarios, contact modification and removal are intended to be handled with `CollisionHooks`.
### `Contacts`
The `is_sensor`, `during_current_frame`, and `during_previous_frame` properties of `Contacts` have been removed in favor of a `flags` property storing information in a more compact bitflag format. The `is_sensor`, `is_touching`, `collision_started`, and `collision_ended` helper methods can be used instead.
### `ContactManifold`
Methods such as `AnyCollider::contact_manifolds_with_context` now take `&mut Vec<ContactManifold>` instead of returning a new vector every time. This allows manifolds to be persisted more effectively, and reduces unnecessary allocations.
### `BroadCollisionPairs`
The `BroadCollisionPairs` resource has been removed. Use the `ContactGraph` resource instead.
### `AabbIntersections`
The `AabbIntersections` component has been removed. Use `ContactGraph::entities_colliding_with` instead.1 parent 072ec52 commit e7d1a27
File tree
45 files changed
+4181
-2153
lines changed- crates
- avian2d
- examples
- kinematic_character_2d
- avian3d
- examples
- kinematic_character_3d
- src
- collision
- collider
- parry
- contact_types
- narrow_phase
- data_structures
- debug_render
- diagnostics
- dynamics
- ccd
- integrator
- sleeping
- solver
- contact
- schedule
- spatial_query
- tests
Some content is hidden
Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
45 files changed
+4181
-2153
lines changed| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
27 | 27 | | |
28 | 28 | | |
29 | 29 | | |
30 | | - | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
31 | 36 | | |
32 | 37 | | |
33 | | - | |
34 | | - | |
35 | 38 | | |
36 | 39 | | |
| 40 | + | |
| 41 | + | |
37 | 42 | | |
38 | 43 | | |
39 | 44 | | |
| |||
81 | 86 | | |
82 | 87 | | |
83 | 88 | | |
84 | | - | |
85 | 89 | | |
86 | | - | |
87 | 90 | | |
88 | 91 | | |
| 92 | + | |
89 | 93 | | |
90 | 94 | | |
91 | 95 | | |
| |||
152 | 156 | | |
153 | 157 | | |
154 | 158 | | |
| 159 | + | |
| 160 | + | |
| 161 | + | |
| 162 | + | |
155 | 163 | | |
156 | 164 | | |
157 | 165 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
76 | 76 | | |
77 | 77 | | |
78 | 78 | | |
| 79 | + | |
79 | 80 | | |
80 | | - | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
81 | 85 | | |
82 | 86 | | |
83 | 87 | | |
| |||
98 | 102 | | |
99 | 103 | | |
100 | 104 | | |
101 | | - | |
102 | | - | |
103 | | - | |
104 | | - | |
105 | | - | |
106 | | - | |
107 | | - | |
108 | | - | |
109 | | - | |
110 | | - | |
111 | | - | |
112 | | - | |
113 | | - | |
114 | | - | |
115 | | - | |
116 | | - | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
117 | 113 | | |
118 | 114 | | |
119 | 115 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
19 | 19 | | |
20 | 20 | | |
21 | 21 | | |
22 | | - | |
23 | | - | |
| 22 | + | |
| 23 | + | |
24 | 24 | | |
25 | 25 | | |
26 | 26 | | |
| |||
34 | 34 | | |
35 | 35 | | |
36 | 36 | | |
37 | | - | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
38 | 40 | | |
39 | 41 | | |
40 | 42 | | |
41 | 43 | | |
42 | | - | |
43 | 44 | | |
44 | 45 | | |
45 | 46 | | |
| |||
183 | 184 | | |
184 | 185 | | |
185 | 186 | | |
186 | | - | |
187 | | - | |
188 | | - | |
189 | | - | |
| 187 | + | |
| 188 | + | |
| 189 | + | |
| 190 | + | |
| 191 | + | |
| 192 | + | |
| 193 | + | |
| 194 | + | |
| 195 | + | |
| 196 | + | |
| 197 | + | |
| 198 | + | |
| 199 | + | |
| 200 | + | |
| 201 | + | |
| 202 | + | |
190 | 203 | | |
191 | 204 | | |
192 | 205 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1 | | - | |
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
2 | 5 | | |
3 | 6 | | |
4 | 7 | | |
| |||
23 | 26 | | |
24 | 27 | | |
25 | 28 | | |
26 | | - | |
27 | | - | |
| 29 | + | |
| 30 | + | |
28 | 31 | | |
29 | 32 | | |
30 | 33 | | |
| |||
265 | 268 | | |
266 | 269 | | |
267 | 270 | | |
268 | | - | |
| 271 | + | |
269 | 272 | | |
270 | 273 | | |
271 | 274 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 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 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
67 | 67 | | |
68 | 68 | | |
69 | 69 | | |
| 70 | + | |
| 71 | + | |
70 | 72 | | |
71 | 73 | | |
72 | 74 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
28 | 28 | | |
29 | 29 | | |
30 | 30 | | |
31 | | - | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
32 | 37 | | |
33 | 38 | | |
34 | | - | |
35 | | - | |
36 | 39 | | |
37 | 40 | | |
| 41 | + | |
| 42 | + | |
38 | 43 | | |
39 | 44 | | |
40 | 45 | | |
| |||
83 | 88 | | |
84 | 89 | | |
85 | 90 | | |
86 | | - | |
87 | 91 | | |
88 | | - | |
89 | 92 | | |
90 | 93 | | |
| 94 | + | |
91 | 95 | | |
92 | 96 | | |
93 | 97 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
38 | 38 | | |
39 | 39 | | |
40 | 40 | | |
41 | | - | |
| 41 | + | |
42 | 42 | | |
43 | 43 | | |
44 | 44 | | |
45 | 45 | | |
46 | 46 | | |
47 | | - | |
48 | | - | |
| 47 | + | |
| 48 | + | |
49 | 49 | | |
50 | 50 | | |
51 | 51 | | |
| |||
59 | 59 | | |
60 | 60 | | |
61 | 61 | | |
62 | | - | |
| 62 | + | |
63 | 63 | | |
64 | 64 | | |
65 | 65 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1 | | - | |
| 1 | + | |
2 | 2 | | |
3 | 3 | | |
4 | 4 | | |
| |||
7 | 7 | | |
8 | 8 | | |
9 | 9 | | |
10 | | - | |
| 10 | + | |
11 | 11 | | |
12 | 12 | | |
13 | 13 | | |
| |||
56 | 56 | | |
57 | 57 | | |
58 | 58 | | |
59 | | - | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
60 | 62 | | |
61 | 63 | | |
62 | 64 | | |
63 | 65 | | |
64 | 66 | | |
65 | | - | |
66 | | - | |
67 | | - | |
68 | | - | |
69 | | - | |
70 | | - | |
71 | | - | |
72 | | - | |
73 | | - | |
74 | | - | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
75 | 72 | | |
76 | 73 | | |
77 | 74 | | |
78 | 75 | | |
79 | 76 | | |
80 | | - | |
| 77 | + | |
81 | 78 | | |
82 | | - | |
83 | | - | |
84 | | - | |
85 | 79 | | |
86 | | - | |
87 | | - | |
88 | | - | |
89 | | - | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
90 | 96 | | |
91 | 97 | | |
92 | 98 | | |
0 commit comments