Skip to main content

fast_mesh_query/
selfpairs.rs

1//! Self-intersection: the triangle pairs of one mesh that intersect each
2//! other.
3
4use std::sync::atomic::{AtomicBool, Ordering};
5
6use fast_mesh_bvh::{Aabb, Bvh, NodeId, NodeKind};
7use fast_mesh_store::{Mesh, TriangleId, par};
8
9use crate::pair::{
10    PAIR_STACK, PAR_PARTS, Step, oom, split_pair, traverse_subtree_pairs_with, try_push,
11};
12use crate::ray::points;
13use crate::tritri::{P3, collinear, tri_tri_intersect};
14use crate::{Appender, Result, Rigid, check};
15
16/// Appends to `out` every pair `(a, b)` with `a < b` of alive triangles of
17/// `mesh` that intersect or touch and share no vertex id. The appended pairs
18/// are sorted; elements already in `out` are kept and stay in front.
19///
20/// The test is exact on the `f64` coordinates, as for
21/// [`intersecting_pairs`](crate::intersecting_pairs). Pairs that share a
22/// vertex id are not tested at all: neighbours always touch along their
23/// shared edge or corner, so such a pair is never reported, even when the two
24/// triangles also cross elsewhere (a fold back onto a neighbour). Degenerate
25/// triangles, whose corners are collinear or repeat a vertex id, are skipped
26/// too. Distinct vertices at the same position are distinct vertices: two
27/// triangles meeting only through such a pair of vertices touch, and are
28/// reported.
29///
30/// The BVH is walked against itself so that each unordered pair of nodes is
31/// visited once, and each pair of triangles tested once.
32///
33/// # Errors
34/// [`QueryError::MeshMismatch`](crate::QueryError::MeshMismatch) or
35/// [`QueryError::StaleBvh`](crate::QueryError::StaleBvh) when `bvh` does not
36/// match `mesh`, and [`QueryError::OutOfMemory`](crate::QueryError::OutOfMemory)
37/// when `out` cannot grow. On any error `out` keeps its original length.
38pub fn self_intersecting_pairs(
39    mesh: &Mesh,
40    bvh: &Bvh,
41    out: &mut Vec<(TriangleId, TriangleId)>,
42) -> Result<()> {
43    check(mesh, bvh)?;
44    let mut ap = Appender::new(out);
45    let Some(root) = bvh.root() else {
46        return Ok(());
47    };
48    if par::enabled() && !par::force_sequential() && mesh.triangle_count() >= PAR_MIN_SELF_TRIS {
49        // Each part's pairs in part order, sorted below.
50        let parts = split(bvh, root)?;
51        let found = par::map_chunks(&parts, 1, 0, oom, |_, part| {
52            let mut pairs = Vec::new();
53            let mut failed = None;
54            part[0].search(bvh, &mut |a, b| {
55                if let Some(pair) = intersecting(mesh, a, b)
56                    && let Err(e) = try_push(&mut pairs, pair)
57                {
58                    failed = Some(e);
59                    return false;
60                }
61                true
62            });
63            failed.map_or(Ok(pairs), Err)
64        })?;
65        for pairs in &found {
66            ap.extend(pairs)?;
67        }
68    } else {
69        let mut failed = None;
70        Part::Within(root).search(bvh, &mut |a, b| {
71            if let Some(pair) = intersecting(mesh, a, b)
72                && let Err(e) = ap.push(pair)
73            {
74                failed = Some(e);
75                return false;
76            }
77            true
78        });
79        if let Some(e) = failed {
80            return Err(e);
81        }
82    }
83    ap.appended().sort_unstable();
84    Ok(())
85}
86
87/// Whether any two alive triangles of `mesh` intersect or touch while
88/// sharing no vertex id: the early-exit form of
89/// [`self_intersecting_pairs`], for callers that only need the bool.
90///
91/// The pair test is the same one, so the answer agrees with whether
92/// [`self_intersecting_pairs`] appends anything: neighbours that share a
93/// vertex id are never reported, degenerate triangles are skipped, and
94/// distinct vertices at the same position are distinct vertices. The
95/// search stops at the first intersecting pair it finds; the sequential
96/// search allocates nothing.
97///
98/// # Errors
99/// [`QueryError::MeshMismatch`](crate::QueryError::MeshMismatch) or
100/// [`QueryError::StaleBvh`](crate::QueryError::StaleBvh) when `bvh` does not
101/// match `mesh`, and [`QueryError::OutOfMemory`](crate::QueryError::OutOfMemory)
102/// when the parallel split of the search cannot grow.
103pub fn self_intersects(mesh: &Mesh, bvh: &Bvh) -> Result<bool> {
104    check(mesh, bvh)?;
105    let Some(root) = bvh.root() else {
106        return Ok(false);
107    };
108    if par::enabled() && !par::force_sequential() && mesh.triangle_count() >= PAR_MIN_SELF_TRIS {
109        // Each part stops at its own first hit, or once another part found one.
110        let parts = split(bvh, root)?;
111        let found = AtomicBool::new(false);
112        let hit = par::map_chunks(&parts, 1, 0, oom, |_, part| {
113            let mut yes = false;
114            part[0].search(bvh, &mut |a, b| {
115                if found.load(Ordering::Relaxed) {
116                    return false;
117                }
118                if intersecting(mesh, a, b).is_some() {
119                    found.store(true, Ordering::Relaxed);
120                    yes = true;
121                    return false;
122                }
123                true
124            });
125            Ok(yes)
126        })?;
127        Ok(hit.into_iter().any(|b| b))
128    } else {
129        let mut yes = false;
130        Part::Within(root).search(bvh, &mut |a, b| {
131            if intersecting(mesh, a, b).is_some() {
132                yes = true;
133                false
134            } else {
135                true
136            }
137        });
138        Ok(yes)
139    }
140}
141
142/// Triangles from which [`self_intersecting_pairs`] splits its search into
143/// [`Part`]s searched in parallel, with the `parallel` feature.
144///
145/// Measured on the i5-13600K on terrains (no pair intersects): at 128
146/// triangles parallel is 1.2 to 1.5 times slower, at 512 between 1.3 times
147/// slower and 1.3 times faster from run to run, at 1,058 three times faster
148/// and at 32k nine times. Meshes that do cross themselves gain more.
149const PAR_MIN_SELF_TRIS: usize = 1024;
150
151/// A part of the self-intersection search.
152#[derive(Debug, Clone, Copy)]
153enum Part {
154    /// Every pair of triangles under one node.
155    Within(NodeId),
156    /// Every pair of a triangle under the first node and one under the
157    /// second, two disjoint subtrees whose boxes overlap.
158    Across(NodeId, NodeId),
159}
160
161impl Part {
162    /// Calls `test(a, b)` on every pair of triangles of the part that may
163    /// intersect, once each, until it returns `false`.
164    fn search(self, bvh: &Bvh, test: &mut impl FnMut(TriangleId, TriangleId) -> bool) {
165        match self {
166            Part::Within(root) => within(bvh, root, test),
167            Part::Across(p, q) => {
168                across(bvh, p, q, test);
169            }
170        }
171    }
172}
173
174/// Cuts the search of the tree below `root` into disjoint [`Part`]s, one
175/// level at a time, until there are [`PAR_PARTS`] or more, or none can be
176/// cut further. A node's `Within` becomes its children's two `Within`s and,
177/// when their boxes overlap, their `Across`; an `Across` becomes the
178/// `Across`es of its child pairs whose boxes overlap ([`split_pair`]). Each
179/// pair of triangles the whole search tests is in exactly one part, and in
180/// none of the dropped ones. The parts come in a fixed order for a given
181/// tree.
182fn split(bvh: &Bvh, root: NodeId) -> Result<Vec<Part>> {
183    let overlap = |p: &Aabb, q: &Aabb| p.overlaps(q).then_some(());
184    let mut parts = Vec::new();
185    try_push(&mut parts, Part::Within(root))?;
186    while parts.len() < PAR_PARTS {
187        let mut next = Vec::new();
188        let mut cut = false;
189        for &part in &parts {
190            match part {
191                Part::Within(n) => match bvh.node(n).kind {
192                    NodeKind::Leaf { .. } => try_push(&mut next, part)?,
193                    NodeKind::Inner { left, right } => {
194                        cut = true;
195                        try_push(&mut next, Part::Within(left))?;
196                        try_push(&mut next, Part::Within(right))?;
197                        if bvh.node(left).bounds.overlaps(&bvh.node(right).bounds) {
198                            try_push(&mut next, Part::Across(left, right))?;
199                        }
200                    }
201                },
202                Part::Across(p, q) => {
203                    match split_pair(bvh, p, bvh, q, &Rigid::IDENTITY, &overlap) {
204                        None => try_push(&mut next, part)?,
205                        Some(children) => {
206                            cut = true;
207                            for (p, q) in children.into_iter().flatten() {
208                                try_push(&mut next, Part::Across(p, q))?;
209                            }
210                        }
211                    }
212                }
213            }
214        }
215        parts = next;
216        if !cut {
217            break;
218        }
219    }
220    Ok(parts)
221}
222
223/// Calls `test` on the pairs under `root` that may intersect, until it
224/// returns `false`.
225fn within(bvh: &Bvh, root: NodeId, test: &mut impl FnMut(TriangleId, TriangleId) -> bool) {
226    let mut ok = true;
227    // Each unordered pair of distinct leaves has one lowest common ancestor,
228    // with one leaf under each child; each pair inside one leaf is that
229    // leaf's. So walking every node once, pairing its two children's
230    // subtrees or a leaf's triangles among themselves, tests every pair of
231    // triangles once.
232    bvh.traverse_from(root, |_, view| {
233        if !ok {
234            return false;
235        }
236        match view.kind {
237            NodeKind::Leaf { triangles } => {
238                for (i, &a) in triangles.iter().enumerate() {
239                    for &b in &triangles[i + 1..] {
240                        if !test(a, b) {
241                            ok = false;
242                            return false;
243                        }
244                    }
245                }
246            }
247            NodeKind::Inner { left, right } => {
248                if bvh.node(left).bounds.overlaps(&bvh.node(right).bounds) {
249                    ok = across(bvh, left, right, test);
250                }
251            }
252        }
253        ok
254    });
255}
256
257/// Calls `test` on the pairs of a triangle under `p` and one under `q` that
258/// may intersect, until it returns `false`; returns `false` then.
259fn across(
260    bvh: &Bvh,
261    p: NodeId,
262    q: NodeId,
263    test: &mut impl FnMut(TriangleId, TriangleId) -> bool,
264) -> bool {
265    let mut ok = true;
266    traverse_subtree_pairs_with::<PAIR_STACK, f64>(
267        bvh,
268        p,
269        bvh,
270        q,
271        &Rigid::IDENTITY,
272        |p, q| p.overlaps(q).then_some(0.0),
273        |va, vb, _| {
274            let (NodeKind::Leaf { triangles: ta }, NodeKind::Leaf { triangles: tb }) =
275                (va.kind, vb.kind)
276            else {
277                return Step::Descend;
278            };
279            for &a in ta {
280                for &b in tb {
281                    if !test(a, b) {
282                        ok = false;
283                        return Step::Stop;
284                    }
285                }
286            }
287            Step::Skip
288        },
289    );
290    ok
291}
292
293/// `(min, max)` of `a` and `b` when they share no vertex id, neither is
294/// degenerate, and they intersect.
295fn intersecting(mesh: &Mesh, a: TriangleId, b: TriangleId) -> Option<(TriangleId, TriangleId)> {
296    let (va, vb) = (mesh.triangles()[a as usize], mesh.triangles()[b as usize]);
297    if va.iter().any(|v| vb.contains(v)) {
298        return None;
299    }
300    let (pa, pb) = (tri(mesh, a), tri(mesh, b));
301    let apart = (0..3).any(|k| {
302        let lo = |p: &[P3; 3]| p[0][k].min(p[1][k]).min(p[2][k]);
303        let hi = |p: &[P3; 3]| p[0][k].max(p[1][k]).max(p[2][k]);
304        lo(&pa) > hi(&pb) || lo(&pb) > hi(&pa)
305    });
306    // A repeated vertex id makes the corners collinear. Degeneracy only
307    // matters for pairs that intersect, so it is decided last.
308    (!apart && tri_tri_intersect(pa, pb) && !collinear::<false>(&pa) && !collinear::<false>(&pb))
309        .then_some((a.min(b), a.max(b)))
310}
311
312/// Triangle `t` in `f64`.
313fn tri(mesh: &Mesh, t: TriangleId) -> [P3; 3] {
314    points(mesh, t).map(|p| p.map(f64::from))
315}
316
317#[cfg(test)]
318mod tests {
319    use fast_mesh_bvh::Quality;
320
321    use super::*;
322    use crate::test_util::{build, wavy};
323    use crate::tritri::{collinear, tri_tri_intersect};
324    use crate::{QueryError, fail_allocations_after};
325
326    /// A closed UV sphere of radius 1 with outward winding.
327    fn uv_sphere(stacks: u32, slices: u32) -> Mesh {
328        use std::f64::consts::PI;
329        let mut positions = vec![0.0f32, 0.0, 1.0];
330        for i in 1..stacks {
331            let theta = PI * f64::from(i) / f64::from(stacks);
332            for j in 0..slices {
333                let phi = 2.0 * PI * f64::from(j) / f64::from(slices);
334                positions.extend([
335                    (theta.sin() * phi.cos()) as f32,
336                    (theta.sin() * phi.sin()) as f32,
337                    theta.cos() as f32,
338                ]);
339            }
340        }
341        let south = (positions.len() / 3) as u32;
342        positions.extend([0.0, 0.0, -1.0]);
343        let ring = |i: u32, j: u32| 1 + (i - 1) * slices + j % slices;
344        let mut indices = Vec::new();
345        for j in 0..slices {
346            indices.extend([0, ring(1, j), ring(1, j + 1)]);
347            for i in 1..stacks - 1 {
348                let (a, b, c, d) = (
349                    ring(i, j),
350                    ring(i + 1, j),
351                    ring(i + 1, j + 1),
352                    ring(i, j + 1),
353                );
354                indices.extend([a, b, c, a, c, d]);
355            }
356            indices.extend([ring(stacks - 1, j), south, ring(stacks - 1, j + 1)]);
357        }
358        Mesh::from_buffers(&positions, &indices).unwrap()
359    }
360
361    /// Two closed boxes, `[0, 2]^3` and `[1, 3]^3`, that interpenetrate
362    /// without sharing a vertex.
363    fn crossed_boxes() -> Mesh {
364        let mut positions = Vec::new();
365        let mut indices = Vec::new();
366        for (base, lo, hi) in [(0u32, 0.0f32, 2.0f32), (8, 1.0, 3.0)] {
367            for i in 0..8 {
368                positions.extend((0..3).map(|k| if i >> k & 1 == 1 { hi } else { lo }));
369            }
370            let faces: [[u32; 3]; 12] = [
371                [0, 4, 6],
372                [0, 6, 2],
373                [1, 3, 7],
374                [1, 7, 5],
375                [0, 1, 5],
376                [0, 5, 4],
377                [2, 6, 7],
378                [2, 7, 3],
379                [0, 2, 3],
380                [0, 3, 1],
381                [4, 5, 7],
382                [4, 7, 6],
383            ];
384            indices.extend(faces.iter().flatten().map(|v| v + base));
385        }
386        Mesh::from_buffers(&positions, &indices).unwrap()
387    }
388
389    /// Every qualifying pair by brute force over all triangle pairs.
390    fn reference(mesh: &Mesh) -> Vec<(TriangleId, TriangleId)> {
391        let tri = |t: TriangleId| {
392            let [a, b, c] = mesh.triangles()[t as usize];
393            let p = |v: u32| mesh.positions()[v as usize].map(f64::from);
394            ([a, b, c], [p(a), p(b), p(c)])
395        };
396        let n = mesh.triangle_slots() as TriangleId;
397        let mut out = Vec::new();
398        for a in (0..n).filter(|&t| mesh.is_triangle_alive(t)) {
399            for b in (a + 1..n).filter(|&t| mesh.is_triangle_alive(t)) {
400                let ((va, pa), (vb, pb)) = (tri(a), tri(b));
401                let shared = va.iter().any(|v| vb.contains(v));
402                if !shared
403                    && !collinear::<false>(&pa)
404                    && !collinear::<false>(&pb)
405                    && tri_tri_intersect(pa, pb)
406                {
407                    out.push((a, b));
408                }
409            }
410        }
411        out
412    }
413
414    fn pairs(mesh: &Mesh, bvh: &Bvh) -> Vec<(TriangleId, TriangleId)> {
415        let mut out = Vec::new();
416        self_intersecting_pairs(mesh, bvh, &mut out).unwrap();
417        out
418    }
419
420    #[test]
421    fn self_pairs_crossed_boxes_nonempty_and_sphere_empty() {
422        let boxes = crossed_boxes();
423        let sphere = uv_sphere(8, 12);
424        for q in [Quality::Sah, Quality::Fast] {
425            let got = pairs(&boxes, &build(&boxes, q));
426            assert!(!got.is_empty());
427            assert_eq!(got, reference(&boxes));
428            assert!(got.iter().all(|&(a, b)| a < 12 && b >= 12));
429            assert!(pairs(&sphere, &build(&sphere, q)).is_empty());
430        }
431        // Default leaves hold several triangles: pairs inside one leaf count.
432        let bvh = Bvh::build(&boxes, &Default::default()).unwrap();
433        assert_eq!(pairs(&boxes, &bvh), reference(&boxes));
434    }
435
436    #[test]
437    fn self_pairs_skip_vertex_sharing_neighbours() {
438        // Every triangle of a closed sphere touches its neighbours, through
439        // shared vertex ids only.
440        let sphere = uv_sphere(16, 24);
441        let bvh = build(&sphere, Quality::Sah);
442        assert_eq!(pairs(&sphere, &bvh), vec![]);
443        // Two wavy sheets, one shifted into the other: they cross, and only
444        // the pairs between the sheets, which share no vertex, are reported.
445        let (sheet, lifted) = (wavy(4), wavy(4));
446        let mut positions: Vec<f32> = sheet.positions().iter().flatten().copied().collect();
447        let base = (positions.len() / 3) as u32;
448        positions.extend(
449            lifted
450                .positions()
451                .iter()
452                .flat_map(|p| [p[0] + 0.25, p[1] + 0.5, p[2] + 0.5]),
453        );
454        let mut indices: Vec<u32> = sheet.triangles().iter().flatten().copied().collect();
455        indices.extend(lifted.triangles().iter().flatten().map(|v| v + base));
456        let mesh = Mesh::from_buffers(&positions, &indices).unwrap();
457        for q in [Quality::Sah, Quality::Fast] {
458            let got = pairs(&mesh, &build(&mesh, q));
459            assert!(!got.is_empty());
460            assert_eq!(got, reference(&mesh));
461        }
462    }
463
464    #[test]
465    fn self_pairs_skip_degenerate_and_keep_coincident_vertices() {
466        // A triangle crossing a flat square, as one with a repeated id, as a
467        // collinear one, and as a proper one.
468        #[rustfmt::skip]
469        let positions = [
470            0.0, 0.0, 0.0, 2.0, 0.0, 0.0, 2.0, 2.0, 0.0, 0.0, 2.0, 0.0, // 0-3 square
471            1.0, 1.0, -1.0, 1.0, 1.0, 1.0, 1.5, 1.0, 1.0, // 4-6 crossing
472            1.0, 1.0, 0.0, // 7, collinear with 4 and 5
473            0.0, 0.0, 0.0, // 8, at vertex 0's position
474            -1.0, 0.0, 1.0, 0.0, -1.0, 1.0, // 9-10, above the square's corner
475        ];
476        #[rustfmt::skip]
477        let indices = [
478            0, 1, 2, 0, 2, 3, // triangles 0-1: the square
479            4, 5, 5, // 2: a repeated id
480            4, 5, 7, // 3: collinear
481            4, 5, 6, // 4: proper, through the square's diagonal
482            8, 9, 10, // 5: touching the square only at vertex 8
483        ];
484        let mesh = Mesh::from_buffers(&positions, &indices).unwrap();
485        let bvh = build(&mesh, Quality::Sah);
486        // Triangles 2 and 3 cross the square too but are degenerate. Triangle
487        // 4 meets the diagonal shared by 0 and 1; triangle 5 touches both at
488        // the corner where vertex 8 coincides with vertex 0, sharing no id.
489        assert_eq!(pairs(&mesh, &bvh), vec![(0, 4), (0, 5), (1, 4), (1, 5)]);
490        assert_eq!(pairs(&mesh, &bvh), reference(&mesh));
491    }
492
493    #[test]
494    fn self_pairs_appends_sorted_and_checks_the_bvh() {
495        let boxes = crossed_boxes();
496        let bvh = build(&boxes, Quality::Sah);
497        let mut out = vec![(99, 99)];
498        self_intersecting_pairs(&boxes, &bvh, &mut out).unwrap();
499        assert_eq!(out[0], (99, 99));
500        assert_eq!(&out[1..], reference(&boxes).as_slice());
501
502        let other = crossed_boxes();
503        assert_eq!(
504            self_intersecting_pairs(&other, &bvh, &mut out),
505            Err(QueryError::MeshMismatch)
506        );
507        let mut edited = crossed_boxes();
508        let stale = build(&edited, Quality::Sah);
509        let mut tx = edited.begin_edit();
510        tx.set_positions(&[0], &[[0.5, 0.0, 0.0]]).unwrap();
511        tx.commit().unwrap();
512        assert_eq!(
513            self_intersecting_pairs(&edited, &stale, &mut out),
514            Err(QueryError::StaleBvh)
515        );
516    }
517
518    #[test]
519    fn self_pairs_oom_keeps_out() {
520        let boxes = crossed_boxes();
521        let bvh = build(&boxes, Quality::Sah);
522        let want = reference(&boxes);
523        for n in 0..want.len() {
524            let mut out = vec![(7, 7)];
525            fail_allocations_after(Some(n));
526            let got = self_intersecting_pairs(&boxes, &bvh, &mut out);
527            fail_allocations_after(None);
528            assert_eq!(got, Err(QueryError::OutOfMemory));
529            assert_eq!(out, vec![(7, 7)]);
530        }
531    }
532
533    #[test]
534    fn self_pairs_bit_identical() {
535        use crate::test_util::{assert_parallel_identical, crossed_ripples};
536        let mesh = crossed_ripples(60);
537        assert!(mesh.triangle_count() >= PAR_MIN_SELF_TRIS);
538        for quality in [Quality::Sah, Quality::Fast] {
539            let bvh = build(&mesh, quality);
540            let found = assert_parallel_identical("self pairs", || pairs(&mesh, &bvh));
541            assert!(found.len() > 500, "{quality:?}: {} pairs", found.len());
542        }
543    }
544
545    #[test]
546    fn self_intersects_matches_pairs_emptiness() {
547        let boxes = crossed_boxes();
548        let sphere = uv_sphere(8, 12);
549        for q in [Quality::Sah, Quality::Fast] {
550            assert_eq!(
551                self_intersects(&boxes, &build(&boxes, q)),
552                Ok(!pairs(&boxes, &build(&boxes, q)).is_empty())
553            );
554            assert_eq!(
555                self_intersects(&sphere, &build(&sphere, q)),
556                Ok(!pairs(&sphere, &build(&sphere, q)).is_empty())
557            );
558        }
559        assert_eq!(
560            self_intersects(&boxes, &build(&boxes, Quality::Sah)),
561            Ok(true)
562        );
563        assert_eq!(
564            self_intersects(&sphere, &build(&sphere, Quality::Sah)),
565            Ok(false)
566        );
567    }
568
569    #[test]
570    fn self_intersects_checks_the_bvh_and_allocates_nothing_sequential() {
571        let boxes = crossed_boxes();
572        let bvh = build(&boxes, Quality::Sah);
573        let other = crossed_boxes();
574        assert_eq!(self_intersects(&other, &bvh), Err(QueryError::MeshMismatch));
575        let mut edited = crossed_boxes();
576        let stale = build(&edited, Quality::Sah);
577        let mut tx = edited.begin_edit();
578        tx.set_positions(&[0], &[[0.5, 0.0, 0.0]]).unwrap();
579        tx.commit().unwrap();
580        assert_eq!(self_intersects(&edited, &stale), Err(QueryError::StaleBvh));
581        // The sequential search allocates nothing: the fault hook armed at
582        // the first reservation still lets it answer.
583        fail_allocations_after(Some(0));
584        let got = self_intersects(&boxes, &bvh);
585        fail_allocations_after(None);
586        assert_eq!(got, Ok(true));
587    }
588
589    #[test]
590    fn self_intersects_bit_identical() {
591        use crate::test_util::{assert_parallel_identical, crossed_ripples};
592        let mesh = crossed_ripples(60);
593        assert!(mesh.triangle_count() >= PAR_MIN_SELF_TRIS);
594        for quality in [Quality::Sah, Quality::Fast] {
595            let bvh = build(&mesh, quality);
596            let found =
597                assert_parallel_identical("self intersects", || self_intersects(&mesh, &bvh));
598            assert_eq!(found, Ok(true));
599        }
600        let sphere = uv_sphere(16, 24);
601        let bvh = build(&sphere, Quality::Sah);
602        assert_eq!(self_intersects(&sphere, &bvh), Ok(false));
603    }
604}