Skip to main content

obvhs/bvh2/
insertion_removal.rs

1use core::f32;
2
3use crate::{
4    Boundable, INVALID,
5    aabb::Aabb,
6    bvh2::{Bvh2, Bvh2Node, update_primitives_to_nodes_for_node},
7    faststack::{FastStack, HeapStack},
8};
9
10use super::DEFAULT_MAX_STACK_DEPTH;
11
12// The index and inherited_cost of a given candidate sibling used for insertion.
13#[doc(hidden)]
14#[derive(Debug, Default, Clone, Copy)]
15pub struct SiblingInsertionCandidate {
16    inherited_cost: f32,
17    index: u32,
18}
19
20impl Bvh2 {
21    /// Removes and returns the leaf specified by `node_id`.
22    /// Puts `node_id` sibling in its parents place then moves the last two nodes into the now empty slots at `node_id`
23    /// and its sibling.
24    ///
25    /// Doesn't update the primitive_indices mapping. If this node is just going to be re-inserted again, nothing needs
26    /// to be done with primitive_indices, the mapping will still be valid. If this primitive needs to be removed
27    /// permanently see Bvh2::remove_primitive().
28    ///
29    /// # Arguments
30    /// * `node_id` - The index into self.nodes of the node that is to be removed
31    pub fn remove_leaf(&mut self, node_id: usize) -> Bvh2Node {
32        assert!(
33            !self.uses_spatial_splits,
34            "Removing leaves while using spatial splits is currently unsupported as it would require a mapping \
35from one primitive to multiple nodes in `Bvh2::primitives_to_nodes`."
36        );
37
38        let node_to_remove = self.nodes[node_id];
39        assert!(node_to_remove.is_leaf());
40
41        if self.nodes.len() == 1 {
42            // Special case if the BVH is just a leaf
43            self.nodes.clear();
44            self.parents.clear();
45            self.primitives_to_nodes.clear();
46            return node_to_remove;
47        }
48
49        // if primitives_to_nodes has already been initialized
50        if !self.primitives_to_nodes.is_empty() {
51            // Invalidate primitives_to_nodes instances
52            for node_prim_id in
53                node_to_remove.first_index..node_to_remove.first_index + node_to_remove.prim_count
54            {
55                let direct_prim_id = self.primitive_indices[node_prim_id as usize];
56                self.primitives_to_nodes[direct_prim_id as usize] = INVALID;
57            }
58        }
59
60        let sibling_id = Bvh2Node::get_sibling_id(node_id);
61        debug_assert_eq!(self.parents[node_id], self.parents[sibling_id]); // Both children should already have the same parent.
62        let mut parent_id = self.parents[node_id] as usize;
63
64        // Put sibling in parent's place (parent doesn't exist anymore)
65        self.nodes[parent_id] = self.nodes[sibling_id];
66        let sibling = &mut self.nodes[parent_id];
67        if sibling.is_leaf() {
68            // Tell primitives where their node went.
69            update_primitives_to_nodes_for_node(
70                sibling,
71                parent_id,
72                &self.primitive_indices,
73                &mut self.primitives_to_nodes,
74            )
75        } else {
76            // Tell children of sibling where their parent went.
77            let left_sibling_child = sibling.first_index as usize;
78            self.parents[left_sibling_child] = parent_id as u32;
79            self.parents[left_sibling_child + 1] = parent_id as u32;
80        }
81        // Don't need to update other parents here since the parent that was for this `parent_id` slot is now the direct
82        // parent of the moved sibling, and the parents of `node_id` and `sibling_id` are updated below.
83
84        // Now slots at both node_id and sibling_id are empty.
85        // Take the last two nodes "src" and put them in those now empty "dst" slots.
86        let end_nodes = node_id >= self.nodes.len() - 2;
87        if end_nodes {
88            // If these were already the last 2 nodes in the list we can just discard both.
89            self.nodes.pop().unwrap();
90            self.nodes.pop().unwrap();
91            self.parents.pop().unwrap();
92            self.parents.pop().unwrap();
93        } else {
94            let dst_left_id = Bvh2Node::get_left_sibling_id(node_id);
95            let dst_right_id = Bvh2Node::get_right_sibling_id(node_id);
96
97            let src_left_id = self.nodes.len() as u32 - 2;
98            let src_right_id = Bvh2Node::get_sibling_id32(src_left_id);
99            let src_right_parent = self.parents.pop().unwrap();
100            let src_left_parent = self.parents.pop().unwrap();
101
102            self.parents[dst_left_id] = src_left_parent;
103            self.parents[dst_right_id] = src_right_parent;
104
105            debug_assert_eq!(src_left_parent, src_right_parent); // Both children should already have the same parent.
106            let parent_of_relocated = &mut self.nodes[src_left_parent as usize];
107            debug_assert!(!parent_of_relocated.is_leaf());
108            debug_assert_eq!(parent_of_relocated.first_index, src_left_id);
109            debug_assert_eq!(parent_of_relocated.first_index + 1, src_right_id);
110            // Tell the actual parent of the nodes that are moving where they're going to be now.
111            self.nodes[src_left_parent as usize].first_index = dst_left_id as u32;
112
113            let right_src_sibling = self.nodes.pop().unwrap(); // Last node is right src sibling
114            if right_src_sibling.is_leaf() {
115                // Tell primitives where their node went.
116                update_primitives_to_nodes_for_node(
117                    &right_src_sibling,
118                    dst_right_id,
119                    &self.primitive_indices,
120                    &mut self.primitives_to_nodes,
121                );
122            } else {
123                // Go to children of right_src_sibling and tell them where their parent went
124                self.parents[right_src_sibling.first_index as usize] = dst_right_id as u32;
125                self.parents[right_src_sibling.first_index as usize + 1] = dst_right_id as u32;
126            }
127            self.nodes[dst_right_id] = right_src_sibling;
128
129            let left_src_sibling = self.nodes.pop().unwrap(); // Last node is left src sibling
130            if left_src_sibling.is_leaf() {
131                // Tell primitives where their node went.
132                update_primitives_to_nodes_for_node(
133                    &left_src_sibling,
134                    dst_left_id,
135                    &self.primitive_indices,
136                    &mut self.primitives_to_nodes,
137                );
138            } else {
139                // Go to children of left_src_sibling and tell them where their parent went
140                self.parents[left_src_sibling.first_index as usize] = dst_left_id as u32;
141                self.parents[left_src_sibling.first_index as usize + 1] = dst_left_id as u32;
142            }
143            self.nodes[dst_left_id] = left_src_sibling;
144
145            // If the to be removed node's parent was at the end of the array and has now moved update parent_id:
146            if parent_id as u32 == src_left_id {
147                parent_id = dst_left_id;
148            }
149            if parent_id as u32 == src_right_id {
150                parent_id = dst_right_id;
151            }
152        }
153
154        // Need to work up the tree updating the aabbs since we just removed a node.
155        self.refit_from_fast(parent_id);
156
157        self.children_are_ordered_after_parents = false;
158        // Return the removed node.
159        node_to_remove
160    }
161
162    /// Searches the tree recursively to find the best sibling for the node being inserted. The best sibling is
163    /// classified as the sibling that if chosen it would increase the surface area of the BVH the least.
164    /// When the best sibling is found, a parent of both the sibling and the new node is put in the location of
165    /// the sibling and both the sibling and new node are added to the end of the bvh.nodes.
166    /// See "Branch and Bound" <https://box2d.org/files/ErinCatto_DynamicBVH_Full.pdf>
167    /// Jiˇrí Bittner et al. 2012 Fast Insertion-Based Optimization of Bounding Volume Hierarchies
168    ///
169    /// # Returns
170    /// The index of the newly added node (always `bvh.nodes.len() - 1` since the node it put at the end).
171    ///
172    /// # Arguments
173    /// * `new_node` - This node must be a leaf and already have a valid first_index into primitive_indices
174    /// * `stack` - Used for the traversal stack. Needs to be large enough to initially accommodate traversal to the
175    ///   deepest leaf of the BVH. insert_leaf() will resize this stack after traversal to be at least 2x the
176    ///   required size. This ends up being quite a bit faster than using a Vec and works well when inserting multiple
177    ///   nodes. But does require the user to provide a good initial guess. SiblingInsertionCandidate is tiny so be
178    ///   generous. Something like: `stack.reserve(bvh.depth(0) * 2).max(1000);` If you are inserting a lot of leaves
179    ///   don't call bvh.depth(0) with each leaf just let insert_leaf() resize the stack as needed.
180    pub fn insert_leaf(
181        &mut self,
182        new_node: Bvh2Node,
183        stack: &mut HeapStack<SiblingInsertionCandidate>,
184    ) -> usize {
185        assert!(new_node.is_leaf());
186
187        if self.nodes.is_empty() {
188            self.nodes.push(new_node);
189            self.parents.clear();
190            self.parents.push(0);
191            return 0;
192        }
193
194        self.init_parents_if_uninit();
195
196        let mut min_cost = f32::MAX;
197        let mut best_sibling_candidate_id = 0;
198        let mut max_stack_len = 1;
199        let new_node_cost = new_node.aabb().half_area();
200
201        stack.clear();
202        let root_aabb = self.nodes[0].aabb();
203
204        // Traverse the BVH to find the best sibling
205        stack.push(SiblingInsertionCandidate {
206            inherited_cost: root_aabb.union(new_node.aabb()).half_area() - root_aabb.half_area(),
207            index: 0,
208        });
209        while let Some(sibling_candidate) = stack.pop() {
210            let current_node_index = sibling_candidate.index as usize;
211
212            let candidate = &self.nodes[current_node_index];
213
214            let direct_cost = candidate.aabb().union(new_node.aabb()).half_area();
215            let total_cost = direct_cost + sibling_candidate.inherited_cost;
216
217            if total_cost < min_cost {
218                min_cost = total_cost;
219                best_sibling_candidate_id = current_node_index;
220            }
221
222            // If this is not a leaf, it's possible a better cost could be found further down.
223            if !candidate.is_leaf() {
224                let inherited_cost = total_cost - candidate.aabb().half_area();
225                let min_subtree_cost = new_node_cost + inherited_cost;
226                if min_subtree_cost < min_cost {
227                    stack.push(SiblingInsertionCandidate {
228                        inherited_cost,
229                        index: candidate.first_index,
230                    });
231                    stack.push(SiblingInsertionCandidate {
232                        inherited_cost,
233                        index: candidate.first_index + 1,
234                    });
235                    max_stack_len = stack.len().max(max_stack_len);
236                }
237            }
238        }
239
240        if max_stack_len * 2 > stack.cap() {
241            stack.reserve(max_stack_len * 2);
242        }
243
244        let best_sibling_candidate = self.nodes[best_sibling_candidate_id];
245
246        // To avoid having gaps or re-arranging the BVH:
247        // The new parent goes in the sibling's position.
248        // The sibling and new node go on the end.
249        let new_sibling_id = self.nodes.len() as u32;
250        let new_parent = Bvh2Node::new(
251            new_node.aabb().union(best_sibling_candidate.aabb()),
252            0,
253            new_sibling_id,
254        );
255
256        // New parent goes in the sibling's position.
257        let new_parent_id = best_sibling_candidate_id;
258        self.nodes[new_parent_id] = new_parent;
259
260        if best_sibling_candidate.is_leaf() {
261            // Tell primitives where their node went.
262            update_primitives_to_nodes_for_node(
263                &best_sibling_candidate,
264                new_sibling_id as usize,
265                &self.primitive_indices,
266                &mut self.primitives_to_nodes,
267            )
268        } else {
269            // If the best selected sibling was an inner node, we need to update the parents mapping so that the children of
270            // that node point to the new location that it's being moved to.
271            self.parents[best_sibling_candidate.first_index as usize] = new_sibling_id;
272            self.parents[best_sibling_candidate.first_index as usize + 1] = new_sibling_id;
273
274            // The sibling was moved to the end of the node list, but its children stayed where they were,
275            // so they now come before their parent.
276            self.children_are_ordered_after_parents = false;
277        }
278        self.nodes.push(best_sibling_candidate);
279        let new_node_id = self.nodes.len();
280        self.nodes.push(new_node); // Put the new node at the very end.
281        self.parents.push(new_parent_id as u32);
282        self.parents.push(new_parent_id as u32);
283
284        // if primitives_to_nodes has already been initialized
285        if !self.primitives_to_nodes.is_empty() {
286            // Tell primitives where their node went.
287            let end = new_node.first_index + new_node.prim_count;
288            if self.primitives_to_nodes.len() < end as usize {
289                // Since we are adding a primitive it's possible that primitives_to_nodes is not large enough yet.
290                self.primitives_to_nodes.resize(end as usize, INVALID);
291            }
292            update_primitives_to_nodes_for_node(
293                &new_node,
294                new_node_id,
295                &self.primitive_indices,
296                &mut self.primitives_to_nodes,
297            )
298        }
299
300        // Need to work up the tree updating the aabbs since we just added a node.
301        self.refit_from_fast(new_parent_id);
302
303        new_node_id
304    }
305
306    /// Removes the leaf that contains the given primitive. Should be correct for nodes with multiple primitives per
307    /// leaf but faster for nodes with only one primitive per leaf, and will leave node aabb oversized.
308    /// Updates Bvh2::primitive_indices and Bvh2::primitive_indices_freelist.
309    ///
310    /// # Arguments
311    /// * `primitive_id` - The index of the primitive being removed.
312    pub fn remove_primitive(&mut self, primitive_id: u32) {
313        assert!(
314            !self.uses_spatial_splits,
315            "Removing primitives while using spatial splits is currently unsupported as it would require a mapping \
316from one primitive to multiple nodes in `Bvh2::primitives_to_nodes`."
317        );
318        let remove_primitive_id = primitive_id;
319        self.init_parents_if_uninit();
320        self.init_primitives_to_nodes_if_uninit();
321
322        let node_id = self.primitives_to_nodes[remove_primitive_id as usize];
323
324        let node = &self.nodes[node_id as usize];
325        assert!(node.is_leaf());
326        if node.prim_count == 1 {
327            let removed_leaf = self.remove_leaf(node_id as usize);
328            self.primitive_indices_freelist
329                .push(removed_leaf.first_index);
330            self.primitive_indices[removed_leaf.first_index as usize] = INVALID;
331        } else {
332            // Update leaf with the remaining primitives, use the existing leftover space in primitive_indices and
333            // only add the removed primitive to the freelist
334
335            let node = &mut self.nodes[node_id as usize];
336
337            let start = node.first_index as usize;
338            let end = (node.first_index + node.prim_count) as usize;
339            let last = end - 1;
340            let mut spare_spot_id = start;
341            // Condense primitive_indices for this node.
342            for node_prim_id in start..end {
343                let direct_prim_id = self.primitive_indices[node_prim_id];
344                if direct_prim_id == remove_primitive_id {
345                    break;
346                }
347                spare_spot_id += 1;
348            }
349            if spare_spot_id < last {
350                self.primitive_indices[spare_spot_id] = self.primitive_indices[last];
351            }
352            // Free now open last position.
353            self.primitive_indices_freelist.push(last as u32);
354            self.primitive_indices[last] = INVALID;
355
356            assert!(node.prim_count > 1);
357            node.prim_count -= 1;
358        }
359
360        if self.primitives_to_nodes.len() > remove_primitive_id as usize {
361            self.primitives_to_nodes[remove_primitive_id as usize] = INVALID;
362        }
363    }
364
365    /// Searches the tree recursively to find the best sibling for the primitive being inserted
366    /// (see Bvh2::insert_leaf()). Updates Bvh2::primitive_indices and Bvh2::primitive_indices_freelist.
367    ///
368    /// # Returns
369    /// The index of the newly added node.
370    ///
371    /// # Arguments
372    /// * `bvh` - The Bvh2 the new node is being added to
373    /// * `primitive_id` - The index of the primitive being inserted.
374    /// * `stack` - Used for the traversal stack. Needs to be large enough to initially accommodate traversal to the
375    ///   deepest leaf of the BVH. insert_leaf() will resize this stack after traversal to be at least 2x the
376    ///   required size. This ends up being quite a bit faster than using a Vec and works well when inserting multiple
377    ///   nodes. But does require the user to provide a good initial guess. SiblingInsertionCandidate is tiny so be
378    ///   generous. Something like: `stack.reserve(bvh.depth(0) * 2).max(1000);` If you are inserting a lot of leaves
379    ///   don't call bvh.depth(0) with each leaf just let insert_leaf() resize the stack as needed.
380    pub fn insert_primitive(
381        &mut self,
382        aabb: Aabb,
383        primitive_id: u32,
384        stack: &mut HeapStack<SiblingInsertionCandidate>,
385    ) -> usize {
386        self.init_primitives_to_nodes_if_uninit();
387        self.init_parents_if_uninit();
388        if self.primitives_to_nodes.len() <= primitive_id as usize {
389            self.primitives_to_nodes
390                .resize(primitive_id as usize + 1, INVALID);
391        }
392        let first_index = if let Some(free_slot) = self.primitive_indices_freelist.pop() {
393            self.primitive_indices[free_slot as usize] = primitive_id;
394            free_slot
395        } else {
396            self.primitive_indices.push(primitive_id);
397            self.primitive_indices.len() as u32 - 1
398        };
399        let new_node_id = self.insert_leaf(Bvh2Node::new(aabb, 1, first_index), stack);
400        self.primitives_to_nodes[primitive_id as usize] = new_node_id as u32;
401        new_node_id
402    }
403}
404
405/// Slow at building, makes a slow bvh, just for testing insertion.
406/// Can result in very deep BVHs in some cases.
407///
408/// Dramatically slower than ploc at both building and traversal. Easily 10x or 100x slower at building.
409/// (goes up by something like n^3 after a certain threshold).
410/// (BVH quality still improved afterward lot by reinsertion/collapse).
411#[doc(hidden)]
412pub fn build_bvh2_by_insertion<T: Boundable>(primitives: &[T]) -> Bvh2 {
413    let mut bvh = Bvh2::default();
414
415    let mut stack = HeapStack::new_with_capacity(1000);
416
417    for prim_id in 1..primitives.len() {
418        bvh.insert_primitive(primitives[prim_id].aabb(), prim_id as u32, &mut stack);
419    }
420
421    // Update max depth for validate
422    bvh.max_depth = (bvh.depth(0) + 1).max(DEFAULT_MAX_STACK_DEPTH);
423
424    #[cfg(debug_assertions)]
425    {
426        bvh.validate(primitives, false, true);
427    }
428
429    bvh
430}
431
432/// Just here to for testing/benchmarking/validating leaf removed and inserting. See reinsertion.rs if you want to
433/// optimize a BVH2. This currently actually tends to make a good bvh slower since doing a lot of insert_leaf_node tends
434/// to result in very deep BVHs.
435#[doc(hidden)]
436pub fn slow_leaf_reinsertion(bvh: &mut Bvh2) {
437    let mut stack = HeapStack::new_with_capacity(1000);
438    for node_id in 1..bvh.nodes.len() {
439        if bvh.nodes.len() <= node_id {
440            break;
441        }
442        if bvh.nodes[node_id].is_leaf() {
443            // Assert that the parent of this node is not a leaf (a parent could never be a leaf)
444            assert!(!bvh.nodes[bvh.parents[node_id] as usize].is_leaf());
445            // If the node is a leaf, remove it
446            let removed_leaf = bvh.remove_leaf(node_id);
447            // Insert it again, maybe it will find a better spot
448            bvh.insert_leaf(removed_leaf, &mut stack);
449        }
450    }
451    #[cfg(debug_assertions)]
452    {
453        bvh.validate_parents();
454    }
455}
456
457#[cfg(test)]
458mod tests {
459    use std::time::Duration;
460
461    use super::*;
462    use crate::{BvhBuildParams, bvh2::builder::build_bvh2, test_util::geometry::demoscene};
463
464    #[test]
465    fn build_by_insertion() {
466        for res in 30..=32 {
467            let tris = demoscene(res, 0);
468            let bvh = build_bvh2_by_insertion(&tris);
469            bvh.validate(&tris, false, false);
470        }
471    }
472
473    #[test]
474    fn test_slow_leaf_reinsertion() {
475        for res in 30..=32 {
476            let tris = demoscene(res, 0);
477
478            let mut bvh = build_bvh2(
479                &tris,
480                BvhBuildParams::medium_build(),
481                &mut Duration::default(),
482            );
483            bvh.init_primitives_to_nodes_if_uninit();
484            bvh.init_parents_if_uninit();
485            slow_leaf_reinsertion(&mut bvh);
486            bvh.validate(&tris, false, false);
487            bvh.reorder_in_stack_traversal_order();
488            bvh.validate(&tris, false, false);
489        }
490    }
491
492    #[test]
493    fn remove_all_primitives() {
494        let tris = demoscene(16, 0);
495
496        // Test with both a bvh that only has one primitive per leaf
497        // and also with one that has multiple primitives per leaf.
498        let bvh1 = build_bvh2(
499            &tris,
500            BvhBuildParams::fastest_build(),
501            &mut Duration::default(),
502        );
503        let bvh2 = build_bvh2(
504            &tris,
505            BvhBuildParams::medium_build(),
506            &mut Duration::default(),
507        );
508
509        let mut found_leaf_with_multiple_nodes = false;
510        for node in &bvh2.nodes {
511            if node.prim_count > 1 {
512                found_leaf_with_multiple_nodes = true;
513                break;
514            }
515        }
516        if !found_leaf_with_multiple_nodes {
517            panic!(
518                "Test remove_all_primitives bvh2 should have some nodes that contain multiple primitives"
519            );
520        }
521
522        for bvh in &mut [bvh1, bvh2] {
523            bvh.init_primitives_to_nodes_if_uninit();
524            bvh.init_parents_if_uninit();
525            bvh.validate(&tris, false, false);
526
527            for primitive_id in 0..tris.len() as u32 {
528                bvh.remove_primitive(primitive_id);
529                bvh.validate(&tris, false, false);
530            }
531
532            assert_eq!(bvh.nodes.len(), 0);
533            assert_eq!(bvh.parents.len(), 0);
534            assert_eq!(bvh.primitives_to_nodes.len(), 0);
535            bvh.validate(&tris, false, false);
536        }
537    }
538
539    #[test]
540    fn remove_and_insert_all_primitives() {
541        let tris = demoscene(16, 0);
542
543        let mut bvh = build_bvh2(
544            &tris,
545            BvhBuildParams::medium_build(),
546            &mut Duration::default(),
547        );
548        bvh.init_primitives_to_nodes_if_uninit();
549        bvh.init_parents_if_uninit();
550        bvh.validate(&tris, false, false);
551
552        let mut stack = HeapStack::new_with_capacity(1000);
553
554        for primitive_id in 0..tris.len() as u32 {
555            bvh.remove_primitive(primitive_id);
556            bvh.validate(&tris, false, false);
557        }
558
559        for primitive_id in 0..tris.len() as u32 {
560            bvh.insert_primitive(tris[primitive_id as usize].aabb(), primitive_id, &mut stack);
561            bvh.validate_primitives_to_nodes();
562        }
563
564        bvh.validate(&tris, false, false);
565    }
566
567    #[test]
568    fn insert_leaf_clears_children_are_ordered_after_parents() {
569        // Regression test
570        //
571        // If `Bvh2::children_are_ordered_after_parents` isn't cleared when inserting primitives,
572        // `Bvh2::refit_all()` takes the fast path, and computes some parents from children
573        // it hasn't updated yet, leaving nodes that don't fit their children.
574
575        let tris = demoscene(16, 0);
576
577        let mut bvh = build_bvh2(
578            &tris,
579            BvhBuildParams::medium_build(),
580            &mut Duration::default(),
581        );
582        bvh.init_primitives_to_nodes_if_uninit();
583        bvh.init_parents_if_uninit();
584
585        // Remove and re-insert some primitives.
586        let mut stack = HeapStack::new_with_capacity(1000);
587        let primitive_ids = (0..tris.len() as u32).step_by(32).collect::<Vec<_>>();
588
589        for &primitive_id in &primitive_ids {
590            bvh.remove_primitive(primitive_id);
591        }
592
593        // Removal clears `children_are_ordered_after_parents`, so we restore the ordering
594        // here so that we can test that insertion clears it again.
595        bvh.reorder_in_stack_traversal_order();
596        assert!(bvh.children_are_ordered_after_parents);
597
598        for &primitive_id in &primitive_ids {
599            let mut aabb = tris[primitive_id as usize].aabb();
600            aabb.min -= 0.05;
601            aabb.max += 0.05;
602            bvh.insert_primitive(aabb, primitive_id, &mut stack);
603        }
604
605        bvh.max_depth = (bvh.depth(0) + 1).max(DEFAULT_MAX_STACK_DEPTH);
606
607        bvh.validate(&tris, false, false);
608
609        // Grow every leaf, then refit the whole bvh.
610        for node in &mut bvh.nodes {
611            if node.is_leaf() {
612                let mut aabb = *node.aabb();
613                aabb.max += 0.1;
614                node.set_aabb(aabb);
615            }
616        }
617        bvh.refit_all();
618
619        // Every inner node should tightly fit its children.
620        for (node_id, node) in bvh.nodes.iter().enumerate() {
621            if !node.is_leaf() {
622                let children_aabb = bvh.nodes[node.first_index as usize]
623                    .aabb()
624                    .union(bvh.nodes[node.first_index as usize + 1].aabb());
625                assert_eq!(
626                    *node.aabb(),
627                    children_aabb,
628                    "node {node_id} does not fit its children after refit_all()"
629                );
630            }
631        }
632    }
633}