Skip to main content

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
15type Tri = [[f64; 3]; 3];
16type Aabb = ([f64; 3], [f64; 3]);
17
18pub fn tri_aabb(t: &Tri) -> Aabb {
19    let mut lo = t[0];
20    let mut hi = t[0];
21    for p in t.iter().skip(1) {
22        for k in 0..3 {
23            lo[k] = lo[k].min(p[k]);
24            hi[k] = hi[k].max(p[k]);
25        }
26    }
27    (lo, hi)
28}
29
30fn overlap(a: &Aabb, b: &Aabb) -> bool {
31    (0..3).all(|k| a.0[k] <= b.1[k] && b.0[k] <= a.1[k])
32}
33
34/// Whether point `p` is inside `bb` grown by `pad` on every side.
35fn aabb_contains(p: [f64; 3], bb: &Aabb, pad: f64) -> bool {
36    (0..3).all(|k| p[k] >= bb.0[k] - pad && p[k] <= bb.1[k] + pad)
37}
38
39/// Slab test: whether the segment `p`→`far` intersects `bb` grown by `pad`.
40/// Conservative — returns true on any rounding-ambiguous near-miss so the BVH
41/// never drops a triangle the exact ray-cast would hit.
42fn seg_hits_aabb(p: [f64; 3], far: [f64; 3], bb: &Aabb, pad: f64) -> bool {
43    let (mut tmin, mut tmax) = (0.0f64, 1.0f64);
44    for k in 0..3 {
45        let (lo, hi) = (bb.0[k] - pad, bb.1[k] + pad);
46        let d = far[k] - p[k];
47        if d.abs() <= f64::MIN_POSITIVE {
48            // Segment parallel to this slab — admit unless clearly outside it.
49            if p[k] < lo || p[k] > hi {
50                return false;
51            }
52        } else {
53            let inv = 1.0 / d;
54            let (mut t0, mut t1) = ((lo - p[k]) * inv, (hi - p[k]) * inv);
55            if t0 > t1 {
56                std::mem::swap(&mut t0, &mut t1);
57            }
58            tmin = tmin.max(t0);
59            tmax = tmax.min(t1);
60            if tmin > tmax {
61                return false;
62            }
63        }
64    }
65    true
66}
67
68struct Node {
69    aabb: Aabb,
70    tri: u32, // u32::MAX ⇒ inner node
71    left: u32,
72    right: u32,
73}
74
75pub struct Bvh {
76    nodes: Vec<Node>,
77    root: u32,
78    /// Conservative padding (a small fraction of the scene diagonal) added to
79    /// every AABB test so f64 rounding in the slab / containment math can never
80    /// prune a node a triangle the EXACT predicate would hit lives under. The
81    /// exact test on the returned candidates is what decides — the pad only ever
82    /// admits a few extra candidates, never drops a real one, so ray/point
83    /// queries are a conservative SUPERSET of the brute-force scan and the
84    /// downstream exact parity/containment result is byte-identical.
85    pad: f64,
86}
87
88impl Bvh {
89    pub fn build(tris: &[Tri]) -> Bvh {
90        let mut items: Vec<(u32, Aabb, [f64; 3])> = tris
91            .iter()
92            .enumerate()
93            .map(|(i, t)| {
94                let bb = tri_aabb(t);
95                let c = [
96                    0.5 * (bb.0[0] + bb.1[0]),
97                    0.5 * (bb.0[1] + bb.1[1]),
98                    0.5 * (bb.0[2] + bb.1[2]),
99                ];
100                (i as u32, bb, c)
101            })
102            .collect();
103        let mut nodes = Vec::new();
104        let root = if items.is_empty() {
105            u32::MAX
106        } else {
107            build_node(&mut nodes, &mut items)
108        };
109        let pad = if root == u32::MAX {
110            0.0
111        } else {
112            let (lo, hi) = nodes[root as usize].aabb;
113            let diag = ((hi[0] - lo[0]).powi(2) + (hi[1] - lo[1]).powi(2) + (hi[2] - lo[2]).powi(2))
114                .sqrt();
115            (diag * 1.0e-9).max(1.0e-12)
116        };
117        Bvh { nodes, root, pad }
118    }
119
120    /// Append the index of every triangle whose (padded) AABB the segment
121    /// `p`→`far` may pass through — a conservative superset of the triangles the
122    /// exact ray could hit. Used to turn the O(N) `point_inside` ray-cast into an
123    /// O(log N + hits) one without changing its parity result.
124    pub fn ray_candidates(&self, p: [f64; 3], far: [f64; 3], out: &mut Vec<u32>) {
125        if self.root != u32::MAX {
126            self.descend_ray(self.root, p, far, out);
127        }
128    }
129
130    fn descend_ray(&self, idx: u32, p: [f64; 3], far: [f64; 3], out: &mut Vec<u32>) {
131        let n = &self.nodes[idx as usize];
132        if !seg_hits_aabb(p, far, &n.aabb, self.pad) {
133            return;
134        }
135        if n.tri != u32::MAX {
136            out.push(n.tri);
137        } else {
138            self.descend_ray(n.left, p, far, out);
139            self.descend_ray(n.right, p, far, out);
140        }
141    }
142
143    /// Append the index of every triangle whose AABB, grown by `radius` (plus the
144    /// conservative pad), contains point `p`. `radius = 0` ⇒ exact-on-surface
145    /// candidates; `radius = band` ⇒ near-coplanar-flush candidates. A conservative
146    /// superset, so the exact per-triangle test that follows decides the verdict.
147    pub fn point_candidates(&self, p: [f64; 3], radius: f64, out: &mut Vec<u32>) {
148        if self.root != u32::MAX {
149            self.descend_point(self.root, p, radius, out);
150        }
151    }
152
153    fn descend_point(&self, idx: u32, p: [f64; 3], radius: f64, out: &mut Vec<u32>) {
154        let n = &self.nodes[idx as usize];
155        if !aabb_contains(p, &n.aabb, self.pad + radius) {
156            return;
157        }
158        if n.tri != u32::MAX {
159            out.push(n.tri);
160        } else {
161            self.descend_point(n.left, p, radius, out);
162            self.descend_point(n.right, p, radius, out);
163        }
164    }
165
166    /// Append the indices of every triangle whose AABB overlaps `q`.
167    pub fn query(&self, q: &Aabb, out: &mut Vec<u32>) {
168        if self.root != u32::MAX {
169            self.descend(self.root, q, out);
170        }
171    }
172
173    fn descend(&self, idx: u32, q: &Aabb, out: &mut Vec<u32>) {
174        let n = &self.nodes[idx as usize];
175        if !overlap(&n.aabb, q) {
176            return;
177        }
178        if n.tri != u32::MAX {
179            out.push(n.tri);
180        } else {
181            self.descend(n.left, q, out);
182            self.descend(n.right, q, out);
183        }
184    }
185}
186
187fn bounds(items: &[(u32, Aabb, [f64; 3])]) -> Aabb {
188    let mut lo = items[0].1 .0;
189    let mut hi = items[0].1 .1;
190    for it in &items[1..] {
191        for k in 0..3 {
192            lo[k] = lo[k].min(it.1 .0[k]);
193            hi[k] = hi[k].max(it.1 .1[k]);
194        }
195    }
196    (lo, hi)
197}
198
199fn build_node(nodes: &mut Vec<Node>, items: &mut [(u32, Aabb, [f64; 3])]) -> u32 {
200    let bb = bounds(items);
201    if items.len() == 1 {
202        let idx = nodes.len() as u32;
203        nodes.push(Node { aabb: bb, tri: items[0].0, left: 0, right: 0 });
204        return idx;
205    }
206    // split on the longest centroid axis at the median
207    let mut span = [0.0f64; 3];
208    let (mut clo, mut chi) = (items[0].2, items[0].2);
209    for it in items.iter() {
210        for k in 0..3 {
211            clo[k] = clo[k].min(it.2[k]);
212            chi[k] = chi[k].max(it.2[k]);
213        }
214    }
215    for k in 0..3 {
216        span[k] = chi[k] - clo[k];
217    }
218    let axis = if span[0] >= span[1] && span[0] >= span[2] {
219        0
220    } else if span[1] >= span[2] {
221        1
222    } else {
223        2
224    };
225    // total_cmp: identical to partial_cmp for the finite centroids built here,
226    // but a total order by construction (no Equal-on-NaN escape hatch).
227    items.sort_by(|a, b| a.2[axis].total_cmp(&b.2[axis]));
228    let mid = items.len() / 2;
229    let (l, r) = items.split_at_mut(mid);
230    let left = build_node(nodes, l);
231    let right = build_node(nodes, r);
232    let idx = nodes.len() as u32;
233    nodes.push(Node { aabb: bb, tri: u32::MAX, left, right });
234    idx
235}
236
237/// All AABB-overlapping `(i, j)` pairs between `a` and `b`, sorted by `(i, j)` —
238/// a drop-in for the all-pairs+bbox loop with identical output order.
239pub fn candidate_pairs(a: &[Tri], b: &[Tri]) -> Vec<(usize, usize)> {
240    let bvh = Bvh::build(b);
241    let mut pairs = Vec::new();
242    let mut cand = Vec::new();
243    for (i, ta) in a.iter().enumerate() {
244        cand.clear();
245        bvh.query(&tri_aabb(ta), &mut cand);
246        for &j in &cand {
247            pairs.push((i, j as usize));
248        }
249    }
250    pairs.sort_unstable();
251    pairs
252}
253
254#[cfg(test)]
255mod tests {
256    use super::*;
257
258    fn brute(a: &[Tri], b: &[Tri]) -> Vec<(usize, usize)> {
259        let mut p = Vec::new();
260        for (i, ta) in a.iter().enumerate() {
261            for (j, tb) in b.iter().enumerate() {
262                if overlap(&tri_aabb(ta), &tri_aabb(tb)) {
263                    p.push((i, j));
264                }
265            }
266        }
267        p.sort_unstable();
268        p
269    }
270
271    #[test]
272    fn ray_and_point_candidates_are_conservative_supersets() {
273        // A scattered cloud of triangles; every ray/point query's candidate set
274        // must CONTAIN every triangle the brute-force AABB test admits (a missed
275        // candidate would change the exact parity/containment downstream).
276        let tris: Vec<Tri> = (0..60)
277            .map(|i| {
278                let (x, y, z) = (i as f64 * 0.7, (i % 7) as f64 * 1.3, (i % 5) as f64 * 0.9);
279                [[x, y, z], [x + 0.4, y, z], [x, y + 0.5, z + 0.3]]
280            })
281            .collect();
282        let bvh = Bvh::build(&tris);
283        let rays = [
284            ([0.0, 0.0, 0.0], [40.0, 9.0, 4.0]),
285            ([5.0, 3.0, 1.0], [5.0001, 3.0, 100.0]),
286            ([-2.0, -2.0, -2.0], [42.0, 12.0, 6.0]),
287        ];
288        for (p, far) in rays {
289            let mut cand = Vec::new();
290            bvh.ray_candidates(p, far, &mut cand);
291            let cset: std::collections::HashSet<u32> = cand.into_iter().collect();
292            for (i, t) in tris.iter().enumerate() {
293                if seg_hits_aabb(p, far, &tri_aabb(t), 0.0) {
294                    assert!(cset.contains(&(i as u32)), "ray missed candidate {i}");
295                }
296            }
297        }
298        for p in [[5.2, 3.0, 0.1], [0.1, 0.0, 0.0], [41.0, 7.0, 3.6]] {
299            let mut cand = Vec::new();
300            bvh.point_candidates(p, 0.0, &mut cand);
301            let cset: std::collections::HashSet<u32> = cand.into_iter().collect();
302            for (i, t) in tris.iter().enumerate() {
303                if aabb_contains(p, &tri_aabb(t), 0.0) {
304                    assert!(cset.contains(&(i as u32)), "point missed candidate {i}");
305                }
306            }
307        }
308    }
309
310    #[test]
311    fn bvh_candidate_pairs_match_brute_force() {
312        // a fan of triangles overlapping a moved fan — the BVH must return EXACTLY
313        // the all-pairs+bbox result (in identical (i,j) order).
314        let mk = |dx: f64| -> Vec<Tri> {
315            (0..20)
316                .map(|i| {
317                    let x = dx + i as f64 * 0.3;
318                    [[x, 0., 0.], [x + 0.5, 0., 0.], [x, 0.5, 0.4]]
319                })
320                .collect()
321        };
322        let a = mk(0.0);
323        let b = mk(1.7);
324        assert_eq!(candidate_pairs(&a, &b), brute(&a, &b));
325        // disjoint sets → no pairs
326        let far = mk(1000.0);
327        assert!(candidate_pairs(&a, &far).is_empty());
328    }
329}