Vendor dependencies for 0.3.0 release

This commit is contained in:
2025-09-27 10:29:08 -05:00
parent 0c8d39d483
commit 82ab7f317b
26803 changed files with 16134934 additions and 0 deletions

View File

@@ -0,0 +1,770 @@
mod primitive_impls;
use super::{BoundingVolume, IntersectsVolume};
use crate::{
ops,
prelude::{Mat2, Rot2, Vec2},
FloatPow, Isometry2d,
};
#[cfg(feature = "bevy_reflect")]
use bevy_reflect::Reflect;
#[cfg(all(feature = "bevy_reflect", feature = "serialize"))]
use bevy_reflect::{ReflectDeserialize, ReflectSerialize};
#[cfg(feature = "serialize")]
use serde::{Deserialize, Serialize};
/// Computes the geometric center of the given set of points.
#[inline(always)]
fn point_cloud_2d_center(points: &[Vec2]) -> Vec2 {
assert!(
!points.is_empty(),
"cannot compute the center of an empty set of points"
);
let denom = 1.0 / points.len() as f32;
points.iter().fold(Vec2::ZERO, |acc, point| acc + *point) * denom
}
/// A trait with methods that return 2D bounding volumes for a shape.
pub trait Bounded2d {
/// Get an axis-aligned bounding box for the shape translated and rotated by the given isometry.
fn aabb_2d(&self, isometry: impl Into<Isometry2d>) -> Aabb2d;
/// Get a bounding circle for the shape translated and rotated by the given isometry.
fn bounding_circle(&self, isometry: impl Into<Isometry2d>) -> BoundingCircle;
}
/// A 2D axis-aligned bounding box, or bounding rectangle
#[doc(alias = "BoundingRectangle")]
#[derive(Clone, Copy, Debug, PartialEq)]
#[cfg_attr(
feature = "bevy_reflect",
derive(Reflect),
reflect(Debug, PartialEq, Clone)
)]
#[cfg_attr(feature = "serialize", derive(Serialize), derive(Deserialize))]
#[cfg_attr(
all(feature = "serialize", feature = "bevy_reflect"),
reflect(Serialize, Deserialize)
)]
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 Aabb2d {
/// Constructs an AABB from its center and half-size.
#[inline(always)]
pub fn new(center: Vec2, half_size: Vec2) -> Self {
debug_assert!(half_size.x >= 0.0 && half_size.y >= 0.0);
Self {
min: center - half_size,
max: center + half_size,
}
}
/// Computes the smallest [`Aabb2d`] containing the given set of points,
/// transformed by the rotation and translation of the given isometry.
///
/// # Panics
///
/// Panics if the given set of points is empty.
#[inline(always)]
pub fn from_point_cloud(isometry: impl Into<Isometry2d>, points: &[Vec2]) -> Aabb2d {
let isometry = isometry.into();
// Transform all points by rotation
let mut iter = points.iter().map(|point| isometry.rotation * *point);
let first = iter
.next()
.expect("point cloud must contain at least one point for Aabb2d construction");
let (min, max) = iter.fold((first, first), |(prev_min, prev_max), point| {
(point.min(prev_min), point.max(prev_max))
});
Aabb2d {
min: min + isometry.translation,
max: max + isometry.translation,
}
}
/// Computes the smallest [`BoundingCircle`] containing this [`Aabb2d`].
#[inline(always)]
pub fn bounding_circle(&self) -> BoundingCircle {
let radius = self.min.distance(self.max) / 2.0;
BoundingCircle::new(self.center(), radius)
}
/// Finds the point on the AABB that is closest to the given `point`.
///
/// If the point is outside the AABB, the returned point will be on the perimeter of the AABB.
/// Otherwise, it will be inside the AABB and returned as is.
#[inline(always)]
pub fn closest_point(&self, point: Vec2) -> Vec2 {
// Clamp point coordinates to the AABB
point.clamp(self.min, self.max)
}
}
impl BoundingVolume for Aabb2d {
type Translation = Vec2;
type Rotation = Rot2;
type HalfSize = Vec2;
#[inline(always)]
fn center(&self) -> Self::Translation {
(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: impl Into<Self::HalfSize>) -> Self {
let amount = amount.into();
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: impl Into<Self::HalfSize>) -> Self {
let amount = amount.into();
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 scale_around_center(&self, scale: impl Into<Self::HalfSize>) -> Self {
let scale = scale.into();
let b = Self {
min: self.center() - (self.half_size() * scale),
max: self.center() + (self.half_size() * scale),
};
debug_assert!(b.min.x <= b.max.x && b.min.y <= b.max.y);
b
}
/// Transforms the bounding volume by first rotating it around the origin and then applying a translation.
///
/// The result is an Axis-Aligned Bounding Box that encompasses the rotated shape.
///
/// Note that the result may not be as tightly fitting as the original, and repeated rotations
/// can cause the AABB to grow indefinitely. Avoid applying multiple rotations to the same AABB,
/// and consider storing the original AABB and rotating that every time instead.
#[inline(always)]
fn transformed_by(
mut self,
translation: impl Into<Self::Translation>,
rotation: impl Into<Self::Rotation>,
) -> Self {
self.transform_by(translation, rotation);
self
}
/// Transforms the bounding volume by first rotating it around the origin and then applying a translation.
///
/// The result is an Axis-Aligned Bounding Box that encompasses the rotated shape.
///
/// Note that the result may not be as tightly fitting as the original, and repeated rotations
/// can cause the AABB to grow indefinitely. Avoid applying multiple rotations to the same AABB,
/// and consider storing the original AABB and rotating that every time instead.
#[inline(always)]
fn transform_by(
&mut self,
translation: impl Into<Self::Translation>,
rotation: impl Into<Self::Rotation>,
) {
self.rotate_by(rotation);
self.translate_by(translation);
}
#[inline(always)]
fn translate_by(&mut self, translation: impl Into<Self::Translation>) {
let translation = translation.into();
self.min += translation;
self.max += translation;
}
/// Rotates the bounding volume around the origin by the given rotation.
///
/// The result is an Axis-Aligned Bounding Box that encompasses the rotated shape.
///
/// Note that the result may not be as tightly fitting as the original, and repeated rotations
/// can cause the AABB to grow indefinitely. Avoid applying multiple rotations to the same AABB,
/// and consider storing the original AABB and rotating that every time instead.
#[inline(always)]
fn rotated_by(mut self, rotation: impl Into<Self::Rotation>) -> Self {
self.rotate_by(rotation);
self
}
/// Rotates the bounding volume around the origin by the given rotation.
///
/// The result is an Axis-Aligned Bounding Box that encompasses the rotated shape.
///
/// Note that the result may not be as tightly fitting as the original, and repeated rotations
/// can cause the AABB to grow indefinitely. Avoid applying multiple rotations to the same AABB,
/// and consider storing the original AABB and rotating that every time instead.
#[inline(always)]
fn rotate_by(&mut self, rotation: impl Into<Self::Rotation>) {
let rot_mat = Mat2::from(rotation.into());
let half_size = rot_mat.abs() * self.half_size();
*self = Self::new(rot_mat * self.center(), half_size);
}
}
impl IntersectsVolume<Self> for Aabb2d {
#[inline(always)]
fn intersects(&self, other: &Self) -> bool {
let x_overlaps = self.min.x <= other.max.x && self.max.x >= other.min.x;
let y_overlaps = self.min.y <= other.max.y && self.max.y >= other.min.y;
x_overlaps && y_overlaps
}
}
impl IntersectsVolume<BoundingCircle> for Aabb2d {
#[inline(always)]
fn intersects(&self, circle: &BoundingCircle) -> bool {
let closest_point = self.closest_point(circle.center);
let distance_squared = circle.center.distance_squared(closest_point);
let radius_squared = circle.radius().squared();
distance_squared <= radius_squared
}
}
#[cfg(test)]
mod aabb2d_tests {
use approx::assert_relative_eq;
use super::Aabb2d;
use crate::{
bounding::{BoundingCircle, BoundingVolume, IntersectsVolume},
ops, 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() < 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() < 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() < f32::EPSILON);
}
#[test]
fn area() {
let aabb = Aabb2d {
min: Vec2::new(-1., -1.),
max: Vec2::new(1., 1.),
};
assert!(ops::abs(aabb.visible_area() - 4.) < f32::EPSILON);
let aabb = Aabb2d {
min: Vec2::new(0., 0.),
max: Vec2::new(1., 0.5),
};
assert!(ops::abs(aabb.visible_area() - 0.5) < 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() < f32::EPSILON);
assert!((merged.max - Vec2::new(1., 1.)).length() < 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() < f32::EPSILON);
assert!((padded.max - Vec2::new(2., 2.)).length() < 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() < f32::EPSILON);
assert!((shrunk.max - Vec2::new(1., 1.)).length() < f32::EPSILON);
assert!(a.contains(&shrunk));
assert!(!shrunk.contains(&a));
}
#[test]
fn scale_around_center() {
let a = Aabb2d {
min: Vec2::NEG_ONE,
max: Vec2::ONE,
};
let scaled = a.scale_around_center(Vec2::splat(2.));
assert!((scaled.min - Vec2::splat(-2.)).length() < f32::EPSILON);
assert!((scaled.max - Vec2::splat(2.)).length() < f32::EPSILON);
assert!(!a.contains(&scaled));
assert!(scaled.contains(&a));
}
#[test]
fn rotate() {
let a = Aabb2d {
min: Vec2::new(-2.0, -2.0),
max: Vec2::new(2.0, 2.0),
};
let rotated = a.rotated_by(core::f32::consts::PI);
assert_relative_eq!(rotated.min, a.min);
assert_relative_eq!(rotated.max, a.max);
}
#[test]
fn transform() {
let a = Aabb2d {
min: Vec2::new(-2.0, -2.0),
max: Vec2::new(2.0, 2.0),
};
let transformed = a.transformed_by(Vec2::new(2.0, -2.0), core::f32::consts::FRAC_PI_4);
let half_length = ops::hypot(2.0, 2.0);
assert_eq!(
transformed.min,
Vec2::new(2.0 - half_length, -half_length - 2.0)
);
assert_eq!(
transformed.max,
Vec2::new(2.0 + half_length, half_length - 2.0)
);
}
#[test]
fn closest_point() {
let aabb = Aabb2d {
min: Vec2::NEG_ONE,
max: Vec2::ONE,
};
assert_eq!(aabb.closest_point(Vec2::X * 10.0), Vec2::X);
assert_eq!(aabb.closest_point(Vec2::NEG_ONE * 10.0), Vec2::NEG_ONE);
assert_eq!(
aabb.closest_point(Vec2::new(0.25, 0.1)),
Vec2::new(0.25, 0.1)
);
}
#[test]
fn intersect_aabb() {
let aabb = Aabb2d {
min: Vec2::NEG_ONE,
max: Vec2::ONE,
};
assert!(aabb.intersects(&aabb));
assert!(aabb.intersects(&Aabb2d {
min: Vec2::new(0.5, 0.5),
max: Vec2::new(2.0, 2.0),
}));
assert!(aabb.intersects(&Aabb2d {
min: Vec2::new(-2.0, -2.0),
max: Vec2::new(-0.5, -0.5),
}));
assert!(!aabb.intersects(&Aabb2d {
min: Vec2::new(1.1, 0.0),
max: Vec2::new(2.0, 0.5),
}));
}
#[test]
fn intersect_bounding_circle() {
let aabb = Aabb2d {
min: Vec2::NEG_ONE,
max: Vec2::ONE,
};
assert!(aabb.intersects(&BoundingCircle::new(Vec2::ZERO, 1.0)));
assert!(aabb.intersects(&BoundingCircle::new(Vec2::ONE * 1.5, 1.0)));
assert!(aabb.intersects(&BoundingCircle::new(Vec2::NEG_ONE * 1.5, 1.0)));
assert!(!aabb.intersects(&BoundingCircle::new(Vec2::ONE * 1.75, 1.0)));
}
}
use crate::primitives::Circle;
/// A bounding circle
#[derive(Clone, Copy, Debug, PartialEq)]
#[cfg_attr(
feature = "bevy_reflect",
derive(Reflect),
reflect(Debug, PartialEq, Clone)
)]
#[cfg_attr(feature = "serialize", derive(Serialize), derive(Deserialize))]
#[cfg_attr(
all(feature = "serialize", feature = "bevy_reflect"),
reflect(Serialize, Deserialize)
)]
pub struct BoundingCircle {
/// The center of the bounding circle
pub center: Vec2,
/// The circle
pub circle: Circle,
}
impl BoundingCircle {
/// Constructs 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 },
}
}
/// Computes a [`BoundingCircle`] containing the given set of points,
/// transformed by the rotation and translation of the given isometry.
///
/// The bounding circle is not guaranteed to be the smallest possible.
#[inline(always)]
pub fn from_point_cloud(isometry: impl Into<Isometry2d>, points: &[Vec2]) -> BoundingCircle {
let isometry = isometry.into();
let center = point_cloud_2d_center(points);
let mut radius_squared = 0.0;
for point in points {
// Get squared version to avoid unnecessary sqrt calls
let distance_squared = point.distance_squared(center);
if distance_squared > radius_squared {
radius_squared = distance_squared;
}
}
BoundingCircle::new(isometry * center, ops::sqrt(radius_squared))
}
/// Get the radius of the bounding circle
#[inline(always)]
pub fn radius(&self) -> f32 {
self.circle.radius
}
/// Computes the smallest [`Aabb2d`] containing this [`BoundingCircle`].
#[inline(always)]
pub fn aabb_2d(&self) -> Aabb2d {
Aabb2d {
min: self.center - Vec2::splat(self.radius()),
max: self.center + Vec2::splat(self.radius()),
}
}
/// Finds the point on the bounding circle that is closest to the given `point`.
///
/// If the point is outside the circle, the returned point will be on the perimeter of the circle.
/// Otherwise, it will be inside the circle and returned as is.
#[inline(always)]
pub fn closest_point(&self, point: Vec2) -> Vec2 {
self.circle.closest_point(point - self.center) + self.center
}
}
impl BoundingVolume for BoundingCircle {
type Translation = Vec2;
type Rotation = Rot2;
type HalfSize = f32;
#[inline(always)]
fn center(&self) -> Self::Translation {
self.center
}
#[inline(always)]
fn half_size(&self) -> Self::HalfSize {
self.radius()
}
#[inline(always)]
fn visible_area(&self) -> f32 {
core::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) <= ops::copysign(diff.squared(), 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;
}
if other.radius() >= length + self.radius() {
return *other;
}
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: impl Into<Self::HalfSize>) -> Self {
let amount = amount.into();
debug_assert!(amount >= 0.);
Self::new(self.center, self.radius() + amount)
}
#[inline(always)]
fn shrink(&self, amount: impl Into<Self::HalfSize>) -> Self {
let amount = amount.into();
debug_assert!(amount >= 0.);
debug_assert!(self.radius() >= amount);
Self::new(self.center, self.radius() - amount)
}
#[inline(always)]
fn scale_around_center(&self, scale: impl Into<Self::HalfSize>) -> Self {
let scale = scale.into();
debug_assert!(scale >= 0.);
Self::new(self.center, self.radius() * scale)
}
#[inline(always)]
fn translate_by(&mut self, translation: impl Into<Self::Translation>) {
self.center += translation.into();
}
#[inline(always)]
fn rotate_by(&mut self, rotation: impl Into<Self::Rotation>) {
let rotation: Rot2 = rotation.into();
self.center = rotation * self.center;
}
}
impl IntersectsVolume<Self> for BoundingCircle {
#[inline(always)]
fn intersects(&self, other: &Self) -> bool {
let center_distance_squared = self.center.distance_squared(other.center);
let radius_sum_squared = (self.radius() + other.radius()).squared();
center_distance_squared <= radius_sum_squared
}
}
impl IntersectsVolume<Aabb2d> for BoundingCircle {
#[inline(always)]
fn intersects(&self, aabb: &Aabb2d) -> bool {
aabb.intersects(self)
}
}
#[cfg(test)]
mod bounding_circle_tests {
use super::BoundingCircle;
use crate::{
bounding::{BoundingVolume, IntersectsVolume},
ops, Vec2,
};
#[test]
fn area() {
let circle = BoundingCircle::new(Vec2::ONE, 5.);
// Since this number is messy we check it with a higher threshold
assert!(ops::abs(circle.visible_area() - 78.5398) < 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() < f32::EPSILON);
assert!(ops::abs(merged.radius() - 5.5) < 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!(ops::abs(padded.radius() - 6.25) < 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!(ops::abs(shrunk.radius() - 4.5) < f32::EPSILON);
assert!(a.contains(&shrunk));
assert!(!shrunk.contains(&a));
}
#[test]
fn scale_around_center() {
let a = BoundingCircle::new(Vec2::ONE, 5.);
let scaled = a.scale_around_center(2.);
assert!(ops::abs(scaled.radius() - 10.) < f32::EPSILON);
assert!(!a.contains(&scaled));
assert!(scaled.contains(&a));
}
#[test]
fn transform() {
let a = BoundingCircle::new(Vec2::ONE, 5.0);
let transformed = a.transformed_by(Vec2::new(2.0, -2.0), core::f32::consts::FRAC_PI_4);
assert_eq!(
transformed.center,
Vec2::new(2.0, core::f32::consts::SQRT_2 - 2.0)
);
assert_eq!(transformed.radius(), 5.0);
}
#[test]
fn closest_point() {
let circle = BoundingCircle::new(Vec2::ZERO, 1.0);
assert_eq!(circle.closest_point(Vec2::X * 10.0), Vec2::X);
assert_eq!(
circle.closest_point(Vec2::NEG_ONE * 10.0),
Vec2::NEG_ONE.normalize()
);
assert_eq!(
circle.closest_point(Vec2::new(0.25, 0.1)),
Vec2::new(0.25, 0.1)
);
}
#[test]
fn intersect_bounding_circle() {
let circle = BoundingCircle::new(Vec2::ZERO, 1.0);
assert!(circle.intersects(&BoundingCircle::new(Vec2::ZERO, 1.0)));
assert!(circle.intersects(&BoundingCircle::new(Vec2::ONE * 1.25, 1.0)));
assert!(circle.intersects(&BoundingCircle::new(Vec2::NEG_ONE * 1.25, 1.0)));
assert!(!circle.intersects(&BoundingCircle::new(Vec2::ONE * 1.5, 1.0)));
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,473 @@
use core::f32::consts::FRAC_PI_2;
use glam::{Vec2, Vec3A, Vec3Swizzles};
use crate::{
bounding::{BoundingCircle, BoundingVolume},
ops,
primitives::{
Capsule2d, Cuboid, Cylinder, Ellipse, Extrusion, Line2d, Polygon, Polyline2d, Primitive2d,
Rectangle, RegularPolygon, Segment2d, Triangle2d,
},
Isometry2d, Isometry3d, Quat, Rot2,
};
#[cfg(feature = "alloc")]
use crate::primitives::{BoxedPolygon, BoxedPolyline2d};
use crate::{bounding::Bounded2d, primitives::Circle};
use super::{Aabb3d, Bounded3d, BoundingSphere};
impl BoundedExtrusion for Circle {
fn extrusion_aabb_3d(&self, half_depth: f32, isometry: impl Into<Isometry3d>) -> Aabb3d {
// Reference: http://iquilezles.org/articles/diskbbox/
let isometry = isometry.into();
let segment_dir = isometry.rotation * Vec3A::Z;
let top = (segment_dir * half_depth).abs();
let e = (Vec3A::ONE - segment_dir * segment_dir).max(Vec3A::ZERO);
let half_size = self.radius * Vec3A::new(ops::sqrt(e.x), ops::sqrt(e.y), ops::sqrt(e.z));
Aabb3d {
min: isometry.translation - half_size - top,
max: isometry.translation + half_size + top,
}
}
}
impl BoundedExtrusion for Ellipse {
fn extrusion_aabb_3d(&self, half_depth: f32, isometry: impl Into<Isometry3d>) -> Aabb3d {
let isometry = isometry.into();
let Vec2 { x: a, y: b } = self.half_size;
let normal = isometry.rotation * Vec3A::Z;
let conjugate_rot = isometry.rotation.conjugate();
let [max_x, max_y, max_z] = Vec3A::AXES.map(|axis| {
let Some(axis) = (conjugate_rot * axis.reject_from(normal))
.xy()
.try_normalize()
else {
return Vec3A::ZERO;
};
if axis.element_product() == 0. {
return isometry.rotation * Vec3A::new(a * axis.y, b * axis.x, 0.);
}
let m = -axis.x / axis.y;
let signum = axis.signum();
let y = signum.y * b * b / ops::sqrt(b * b + m * m * a * a);
let x = signum.x * a * ops::sqrt(1. - y * y / b / b);
isometry.rotation * Vec3A::new(x, y, 0.)
});
let half_size = Vec3A::new(max_x.x, max_y.y, max_z.z).abs() + (normal * half_depth).abs();
Aabb3d::new(isometry.translation, half_size)
}
}
impl BoundedExtrusion for Line2d {
fn extrusion_aabb_3d(&self, half_depth: f32, isometry: impl Into<Isometry3d>) -> Aabb3d {
let isometry = isometry.into();
let dir = isometry.rotation * Vec3A::from(self.direction.extend(0.));
let half_depth = (isometry.rotation * Vec3A::new(0., 0., half_depth)).abs();
let max = f32::MAX / 2.;
let half_size = Vec3A::new(
if dir.x == 0. { half_depth.x } else { max },
if dir.y == 0. { half_depth.y } else { max },
if dir.z == 0. { half_depth.z } else { max },
);
Aabb3d::new(isometry.translation, half_size)
}
}
impl BoundedExtrusion for Segment2d {
fn extrusion_aabb_3d(&self, half_depth: f32, isometry: impl Into<Isometry3d>) -> Aabb3d {
let isometry = isometry.into();
let half_size = isometry.rotation * Vec3A::from(self.point1().extend(0.));
let depth = isometry.rotation * Vec3A::new(0., 0., half_depth);
Aabb3d::new(isometry.translation, half_size.abs() + depth.abs())
}
}
impl<const N: usize> BoundedExtrusion for Polyline2d<N> {
fn extrusion_aabb_3d(&self, half_depth: f32, isometry: impl Into<Isometry3d>) -> Aabb3d {
let isometry = isometry.into();
let aabb =
Aabb3d::from_point_cloud(isometry, self.vertices.map(|v| v.extend(0.)).into_iter());
let depth = isometry.rotation * Vec3A::new(0., 0., half_depth);
aabb.grow(depth.abs())
}
}
#[cfg(feature = "alloc")]
impl BoundedExtrusion for BoxedPolyline2d {
fn extrusion_aabb_3d(&self, half_depth: f32, isometry: impl Into<Isometry3d>) -> Aabb3d {
let isometry = isometry.into();
let aabb = Aabb3d::from_point_cloud(isometry, self.vertices.iter().map(|v| v.extend(0.)));
let depth = isometry.rotation * Vec3A::new(0., 0., half_depth);
aabb.grow(depth.abs())
}
}
impl BoundedExtrusion for Triangle2d {
fn extrusion_aabb_3d(&self, half_depth: f32, isometry: impl Into<Isometry3d>) -> Aabb3d {
let isometry = isometry.into();
let aabb = Aabb3d::from_point_cloud(isometry, self.vertices.iter().map(|v| v.extend(0.)));
let depth = isometry.rotation * Vec3A::new(0., 0., half_depth);
aabb.grow(depth.abs())
}
}
impl BoundedExtrusion for Rectangle {
fn extrusion_aabb_3d(&self, half_depth: f32, isometry: impl Into<Isometry3d>) -> Aabb3d {
Cuboid {
half_size: self.half_size.extend(half_depth),
}
.aabb_3d(isometry)
}
}
impl<const N: usize> BoundedExtrusion for Polygon<N> {
fn extrusion_aabb_3d(&self, half_depth: f32, isometry: impl Into<Isometry3d>) -> Aabb3d {
let isometry = isometry.into();
let aabb =
Aabb3d::from_point_cloud(isometry, self.vertices.map(|v| v.extend(0.)).into_iter());
let depth = isometry.rotation * Vec3A::new(0., 0., half_depth);
aabb.grow(depth.abs())
}
}
#[cfg(feature = "alloc")]
impl BoundedExtrusion for BoxedPolygon {
fn extrusion_aabb_3d(&self, half_depth: f32, isometry: impl Into<Isometry3d>) -> Aabb3d {
let isometry = isometry.into();
let aabb = Aabb3d::from_point_cloud(isometry, self.vertices.iter().map(|v| v.extend(0.)));
let depth = isometry.rotation * Vec3A::new(0., 0., half_depth);
aabb.grow(depth.abs())
}
}
impl BoundedExtrusion for RegularPolygon {
fn extrusion_aabb_3d(&self, half_depth: f32, isometry: impl Into<Isometry3d>) -> Aabb3d {
let isometry = isometry.into();
let aabb = Aabb3d::from_point_cloud(
isometry,
self.vertices(0.).into_iter().map(|v| v.extend(0.)),
);
let depth = isometry.rotation * Vec3A::new(0., 0., half_depth);
aabb.grow(depth.abs())
}
}
impl BoundedExtrusion for Capsule2d {
fn extrusion_aabb_3d(&self, half_depth: f32, isometry: impl Into<Isometry3d>) -> Aabb3d {
let isometry = isometry.into();
let aabb = Cylinder {
half_height: half_depth,
radius: self.radius,
}
.aabb_3d(isometry.rotation * Quat::from_rotation_x(FRAC_PI_2));
let up = isometry.rotation * Vec3A::new(0., self.half_length, 0.);
let half_size = aabb.max + up.abs();
Aabb3d::new(isometry.translation, half_size)
}
}
impl<T: BoundedExtrusion> Bounded3d for Extrusion<T> {
fn aabb_3d(&self, isometry: impl Into<Isometry3d>) -> Aabb3d {
self.base_shape.extrusion_aabb_3d(self.half_depth, isometry)
}
fn bounding_sphere(&self, isometry: impl Into<Isometry3d>) -> BoundingSphere {
self.base_shape
.extrusion_bounding_sphere(self.half_depth, isometry)
}
}
/// A trait implemented on 2D shapes which determines the 3D bounding volumes of their extrusions.
///
/// Since default implementations can be inferred from 2D bounding volumes, this allows a `Bounded2d`
/// implementation on some shape `MyShape` to be extrapolated to a `Bounded3d` implementation on
/// `Extrusion<MyShape>` without supplying any additional data; e.g.:
/// `impl BoundedExtrusion for MyShape {}`
pub trait BoundedExtrusion: Primitive2d + Bounded2d {
/// Get an axis-aligned bounding box for an extrusion with this shape as a base and the given `half_depth`, transformed by the given `translation` and `rotation`.
fn extrusion_aabb_3d(&self, half_depth: f32, isometry: impl Into<Isometry3d>) -> Aabb3d {
let isometry = isometry.into();
let cap_normal = isometry.rotation * Vec3A::Z;
let conjugate_rot = isometry.rotation.conjugate();
// The `(halfsize, offset)` for each axis
let axis_values = Vec3A::AXES.map(|ax| {
// This is the direction of the line of intersection of a plane with the `ax` normal and the plane containing the cap of the extrusion.
let intersect_line = ax.cross(cap_normal);
if intersect_line.length_squared() <= f32::EPSILON {
return (0., 0.);
};
// This is the normal vector of the intersection line rotated to be in the XY-plane
let line_normal = (conjugate_rot * intersect_line).yx();
let angle = line_normal.to_angle();
// Since the plane containing the caps of the extrusion is not guaranteed to be orthogonal to the `ax` plane, only a certain "scale" factor
// of the `Aabb2d` will actually go towards the dimensions of the `Aabb3d`
let scale = cap_normal.reject_from(ax).length();
// Calculate the `Aabb2d` of the base shape. The shape is rotated so that the line of intersection is parallel to the Y axis in the `Aabb2d` calculations.
// This guarantees that the X value of the `Aabb2d` is closest to the `ax` plane
let aabb2d = self.aabb_2d(Rot2::radians(angle));
(aabb2d.half_size().x * scale, aabb2d.center().x * scale)
});
let offset = Vec3A::from_array(axis_values.map(|(_, offset)| offset));
let cap_size = Vec3A::from_array(axis_values.map(|(max_val, _)| max_val)).abs();
let depth = isometry.rotation * Vec3A::new(0., 0., half_depth);
Aabb3d::new(isometry.translation - offset, cap_size + depth.abs())
}
/// Get a bounding sphere for an extrusion of the `base_shape` with the given `half_depth` with the given translation and rotation
fn extrusion_bounding_sphere(
&self,
half_depth: f32,
isometry: impl Into<Isometry3d>,
) -> BoundingSphere {
let isometry = isometry.into();
// We calculate the bounding circle of the base shape.
// Since each of the extrusions bases will have the same distance from its center,
// and they are just shifted along the Z-axis, the minimum bounding sphere will be the bounding sphere
// of the cylinder defined by the two bounding circles of the bases for any base shape
let BoundingCircle {
center,
circle: Circle { radius },
} = self.bounding_circle(Isometry2d::IDENTITY);
let radius = ops::hypot(radius, half_depth);
let center = isometry * Vec3A::from(center.extend(0.));
BoundingSphere::new(center, radius)
}
}
#[cfg(test)]
mod tests {
use core::f32::consts::FRAC_PI_4;
use glam::{EulerRot, Quat, Vec2, Vec3, Vec3A};
use crate::{
bounding::{Bounded3d, BoundingVolume},
ops,
primitives::{
Capsule2d, Circle, Ellipse, Extrusion, Line2d, Polygon, Polyline2d, Rectangle,
RegularPolygon, Segment2d, Triangle2d,
},
Dir2, Isometry3d,
};
#[test]
fn circle() {
let cylinder = Extrusion::new(Circle::new(0.5), 2.0);
let translation = Vec3::new(2.0, 1.0, 0.0);
let aabb = cylinder.aabb_3d(translation);
assert_eq!(aabb.center(), Vec3A::from(translation));
assert_eq!(aabb.half_size(), Vec3A::new(0.5, 0.5, 1.0));
let bounding_sphere = cylinder.bounding_sphere(translation);
assert_eq!(bounding_sphere.center, translation.into());
assert_eq!(bounding_sphere.radius(), ops::hypot(1.0, 0.5));
}
#[test]
fn ellipse() {
let extrusion = Extrusion::new(Ellipse::new(2.0, 0.5), 4.0);
let translation = Vec3::new(3., 4., 5.);
let rotation = Quat::from_euler(EulerRot::ZYX, FRAC_PI_4, FRAC_PI_4, FRAC_PI_4);
let isometry = Isometry3d::new(translation, rotation);
let aabb = extrusion.aabb_3d(isometry);
assert_eq!(aabb.center(), Vec3A::from(translation));
assert_eq!(aabb.half_size(), Vec3A::new(2.709784, 1.3801551, 2.436141));
let bounding_sphere = extrusion.bounding_sphere(isometry);
assert_eq!(bounding_sphere.center, translation.into());
assert_eq!(bounding_sphere.radius(), ops::sqrt(8f32));
}
#[test]
fn line() {
let extrusion = Extrusion::new(
Line2d {
direction: Dir2::new_unchecked(Vec2::Y),
},
4.,
);
let translation = Vec3::new(3., 4., 5.);
let rotation = Quat::from_rotation_y(FRAC_PI_4);
let isometry = Isometry3d::new(translation, rotation);
let aabb = extrusion.aabb_3d(isometry);
assert_eq!(aabb.min, Vec3A::new(1.5857864, f32::MIN / 2., 3.5857865));
assert_eq!(aabb.max, Vec3A::new(4.4142136, f32::MAX / 2., 6.414213));
let bounding_sphere = extrusion.bounding_sphere(isometry);
assert_eq!(bounding_sphere.center(), translation.into());
assert_eq!(bounding_sphere.radius(), f32::MAX / 2.);
}
#[test]
fn rectangle() {
let extrusion = Extrusion::new(Rectangle::new(2.0, 1.0), 4.0);
let translation = Vec3::new(3., 4., 5.);
let rotation = Quat::from_rotation_z(FRAC_PI_4);
let isometry = Isometry3d::new(translation, rotation);
let aabb = extrusion.aabb_3d(isometry);
assert_eq!(aabb.center(), translation.into());
assert_eq!(aabb.half_size(), Vec3A::new(1.0606602, 1.0606602, 2.));
let bounding_sphere = extrusion.bounding_sphere(isometry);
assert_eq!(bounding_sphere.center, translation.into());
assert_eq!(bounding_sphere.radius(), 2.291288);
}
#[test]
fn segment() {
let extrusion = Extrusion::new(
Segment2d::new(Vec2::new(0.0, -1.5), Vec2::new(0.0, 1.5)),
4.0,
);
let translation = Vec3::new(3., 4., 5.);
let rotation = Quat::from_rotation_x(FRAC_PI_4);
let isometry = Isometry3d::new(translation, rotation);
let aabb = extrusion.aabb_3d(isometry);
assert_eq!(aabb.center(), translation.into());
assert_eq!(aabb.half_size(), Vec3A::new(0., 2.4748735, 2.4748735));
let bounding_sphere = extrusion.bounding_sphere(isometry);
assert_eq!(bounding_sphere.center, translation.into());
assert_eq!(bounding_sphere.radius(), 2.5);
}
#[test]
fn polyline() {
let polyline = Polyline2d::<4>::new([
Vec2::ONE,
Vec2::new(-1.0, 1.0),
Vec2::NEG_ONE,
Vec2::new(1.0, -1.0),
]);
let extrusion = Extrusion::new(polyline, 3.0);
let translation = Vec3::new(3., 4., 5.);
let rotation = Quat::from_rotation_x(FRAC_PI_4);
let isometry = Isometry3d::new(translation, rotation);
let aabb = extrusion.aabb_3d(isometry);
assert_eq!(aabb.center(), translation.into());
assert_eq!(aabb.half_size(), Vec3A::new(1., 1.7677668, 1.7677668));
let bounding_sphere = extrusion.bounding_sphere(isometry);
assert_eq!(bounding_sphere.center, translation.into());
assert_eq!(bounding_sphere.radius(), 2.0615528);
}
#[test]
fn triangle() {
let triangle = Triangle2d::new(
Vec2::new(0.0, 1.0),
Vec2::new(-10.0, -1.0),
Vec2::new(10.0, -1.0),
);
let extrusion = Extrusion::new(triangle, 3.0);
let translation = Vec3::new(3., 4., 5.);
let rotation = Quat::from_rotation_x(FRAC_PI_4);
let isometry = Isometry3d::new(translation, rotation);
let aabb = extrusion.aabb_3d(isometry);
assert_eq!(aabb.center(), translation.into());
assert_eq!(aabb.half_size(), Vec3A::new(10., 1.7677668, 1.7677668));
let bounding_sphere = extrusion.bounding_sphere(isometry);
assert_eq!(
bounding_sphere.center,
Vec3A::new(3.0, 3.2928934, 4.2928934)
);
assert_eq!(bounding_sphere.radius(), 10.111875);
}
#[test]
fn polygon() {
let polygon = Polygon::<4>::new([
Vec2::ONE,
Vec2::new(-1.0, 1.0),
Vec2::NEG_ONE,
Vec2::new(1.0, -1.0),
]);
let extrusion = Extrusion::new(polygon, 3.0);
let translation = Vec3::new(3., 4., 5.);
let rotation = Quat::from_rotation_x(FRAC_PI_4);
let isometry = Isometry3d::new(translation, rotation);
let aabb = extrusion.aabb_3d(isometry);
assert_eq!(aabb.center(), translation.into());
assert_eq!(aabb.half_size(), Vec3A::new(1., 1.7677668, 1.7677668));
let bounding_sphere = extrusion.bounding_sphere(isometry);
assert_eq!(bounding_sphere.center, translation.into());
assert_eq!(bounding_sphere.radius(), 2.0615528);
}
#[test]
fn regular_polygon() {
let extrusion = Extrusion::new(RegularPolygon::new(2.0, 7), 4.0);
let translation = Vec3::new(3., 4., 5.);
let rotation = Quat::from_rotation_x(FRAC_PI_4);
let isometry = Isometry3d::new(translation, rotation);
let aabb = extrusion.aabb_3d(isometry);
assert_eq!(
aabb.center(),
Vec3A::from(translation) + Vec3A::new(0., 0.0700254, 0.0700254)
);
assert_eq!(
aabb.half_size(),
Vec3A::new(1.9498558, 2.7584014, 2.7584019)
);
let bounding_sphere = extrusion.bounding_sphere(isometry);
assert_eq!(bounding_sphere.center, translation.into());
assert_eq!(bounding_sphere.radius(), ops::sqrt(8f32));
}
#[test]
fn capsule() {
let extrusion = Extrusion::new(Capsule2d::new(0.5, 2.0), 4.0);
let translation = Vec3::new(3., 4., 5.);
let rotation = Quat::from_rotation_x(FRAC_PI_4);
let isometry = Isometry3d::new(translation, rotation);
let aabb = extrusion.aabb_3d(isometry);
assert_eq!(aabb.center(), translation.into());
assert_eq!(aabb.half_size(), Vec3A::new(0.5, 2.4748735, 2.4748735));
let bounding_sphere = extrusion.bounding_sphere(isometry);
assert_eq!(bounding_sphere.center, translation.into());
assert_eq!(bounding_sphere.radius(), 2.5);
}
}

View File

@@ -0,0 +1,807 @@
mod extrusion;
mod primitive_impls;
use glam::Mat3;
use super::{BoundingVolume, IntersectsVolume};
use crate::{
ops::{self, FloatPow},
Isometry3d, Quat, Vec3A,
};
#[cfg(feature = "bevy_reflect")]
use bevy_reflect::Reflect;
#[cfg(all(feature = "bevy_reflect", feature = "serialize"))]
use bevy_reflect::{ReflectDeserialize, ReflectSerialize};
#[cfg(feature = "serialize")]
use serde::{Deserialize, Serialize};
pub use extrusion::BoundedExtrusion;
/// Computes the geometric center of the given set of points.
#[inline(always)]
fn point_cloud_3d_center(points: impl Iterator<Item = impl Into<Vec3A>>) -> Vec3A {
let (acc, len) = points.fold((Vec3A::ZERO, 0), |(acc, len), point| {
(acc + point.into(), len + 1)
});
assert!(
len > 0,
"cannot compute the center of an empty set of points"
);
acc / len as f32
}
/// A trait with methods that return 3D bounding volumes for a shape.
pub trait Bounded3d {
/// Get an axis-aligned bounding box for the shape translated and rotated by the given isometry.
fn aabb_3d(&self, isometry: impl Into<Isometry3d>) -> Aabb3d;
/// Get a bounding sphere for the shape translated and rotated by the given isometry.
fn bounding_sphere(&self, isometry: impl Into<Isometry3d>) -> BoundingSphere;
}
/// A 3D axis-aligned bounding box
#[derive(Clone, Copy, Debug, PartialEq)]
#[cfg_attr(
feature = "bevy_reflect",
derive(Reflect),
reflect(Debug, PartialEq, Clone)
)]
#[cfg_attr(feature = "serialize", derive(Serialize), derive(Deserialize))]
#[cfg_attr(
all(feature = "serialize", feature = "bevy_reflect"),
reflect(Serialize, Deserialize)
)]
pub struct Aabb3d {
/// The minimum point of the box
pub min: Vec3A,
/// The maximum point of the box
pub max: Vec3A,
}
impl Aabb3d {
/// Constructs an AABB from its center and half-size.
#[inline(always)]
pub fn new(center: impl Into<Vec3A>, half_size: impl Into<Vec3A>) -> Self {
let (center, half_size) = (center.into(), half_size.into());
debug_assert!(half_size.x >= 0.0 && half_size.y >= 0.0 && half_size.z >= 0.0);
Self {
min: center - half_size,
max: center + half_size,
}
}
/// Computes the smallest [`Aabb3d`] containing the given set of points,
/// transformed by the rotation and translation of the given isometry.
///
/// # Panics
///
/// Panics if the given set of points is empty.
#[inline(always)]
pub fn from_point_cloud(
isometry: impl Into<Isometry3d>,
points: impl Iterator<Item = impl Into<Vec3A>>,
) -> Aabb3d {
let isometry = isometry.into();
// Transform all points by rotation
let mut iter = points.map(|point| isometry.rotation * point.into());
let first = iter
.next()
.expect("point cloud must contain at least one point for Aabb3d construction");
let (min, max) = iter.fold((first, first), |(prev_min, prev_max), point| {
(point.min(prev_min), point.max(prev_max))
});
Aabb3d {
min: min + isometry.translation,
max: max + isometry.translation,
}
}
/// Computes the smallest [`BoundingSphere`] containing this [`Aabb3d`].
#[inline(always)]
pub fn bounding_sphere(&self) -> BoundingSphere {
let radius = self.min.distance(self.max) / 2.0;
BoundingSphere::new(self.center(), radius)
}
/// Finds the point on the AABB that is closest to the given `point`.
///
/// If the point is outside the AABB, the returned point will be on the surface of the AABB.
/// Otherwise, it will be inside the AABB and returned as is.
#[inline(always)]
pub fn closest_point(&self, point: impl Into<Vec3A>) -> Vec3A {
// Clamp point coordinates to the AABB
point.into().clamp(self.min, self.max)
}
}
impl BoundingVolume for Aabb3d {
type Translation = Vec3A;
type Rotation = Quat;
type HalfSize = Vec3A;
#[inline(always)]
fn center(&self) -> Self::Translation {
(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.cmpge(self.min).all() && other.max.cmple(self.max).all()
}
#[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: impl Into<Self::HalfSize>) -> Self {
let amount = amount.into();
let b = Self {
min: self.min - amount,
max: self.max + amount,
};
debug_assert!(b.min.cmple(b.max).all());
b
}
#[inline(always)]
fn shrink(&self, amount: impl Into<Self::HalfSize>) -> Self {
let amount = amount.into();
let b = Self {
min: self.min + amount,
max: self.max - amount,
};
debug_assert!(b.min.cmple(b.max).all());
b
}
#[inline(always)]
fn scale_around_center(&self, scale: impl Into<Self::HalfSize>) -> Self {
let scale = scale.into();
let b = Self {
min: self.center() - (self.half_size() * scale),
max: self.center() + (self.half_size() * scale),
};
debug_assert!(b.min.cmple(b.max).all());
b
}
/// Transforms the bounding volume by first rotating it around the origin and then applying a translation.
///
/// The result is an Axis-Aligned Bounding Box that encompasses the rotated shape.
///
/// Note that the result may not be as tightly fitting as the original, and repeated rotations
/// can cause the AABB to grow indefinitely. Avoid applying multiple rotations to the same AABB,
/// and consider storing the original AABB and rotating that every time instead.
#[inline(always)]
fn transformed_by(
mut self,
translation: impl Into<Self::Translation>,
rotation: impl Into<Self::Rotation>,
) -> Self {
self.transform_by(translation, rotation);
self
}
/// Transforms the bounding volume by first rotating it around the origin and then applying a translation.
///
/// The result is an Axis-Aligned Bounding Box that encompasses the rotated shape.
///
/// Note that the result may not be as tightly fitting as the original, and repeated rotations
/// can cause the AABB to grow indefinitely. Avoid applying multiple rotations to the same AABB,
/// and consider storing the original AABB and rotating that every time instead.
#[inline(always)]
fn transform_by(
&mut self,
translation: impl Into<Self::Translation>,
rotation: impl Into<Self::Rotation>,
) {
self.rotate_by(rotation);
self.translate_by(translation);
}
#[inline(always)]
fn translate_by(&mut self, translation: impl Into<Self::Translation>) {
let translation = translation.into();
self.min += translation;
self.max += translation;
}
/// Rotates the bounding volume around the origin by the given rotation.
///
/// The result is an Axis-Aligned Bounding Box that encompasses the rotated shape.
///
/// Note that the result may not be as tightly fitting as the original, and repeated rotations
/// can cause the AABB to grow indefinitely. Avoid applying multiple rotations to the same AABB,
/// and consider storing the original AABB and rotating that every time instead.
#[inline(always)]
fn rotated_by(mut self, rotation: impl Into<Self::Rotation>) -> Self {
self.rotate_by(rotation);
self
}
/// Rotates the bounding volume around the origin by the given rotation.
///
/// The result is an Axis-Aligned Bounding Box that encompasses the rotated shape.
///
/// Note that the result may not be as tightly fitting as the original, and repeated rotations
/// can cause the AABB to grow indefinitely. Avoid applying multiple rotations to the same AABB,
/// and consider storing the original AABB and rotating that every time instead.
#[inline(always)]
fn rotate_by(&mut self, rotation: impl Into<Self::Rotation>) {
let rot_mat = Mat3::from_quat(rotation.into());
let half_size = rot_mat.abs() * self.half_size();
*self = Self::new(rot_mat * self.center(), half_size);
}
}
impl IntersectsVolume<Self> for Aabb3d {
#[inline(always)]
fn intersects(&self, other: &Self) -> bool {
self.min.cmple(other.max).all() && self.max.cmpge(other.min).all()
}
}
impl IntersectsVolume<BoundingSphere> for Aabb3d {
#[inline(always)]
fn intersects(&self, sphere: &BoundingSphere) -> bool {
let closest_point = self.closest_point(sphere.center);
let distance_squared = sphere.center.distance_squared(closest_point);
let radius_squared = sphere.radius().squared();
distance_squared <= radius_squared
}
}
#[cfg(test)]
mod aabb3d_tests {
use approx::assert_relative_eq;
use super::Aabb3d;
use crate::{
bounding::{BoundingSphere, BoundingVolume, IntersectsVolume},
ops, Quat, Vec3, Vec3A,
};
#[test]
fn center() {
let aabb = Aabb3d {
min: Vec3A::new(-0.5, -1., -0.5),
max: Vec3A::new(1., 1., 2.),
};
assert!((aabb.center() - Vec3A::new(0.25, 0., 0.75)).length() < f32::EPSILON);
let aabb = Aabb3d {
min: Vec3A::new(5., 5., -10.),
max: Vec3A::new(10., 10., -5.),
};
assert!((aabb.center() - Vec3A::new(7.5, 7.5, -7.5)).length() < f32::EPSILON);
}
#[test]
fn half_size() {
let aabb = Aabb3d {
min: Vec3A::new(-0.5, -1., -0.5),
max: Vec3A::new(1., 1., 2.),
};
assert!((aabb.half_size() - Vec3A::new(0.75, 1., 1.25)).length() < f32::EPSILON);
}
#[test]
fn area() {
let aabb = Aabb3d {
min: Vec3A::new(-1., -1., -1.),
max: Vec3A::new(1., 1., 1.),
};
assert!(ops::abs(aabb.visible_area() - 12.) < f32::EPSILON);
let aabb = Aabb3d {
min: Vec3A::new(0., 0., 0.),
max: Vec3A::new(1., 0.5, 0.25),
};
assert!(ops::abs(aabb.visible_area() - 0.875) < f32::EPSILON);
}
#[test]
fn contains() {
let a = Aabb3d {
min: Vec3A::new(-1., -1., -1.),
max: Vec3A::new(1., 1., 1.),
};
let b = Aabb3d {
min: Vec3A::new(-2., -1., -1.),
max: Vec3A::new(1., 1., 1.),
};
assert!(!a.contains(&b));
let b = Aabb3d {
min: Vec3A::new(-0.25, -0.8, -0.9),
max: Vec3A::new(1., 1., 0.9),
};
assert!(a.contains(&b));
}
#[test]
fn merge() {
let a = Aabb3d {
min: Vec3A::new(-1., -1., -1.),
max: Vec3A::new(1., 0.5, 1.),
};
let b = Aabb3d {
min: Vec3A::new(-2., -0.5, -0.),
max: Vec3A::new(0.75, 1., 2.),
};
let merged = a.merge(&b);
assert!((merged.min - Vec3A::new(-2., -1., -1.)).length() < f32::EPSILON);
assert!((merged.max - Vec3A::new(1., 1., 2.)).length() < 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: Vec3A::new(-1., -1., -1.),
max: Vec3A::new(1., 1., 1.),
};
let padded = a.grow(Vec3A::ONE);
assert!((padded.min - Vec3A::new(-2., -2., -2.)).length() < f32::EPSILON);
assert!((padded.max - Vec3A::new(2., 2., 2.)).length() < f32::EPSILON);
assert!(padded.contains(&a));
assert!(!a.contains(&padded));
}
#[test]
fn shrink() {
let a = Aabb3d {
min: Vec3A::new(-2., -2., -2.),
max: Vec3A::new(2., 2., 2.),
};
let shrunk = a.shrink(Vec3A::ONE);
assert!((shrunk.min - Vec3A::new(-1., -1., -1.)).length() < f32::EPSILON);
assert!((shrunk.max - Vec3A::new(1., 1., 1.)).length() < f32::EPSILON);
assert!(a.contains(&shrunk));
assert!(!shrunk.contains(&a));
}
#[test]
fn scale_around_center() {
let a = Aabb3d {
min: Vec3A::NEG_ONE,
max: Vec3A::ONE,
};
let scaled = a.scale_around_center(Vec3A::splat(2.));
assert!((scaled.min - Vec3A::splat(-2.)).length() < f32::EPSILON);
assert!((scaled.max - Vec3A::splat(2.)).length() < f32::EPSILON);
assert!(!a.contains(&scaled));
assert!(scaled.contains(&a));
}
#[test]
fn rotate() {
use core::f32::consts::PI;
let a = Aabb3d {
min: Vec3A::new(-2.0, -2.0, -2.0),
max: Vec3A::new(2.0, 2.0, 2.0),
};
let rotation = Quat::from_euler(glam::EulerRot::XYZ, PI, PI, 0.0);
let rotated = a.rotated_by(rotation);
assert_relative_eq!(rotated.min, a.min);
assert_relative_eq!(rotated.max, a.max);
}
#[test]
fn transform() {
let a = Aabb3d {
min: Vec3A::new(-2.0, -2.0, -2.0),
max: Vec3A::new(2.0, 2.0, 2.0),
};
let transformed = a.transformed_by(
Vec3A::new(2.0, -2.0, 4.0),
Quat::from_rotation_z(core::f32::consts::FRAC_PI_4),
);
let half_length = ops::hypot(2.0, 2.0);
assert_eq!(
transformed.min,
Vec3A::new(2.0 - half_length, -half_length - 2.0, 2.0)
);
assert_eq!(
transformed.max,
Vec3A::new(2.0 + half_length, half_length - 2.0, 6.0)
);
}
#[test]
fn closest_point() {
let aabb = Aabb3d {
min: Vec3A::NEG_ONE,
max: Vec3A::ONE,
};
assert_eq!(aabb.closest_point(Vec3A::X * 10.0), Vec3A::X);
assert_eq!(aabb.closest_point(Vec3A::NEG_ONE * 10.0), Vec3A::NEG_ONE);
assert_eq!(
aabb.closest_point(Vec3A::new(0.25, 0.1, 0.3)),
Vec3A::new(0.25, 0.1, 0.3)
);
}
#[test]
fn intersect_aabb() {
let aabb = Aabb3d {
min: Vec3A::NEG_ONE,
max: Vec3A::ONE,
};
assert!(aabb.intersects(&aabb));
assert!(aabb.intersects(&Aabb3d {
min: Vec3A::splat(0.5),
max: Vec3A::splat(2.0),
}));
assert!(aabb.intersects(&Aabb3d {
min: Vec3A::splat(-2.0),
max: Vec3A::splat(-0.5),
}));
assert!(!aabb.intersects(&Aabb3d {
min: Vec3A::new(1.1, 0.0, 0.0),
max: Vec3A::new(2.0, 0.5, 0.25),
}));
}
#[test]
fn intersect_bounding_sphere() {
let aabb = Aabb3d {
min: Vec3A::NEG_ONE,
max: Vec3A::ONE,
};
assert!(aabb.intersects(&BoundingSphere::new(Vec3::ZERO, 1.0)));
assert!(aabb.intersects(&BoundingSphere::new(Vec3::ONE * 1.5, 1.0)));
assert!(aabb.intersects(&BoundingSphere::new(Vec3::NEG_ONE * 1.5, 1.0)));
assert!(!aabb.intersects(&BoundingSphere::new(Vec3::ONE * 1.75, 1.0)));
}
}
use crate::primitives::Sphere;
/// A bounding sphere
#[derive(Clone, Copy, Debug, PartialEq)]
#[cfg_attr(
feature = "bevy_reflect",
derive(Reflect),
reflect(Debug, PartialEq, Clone)
)]
#[cfg_attr(feature = "serialize", derive(Serialize), derive(Deserialize))]
#[cfg_attr(
all(feature = "serialize", feature = "bevy_reflect"),
reflect(Serialize, Deserialize)
)]
pub struct BoundingSphere {
/// The center of the bounding sphere
pub center: Vec3A,
/// The sphere
pub sphere: Sphere,
}
impl BoundingSphere {
/// Constructs a bounding sphere from its center and radius.
pub fn new(center: impl Into<Vec3A>, radius: f32) -> Self {
debug_assert!(radius >= 0.);
Self {
center: center.into(),
sphere: Sphere { radius },
}
}
/// Computes a [`BoundingSphere`] containing the given set of points,
/// transformed by the rotation and translation of the given isometry.
///
/// The bounding sphere is not guaranteed to be the smallest possible.
#[inline(always)]
pub fn from_point_cloud(
isometry: impl Into<Isometry3d>,
points: &[impl Copy + Into<Vec3A>],
) -> BoundingSphere {
let isometry = isometry.into();
let center = point_cloud_3d_center(points.iter().map(|v| Into::<Vec3A>::into(*v)));
let mut radius_squared: f32 = 0.0;
for point in points {
// Get squared version to avoid unnecessary sqrt calls
let distance_squared = Into::<Vec3A>::into(*point).distance_squared(center);
if distance_squared > radius_squared {
radius_squared = distance_squared;
}
}
BoundingSphere::new(isometry * center, ops::sqrt(radius_squared))
}
/// Get the radius of the bounding sphere
#[inline(always)]
pub fn radius(&self) -> f32 {
self.sphere.radius
}
/// Computes the smallest [`Aabb3d`] containing this [`BoundingSphere`].
#[inline(always)]
pub fn aabb_3d(&self) -> Aabb3d {
Aabb3d {
min: self.center - self.radius(),
max: self.center + self.radius(),
}
}
/// Finds the point on the bounding sphere that is closest to the given `point`.
///
/// If the point is outside the sphere, the returned point will be on the surface of the sphere.
/// Otherwise, it will be inside the sphere and returned as is.
#[inline(always)]
pub fn closest_point(&self, point: impl Into<Vec3A>) -> Vec3A {
let point = point.into();
let radius = self.radius();
let distance_squared = (point - self.center).length_squared();
if distance_squared <= radius.squared() {
// The point is inside the sphere.
point
} else {
// The point is outside the sphere.
// Find the closest point on the surface of the sphere.
let dir_to_point = point / ops::sqrt(distance_squared);
self.center + radius * dir_to_point
}
}
}
impl BoundingVolume for BoundingSphere {
type Translation = Vec3A;
type Rotation = Quat;
type HalfSize = f32;
#[inline(always)]
fn center(&self) -> Self::Translation {
self.center
}
#[inline(always)]
fn half_size(&self) -> Self::HalfSize {
self.radius()
}
#[inline(always)]
fn visible_area(&self) -> f32 {
2. * core::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) <= ops::copysign(diff.squared(), 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;
}
if other.radius() >= length + self.radius() {
return *other;
}
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: impl Into<Self::HalfSize>) -> Self {
let amount = amount.into();
debug_assert!(amount >= 0.);
Self {
center: self.center,
sphere: Sphere {
radius: self.radius() + amount,
},
}
}
#[inline(always)]
fn shrink(&self, amount: impl Into<Self::HalfSize>) -> Self {
let amount = amount.into();
debug_assert!(amount >= 0.);
debug_assert!(self.radius() >= amount);
Self {
center: self.center,
sphere: Sphere {
radius: self.radius() - amount,
},
}
}
#[inline(always)]
fn scale_around_center(&self, scale: impl Into<Self::HalfSize>) -> Self {
let scale = scale.into();
debug_assert!(scale >= 0.);
Self::new(self.center, self.radius() * scale)
}
#[inline(always)]
fn translate_by(&mut self, translation: impl Into<Self::Translation>) {
self.center += translation.into();
}
#[inline(always)]
fn rotate_by(&mut self, rotation: impl Into<Self::Rotation>) {
let rotation: Quat = rotation.into();
self.center = rotation * self.center;
}
}
impl IntersectsVolume<Self> for BoundingSphere {
#[inline(always)]
fn intersects(&self, other: &Self) -> bool {
let center_distance_squared = self.center.distance_squared(other.center);
let radius_sum_squared = (self.radius() + other.radius()).squared();
center_distance_squared <= radius_sum_squared
}
}
impl IntersectsVolume<Aabb3d> for BoundingSphere {
#[inline(always)]
fn intersects(&self, aabb: &Aabb3d) -> bool {
aabb.intersects(self)
}
}
#[cfg(test)]
mod bounding_sphere_tests {
use approx::assert_relative_eq;
use super::BoundingSphere;
use crate::{
bounding::{BoundingVolume, IntersectsVolume},
ops, Quat, Vec3, Vec3A,
};
#[test]
fn area() {
let sphere = BoundingSphere::new(Vec3::ONE, 5.);
// Since this number is messy we check it with a higher threshold
assert!(ops::abs(sphere.visible_area() - 157.0796) < 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 - Vec3A::new(1., 1., 0.5)).length() < f32::EPSILON);
assert!(ops::abs(merged.radius() - 5.5) < 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!(ops::abs(padded.radius() - 6.25) < 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!(ops::abs(shrunk.radius() - 4.5) < f32::EPSILON);
assert!(a.contains(&shrunk));
assert!(!shrunk.contains(&a));
}
#[test]
fn scale_around_center() {
let a = BoundingSphere::new(Vec3::ONE, 5.);
let scaled = a.scale_around_center(2.);
assert!(ops::abs(scaled.radius() - 10.) < f32::EPSILON);
assert!(!a.contains(&scaled));
assert!(scaled.contains(&a));
}
#[test]
fn transform() {
let a = BoundingSphere::new(Vec3::ONE, 5.0);
let transformed = a.transformed_by(
Vec3::new(2.0, -2.0, 4.0),
Quat::from_rotation_z(core::f32::consts::FRAC_PI_4),
);
assert_relative_eq!(
transformed.center,
Vec3A::new(2.0, core::f32::consts::SQRT_2 - 2.0, 5.0)
);
assert_eq!(transformed.radius(), 5.0);
}
#[test]
fn closest_point() {
let sphere = BoundingSphere::new(Vec3::ZERO, 1.0);
assert_eq!(sphere.closest_point(Vec3::X * 10.0), Vec3A::X);
assert_eq!(
sphere.closest_point(Vec3::NEG_ONE * 10.0),
Vec3A::NEG_ONE.normalize()
);
assert_eq!(
sphere.closest_point(Vec3::new(0.25, 0.1, 0.3)),
Vec3A::new(0.25, 0.1, 0.3)
);
}
#[test]
fn intersect_bounding_sphere() {
let sphere = BoundingSphere::new(Vec3::ZERO, 1.0);
assert!(sphere.intersects(&BoundingSphere::new(Vec3::ZERO, 1.0)));
assert!(sphere.intersects(&BoundingSphere::new(Vec3::ONE * 1.1, 1.0)));
assert!(sphere.intersects(&BoundingSphere::new(Vec3::NEG_ONE * 1.1, 1.0)));
assert!(!sphere.intersects(&BoundingSphere::new(Vec3::ONE * 1.2, 1.0)));
}
}

View File

@@ -0,0 +1,700 @@
//! Contains [`Bounded3d`] implementations for [geometric primitives](crate::primitives).
use crate::{
bounding::{Bounded2d, BoundingCircle, BoundingVolume},
ops,
primitives::{
Capsule3d, Cone, ConicalFrustum, Cuboid, Cylinder, InfinitePlane3d, Line3d, Polyline3d,
Segment3d, Sphere, Torus, Triangle2d, Triangle3d,
},
Isometry2d, Isometry3d, Mat3, Vec2, Vec3, Vec3A,
};
#[cfg(feature = "alloc")]
use crate::primitives::BoxedPolyline3d;
use super::{Aabb3d, Bounded3d, BoundingSphere};
impl Bounded3d for Sphere {
fn aabb_3d(&self, isometry: impl Into<Isometry3d>) -> Aabb3d {
let isometry = isometry.into();
Aabb3d::new(isometry.translation, Vec3::splat(self.radius))
}
fn bounding_sphere(&self, isometry: impl Into<Isometry3d>) -> BoundingSphere {
let isometry = isometry.into();
BoundingSphere::new(isometry.translation, self.radius)
}
}
impl Bounded3d for InfinitePlane3d {
fn aabb_3d(&self, isometry: impl Into<Isometry3d>) -> Aabb3d {
let isometry = isometry.into();
let normal = isometry.rotation * *self.normal;
let facing_x = normal == Vec3::X || normal == Vec3::NEG_X;
let facing_y = normal == Vec3::Y || normal == Vec3::NEG_Y;
let facing_z = normal == Vec3::Z || normal == Vec3::NEG_Z;
// Dividing `f32::MAX` by 2.0 is helpful so that we can do operations
// like growing or shrinking the AABB without breaking things.
let half_width = if facing_x { 0.0 } else { f32::MAX / 2.0 };
let half_height = if facing_y { 0.0 } else { f32::MAX / 2.0 };
let half_depth = if facing_z { 0.0 } else { f32::MAX / 2.0 };
let half_size = Vec3A::new(half_width, half_height, half_depth);
Aabb3d::new(isometry.translation, half_size)
}
fn bounding_sphere(&self, isometry: impl Into<Isometry3d>) -> BoundingSphere {
let isometry = isometry.into();
BoundingSphere::new(isometry.translation, f32::MAX / 2.0)
}
}
impl Bounded3d for Line3d {
fn aabb_3d(&self, isometry: impl Into<Isometry3d>) -> Aabb3d {
let isometry = isometry.into();
let direction = isometry.rotation * *self.direction;
// Dividing `f32::MAX` by 2.0 is helpful so that we can do operations
// like growing or shrinking the AABB without breaking things.
let max = f32::MAX / 2.0;
let half_width = if direction.x == 0.0 { 0.0 } else { max };
let half_height = if direction.y == 0.0 { 0.0 } else { max };
let half_depth = if direction.z == 0.0 { 0.0 } else { max };
let half_size = Vec3A::new(half_width, half_height, half_depth);
Aabb3d::new(isometry.translation, half_size)
}
fn bounding_sphere(&self, isometry: impl Into<Isometry3d>) -> BoundingSphere {
let isometry = isometry.into();
BoundingSphere::new(isometry.translation, f32::MAX / 2.0)
}
}
impl Bounded3d for Segment3d {
fn aabb_3d(&self, isometry: impl Into<Isometry3d>) -> Aabb3d {
Aabb3d::from_point_cloud(isometry, [self.point1(), self.point2()].iter().copied())
}
fn bounding_sphere(&self, isometry: impl Into<Isometry3d>) -> BoundingSphere {
let isometry = isometry.into();
let local_sphere = BoundingSphere::new(self.center(), self.length() / 2.);
local_sphere.transformed_by(isometry.translation, isometry.rotation)
}
}
impl<const N: usize> Bounded3d for Polyline3d<N> {
fn aabb_3d(&self, isometry: impl Into<Isometry3d>) -> Aabb3d {
Aabb3d::from_point_cloud(isometry, self.vertices.iter().copied())
}
fn bounding_sphere(&self, isometry: impl Into<Isometry3d>) -> BoundingSphere {
BoundingSphere::from_point_cloud(isometry, &self.vertices)
}
}
#[cfg(feature = "alloc")]
impl Bounded3d for BoxedPolyline3d {
fn aabb_3d(&self, isometry: impl Into<Isometry3d>) -> Aabb3d {
Aabb3d::from_point_cloud(isometry, self.vertices.iter().copied())
}
fn bounding_sphere(&self, isometry: impl Into<Isometry3d>) -> BoundingSphere {
BoundingSphere::from_point_cloud(isometry, &self.vertices)
}
}
impl Bounded3d for Cuboid {
fn aabb_3d(&self, isometry: impl Into<Isometry3d>) -> Aabb3d {
let isometry = isometry.into();
// Compute the AABB of the rotated cuboid by transforming the half-size
// by an absolute rotation matrix.
let rot_mat = Mat3::from_quat(isometry.rotation);
let abs_rot_mat = Mat3::from_cols(
rot_mat.x_axis.abs(),
rot_mat.y_axis.abs(),
rot_mat.z_axis.abs(),
);
let half_size = abs_rot_mat * self.half_size;
Aabb3d::new(isometry.translation, half_size)
}
fn bounding_sphere(&self, isometry: impl Into<Isometry3d>) -> BoundingSphere {
let isometry = isometry.into();
BoundingSphere::new(isometry.translation, self.half_size.length())
}
}
impl Bounded3d for Cylinder {
fn aabb_3d(&self, isometry: impl Into<Isometry3d>) -> Aabb3d {
// Reference: http://iquilezles.org/articles/diskbbox/
let isometry = isometry.into();
let segment_dir = isometry.rotation * Vec3A::Y;
let top = segment_dir * self.half_height;
let bottom = -top;
let e = (Vec3A::ONE - segment_dir * segment_dir).max(Vec3A::ZERO);
let half_size = self.radius * Vec3A::new(ops::sqrt(e.x), ops::sqrt(e.y), ops::sqrt(e.z));
Aabb3d {
min: isometry.translation + (top - half_size).min(bottom - half_size),
max: isometry.translation + (top + half_size).max(bottom + half_size),
}
}
fn bounding_sphere(&self, isometry: impl Into<Isometry3d>) -> BoundingSphere {
let isometry = isometry.into();
let radius = ops::hypot(self.radius, self.half_height);
BoundingSphere::new(isometry.translation, radius)
}
}
impl Bounded3d for Capsule3d {
fn aabb_3d(&self, isometry: impl Into<Isometry3d>) -> Aabb3d {
let isometry = isometry.into();
// Get the line segment between the hemispheres of the rotated capsule
let segment_dir = isometry.rotation * Vec3A::Y;
let top = segment_dir * self.half_length;
let bottom = -top;
// Expand the line segment by the capsule radius to get the capsule half-extents
let min = bottom.min(top) - Vec3A::splat(self.radius);
let max = bottom.max(top) + Vec3A::splat(self.radius);
Aabb3d {
min: min + isometry.translation,
max: max + isometry.translation,
}
}
fn bounding_sphere(&self, isometry: impl Into<Isometry3d>) -> BoundingSphere {
let isometry = isometry.into();
BoundingSphere::new(isometry.translation, self.radius + self.half_length)
}
}
impl Bounded3d for Cone {
fn aabb_3d(&self, isometry: impl Into<Isometry3d>) -> Aabb3d {
// Reference: http://iquilezles.org/articles/diskbbox/
let isometry = isometry.into();
let segment_dir = isometry.rotation * Vec3A::Y;
let top = segment_dir * 0.5 * self.height;
let bottom = -top;
let e = (Vec3A::ONE - segment_dir * segment_dir).max(Vec3A::ZERO);
let half_extents = Vec3A::new(ops::sqrt(e.x), ops::sqrt(e.y), ops::sqrt(e.z));
Aabb3d {
min: isometry.translation + top.min(bottom - self.radius * half_extents),
max: isometry.translation + top.max(bottom + self.radius * half_extents),
}
}
fn bounding_sphere(&self, isometry: impl Into<Isometry3d>) -> BoundingSphere {
let isometry = isometry.into();
// Get the triangular cross-section of the cone.
let half_height = 0.5 * self.height;
let triangle = Triangle2d::new(
half_height * Vec2::Y,
Vec2::new(-self.radius, -half_height),
Vec2::new(self.radius, -half_height),
);
// Because of circular symmetry, we can use the bounding circle of the triangle
// for the bounding sphere of the cone.
let BoundingCircle { circle, center } = triangle.bounding_circle(Isometry2d::IDENTITY);
BoundingSphere::new(
isometry.rotation * Vec3A::from(center.extend(0.0)) + isometry.translation,
circle.radius,
)
}
}
impl Bounded3d for ConicalFrustum {
fn aabb_3d(&self, isometry: impl Into<Isometry3d>) -> Aabb3d {
// Reference: http://iquilezles.org/articles/diskbbox/
let isometry = isometry.into();
let segment_dir = isometry.rotation * Vec3A::Y;
let top = segment_dir * 0.5 * self.height;
let bottom = -top;
let e = (Vec3A::ONE - segment_dir * segment_dir).max(Vec3A::ZERO);
let half_extents = Vec3A::new(ops::sqrt(e.x), ops::sqrt(e.y), ops::sqrt(e.z));
Aabb3d {
min: isometry.translation
+ (top - self.radius_top * half_extents)
.min(bottom - self.radius_bottom * half_extents),
max: isometry.translation
+ (top + self.radius_top * half_extents)
.max(bottom + self.radius_bottom * half_extents),
}
}
fn bounding_sphere(&self, isometry: impl Into<Isometry3d>) -> BoundingSphere {
let isometry = isometry.into();
let half_height = 0.5 * self.height;
// To compute the bounding sphere, we'll get the center and radius of the circumcircle
// passing through all four vertices of the trapezoidal cross-section of the conical frustum.
//
// If the circumcenter is inside the trapezoid, we can use that for the bounding sphere.
// Otherwise, we clamp it to the longer parallel side to get a more tightly fitting bounding sphere.
//
// The circumcenter is at the intersection of the bisectors perpendicular to the sides.
// For the isosceles trapezoid, the X coordinate is zero at the center, so a single bisector is enough.
//
// A
// *-------*
// / | \
// / | \
// AB / \ | / \
// / \ | / \
// / C \
// *-------------------*
// B
let a = Vec2::new(-self.radius_top, half_height);
let b = Vec2::new(-self.radius_bottom, -half_height);
let ab = a - b;
let ab_midpoint = b + 0.5 * ab;
let bisector = ab.perp();
// Compute intersection between bisector and vertical line at x = 0.
//
// x = ab_midpoint.x + t * bisector.x = 0
// y = ab_midpoint.y + t * bisector.y = ?
//
// Because ab_midpoint.y = 0 for our conical frustum, we get:
// y = t * bisector.y
//
// Solve x for t:
// t = -ab_midpoint.x / bisector.x
//
// Substitute t to solve for y:
// y = -ab_midpoint.x / bisector.x * bisector.y
let circumcenter_y = -ab_midpoint.x / bisector.x * bisector.y;
// If the circumcenter is outside the trapezoid, the bounding circle is too large.
// In those cases, we clamp it to the longer parallel side.
let (center, radius) = if circumcenter_y <= -half_height {
(Vec2::new(0.0, -half_height), self.radius_bottom)
} else if circumcenter_y >= half_height {
(Vec2::new(0.0, half_height), self.radius_top)
} else {
let circumcenter = Vec2::new(0.0, circumcenter_y);
// We can use the distance from an arbitrary vertex because they all lie on the circumcircle.
(circumcenter, a.distance(circumcenter))
};
BoundingSphere::new(
isometry.translation + isometry.rotation * Vec3A::from(center.extend(0.0)),
radius,
)
}
}
impl Bounded3d for Torus {
fn aabb_3d(&self, isometry: impl Into<Isometry3d>) -> Aabb3d {
let isometry = isometry.into();
// Compute the AABB of a flat disc with the major radius of the torus.
// Reference: http://iquilezles.org/articles/diskbbox/
let normal = isometry.rotation * Vec3A::Y;
let e = (Vec3A::ONE - normal * normal).max(Vec3A::ZERO);
let disc_half_size =
self.major_radius * Vec3A::new(ops::sqrt(e.x), ops::sqrt(e.y), ops::sqrt(e.z));
// Expand the disc by the minor radius to get the torus half-size
let half_size = disc_half_size + Vec3A::splat(self.minor_radius);
Aabb3d::new(isometry.translation, half_size)
}
fn bounding_sphere(&self, isometry: impl Into<Isometry3d>) -> BoundingSphere {
let isometry = isometry.into();
BoundingSphere::new(isometry.translation, self.outer_radius())
}
}
impl Bounded3d for Triangle3d {
/// Get the bounding box of the triangle.
fn aabb_3d(&self, isometry: impl Into<Isometry3d>) -> Aabb3d {
let isometry = isometry.into();
let [a, b, c] = self.vertices;
let a = isometry.rotation * a;
let b = isometry.rotation * b;
let c = isometry.rotation * c;
let min = Vec3A::from(a.min(b).min(c));
let max = Vec3A::from(a.max(b).max(c));
let bounding_center = (max + min) / 2.0 + isometry.translation;
let half_extents = (max - min) / 2.0;
Aabb3d::new(bounding_center, half_extents)
}
/// Get the bounding sphere of the triangle.
///
/// The [`Triangle3d`] implements the minimal bounding sphere calculation. For acute triangles, the circumcenter is used as
/// the center of the sphere. For the others, the bounding sphere is the minimal sphere
/// that contains the largest side of the triangle.
fn bounding_sphere(&self, isometry: impl Into<Isometry3d>) -> BoundingSphere {
let isometry = isometry.into();
if self.is_degenerate() || self.is_obtuse() {
let (p1, p2) = self.largest_side();
let (p1, p2) = (Vec3A::from(p1), Vec3A::from(p2));
let mid_point = (p1 + p2) / 2.0;
let radius = mid_point.distance(p1);
BoundingSphere::new(mid_point + isometry.translation, radius)
} else {
let [a, _, _] = self.vertices;
let circumcenter = self.circumcenter();
let radius = circumcenter.distance(a);
BoundingSphere::new(Vec3A::from(circumcenter) + isometry.translation, radius)
}
}
}
#[cfg(test)]
mod tests {
use crate::{bounding::BoundingVolume, ops, Isometry3d};
use glam::{Quat, Vec3, Vec3A};
use crate::{
bounding::Bounded3d,
primitives::{
Capsule3d, Cone, ConicalFrustum, Cuboid, Cylinder, InfinitePlane3d, Line3d, Polyline3d,
Segment3d, Sphere, Torus, Triangle3d,
},
Dir3,
};
#[test]
fn sphere() {
let sphere = Sphere { radius: 1.0 };
let translation = Vec3::new(2.0, 1.0, 0.0);
let aabb = sphere.aabb_3d(translation);
assert_eq!(aabb.min, Vec3A::new(1.0, 0.0, -1.0));
assert_eq!(aabb.max, Vec3A::new(3.0, 2.0, 1.0));
let bounding_sphere = sphere.bounding_sphere(translation);
assert_eq!(bounding_sphere.center, translation.into());
assert_eq!(bounding_sphere.radius(), 1.0);
}
#[test]
fn plane() {
let translation = Vec3::new(2.0, 1.0, 0.0);
let aabb1 = InfinitePlane3d::new(Vec3::X).aabb_3d(translation);
assert_eq!(aabb1.min, Vec3A::new(2.0, -f32::MAX / 2.0, -f32::MAX / 2.0));
assert_eq!(aabb1.max, Vec3A::new(2.0, f32::MAX / 2.0, f32::MAX / 2.0));
let aabb2 = InfinitePlane3d::new(Vec3::Y).aabb_3d(translation);
assert_eq!(aabb2.min, Vec3A::new(-f32::MAX / 2.0, 1.0, -f32::MAX / 2.0));
assert_eq!(aabb2.max, Vec3A::new(f32::MAX / 2.0, 1.0, f32::MAX / 2.0));
let aabb3 = InfinitePlane3d::new(Vec3::Z).aabb_3d(translation);
assert_eq!(aabb3.min, Vec3A::new(-f32::MAX / 2.0, -f32::MAX / 2.0, 0.0));
assert_eq!(aabb3.max, Vec3A::new(f32::MAX / 2.0, f32::MAX / 2.0, 0.0));
let aabb4 = InfinitePlane3d::new(Vec3::ONE).aabb_3d(translation);
assert_eq!(aabb4.min, Vec3A::splat(-f32::MAX / 2.0));
assert_eq!(aabb4.max, Vec3A::splat(f32::MAX / 2.0));
let bounding_sphere = InfinitePlane3d::new(Vec3::Y).bounding_sphere(translation);
assert_eq!(bounding_sphere.center, translation.into());
assert_eq!(bounding_sphere.radius(), f32::MAX / 2.0);
}
#[test]
fn line() {
let translation = Vec3::new(2.0, 1.0, 0.0);
let aabb1 = Line3d { direction: Dir3::Y }.aabb_3d(translation);
assert_eq!(aabb1.min, Vec3A::new(2.0, -f32::MAX / 2.0, 0.0));
assert_eq!(aabb1.max, Vec3A::new(2.0, f32::MAX / 2.0, 0.0));
let aabb2 = Line3d { direction: Dir3::X }.aabb_3d(translation);
assert_eq!(aabb2.min, Vec3A::new(-f32::MAX / 2.0, 1.0, 0.0));
assert_eq!(aabb2.max, Vec3A::new(f32::MAX / 2.0, 1.0, 0.0));
let aabb3 = Line3d { direction: Dir3::Z }.aabb_3d(translation);
assert_eq!(aabb3.min, Vec3A::new(2.0, 1.0, -f32::MAX / 2.0));
assert_eq!(aabb3.max, Vec3A::new(2.0, 1.0, f32::MAX / 2.0));
let aabb4 = Line3d {
direction: Dir3::from_xyz(1.0, 1.0, 1.0).unwrap(),
}
.aabb_3d(translation);
assert_eq!(aabb4.min, Vec3A::splat(-f32::MAX / 2.0));
assert_eq!(aabb4.max, Vec3A::splat(f32::MAX / 2.0));
let bounding_sphere = Line3d { direction: Dir3::Y }.bounding_sphere(translation);
assert_eq!(bounding_sphere.center, translation.into());
assert_eq!(bounding_sphere.radius(), f32::MAX / 2.0);
}
#[test]
fn segment() {
let segment = Segment3d::new(Vec3::new(-1.0, -0.5, 0.0), Vec3::new(1.0, 0.5, 0.0));
let translation = Vec3::new(2.0, 1.0, 0.0);
let aabb = segment.aabb_3d(translation);
assert_eq!(aabb.min, Vec3A::new(1.0, 0.5, 0.0));
assert_eq!(aabb.max, Vec3A::new(3.0, 1.5, 0.0));
let bounding_sphere = segment.bounding_sphere(translation);
assert_eq!(bounding_sphere.center, translation.into());
assert_eq!(bounding_sphere.radius(), ops::hypot(1.0, 0.5));
}
#[test]
fn polyline() {
let polyline = Polyline3d::<4>::new([
Vec3::ONE,
Vec3::new(-1.0, 1.0, 1.0),
Vec3::NEG_ONE,
Vec3::new(1.0, -1.0, -1.0),
]);
let translation = Vec3::new(2.0, 1.0, 0.0);
let aabb = polyline.aabb_3d(translation);
assert_eq!(aabb.min, Vec3A::new(1.0, 0.0, -1.0));
assert_eq!(aabb.max, Vec3A::new(3.0, 2.0, 1.0));
let bounding_sphere = polyline.bounding_sphere(translation);
assert_eq!(bounding_sphere.center, translation.into());
assert_eq!(
bounding_sphere.radius(),
ops::hypot(ops::hypot(1.0, 1.0), 1.0)
);
}
#[test]
fn cuboid() {
let cuboid = Cuboid::new(2.0, 1.0, 1.0);
let translation = Vec3::new(2.0, 1.0, 0.0);
let aabb = cuboid.aabb_3d(Isometry3d::new(
translation,
Quat::from_rotation_z(core::f32::consts::FRAC_PI_4),
));
let expected_half_size = Vec3A::new(1.0606601, 1.0606601, 0.5);
assert_eq!(aabb.min, Vec3A::from(translation) - expected_half_size);
assert_eq!(aabb.max, Vec3A::from(translation) + expected_half_size);
let bounding_sphere = cuboid.bounding_sphere(translation);
assert_eq!(bounding_sphere.center, translation.into());
assert_eq!(
bounding_sphere.radius(),
ops::hypot(ops::hypot(1.0, 0.5), 0.5)
);
}
#[test]
fn cylinder() {
let cylinder = Cylinder::new(0.5, 2.0);
let translation = Vec3::new(2.0, 1.0, 0.0);
let aabb = cylinder.aabb_3d(translation);
assert_eq!(
aabb.min,
Vec3A::from(translation) - Vec3A::new(0.5, 1.0, 0.5)
);
assert_eq!(
aabb.max,
Vec3A::from(translation) + Vec3A::new(0.5, 1.0, 0.5)
);
let bounding_sphere = cylinder.bounding_sphere(translation);
assert_eq!(bounding_sphere.center, translation.into());
assert_eq!(bounding_sphere.radius(), ops::hypot(1.0, 0.5));
}
#[test]
fn capsule() {
let capsule = Capsule3d::new(0.5, 2.0);
let translation = Vec3::new(2.0, 1.0, 0.0);
let aabb = capsule.aabb_3d(translation);
assert_eq!(
aabb.min,
Vec3A::from(translation) - Vec3A::new(0.5, 1.5, 0.5)
);
assert_eq!(
aabb.max,
Vec3A::from(translation) + Vec3A::new(0.5, 1.5, 0.5)
);
let bounding_sphere = capsule.bounding_sphere(translation);
assert_eq!(bounding_sphere.center, translation.into());
assert_eq!(bounding_sphere.radius(), 1.5);
}
#[test]
fn cone() {
let cone = Cone {
radius: 1.0,
height: 2.0,
};
let translation = Vec3::new(2.0, 1.0, 0.0);
let aabb = cone.aabb_3d(translation);
assert_eq!(aabb.min, Vec3A::new(1.0, 0.0, -1.0));
assert_eq!(aabb.max, Vec3A::new(3.0, 2.0, 1.0));
let bounding_sphere = cone.bounding_sphere(translation);
assert_eq!(
bounding_sphere.center,
Vec3A::from(translation) + Vec3A::NEG_Y * 0.25
);
assert_eq!(bounding_sphere.radius(), 1.25);
}
#[test]
fn conical_frustum() {
let conical_frustum = ConicalFrustum {
radius_top: 0.5,
radius_bottom: 1.0,
height: 2.0,
};
let translation = Vec3::new(2.0, 1.0, 0.0);
let aabb = conical_frustum.aabb_3d(translation);
assert_eq!(aabb.min, Vec3A::new(1.0, 0.0, -1.0));
assert_eq!(aabb.max, Vec3A::new(3.0, 2.0, 1.0));
let bounding_sphere = conical_frustum.bounding_sphere(translation);
assert_eq!(
bounding_sphere.center,
Vec3A::from(translation) + Vec3A::NEG_Y * 0.1875
);
assert_eq!(bounding_sphere.radius(), 1.2884705);
}
#[test]
fn wide_conical_frustum() {
let conical_frustum = ConicalFrustum {
radius_top: 0.5,
radius_bottom: 5.0,
height: 1.0,
};
let translation = Vec3::new(2.0, 1.0, 0.0);
let aabb = conical_frustum.aabb_3d(translation);
assert_eq!(aabb.min, Vec3A::new(-3.0, 0.5, -5.0));
assert_eq!(aabb.max, Vec3A::new(7.0, 1.5, 5.0));
// For wide conical frusta like this, the circumcenter can be outside the frustum,
// so the center and radius should be clamped to the longest side.
let bounding_sphere = conical_frustum.bounding_sphere(translation);
assert_eq!(
bounding_sphere.center,
Vec3A::from(translation) + Vec3A::NEG_Y * 0.5
);
assert_eq!(bounding_sphere.radius(), 5.0);
}
#[test]
fn torus() {
let torus = Torus {
minor_radius: 0.5,
major_radius: 1.0,
};
let translation = Vec3::new(2.0, 1.0, 0.0);
let aabb = torus.aabb_3d(translation);
assert_eq!(aabb.min, Vec3A::new(0.5, 0.5, -1.5));
assert_eq!(aabb.max, Vec3A::new(3.5, 1.5, 1.5));
let bounding_sphere = torus.bounding_sphere(translation);
assert_eq!(bounding_sphere.center, translation.into());
assert_eq!(bounding_sphere.radius(), 1.5);
}
#[test]
fn triangle3d() {
let zero_degenerate_triangle = Triangle3d::new(Vec3::ZERO, Vec3::ZERO, Vec3::ZERO);
let br = zero_degenerate_triangle.aabb_3d(Isometry3d::IDENTITY);
assert_eq!(
br.center(),
Vec3::ZERO.into(),
"incorrect bounding box center"
);
assert_eq!(
br.half_size(),
Vec3::ZERO.into(),
"incorrect bounding box half extents"
);
let bs = zero_degenerate_triangle.bounding_sphere(Isometry3d::IDENTITY);
assert_eq!(
bs.center,
Vec3::ZERO.into(),
"incorrect bounding sphere center"
);
assert_eq!(bs.sphere.radius, 0.0, "incorrect bounding sphere radius");
let dup_degenerate_triangle = Triangle3d::new(Vec3::ZERO, Vec3::X, Vec3::X);
let bs = dup_degenerate_triangle.bounding_sphere(Isometry3d::IDENTITY);
assert_eq!(
bs.center,
Vec3::new(0.5, 0.0, 0.0).into(),
"incorrect bounding sphere center"
);
assert_eq!(bs.sphere.radius, 0.5, "incorrect bounding sphere radius");
let br = dup_degenerate_triangle.aabb_3d(Isometry3d::IDENTITY);
assert_eq!(
br.center(),
Vec3::new(0.5, 0.0, 0.0).into(),
"incorrect bounding box center"
);
assert_eq!(
br.half_size(),
Vec3::new(0.5, 0.0, 0.0).into(),
"incorrect bounding box half extents"
);
let collinear_degenerate_triangle = Triangle3d::new(Vec3::NEG_X, Vec3::ZERO, Vec3::X);
let bs = collinear_degenerate_triangle.bounding_sphere(Isometry3d::IDENTITY);
assert_eq!(
bs.center,
Vec3::ZERO.into(),
"incorrect bounding sphere center"
);
assert_eq!(bs.sphere.radius, 1.0, "incorrect bounding sphere radius");
let br = collinear_degenerate_triangle.aabb_3d(Isometry3d::IDENTITY);
assert_eq!(
br.center(),
Vec3::ZERO.into(),
"incorrect bounding box center"
);
assert_eq!(
br.half_size(),
Vec3::new(1.0, 0.0, 0.0).into(),
"incorrect bounding box half extents"
);
}
}

117
vendor/bevy_math/src/bounding/mod.rs vendored Normal file
View File

@@ -0,0 +1,117 @@
//! 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: Sized {
/// The position type used for the volume. This should be `Vec2` for 2D and `Vec3` for 3D.
type Translation: Clone + Copy + PartialEq;
/// The rotation type used for the volume. This should be `Rot2` for 2D and `Quat` for 3D.
type Rotation: 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::Translation;
/// 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;
/// Increases the size of the bounding volume in each direction by the given amount.
fn grow(&self, amount: impl Into<Self::HalfSize>) -> Self;
/// Decreases the size of the bounding volume in each direction by the given amount.
fn shrink(&self, amount: impl Into<Self::HalfSize>) -> Self;
/// Scale the size of the bounding volume around its center by the given amount
fn scale_around_center(&self, scale: impl Into<Self::HalfSize>) -> Self;
/// Transforms the bounding volume by first rotating it around the origin and then applying a translation.
fn transformed_by(
mut self,
translation: impl Into<Self::Translation>,
rotation: impl Into<Self::Rotation>,
) -> Self {
self.transform_by(translation, rotation);
self
}
/// Transforms the bounding volume by first rotating it around the origin and then applying a translation.
fn transform_by(
&mut self,
translation: impl Into<Self::Translation>,
rotation: impl Into<Self::Rotation>,
) {
self.rotate_by(rotation);
self.translate_by(translation);
}
/// Translates the bounding volume by the given translation.
fn translated_by(mut self, translation: impl Into<Self::Translation>) -> Self {
self.translate_by(translation);
self
}
/// Translates the bounding volume by the given translation.
fn translate_by(&mut self, translation: impl Into<Self::Translation>);
/// Rotates the bounding volume around the origin by the given rotation.
///
/// The result is a combination of the original volume and the rotated volume,
/// so it is guaranteed to be either the same size or larger than the original.
fn rotated_by(mut self, rotation: impl Into<Self::Rotation>) -> Self {
self.rotate_by(rotation);
self
}
/// Rotates the bounding volume around the origin by the given rotation.
///
/// The result is a combination of the original volume and the rotated volume,
/// so it is guaranteed to be either the same size or larger than the original.
fn rotate_by(&mut self, rotation: impl Into<Self::Rotation>);
}
/// 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::*;
mod raycast2d;
pub use raycast2d::*;
mod raycast3d;
pub use raycast3d::*;

