use super::geometry::Aabb;
use glam::{Mat4, Vec3, Vec4};
#[derive(Debug, Clone, Copy)]
pub struct Plane {
pub normal: Vec3,
pub distance: f32,
}
impl Plane {
pub fn new(normal: Vec3, distance: f32) -> Self {
Self { normal, distance }
}
pub fn from_vec4(v: Vec4) -> Self {
Self {
normal: Vec3::new(v.x, v.y, v.z),
distance: v.w,
}
}
pub fn normalize(&self) -> Self {
let len = self.normal.length();
if len > 0.0 {
Self {
normal: self.normal / len,
distance: self.distance / len,
}
} else {
*self
}
}
pub fn signed_distance(&self, point: Vec3) -> f32 {
self.normal.dot(point) + self.distance
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Intersection {
Outside,
Inside,
Intersecting,
}
#[derive(Debug, Clone, Copy)]
pub struct Frustum {
pub planes: [Plane; 6],
}
impl Frustum {
pub fn from_view_projection(vp: Mat4) -> Self {
let col0 = vp.col(0);
let col1 = vp.col(1);
let col2 = vp.col(2);
let col3 = vp.col(3);
let planes = [
Plane::from_vec4(col3 + col0).normalize(),
Plane::from_vec4(col3 - col0).normalize(),
Plane::from_vec4(col3 + col1).normalize(),
Plane::from_vec4(col3 - col1).normalize(),
Plane::from_vec4(col3 + col2).normalize(),
Plane::from_vec4(col3 - col2).normalize(),
];
Self { planes }
}
pub fn contains_point(&self, point: Vec3) -> bool {
for plane in &self.planes {
if plane.signed_distance(point) < 0.0 {
return false;
}
}
true
}
pub fn test_aabb(&self, aabb: &Aabb) -> Intersection {
let mut result = Intersection::Inside;
for plane in &self.planes {
let p_vertex = 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
},
);
let n_vertex = Vec3::new(
if plane.normal.x >= 0.0 {
aabb.min.x
} else {
aabb.max.x
},
if plane.normal.y >= 0.0 {
aabb.min.y
} else {
aabb.max.y
},
if plane.normal.z >= 0.0 {
aabb.min.z
} else {
aabb.max.z
},
);
if plane.signed_distance(p_vertex) < 0.0 {
return Intersection::Outside;
}
if plane.signed_distance(n_vertex) < 0.0 {
result = Intersection::Intersecting;
}
}
result
}
pub fn contains_aabb(&self, aabb: &Aabb) -> bool {
self.test_aabb(aabb) != Intersection::Outside
}
pub fn contains_sphere(&self, center: Vec3, radius: f32) -> bool {
for plane in &self.planes {
if plane.signed_distance(center) < -radius {
return false;
}
}
true
}
}
pub struct FrustumCuller {
frustum: Frustum,
}
impl FrustumCuller {
pub fn new(view_projection: Mat4) -> Self {
Self {
frustum: Frustum::from_view_projection(view_projection),
}
}
pub fn update(&mut self, view_projection: Mat4) {
self.frustum = Frustum::from_view_projection(view_projection);
}
pub fn frustum(&self) -> &Frustum {
&self.frustum
}
pub fn should_cull(&self, aabb: &Aabb) -> bool {
!self.frustum.contains_aabb(aabb)
}
pub fn should_cull_sphere(&self, center: Vec3, radius: f32) -> bool {
!self.frustum.contains_sphere(center, radius)
}
pub fn filter_visible<T, F>(&self, items: &[T], get_aabb: F) -> Vec<usize>
where
F: Fn(&T) -> &Aabb,
{
items
.iter()
.enumerate()
.filter(|(_, item)| self.frustum.contains_aabb(get_aabb(item)))
.map(|(i, _)| i)
.collect()
}
}
#[cfg(test)]
mod tests {
use super::*;
use glam::Vec3;
#[test]
fn test_plane_signed_distance() {
let plane = Plane::new(Vec3::Z, 0.0);
assert!(plane.signed_distance(Vec3::new(0.0, 0.0, 1.0)) > 0.0);
assert!(plane.signed_distance(Vec3::new(0.0, 0.0, -1.0)) < 0.0);
assert!((plane.signed_distance(Vec3::ZERO)).abs() < 0.0001);
}
#[test]
fn test_frustum_contains_point() {
let vp = Mat4::orthographic_rh(-10.0, 10.0, -10.0, 10.0, 0.1, 100.0);
let frustum = Frustum::from_view_projection(vp);
assert!(frustum.contains_point(Vec3::new(0.0, 0.0, -50.0)));
assert!(!frustum.contains_point(Vec3::new(0.0, 0.0, -150.0)));
}
#[test]
fn test_aabb_inside_frustum() {
let vp = Mat4::orthographic_rh(-10.0, 10.0, -10.0, 10.0, 0.1, 100.0);
let frustum = Frustum::from_view_projection(vp);
let aabb = Aabb::new(Vec3::new(-5.0, -5.0, -50.0), Vec3::new(5.0, 5.0, -40.0));
assert!(frustum.contains_aabb(&aabb));
let aabb_outside = Aabb::new(Vec3::new(20.0, 20.0, -50.0), Vec3::new(30.0, 30.0, -40.0));
assert!(!frustum.contains_aabb(&aabb_outside));
}
}