Skip to main content

scenix_math/
bounds.rs

1use crate::{Mat4, Vec3};
2
3/// An axis-aligned bounding box.
4#[derive(Clone, Copy, Debug, PartialEq)]
5#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
6pub struct Aabb {
7    /// Minimum corner.
8    pub min: Vec3,
9    /// Maximum corner.
10    pub max: Vec3,
11}
12
13/// A bounding sphere.
14#[derive(Clone, Copy, Debug, PartialEq)]
15#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
16pub struct Sphere {
17    /// Sphere center.
18    pub center: Vec3,
19    /// Sphere radius.
20    pub radius: f32,
21}
22
23impl Aabb {
24    /// An empty zero-sized box at the origin.
25    pub const ZERO: Self = Self::new(Vec3::ZERO, Vec3::ZERO);
26
27    /// Creates an AABB from minimum and maximum corners.
28    #[inline]
29    pub const fn new(min: Vec3, max: Vec3) -> Self {
30        Self { min, max }
31    }
32
33    /// Creates the smallest AABB containing all points.
34    pub fn from_points(points: &[Vec3]) -> Self {
35        let Some((first, rest)) = points.split_first() else {
36            return Self::ZERO;
37        };
38        let mut min = *first;
39        let mut max = *first;
40        for point in rest {
41            min.x = min.x.min(point.x);
42            min.y = min.y.min(point.y);
43            min.z = min.z.min(point.z);
44            max.x = max.x.max(point.x);
45            max.y = max.y.max(point.y);
46            max.z = max.z.max(point.z);
47        }
48        Self::new(min, max)
49    }
50
51    /// Returns the center point.
52    #[inline]
53    pub fn center(self) -> Vec3 {
54        (self.min + self.max) * 0.5
55    }
56
57    /// Returns half the box extents.
58    #[inline]
59    pub fn half_extents(self) -> Vec3 {
60        (self.max - self.min) * 0.5
61    }
62
63    /// Returns whether a point is inside or on the boundary.
64    #[inline]
65    pub fn contains_point(self, point: Vec3) -> bool {
66        point.x >= self.min.x
67            && point.y >= self.min.y
68            && point.z >= self.min.z
69            && point.x <= self.max.x
70            && point.y <= self.max.y
71            && point.z <= self.max.z
72    }
73
74    /// Returns whether this box intersects another box.
75    #[inline]
76    pub fn intersects_aabb(self, other: Self) -> bool {
77        self.min.x <= other.max.x
78            && self.max.x >= other.min.x
79            && self.min.y <= other.max.y
80            && self.max.y >= other.min.y
81            && self.min.z <= other.max.z
82            && self.max.z >= other.min.z
83    }
84
85    /// Returns the merged AABB containing both inputs.
86    #[inline]
87    pub fn merge(self, other: Self) -> Self {
88        Self::new(
89            Vec3::new(
90                self.min.x.min(other.min.x),
91                self.min.y.min(other.min.y),
92                self.min.z.min(other.min.z),
93            ),
94            Vec3::new(
95                self.max.x.max(other.max.x),
96                self.max.y.max(other.max.y),
97                self.max.z.max(other.max.z),
98            ),
99        )
100    }
101
102    /// Conservatively transforms the AABB by transforming all eight corners.
103    pub fn transform(self, matrix: Mat4) -> Self {
104        let min = self.min;
105        let max = self.max;
106        let corners = [
107            Vec3::new(min.x, min.y, min.z),
108            Vec3::new(max.x, min.y, min.z),
109            Vec3::new(min.x, max.y, min.z),
110            Vec3::new(max.x, max.y, min.z),
111            Vec3::new(min.x, min.y, max.z),
112            Vec3::new(max.x, min.y, max.z),
113            Vec3::new(min.x, max.y, max.z),
114            Vec3::new(max.x, max.y, max.z),
115        ];
116        let mut transformed = [Vec3::ZERO; 8];
117        for (out, corner) in transformed.iter_mut().zip(corners) {
118            *out = matrix.mul_vec3(corner);
119        }
120        Self::from_points(&transformed)
121    }
122
123    /// Returns the surface area.
124    #[inline]
125    pub fn surface_area(self) -> f32 {
126        let extents = self.max - self.min;
127        let x = extents.x.max(0.0);
128        let y = extents.y.max(0.0);
129        let z = extents.z.max(0.0);
130        2.0 * (x * y + y * z + z * x)
131    }
132
133    /// Returns a conservative bounding sphere.
134    #[inline]
135    pub fn bounding_sphere(self) -> Sphere {
136        let center = self.center();
137        Sphere::new(center, self.max.distance(center))
138    }
139}
140
141impl Default for Aabb {
142    #[inline]
143    fn default() -> Self {
144        Self::ZERO
145    }
146}
147
148impl Sphere {
149    /// Creates a sphere from a center and radius.
150    #[inline]
151    pub const fn new(center: Vec3, radius: f32) -> Self {
152        Self { center, radius }
153    }
154
155    /// Returns whether the point is inside or on the sphere.
156    #[inline]
157    pub fn contains_point(self, point: Vec3) -> bool {
158        point.distance(self.center) <= self.radius
159    }
160}
161
162#[cfg(test)]
163mod tests {
164    use super::*;
165
166    #[test]
167    fn aabb_from_points_handles_empty_and_extents() {
168        assert_eq!(Aabb::from_points(&[]), Aabb::ZERO);
169        let aabb = Aabb::from_points(&[Vec3::new(-1.0, 2.0, 0.0), Vec3::new(3.0, -2.0, 1.0)]);
170        assert_eq!(aabb.min, Vec3::new(-1.0, -2.0, 0.0));
171        assert_eq!(aabb.max, Vec3::new(3.0, 2.0, 1.0));
172        assert_eq!(aabb.center(), Vec3::new(1.0, 0.0, 0.5));
173        assert_eq!(aabb.half_extents(), Vec3::new(2.0, 2.0, 0.5));
174    }
175
176    #[test]
177    fn aabb_contains_intersects_merges_and_measures_area() {
178        let a = Aabb::new(Vec3::ZERO, Vec3::new(1.0, 1.0, 1.0));
179        let b = Aabb::new(Vec3::new(0.5, 0.5, 0.5), Vec3::new(2.0, 2.0, 2.0));
180        assert!(a.contains_point(Vec3::new(0.5, 0.5, 0.5)));
181        assert!(a.intersects_aabb(b));
182        assert_eq!(a.merge(b).max, Vec3::new(2.0, 2.0, 2.0));
183        assert_eq!(a.surface_area(), 6.0);
184    }
185}