viewport_lib/camera/
frustum.rs1use crate::scene::aabb::Aabb;
7
8#[derive(Debug, Clone, Copy)]
10pub struct Plane {
11 pub normal: glam::Vec3,
13 pub d: f32,
15}
16
17#[derive(Debug, Clone)]
19pub struct Frustum {
20 pub planes: [Plane; 6],
22}
23
24impl Frustum {
25 pub fn from_view_proj(vp: &glam::Mat4) -> Self {
30 let row0 = vp.row(0);
31 let row1 = vp.row(1);
32 let row2 = vp.row(2);
33 let row3 = vp.row(3);
34
35 let mut planes = [
36 extract_plane(row3 + row0),
38 extract_plane(row3 - row0),
40 extract_plane(row3 + row1),
42 extract_plane(row3 - row1),
44 extract_plane(row2),
46 extract_plane(row3 - row2),
48 ];
49
50 for plane in &mut planes {
52 let len = plane.normal.length();
53 if len > 1e-8 {
54 plane.normal /= len;
55 plane.d /= len;
56 }
57 }
58
59 Self { planes }
60 }
61
62 pub fn cull_aabb(&self, aabb: &Aabb) -> bool {
67 for plane in &self.planes {
68 let p = glam::Vec3::new(
72 if plane.normal.x >= 0.0 {
73 aabb.max.x
74 } else {
75 aabb.min.x
76 },
77 if plane.normal.y >= 0.0 {
78 aabb.max.y
79 } else {
80 aabb.min.y
81 },
82 if plane.normal.z >= 0.0 {
83 aabb.max.z
84 } else {
85 aabb.min.z
86 },
87 );
88 if plane.normal.dot(p) + plane.d < 0.0 {
89 return true; }
91 }
92 false }
94}
95
96fn extract_plane(row: glam::Vec4) -> Plane {
97 Plane {
98 normal: glam::Vec3::new(row.x, row.y, row.z),
99 d: row.w,
100 }
101}
102
103#[derive(Debug, Clone, Copy, Default)]
105pub struct CullStats {
106 pub total: u32,
108 pub visible: u32,
110 pub culled: u32,
112}
113
114#[cfg(test)]
115mod tests {
116 use super::*;
117
118 fn test_camera_vp() -> glam::Mat4 {
119 let view = glam::Mat4::look_at_rh(
120 glam::Vec3::new(0.0, 0.0, 5.0),
121 glam::Vec3::ZERO,
122 glam::Vec3::Y,
123 );
124 let proj = glam::Mat4::perspective_rh(std::f32::consts::FRAC_PI_4, 1.0, 0.1, 100.0);
125 proj * view
126 }
127
128 #[test]
129 fn test_frustum_from_perspective() {
130 let frustum = Frustum::from_view_proj(&test_camera_vp());
131 for plane in &frustum.planes {
133 let len = plane.normal.length();
134 assert!(
135 (len - 1.0).abs() < 1e-4,
136 "plane normal not unit length: {len}"
137 );
138 }
139 }
140
141 #[test]
142 fn test_cull_aabb_inside() {
143 let frustum = Frustum::from_view_proj(&test_camera_vp());
144 let aabb = Aabb {
146 min: glam::Vec3::splat(-0.5),
147 max: glam::Vec3::splat(0.5),
148 };
149 assert!(!frustum.cull_aabb(&aabb), "box at origin should be visible");
150 }
151
152 #[test]
153 fn test_cull_aabb_behind_camera() {
154 let frustum = Frustum::from_view_proj(&test_camera_vp());
155 let aabb = Aabb {
157 min: glam::Vec3::new(-0.5, -0.5, 99.5),
158 max: glam::Vec3::new(0.5, 0.5, 100.5),
159 };
160 assert!(
161 frustum.cull_aabb(&aabb),
162 "box behind camera should be culled"
163 );
164 }
165
166 #[test]
167 fn test_cull_aabb_far_left() {
168 let frustum = Frustum::from_view_proj(&test_camera_vp());
169 let aabb = Aabb {
171 min: glam::Vec3::new(-1000.0, -0.5, -0.5),
172 max: glam::Vec3::new(-999.0, 0.5, 0.5),
173 };
174 assert!(frustum.cull_aabb(&aabb), "box far left should be culled");
175 }
176
177 #[test]
178 fn test_cull_aabb_straddling_near_plane() {
179 let frustum = Frustum::from_view_proj(&test_camera_vp());
180 let aabb = Aabb {
182 min: glam::Vec3::splat(-2.0),
183 max: glam::Vec3::splat(2.0),
184 };
185 assert!(!frustum.cull_aabb(&aabb), "large box should be visible");
186 }
187}