rapier3d/geometry/
broad_phase_bvh.rs

1use crate::dynamics::{IntegrationParameters, RigidBodySet};
2use crate::geometry::{BroadPhase, BroadPhasePairEvent, ColliderHandle, ColliderPair, ColliderSet};
3use parry::bounding_volume::BoundingVolume;
4use parry::partitioning::{Bvh, BvhWorkspace};
5use parry::utils::hashmap::{Entry, HashMap};
6
7/// A broad-phase based on parry’s [`Bvh`] data structure.
8#[derive(Default, Clone)]
9#[cfg_attr(feature = "serde-serialize", derive(Serialize, Deserialize))]
10pub struct BroadPhaseBvh {
11    pub(crate) tree: Bvh,
12    #[cfg_attr(feature = "serde-serialize", serde(skip))]
13    workspace: BvhWorkspace,
14    pairs: HashMap<(ColliderHandle, ColliderHandle), u32>,
15    frame_index: u32,
16    optimization_strategy: BvhOptimizationStrategy,
17}
18
19// TODO: would be interesting to try out:
20// "Fast Insertion-Based Optimization of Bounding Volume Hierarchies"
21// by Bittner et al.
22/// Selection of strategies to maintain through time the broad-phase BVH in shape that remains
23/// efficient for collision-detection and scene queries.
24#[cfg_attr(feature = "serde-serialize", derive(Serialize, Deserialize))]
25#[derive(Default, PartialEq, Eq, Copy, Clone)]
26pub enum BvhOptimizationStrategy {
27    /// Different sub-trees of the BVH will be optimized at each frame.
28    #[default]
29    SubtreeOptimizer,
30    /// Disables incremental BVH optimization (discouraged).
31    ///
32    /// This should not be used except for debugging purpose.
33    None,
34}
35
36const ENABLE_TREE_VALIDITY_CHECK: bool = false;
37
38impl BroadPhaseBvh {
39    /// Initializes a new empty broad-phase.
40    pub fn new() -> Self {
41        Self::default()
42    }
43
44    /// Initializes a new empty broad-phase with the specified strategy for incremental
45    /// BVH optimization.
46    pub fn with_optimization_strategy(optimization_strategy: BvhOptimizationStrategy) -> Self {
47        Self {
48            optimization_strategy,
49            ..Default::default()
50        }
51    }
52
53    fn update_with_strategy(
54        &mut self,
55        params: &IntegrationParameters,
56        colliders: &ColliderSet,
57        bodies: &RigidBodySet,
58        modified_colliders: &[ColliderHandle],
59        removed_colliders: &[ColliderHandle],
60        events: &mut Vec<BroadPhasePairEvent>,
61        strategy: BvhOptimizationStrategy,
62    ) {
63        const CHANGE_DETECTION_ENABLED: bool = true;
64
65        self.frame_index = self.frame_index.overflowing_add(1).0;
66
67        // Removals must be handled first, in case another collider in
68        // `modified_colliders` shares the same index.
69        for handle in removed_colliders {
70            self.tree.remove(handle.into_raw_parts().0);
71        }
72
73        if modified_colliders.is_empty() {
74            return;
75        }
76
77        let first_pass = self.tree.is_empty();
78
79        // let t0 = std::time::Instant::now();
80        for modified in modified_colliders {
81            if let Some(collider) = colliders.get(*modified) {
82                if !collider.is_enabled() || !collider.changes.needs_broad_phase_update() {
83                    continue;
84                }
85
86                // Take soft-ccd into account by growing the aabb.
87                let next_pose = collider.parent.and_then(|p| {
88                    let parent = bodies.get(p.handle)?;
89                    (parent.soft_ccd_prediction() > 0.0).then(|| {
90                        parent.predict_position_using_velocity_and_forces_with_max_dist(
91                            params.dt,
92                            parent.soft_ccd_prediction(),
93                        ) * p.pos_wrt_parent
94                    })
95                });
96
97                let prediction_distance = params.prediction_distance();
98                let mut aabb = collider.compute_collision_aabb(prediction_distance / 2.0);
99                if let Some(next_pose) = next_pose {
100                    let next_aabb = collider
101                        .shape
102                        .compute_aabb(&next_pose)
103                        .loosened(collider.contact_skin() + prediction_distance / 2.0);
104                    aabb.merge(&next_aabb);
105                }
106
107                let change_detection_skin = if CHANGE_DETECTION_ENABLED {
108                    1.0e-2 * params.length_unit
109                } else {
110                    0.0
111                };
112
113                self.tree.insert_or_update_partially(
114                    aabb,
115                    modified.into_raw_parts().0,
116                    change_detection_skin,
117                );
118            }
119        }
120
121        if ENABLE_TREE_VALIDITY_CHECK {
122            if first_pass {
123                self.tree.assert_well_formed();
124            }
125
126            self.tree.assert_well_formed_topology_only();
127        }
128
129        // let t0 = std::time::Instant::now();
130        match strategy {
131            BvhOptimizationStrategy::SubtreeOptimizer => {
132                self.tree.optimize_incremental(&mut self.workspace);
133            }
134            BvhOptimizationStrategy::None => {}
135        };
136        // println!(
137        //     "Incremental optimization: {}",
138        //     t0.elapsed().as_secs_f32() * 1000.0
139        // );
140
141        // NOTE: we run refit after optimization so we can skip updating internal nodes during
142        //       optimization, and so we can reorder the tree in memory (in depth-first order)
143        //       to make it more cache friendly after the rebuild shuffling everything around.
144        // let t0 = std::time::Instant::now();
145        self.tree.refit(&mut self.workspace);
146
147        if ENABLE_TREE_VALIDITY_CHECK {
148            self.tree.assert_well_formed();
149        }
150
151        // println!("Refit: {}", t0.elapsed().as_secs_f32() * 1000.0);
152        // println!(
153        //     "leaf count: {}/{} (changed: {})",
154        //     self.tree.leaf_count(),
155        //     self.tree.reachable_leaf_count(0),
156        //     self.tree.changed_leaf_count(0),
157        // );
158        // self.tree.assert_is_depth_first();
159        // self.tree.assert_well_formed();
160        // println!(
161        //     "Is well formed. Tree height: {}",
162        //     self.tree.subtree_height(0),
163        // );
164        // // println!("Tree quality: {}", self.tree.quality_metric());
165
166        let mut pairs_collector = |co1: u32, co2: u32| {
167            assert_ne!(co1, co2);
168
169            let Some((_, mut handle1)) = colliders.get_unknown_gen(co1) else {
170                return;
171            };
172            let Some((_, mut handle2)) = colliders.get_unknown_gen(co2) else {
173                return;
174            };
175
176            if co1 > co2 {
177                std::mem::swap(&mut handle1, &mut handle2);
178            }
179
180            match self.pairs.entry((handle1, handle2)) {
181                Entry::Occupied(e) => *e.into_mut() = self.frame_index,
182                Entry::Vacant(e) => {
183                    e.insert(self.frame_index);
184                    events.push(BroadPhasePairEvent::AddPair(ColliderPair::new(
185                        handle1, handle2,
186                    )));
187                }
188            }
189        };
190
191        // let t0 = std::time::Instant::now();
192        self.tree
193            .traverse_bvtt_single_tree::<CHANGE_DETECTION_ENABLED>(
194                &mut self.workspace,
195                &mut pairs_collector,
196            );
197        // println!("Detection: {}", t0.elapsed().as_secs_f32() * 1000.0);
198        // println!(">>>>>> Num events: {}", events.iter().len());
199
200        // Find outdated entries.
201        // TODO PERF:
202        // Currently, the narrow-phase isn’t capable of removing its own outdated
203        // collision pairs. So we need to run a pass here to find aabbs that are
204        // no longer overlapping. This, and the pair deduplication happening in
205        // the `pairs_collector` is expensive and should be done more efficiently
206        // by the narrow-phase itself (or islands) once we rework it.
207        //
208        // let t0 = std::time::Instant::now();
209        self.pairs.retain(|(h0, h1), timestamp| {
210            if *timestamp != self.frame_index {
211                if !colliders.contains(*h0) || !colliders.contains(*h1) {
212                    // At least one of the colliders no longer exist, don’t retain the pair.
213                    return false;
214                }
215
216                let Some(node0) = self.tree.leaf_node(h0.into_raw_parts().0) else {
217                    return false;
218                };
219                let Some(node1) = self.tree.leaf_node(h1.into_raw_parts().0) else {
220                    return false;
221                };
222
223                if (!CHANGE_DETECTION_ENABLED || node0.is_changed() || node1.is_changed())
224                    && !node0.intersects(node1)
225                {
226                    events.push(BroadPhasePairEvent::DeletePair(ColliderPair::new(*h0, *h1)));
227                    false
228                } else {
229                    true
230                }
231            } else {
232                // If the timestamps match, we already saw this pair during traversal.
233                // There can be rare occurrences where the timestamp will be equal
234                // even though we didn’t see the pair during traversal. This happens
235                // if the frame index overflowed. But this is fine, we’ll catch it
236                // in another frame.
237                true
238            }
239        });
240
241        // println!(
242        //     "Post-filtering: {} (added pairs: {}, removed pairs: {})",
243        //     t0.elapsed().as_secs_f32() * 1000.0,
244        //     added_pairs,
245        //     removed_pairs
246        // );
247    }
248}
249
250impl BroadPhase for BroadPhaseBvh {
251    fn update(
252        &mut self,
253        params: &IntegrationParameters,
254        colliders: &ColliderSet,
255        bodies: &RigidBodySet,
256        modified_colliders: &[ColliderHandle],
257        removed_colliders: &[ColliderHandle],
258        events: &mut Vec<BroadPhasePairEvent>,
259    ) {
260        self.update_with_strategy(
261            params,
262            colliders,
263            bodies,
264            modified_colliders,
265            removed_colliders,
266            events,
267            self.optimization_strategy,
268        );
269    }
270}