avian2d/collision/
broad_phase.rs

1//! Finds pairs of entities with overlapping [`ColliderAabb`]s to reduce
2//! the number of potential contacts for the [narrow phase](super::narrow_phase).
3//!
4//! See [`BroadPhasePlugin`].
5
6use core::marker::PhantomData;
7
8use crate::{
9    data_structures::pair_key::PairKey, dynamics::solver::joint_graph::JointGraph, prelude::*,
10};
11use bevy::{
12    ecs::{
13        entity::{EntityMapper, MapEntities},
14        entity_disabling::Disabled,
15        system::{StaticSystemParam, SystemParamItem, lifetimeless::Read},
16    },
17    prelude::*,
18};
19
20use super::{
21    CollisionDiagnostics,
22    contact_types::{ContactEdge, ContactEdgeFlags},
23};
24
25/// Finds pairs of entities with overlapping [`ColliderAabb`]s to reduce
26/// the number of potential contacts for the [narrow phase](super::narrow_phase).
27///
28/// A contact pair is created in the [`ContactGraph`] resource for each pair found.
29/// Removing and updating these pairs is left to the [narrow phase](super::narrow_phase).
30///
31/// Currently, the broad phase uses the [sweep and prune](https://en.wikipedia.org/wiki/Sweep_and_prune) algorithm.
32///
33/// The broad phase systems run in [`PhysicsStepSystems::BroadPhase`].
34///
35/// [`CollisionHooks`] can be provided with generics to apply custom filtering for collision pairs.
36pub struct BroadPhasePlugin<H: CollisionHooks = ()>(PhantomData<H>);
37
38impl<H: CollisionHooks> Default for BroadPhasePlugin<H> {
39    fn default() -> Self {
40        Self(PhantomData)
41    }
42}
43
44impl<H: CollisionHooks + 'static> Plugin for BroadPhasePlugin<H>
45where
46    for<'w, 's> SystemParamItem<'w, 's, H>: CollisionHooks,
47{
48    fn build(&self, app: &mut App) {
49        app.init_resource::<AabbIntervals>();
50
51        app.configure_sets(
52            PhysicsSchedule,
53            (
54                BroadPhaseSystems::First,
55                BroadPhaseSystems::UpdateStructures,
56                BroadPhaseSystems::CollectCollisions,
57                BroadPhaseSystems::Last,
58            )
59                .chain()
60                .in_set(PhysicsStepSystems::BroadPhase),
61        );
62
63        let physics_schedule = app
64            .get_schedule_mut(PhysicsSchedule)
65            .expect("add PhysicsSchedule first");
66
67        physics_schedule.add_systems(
68            (update_aabb_intervals, add_new_aabb_intervals)
69                .chain()
70                .in_set(BroadPhaseSystems::UpdateStructures),
71        );
72
73        physics_schedule
74            .add_systems(collect_collision_pairs::<H>.in_set(BroadPhaseSystems::CollectCollisions));
75
76        // TODO: Deduplicate these observers.
77        // Add colliders back to the broad phase when `Disabled` is removed.
78        app.add_observer(
79            |trigger: On<Remove, Disabled>,
80             // TODO: Use `Allows<T>` in Bevy 0.17.
81             query: Query<
82                AabbIntervalQueryData,
83                (
84                    Without<ColliderDisabled>,
85                    Or<(With<Disabled>, Without<Disabled>)>,
86                ),
87            >,
88             rbs: Query<(&RigidBody, Has<RigidBodyDisabled>)>,
89             mut intervals: ResMut<AabbIntervals>| {
90                let entity = trigger.entity;
91
92                // Re-enable the collider.
93                if let Ok((entity, collider_of, aabb, layers, is_sensor, events_enabled, hooks)) =
94                    query.get(entity)
95                {
96                    let flags = init_aabb_interval_flags(
97                        collider_of,
98                        &rbs,
99                        is_sensor,
100                        events_enabled,
101                        hooks,
102                    );
103                    let interval = (
104                        entity,
105                        collider_of.map_or(ColliderOf { body: entity }, |p| *p),
106                        *aabb,
107                        *layers,
108                        flags,
109                    );
110
111                    // Add the re-enabled collider to the intervals.
112                    intervals.0.push(interval);
113                }
114            },
115        );
116
117        // Add colliders back to the broad phase when `ColliderDisabled` is removed.
118        app.add_observer(
119            |trigger: On<Remove, ColliderDisabled>,
120             query: Query<AabbIntervalQueryData>,
121             rbs: Query<(&RigidBody, Has<RigidBodyDisabled>)>,
122             mut intervals: ResMut<AabbIntervals>| {
123                let entity = trigger.entity;
124
125                // Re-enable the collider.
126                if let Ok((entity, collider_of, aabb, layers, is_sensor, events_enabled, hooks)) =
127                    query.get(entity)
128                {
129                    let flags = init_aabb_interval_flags(
130                        collider_of,
131                        &rbs,
132                        is_sensor,
133                        events_enabled,
134                        hooks,
135                    );
136                    let interval = (
137                        entity,
138                        collider_of.map_or(ColliderOf { body: entity }, |p| *p),
139                        *aabb,
140                        *layers,
141                        flags,
142                    );
143
144                    // Add the re-enabled collider to the intervals.
145                    intervals.0.push(interval);
146                }
147            },
148        );
149    }
150
151    fn finish(&self, app: &mut App) {
152        // Register timer and counter diagnostics for collision detection.
153        app.register_physics_diagnostics::<CollisionDiagnostics>();
154    }
155}
156
157/// System sets for systems running in [`PhysicsStepSystems::BroadPhase`].
158#[derive(SystemSet, Clone, Copy, Debug, PartialEq, Eq, Hash)]
159pub enum BroadPhaseSystems {
160    /// Runs at the start of the broad phase. Empty by default.
161    First,
162    /// Updates acceleration structures and other data needed for broad phase collision detection.
163    UpdateStructures,
164    /// Finds pairs of entities with overlapping [`ColliderAabb`]s
165    /// and creates contact pairs for them in the [`ContactGraph`].
166    CollectCollisions,
167    /// Runs at the end of the broad phase. Empty by default.
168    Last,
169}
170
171/// A deprecated alias for [`BroadPhaseSystems`].
172#[deprecated(since = "0.4.0", note = "Renamed to `BroadPhaseSystems`")]
173pub type BroadPhaseSet = BroadPhaseSystems;
174
175/// Entities with [`ColliderAabb`]s sorted along an axis by their extents.
176#[derive(Resource, Default)]
177struct AabbIntervals(Vec<AabbInterval>);
178
179type AabbInterval = (
180    Entity,
181    ColliderOf,
182    ColliderAabb,
183    CollisionLayers,
184    AabbIntervalFlags,
185);
186
187bitflags::bitflags! {
188    /// Flags for AABB intervals in the broad phase.
189    #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
190    pub struct AabbIntervalFlags: u8 {
191        /// Set if the body is sleeping or static.
192        const IS_INACTIVE = 1 << 0;
193        /// Set if collision events are enabled for this entity.
194        const CONTACT_EVENTS = 1 << 1;
195        /// Set if the collider should generate contact constraints.
196        const GENERATE_CONSTRAINTS = 1 << 2;
197        /// Set if [`CollisionHooks::filter_pairs`] should be called for this entity.
198        const CUSTOM_FILTER = 1 << 3;
199        /// Set if [`CollisionHooks::modify_contacts`] should be called for this entity.
200        const MODIFY_CONTACTS = 1 << 4;
201    }
202}
203
204impl MapEntities for AabbIntervals {
205    fn map_entities<M: EntityMapper>(&mut self, entity_mapper: &mut M) {
206        for interval in self.0.iter_mut() {
207            interval.0 = entity_mapper.get_mapped(interval.0);
208        }
209    }
210}
211
212/// Updates [`AabbIntervals`] to keep them in sync with the [`ColliderAabb`]s.
213#[allow(clippy::type_complexity)]
214fn update_aabb_intervals(
215    aabbs: Query<
216        (
217            &ColliderAabb,
218            Option<&ColliderOf>,
219            &CollisionLayers,
220            Has<Sensor>,
221            Has<CollisionEventsEnabled>,
222            Option<&ActiveCollisionHooks>,
223            Has<Sleeping>,
224        ),
225        Without<ColliderDisabled>,
226    >,
227    rbs: Query<(&RigidBody, Has<RigidBodyDisabled>)>,
228    mut intervals: ResMut<AabbIntervals>,
229) {
230    intervals
231        .0
232        .retain_mut(|(collider_entity, collider_of, aabb, layers, flags)| {
233            if let Ok((
234                new_aabb,
235                new_collider_of,
236                new_layers,
237                is_sensor,
238                events_enabled,
239                hooks,
240                is_sleeping,
241            )) = aabbs.get(*collider_entity)
242            {
243                if !new_aabb.min.is_finite() || !new_aabb.max.is_finite() {
244                    return false;
245                }
246
247                *aabb = *new_aabb;
248                *collider_of = new_collider_of.map_or(
249                    ColliderOf {
250                        body: *collider_entity,
251                    },
252                    |p| *p,
253                );
254                *layers = *new_layers;
255
256                let rb = new_collider_of.and_then(|collider_of| rbs.get(collider_of.body).ok());
257                let is_static = rb.is_some_and(|(body, _)| body.is_static());
258                let is_disabled = rb.is_some_and(|(_, is_disabled)| is_disabled);
259
260                flags.set(AabbIntervalFlags::IS_INACTIVE, is_static || is_sleeping);
261                flags.set(AabbIntervalFlags::CONTACT_EVENTS, events_enabled);
262                flags.set(
263                    AabbIntervalFlags::GENERATE_CONSTRAINTS,
264                    !is_sensor && !is_disabled,
265                );
266                flags.set(
267                    AabbIntervalFlags::CUSTOM_FILTER,
268                    hooks.is_some_and(|h| h.contains(ActiveCollisionHooks::FILTER_PAIRS)),
269                );
270                flags.set(
271                    AabbIntervalFlags::MODIFY_CONTACTS,
272                    hooks.is_some_and(|h| h.contains(ActiveCollisionHooks::MODIFY_CONTACTS)),
273                );
274
275                true
276            } else {
277                false
278            }
279        });
280}
281
282type AabbIntervalQueryData = (
283    Entity,
284    Option<Read<ColliderOf>>,
285    Read<ColliderAabb>,
286    Read<CollisionLayers>,
287    Has<Sensor>,
288    Has<CollisionEventsEnabled>,
289    Option<Read<ActiveCollisionHooks>>,
290);
291
292// TODO: This is pretty gross and inefficient. This should be done with observers or hooks
293//       once we rework the broad phase.
294/// Adds new [`ColliderAabb`]s to [`AabbIntervals`].
295#[allow(clippy::type_complexity)]
296fn add_new_aabb_intervals(
297    added_aabbs: Query<AabbIntervalQueryData, (Added<ColliderAabb>, Without<ColliderDisabled>)>,
298    rbs: Query<(&RigidBody, Has<RigidBodyDisabled>)>,
299    mut intervals: ResMut<AabbIntervals>,
300) {
301    let aabbs = added_aabbs.iter().map(
302        |(entity, collider_of, aabb, layers, is_sensor, events_enabled, hooks)| {
303            let flags =
304                init_aabb_interval_flags(collider_of, &rbs, is_sensor, events_enabled, hooks);
305            (
306                entity,
307                collider_of.map_or(ColliderOf { body: entity }, |p| *p),
308                *aabb,
309                *layers,
310                flags,
311            )
312        },
313    );
314    intervals.0.extend(aabbs);
315}
316
317fn init_aabb_interval_flags(
318    collider_of: Option<&ColliderOf>,
319    rbs: &Query<(&RigidBody, Has<RigidBodyDisabled>)>,
320    is_sensor: bool,
321    events_enabled: bool,
322    hooks: Option<&ActiveCollisionHooks>,
323) -> AabbIntervalFlags {
324    let mut flags = AabbIntervalFlags::empty();
325    let rb = collider_of.and_then(|collider_of| rbs.get(collider_of.body).ok());
326    let is_static = rb.is_some_and(|(body, _)| body.is_static());
327    let is_body_disabled = rb.is_some_and(|(_, is_disabled)| is_disabled);
328    flags.set(AabbIntervalFlags::IS_INACTIVE, is_static);
329    flags.set(AabbIntervalFlags::CONTACT_EVENTS, events_enabled);
330    flags.set(
331        AabbIntervalFlags::GENERATE_CONSTRAINTS,
332        !is_sensor && !is_body_disabled,
333    );
334    flags.set(
335        AabbIntervalFlags::CUSTOM_FILTER,
336        hooks.is_some_and(|h| h.contains(ActiveCollisionHooks::FILTER_PAIRS)),
337    );
338    flags.set(
339        AabbIntervalFlags::MODIFY_CONTACTS,
340        hooks.is_some_and(|h| h.contains(ActiveCollisionHooks::MODIFY_CONTACTS)),
341    );
342    flags
343}
344
345/// Finds pairs of entities with overlapping [`ColliderAabb`]s
346/// and creates contact pairs for them in the [`ContactGraph`].
347fn collect_collision_pairs<H: CollisionHooks>(
348    intervals: ResMut<AabbIntervals>,
349    mut contact_graph: ResMut<ContactGraph>,
350    joint_graph: Res<JointGraph>,
351    hooks: StaticSystemParam<H>,
352    mut commands: Commands,
353    mut diagnostics: ResMut<CollisionDiagnostics>,
354) where
355    for<'w, 's> SystemParamItem<'w, 's, H>: CollisionHooks,
356{
357    let start = crate::utils::Instant::now();
358
359    sweep_and_prune::<H>(
360        intervals,
361        &mut contact_graph,
362        &joint_graph,
363        &mut hooks.into_inner(),
364        &mut commands,
365    );
366
367    diagnostics.broad_phase = start.elapsed();
368}
369
370/// Sorts the entities by their minimum extents along an axis and collects the entity pairs that have intersecting AABBs.
371///
372/// Sweep and prune exploits temporal coherence, as bodies are unlikely to move significantly between two simulation steps. Insertion sort is used, as it is good at sorting nearly sorted lists efficiently.
373fn sweep_and_prune<H: CollisionHooks>(
374    mut intervals: ResMut<AabbIntervals>,
375    contact_graph: &mut ContactGraph,
376    joint_graph: &JointGraph,
377    hooks: &mut H::Item<'_, '_>,
378    commands: &mut Commands,
379) where
380    for<'w, 's> SystemParamItem<'w, 's, H>: CollisionHooks,
381{
382    // Sort bodies along the x-axis using insertion sort, a sorting algorithm great for sorting nearly sorted lists.
383    insertion_sort(&mut intervals.0, |a, b| a.2.min.x > b.2.min.x);
384
385    // Find potential collisions by checking for AABB intersections along all axes.
386    // TODO: Find pairs in parallel, but create contact pairs serially for determinism.
387    for (i, (entity1, collider_of1, aabb1, layers1, flags1)) in intervals.0.iter().enumerate() {
388        for (entity2, collider_of2, aabb2, layers2, flags2) in intervals.0.iter().skip(i + 1) {
389            // x doesn't intersect; check this first so we can discard as soon as possible.
390            if aabb2.min.x > aabb1.max.x {
391                break;
392            }
393
394            // y doesn't intersect.
395            if aabb1.min.y > aabb2.max.y || aabb1.max.y < aabb2.min.y {
396                continue;
397            }
398
399            #[cfg(feature = "3d")]
400            // z doesn't intersect.
401            if aabb1.min.z > aabb2.max.z || aabb1.max.z < aabb2.min.z {
402                continue;
403            }
404
405            // No collisions between bodies that haven't moved or colliders with incompatible layers
406            // or colliders attached to the same rigid body.
407            if flags1
408                .intersection(*flags2)
409                .contains(AabbIntervalFlags::IS_INACTIVE)
410                || !layers1.interacts_with(*layers2)
411                || collider_of1 == collider_of2
412            {
413                continue;
414            }
415
416            // Avoid duplicate pairs.
417            let pair_key = PairKey::new(entity1.index(), entity2.index());
418            if contact_graph.contains_key(&pair_key) {
419                continue;
420            }
421
422            // Check if a joint disables contacts between the two bodies.
423            if joint_graph
424                .joints_between(collider_of1.body, collider_of2.body)
425                .any(|edge| edge.collision_disabled)
426            {
427                continue;
428            }
429
430            // Apply user-defined filter.
431            if flags1
432                .union(*flags2)
433                .contains(AabbIntervalFlags::CUSTOM_FILTER)
434            {
435                let should_collide = hooks.filter_pairs(*entity1, *entity2, commands);
436                if !should_collide {
437                    continue;
438                }
439            }
440
441            // Create a new contact pair as non-touching.
442            // The narrow phase will determine if the entities are touching and compute contact data.
443            let mut contact_edge = ContactEdge::new(*entity1, *entity2);
444            contact_edge.body1 = Some(collider_of1.body);
445            contact_edge.body2 = Some(collider_of2.body);
446            contact_edge.flags.set(
447                ContactEdgeFlags::CONTACT_EVENTS,
448                flags1
449                    .union(*flags2)
450                    .contains(AabbIntervalFlags::CONTACT_EVENTS),
451            );
452            contact_graph
453                .add_edge_and_key_with(contact_edge, pair_key, |contact_pair| {
454                    contact_pair.body1 = Some(collider_of1.body);
455                    contact_pair.body2 = Some(collider_of2.body);
456                    contact_pair.flags.set(
457                        ContactPairFlags::MODIFY_CONTACTS,
458                        flags1
459                            .union(*flags2)
460                            .contains(AabbIntervalFlags::MODIFY_CONTACTS),
461                    );
462                    contact_pair.flags.set(
463                        ContactPairFlags::GENERATE_CONSTRAINTS,
464                        flags1
465                            .union(*flags2)
466                            .contains(AabbIntervalFlags::GENERATE_CONSTRAINTS),
467                    );
468                })
469                .unwrap_or_else(|| {
470                    panic!("Pair key already exists in contact graph: {pair_key:?}")
471                });
472        }
473    }
474}
475
476/// Sorts a list iteratively using comparisons. In an ascending sort order, when a smaller value is encountered, it is moved lower in the list until it is larger than the item before it.
477///
478/// This is relatively slow for large lists, but very efficient in cases where the list is already mostly sorted.
479fn insertion_sort<T>(items: &mut [T], comparison: fn(&T, &T) -> bool) {
480    for i in 1..items.len() {
481        let mut j = i;
482        while j > 0 && comparison(&items[j - 1], &items[j]) {
483            items.swap(j - 1, j);
484            j -= 1;
485        }
486    }
487}