chunkedge_math/
aabb.rs

1use std::ops::{Add, Sub};
2
3use glam::DVec3;
4
5/// A three-dimensional axis-aligned bounding box, or "AABB".
6///
7/// The AABB is defined by two points—`min` and `max`. `min` is less than or
8/// equal to `max` componentwise.
9#[derive(Copy, Clone, PartialEq, Default, Debug)]
10pub struct Aabb {
11    min: DVec3,
12    max: DVec3,
13}
14
15impl Aabb {
16    pub const ZERO: Self = Self {
17        min: DVec3::ZERO,
18        max: DVec3::ZERO,
19    };
20
21    /// Constructs a new AABB from `min` and `max` points.
22    ///
23    /// # Panics
24    ///
25    /// Panics if `debug_assertions` are enabled and `min` is not less than or
26    /// equal to `max` componentwise.
27    #[cfg_attr(debug_assertions, track_caller)]
28    pub fn new(min: DVec3, max: DVec3) -> Self {
29        debug_assert!(
30            min.x <= max.x && min.y <= max.y && min.z <= max.z,
31            "`min` must be less than or equal to `max` componentwise (min = {min}, max = {max})"
32        );
33
34        Self { min, max }
35    }
36
37    // TODO: remove when the assertion in `new` can be done in a `const` context.
38    #[doc(hidden)]
39    pub const fn new_unchecked(min: DVec3, max: DVec3) -> Self {
40        Self { min, max }
41    }
42
43    /// Returns a new AABB containing a single point `p`.
44    pub fn new_point(p: DVec3) -> Self {
45        Self::new(p, p)
46    }
47
48    pub fn from_bottom_size(bottom: DVec3, size: DVec3) -> Self {
49        Self::new(
50            DVec3 {
51                x: bottom.x - size.x / 2.0,
52                y: bottom.y,
53                z: bottom.z - size.z / 2.0,
54            },
55            DVec3 {
56                x: bottom.x + size.x / 2.0,
57                y: bottom.y + size.y,
58                z: bottom.z + size.z / 2.0,
59            },
60        )
61    }
62
63    pub const fn min(self) -> DVec3 {
64        self.min
65    }
66
67    pub const fn max(self) -> DVec3 {
68        self.max
69    }
70
71    pub fn union(self, other: Self) -> Self {
72        Self::new(self.min.min(other.min), self.max.max(other.max))
73    }
74
75    /// Does this bounding box intersect with another bounding box (including
76    /// touching)?
77    ///
78    /// See [`Aabb::intersects_strict`] if you don't want to include touching.
79    pub fn intersects(self, other: Self) -> bool {
80        self.max.x >= other.min.x
81            && other.max.x >= self.min.x
82            && self.max.y >= other.min.y
83            && other.max.y >= self.min.y
84            && self.max.z >= other.min.z
85            && other.max.z >= self.min.z
86    }
87
88    /// Does this bounding box intersect with another bounding box (not
89    /// including touching)?
90    ///
91    /// See [`Aabb::intersects`] if you want to include touching.
92    pub fn intersects_strict(self, other: Self) -> bool {
93        self.max.x > other.min.x
94            && other.max.x > self.min.x
95            && self.max.y > other.min.y
96            && other.max.y > self.min.y
97            && self.max.z > other.min.z
98            && other.max.z > self.min.z
99    }
100
101    /// Does this bounding box contain the given point?
102    pub fn contains_point(self, p: DVec3) -> bool {
103        self.min.x <= p.x
104            && self.min.y <= p.y
105            && self.min.z <= p.z
106            && self.max.x >= p.x
107            && self.max.y >= p.y
108            && self.max.z >= p.z
109    }
110
111    /// Returns the closest point in the AABB to the given point.
112    pub fn projected_point(self, p: DVec3) -> DVec3 {
113        p.clamp(self.min, self.max)
114    }
115
116    /// Returns the smallest distance from the AABB to the point.
117    pub fn distance_to_point(self, p: DVec3) -> f64 {
118        self.projected_point(p).distance(p)
119    }
120
121    /// Calculates the intersection between this AABB and a ray
122    /// defined by its `origin` point and `direction` vector.
123    ///
124    /// If an intersection occurs, `Some([near, far])` is returned. `near` and
125    /// `far` are the values of `t` in the equation `origin + t * direction =
126    /// point` where `point` is the nearest or furthest intersection point to
127    /// the `origin`. If no intersection occurs, then `None` is returned.
128    ///
129    /// In other words, if `direction` is normalized, then `near` and `far` are
130    /// the distances to the nearest and furthest intersection points.
131    pub fn ray_intersection(self, origin: DVec3, direction: DVec3) -> Option<[f64; 2]> {
132        let mut near: f64 = 0.0;
133        let mut far = f64::INFINITY;
134
135        for i in 0..3 {
136            // Rust's definition of `min` and `max` properly handle the NaNs these
137            // computations may produce.
138            let t0 = (self.min[i] - origin[i]) / direction[i];
139            let t1 = (self.max[i] - origin[i]) / direction[i];
140
141            near = near.max(t0.min(t1));
142            far = far.min(t0.max(t1));
143        }
144
145        (near <= far).then_some([near, far])
146    }
147}
148
149impl Add<DVec3> for Aabb {
150    type Output = Aabb;
151
152    fn add(self, rhs: DVec3) -> Self::Output {
153        Self::new(self.min + rhs, self.max + rhs)
154    }
155}
156
157impl Add<Aabb> for DVec3 {
158    type Output = Aabb;
159
160    fn add(self, rhs: Aabb) -> Self::Output {
161        rhs + self
162    }
163}
164
165impl Sub<DVec3> for Aabb {
166    type Output = Aabb;
167
168    fn sub(self, rhs: DVec3) -> Self::Output {
169        Self::new(self.min - rhs, self.max - rhs)
170    }
171}
172
173impl Sub<Aabb> for DVec3 {
174    type Output = Aabb;
175
176    fn sub(self, rhs: Aabb) -> Self::Output {
177        rhs - self
178    }
179}
180
181#[cfg(test)]
182mod tests {
183    use super::*;
184
185    #[test]
186    fn ray_intersect_edge_cases() {
187        let bb = Aabb::new([0.0, 0.0, 0.0].into(), [1.0, 1.0, 1.0].into());
188
189        let ros = [
190            // On a corner
191            DVec3::new(0.0, 0.0, 0.0),
192            // Outside
193            DVec3::new(-0.5, 0.5, -0.5),
194            // In the center
195            DVec3::new(0.5, 0.5, 0.5),
196            // On an edge
197            DVec3::new(0.0, 0.5, 0.0),
198            // On a face
199            DVec3::new(0.0, 0.5, 0.5),
200            // Outside slabs
201            DVec3::new(-2.0, -2.0, -2.0),
202        ];
203
204        let rds = [
205            DVec3::new(1.0, 0.0, 0.0),
206            DVec3::new(-1.0, 0.0, 0.0),
207            DVec3::new(0.0, 1.0, 0.0),
208            DVec3::new(0.0, -1.0, 0.0),
209            DVec3::new(0.0, 0.0, 1.0),
210            DVec3::new(0.0, 0.0, -1.0),
211        ];
212
213        assert!(rds.iter().all(|d| d.is_normalized()));
214
215        for ro in ros {
216            for rd in rds {
217                if let Some([near, far]) = bb.ray_intersection(ro, rd) {
218                    assert!(near.is_finite());
219                    assert!(far.is_finite());
220                    assert!(near <= far);
221                    assert!(near >= 0.0);
222                    assert!(far >= 0.0);
223                }
224            }
225        }
226    }
227}