View File

@@ -0,0 +1,536 @@
use super::{Aabb2d, BoundingCircle, IntersectsVolume};
use crate::{
ops::{self, FloatPow},
Dir2, Ray2d, Vec2,
};
#[cfg(feature = "bevy_reflect")]
use bevy_reflect::Reflect;
/// A raycast intersection test for 2D bounding volumes
#[derive(Clone, Debug)]
#[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Debug, Clone))]
pub struct RayCast2d {
/// The ray for the test
pub ray: Ray2d,
/// The maximum distance for the ray
pub max: f32,
/// The multiplicative inverse direction of the ray
direction_recip: Vec2,
}
impl RayCast2d {
/// Construct a [`RayCast2d`] from an origin, [`Dir2`], and max distance.
pub fn new(origin: Vec2, direction: Dir2, max: f32) -> Self {
Self::from_ray(Ray2d { origin, direction }, max)
}
/// Construct a [`RayCast2d`] from a [`Ray2d`] and max distance.
pub fn from_ray(ray: Ray2d, max: f32) -> Self {
Self {
ray,
direction_recip: ray.direction.recip(),
max,
}
}
/// Get the cached multiplicative inverse of the direction of the ray.
pub fn direction_recip(&self) -> Vec2 {
self.direction_recip
}
/// Get the distance of an intersection with an [`Aabb2d`], if any.
pub fn aabb_intersection_at(&self, aabb: &Aabb2d) -> Option<f32> {
let (min_x, max_x) = if self.ray.direction.x.is_sign_positive() {
(aabb.min.x, aabb.max.x)
} else {
(aabb.max.x, aabb.min.x)
};
let (min_y, max_y) = if self.ray.direction.y.is_sign_positive() {
(aabb.min.y, aabb.max.y)
} else {
(aabb.max.y, aabb.min.y)
};
// Calculate the minimum/maximum time for each axis based on how much the direction goes that
// way. These values can get arbitrarily large, or even become NaN, which is handled by the
// min/max operations below
let tmin_x = (min_x - self.ray.origin.x) * self.direction_recip.x;
let tmin_y = (min_y - self.ray.origin.y) * self.direction_recip.y;
let tmax_x = (max_x - self.ray.origin.x) * self.direction_recip.x;
let tmax_y = (max_y - self.ray.origin.y) * self.direction_recip.y;
// An axis that is not relevant to the ray direction will be NaN. When one of the arguments
// to min/max is NaN, the other argument is used.
// An axis for which the direction is the wrong way will return an arbitrarily large
// negative value.
let tmin = tmin_x.max(tmin_y).max(0.);
let tmax = tmax_y.min(tmax_x).min(self.max);
if tmin <= tmax {
Some(tmin)
} else {
None
}
}
/// Get the distance of an intersection with a [`BoundingCircle`], if any.
pub fn circle_intersection_at(&self, circle: &BoundingCircle) -> Option<f32> {
let offset = self.ray.origin - circle.center;
let projected = offset.dot(*self.ray.direction);
let closest_point = offset - projected * *self.ray.direction;
let distance_squared = circle.radius().squared() - closest_point.length_squared();
if distance_squared < 0.
|| ops::copysign(projected.squared(), -projected) < -distance_squared
{
None
} else {
let toi = -projected - ops::sqrt(distance_squared);
if toi > self.max {
None
} else {
Some(toi.max(0.))
}
}
}
}
impl IntersectsVolume<Aabb2d> for RayCast2d {
fn intersects(&self, volume: &Aabb2d) -> bool {
self.aabb_intersection_at(volume).is_some()
}
}
impl IntersectsVolume<BoundingCircle> for RayCast2d {
fn intersects(&self, volume: &BoundingCircle) -> bool {
self.circle_intersection_at(volume).is_some()
}
}
/// An intersection test that casts an [`Aabb2d`] along a ray.
#[derive(Clone, Debug)]
#[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Debug, Clone))]
pub struct AabbCast2d {
/// The ray along which to cast the bounding volume
pub ray: RayCast2d,
/// The aabb that is being cast
pub aabb: Aabb2d,
}
impl AabbCast2d {
/// Construct an [`AabbCast2d`] from an [`Aabb2d`], origin, [`Dir2`], and max distance.
pub fn new(aabb: Aabb2d, origin: Vec2, direction: Dir2, max: f32) -> Self {
Self::from_ray(aabb, Ray2d { origin, direction }, max)
}
/// Construct an [`AabbCast2d`] from an [`Aabb2d`], [`Ray2d`], and max distance.
pub fn from_ray(aabb: Aabb2d, ray: Ray2d, max: f32) -> Self {
Self {
ray: RayCast2d::from_ray(ray, max),
aabb,
}
}
/// Get the distance at which the [`Aabb2d`]s collide, if at all.
pub fn aabb_collision_at(&self, mut aabb: Aabb2d) -> Option<f32> {
aabb.min -= self.aabb.max;
aabb.max -= self.aabb.min;
self.ray.aabb_intersection_at(&aabb)
}
}
impl IntersectsVolume<Aabb2d> for AabbCast2d {
fn intersects(&self, volume: &Aabb2d) -> bool {
self.aabb_collision_at(*volume).is_some()
}
}
/// An intersection test that casts a [`BoundingCircle`] along a ray.
#[derive(Clone, Debug)]
#[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Debug, Clone))]
pub struct BoundingCircleCast {
/// The ray along which to cast the bounding volume
pub ray: RayCast2d,
/// The circle that is being cast
pub circle: BoundingCircle,
}
impl BoundingCircleCast {
/// Construct a [`BoundingCircleCast`] from a [`BoundingCircle`], origin, [`Dir2`], and max distance.
pub fn new(circle: BoundingCircle, origin: Vec2, direction: Dir2, max: f32) -> Self {
Self::from_ray(circle, Ray2d { origin, direction }, max)
}
/// Construct a [`BoundingCircleCast`] from a [`BoundingCircle`], [`Ray2d`], and max distance.
pub fn from_ray(circle: BoundingCircle, ray: Ray2d, max: f32) -> Self {
Self {
ray: RayCast2d::from_ray(ray, max),
circle,
}
}
/// Get the distance at which the [`BoundingCircle`]s collide, if at all.
pub fn circle_collision_at(&self, mut circle: BoundingCircle) -> Option<f32> {
circle.center -= self.circle.center;
circle.circle.radius += self.circle.radius();
self.ray.circle_intersection_at(&circle)
}
}
impl IntersectsVolume<BoundingCircle> for BoundingCircleCast {
fn intersects(&self, volume: &BoundingCircle) -> bool {
self.circle_collision_at(*volume).is_some()
}
}
#[cfg(test)]
mod tests {
use super::*;
const EPSILON: f32 = 0.001;
#[test]
fn test_ray_intersection_circle_hits() {
for (test, volume, expected_distance) in &[
(
// Hit the center of a centered bounding circle
RayCast2d::new(Vec2::Y * -5., Dir2::Y, 90.),
BoundingCircle::new(Vec2::ZERO, 1.),
4.,
),
(
// Hit the center of a centered bounding circle, but from the other side
RayCast2d::new(Vec2::Y * 5., -Dir2::Y, 90.),
BoundingCircle::new(Vec2::ZERO, 1.),
4.,
),
(
// Hit the center of an offset circle
RayCast2d::new(Vec2::ZERO, Dir2::Y, 90.),
BoundingCircle::new(Vec2::Y * 3., 2.),
1.,
),
(
// Just barely hit the circle before the max distance
RayCast2d::new(Vec2::X, Dir2::Y, 1.),
BoundingCircle::new(Vec2::ONE, 0.01),
0.99,
),
(
// Hit a circle off-center
RayCast2d::new(Vec2::X, Dir2::Y, 90.),
BoundingCircle::new(Vec2::Y * 5., 2.),
3.268,
),
(
// Barely hit a circle on the side
RayCast2d::new(Vec2::X * 0.99999, Dir2::Y, 90.),
BoundingCircle::new(Vec2::Y * 5., 1.),
4.996,
),
] {
assert!(
test.intersects(volume),
"Case:\n Test: {test:?}\n Volume: {volume:?}\n Expected distance: {expected_distance:?}",
);
let actual_distance = test.circle_intersection_at(volume).unwrap();
assert!(
ops::abs(actual_distance - expected_distance) < EPSILON,
"Case:\n Test: {test:?}\n Volume: {volume:?}\n Expected distance: {expected_distance:?}\n Actual distance: {actual_distance}",
);
let inverted_ray = RayCast2d::new(test.ray.origin, -test.ray.direction, test.max);
assert!(
!inverted_ray.intersects(volume),
"Case:\n Test: {test:?}\n Volume: {volume:?}\n Expected distance: {expected_distance:?}",
);
}
}
#[test]
fn test_ray_intersection_circle_misses() {
for (test, volume) in &[
(
// The ray doesn't go in the right direction
RayCast2d::new(Vec2::ZERO, Dir2::X, 90.),
BoundingCircle::new(Vec2::Y * 2., 1.),
),
(
// Ray's alignment isn't enough to hit the circle
RayCast2d::new(Vec2::ZERO, Dir2::from_xy(1., 1.).unwrap(), 90.),
BoundingCircle::new(Vec2::Y * 2., 1.),
),
(
// The ray's maximum distance isn't high enough
RayCast2d::new(Vec2::ZERO, Dir2::Y, 0.5),
BoundingCircle::new(Vec2::Y * 2., 1.),
),
] {
assert!(
!test.intersects(volume),
"Case:\n Test: {test:?}\n Volume: {volume:?}",
);
}
}
#[test]
fn test_ray_intersection_circle_inside() {
let volume = BoundingCircle::new(Vec2::splat(0.5), 1.);
for origin in &[Vec2::X, Vec2::Y, Vec2::ONE, Vec2::ZERO] {
for direction in &[Dir2::X, Dir2::Y, -Dir2::X, -Dir2::Y] {
for max in &[0., 1., 900.] {
let test = RayCast2d::new(*origin, *direction, *max);
assert!(
test.intersects(&volume),
"Case:\n origin: {origin:?}\n Direction: {direction:?}\n Max: {max}",
);
let actual_distance = test.circle_intersection_at(&volume);
assert_eq!(
actual_distance,
Some(0.),
"Case:\n origin: {origin:?}\n Direction: {direction:?}\n Max: {max}",
);
}
}
}
}
#[test]
fn test_ray_intersection_aabb_hits() {
for (test, volume, expected_distance) in &[
(
// Hit the center of a centered aabb
RayCast2d::new(Vec2::Y * -5., Dir2::Y, 90.),
Aabb2d::new(Vec2::ZERO, Vec2::ONE),
4.,
),
(
// Hit the center of a centered aabb, but from the other side
RayCast2d::new(Vec2::Y * 5., -Dir2::Y, 90.),
Aabb2d::new(Vec2::ZERO, Vec2::ONE),
4.,
),
(
// Hit the center of an offset aabb
RayCast2d::new(Vec2::ZERO, Dir2::Y, 90.),
Aabb2d::new(Vec2::Y * 3., Vec2::splat(2.)),
1.,
),
(
// Just barely hit the aabb before the max distance
RayCast2d::new(Vec2::X, Dir2::Y, 1.),
Aabb2d::new(Vec2::ONE, Vec2::splat(0.01)),
0.99,
),
(
// Hit an aabb off-center
RayCast2d::new(Vec2::X, Dir2::Y, 90.),
Aabb2d::new(Vec2::Y * 5., Vec2::splat(2.)),
3.,
),
(
// Barely hit an aabb on corner
RayCast2d::new(Vec2::X * -0.001, Dir2::from_xy(1., 1.).unwrap(), 90.),
Aabb2d::new(Vec2::Y * 2., Vec2::ONE),
1.414,
),
] {
assert!(
test.intersects(volume),
"Case:\n Test: {test:?}\n Volume: {volume:?}\n Expected distance: {expected_distance:?}",
);
let actual_distance = test.aabb_intersection_at(volume).unwrap();
assert!(
ops::abs(actual_distance - expected_distance) < EPSILON,
"Case:\n Test: {test:?}\n Volume: {volume:?}\n Expected distance: {expected_distance:?}\n Actual distance: {actual_distance}",
);
let inverted_ray = RayCast2d::new(test.ray.origin, -test.ray.direction, test.max);
assert!(
!inverted_ray.intersects(volume),
"Case:\n Test: {test:?}\n Volume: {volume:?}\n Expected distance: {expected_distance:?}",
);
}
}
#[test]
fn test_ray_intersection_aabb_misses() {
for (test, volume) in &[
(
// The ray doesn't go in the right direction
RayCast2d::new(Vec2::ZERO, Dir2::X, 90.),
Aabb2d::new(Vec2::Y * 2., Vec2::ONE),
),
(
// Ray's alignment isn't enough to hit the aabb
RayCast2d::new(Vec2::ZERO, Dir2::from_xy(1., 0.99).unwrap(), 90.),
Aabb2d::new(Vec2::Y * 2., Vec2::ONE),
),
(
// The ray's maximum distance isn't high enough
RayCast2d::new(Vec2::ZERO, Dir2::Y, 0.5),
Aabb2d::new(Vec2::Y * 2., Vec2::ONE),
),
] {
assert!(
!test.intersects(volume),
"Case:\n Test: {test:?}\n Volume: {volume:?}",
);
}
}
#[test]
fn test_ray_intersection_aabb_inside() {
let volume = Aabb2d::new(Vec2::splat(0.5), Vec2::ONE);
for origin in &[Vec2::X, Vec2::Y, Vec2::ONE, Vec2::ZERO] {
for direction in &[Dir2::X, Dir2::Y, -Dir2::X, -Dir2::Y] {
for max in &[0., 1., 900.] {
let test = RayCast2d::new(*origin, *direction, *max);
assert!(
test.intersects(&volume),
"Case:\n origin: {origin:?}\n Direction: {direction:?}\n Max: {max}",
);
let actual_distance = test.aabb_intersection_at(&volume);
assert_eq!(
actual_distance,
Some(0.),
"Case:\n origin: {origin:?}\n Direction: {direction:?}\n Max: {max}",
);
}
}
}
}
#[test]
fn test_aabb_cast_hits() {
for (test, volume, expected_distance) in &[
(
// Hit the center of the aabb, that a ray would've also hit
AabbCast2d::new(Aabb2d::new(Vec2::ZERO, Vec2::ONE), Vec2::ZERO, Dir2::Y, 90.),
Aabb2d::new(Vec2::Y * 5., Vec2::ONE),
3.,
),
(
// Hit the center of the aabb, but from the other side
AabbCast2d::new(
Aabb2d::new(Vec2::ZERO, Vec2::ONE),
Vec2::Y * 10.,
-Dir2::Y,
90.,
),
Aabb2d::new(Vec2::Y * 5., Vec2::ONE),
3.,
),
(
// Hit the edge of the aabb, that a ray would've missed
AabbCast2d::new(
Aabb2d::new(Vec2::ZERO, Vec2::ONE),
Vec2::X * 1.5,
Dir2::Y,
90.,
),
Aabb2d::new(Vec2::Y * 5., Vec2::ONE),
3.,
),
(
// Hit the edge of the aabb, by casting an off-center AABB
AabbCast2d::new(
Aabb2d::new(Vec2::X * -2., Vec2::ONE),
Vec2::X * 3.,
Dir2::Y,
90.,
),
Aabb2d::new(Vec2::Y * 5., Vec2::ONE),
3.,
),
] {
assert!(
test.intersects(volume),
"Case:\n Test: {test:?}\n Volume: {volume:?}\n Expected distance: {expected_distance:?}",
);
let actual_distance = test.aabb_collision_at(*volume).unwrap();
assert!(
ops::abs(actual_distance - expected_distance) < EPSILON,
"Case:\n Test: {test:?}\n Volume: {volume:?}\n Expected distance: {expected_distance:?}\n Actual distance: {actual_distance}",
);
let inverted_ray =
RayCast2d::new(test.ray.ray.origin, -test.ray.ray.direction, test.ray.max);
assert!(
!inverted_ray.intersects(volume),
"Case:\n Test: {test:?}\n Volume: {volume:?}\n Expected distance: {expected_distance:?}",
);
}
}
#[test]
fn test_circle_cast_hits() {
for (test, volume, expected_distance) in &[
(
// Hit the center of the bounding circle, that a ray would've also hit
BoundingCircleCast::new(
BoundingCircle::new(Vec2::ZERO, 1.),
Vec2::ZERO,
Dir2::Y,
90.,
),
BoundingCircle::new(Vec2::Y * 5., 1.),
3.,
),
(
// Hit the center of the bounding circle, but from the other side
BoundingCircleCast::new(
BoundingCircle::new(Vec2::ZERO, 1.),
Vec2::Y * 10.,
-Dir2::Y,
90.,
),
BoundingCircle::new(Vec2::Y * 5., 1.),
3.,
),
(
// Hit the bounding circle off-center, that a ray would've missed
BoundingCircleCast::new(
BoundingCircle::new(Vec2::ZERO, 1.),
Vec2::X * 1.5,
Dir2::Y,
90.,
),
BoundingCircle::new(Vec2::Y * 5., 1.),
3.677,
),
(
// Hit the bounding circle off-center, by casting a circle that is off-center
BoundingCircleCast::new(
BoundingCircle::new(Vec2::X * -1.5, 1.),
Vec2::X * 3.,
Dir2::Y,
90.,
),
BoundingCircle::new(Vec2::Y * 5., 1.),
3.677,
),
] {
assert!(
test.intersects(volume),
"Case:\n Test: {test:?}\n Volume: {volume:?}\n Expected distance: {expected_distance:?}",
);
let actual_distance = test.circle_collision_at(*volume).unwrap();
assert!(
ops::abs(actual_distance - expected_distance) < EPSILON,
"Case:\n Test: {test:?}\n Volume: {volume:?}\n Expected distance: {expected_distance:?}\n Actual distance: {actual_distance}",
);
let inverted_ray =
RayCast2d::new(test.ray.ray.origin, -test.ray.ray.direction, test.ray.max);
assert!(
!inverted_ray.intersects(volume),
"Case:\n Test: {test:?}\n Volume: {volume:?}\n Expected distance: {expected_distance:?}",
);
}
}
}

