ifc_lite_geometry/kernel/
broadphase.rs

1// This Source Code Form is subject to the terms of the Mozilla Public
2// License, v. 2.0. If a copy of the MPL was not distributed with this
3// file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
5//! Broadphase — a hand-rolled f64 median-split AABB BVH over one operand's
6//! triangles, queried by the other operand's triangle AABBs to replace the
7//! O(|A|·|B|) all-pairs scan.
8//!
9//! The BVH is a conservative FILTER only: it returns the set of AABB-overlapping
10//! triangle indices (the exact pairs the old all-pairs+bbox loop produced). The
11//! float SAH/centroid sort never decides topology — callers canonicalise the
12//! candidate pairs by exact `(i,j)` keys before processing, so the arrangement
13//! topology (and the pinned determinism manifests) are byte-identical.
14//!
15//! # What the `tests` module's "brute-force" checks can and cannot catch (#2830)
16//!
17//! `bvh_candidate_pairs_match_brute_force` and
18//! `ray_and_point_candidates_are_conservative_supersets` read as independent
19//! oracles, but their reference computations call the SAME predicate functions
20//! the production paths call: `Bvh::query` and the tests' own `brute()` both
21//! call `overlap()` (query at line ~175, brute at line ~262); `Bvh::ray_candidates`
22//! / `Bvh::point_candidates` and the superset test both call `seg_hits_aabb()` /
23//! `aabb_contains()`. These tests therefore validate the TREE — median-split
24//! construction, node-bounds unions, pruning/traversal — never dropping a
25//! candidate the shared predicate would accept; they cannot validate the
26//! predicates (`overlap`, `seg_hits_aabb`, `aabb_contains`) themselves. A sign
27//! or inequality-strictness bug in `overlap()` shifts both the BVH's own
28//! candidate set and the "brute-force" reference identically and both tests
29//! stay green (mutation-confirmed: changing `overlap()`'s `<=` to `<` leaves
30//! both tests passing). Predicate correctness needs its own fixture, derived
31//! independently of these functions.
32
33mod bounded;
34
35type Tri = [[f64; 3]; 3];
36pub(crate) type Aabb = ([f64; 3], [f64; 3]);
37
38pub fn tri_aabb(t: &Tri) -> Aabb {
39    let mut lo = t[0];
40    let mut hi = t[0];
41    for p in t.iter().skip(1) {
42        for k in 0..3 {
43            lo[k] = lo[k].min(p[k]);
44            hi[k] = hi[k].max(p[k]);
45        }
46    }
47    (lo, hi)
48}
49
50fn overlap(a: &Aabb, b: &Aabb) -> bool {
51    (0..3).all(|k| a.0[k] <= b.1[k] && b.0[k] <= a.1[k])
52}
53
54/// Whether point `p` is inside `bb` grown by `pad` on every side.
55fn aabb_contains(p: [f64; 3], bb: &Aabb, pad: f64) -> bool {
56    (0..3).all(|k| p[k] >= bb.0[k] - pad && p[k] <= bb.1[k] + pad)
57}
58
59/// Slab test: whether the segment `p`→`far` intersects `bb` grown by `pad`.
60/// Conservative — returns true on any rounding-ambiguous near-miss so the BVH
61/// never drops a triangle the exact ray-cast would hit.
62fn seg_hits_aabb(p: [f64; 3], far: [f64; 3], bb: &Aabb, pad: f64) -> bool {
63    let (mut tmin, mut tmax) = (0.0f64, 1.0f64);
64    for k in 0..3 {
65        let (lo, hi) = (bb.0[k] - pad, bb.1[k] + pad);
66        let d = far[k] - p[k];
67        if d.abs() <= f64::MIN_POSITIVE {
68            // Segment parallel to this slab — admit unless clearly outside it.
69            if p[k] < lo || p[k] > hi {
70                return false;
71            }
72        } else {
73            let inv = 1.0 / d;
74            let (mut t0, mut t1) = ((lo - p[k]) * inv, (hi - p[k]) * inv);
75            if t0 > t1 {
76                std::mem::swap(&mut t0, &mut t1);
77            }
78            tmin = tmin.max(t0);
79            tmax = tmax.min(t1);
80            if tmin > tmax {
81                return false;
82            }
83        }
84    }
85    true
86}
87
88struct Node {
89    aabb: Aabb,
90    tri: u32, // u32::MAX ⇒ inner node
91    left: u32,
92    right: u32,
93}
94
95pub struct Bvh {
96    nodes: Vec<Node>,
97    root: u32,
98    /// Conservative padding (a small fraction of the scene diagonal) added to
99    /// every AABB test so f64 rounding in the slab / containment math can never
100    /// prune a node a triangle the EXACT predicate would hit lives under. The
101    /// exact test on the returned candidates is what decides — the pad only ever
102    /// admits a few extra candidates, never drops a real one, so ray/point
103    /// queries are a conservative SUPERSET of the brute-force scan and the
104    /// downstream exact parity/containment result is byte-identical.
105    pad: f64,
106}
107
108/// The "no box" value: inverted bounds no point can satisfy, so nothing can be
109/// reported inside it. Only [`tris_aabb`] returns it; `Bvh::root_aabb` says the
110/// same thing with `None`, and the two are deliberately separate because their
111/// callers want different shapes.
112pub(crate) const EMPTY_AABB: Aabb = ([f64::MAX; 3], [f64::MIN; 3]);
113
114/// Per-axis AABB over a triangle slice, or [`EMPTY_AABB`] when there are none.
115/// The slice case of [`tri_aabb`]; the union-of-boxes case is `bounds`.
116pub(crate) fn tris_aabb(tris: &[Tri]) -> Aabb {
117    let (mut lo, mut hi) = EMPTY_AABB;
118    for t in tris {
119        for v in t {
120            for k in 0..3 {
121                lo[k] = lo[k].min(v[k]);
122                hi[k] = hi[k].max(v[k]);
123            }
124        }
125    }
126    (lo, hi)
127}
128
129impl Bvh {
130    pub fn build(tris: &[Tri]) -> Bvh {
131        let mut items: Vec<(u32, Aabb, [f64; 3])> = tris
132            .iter()
133            .enumerate()
134            .map(|(i, t)| {
135                let bb = tri_aabb(t);
136                let c = [
137                    0.5 * (bb.0[0] + bb.1[0]),
138                    0.5 * (bb.0[1] + bb.1[1]),
139                    0.5 * (bb.0[2] + bb.1[2]),
140                ];
141                (i as u32, bb, c)
142            })
143            .collect();
144        let mut nodes = Vec::new();
145        let root = if items.is_empty() {
146            u32::MAX
147        } else {
148            build_node(&mut nodes, &mut items)
149        };
150        let pad = if root == u32::MAX {
151            0.0
152        } else {
153            let (lo, hi) = nodes[root as usize].aabb;
154            let diag = ((hi[0] - lo[0]).powi(2) + (hi[1] - lo[1]).powi(2) + (hi[2] - lo[2]).powi(2))
155                .sqrt();
156            (diag * 1.0e-9).max(1.0e-12)
157        };
158        Bvh { nodes, root, pad }
159    }
160
161    /// The UNPADDED root AABB over every triangle (`None` for an empty tree).
162    /// Lets `point_inside_bvh` bound its parity segment's far endpoint outside
163    /// the mesh without an O(N) rescan.
164    pub(crate) fn root_aabb(&self) -> Option<Aabb> {
165        (self.root != u32::MAX).then(|| self.nodes[self.root as usize].aabb)
166    }
167
168    /// Append the index of every triangle whose (padded) AABB the segment
169    /// `p`→`far` may pass through — a conservative superset of the triangles the
170    /// exact ray could hit. Used to turn the O(N) `point_inside` ray-cast into an
171    /// O(log N + hits) one without changing its parity result.
172    pub fn ray_candidates(&self, p: [f64; 3], far: [f64; 3], out: &mut Vec<u32>) {
173        if self.root != u32::MAX {
174            self.descend_ray(self.root, p, far, out);
175        }
176    }
177
178    fn descend_ray(&self, idx: u32, p: [f64; 3], far: [f64; 3], out: &mut Vec<u32>) {
179        let n = &self.nodes[idx as usize];
180        if !seg_hits_aabb(p, far, &n.aabb, self.pad) {
181            return;
182        }
183        if n.tri != u32::MAX {
184            out.push(n.tri);
185        } else {
186            self.descend_ray(n.left, p, far, out);
187            self.descend_ray(n.right, p, far, out);
188        }
189    }
190
191    /// Append the index of every triangle whose AABB, grown by `radius` (plus the
192    /// conservative pad), contains point `p`. `radius = 0` ⇒ exact-on-surface
193    /// candidates; `radius = band` ⇒ near-coplanar-flush candidates. A conservative
194    /// superset, so the exact per-triangle test that follows decides the verdict.
195    pub fn point_candidates(&self, p: [f64; 3], radius: f64, out: &mut Vec<u32>) {
196        if self.root != u32::MAX {
197            self.descend_point(self.root, p, radius, out);
198        }
199    }
200
201    fn descend_point(&self, idx: u32, p: [f64; 3], radius: f64, out: &mut Vec<u32>) {
202        let n = &self.nodes[idx as usize];
203        if !aabb_contains(p, &n.aabb, self.pad + radius) {
204            return;
205        }
206        if n.tri != u32::MAX {
207            out.push(n.tri);
208        } else {
209            self.descend_point(n.left, p, radius, out);
210            self.descend_point(n.right, p, radius, out);
211        }
212    }
213
214    /// Append the indices of every triangle whose AABB overlaps `q`.
215    pub fn query(&self, q: &Aabb, out: &mut Vec<u32>) {
216        if self.root != u32::MAX {
217            self.descend(self.root, q, out);
218        }
219    }
220
221    fn descend(&self, idx: u32, q: &Aabb, out: &mut Vec<u32>) {
222        let n = &self.nodes[idx as usize];
223        if !overlap(&n.aabb, q) {
224            return;
225        }
226        if n.tri != u32::MAX {
227            out.push(n.tri);
228        } else {
229            self.descend(n.left, q, out);
230            self.descend(n.right, q, out);
231        }
232    }
233}
234
235fn bounds(items: &[(u32, Aabb, [f64; 3])]) -> Aabb {
236    let mut lo = items[0].1 .0;
237    let mut hi = items[0].1 .1;
238    for it in &items[1..] {
239        for k in 0..3 {
240            lo[k] = lo[k].min(it.1 .0[k]);
241            hi[k] = hi[k].max(it.1 .1[k]);
242        }
243    }
244    (lo, hi)
245}
246
247fn build_node(nodes: &mut Vec<Node>, items: &mut [(u32, Aabb, [f64; 3])]) -> u32 {
248    let bb = bounds(items);
249    if items.len() == 1 {
250        let idx = nodes.len() as u32;
251        nodes.push(Node { aabb: bb, tri: items[0].0, left: 0, right: 0 });
252        return idx;
253    }
254    // split on the longest centroid axis at the median
255    let mut span = [0.0f64; 3];
256    let (mut clo, mut chi) = (items[0].2, items[0].2);
257    for it in items.iter() {
258        for k in 0..3 {
259            clo[k] = clo[k].min(it.2[k]);
260            chi[k] = chi[k].max(it.2[k]);
261        }
262    }
263    for k in 0..3 {
264        span[k] = chi[k] - clo[k];
265    }
266    let axis = if span[0] >= span[1] && span[0] >= span[2] {
267        0
268    } else if span[1] >= span[2] {
269        1
270    } else {
271        2
272    };
273    // total_cmp: identical to partial_cmp for the finite centroids built here,
274    // but a total order by construction (no Equal-on-NaN escape hatch).
275    items.sort_by(|a, b| a.2[axis].total_cmp(&b.2[axis]));
276    let mid = items.len() / 2;
277    let (l, r) = items.split_at_mut(mid);
278    let left = build_node(nodes, l);
279    let right = build_node(nodes, r);
280    let idx = nodes.len() as u32;
281    nodes.push(Node { aabb: bb, tri: u32::MAX, left, right });
282    idx
283}
284
285/// All AABB-overlapping `(i, j)` pairs between `a` and `b`, sorted by `(i, j)` —
286/// a drop-in for the all-pairs+bbox loop with identical output order.
287pub fn candidate_pairs(a: &[Tri], b: &[Tri]) -> Vec<(usize, usize)> {
288    let bvh = Bvh::build(b);
289    let mut pairs = Vec::new();
290    let mut cand = Vec::new();
291    for (i, ta) in a.iter().enumerate() {
292        cand.clear();
293        bvh.query(&tri_aabb(ta), &mut cand);
294        for &j in &cand {
295            pairs.push((i, j as usize));
296        }
297    }
298    pairs.sort_unstable();
299    pairs
300}
301
302#[cfg(test)]
303mod tests {
304    use super::*;
305
306    fn brute(a: &[Tri], b: &[Tri]) -> Vec<(usize, usize)> {
307        let mut p = Vec::new();
308        for (i, ta) in a.iter().enumerate() {
309            for (j, tb) in b.iter().enumerate() {
310                if overlap(&tri_aabb(ta), &tri_aabb(tb)) {
311                    p.push((i, j));
312                }
313            }
314        }
315        p.sort_unstable();
316        p
317    }
318
319    #[test]
320    fn ray_and_point_candidates_are_conservative_supersets() {
321        // A scattered cloud of triangles; every ray/point query's candidate set
322        // must CONTAIN every triangle the brute-force AABB test admits (a missed
323        // candidate would change the exact parity/containment downstream).
324        let tris: Vec<Tri> = (0..60)
325            .map(|i| {
326                let (x, y, z) = (i as f64 * 0.7, (i % 7) as f64 * 1.3, (i % 5) as f64 * 0.9);
327                [[x, y, z], [x + 0.4, y, z], [x, y + 0.5, z + 0.3]]
328            })
329            .collect();
330        let bvh = Bvh::build(&tris);
331        let rays = [
332            ([0.0, 0.0, 0.0], [40.0, 9.0, 4.0]),
333            ([5.0, 3.0, 1.0], [5.0001, 3.0, 100.0]),
334            ([-2.0, -2.0, -2.0], [42.0, 12.0, 6.0]),
335        ];
336        for (p, far) in rays {
337            let mut cand = Vec::new();
338            bvh.ray_candidates(p, far, &mut cand);
339            let cset: std::collections::HashSet<u32> = cand.into_iter().collect();
340            for (i, t) in tris.iter().enumerate() {
341                if seg_hits_aabb(p, far, &tri_aabb(t), 0.0) {
342                    assert!(cset.contains(&(i as u32)), "ray missed candidate {i}");
343                }
344            }
345        }
346        for p in [[5.2, 3.0, 0.1], [0.1, 0.0, 0.0], [41.0, 7.0, 3.6]] {
347            let mut cand = Vec::new();
348            bvh.point_candidates(p, 0.0, &mut cand);
349            let cset: std::collections::HashSet<u32> = cand.into_iter().collect();
350            for (i, t) in tris.iter().enumerate() {
351                if aabb_contains(p, &tri_aabb(t), 0.0) {
352                    assert!(cset.contains(&(i as u32)), "point missed candidate {i}");
353                }
354            }
355        }
356    }
357
358    #[test]
359    fn bvh_candidate_pairs_match_brute_force() {
360        // a fan of triangles overlapping a moved fan — the BVH must return EXACTLY
361        // the all-pairs+bbox result (in identical (i,j) order).
362        let mk = |dx: f64| -> Vec<Tri> {
363            (0..20)
364                .map(|i| {
365                    let x = dx + i as f64 * 0.3;
366                    [[x, 0., 0.], [x + 0.5, 0., 0.], [x, 0.5, 0.4]]
367                })
368                .collect()
369        };
370        let a = mk(0.0);
371        let b = mk(1.7);
372        assert_eq!(candidate_pairs(&a, &b), brute(&a, &b));
373        // disjoint sets → no pairs
374        let far = mk(1000.0);
375        assert!(candidate_pairs(&a, &far).is_empty());
376    }
377}
378
379mod nearest;