use crate::scene::aabb::Aabb;
#[derive(Debug, Clone, Copy)]
pub struct Plane {
pub normal: glam::Vec3,
pub d: f32,
}
#[derive(Debug, Clone)]
pub struct Frustum {
pub planes: [Plane; 6],
}
impl Frustum {
pub fn from_view_proj(vp: &glam::Mat4) -> Self {
let row0 = vp.row(0);
let row1 = vp.row(1);
let row2 = vp.row(2);
let row3 = vp.row(3);
let mut planes = [
extract_plane(row3 + row0),
extract_plane(row3 - row0),
extract_plane(row3 + row1),
extract_plane(row3 - row1),
extract_plane(row2),
extract_plane(row3 - row2),
];
for plane in &mut planes {
let len = plane.normal.length();
if len > 1e-8 {
plane.normal /= len;
plane.d /= len;
}
}
Self { planes }
}
pub fn cull_aabb(&self, aabb: &Aabb) -> bool {
for plane in &self.planes {
let p = glam::Vec3::new(
if plane.normal.x >= 0.0 {
aabb.max.x
} else {
aabb.min.x
},
if plane.normal.y >= 0.0 {
aabb.max.y
} else {
aabb.min.y
},
if plane.normal.z >= 0.0 {
aabb.max.z
} else {
aabb.min.z
},
);
if plane.normal.dot(p) + plane.d < 0.0 {
return true; }
}
false }
}
fn extract_plane(row: glam::Vec4) -> Plane {
Plane {
normal: glam::Vec3::new(row.x, row.y, row.z),
d: row.w,
}
}
#[derive(Debug, Clone, Copy, Default)]
pub struct CullStats {
pub total: u32,
pub visible: u32,
pub culled: u32,
}
#[cfg(test)]
mod tests {
use super::*;
fn test_camera_vp() -> glam::Mat4 {
let view = glam::Mat4::look_at_rh(
glam::Vec3::new(0.0, 0.0, 5.0),
glam::Vec3::ZERO,
glam::Vec3::Y,
);
let proj = glam::Mat4::perspective_rh(std::f32::consts::FRAC_PI_4, 1.0, 0.1, 100.0);
proj * view
}
#[test]
fn test_frustum_from_perspective() {
let frustum = Frustum::from_view_proj(&test_camera_vp());
for plane in &frustum.planes {
let len = plane.normal.length();
assert!(
(len - 1.0).abs() < 1e-4,
"plane normal not unit length: {len}"
);
}
}
#[test]
fn test_cull_aabb_inside() {
let frustum = Frustum::from_view_proj(&test_camera_vp());
let aabb = Aabb {
min: glam::Vec3::splat(-0.5),
max: glam::Vec3::splat(0.5),
};
assert!(!frustum.cull_aabb(&aabb), "box at origin should be visible");
}
#[test]
fn test_cull_aabb_behind_camera() {
let frustum = Frustum::from_view_proj(&test_camera_vp());
let aabb = Aabb {
min: glam::Vec3::new(-0.5, -0.5, 99.5),
max: glam::Vec3::new(0.5, 0.5, 100.5),
};
assert!(
frustum.cull_aabb(&aabb),
"box behind camera should be culled"
);
}
#[test]
fn test_cull_aabb_far_left() {
let frustum = Frustum::from_view_proj(&test_camera_vp());
let aabb = Aabb {
min: glam::Vec3::new(-1000.0, -0.5, -0.5),
max: glam::Vec3::new(-999.0, 0.5, 0.5),
};
assert!(frustum.cull_aabb(&aabb), "box far left should be culled");
}
#[test]
fn test_cull_aabb_straddling_near_plane() {
let frustum = Frustum::from_view_proj(&test_camera_vp());
let aabb = Aabb {
min: glam::Vec3::splat(-2.0),
max: glam::Vec3::splat(2.0),
};
assert!(!frustum.cull_aabb(&aabb), "large box should be visible");
}
}