View File

@@ -0,0 +1,546 @@
use super::{Aabb3d, BoundingSphere, IntersectsVolume};
use crate::{
ops::{self, FloatPow},
Dir3A, Ray3d, Vec3A,
};
#[cfg(feature = "bevy_reflect")]
use bevy_reflect::Reflect;
/// A raycast intersection test for 3D bounding volumes
#[derive(Clone, Debug)]
#[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Debug, Clone))]
pub struct RayCast3d {
/// The origin of the ray.
pub origin: Vec3A,
/// The direction of the ray.
pub direction: Dir3A,
/// The maximum distance for the ray
pub max: f32,
/// The multiplicative inverse direction of the ray
direction_recip: Vec3A,
}
impl RayCast3d {
/// Construct a [`RayCast3d`] from an origin, [direction], and max distance.
///
/// [direction]: crate::direction::Dir3
pub fn new(origin: impl Into<Vec3A>, direction: impl Into<Dir3A>, max: f32) -> Self {
let direction = direction.into();
Self {
origin: origin.into(),
direction,
direction_recip: direction.recip(),
max,
}
}
/// Construct a [`RayCast3d`] from a [`Ray3d`] and max distance.
pub fn from_ray(ray: Ray3d, max: f32) -> Self {
Self::new(ray.origin, ray.direction, max)
}
/// Get the cached multiplicative inverse of the direction of the ray.
pub fn direction_recip(&self) -> Vec3A {
self.direction_recip
}
/// Get the distance of an intersection with an [`Aabb3d`], if any.
pub fn aabb_intersection_at(&self, aabb: &Aabb3d) -> Option<f32> {
let positive = self.direction.signum().cmpgt(Vec3A::ZERO);
let min = Vec3A::select(positive, aabb.min, aabb.max);
let max = Vec3A::select(positive, aabb.max, aabb.min);
// Calculate the minimum/maximum time for each axis based on how much the direction goes that
// way. These values can get arbitrarily large, or even become NaN, which is handled by the
// min/max operations below
let tmin = (min - self.origin) * self.direction_recip;
let tmax = (max - self.origin) * self.direction_recip;
// An axis that is not relevant to the ray direction will be NaN. When one of the arguments
// to min/max is NaN, the other argument is used.
// An axis for which the direction is the wrong way will return an arbitrarily large
// negative value.
let tmin = tmin.max_element().max(0.);
let tmax = tmax.min_element().min(self.max);
if tmin <= tmax {
Some(tmin)
} else {
None
}
}
/// Get the distance of an intersection with a [`BoundingSphere`], if any.
pub fn sphere_intersection_at(&self, sphere: &BoundingSphere) -> Option<f32> {
let offset = self.origin - sphere.center;
let projected = offset.dot(*self.direction);
let closest_point = offset - projected * *self.direction;
let distance_squared = sphere.radius().squared() - closest_point.length_squared();
if distance_squared < 0.
|| ops::copysign(projected.squared(), -projected) < -distance_squared
{
None
} else {
let toi = -projected - ops::sqrt(distance_squared);
if toi > self.max {
None
} else {
Some(toi.max(0.))
}
}
}
}
impl IntersectsVolume<Aabb3d> for RayCast3d {
fn intersects(&self, volume: &Aabb3d) -> bool {
self.aabb_intersection_at(volume).is_some()
}
}
impl IntersectsVolume<BoundingSphere> for RayCast3d {
fn intersects(&self, volume: &BoundingSphere) -> bool {
self.sphere_intersection_at(volume).is_some()
}
}
/// An intersection test that casts an [`Aabb3d`] along a ray.
#[derive(Clone, Debug)]
#[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Debug, Clone))]
pub struct AabbCast3d {
/// The ray along which to cast the bounding volume
pub ray: RayCast3d,
/// The aabb that is being cast
pub aabb: Aabb3d,
}
impl AabbCast3d {
/// Construct an [`AabbCast3d`] from an [`Aabb3d`], origin, [direction], and max distance.
///
/// [direction]: crate::direction::Dir3
pub fn new(
aabb: Aabb3d,
origin: impl Into<Vec3A>,
direction: impl Into<Dir3A>,
max: f32,
) -> Self {
Self {
ray: RayCast3d::new(origin, direction, max),
aabb,
}
}
/// Construct an [`AabbCast3d`] from an [`Aabb3d`], [`Ray3d`], and max distance.
pub fn from_ray(aabb: Aabb3d, ray: Ray3d, max: f32) -> Self {
Self::new(aabb, ray.origin, ray.direction, max)
}
/// Get the distance at which the [`Aabb3d`]s collide, if at all.
pub fn aabb_collision_at(&self, mut aabb: Aabb3d) -> Option<f32> {
aabb.min -= self.aabb.max;
aabb.max -= self.aabb.min;
self.ray.aabb_intersection_at(&aabb)
}
}
impl IntersectsVolume<Aabb3d> for AabbCast3d {
fn intersects(&self, volume: &Aabb3d) -> bool {
self.aabb_collision_at(*volume).is_some()
}
}
/// An intersection test that casts a [`BoundingSphere`] along a ray.
#[derive(Clone, Debug)]
#[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Debug, Clone))]
pub struct BoundingSphereCast {
/// The ray along which to cast the bounding volume
pub ray: RayCast3d,
/// The sphere that is being cast
pub sphere: BoundingSphere,
}
impl BoundingSphereCast {
/// Construct a [`BoundingSphereCast`] from a [`BoundingSphere`], origin, [direction], and max distance.
///
/// [direction]: crate::direction::Dir3
pub fn new(
sphere: BoundingSphere,
origin: impl Into<Vec3A>,
direction: impl Into<Dir3A>,
max: f32,
) -> Self {
Self {
ray: RayCast3d::new(origin, direction, max),
sphere,
}
}
/// Construct a [`BoundingSphereCast`] from a [`BoundingSphere`], [`Ray3d`], and max distance.
pub fn from_ray(sphere: BoundingSphere, ray: Ray3d, max: f32) -> Self {
Self::new(sphere, ray.origin, ray.direction, max)
}
/// Get the distance at which the [`BoundingSphere`]s collide, if at all.
pub fn sphere_collision_at(&self, mut sphere: BoundingSphere) -> Option<f32> {
sphere.center -= self.sphere.center;
sphere.sphere.radius += self.sphere.radius();
self.ray.sphere_intersection_at(&sphere)
}
}
impl IntersectsVolume<BoundingSphere> for BoundingSphereCast {
fn intersects(&self, volume: &BoundingSphere) -> bool {
self.sphere_collision_at(*volume).is_some()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{Dir3, Vec3};
const EPSILON: f32 = 0.001;
#[test]
fn test_ray_intersection_sphere_hits() {
for (test, volume, expected_distance) in &[
(
// Hit the center of a centered bounding sphere
RayCast3d::new(Vec3::Y * -5., Dir3::Y, 90.),
BoundingSphere::new(Vec3::ZERO, 1.),
4.,
),
(
// Hit the center of a centered bounding sphere, but from the other side
RayCast3d::new(Vec3::Y * 5., -Dir3::Y, 90.),
BoundingSphere::new(Vec3::ZERO, 1.),
4.,
),
(
// Hit the center of an offset sphere
RayCast3d::new(Vec3::ZERO, Dir3::Y, 90.),
BoundingSphere::new(Vec3::Y * 3., 2.),
1.,
),
(
// Just barely hit the sphere before the max distance
RayCast3d::new(Vec3::X, Dir3::Y, 1.),
BoundingSphere::new(Vec3::new(1., 1., 0.), 0.01),
0.99,
),
(
// Hit a sphere off-center
RayCast3d::new(Vec3::X, Dir3::Y, 90.),
BoundingSphere::new(Vec3::Y * 5., 2.),
3.268,
),
(
// Barely hit a sphere on the side
RayCast3d::new(Vec3::X * 0.99999, Dir3::Y, 90.),
BoundingSphere::new(Vec3::Y * 5., 1.),
4.996,
),
] {
assert!(
test.intersects(volume),
"Case:\n Test: {test:?}\n Volume: {volume:?}\n Expected distance: {expected_distance:?}",
);
let actual_distance = test.sphere_intersection_at(volume).unwrap();
assert!(
ops::abs(actual_distance - expected_distance) < EPSILON,
"Case:\n Test: {test:?}\n Volume: {volume:?}\n Expected distance: {expected_distance:?}\n Actual distance: {actual_distance}",
);
let inverted_ray = RayCast3d::new(test.origin, -test.direction, test.max);
assert!(
!inverted_ray.intersects(volume),
"Case:\n Test: {test:?}\n Volume: {volume:?}\n Expected distance: {expected_distance:?}",
);
}
}
#[test]
fn test_ray_intersection_sphere_misses() {
for (test, volume) in &[
(
// The ray doesn't go in the right direction
RayCast3d::new(Vec3::ZERO, Dir3::X, 90.),
BoundingSphere::new(Vec3::Y * 2., 1.),
),
(
// Ray's alignment isn't enough to hit the sphere
RayCast3d::new(Vec3::ZERO, Dir3::from_xyz(1., 1., 1.).unwrap(), 90.),
BoundingSphere::new(Vec3::Y * 2., 1.),
),
(
// The ray's maximum distance isn't high enough
RayCast3d::new(Vec3::ZERO, Dir3::Y, 0.5),
BoundingSphere::new(Vec3::Y * 2., 1.),
),
] {
assert!(
!test.intersects(volume),
"Case:\n Test: {test:?}\n Volume: {volume:?}",
);
}
}
#[test]
fn test_ray_intersection_sphere_inside() {
let volume = BoundingSphere::new(Vec3::splat(0.5), 1.);
for origin in &[Vec3::X, Vec3::Y, Vec3::ONE, Vec3::ZERO] {
for direction in &[Dir3::X, Dir3::Y, Dir3::Z, -Dir3::X, -Dir3::Y, -Dir3::Z] {
for max in &[0., 1., 900.] {
let test = RayCast3d::new(*origin, *direction, *max);
assert!(
test.intersects(&volume),
"Case:\n origin: {origin:?}\n Direction: {direction:?}\n Max: {max}",
);
let actual_distance = test.sphere_intersection_at(&volume);
assert_eq!(
actual_distance,
Some(0.),
"Case:\n origin: {origin:?}\n Direction: {direction:?}\n Max: {max}",
);
}
}
}
}
#[test]
fn test_ray_intersection_aabb_hits() {
for (test, volume, expected_distance) in &[
(
// Hit the center of a centered aabb
RayCast3d::new(Vec3::Y * -5., Dir3::Y, 90.),
Aabb3d::new(Vec3::ZERO, Vec3::ONE),
4.,
),
(
// Hit the center of a centered aabb, but from the other side
RayCast3d::new(Vec3::Y * 5., -Dir3::Y, 90.),
Aabb3d::new(Vec3::ZERO, Vec3::ONE),
4.,
),
(
// Hit the center of an offset aabb
RayCast3d::new(Vec3::ZERO, Dir3::Y, 90.),
Aabb3d::new(Vec3::Y * 3., Vec3::splat(2.)),
1.,
),
(
// Just barely hit the aabb before the max distance
RayCast3d::new(Vec3::X, Dir3::Y, 1.),
Aabb3d::new(Vec3::new(1., 1., 0.), Vec3::splat(0.01)),
0.99,
),
(
// Hit an aabb off-center
RayCast3d::new(Vec3::X, Dir3::Y, 90.),
Aabb3d::new(Vec3::Y * 5., Vec3::splat(2.)),
3.,
),
(
// Barely hit an aabb on corner
RayCast3d::new(Vec3::X * -0.001, Dir3::from_xyz(1., 1., 1.).unwrap(), 90.),
Aabb3d::new(Vec3::Y * 2., Vec3::ONE),
1.732,
),
] {
assert!(
test.intersects(volume),
"Case:\n Test: {test:?}\n Volume: {volume:?}\n Expected distance: {expected_distance:?}",
);
let actual_distance = test.aabb_intersection_at(volume).unwrap();
assert!(
ops::abs(actual_distance - expected_distance) < EPSILON,
"Case:\n Test: {test:?}\n Volume: {volume:?}\n Expected distance: {expected_distance:?}\n Actual distance: {actual_distance}",
);
let inverted_ray = RayCast3d::new(test.origin, -test.direction, test.max);
assert!(
!inverted_ray.intersects(volume),
"Case:\n Test: {test:?}\n Volume: {volume:?}\n Expected distance: {expected_distance:?}",
);
}
}
#[test]
fn test_ray_intersection_aabb_misses() {
for (test, volume) in &[
(
// The ray doesn't go in the right direction
RayCast3d::new(Vec3::ZERO, Dir3::X, 90.),
Aabb3d::new(Vec3::Y * 2., Vec3::ONE),
),
(
// Ray's alignment isn't enough to hit the aabb
RayCast3d::new(Vec3::ZERO, Dir3::from_xyz(1., 0.99, 1.).unwrap(), 90.),
Aabb3d::new(Vec3::Y * 2., Vec3::ONE),
),
(
// The ray's maximum distance isn't high enough
RayCast3d::new(Vec3::ZERO, Dir3::Y, 0.5),
Aabb3d::new(Vec3::Y * 2., Vec3::ONE),
),
] {
assert!(
!test.intersects(volume),
"Case:\n Test: {test:?}\n Volume: {volume:?}",
);
}
}
#[test]
fn test_ray_intersection_aabb_inside() {
let volume = Aabb3d::new(Vec3::splat(0.5), Vec3::ONE);
for origin in &[Vec3::X, Vec3::Y, Vec3::ONE, Vec3::ZERO] {
for direction in &[Dir3::X, Dir3::Y, Dir3::Z, -Dir3::X, -Dir3::Y, -Dir3::Z] {
for max in &[0., 1., 900.] {
let test = RayCast3d::new(*origin, *direction, *max);
assert!(
test.intersects(&volume),
"Case:\n origin: {origin:?}\n Direction: {direction:?}\n Max: {max}",
);
let actual_distance = test.aabb_intersection_at(&volume);
assert_eq!(
actual_distance,
Some(0.),
"Case:\n origin: {origin:?}\n Direction: {direction:?}\n Max: {max}",
);
}
}
}
}
#[test]
fn test_aabb_cast_hits() {
for (test, volume, expected_distance) in &[
(
// Hit the center of the aabb, that a ray would've also hit
AabbCast3d::new(Aabb3d::new(Vec3::ZERO, Vec3::ONE), Vec3::ZERO, Dir3::Y, 90.),
Aabb3d::new(Vec3::Y * 5., Vec3::ONE),
3.,
),
(
// Hit the center of the aabb, but from the other side
AabbCast3d::new(
Aabb3d::new(Vec3::ZERO, Vec3::ONE),
Vec3::Y * 10.,
-Dir3::Y,
90.,
),
Aabb3d::new(Vec3::Y * 5., Vec3::ONE),
3.,
),
(
// Hit the edge of the aabb, that a ray would've missed
AabbCast3d::new(
Aabb3d::new(Vec3::ZERO, Vec3::ONE),
Vec3::X * 1.5,
Dir3::Y,
90.,
),
Aabb3d::new(Vec3::Y * 5., Vec3::ONE),
3.,
),
(
// Hit the edge of the aabb, by casting an off-center AABB
AabbCast3d::new(
Aabb3d::new(Vec3::X * -2., Vec3::ONE),
Vec3::X * 3.,
Dir3::Y,
90.,
),
Aabb3d::new(Vec3::Y * 5., Vec3::ONE),
3.,
),
] {
assert!(
test.intersects(volume),
"Case:\n Test: {test:?}\n Volume: {volume:?}\n Expected distance: {expected_distance:?}",
);
let actual_distance = test.aabb_collision_at(*volume).unwrap();
assert!(
ops::abs(actual_distance - expected_distance) < EPSILON,
"Case:\n Test: {test:?}\n Volume: {volume:?}\n Expected distance: {expected_distance:?}\n Actual distance: {actual_distance}",
);
let inverted_ray = RayCast3d::new(test.ray.origin, -test.ray.direction, test.ray.max);
assert!(
!inverted_ray.intersects(volume),
"Case:\n Test: {test:?}\n Volume: {volume:?}\n Expected distance: {expected_distance:?}",
);
}
}
#[test]
fn test_sphere_cast_hits() {
for (test, volume, expected_distance) in &[
(
// Hit the center of the bounding sphere, that a ray would've also hit
BoundingSphereCast::new(
BoundingSphere::new(Vec3::ZERO, 1.),
Vec3::ZERO,
Dir3::Y,
90.,
),
BoundingSphere::new(Vec3::Y * 5., 1.),
3.,
),
(
// Hit the center of the bounding sphere, but from the other side
BoundingSphereCast::new(
BoundingSphere::new(Vec3::ZERO, 1.),
Vec3::Y * 10.,
-Dir3::Y,
90.,
),
BoundingSphere::new(Vec3::Y * 5., 1.),
3.,
),
(
// Hit the bounding sphere off-center, that a ray would've missed
BoundingSphereCast::new(
BoundingSphere::new(Vec3::ZERO, 1.),
Vec3::X * 1.5,
Dir3::Y,
90.,
),
BoundingSphere::new(Vec3::Y * 5., 1.),
3.677,
),
(
// Hit the bounding sphere off-center, by casting a sphere that is off-center
BoundingSphereCast::new(
BoundingSphere::new(Vec3::X * -1.5, 1.),
Vec3::X * 3.,
Dir3::Y,
90.,
),
BoundingSphere::new(Vec3::Y * 5., 1.),
3.677,
),
] {
assert!(
test.intersects(volume),
"Case:\n Test: {test:?}\n Volume: {volume:?}\n Expected distance: {expected_distance:?}",
);
let actual_distance = test.sphere_collision_at(*volume).unwrap();
assert!(
ops::abs(actual_distance - expected_distance) < EPSILON,
"Case:\n Test: {test:?}\n Volume: {volume:?}\n Expected distance: {expected_distance:?}\n Actual distance: {actual_distance}",
);
let inverted_ray = RayCast3d::new(test.ray.origin, -test.ray.direction, test.ray.max);
assert!(
!inverted_ray.intersects(volume),
"Case:\n Test: {test:?}\n Volume: {volume:?}\n Expected distance: {expected_distance:?}",
);
}
}
}