rapier2d/geometry/broad_phase_bvh/mod.rs
1use crate::alloc_prelude::*;
2use crate::data::Coarena;
3use crate::dynamics::IntegrationParameters;
4use crate::geometry::{Aabb, ColliderHandle};
5use crate::math::Real;
6use parry::partitioning::{Bvh, BvhLeafUpdateStatus, BvhWorkspace};
7use parry::utils::hashmap::HashMap;
8
9mod update;
10
11/// The broad-phase collision detector that quickly filters out distant object pairs.
12///
13/// The broad-phase is the "first pass" of collision detection. It uses a hierarchical
14/// bounding volume tree (BVH) to quickly identify which collider pairs are close enough
15/// to potentially collide, avoiding expensive narrow-phase checks for distant objects.
16///
17/// Think of it as a "spatial index" that answers: "Which objects are near each other?"
18///
19/// You typically don't interact with this directly - it's managed by [`PhysicsPipeline`](crate::pipeline::PhysicsPipeline).
20/// However, you can use it to create a [`QueryPipeline`](crate::pipeline::QueryPipeline) for spatial queries.
21#[derive(Default, Clone)]
22#[cfg_attr(feature = "serde-serialize", derive(Serialize, Deserialize))]
23pub struct BroadPhaseBvh {
24 pub(crate) tree: Bvh,
25 #[cfg_attr(feature = "serde-serialize", serde(skip))]
26 workspace: BvhWorkspace,
27 #[cfg_attr(
28 feature = "serde-serialize",
29 serde(
30 serialize_with = "serialize_pairs",
31 deserialize_with = "crate::utils::serde::deserialize_from_vec_tuple"
32 )
33 )]
34 pairs: HashMap<(ColliderHandle, ColliderHandle), u32>,
35 /// For each collider, the other colliders it currently forms a pair with. Lets
36 /// stale-pair detection examine only pairs adjacent to changed colliders instead of
37 /// re-scanning the whole `pairs` map (a pair can only stop overlapping if one side changed).
38 pair_adjacency: Coarena<Vec<ColliderHandle>>,
39 /// Scratch buffer holding the colliders whose AABB was updated in the tree
40 /// during the last `update` call.
41 #[cfg_attr(feature = "serde-serialize", serde(skip))]
42 updated_colliders: Vec<ColliderHandle>,
43 /// Scratch buffer holding the leaf pairs reported by the tree traversal. Only the
44 /// sequential traversal needs it (it reports through a closure); the parallel one
45 /// returns its own vector.
46 #[cfg(not(feature = "parallel"))]
47 #[cfg_attr(feature = "serde-serialize", serde(skip))]
48 candidates_scratch: Vec<(u32, u32)>,
49 /// Scratch: per-collider "was updated this step" bit (collider arena index), so the
50 /// stale-pair scan can visit a pair from one side only when both sides moved.
51 #[cfg_attr(feature = "serde-serialize", serde(skip))]
52 updated_mask: Vec<bool>,
53 /// Scratch buffer holding the stale pairs detected during `update`.
54 ///
55 /// The boolean indicates if a `DeletePair` event must be emitted for the pair.
56 #[cfg_attr(feature = "serde-serialize", serde(skip))]
57 stale_pairs: Vec<(ColliderHandle, ColliderHandle, bool)>,
58 /// Leaves updated at the previous `update` call — (a superset of) the leaves whose
59 /// change flag the previous refit set; partial refitting needs it to clear those flags.
60 ///
61 /// Note that this needs to be serialized for determinism after snapshot restore.
62 prev_updated_leaves: Vec<u32>,
63 /// Leaves updated in the tree during the current `update` call.
64 #[cfg_attr(feature = "serde-serialize", serde(skip))]
65 curr_updated_leaves: Vec<u32>,
66 /// Colliders whose tree leaf was updated through [`Self::set_aabb`] since the last
67 /// `update` call (e.g. by the physics pipeline at the end of the previous step).
68 /// They count as changed colliders for the next `update` call.
69 pending_set_aabb: Vec<ColliderHandle>,
70 /// Quality-degrading tree changes (in-place leaf updates, removals) since the last
71 /// incremental optimization; re-inserted leaves don't count (SAH re-insertion is
72 /// self-optimizing). Periodic optimization is skipped while small relative to tree size.
73 changes_since_optimize: u32,
74 /// True when the previous `update` saw few leaves change: that regime relocates moved leaves via
75 /// SAH re-insertion (tree quality without an O(tree) optimizer/refit pass); bulk regimes keep cheaper
76 /// in-place updates + the periodic optimizer. One step of hysteresis: `set_aabb` runs between updates.
77 reinsert_leaf_updates: bool,
78 /// Scratch buffer for the precomputed leaf updates of [`Self::update`].
79 #[cfg_attr(feature = "serde-serialize", serde(skip))]
80 update_scratch: Vec<(ColliderHandle, Aabb, Real)>,
81 /// Workspace of the parallel leaf-update batches (the tree API takes raw
82 /// leaf indices).
83 #[cfg(feature = "parallel")]
84 #[cfg_attr(feature = "serde-serialize", serde(skip))]
85 update_batch_scratch: Vec<(Aabb, u32, Real)>,
86 #[cfg(feature = "parallel")]
87 #[cfg_attr(feature = "serde-serialize", serde(skip))]
88 update_batch_statuses: Vec<BvhLeafUpdateStatus>,
89 frame_index: u32,
90 optimization_strategy: BvhOptimizationStrategy,
91 /// If enabled, each tree leaf's change-detection margin adapts to the collider's size
92 /// (12.5% of its smallest AABB extent, capped) instead of a fixed fraction of the
93 /// length unit (default: `false`). Large shapes then keep their leaf and candidate
94 /// pairs valid across bigger displacements — fewer tree updates and pair re-checks,
95 /// at the cost of slightly fatter AABBs (more candidate pairs for the narrow-phase).
96 #[cfg_attr(feature = "serde-serialize", serde(default))]
97 pub adaptive_change_detection_margin: bool,
98 /// True when the last `update` deferred its (quality-only) BVH optimization pass
99 /// so the physics pipeline can run it concurrently with the narrow phase and
100 /// solver; consumed by [`Self::take_deferred_optimize`].
101 #[cfg_attr(feature = "serde-serialize", serde(skip))]
102 deferred_optimize_pending: bool,
103}
104
105// TODO: would be interesting to try out:
106// "Fast Insertion-Based Optimization of Bounding Volume Hierarchies"
107// by Bittner et al.
108/// Selection of strategies to maintain through time the broad-phase BVH in shape that remains
109/// efficient for collision-detection and scene queries.
110#[cfg_attr(feature = "serde-serialize", derive(Serialize, Deserialize))]
111#[derive(Default, PartialEq, Eq, Copy, Clone)]
112pub enum BvhOptimizationStrategy {
113 /// Different sub-trees of the BVH will be optimized at each frame.
114 #[default]
115 SubtreeOptimizer,
116 /// Disables incremental BVH optimization (discouraged).
117 ///
118 /// This should not be used except for debugging purpose.
119 None,
120}
121
122/// Runs the deferred (quality-only) optimization pass on `tree`.
123///
124/// The parallel and sequential refits produce the same nodes (parry pins that in
125/// `refit_parallel_matches_sequential`), so which one runs is a pure execution choice.
126pub(crate) fn run_bvh_optimize(tree: &mut Bvh, workspace: &mut BvhWorkspace) {
127 tree.optimize_incremental(workspace);
128 // Flag-preserving refit: the change-detection flags were already resolved by
129 // the partial refit that ran before this step's pair traversal, and the next
130 // step's traversal must see them untouched.
131 #[cfg(feature = "parallel")]
132 tree.refit_without_resolve_parallel(workspace);
133 #[cfg(not(feature = "parallel"))]
134 tree.refit_without_resolve(workspace);
135}
136
137/// A pending (quality-only) BVH optimization pass, extracted from the broad-phase so
138/// it can run on another thread while the rest of the step doesn't touch the tree.
139///
140/// Deferred in every build, so every consumer sees the same tree at the same point of
141/// the step: with a spare worker the pass runs concurrently, otherwise it runs inline
142/// at the join point (see `PhysicsPipeline::join_deferred_bvh_optimize`). Running it
143/// eagerly instead would optimize the tree *before* this step's pair traversal rather
144/// than after — a different tree, hence different pairs.
145pub(crate) struct DeferredBvhOptimize {
146 tree: Bvh,
147 workspace: BvhWorkspace,
148}
149
150impl DeferredBvhOptimize {
151 pub(crate) fn run(&mut self) {
152 run_bvh_optimize(&mut self.tree, &mut self.workspace);
153 }
154}
155
156/// Serializes the pair map by collider index, so the bytes describe the pair *set* rather
157/// than the map's insertion history (see `serialize_sorted_to_vec_tuple`).
158#[cfg(feature = "serde-serialize")]
159fn serialize_pairs<S: serde::Serializer>(
160 pairs: &HashMap<(ColliderHandle, ColliderHandle), u32>,
161 s: S,
162) -> Result<S::Ok, S::Error> {
163 crate::utils::serde::serialize_sorted_to_vec_tuple(
164 pairs,
165 |(a, b)| (a.into_raw_parts(), b.into_raw_parts()),
166 s,
167 )
168}
169
170impl BroadPhaseBvh {
171 const CHANGE_DETECTION_ENABLED: bool = true;
172 // Fraction of the length unit each tree leaf is fattened by (movement within the skin
173 // leaves tree and pairs untouched; pairs appear up to `2 * factor` early). 0.04 keeps
174 // broad-phase cost low without a measurable narrow-phase hit.
175 const CHANGE_DETECTION_FACTOR: Real = 4.0e-2;
176 /// Upper bound of the adaptive change-detection margin, as a fraction of the
177 /// length unit (see [`Self::adaptive_change_detection_margin`]).
178 const ADAPTIVE_CHANGE_DETECTION_CAP: Real = 0.25;
179
180 /// Initializes a new empty broad-phase.
181 pub fn new() -> Self {
182 Self::default()
183 }
184
185 /// The change-detection margin (fat-AABB skin) for a leaf with the given AABB:
186 /// a fixed fraction of the length unit, or, with
187 /// [`Self::adaptive_change_detection_margin`], proportional to the shape's
188 /// smallest extent and kept within [fixed margin, cap].
189 fn change_detection_skin(&self, params: &IntegrationParameters, aabb: &Aabb) -> Real {
190 if !Self::CHANGE_DETECTION_ENABLED {
191 0.0
192 } else if self.adaptive_change_detection_margin {
193 let min_extent = aabb.extents().min_element();
194 (min_extent * 0.125).clamp(
195 Self::CHANGE_DETECTION_FACTOR * params.length_unit,
196 Self::ADAPTIVE_CHANGE_DETECTION_CAP * params.length_unit,
197 )
198 } else {
199 Self::CHANGE_DETECTION_FACTOR * params.length_unit
200 }
201 }
202
203 /// Initializes a new empty broad-phase with the specified strategy for incremental
204 /// BVH optimization.
205 pub fn with_optimization_strategy(optimization_strategy: BvhOptimizationStrategy) -> Self {
206 Self {
207 optimization_strategy,
208 ..Default::default()
209 }
210 }
211
212 /// Extracts the deferred BVH optimization pass requested by the last [`Self::update`],
213 /// if any, moving the tree out of the broad-phase. The tree MUST be handed back through
214 /// [`Self::finish_deferred_optimize`] before anything else uses this broad-phase.
215 pub(crate) fn take_deferred_optimize(&mut self) -> Option<DeferredBvhOptimize> {
216 self.deferred_optimize_pending.then(|| {
217 self.deferred_optimize_pending = false;
218 DeferredBvhOptimize {
219 tree: core::mem::replace(&mut self.tree, Bvh::new()),
220 workspace: core::mem::take(&mut self.workspace),
221 }
222 })
223 }
224
225 /// Puts back the tree extracted by [`Self::take_deferred_optimize`].
226 pub(crate) fn finish_deferred_optimize(&mut self, task: DeferredBvhOptimize) {
227 self.tree = task.tree;
228 self.workspace = task.workspace;
229 }
230
231 /// Sets the AABB associated to the given collider.
232 ///
233 /// The change is immediately applied and propagated through the underlying BVH;
234 /// change detection accounts for it during the next broad-phase update.
235 pub fn set_aabb(&mut self, params: &IntegrationParameters, handle: ColliderHandle, aabb: Aabb) {
236 let change_detection_skin = self.change_detection_skin(params, &aabb);
237 let leaf_index = handle.into_raw_parts().0;
238 // Same regime split as the `update` loop: small change volumes relocate
239 // moved leaves through self-optimizing SAH re-insertion, bulk volumes use
240 // in-place updates (and count toward the periodic optimizer).
241 let status = if self.reinsert_leaf_updates {
242 self.tree.reinsert_or_update_with_change_detection(
243 aabb,
244 leaf_index,
245 change_detection_skin,
246 )
247 } else {
248 self.tree
249 .insert_with_change_detection(aabb, leaf_index, change_detection_skin)
250 };
251 match status {
252 // The new AABB stayed within the leaf's fattened AABB: the tree was left
253 // untouched, so the next `update` has nothing to refit or re-check for
254 // this collider.
255 BvhLeafUpdateStatus::Unchanged => {}
256 BvhLeafUpdateStatus::UpdatedInPlace | BvhLeafUpdateStatus::Inserted => {
257 if !self.reinsert_leaf_updates && status == BvhLeafUpdateStatus::UpdatedInPlace {
258 self.changes_since_optimize = self.changes_since_optimize.saturating_add(1);
259 }
260 self.pending_set_aabb.push(handle);
261 }
262 }
263 }
264}
265
266#[cfg(test)]
267#[cfg(all(feature = "dim3", feature = "f32"))]
268mod test {
269 #[allow(unused_imports)]
270 use crate::alloc_prelude::*;
271 use crate::math::Vector;
272 use crate::prelude::{
273 CCDSolver, ColliderBuilder, ColliderSet, DefaultBroadPhase, ImpulseJointSet,
274 IntegrationParameters, IslandManager, MultibodyJointSet, NarrowPhase, PhysicsPipeline,
275 RigidBodyBuilder, RigidBodySet,
276 };
277
278 /// With the adaptive change-detection margin enabled, collisions must still be
279 /// detected and resolved like with the fixed margin (the margin only affects how
280 /// often tree leaves are refreshed, not which pairs eventually collide).
281 #[test]
282 fn adaptive_change_detection_margin_smoke() {
283 let mut final_ys = [0.0; 2];
284
285 for (i, adaptive) in [false, true].into_iter().enumerate() {
286 let mut bodies = RigidBodySet::new();
287 let mut colliders = ColliderSet::new();
288 let mut impulse_joints = ImpulseJointSet::new();
289 let mut multibody_joints = MultibodyJointSet::new();
290 let mut pipeline = PhysicsPipeline::new();
291 let mut islands = IslandManager::new();
292 let mut broad_phase = DefaultBroadPhase::new();
293 broad_phase.adaptive_change_detection_margin = adaptive;
294 let mut narrow_phase = NarrowPhase::new();
295 let mut ccd = CCDSolver::new();
296
297 colliders.insert(ColliderBuilder::cuboid(10.0, 0.5, 10.0));
298 let ball =
299 bodies.insert(RigidBodyBuilder::dynamic().translation(Vector::new(0.0, 4.0, 0.0)));
300 colliders.insert_with_parent(ColliderBuilder::ball(0.5), ball, &mut bodies);
301
302 let params = IntegrationParameters::default();
303 for _ in 0..200 {
304 pipeline.step(
305 Vector::new(0.0, -9.81, 0.0),
306 ¶ms,
307 &mut islands,
308 &mut broad_phase,
309 &mut narrow_phase,
310 &mut bodies,
311 &mut colliders,
312 &mut impulse_joints,
313 &mut multibody_joints,
314 &mut ccd,
315 &(),
316 &(),
317 );
318 }
319
320 final_ys[i] = bodies[ball].translation().y;
321 }
322
323 // Both must rest on the floor (0.5 half-thickness + 0.5 radius).
324 for y in final_ys {
325 assert!(
326 (y - 1.0).abs() < 0.02,
327 "ball did not rest on the floor: y = {y}"
328 );
329 }
330 }
331}