Skip to main content

avian3d/collision/narrow_phase/
mod.rs

1//! Updates and manages contact pairs between colliders.
2//!
3//! # Overview
4//!
5//! Before the narrow phase, the [broad phase](super::broad_phase) creates a contact pair
6//! in the [`ContactGraph`] resource for each pair of intersecting [`ColliderAabb`]s.
7//!
8//! The narrow phase then determines which contact pairs found in the [`ContactGraph`] are touching,
9//! and computes updated contact points and normals in a parallel loop.
10//!
11//! Afterwards, the narrow phase removes contact pairs whose AABBs no longer overlap,
12//! and writes collision events for colliders that started or stopped touching.
13//! This is done in a fast serial loop to preserve determinism.
14//!
15//! The [solver](dynamics::solver) then generates a [`ContactConstraint`]
16//! for each contact pair that is touching or expected to touch during the time step.
17//!
18//! [`ContactConstraint`]: dynamics::solver::contact::ContactConstraint
19
20mod system_param;
21use system_param::ContactStatusBits;
22#[cfg(feature = "parallel")]
23use system_param::NarrowPhaseThreadLocals;
24pub use system_param::{ContactStatusChange, ContactStatusChangeQueue, NarrowPhase};
25
26use core::marker::PhantomData;
27
28use crate::{dynamics::joints::joint_graph::JointGraph, prelude::*};
29use bevy::{
30    ecs::{
31        entity_disabling::Disabled,
32        intern::Interned,
33        schedule::ScheduleLabel,
34        system::{StaticSystemParam, SystemParam, SystemParamItem, SystemState},
35    },
36    prelude::*,
37};
38
39use super::{CollisionDiagnostics, contact_types::ContactEdgeFlags};
40
41/// A [narrow phase](crate::collision::narrow_phase) plugin for updating and managing contact pairs
42/// between colliders of type `C`.
43///
44/// See the [module-level documentation](crate::collision::narrow_phase) for more information.
45///
46/// # Collider Types
47///
48/// The plugin takes a collider type. This should be [`Collider`] for
49/// the vast majority of applications, but for custom collision backends
50/// you may use any collider that implements the [`AnyCollider`] trait.
51pub struct NarrowPhasePlugin<C: AnyCollider, H: CollisionHooks = ()> {
52    schedule: Interned<dyn ScheduleLabel>,
53    _phantom: PhantomData<(C, H)>,
54}
55
56impl<C: AnyCollider, H: CollisionHooks> NarrowPhasePlugin<C, H> {
57    /// Creates a [`NarrowPhasePlugin`] with the schedule used for running its systems.
58    ///
59    /// The default schedule is [`PhysicsSchedule`].
60    pub fn new(schedule: impl ScheduleLabel) -> Self {
61        Self {
62            schedule: schedule.intern(),
63            _phantom: PhantomData,
64        }
65    }
66}
67
68impl<C: AnyCollider, H: CollisionHooks> Default for NarrowPhasePlugin<C, H> {
69    fn default() -> Self {
70        Self::new(PhysicsSchedule)
71    }
72}
73
74/// A resource that indicates that the narrow phase has been initialized.
75///
76/// This is used to ensure that some systems are only added once
77/// even with multiple collider types.
78#[derive(Resource, Default)]
79struct NarrowPhaseInitialized;
80
81impl<C: AnyCollider, H: CollisionHooks + 'static> Plugin for NarrowPhasePlugin<C, H>
82where
83    for<'w, 's> SystemParamItem<'w, 's, H>: CollisionHooks,
84{
85    fn build(&self, app: &mut App) {
86        let already_initialized = app.world().is_resource_added::<NarrowPhaseInitialized>();
87
88        app.init_resource::<NarrowPhaseConfig>()
89            .init_resource::<ContactGraph>()
90            .init_resource::<JointGraph>()
91            .init_resource::<ContactStatusBits>()
92            .init_resource::<ContactStatusChangeQueue>()
93            .init_resource::<DefaultFriction>()
94            .init_resource::<DefaultRestitution>();
95
96        #[cfg(feature = "parallel")]
97        app.init_resource::<NarrowPhaseThreadLocals>();
98
99        app.add_message::<CollisionStart>()
100            .add_message::<CollisionEnd>();
101
102        // Set up system set scheduling.
103        app.configure_sets(
104            self.schedule,
105            (
106                NarrowPhaseSystems::First,
107                NarrowPhaseSystems::Update,
108                NarrowPhaseSystems::Last,
109            )
110                .chain()
111                .in_set(PhysicsStepSystems::NarrowPhase),
112        );
113        app.configure_sets(
114            self.schedule,
115            CollisionEventSystems.in_set(PhysicsStepSystems::Finalize),
116        );
117
118        // Perform narrow phase collision detection.
119        app.add_systems(
120            self.schedule,
121            update_narrow_phase::<C, H>
122                .in_set(NarrowPhaseSystems::Update)
123                // Allowing ambiguities is required so that it's possible
124                // to have multiple collision backends at the same time.
125                .ambiguous_with_all(),
126        );
127
128        if !already_initialized {
129            // Remove collision pairs when colliders are disabled or removed.
130            app.add_observer(remove_collider_on::<Add<(Disabled, ColliderDisabled)>>);
131            app.add_observer(remove_collider_on::<Remove<ColliderMarker>>);
132
133            // Add colliders to the constraint graph when `Sensor` is removed,
134            // and remove them when `Sensor` is added.
135            // TODO: If we separate sensors from normal colliders, this won't be needed.
136            app.add_observer(on_add_sensor);
137            app.add_observer(on_remove_sensor);
138
139            // Add contacts to the constraint graph when a body is enabled,
140            // and remove them when a body is disabled.
141            app.add_observer(on_body_remove_rigid_body_disabled);
142            app.add_observer(on_disable_body);
143
144            // Remove contacts when the body body is disabled or `RigidBody` is replaced or removed.
145            app.add_observer(remove_body_on::<Insert<RigidBody>>);
146            app.add_observer(remove_body_on::<Remove<RigidBody>>);
147
148            // Trigger collision events for colliders that started or stopped touching.
149            app.add_systems(
150                self.schedule,
151                trigger_collision_events
152                    .in_set(CollisionEventSystems)
153                    // TODO: Ideally we don't need to make this ambiguous, but currently it is
154                    //       to avoid conflicts since the system has exclusive world access.
155                    .ambiguous_with(PhysicsStepSystems::Finalize),
156            );
157        }
158
159        app.init_resource::<NarrowPhaseInitialized>();
160    }
161
162    fn finish(&self, app: &mut App) {
163        // Register timer and counter diagnostics for collision detection.
164        app.register_physics_diagnostics::<CollisionDiagnostics>();
165    }
166}
167
168/// A system set for triggering the [`CollisionStart`] and [`CollisionEnd`] events.
169///
170/// Runs in [`PhysicsStepSystems::Finalize`], after the solver has run and contact impulses
171/// have been computed and applied.
172#[derive(SystemSet, Clone, Copy, Debug, PartialEq, Eq, Hash)]
173pub struct CollisionEventSystems;
174
175/// A resource for configuring the [narrow phase](NarrowPhasePlugin).
176#[derive(Resource, Reflect, Clone, Debug, PartialEq)]
177#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
178#[cfg_attr(feature = "serialize", reflect(Serialize, Deserialize))]
179#[reflect(Debug, Resource, PartialEq)]
180pub struct NarrowPhaseConfig {
181    /// A small, positive contact tolerance to help ensure that contacts are not missed
182    /// due to numerical issues or solver jitter for objects that are in continuous
183    /// contact, such as pushing against each other.
184    ///
185    /// This is implicitly scaled by the [`PhysicsLengthUnit`].
186    ///
187    /// Default: `0.02`
188    pub contact_tolerance: f32,
189
190    /// The distance at which contact points can be recycled from the previous frame
191    /// to the current frame.
192    ///
193    /// Setting this to zero will disable contact recycling.
194    ///
195    /// This is implicitly scaled by the [`PhysicsLengthUnit`].
196    ///
197    /// Default: `0.05`
198    pub recycle_distance: f32,
199
200    /// The angle (in radians) between the previous contact normal and the current contact normal
201    /// at which contact points can be recycled from the previous frame to the current frame.
202    ///
203    #[cfg_attr(feature = "2d", doc = "Default: `0.2` (approximately 11.5 degrees)")]
204    #[cfg_attr(feature = "3d", doc = "Default: 0.175 (approximately 10 degrees)")]
205    pub recycle_angle: f32,
206
207    /// If `true`, the current contacts will be matched with the previous contacts
208    /// based on feature IDs or contact positions, and the contact impulses from
209    /// the previous frame will be copied over for the new contacts.
210    ///
211    /// Using these impulses as the initial guess is referred to as *warm starting*,
212    /// and it can help the contact solver resolve overlap and stabilize much faster.
213    ///
214    /// Default: `true`
215    pub match_contacts: bool,
216}
217
218impl Default for NarrowPhaseConfig {
219    fn default() -> Self {
220        Self {
221            // TODO: Investigate if this could be smaller
222            contact_tolerance: 0.02,
223            recycle_distance: 0.05,
224            // NOTE: These defaults are from Box2D and Box3D
225            #[cfg(feature = "2d")]
226            recycle_angle: 0.2,
227            #[cfg(feature = "3d")]
228            recycle_angle: 0.175,
229            match_contacts: true,
230        }
231    }
232}
233
234/// System sets for systems running in [`PhysicsStepSystems::NarrowPhase`].
235#[derive(SystemSet, Clone, Copy, Debug, PartialEq, Eq, Hash)]
236pub enum NarrowPhaseSystems {
237    /// Runs at the start of the narrow phase. Empty by default.
238    First,
239    /// Updates contacts in the [`ContactGraph`] and processes contact state changes.
240    Update,
241    /// Runs at the end of the narrow phase. Empty by default.
242    Last,
243}
244
245/// A deprecated alias for [`NarrowPhaseSystems`].
246#[deprecated(since = "0.4.0", note = "Renamed to `NarrowPhaseSystems`")]
247pub type NarrowPhaseSet = NarrowPhaseSystems;
248
249fn update_narrow_phase<C: AnyCollider, H: CollisionHooks + 'static>(
250    mut narrow_phase: NarrowPhase<C>,
251    mut collision_started_writer: MessageWriter<CollisionStart>,
252    mut collision_ended_writer: MessageWriter<CollisionEnd>,
253    time: Res<Time>,
254    hooks: StaticSystemParam<H>,
255    context: StaticSystemParam<C::Context>,
256    mut commands: ParallelCommands,
257    mut diagnostics: ResMut<CollisionDiagnostics>,
258) where
259    for<'w, 's> SystemParamItem<'w, 's, H>: CollisionHooks,
260{
261    let start = crate::utils::Instant::now();
262
263    narrow_phase.update::<H>(
264        &mut collision_started_writer,
265        &mut collision_ended_writer,
266        time.delta_secs(),
267        &hooks,
268        &context,
269        &mut diagnostics,
270        &mut commands,
271    );
272
273    diagnostics.narrow_phase = start.elapsed();
274    diagnostics.contact_count = narrow_phase.contact_graph.edges.edge_count() as u32;
275}
276
277#[derive(SystemParam)]
278struct TriggerCollisionEventsContext<'w, 's> {
279    query: Query<'w, 's, Has<CollisionEventsEnabled>>,
280    started: MessageReader<'w, 's, CollisionStart>,
281    ended: MessageReader<'w, 's, CollisionEnd>,
282}
283
284/// Triggers [`CollisionStart`] and [`CollisionEnd`] events for colliders
285/// that started or stopped touching and have the [`CollisionEventsEnabled`] component.
286fn trigger_collision_events(
287    // We use exclusive access here to avoid queuing a new command for each event.
288    world: &mut World,
289    state: &mut SystemState<TriggerCollisionEventsContext>,
290    // Cache pairs in buffers to avoid reallocating every time.
291    mut started: Local<Vec<CollisionStart>>,
292    mut ended: Local<Vec<CollisionEnd>>,
293) {
294    let mut state = state.get_mut(world).unwrap();
295
296    // Collect `CollisionStart` events.
297    for event in state.started.read() {
298        let Ok([events_enabled1, events_enabled2]) =
299            state.query.get_many([event.collider1, event.collider2])
300        else {
301            continue;
302        };
303
304        if events_enabled1 {
305            started.push(CollisionStart {
306                collider1: event.collider1,
307                collider2: event.collider2,
308                body1: event.body1,
309                body2: event.body2,
310            });
311        }
312        if events_enabled2 {
313            started.push(CollisionStart {
314                collider1: event.collider2,
315                collider2: event.collider1,
316                body1: event.body2,
317                body2: event.body1,
318            });
319        }
320    }
321
322    // Collect `CollisionEnd` events.
323    for event in state.ended.read() {
324        let Ok([events_enabled1, events_enabled2]) =
325            state.query.get_many([event.collider1, event.collider2])
326        else {
327            continue;
328        };
329
330        if events_enabled1 {
331            ended.push(CollisionEnd {
332                collider1: event.collider1,
333                collider2: event.collider2,
334                body1: event.body1,
335                body2: event.body2,
336            });
337        }
338        if events_enabled2 {
339            ended.push(CollisionEnd {
340                collider1: event.collider2,
341                collider2: event.collider1,
342                body1: event.body2,
343                body2: event.body1,
344            });
345        }
346    }
347
348    // Trigger the events, draining the buffers in the process.
349    started.drain(..).for_each(|event| {
350        world.trigger(event);
351    });
352    ended.drain(..).for_each(|event| {
353        world.trigger(event);
354    });
355}
356
357// ===============================================================
358// The rest of this module contains observers and helper functions
359// for updating the contact graph when bodies or colliders are
360// added/removed or enabled/disabled.
361// ===============================================================
362
363// Cases to consider:
364// - Collider is removed -> remove all contacts
365// - Collider is disabled -> remove all contacts
366// - Collider becomes a sensor -> remove all touching contacts from constraint graph and islands
367// - Collider stops being a sensor -> add all touching contacts to constraint graph and islands
368// - Body is removed -> remove all touching contacts from constraint graph and islands
369// - Body is disabled -> remove all touching contacts from constraint graph and islands
370// - Body is enabled -> add all touching contacts to constraint graph and islands
371// - Body becomes static -> remove all static-static contacts
372
373/// Removes a collider from the [`ContactGraph`].
374///
375/// Also removes the collider from the [`CollidingEntities`] of the other entity
376/// and writes a [`CollisionEnd`] event.
377///
378/// The constraint graph and island bookkeeping are *not* updated here.
379/// Instead, a [`ContactStatusChange`] is recorded for each touching contact.
380/// This keeps the narrow phase and solver decoupled.
381fn remove_collider(
382    entity: Entity,
383    contact_graph: &mut ContactGraph,
384    contact_status_changes: &mut ContactStatusChangeQueue,
385    colliding_entities_query: &mut Query<&mut CollidingEntities, Allow<Disabled>>,
386    message_writer: &mut MessageWriter<CollisionEnd>,
387) {
388    contact_graph.remove_collider_with(entity, |contact_graph, contact_id| {
389        // Get the contact edge.
390        let contact_edge = contact_graph.edge_weight(contact_id.into()).unwrap();
391
392        // If the contact pair was not touching, we don't need to do anything.
393        if !contact_edge.flags.contains(ContactEdgeFlags::TOUCHING) {
394            return;
395        }
396
397        // Send a collision ended event.
398        if contact_edge
399            .flags
400            .contains(ContactEdgeFlags::CONTACT_EVENTS)
401        {
402            message_writer.write(CollisionEnd {
403                collider1: contact_edge.collider1,
404                collider2: contact_edge.collider2,
405                body1: contact_edge.body1,
406                body2: contact_edge.body2,
407            });
408        }
409
410        // Remove the entity from the `CollidingEntities` of the other entity.
411        let other_entity = if contact_edge.collider1 == entity {
412            contact_edge.collider2
413        } else {
414            contact_edge.collider1
415        };
416        if let Ok(mut colliding_entities) = colliding_entities_query.get_mut(other_entity) {
417            colliding_entities.remove(&entity);
418        }
419
420        if let (Some(body1), Some(body2)) = (contact_edge.body1, contact_edge.body2) {
421            contact_status_changes.push(ContactStatusChange::StoppedGeneratingConstraints {
422                contact_id,
423                body1,
424                body2,
425            });
426        }
427    });
428}
429
430/// Removes contacts from the [`ContactGraph`] when a body is removed.
431fn remove_body_on<E: EventPattern<Event: EntityEvent>>(
432    trigger: On<E>,
433    body_collider_query: Query<&RigidBodyColliders>,
434    mut colliding_entities_query: Query<&mut CollidingEntities, Allow<Disabled>>,
435    mut message_writer: MessageWriter<CollisionEnd>,
436    mut contact_status_changes: ResMut<ContactStatusChangeQueue>,
437    mut contact_graph: ResMut<ContactGraph>,
438) {
439    let Ok(colliders) = body_collider_query.get(trigger.event_target()) else {
440        return;
441    };
442
443    // TODO: Only remove static-static contacts and unlink from islands.
444    for collider in colliders {
445        remove_collider(
446            collider,
447            &mut contact_graph,
448            &mut contact_status_changes,
449            &mut colliding_entities_query,
450            &mut message_writer,
451        );
452    }
453}
454
455/// Removes colliders from the [`ContactGraph`] when the given trigger is activated.
456///
457/// Also removes the collider from the [`CollidingEntities`] of the other entity,
458/// wakes up the other body, and writes a [`CollisionEnd`] event.
459fn remove_collider_on<E: EventPattern<Event: EntityEvent>>(
460    trigger: On<E>,
461    mut contact_graph: ResMut<ContactGraph>,
462    mut contact_status_changes: ResMut<ContactStatusChangeQueue>,
463    mut query: Query<&mut CollidingEntities, Allow<Disabled>>,
464    mut message_writer: MessageWriter<CollisionEnd>,
465) {
466    let entity = trigger.event_target();
467
468    // Remove the collider from the contact graph.
469    remove_collider(
470        entity,
471        &mut contact_graph,
472        &mut contact_status_changes,
473        &mut query,
474        &mut message_writer,
475    );
476}
477
478/// Removes the touching contacts of a body from the [`ContactGraph`] when the body
479/// is enabled by removing [`RigidBodyDisabled`], so that they are re-created fresh.
480fn on_body_remove_rigid_body_disabled(
481    trigger: On<Remove<RigidBodyDisabled>>,
482    body_collider_query: Query<&RigidBodyColliders>,
483    mut contact_status_changes: ResMut<ContactStatusChangeQueue>,
484    mut contact_graph: ResMut<ContactGraph>,
485    mut colliding_entities_query: Query<&mut CollidingEntities, Allow<Disabled>>,
486    mut message_writer: MessageWriter<CollisionEnd>,
487) {
488    let Ok(colliders) = body_collider_query.get(trigger.entity) else {
489        return;
490    };
491
492    for collider in colliders {
493        remove_collider(
494            collider,
495            &mut contact_graph,
496            &mut contact_status_changes,
497            &mut colliding_entities_query,
498            &mut message_writer,
499        );
500    }
501}
502
503/// Removes the touching contacts of a body from the [`ContactGraph`]
504/// when the body is disabled with [`Disabled`] or [`RigidBodyDisabled`].
505fn on_disable_body(
506    trigger: On<Add<(Disabled, RigidBodyDisabled)>>,
507    body_collider_query: Query<&RigidBodyColliders, Allow<Disabled>>,
508    mut contact_status_changes: ResMut<ContactStatusChangeQueue>,
509    mut contact_graph: ResMut<ContactGraph>,
510    mut colliding_entities_query: Query<&mut CollidingEntities, Allow<Disabled>>,
511    mut message_writer: MessageWriter<CollisionEnd>,
512) {
513    let Ok(colliders) = body_collider_query.get(trigger.entity) else {
514        return;
515    };
516
517    for collider in colliders {
518        remove_collider(
519            collider,
520            &mut contact_graph,
521            &mut contact_status_changes,
522            &mut colliding_entities_query,
523            &mut message_writer,
524        );
525    }
526}
527
528// TODO: These are currently used just for sensors. It wouldn't be needed if sensor logic
529//       was separate from normal colliders and didn't compute contact manifolds.
530
531/// Removes the touching contacts of a collider from the [`ContactGraph`]
532/// when a collider becomes a [`Sensor`].
533fn on_add_sensor(
534    trigger: On<Add<Sensor>>,
535    mut contact_status_changes: ResMut<ContactStatusChangeQueue>,
536    mut contact_graph: ResMut<ContactGraph>,
537    mut colliding_entities_query: Query<&mut CollidingEntities, Allow<Disabled>>,
538    mut message_writer: MessageWriter<CollisionEnd>,
539) {
540    remove_collider(
541        trigger.entity,
542        &mut contact_graph,
543        &mut contact_status_changes,
544        &mut colliding_entities_query,
545        &mut message_writer,
546    );
547}
548
549/// Removes the touching contacts of a collider from the [`ContactGraph`]
550/// when a collider stops being a [`Sensor`], so that they are re-created fresh.
551fn on_remove_sensor(
552    trigger: On<Remove<Sensor>>,
553    mut contact_status_changes: ResMut<ContactStatusChangeQueue>,
554    mut contact_graph: ResMut<ContactGraph>,
555    mut colliding_entities_query: Query<&mut CollidingEntities, Allow<Disabled>>,
556    mut message_writer: MessageWriter<CollisionEnd>,
557) {
558    remove_collider(
559        trigger.entity,
560        &mut contact_graph,
561        &mut contact_status_changes,
562        &mut colliding_entities_query,
563        &mut message_writer,
564    );
565}