Skip to main content

viewport_lib/scene/
aabb.rs

1//! Axis-aligned bounding box for frustum culling and spatial queries.
2
3/// Axis-aligned bounding box stored as min/max corners.
4#[derive(Debug, Clone, Copy)]
5pub struct Aabb {
6    /// Minimum corner of the box.
7    pub min: glam::Vec3,
8    /// Maximum corner of the box.
9    pub max: glam::Vec3,
10}
11
12impl Aabb {
13    /// Compute an AABB from a slice of vertex positions.
14    ///
15    /// Returns a degenerate zero-size AABB at the origin if the slice is empty.
16    pub fn from_positions(positions: &[[f32; 3]]) -> Self {
17        if positions.is_empty() {
18            return Self {
19                min: glam::Vec3::ZERO,
20                max: glam::Vec3::ZERO,
21            };
22        }
23        let mut min = glam::Vec3::splat(f32::INFINITY);
24        let mut max = glam::Vec3::splat(f32::NEG_INFINITY);
25        for p in positions {
26            let v = glam::Vec3::from(*p);
27            min = min.min(v);
28            max = max.max(v);
29        }
30        Self { min, max }
31    }
32
33    /// Center of the bounding box.
34    pub fn center(&self) -> glam::Vec3 {
35        (self.min + self.max) * 0.5
36    }
37
38    /// Half-extents (distance from center to each face).
39    pub fn half_extents(&self) -> glam::Vec3 {
40        (self.max - self.min) * 0.5
41    }
42
43    /// Compute a conservative world-space AABB by transforming all 8 corners
44    /// and taking the min/max of the results.
45    pub fn transformed(&self, mat: &glam::Mat4) -> Self {
46        let corners = [
47            glam::Vec3::new(self.min.x, self.min.y, self.min.z),
48            glam::Vec3::new(self.max.x, self.min.y, self.min.z),
49            glam::Vec3::new(self.min.x, self.max.y, self.min.z),
50            glam::Vec3::new(self.max.x, self.max.y, self.min.z),
51            glam::Vec3::new(self.min.x, self.min.y, self.max.z),
52            glam::Vec3::new(self.max.x, self.min.y, self.max.z),
53            glam::Vec3::new(self.min.x, self.max.y, self.max.z),
54            glam::Vec3::new(self.max.x, self.max.y, self.max.z),
55        ];
56        let mut new_min = glam::Vec3::splat(f32::INFINITY);
57        let mut new_max = glam::Vec3::splat(f32::NEG_INFINITY);
58        for c in &corners {
59            let t = mat.transform_point3(*c);
60            new_min = new_min.min(t);
61            new_max = new_max.max(t);
62        }
63        Self {
64            min: new_min,
65            max: new_max,
66        }
67    }
68
69    /// Returns true if the plane (defined by unit normal + signed distance)
70    /// intersects this AABB — i.e., the AABB spans both sides of the plane.
71    pub fn intersects_plane(&self, normal: glam::Vec3, distance: f32) -> bool {
72        let center = self.center();
73        let extents = self.half_extents();
74        let r =
75            extents.x * normal.x.abs() + extents.y * normal.y.abs() + extents.z * normal.z.abs();
76        let d = normal.dot(center) + distance;
77        d.abs() <= r
78    }
79}
80
81impl Default for Aabb {
82    fn default() -> Self {
83        Self {
84            min: glam::Vec3::ZERO,
85            max: glam::Vec3::ZERO,
86        }
87    }
88}
89
90#[cfg(test)]
91mod tests {
92    use super::*;
93
94    fn unit_cube_positions() -> Vec<[f32; 3]> {
95        vec![
96            [-0.5, -0.5, -0.5],
97            [0.5, -0.5, -0.5],
98            [0.5, 0.5, -0.5],
99            [-0.5, 0.5, -0.5],
100            [-0.5, -0.5, 0.5],
101            [0.5, -0.5, 0.5],
102            [0.5, 0.5, 0.5],
103            [-0.5, 0.5, 0.5],
104        ]
105    }
106
107    #[test]
108    fn test_aabb_from_unit_cube() {
109        let aabb = Aabb::from_positions(&unit_cube_positions());
110        assert!((aabb.min - glam::Vec3::splat(-0.5)).length() < 1e-5);
111        assert!((aabb.max - glam::Vec3::splat(0.5)).length() < 1e-5);
112    }
113
114    #[test]
115    fn test_aabb_transformed_identity() {
116        let aabb = Aabb::from_positions(&unit_cube_positions());
117        let transformed = aabb.transformed(&glam::Mat4::IDENTITY);
118        assert!((transformed.min - aabb.min).length() < 1e-5);
119        assert!((transformed.max - aabb.max).length() < 1e-5);
120    }
121
122    #[test]
123    fn test_aabb_transformed_translated() {
124        let aabb = Aabb::from_positions(&unit_cube_positions());
125        let mat = glam::Mat4::from_translation(glam::Vec3::new(10.0, 20.0, 30.0));
126        let transformed = aabb.transformed(&mat);
127        assert!((transformed.min - glam::Vec3::new(9.5, 19.5, 29.5)).length() < 1e-5);
128        assert!((transformed.max - glam::Vec3::new(10.5, 20.5, 30.5)).length() < 1e-5);
129    }
130
131    #[test]
132    fn test_aabb_center_and_extents() {
133        let aabb = Aabb::from_positions(&unit_cube_positions());
134        assert!(aabb.center().length() < 1e-5);
135        assert!((aabb.half_extents() - glam::Vec3::splat(0.5)).length() < 1e-5);
136    }
137
138    #[test]
139    fn test_aabb_from_empty() {
140        let aabb = Aabb::from_positions(&[]);
141        assert!((aabb.min - glam::Vec3::ZERO).length() < 1e-5);
142        assert!((aabb.max - glam::Vec3::ZERO).length() < 1e-5);
143    }
144
145    #[test]
146    fn test_intersects_plane_through_center() {
147        let aabb = Aabb::from_positions(&unit_cube_positions());
148        // YZ plane at x=0 cuts right through center
149        assert!(aabb.intersects_plane(glam::Vec3::X, 0.0));
150    }
151
152    #[test]
153    fn test_intersects_plane_outside() {
154        let aabb = Aabb::from_positions(&unit_cube_positions());
155        // Plane at x=2.0 — entirely outside
156        assert!(!aabb.intersects_plane(glam::Vec3::X, -2.0));
157        // Plane at x=-2.0 — entirely on other side
158        assert!(!aabb.intersects_plane(glam::Vec3::X, 2.0));
159    }
160
161    #[test]
162    fn test_intersects_plane_tangent() {
163        let aabb = Aabb::from_positions(&unit_cube_positions());
164        // Plane exactly at the +X face (x=0.5 -> distance=-0.5 since dot(n,p)+d=0)
165        assert!(aabb.intersects_plane(glam::Vec3::X, -0.5));
166    }
167}