Skip to content

Commit e7d1a27

Browse files
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

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

crates/avian2d/Cargo.toml

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -27,13 +27,18 @@ f64 = []
2727

2828
debug-plugin = ["bevy/bevy_gizmos", "bevy/bevy_render"]
2929
simd = ["parry2d?/simd-stable", "parry2d-f64?/simd-stable"]
30-
parallel = ["parry2d?/parallel", "parry2d-f64?/parallel"]
30+
parallel = [
31+
"dep:thread_local",
32+
"bevy/multi_threaded",
33+
"parry2d?/parallel",
34+
"parry2d-f64?/parallel",
35+
]
3136
enhanced-determinism = [
3237
"dep:libm",
33-
"parry2d?/enhanced-determinism",
34-
"parry2d-f64?/enhanced-determinism",
3538
"bevy_math/libm",
3639
"bevy_heavy/libm",
40+
"parry2d?/enhanced-determinism",
41+
"parry2d-f64?/enhanced-determinism",
3742
]
3843

3944
default-collider = ["dep:nalgebra"]
@@ -81,11 +86,10 @@ parry2d-f64 = { version = "0.17", optional = true }
8186
nalgebra = { version = "0.33", features = ["convert-glam029"], optional = true }
8287
serde = { version = "1", features = ["derive"], optional = true }
8388
derive_more = "1"
84-
indexmap = "2.0.0"
8589
arrayvec = "0.7"
86-
fxhash = "0.2.1"
8790
itertools = "0.13"
8891
bitflags = "2.5.0"
92+
thread_local = { version = "1.1", optional = true }
8993

9094
[dev-dependencies]
9195
examples_common_2d = { path = "../examples_common_2d" }
@@ -152,6 +156,10 @@ required-features = ["2d", "default-collider"]
152156
name = "prismatic_joint_2d"
153157
required-features = ["2d", "default-collider"]
154158

159+
[[example]]
160+
name = "pyramid_2d"
161+
required-features = ["2d", "default-collider"]
162+
155163
[[example]]
156164
name = "ray_caster"
157165
required-features = ["2d", "default-collider"]

crates/avian2d/examples/custom_collider.rs

Lines changed: 13 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -76,8 +76,12 @@ impl AnyCollider for CircleCollider {
7676
position2: Vector,
7777
rotation2: impl Into<Rotation>,
7878
prediction_distance: Scalar,
79+
manifolds: &mut Vec<ContactManifold>,
7980
_: ContactManifoldContext<Self::Context>,
80-
) -> Vec<ContactManifold> {
81+
) {
82+
// Clear the previous manifolds.
83+
manifolds.clear();
84+
8185
let rotation1: Rotation = rotation1.into();
8286
let rotation2: Rotation = rotation2.into();
8387

@@ -98,22 +102,14 @@ impl AnyCollider for CircleCollider {
98102
let local_point1 = local_normal1 * self.radius;
99103
let local_point2 = local_normal2 * other.radius;
100104

101-
vec![ContactManifold::new(
102-
[ContactPoint {
103-
local_point1,
104-
local_point2,
105-
penetration: sum_radius - distance_squared.sqrt(),
106-
// Impulses are computed by the constraint solver.
107-
normal_impulse: 0.0,
108-
tangent_impulse: 0.0,
109-
feature_id1: PackedFeatureId::face(0),
110-
feature_id2: PackedFeatureId::face(0),
111-
}],
112-
rotation1 * local_normal1,
113-
0,
114-
)]
115-
} else {
116-
vec![]
105+
let point = ContactPoint::new(
106+
local_point1,
107+
local_point2,
108+
sum_radius - distance_squared.sqrt(),
109+
)
110+
.with_feature_ids(PackedFeatureId::face(0), PackedFeatureId::face(0));
111+
112+
manifolds.push(ContactManifold::new([point], rotation1 * local_normal1, 0));
117113
}
118114
}
119115
}

crates/avian2d/examples/determinism_2d.rs

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,8 @@ use avian2d::{
1919
prelude::*,
2020
};
2121
use bevy::{
22-
color::palettes::tailwind::CYAN_400, input::common_conditions::input_just_pressed, prelude::*,
23-
render::camera::ScalingMode,
22+
color::palettes::tailwind::CYAN_400, ecs::system::SystemParam,
23+
input::common_conditions::input_just_pressed, prelude::*, render::camera::ScalingMode,
2424
};
2525
use bytemuck::{Pod, Zeroable};
2626

