diff --git a/CHANGELOG.md b/CHANGELOG.md index 91b9256..49f49ea 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,10 @@ and this project adheres to [Semantic Versioning](http://semver.org/). - Add fuzzing suite [#113](https://github.com/svenstaro/bvh/pull/113) (thanks @finnbear) - Fix some assertions [#129](https://github.com/svenstaro/bvh/pull/129) (thanks @finnbear) - Fix traversal in case of single-node BVH [#130](https://github.com/svenstaro/bvh/pull/130) (thanks @finnbear) +- **Breaking change:** BVH traversal now accepts a `Query: IntersectsAabb` rather than a `Ray`, + allowing points, AABB's, and circles/spheres to be tested, too. Most use-cases involving `Ray` + will continue to compile as-is. If you previously wrote `BvhTraverseIterator`, you'll + need to change it to `BvhTraverseIterator`. [#128](https://github.com/svenstaro/bvh/pull/128) (thanks @finnbear) ## 0.10.0 - 2024-07-06 - Don't panic when traversing empty BVH [#106](https://github.com/svenstaro/bvh/pull/106) (thanks @finnbear) diff --git a/fuzz/fuzz_targets/fuzz.rs b/fuzz/fuzz_targets/fuzz.rs index 75a5cfc..d9cb94f 100644 --- a/fuzz/fuzz_targets/fuzz.rs +++ b/fuzz/fuzz_targets/fuzz.rs @@ -20,9 +20,11 @@ use std::fmt::{self, Debug, Formatter}; use std::hash::{Hash, Hasher}; use arbitrary::Arbitrary; -use bvh::aabb::{Aabb, Bounded}; +use bvh::aabb::{Aabb, Bounded, IntersectsAabb}; +use bvh::ball::Ball; use bvh::bounding_hierarchy::{BHShape, BoundingHierarchy}; use bvh::bvh::Bvh; +use bvh::flat_bvh::FlatBvh; use bvh::ray::Ray; use libfuzzer_sys::fuzz_target; use nalgebra::{Point, SimdPartialOrd}; @@ -32,7 +34,8 @@ type Float = f32; /// Coordinate magnitude should not exceed this which prevents /// certain degenerate cases like infinity, both in inputs -/// and internal computations in the BVH. +/// and internal computations in the BVH. For `Mode::Grid`, +/// offsets of 1/3 should be representable. const LIMIT: Float = 5_000.0; // The entry point for `cargo fuzz`. @@ -45,12 +48,33 @@ fuzz_target!(|workload: Workload<3>| { #[derive(Clone, Arbitrary)] struct ArbitraryPoint { coordinates: [NotNan; D], + mode: Mode, +} + +impl Debug for ArbitraryPoint { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + Debug::fmt(&self.point(), f) + } } impl ArbitraryPoint { /// Produces the corresponding point from the input. fn point(&self) -> Point { - Point::<_, D>::from_slice(&self.coordinates).map(|f| f.into_inner().clamp(-LIMIT, LIMIT)) + Point::<_, D>::from_slice(&self.coordinates).map(|f| { + // Float may be large or infinite, but is guaranteed to not be NaN due + // to the `NotNan` wrapper. + // + // Clamp it to a smaller range so that offsets of 1/3 are easily representable, + // which helps `Mode::Grid`. + let ret = f.into_inner().clamp(-LIMIT, LIMIT); + + if self.mode.is_grid() { + // Round each coordinate to an integer, as per `Mode::Grid` docs. + ret.round() + } else { + ret + } + }) } } @@ -60,7 +84,7 @@ impl ArbitraryPoint { struct ArbitraryShape { a: ArbitraryPoint, b: ArbitraryPoint, - mode: Mode, + /// This will end up being mutated, but initializing it arbitrarily could catch bugs. bh_node_index: usize, } @@ -84,15 +108,19 @@ impl Bounded for ArbitraryShape { let mut aabb = Aabb::with_bounds(a.simd_min(b), a.simd_max(b)); - if self.mode.is_grid() { - let mut center = aabb.center(); - center.iter_mut().for_each(|f| *f = f.round()); - // Unit AABB around center. - aabb.min.iter_mut().enumerate().for_each(|(i, f)| { - *f = center[i] - 0.5; - }); + if self.mode_is_grid() { + let min = aabb.min; aabb.max.iter_mut().enumerate().for_each(|(i, f)| { - *f = center[i] + 0.5; + // Coordinate should already be an integer, because `max` is in grid mode. + // + // Use `max` to ensure the AABB has volume, and add a margin described by `Mode::Grid`. + *f = f.max(min[i]) + 1.0 / 3.0; + }); + aabb.min.iter_mut().for_each(|f| { + // Coordinate should already be an integer, because `min` is in grid mode. + // + // Add a margin described by `Mode::Grid`. + *f -= 1.0 / 3.0; }); } @@ -110,13 +138,18 @@ impl BHShape for ArbitraryShape { } } +impl ArbitraryShape { + fn mode_is_grid(&self) -> bool { + self.a.mode.is_grid() && self.b.mode.is_grid() + } +} + /// The input for arbitrary ray, starting at an `ArbitraryPoint` and having a precisely /// normalized direction. #[derive(Clone, Arbitrary)] struct ArbitraryRay { origin: ArbitraryPoint, destination: ArbitraryPoint, - mode: Mode, } impl Debug for ArbitraryRay { @@ -126,6 +159,10 @@ impl Debug for ArbitraryRay { } impl ArbitraryRay { + fn mode_is_grid(&self) -> bool { + self.origin.mode.is_grid() && self.destination.mode.is_grid() + } + /// Produces the corresponding ray from the input. fn ray(&self) -> Ray { // Note that this eventually gets normalized in `Ray::new`. We don't expect precision issues @@ -142,9 +179,7 @@ impl ArbitraryRay { let mut ray = Ray::new(self.origin.point(), direction); - if self.mode.is_grid() { - ray.origin.iter_mut().for_each(|f| *f = f.round()); - + if self.mode_is_grid() { // Algorithm to find the closest unit-vector parallel to one of the axes. For fuzzing purposes, // we just want all 6 unit-vectors parallel to an axis (in the 3D case) to be *possible*. // @@ -174,6 +209,29 @@ impl ArbitraryRay { } } +/// The input for arbitrary ray, starting at an `ArbitraryPoint` and having a precisely +/// normalized direction. +#[derive(Clone, Arbitrary)] +struct ArbitraryBall { + center: ArbitraryPoint, + radius: NotNan, +} + +impl Debug for ArbitraryBall { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + Debug::fmt(&self.ball(), f) + } +} + +impl ArbitraryBall { + fn ball(&self) -> Ball { + Ball { + center: self.center.point(), + radius: self.radius.into_inner().max(0.0), + } + } +} + /// An arbitrary mutation to apply to the BVH to fuzz BVH optimization. #[derive(Debug, Arbitrary)] enum ArbitraryMutation { @@ -187,12 +245,14 @@ enum Mode { /// AABB's may have mostly arbitrary bounds, and ray may have mostly arbitrary /// origin and direction. Chaos, - /// AABB's are unit cubes, and must have integer coordinates. Ray must have an - /// origin consisting of integer coordinates and a direction that is parallel to - /// one of the axes. + /// AABB's bound integer coordinates with a margin of 1/3. Twice the margin, 2/3, + /// is less than 1, so AABB's can either deeply intersect or will have a gap at + /// least 1/3 wide. Ray must have an origin consisting of integer coordinates and + /// a direction that is parallel to one of the axes. Point must have integer + /// coordinates. /// - /// In this mode, all types of traversal are expected to yield the same results, - /// except when bugs exist that have yet to be fixed. + /// In this mode, all types of ray, AABB, and point traversal are expected to + /// yield the same results, except when bugs exist that have yet to be fixed. Grid, } @@ -206,11 +266,57 @@ impl Mode { #[derive(Debug, Arbitrary)] struct Workload { shapes: Vec>, + /// Traverse by ray. ray: ArbitraryRay, + /// Traverse by point. + point: ArbitraryPoint, + /// Traverse by AABB. + aabb: ArbitraryShape, + /// Traverse by ball. + ball: ArbitraryBall, mutations: Vec>, } impl Workload { + /// Compares normal, iterative, and flat-BVH traversal of the same query. + /// + /// Returns the result of normal traversal. + /// + /// # Panics + /// If `assert_agreement` is true, panics if the results differ in an + /// unexpected way. + fn fuzz_traversal<'a>( + &'a self, + bvh: &'a Bvh, + flat_bvh: &'a FlatBvh, + query: &impl IntersectsAabb, + assert_agreement: bool, + ) -> HashSet>> { + let traverse = bvh + .traverse(query, &self.shapes) + .into_iter() + .map(ByPtr) + .collect::>(); + let traverse_iterator = bvh + .traverse_iterator(query, &self.shapes) + .map(ByPtr) + .collect::>(); + let traverse_flat = flat_bvh + .traverse(query, &self.shapes) + .into_iter() + .map(ByPtr) + .collect::>(); + + if assert_agreement { + assert_eq!(traverse, traverse_iterator); + assert_eq!(traverse, traverse_flat); + } else { + // Fails, probably due to normal rounding errors. + } + + traverse + } + /// Called directly from the `cargo fuzz` entry point. Code in this function is /// easier for `rust-analyzer`` than code in that macro. /// @@ -233,41 +339,54 @@ impl Workload { } loop { + // `self.shapes` are all in grid mode. + let all_shapes_grid = self.shapes.iter().all(|s| s.mode_is_grid()); + // Under these circumstances, the ray either definitively hits an AABB or it definitively // doesn't. The lack of near hits and near misses prevents rounding errors that could cause // different traversal algorithms to disagree. // // This relates to the current state of the BVH. It may change after each mutation is applied // e.g. we could add the first non-grid shape or remove the last non-grid shape. - let assert_traversal_agreement = - self.ray.mode.is_grid() && self.shapes.iter().all(|s| s.mode.is_grid()); + let assert_ray_traversal_agreement = self.ray.mode_is_grid() && all_shapes_grid; + + // Under these circumstances, the `self.aabb` either definitively intersects with an AABB or + // it definitively doesn't. + // + // Similar meaning to `assert_ray_traversal_agreement`. + let assert_aabb_traversal_agreement = self.aabb.mode_is_grid() && all_shapes_grid; + + // Under these circumstances, the `self.point` is either definitively contained by an AABB or + // definitively not contained. + // + // Similar meaning to `assert_ray_traversal_agreement`. + let assert_point_traversal_agreement = self.point.mode.is_grid() && all_shapes_grid; // Check that these don't panic. bvh.assert_consistent(&self.shapes); bvh.assert_tight(); let flat_bvh = bvh.flatten(); - let traverse = bvh - .traverse(&ray, &self.shapes) - .into_iter() - .map(ByPtr) - .collect::>(); - let traverse_iterator = bvh - .traverse_iterator(&ray, &self.shapes) - .map(ByPtr) - .collect::>(); - let traverse_flat = flat_bvh - .traverse(&ray, &self.shapes) - .into_iter() - .map(ByPtr) - .collect::>(); - - if assert_traversal_agreement { - assert_eq!(traverse, traverse_iterator); - assert_eq!(traverse, traverse_flat); - } else { - // Fails, probably due to normal rounding errors. - } + let _traverse_ray = self.fuzz_traversal( + &bvh, + &flat_bvh, + &self.ray.ray(), + assert_ray_traversal_agreement, + ); + self.fuzz_traversal( + &bvh, + &flat_bvh, + &self.aabb.aabb(), + assert_aabb_traversal_agreement, + ); + self.fuzz_traversal( + &bvh, + &flat_bvh, + &self.point.point(), + assert_point_traversal_agreement, + ); + // Due to sphere geometry, `Mode::Grid` doesn't imply traversals will agree. + self.fuzz_traversal(&bvh, &flat_bvh, &self.ball.ball(), false); let nearest_traverse_iterator = bvh .nearest_traverse_iterator(&ray, &self.shapes) @@ -278,9 +397,9 @@ impl Workload { .map(ByPtr) .collect::>(); - if assert_traversal_agreement { + if assert_ray_traversal_agreement { // TODO: Fails, due to bug(s) e.g. https://github.com/svenstaro/bvh/issues/119 - //assert_eq!(traverse_iterator, nearest_traverse_iterator); + //assert_eq!(_traverse_ray, nearest_traverse_iterator); } else { // Fails, probably due to normal rounding errors. } diff --git a/src/aabb.rs b/src/aabb/aabb_impl.rs similarity index 93% rename from src/aabb.rs rename to src/aabb/aabb_impl.rs index b227c06..7bea907 100644 --- a/src/aabb.rs +++ b/src/aabb/aabb_impl.rs @@ -1,5 +1,3 @@ -//! Axis Aligned Bounding Boxes. - use nalgebra::{Point, SVector}; use std::fmt; use std::ops::Index; @@ -225,6 +223,30 @@ impl Aabb { && self.approx_contains_eps(&other.max, epsilon) } + /// Returns true if this [`Aabb`] touches the `other` [`Aabb`]. + /// + /// # Examples + /// ``` + /// use bvh::aabb::Aabb; + /// use nalgebra::Point3; + /// + /// let aabb1 = Aabb::with_bounds(Point3::new(-1.0, -1.0, -1.0), Point3::new(1.0, 1.0, 1.0)); + /// let aabb2 = Aabb::with_bounds(Point3::new(0.5, -0.1, -0.1), Point3::new(1.5, 0.1, 0.1)); + /// + /// assert!(aabb1.intersects_aabb(&aabb2)); + /// ``` + /// + /// [`Aabb`]: struct.Aabb.html + pub fn intersects_aabb(&self, aabb: &Aabb) -> bool { + // TODO: Try adding a SIMD specialization. + for i in 0..D { + if self.max[i] < aabb.min[i] || aabb.max[i] < self.min[i] { + return false; + } + } + true + } + /// Returns true if the `other` [`Aabb`] is approximately equal to this [`Aabb`] /// with respect to some `epsilon`. /// @@ -690,6 +712,32 @@ mod tests { } proptest! { + // Test properties of `Aabb` intersection. + #[test] + fn test_intersecting_aabbs(a: TupleVec, b: TupleVec, c: TupleVec, d: TupleVec, p: TupleVec) { + let a = tuple_to_point(&a); + let b = tuple_to_point(&b); + let c = tuple_to_point(&c); + let d = tuple_to_point(&d); + let aabb1 = TAabb3::empty().grow(&a).join_bounded(&b); + let aabb2 = TAabb3::empty().grow(&c).join_bounded(&d); + if aabb1.intersects_aabb(&aabb2) { + // For intersecting Aabb's, at least one point is shared. + let mut closest = aabb1.center(); + for i in 0..3 { + closest[i] = closest[i].clamp(aabb2.min[i], aabb2.max[i]); + } + assert!(aabb1.contains(&closest), "closest={closest:?}"); + assert!(aabb2.contains(&closest), "closest={closest:?}"); + } else { + // For non-intersecting Aabb's, no point can't be in both Aabb's. + let p = tuple_to_point(&p); + for point in [a, b, c, d, p] { + assert!(!aabb1.contains(&point) || !aabb2.contains(&point)); + } + } + } + // Test whether an empty `Aabb` does not contains anything. #[test] fn test_empty_contains_nothing(tpl: TupleVec) { diff --git a/src/aabb/intersection.rs b/src/aabb/intersection.rs new file mode 100644 index 0000000..43dea1c --- /dev/null +++ b/src/aabb/intersection.rs @@ -0,0 +1,45 @@ +use nalgebra::Point; + +use crate::{aabb::Aabb, bounding_hierarchy::BHValue}; + +/// A trait implemented by things that may or may not intersect an AABB and, by extension, +/// things that can be used to traverse a BVH. +pub trait IntersectsAabb { + /// Returns whether this object intersects an [`Aabb`]. For the purpose of intersection, + /// the [`Aabb`] is a solid with area/volume. As a result, intersecting an [`Aabb`] + /// implies intersecting any [`Aabb`] that contains that [`Aabb`]. + /// + /// # Examples + /// ``` + /// use bvh::aabb::{Aabb, IntersectsAabb}; + /// use nalgebra::Point3; + /// + /// struct XyPlane; + /// + /// impl IntersectsAabb for XyPlane { + /// fn intersects_aabb(&self, aabb: &Aabb) -> bool { + /// aabb.min[2] <= 0.0 && aabb.max[2] >= 0.0 + /// } + /// } + /// + /// let xy_plane = XyPlane; + /// let aabb = Aabb::with_bounds(Point3::new(-1.0,-1.0,-1.0), Point3::new(1.0,1.0,1.0)); + /// assert!(xy_plane.intersects_aabb(&aabb)); + /// ``` + /// + /// [`Aabb`]: struct.Aabb.html + /// + fn intersects_aabb(&self, aabb: &Aabb) -> bool; +} + +impl IntersectsAabb for Aabb { + fn intersects_aabb(&self, aabb: &Aabb) -> bool { + self.intersects_aabb(aabb) + } +} + +impl IntersectsAabb for Point { + fn intersects_aabb(&self, aabb: &Aabb) -> bool { + aabb.contains(self) + } +} diff --git a/src/aabb/mod.rs b/src/aabb/mod.rs new file mode 100644 index 0000000..818fc4b --- /dev/null +++ b/src/aabb/mod.rs @@ -0,0 +1,7 @@ +//! Axis Aligned Bounding Boxes. + +mod aabb_impl; +mod intersection; + +pub use aabb_impl::*; +pub use intersection::*; diff --git a/src/ball.rs b/src/ball.rs new file mode 100644 index 0000000..6e81bf3 --- /dev/null +++ b/src/ball.rs @@ -0,0 +1,125 @@ +//! Balls, including circles and spheres. + +use crate::{ + aabb::{Aabb, IntersectsAabb}, + bounding_hierarchy::BHValue, +}; +use nalgebra::Point; + +/// A circle, which can be used for traversing 2D BVHs. +pub type Circle = Ball; + +/// A sphere, which can be used for traversing 3D BVHs. +pub type Sphere = Ball; + +/// In 2D, a circle. In 3D, a sphere. This can be used for traversing BVHs. +#[derive(Debug, Clone, Copy)] +pub struct Ball { + /// The center of the ball. + pub center: Point, + /// The radius of the ball. + pub radius: T, +} + +impl Ball { + /// Creates a [`Ball`] with the given `center` and `radius`. + /// + /// # Panics + /// Panics, in debug mode, if the radius is negative. + /// + /// # Examples + /// ``` + /// use bvh::ball::Ball; + /// use nalgebra::Point3; + /// + /// let ball = Ball::new(Point3::new(1.0, 1.0, 1.0), 1.0); + /// assert_eq!(ball.center, Point3::new(1.0, 1.0, 1.0)); + /// assert_eq!(ball.radius, 1.0) + /// ``` + /// + /// [`Ball`]: struct.Ball.html + pub fn new(center: Point, radius: T) -> Self { + debug_assert!(radius >= T::from_f32(0.0).unwrap()); + Self { center, radius } + } + + /// Returns true if this [`Ball`] contains the [`Point`]. + /// + /// # Examples + /// ``` + /// use bvh::ball::Ball; + /// use nalgebra::Point3; + /// + /// let ball = Ball::new(Point3::new(1.0, 1.0, 1.0), 1.0); + /// let point = Point3::new(1.25, 1.25, 1.25); + /// + /// assert!(ball.contains(&point)); + /// ``` + /// + /// [`Ball`]: struct.Ball.html + pub fn contains(&self, point: &Point) -> bool { + let mut distance_squared = T::zero(); + for i in 0..D { + distance_squared += (point[i] - self.center[i]).powi(2); + } + // Squaring the RHS is faster than computing the square root of the LHS. + distance_squared <= self.radius.powi(2) + } + + /// Returns true if this [`Ball`] intersects the [`Aabb`]. + /// + /// # Examples + /// ``` + /// use bvh::{aabb::Aabb, ball::Ball}; + /// use nalgebra::Point3; + /// + /// let ball = Ball::new(Point3::new(1.0, 1.0, 1.0), 1.0); + /// let aabb = Aabb::with_bounds(Point3::new(1.25, 1.25, 1.25), Point3::new(3.0, 3.0, 3.0)); + /// + /// assert!(ball.intersects_aabb(&aabb)); + /// ``` + /// + /// [`Aabb`]: struct.Aabb.html + /// [`Ball`]: struct.Ball.html + pub fn intersects_aabb(&self, aabb: &Aabb) -> bool { + // https://gamemath.com/book/geomtests.html#intersection_sphere_aabb + // Finding the point in/on the AABB that is closest to the ball's center or, + // more specifically, find the squared distance between that point and the + // ball's center. + let mut distance_squared = T::zero(); + for i in 0..D { + let closest_on_aabb = self.center[i].clamp(aabb.min[i], aabb.max[i]); + distance_squared += (closest_on_aabb - self.center[i]).powi(2); + } + + // Then test if that point is in/on the ball. Squaring the RHS is faster than computing + // the square root of the LHS. + distance_squared <= self.radius.powi(2) + } +} + +impl IntersectsAabb for Ball { + fn intersects_aabb(&self, aabb: &Aabb) -> bool { + self.intersects_aabb(aabb) + } +} + +#[cfg(test)] +mod tests { + use super::Ball; + use crate::testbase::TPoint3; + + #[test] + fn ball_contains() { + let ball = Ball::new(TPoint3::new(3.0, 4.0, 5.0), 1.5); + + // Ball should contain its own center. + assert!(ball.contains(&ball.center)); + + // Test some manually-selected points. + let just_inside = TPoint3::new(3.04605, 3.23758, 3.81607); + let just_outside = TPoint3::new(3.06066, 3.15813, 3.70917); + assert!(ball.contains(&just_inside)); + assert!(!ball.contains(&just_outside)); + } +} diff --git a/src/bounding_hierarchy.rs b/src/bounding_hierarchy.rs index f38ca8e..1fe99bc 100644 --- a/src/bounding_hierarchy.rs +++ b/src/bounding_hierarchy.rs @@ -5,11 +5,10 @@ use nalgebra::{ }; use num::{Float, FromPrimitive, Signed}; -use crate::aabb::Bounded; +use crate::aabb::{Bounded, IntersectsAabb}; #[cfg(feature = "rayon")] use crate::bvh::rayon_executor; use crate::bvh::BvhNodeBuildArgs; -use crate::ray::Ray; /// Encapsulates the required traits for the value type used in the Bvh. pub trait BHValue: @@ -240,9 +239,9 @@ pub trait BoundingHierarchy { /// [`BoundingHierarchy`]: trait.BoundingHierarchy.html /// [`Aabb`]: ../aabb/struct.Aabb.html /// - fn traverse<'a, Shape: BHShape>( + fn traverse<'a, Query: IntersectsAabb, Shape: BHShape>( &'a self, - ray: &Ray, + query: &Query, shapes: &'a [Shape], ) -> Vec<&'a Shape>; @@ -258,12 +257,12 @@ impl> BoundingHierarchy>( + fn traverse<'a, Query: IntersectsAabb, Shape: BHShape>( &'a self, - ray: &Ray, + query: &Query, shapes: &'a [Shape], ) -> Vec<&'a Shape> { - H::traverse(self, ray, shapes) + H::traverse(self, query, shapes) } fn build_with_executor< diff --git a/src/bvh/bvh_impl.rs b/src/bvh/bvh_impl.rs index 609d7bb..56ac53f 100644 --- a/src/bvh/bvh_impl.rs +++ b/src/bvh/bvh_impl.rs @@ -4,7 +4,7 @@ //! [`BvhNode`]: struct.BvhNode.html //! -use crate::aabb::{Aabb, Bounded}; +use crate::aabb::{Aabb, Bounded, IntersectsAabb}; use crate::bounding_hierarchy::{BHShape, BHValue, BoundingHierarchy}; use crate::bvh::iter::BvhTraverseIterator; use crate::ray::Ray; @@ -97,9 +97,9 @@ impl Bvh { /// [`Bvh`]: struct.Bvh.html /// [`Aabb`]: ../aabb/struct.Aabb.html /// - pub fn traverse<'a, Shape: Bounded>( + pub fn traverse<'a, Query: IntersectsAabb, Shape: Bounded>( &'a self, - ray: &Ray, + query: &Query, shapes: &'a [Shape], ) -> Vec<&'a Shape> { if self.nodes.is_empty() { @@ -107,7 +107,7 @@ impl Bvh { return Vec::new(); } let mut indices = Vec::new(); - BvhNode::traverse_recursive(&self.nodes, 0, shapes, ray, &mut indices); + BvhNode::traverse_recursive(&self.nodes, 0, shapes, query, &mut indices); indices .iter() .map(|index| &shapes[*index]) @@ -115,17 +115,18 @@ impl Bvh { } /// Creates a [`BvhTraverseIterator`] to traverse the [`Bvh`]. - /// Returns a subset of `shapes`, in which the [`Aabb`]s of the elements were hit by [`Ray`]. + /// Returns a subset of `shapes`, in which the [`Aabb`]s of the elements for which + /// [`IntersectsAabb::intersects_aabb`] returns `true`. /// /// [`Bvh`]: struct.Bvh.html /// [`Aabb`]: ../aabb/struct.Aabb.html /// - pub fn traverse_iterator<'bvh, 'shape, Shape: Bounded>( + pub fn traverse_iterator<'bvh, 'shape, Query: IntersectsAabb, Shape: Bounded>( &'bvh self, - ray: &'bvh Ray, + query: &'bvh Query, shapes: &'shape [Shape], - ) -> BvhTraverseIterator<'bvh, 'shape, T, D, Shape> { - BvhTraverseIterator::new(self, ray, shapes) + ) -> BvhTraverseIterator<'bvh, 'shape, T, D, Query, Shape> { + BvhTraverseIterator::new(self, query, shapes) } /// Creates a [`DistanceTraverseIterator`] to traverse the [`Bvh`]. @@ -412,12 +413,12 @@ impl BoundingHierarchy for Bvh::build(shapes) } - fn traverse<'a, Shape: Bounded>( + fn traverse<'a, Query: IntersectsAabb, Shape: Bounded>( &'a self, - ray: &Ray, + query: &Query, shapes: &'a [Shape], ) -> Vec<&'a Shape> { - self.traverse(ray, shapes) + self.traverse(query, shapes) } fn pretty_print(&self) { diff --git a/src/bvh/bvh_node.rs b/src/bvh/bvh_node.rs index 7c32477..9f7d6b1 100644 --- a/src/bvh/bvh_node.rs +++ b/src/bvh/bvh_node.rs @@ -1,7 +1,5 @@ -use crate::aabb::{Aabb, Bounded}; +use crate::aabb::{Aabb, Bounded, IntersectsAabb}; use crate::bounding_hierarchy::{BHShape, BHValue}; - -use crate::ray::Ray; use crate::utils::{joint_aabb_of_shapes, Bucket}; use std::cell::RefCell; use std::marker::PhantomData; @@ -293,11 +291,11 @@ impl BvhNode { /// [`Bvh`]: struct.Bvh.html /// [`Ray`]: ../ray/struct.Ray.html /// - pub(crate) fn traverse_recursive>( + pub(crate) fn traverse_recursive, Shape: Bounded>( nodes: &[BvhNode], node_index: usize, shapes: &[Shape], - ray: &Ray, + query: &Query, indices: &mut Vec, ) { match nodes[node_index] { @@ -308,18 +306,18 @@ impl BvhNode { child_r_index, .. } => { - if ray.intersects_aabb(child_l_aabb) { - BvhNode::traverse_recursive(nodes, child_l_index, shapes, ray, indices); + if query.intersects_aabb(child_l_aabb) { + BvhNode::traverse_recursive(nodes, child_l_index, shapes, query, indices); } - if ray.intersects_aabb(child_r_aabb) { - BvhNode::traverse_recursive(nodes, child_r_index, shapes, ray, indices); + if query.intersects_aabb(child_r_aabb) { + BvhNode::traverse_recursive(nodes, child_r_index, shapes, query, indices); } } BvhNode::Leaf { shape_index, .. } => { // Either we got to a non-root node recursively, in which case the caller // checked our AABB, or we are processing the root node, in which case we // need to check the AABB. - if node_index != 0 || ray.intersects_aabb(&shapes[shape_index].aabb()) { + if node_index != 0 || query.intersects_aabb(&shapes[shape_index].aabb()) { indices.push(shape_index); } } diff --git a/src/bvh/iter.rs b/src/bvh/iter.rs index aafdfbd..57a8a67 100644 --- a/src/bvh/iter.rs +++ b/src/bvh/iter.rs @@ -1,14 +1,20 @@ -use crate::aabb::Bounded; +use crate::aabb::{Bounded, IntersectsAabb}; use crate::bounding_hierarchy::BHValue; use crate::bvh::{Bvh, BvhNode}; -use crate::ray::Ray; /// Iterator to traverse a [`Bvh`] without memory allocations -pub struct BvhTraverseIterator<'bvh, 'shape, T: BHValue, const D: usize, Shape: Bounded> { +pub struct BvhTraverseIterator< + 'bvh, + 'shape, + T: BHValue, + const D: usize, + Query: IntersectsAabb, + Shape: Bounded, +> { /// Reference to the [`Bvh`] to traverse bvh: &'bvh Bvh, - /// Reference to the input ray - ray: &'bvh Ray, + /// Reference to the input query + query: &'bvh Query, /// Reference to the input shapes array shapes: &'shape [Shape], /// Traversal stack. 4 billion items seems enough? @@ -21,19 +27,25 @@ pub struct BvhTraverseIterator<'bvh, 'shape, T: BHValue, const D: usize, Shape: has_node: bool, } -impl<'bvh, 'shape, T: BHValue, const D: usize, Shape: Bounded> - BvhTraverseIterator<'bvh, 'shape, T, D, Shape> +impl< + 'bvh, + 'shape, + T: BHValue, + const D: usize, + Query: IntersectsAabb, + Shape: Bounded, + > BvhTraverseIterator<'bvh, 'shape, T, D, Query, Shape> { /// Creates a new [`BvhTraverseIterator`] - pub fn new(bvh: &'bvh Bvh, ray: &'bvh Ray, shapes: &'shape [Shape]) -> Self { + pub fn new(bvh: &'bvh Bvh, query: &'bvh Query, shapes: &'shape [Shape]) -> Self { BvhTraverseIterator { bvh, - ray, + query, shapes, stack: [0; 32], node_index: 0, stack_size: 0, - has_node: iter_initially_has_node(bvh, ray, shapes), + has_node: iter_initially_has_node(bvh, query, shapes), } } @@ -71,7 +83,7 @@ impl<'bvh, 'shape, T: BHValue, const D: usize, Shape: Bounded> ref child_l_aabb, .. } => { - if self.ray.intersects_aabb(child_l_aabb) { + if self.query.intersects_aabb(child_l_aabb) { self.node_index = child_l_index; self.has_node = true; } else { @@ -93,7 +105,7 @@ impl<'bvh, 'shape, T: BHValue, const D: usize, Shape: Bounded> ref child_r_aabb, .. } => { - if self.ray.intersects_aabb(child_r_aabb) { + if self.query.intersects_aabb(child_r_aabb) { self.node_index = child_r_index; self.has_node = true; } else { @@ -107,8 +119,8 @@ impl<'bvh, 'shape, T: BHValue, const D: usize, Shape: Bounded> } } -impl<'shape, T: BHValue, const D: usize, Shape: Bounded> Iterator - for BvhTraverseIterator<'_, 'shape, T, D, Shape> +impl<'shape, T: BHValue, const D: usize, Query: IntersectsAabb, Shape: Bounded> Iterator + for BvhTraverseIterator<'_, 'shape, T, D, Query, Shape> { type Item = &'shape Shape; @@ -155,15 +167,20 @@ impl<'shape, T: BHValue, const D: usize, Shape: Bounded> Iterator /// Finally, if the root is an interior node, that is the normal case. We set `has_node` to true so /// the iterator can visit the root node and decide what to do next based on the root node's child /// AABB's. -pub(crate) fn iter_initially_has_node>( +pub(crate) fn iter_initially_has_node< + T: BHValue, + const D: usize, + Query: IntersectsAabb, + Shape: Bounded, +>( bvh: &Bvh, - ray: &Ray, + query: &Query, shapes: &[Shape], ) -> bool { match bvh.nodes.first() { // Only process the root leaf node if the shape's AABB is intersected. Some(BvhNode::Leaf { shape_index, .. }) => { - ray.intersects_aabb(&shapes[*shape_index].aabb()) + query.intersects_aabb(&shapes[*shape_index].aabb()) } Some(_) => true, None => false, diff --git a/src/flat_bvh.rs b/src/flat_bvh.rs index 718383d..892979d 100644 --- a/src/flat_bvh.rs +++ b/src/flat_bvh.rs @@ -1,8 +1,7 @@ //! This module exports methods to flatten the [`Bvh`] into a [`FlatBvh`] and traverse it iteratively. -use crate::aabb::{Aabb, Bounded}; +use crate::aabb::{Aabb, Bounded, IntersectsAabb}; use crate::bounding_hierarchy::{BHShape, BHValue, BoundingHierarchy}; use crate::bvh::{Bvh, BvhNode}; -use crate::ray::Ray; use num::Float; @@ -382,7 +381,11 @@ impl BoundingHierarchy for /// let flat_bvh = FlatBvh::build(&mut shapes); /// let hit_shapes = flat_bvh.traverse(&ray, &shapes); /// ``` - fn traverse<'a, B: Bounded>(&'a self, ray: &Ray, shapes: &'a [B]) -> Vec<&'a B> { + fn traverse<'a, Q: IntersectsAabb, B: Bounded>( + &'a self, + query: &Q, + shapes: &'a [B], + ) -> Vec<&'a B> { let mut hit_shapes = Vec::new(); let mut index = 0; @@ -396,13 +399,13 @@ impl BoundingHierarchy for if node.entry_index == u32::MAX { // If the entry_index is MAX_UINT32, then it's a leaf node. let shape = &shapes[node.shape_index as usize]; - if ray.intersects_aabb(&shape.aabb()) { + if query.intersects_aabb(&shape.aabb()) { hit_shapes.push(shape); } // Exit the current node. index = node.exit_index as usize; - } else if ray.intersects_aabb(&node.aabb) { + } else if query.intersects_aabb(&node.aabb) { // If entry_index is not MAX_UINT32 and the Aabb test passes, then // proceed to the node in entry_index (which goes down the bvh branch). index = node.entry_index as usize; diff --git a/src/lib.rs b/src/lib.rs index c397e1b..fb5e8da 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -84,6 +84,7 @@ extern crate test; pub mod aabb; +pub mod ball; pub mod bounding_hierarchy; pub mod bvh; pub mod flat_bvh; diff --git a/src/ray/ray_impl.rs b/src/ray/ray_impl.rs index eafef56..343d440 100644 --- a/src/ray/ray_impl.rs +++ b/src/ray/ray_impl.rs @@ -1,6 +1,7 @@ //! This module defines a Ray structure and intersection algorithms //! for axis aligned bounding boxes and triangles. +use crate::aabb::IntersectsAabb; use crate::utils::{fast_max, fast_min}; use crate::{aabb::Aabb, bounding_hierarchy::BHValue}; use nalgebra::{ @@ -353,6 +354,12 @@ mod tests { } } +impl IntersectsAabb for Ray { + fn intersects_aabb(&self, aabb: &Aabb) -> bool { + self.intersects_aabb(aabb) + } +} + #[cfg(all(feature = "bench", test))] mod bench { use rand::rngs::StdRng; diff --git a/src/testbase.rs b/src/testbase.rs index eaaf7bc..91fde0a 100644 --- a/src/testbase.rs +++ b/src/testbase.rs @@ -1,6 +1,7 @@ //! Common utilities shared by unit tests. -use crate::aabb::Bounded; +use crate::aabb::{Aabb, Bounded, IntersectsAabb}; +use crate::ball::Sphere; use crate::bounding_hierarchy::{BHShape, BoundingHierarchy}; use num::{FromPrimitive, Integer}; @@ -129,18 +130,20 @@ pub fn build_empty_bh>() -> (Vec, BH) { /// Given a ray, a bounding hierarchy, the complete list of shapes in the scene and a list of /// expected hits, verifies, whether the ray hits only the expected shapes. fn traverse_and_verify>( - ray_origin: TPoint3, - ray_direction: TVector3, + query: &impl IntersectsAabb, all_shapes: &[UnitBox], bh: &BH, expected_shapes: &HashSet, ) { - let ray = TRay3::new(ray_origin, ray_direction); - let hit_shapes = bh.traverse(&ray, all_shapes); + let hit_shapes = bh.traverse(query, all_shapes); assert_eq!(expected_shapes.len(), hit_shapes.len()); for shape in hit_shapes { - assert!(expected_shapes.contains(&shape.id)); + assert!( + expected_shapes.contains(&shape.id), + "unexpected shape {}", + shape.id + ); } } @@ -169,7 +172,12 @@ fn traverse_some_built_bh>(all_shapes: &[UnitBox], for id in -10..11 { expected_shapes.insert(id); } - traverse_and_verify(origin, direction, all_shapes, &bh, &expected_shapes); + traverse_and_verify( + &TRay3::new(origin, direction), + all_shapes, + &bh, + &expected_shapes, + ); } { @@ -180,7 +188,12 @@ fn traverse_some_built_bh>(all_shapes: &[UnitBox], // It should hit only one box. let mut expected_shapes = HashSet::new(); expected_shapes.insert(0); - traverse_and_verify(origin, direction, all_shapes, &bh, &expected_shapes); + traverse_and_verify( + &TRay3::new(origin, direction), + all_shapes, + &bh, + &expected_shapes, + ); } { @@ -193,7 +206,53 @@ fn traverse_some_built_bh>(all_shapes: &[UnitBox], expected_shapes.insert(4); expected_shapes.insert(5); expected_shapes.insert(6); - traverse_and_verify(origin, direction, all_shapes, &bh, &expected_shapes); + traverse_and_verify( + &TRay3::new(origin, direction), + all_shapes, + &bh, + &expected_shapes, + ); + } + + { + // Define a point at the origin. + let point = TPoint3::new(0.0, 0.0, 0.0); + + // It should be contained by the middle box. + let mut expected_shapes = HashSet::new(); + expected_shapes.insert(0); + traverse_and_verify(&point, all_shapes, &bh, &expected_shapes); + } + + { + // Define a point far away. + let point = TPoint3::new(0.0, 1000.0, 0.0); + + // It shouldn't be contained by any boxes. + let expected_shapes = HashSet::new(); + traverse_and_verify(&point, all_shapes, &bh, &expected_shapes); + } + + { + // Define an AABB intersecting with some boxes. + let aabb = Aabb::with_bounds(TPoint3::new(5.1, -1.0, -1.0), TPoint3::new(9.9, 1.0, 1.0)); + + let mut expected_shapes = HashSet::new(); + for x in 5..=10 { + expected_shapes.insert(x); + } + traverse_and_verify(&aabb, all_shapes, &bh, &expected_shapes); + } + + { + // Define a sphere intersecting with some boxes. + let sphere = Sphere::new(TPoint3::new(5.0, -1.0, -1.0), 1.4); + + let mut expected_shapes = HashSet::new(); + for x in 4..=6 { + expected_shapes.insert(x); + } + traverse_and_verify(&sphere, all_shapes, &bh, &expected_shapes); } }