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}