@@ -34,12 +34,13 @@ fn main() {
3434
App::new()
3535
.add_plugins((
3636
DefaultPlugins,
37-
PhysicsPlugins::default().with_length_unit(0.5),
37+
PhysicsPlugins::default()
38+
.with_length_unit(0.5)
39+
.with_collision_hooks::<PhysicsHooks>(),
3840
PhysicsDebugPlugin::default(),
3941
))
4042
.init_resource::<Step>()
4143
.add_systems(Startup, (setup_scene, setup_ui))
42-
.add_systems(PostProcessCollisions, ignore_joint_collisions)
4344
.add_systems(FixedUpdate, update_hash)
4445
.add_systems(
4546
PreUpdate,
@@ -183,10 +184,22 @@ fn setup_ui(mut commands: Commands) {
183184
));
184185
}
185186

186-
// TODO: This should be an optimized built-in feature for joints.
187-
fn ignore_joint_collisions(joints: Query<&RevoluteJoint>, mut collisions: ResMut<Collisions>) {
188-
for joint in &joints {
189-
collisions.remove_collision_pair(joint.entity1, joint.entity2);
187+
#[derive(SystemParam)]
188+
pub struct PhysicsHooks<'w, 's> {
189+
joints: Query<'w, 's, &'static RevoluteJoint>,
190+
}
191+
192+
impl CollisionHooks for PhysicsHooks<'_, '_> {
193+
fn filter_pairs(&self, entity1: Entity, entity2: Entity, _commands: &mut Commands) -> bool {
194+
// Ignore the collision if the entities are connected by a joint.
195+
// TODO: This should be an optimized built-in feature for joints.
196+
self.joints
197+
.iter()
198+
.find(|joint| {
199+
(joint.entity1 == entity1 && joint.entity2 == entity2)
200+
|| (joint.entity1 == entity2 && joint.entity2 == entity1)
201+
})
202+
.is_some()
190203
}
191204
}
192205

crates/avian2d/examples/kinematic_character_2d/plugin.rs

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
1-
use avian2d::{math::*, prelude::*};
1+
use avian2d::{
2+
math::*,
3+
prelude::{narrow_phase::NarrowPhaseSet, *},
4+
};
25
use bevy::{ecs::query::Has, prelude::*};
36

47
pub struct CharacterControllerPlugin;
@@ -23,8 +26,8 @@ impl Plugin for CharacterControllerPlugin {
2326
//
2427
// NOTE: The collision implementation here is very basic and a bit buggy.
2528
// A collide-and-slide algorithm would likely work better.
26-
PostProcessCollisions,
27-
kinematic_controller_collisions,
29+
PhysicsSchedule,
30+
kinematic_controller_collisions.in_set(NarrowPhaseSet::Last),
2831
);
2932
}
3033
}
@@ -265,7 +268,7 @@ fn apply_movement_damping(mut query: Query<(&MovementDampingFactor, &mut LinearV
265268
/// and predict collisions using speculative contacts.
266269
#[allow(clippy::type_complexity)]
267270
fn kinematic_controller_collisions(
268-
collisions: Res<Collisions>,
271+
collisions: Collisions,
269272
bodies: Query<&RigidBody>,
270273
collider_rbs: Query<&ColliderOf, Without<Sensor>>,
271274
mut character_controllers: Query<
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
use avian2d::{math::Scalar, prelude::*};
2+
use bevy::{prelude::*, render::camera::ScalingMode};
3+
use examples_common_2d::ExampleCommonPlugin;
4+
5+
fn main() {
6+
let mut app = App::new();
7+
8+
app.add_plugins((
9+
DefaultPlugins,
10+
PhysicsPlugins::default(),
11+
ExampleCommonPlugin,
12+
));
13+
14+
app.add_systems(Startup, setup);
15+
16+
app.run();
17+
}
18+
19+
fn setup(
20+
mut commands: Commands,
21+
mut meshes: ResMut<Assets<Mesh>>,
22+
mut materials: ResMut<Assets<ColorMaterial>>,
23+
) {
24+
commands.spawn((
25+
Camera2d,
26+
Projection::Orthographic(OrthographicProjection {
27+
scaling_mode: ScalingMode::FixedHorizontal {
28+
viewport_width: 150.0,
29+
},
30+
..OrthographicProjection::default_2d()
31+
}),
32+
Transform::from_xyz(0.0, 30.0, 0.0),
33+
));
34+
35+
// Ground
36+
commands.spawn((
37+
RigidBody::Static,
38+
Collider::rectangle(800.0, 40.0),
39+
Transform::from_xyz(0.0, -20.0, 0.0),
40+
Mesh2d(meshes.add(Rectangle::new(800.0, 40.0))),
41+
MeshMaterial2d(materials.add(Color::srgb(0.3, 0.5, 0.3))),
42+
));
43+
44+
let base_count = 50;
45+
let h = 0.5;
46+
let box_size = 2.0 * h;
47+
let collider = Collider::rectangle(box_size as Scalar, box_size as Scalar);
48+
let shift = h;
49+
for i in 0..base_count {
50+
let y = (2.0 * i as f32 + 1.0) * shift * 0.99;
51+
52+
for j in i..base_count {
53+
let x = (i as f32 + 1.0) * shift + 2.0 * (j - i) as f32 * shift - h * base_count as f32;
54+
55+
commands.spawn((
56+
RigidBody::Dynamic,
57+
collider.clone(),
58+
Transform::from_xyz(x, y, 0.0),
59+
Mesh2d(meshes.add(Rectangle::new(box_size, box_size))),
60+
MeshMaterial2d(materials.add(Color::srgb(0.2, 0.7, 0.9))),
61+
));
62+
}
63+
}
64+
}

crates/avian2d/examples/sensor.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,8 @@ fn setup(
6767
Sensor,
6868
RigidBody::Static,
6969
Collider::rectangle(100.0, 100.0),
70+
// Enable collision events for this entity.
71+
CollisionEventsEnabled,
7072
// Read entities colliding with this entity.
7173
CollidingEntities::default(),
7274
));

crates/avian3d/Cargo.toml

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -28,13 +28,18 @@ f64 = []
2828

2929
debug-plugin = ["bevy/bevy_gizmos", "bevy/bevy_render"]
3030
simd = ["parry3d?/simd-stable", "parry3d-f64?/simd-stable"]
31-
parallel = ["parry3d?/parallel", "parry3d-f64?/parallel"]
31+
parallel = [
32+
"dep:thread_local",
33+
"bevy/multi_threaded",
34+
"parry3d?/parallel",
35+
"parry3d-f64?/parallel",
36+
]
3237
enhanced-determinism = [
3338
"dep:libm",
34-
"parry3d?/enhanced-determinism",
35-
"parry3d-f64?/enhanced-determinism",
3639
"bevy_math/libm",
3740
"bevy_heavy/libm",
41+
"parry3d?/enhanced-determinism",
42+
"parry3d-f64?/enhanced-determinism",
3843
]
3944

4045
default-collider = ["dep:nalgebra"]
@@ -83,11 +88,10 @@ parry3d-f64 = { version = "0.17", optional = true }
8388
nalgebra = { version = "0.33", features = ["convert-glam029"], optional = true }
8489
serde = { version = "1", features = ["derive"], optional = true }
8590
derive_more = "1"
86-
indexmap = "2.0.0"
8791
arrayvec = "0.7"
88-
fxhash = "0.2.1"
8992
itertools = "0.13"
9093
bitflags = "2.5.0"
94+
thread_local = { version = "1.1", optional = true }
9195

9296
[dev-dependencies]
9397
examples_common_3d = { path = "../examples_common_3d" }

crates/avian3d/examples/conveyor_belt.rs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -38,14 +38,14 @@ struct ConveyorHooks<'w, 's> {
3838

3939
// Implement the `CollisionHooks` trait for our custom system parameter.
4040
impl CollisionHooks for ConveyorHooks<'_, '_> {
41-
fn modify_contacts(&self, contacts: &mut Contacts, _commands: &mut Commands) -> bool {
41+
fn modify_contacts(&self, contact_pair: &mut Contacts, _commands: &mut Commands) -> bool {
4242
// Get the conveyor belt and its global transform.
4343
// We don't know which entity is the conveyor belt, if any, so we need to check both.
4444
// This also affects the sign used for the conveyor belt's speed to apply it in the correct direction.
4545
let (Ok((conveyor_belt, global_transform)), sign) = self
4646
.conveyor_query
47-
.get(contacts.entity1)
48-
.map_or((self.conveyor_query.get(contacts.entity2), 1.0), |q| {
47+
.get(contact_pair.entity1)
48+
.map_or((self.conveyor_query.get(contact_pair.entity2), 1.0), |q| {
4949
(Ok(q), -1.0)
5050
})
5151
else {
@@ -59,7 +59,7 @@ impl CollisionHooks for ConveyorHooks<'_, '_> {
5959

6060
// Iterate over all contact surfaces between the conveyor belt and the other collider,
6161
// and apply a relative velocity to simulate the movement of the conveyor belt's surface.
62-
for manifold in contacts.manifolds.iter_mut() {
62+
for manifold in contact_pair.manifolds.iter_mut() {
6363
let tangent_velocity = sign * conveyor_belt.speed * direction;
6464
manifold.tangent_velocity = tangent_velocity.adjust_precision();
6565
}

crates/avian3d/examples/custom_broad_phase.rs

Lines changed: 27 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use avian3d::{math::*, prelude::*};
1+
use avian3d::{data_structures::pair_key::PairKey, math::*, prelude::*};
22
use bevy::prelude::*;
33
use examples_common_3d::ExampleCommonPlugin;
44

@@ -7,7 +7,7 @@ fn main() {
77

88
app.add_plugins((DefaultPlugins, ExampleCommonPlugin));
99

10-
// Add PhysicsPlugins and replace the default broad phase with our custom broad phase.
10+
// Add `PhysicsPlugins` and replace the default broad phase with our custom broad phase.
1111
app.add_plugins(
1212
PhysicsPlugins::default()
1313
.build()
@@ -56,37 +56,43 @@ fn setup(
5656
));
5757
}
5858

59-
// Collects pairs of potentially colliding entities into the BroadCollisionPairs resource provided by the physics engine.
59+
/// Finds pairs of entities with overlapping `ColliderAabb`s
60+
/// and creates contact pairs for them in the `ContactGraph`.
61+
///
6062
// A brute force algorithm is used for simplicity.
6163
pub struct BruteForceBroadPhasePlugin;
6264

6365
impl Plugin for BruteForceBroadPhasePlugin {
6466
fn build(&self, app: &mut App) {
65-
// Initialize the resource that the collision pairs are added to.
66-
app.init_resource::<BroadCollisionPairs>();
67-
68-
// Make sure the PhysicsSchedule is available.
69-
let physics_schedule = app
70-
.get_schedule_mut(PhysicsSchedule)
71-
.expect("add PhysicsSchedule first");
72-
73-
// Add the broad phase system into the broad phase set
74-
physics_schedule.add_systems(collect_collision_pairs.in_set(PhysicsStepSet::BroadPhase));
67+
// Add the broad phase system into the broad phase set.
68+
app.add_systems(
69+
PhysicsSchedule,
70+
collect_collision_pairs.in_set(PhysicsStepSet::BroadPhase),
71+
);
7572
}
7673
}
7774

7875
fn collect_collision_pairs(
7976
bodies: Query<(Entity, &ColliderAabb, &RigidBody)>,
80-
mut broad_collision_pairs: ResMut<BroadCollisionPairs>,
77+
mut collisions: ResMut<ContactGraph>,
8178
) {
82-
// Clear old collision pairs.
83-
broad_collision_pairs.0.clear();
84-
8579
// Loop through all entity combinations and collect pairs of bodies with intersecting AABBs.
86-
for [(ent_a, aabb_a, rb_a), (ent_b, aabb_b, rb_b)] in bodies.iter_combinations() {
87-
// At least one of the bodies is dynamic and their AABBs intersect
88-
if (rb_a.is_dynamic() || rb_b.is_dynamic()) && aabb_a.intersects(aabb_b) {
89-
broad_collision_pairs.0.push((ent_a, ent_b));
80+
for [(entity1, aabb1, rb1), (entity2, aabb2, rb2)] in bodies.iter_combinations() {
81+
// At least one of the bodies is dynamic and their AABBs intersect.
82+
if (rb1.is_dynamic() || rb2.is_dynamic()) && aabb1.intersects(aabb2) {
83+
// Create a pair key from the entity indices.
84+
let key = PairKey::new(entity1.index(), entity2.index());
85+
86+
// Avoid duplicate pairs.
87+
if collisions.contains_key(&key) {
88+
continue;
89+
}
90+
91+
// Create a contact pair as non-touching.
92+
// The narrow phase will determine if the entities are touching and compute contact data.
93+
// NOTE: To handle sensors, collision hooks, and child colliders, you may need to configure
94+
// `flags` and other properties of the contact pair. This is not done here for simplicity.
95+
collisions.add_pair_with_key(Contacts::new(entity1, entity2), key);
9096
}
9197
}
9298
}

0 commit comments

Comments
 (0)