Implement bounding volume types (#10946)

# Objective

Implement bounding volume trait and the 4 types from
https://github.com/bevyengine/bevy/issues/10570. I will add intersection
tests in a future PR.

## Solution

Implement mostly everything as written in the issue, except:
- Intersection is no longer a method on the bounding volumes, but a
separate trait.
- I implemented a `visible_area` since it's the most common usecase to
care about the surface that could collide with cast rays.
  - Maybe we want both?

---

## Changelog

- Added bounding volume types to bevy_math

---------

Co-authored-by: Alice Cecile <alice.i.cecile@gmail.com>
This commit is contained in:
NiseVoid 2024-01-11 00:18:51 +01:00 committed by GitHub
parent d4ffd4ff28
commit c4e479a2d4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 775 additions and 0 deletions

View file

@ -0,0 +1,353 @@
use super::BoundingVolume;
use crate::prelude::Vec2;
/// A trait with methods that return 2D bounded volumes for a shape
pub trait Bounded2d {
/// Get an axis-aligned bounding box for the shape with the given translation and rotation.
/// The rotation is in radians, counterclockwise, with 0 meaning no rotation.
fn aabb_2d(&self, translation: Vec2, rotation: f32) -> Aabb2d;
/// Get a bounding circle for the shape
/// The rotation is in radians, counterclockwise, with 0 meaning no rotation.
fn bounding_circle(&self, translation: Vec2, rotation: f32) -> BoundingCircle;
}
/// A 2D axis-aligned bounding box, or bounding rectangle
#[doc(alias = "BoundingRectangle")]
#[derive(Clone, Debug)]
pub struct Aabb2d {
/// The minimum, conventionally bottom-left, point of the box
pub min: Vec2,
/// The maximum, conventionally top-right, point of the box
pub max: Vec2,
}
impl BoundingVolume for Aabb2d {
type Position = Vec2;
type HalfSize = Vec2;
#[inline(always)]
fn center(&self) -> Self::Position {
(self.min + self.max) / 2.
}
#[inline(always)]
fn half_size(&self) -> Self::HalfSize {
(self.max - self.min) / 2.
}
#[inline(always)]
fn visible_area(&self) -> f32 {
let b = self.max - self.min;
b.x * b.y
}
#[inline(always)]
fn contains(&self, other: &Self) -> bool {
other.min.x >= self.min.x
&& other.min.y >= self.min.y
&& other.max.x <= self.max.x
&& other.max.y <= self.max.y
}
#[inline(always)]
fn merge(&self, other: &Self) -> Self {
Self {
min: self.min.min(other.min),
max: self.max.max(other.max),
}
}
#[inline(always)]
fn grow(&self, amount: Self::HalfSize) -> Self {
let b = Self {
min: self.min - amount,
max: self.max + amount,
};
debug_assert!(b.min.x <= b.max.x && b.min.y <= b.max.y);
b
}
#[inline(always)]
fn shrink(&self, amount: Self::HalfSize) -> Self {
let b = Self {
min: self.min + amount,
max: self.max - amount,
};
debug_assert!(b.min.x <= b.max.x && b.min.y <= b.max.y);
b
}
}
#[cfg(test)]
mod aabb2d_tests {
use super::Aabb2d;
use crate::{bounding::BoundingVolume, Vec2};
#[test]
fn center() {
let aabb = Aabb2d {
min: Vec2::new(-0.5, -1.),
max: Vec2::new(1., 1.),
};
assert!((aabb.center() - Vec2::new(0.25, 0.)).length() < std::f32::EPSILON);
let aabb = Aabb2d {
min: Vec2::new(5., -10.),
max: Vec2::new(10., -5.),
};
assert!((aabb.center() - Vec2::new(7.5, -7.5)).length() < std::f32::EPSILON);
}
#[test]
fn half_size() {
let aabb = Aabb2d {
min: Vec2::new(-0.5, -1.),
max: Vec2::new(1., 1.),
};
let half_size = aabb.half_size();
assert!((half_size - Vec2::new(0.75, 1.)).length() < std::f32::EPSILON);
}
#[test]
fn area() {
let aabb = Aabb2d {
min: Vec2::new(-1., -1.),
max: Vec2::new(1., 1.),
};
assert!((aabb.visible_area() - 4.).abs() < std::f32::EPSILON);
let aabb = Aabb2d {
min: Vec2::new(0., 0.),
max: Vec2::new(1., 0.5),
};
assert!((aabb.visible_area() - 0.5).abs() < std::f32::EPSILON);
}
#[test]
fn contains() {
let a = Aabb2d {
min: Vec2::new(-1., -1.),
max: Vec2::new(1., 1.),
};
let b = Aabb2d {
min: Vec2::new(-2., -1.),
max: Vec2::new(1., 1.),
};
assert!(!a.contains(&b));
let b = Aabb2d {
min: Vec2::new(-0.25, -0.8),
max: Vec2::new(1., 1.),
};
assert!(a.contains(&b));
}
#[test]
fn merge() {
let a = Aabb2d {
min: Vec2::new(-1., -1.),
max: Vec2::new(1., 0.5),
};
let b = Aabb2d {
min: Vec2::new(-2., -0.5),
max: Vec2::new(0.75, 1.),
};
let merged = a.merge(&b);
assert!((merged.min - Vec2::new(-2., -1.)).length() < std::f32::EPSILON);
assert!((merged.max - Vec2::new(1., 1.)).length() < std::f32::EPSILON);
assert!(merged.contains(&a));
assert!(merged.contains(&b));
assert!(!a.contains(&merged));
assert!(!b.contains(&merged));
}
#[test]
fn grow() {
let a = Aabb2d {
min: Vec2::new(-1., -1.),
max: Vec2::new(1., 1.),
};
let padded = a.grow(Vec2::ONE);
assert!((padded.min - Vec2::new(-2., -2.)).length() < std::f32::EPSILON);
assert!((padded.max - Vec2::new(2., 2.)).length() < std::f32::EPSILON);
assert!(padded.contains(&a));
assert!(!a.contains(&padded));
}
#[test]
fn shrink() {
let a = Aabb2d {
min: Vec2::new(-2., -2.),
max: Vec2::new(2., 2.),
};
let shrunk = a.shrink(Vec2::ONE);
assert!((shrunk.min - Vec2::new(-1., -1.)).length() < std::f32::EPSILON);
assert!((shrunk.max - Vec2::new(1., 1.)).length() < std::f32::EPSILON);
assert!(a.contains(&shrunk));
assert!(!shrunk.contains(&a));
}
}
use crate::primitives::Circle;
/// A bounding circle
#[derive(Clone, Debug)]
pub struct BoundingCircle {
/// The center of the bounding circle
pub center: Vec2,
/// The circle
pub circle: Circle,
}
impl BoundingCircle {
/// Construct a bounding circle from its center and radius
#[inline(always)]
pub fn new(center: Vec2, radius: f32) -> Self {
debug_assert!(radius >= 0.);
Self {
center,
circle: Circle { radius },
}
}
/// Get the radius of the bounding circle
#[inline(always)]
pub fn radius(&self) -> f32 {
self.circle.radius
}
}
impl BoundingVolume for BoundingCircle {
type Position = Vec2;
type HalfSize = f32;
#[inline(always)]
fn center(&self) -> Self::Position {
self.center
}
#[inline(always)]
fn half_size(&self) -> Self::HalfSize {
self.radius()
}
#[inline(always)]
fn visible_area(&self) -> f32 {
std::f32::consts::PI * self.radius() * self.radius()
}
#[inline(always)]
fn contains(&self, other: &Self) -> bool {
let diff = self.radius() - other.radius();
self.center.distance_squared(other.center) <= diff.powi(2).copysign(diff)
}
#[inline(always)]
fn merge(&self, other: &Self) -> Self {
let diff = other.center - self.center;
let length = diff.length();
if self.radius() >= length + other.radius() {
return self.clone();
}
if other.radius() >= length + self.radius() {
return other.clone();
}
let dir = diff / length;
Self::new(
(self.center + other.center) / 2. + dir * ((other.radius() - self.radius()) / 2.),
(length + self.radius() + other.radius()) / 2.,
)
}
#[inline(always)]
fn grow(&self, amount: Self::HalfSize) -> Self {
debug_assert!(amount >= 0.);
Self::new(self.center, self.radius() + amount)
}
#[inline(always)]
fn shrink(&self, amount: Self::HalfSize) -> Self {
debug_assert!(amount >= 0.);
debug_assert!(self.radius() >= amount);
Self::new(self.center, self.radius() - amount)
}
}
#[cfg(test)]
mod bounding_circle_tests {
use super::BoundingCircle;
use crate::{bounding::BoundingVolume, Vec2};
#[test]
fn area() {
let circle = BoundingCircle::new(Vec2::ONE, 5.);
// Since this number is messy we check it with a higher threshold
assert!((circle.visible_area() - 78.5398).abs() < 0.001);
}
#[test]
fn contains() {
let a = BoundingCircle::new(Vec2::ONE, 5.);
let b = BoundingCircle::new(Vec2::new(5.5, 1.), 1.);
assert!(!a.contains(&b));
let b = BoundingCircle::new(Vec2::new(1., -3.5), 0.5);
assert!(a.contains(&b));
}
#[test]
fn contains_identical() {
let a = BoundingCircle::new(Vec2::ONE, 5.);
assert!(a.contains(&a));
}
#[test]
fn merge() {
// When merging two circles that don't contain each other, we find a center position that
// contains both
let a = BoundingCircle::new(Vec2::ONE, 5.);
let b = BoundingCircle::new(Vec2::new(1., -4.), 1.);
let merged = a.merge(&b);
assert!((merged.center - Vec2::new(1., 0.5)).length() < std::f32::EPSILON);
assert!((merged.radius() - 5.5).abs() < std::f32::EPSILON);
assert!(merged.contains(&a));
assert!(merged.contains(&b));
assert!(!a.contains(&merged));
assert!(!b.contains(&merged));
// When one circle contains the other circle, we use the bigger circle
let b = BoundingCircle::new(Vec2::ZERO, 3.);
assert!(a.contains(&b));
let merged = a.merge(&b);
assert_eq!(merged.center, a.center);
assert_eq!(merged.radius(), a.radius());
// When two circles are at the same point, we use the bigger radius
let b = BoundingCircle::new(Vec2::ONE, 6.);
let merged = a.merge(&b);
assert_eq!(merged.center, a.center);
assert_eq!(merged.radius(), b.radius());
}
#[test]
fn merge_identical() {
let a = BoundingCircle::new(Vec2::ONE, 5.);
let merged = a.merge(&a);
assert_eq!(merged.center, a.center);
assert_eq!(merged.radius(), a.radius());
}
#[test]
fn grow() {
let a = BoundingCircle::new(Vec2::ONE, 5.);
let padded = a.grow(1.25);
assert!((padded.radius() - 6.25).abs() < std::f32::EPSILON);
assert!(padded.contains(&a));
assert!(!a.contains(&padded));
}
#[test]
fn shrink() {
let a = BoundingCircle::new(Vec2::ONE, 5.);
let shrunk = a.shrink(0.5);
assert!((shrunk.radius() - 4.5).abs() < std::f32::EPSILON);
assert!(a.contains(&shrunk));
assert!(!shrunk.contains(&a));
}
}

View file

@ -0,0 +1,360 @@
use super::BoundingVolume;
use crate::prelude::{Quat, Vec3};
/// A trait with methods that return 3D bounded volumes for a shape
pub trait Bounded3d {
/// Get an axis-aligned bounding box for the shape with the given translation and rotation
fn aabb_3d(&self, translation: Vec3, rotation: Quat) -> Aabb3d;
/// Get a bounding sphere for the shape with the given translation and rotation
fn bounding_sphere(&self, translation: Vec3, rotation: Quat) -> BoundingSphere;
}
/// A 3D axis-aligned bounding box
#[derive(Clone, Debug)]
pub struct Aabb3d {
/// The minimum point of the box
pub min: Vec3,
/// The maximum point of the box
pub max: Vec3,
}
impl BoundingVolume for Aabb3d {
type Position = Vec3;
type HalfSize = Vec3;
#[inline(always)]
fn center(&self) -> Self::Position {
(self.min + self.max) / 2.
}
#[inline(always)]
fn half_size(&self) -> Self::HalfSize {
(self.max - self.min) / 2.
}
#[inline(always)]
fn visible_area(&self) -> f32 {
let b = self.max - self.min;
b.x * (b.y + b.z) + b.y * b.z
}
#[inline(always)]
fn contains(&self, other: &Self) -> bool {
other.min.x >= self.min.x
&& other.min.y >= self.min.y
&& other.min.z >= self.min.z
&& other.max.x <= self.max.x
&& other.max.y <= self.max.y
&& other.max.z <= self.max.z
}
#[inline(always)]
fn merge(&self, other: &Self) -> Self {
Self {
min: self.min.min(other.min),
max: self.max.max(other.max),
}
}
#[inline(always)]
fn grow(&self, amount: Self::HalfSize) -> Self {
let b = Self {
min: self.min - amount,
max: self.max + amount,
};
debug_assert!(b.min.x <= b.max.x && b.min.y <= b.max.y && b.min.z <= b.max.z);
b
}
#[inline(always)]
fn shrink(&self, amount: Self::HalfSize) -> Self {
let b = Self {
min: self.min + amount,
max: self.max - amount,
};
debug_assert!(b.min.x <= b.max.x && b.min.y <= b.max.y && b.min.z <= b.max.z);
b
}
}
#[cfg(test)]
mod aabb3d_tests {
use super::Aabb3d;
use crate::{bounding::BoundingVolume, Vec3};
#[test]
fn center() {
let aabb = Aabb3d {
min: Vec3::new(-0.5, -1., -0.5),
max: Vec3::new(1., 1., 2.),
};
assert!((aabb.center() - Vec3::new(0.25, 0., 0.75)).length() < std::f32::EPSILON);
let aabb = Aabb3d {
min: Vec3::new(5., 5., -10.),
max: Vec3::new(10., 10., -5.),
};
assert!((aabb.center() - Vec3::new(7.5, 7.5, -7.5)).length() < std::f32::EPSILON);
}
#[test]
fn half_size() {
let aabb = Aabb3d {
min: Vec3::new(-0.5, -1., -0.5),
max: Vec3::new(1., 1., 2.),
};
assert!((aabb.half_size() - Vec3::new(0.75, 1., 1.25)).length() < std::f32::EPSILON);
}
#[test]
fn area() {
let aabb = Aabb3d {
min: Vec3::new(-1., -1., -1.),
max: Vec3::new(1., 1., 1.),
};
assert!((aabb.visible_area() - 12.).abs() < std::f32::EPSILON);
let aabb = Aabb3d {
min: Vec3::new(0., 0., 0.),
max: Vec3::new(1., 0.5, 0.25),
};
assert!((aabb.visible_area() - 0.875).abs() < std::f32::EPSILON);
}
#[test]
fn contains() {
let a = Aabb3d {
min: Vec3::new(-1., -1., -1.),
max: Vec3::new(1., 1., 1.),
};
let b = Aabb3d {
min: Vec3::new(-2., -1., -1.),
max: Vec3::new(1., 1., 1.),
};
assert!(!a.contains(&b));
let b = Aabb3d {
min: Vec3::new(-0.25, -0.8, -0.9),
max: Vec3::new(1., 1., 0.9),
};
assert!(a.contains(&b));
}
#[test]
fn merge() {
let a = Aabb3d {
min: Vec3::new(-1., -1., -1.),
max: Vec3::new(1., 0.5, 1.),
};
let b = Aabb3d {
min: Vec3::new(-2., -0.5, -0.),
max: Vec3::new(0.75, 1., 2.),
};
let merged = a.merge(&b);
assert!((merged.min - Vec3::new(-2., -1., -1.)).length() < std::f32::EPSILON);
assert!((merged.max - Vec3::new(1., 1., 2.)).length() < std::f32::EPSILON);
assert!(merged.contains(&a));
assert!(merged.contains(&b));
assert!(!a.contains(&merged));
assert!(!b.contains(&merged));
}
#[test]
fn grow() {
let a = Aabb3d {
min: Vec3::new(-1., -1., -1.),
max: Vec3::new(1., 1., 1.),
};
let padded = a.grow(Vec3::ONE);
assert!((padded.min - Vec3::new(-2., -2., -2.)).length() < std::f32::EPSILON);
assert!((padded.max - Vec3::new(2., 2., 2.)).length() < std::f32::EPSILON);
assert!(padded.contains(&a));
assert!(!a.contains(&padded));
}
#[test]
fn shrink() {
let a = Aabb3d {
min: Vec3::new(-2., -2., -2.),
max: Vec3::new(2., 2., 2.),
};
let shrunk = a.shrink(Vec3::ONE);
assert!((shrunk.min - Vec3::new(-1., -1., -1.)).length() < std::f32::EPSILON);
assert!((shrunk.max - Vec3::new(1., 1., 1.)).length() < std::f32::EPSILON);
assert!(a.contains(&shrunk));
assert!(!shrunk.contains(&a));
}
}
use crate::primitives::Sphere;
/// A bounding sphere
#[derive(Clone, Debug)]
pub struct BoundingSphere {
/// The center of the bounding sphere
pub center: Vec3,
/// The sphere
pub sphere: Sphere,
}
impl BoundingSphere {
/// Construct a bounding sphere from its center and radius.
pub fn new(center: Vec3, radius: f32) -> Self {
debug_assert!(radius >= 0.);
Self {
center,
sphere: Sphere { radius },
}
}
/// Get the radius of the bounding sphere
#[inline(always)]
pub fn radius(&self) -> f32 {
self.sphere.radius
}
}
impl BoundingVolume for BoundingSphere {
type Position = Vec3;
type HalfSize = f32;
#[inline(always)]
fn center(&self) -> Self::Position {
self.center
}
#[inline(always)]
fn half_size(&self) -> Self::HalfSize {
self.radius()
}
#[inline(always)]
fn visible_area(&self) -> f32 {
2. * std::f32::consts::PI * self.radius() * self.radius()
}
#[inline(always)]
fn contains(&self, other: &Self) -> bool {
let diff = self.radius() - other.radius();
self.center.distance_squared(other.center) <= diff.powi(2).copysign(diff)
}
#[inline(always)]
fn merge(&self, other: &Self) -> Self {
let diff = other.center - self.center;
let length = diff.length();
if self.radius() >= length + other.radius() {
return self.clone();
}
if other.radius() >= length + self.radius() {
return other.clone();
}
let dir = diff / length;
Self::new(
(self.center + other.center) / 2. + dir * ((other.radius() - self.radius()) / 2.),
(length + self.radius() + other.radius()) / 2.,
)
}
#[inline(always)]
fn grow(&self, amount: Self::HalfSize) -> Self {
debug_assert!(amount >= 0.);
Self {
center: self.center,
sphere: Sphere {
radius: self.radius() + amount,
},
}
}
#[inline(always)]
fn shrink(&self, amount: Self::HalfSize) -> Self {
debug_assert!(amount >= 0.);
debug_assert!(self.radius() >= amount);
Self {
center: self.center,
sphere: Sphere {
radius: self.radius() - amount,
},
}
}
}
#[cfg(test)]
mod bounding_sphere_tests {
use super::BoundingSphere;
use crate::{bounding::BoundingVolume, Vec3};
#[test]
fn area() {
let sphere = BoundingSphere::new(Vec3::ONE, 5.);
// Since this number is messy we check it with a higher threshold
assert!((sphere.visible_area() - 157.0796).abs() < 0.001);
}
#[test]
fn contains() {
let a = BoundingSphere::new(Vec3::ONE, 5.);
let b = BoundingSphere::new(Vec3::new(5.5, 1., 1.), 1.);
assert!(!a.contains(&b));
let b = BoundingSphere::new(Vec3::new(1., -3.5, 1.), 0.5);
assert!(a.contains(&b));
}
#[test]
fn contains_identical() {
let a = BoundingSphere::new(Vec3::ONE, 5.);
assert!(a.contains(&a));
}
#[test]
fn merge() {
// When merging two circles that don't contain each other, we find a center position that
// contains both
let a = BoundingSphere::new(Vec3::ONE, 5.);
let b = BoundingSphere::new(Vec3::new(1., 1., -4.), 1.);
let merged = a.merge(&b);
assert!((merged.center - Vec3::new(1., 1., 0.5)).length() < std::f32::EPSILON);
assert!((merged.radius() - 5.5).abs() < std::f32::EPSILON);
assert!(merged.contains(&a));
assert!(merged.contains(&b));
assert!(!a.contains(&merged));
assert!(!b.contains(&merged));
// When one circle contains the other circle, we use the bigger circle
let b = BoundingSphere::new(Vec3::ZERO, 3.);
assert!(a.contains(&b));
let merged = a.merge(&b);
assert_eq!(merged.center, a.center);
assert_eq!(merged.radius(), a.radius());
// When two circles are at the same point, we use the bigger radius
let b = BoundingSphere::new(Vec3::ONE, 6.);
let merged = a.merge(&b);
assert_eq!(merged.center, a.center);
assert_eq!(merged.radius(), b.radius());
}
#[test]
fn merge_identical() {
let a = BoundingSphere::new(Vec3::ONE, 5.);
let merged = a.merge(&a);
assert_eq!(merged.center, a.center);
assert_eq!(merged.radius(), a.radius());
}
#[test]
fn grow() {
let a = BoundingSphere::new(Vec3::ONE, 5.);
let padded = a.grow(1.25);
assert!((padded.radius() - 6.25).abs() < std::f32::EPSILON);
assert!(padded.contains(&a));
assert!(!a.contains(&padded));
}
#[test]
fn shrink() {
let a = BoundingSphere::new(Vec3::ONE, 5.);
let shrunk = a.shrink(0.5);
assert!((shrunk.radius() - 4.5).abs() < std::f32::EPSILON);
assert!(a.contains(&shrunk));
assert!(!shrunk.contains(&a));
}
}

View file

@ -0,0 +1,61 @@
//! This module contains traits and implements for working with bounding shapes
//!
//! There are four traits used:
//! - [`BoundingVolume`] is a generic abstraction for any bounding volume
//! - [`IntersectsVolume`] abstracts intersection tests against a [`BoundingVolume`]
//! - [`Bounded2d`]/[`Bounded3d`] are abstractions for shapes to generate [`BoundingVolume`]s
/// A trait that generalizes different bounding volumes.
/// Bounding volumes are simplified shapes that are used to get simpler ways to check for
/// overlapping elements or finding intersections.
///
/// This trait supports both 2D and 3D bounding shapes.
pub trait BoundingVolume {
/// The position type used for the volume. This should be `Vec2` for 2D and `Vec3` for 3D.
type Position: Clone + Copy + PartialEq;
/// The type used for the size of the bounding volume. Usually a half size. For example an
/// `f32` radius for a circle, or a `Vec3` with half sizes for x, y and z for a 3D axis-aligned
/// bounding box
type HalfSize;
/// Returns the center of the bounding volume.
fn center(&self) -> Self::Position;
/// Returns the half size of the bounding volume.
fn half_size(&self) -> Self::HalfSize;
/// Computes the visible surface area of the bounding volume.
/// This method can be useful to make decisions about merging bounding volumes,
/// using a Surface Area Heuristic.
///
/// For 2D shapes this would simply be the area of the shape.
/// For 3D shapes this would usually be half the area of the shape.
fn visible_area(&self) -> f32;
/// Checks if this bounding volume contains another one.
fn contains(&self, other: &Self) -> bool;
/// Computes the smallest bounding volume that contains both `self` and `other`.
fn merge(&self, other: &Self) -> Self;
/// Increase the size of the bounding volume in each direction by the given amount
fn grow(&self, amount: Self::HalfSize) -> Self;
/// Decrease the size of the bounding volume in each direction by the given amount
fn shrink(&self, amount: Self::HalfSize) -> Self;
}
/// A trait that generalizes intersection tests against a volume.
/// Intersection tests can be used for a variety of tasks, for example:
/// - Raycasting
/// - Testing for overlap
/// - Checking if an object is within the view frustum of a camera
pub trait IntersectsVolume<Volume: BoundingVolume> {
/// Check if a volume intersects with this intersection test
fn intersects(&self, volume: &Volume) -> bool;
}
mod bounded2d;
pub use bounded2d::*;
mod bounded3d;
pub use bounded3d::*;

View file

@ -8,6 +8,7 @@
mod affine3;
mod aspect_ratio;
pub mod bounding;
pub mod cubic_splines;
pub mod primitives;
mod ray;