Skip to main content

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                    &params,
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}