#[cfg(feature = "aabb-cull")]
use crate::bounds::Aabb;
use nalgebra::{Matrix4, Vector3};
#[cfg(not(feature = "std"))]
#[allow(unused_imports)]
use micromath::F32Ext;
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct HalfSpace {
pub normal: Vector3<f32>,
pub d: f32,
}
impl HalfSpace {
#[inline]
pub fn new(a: f32, b: f32, c: f32, d: f32) -> Self {
let len = (a * a + b * b + c * c).sqrt();
if len > 1e-6 {
let inv_len = 1.0 / len;
Self {
normal: Vector3::new(a * inv_len, b * inv_len, c * inv_len),
d: d * inv_len,
}
} else {
Self {
normal: Vector3::new(0.0, 1.0, 0.0),
d: 0.0,
}
}
}
#[inline]
pub fn signed_distance(&self, point: &Vector3<f32>) -> f32 {
self.normal.dot(point) + self.d
}
#[inline]
pub fn contains_point(&self, point: &Vector3<f32>) -> bool {
self.signed_distance(point) >= 0.0
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct ViewFrustum {
pub planes: [HalfSpace; 6],
}
impl ViewFrustum {
pub const LEFT_PLANE: usize = 0;
pub const RIGHT_PLANE: usize = 1;
pub const BOTTOM_PLANE: usize = 2;
pub const TOP_PLANE: usize = 3;
pub const NEAR_PLANE: usize = 4;
pub const FAR_PLANE: usize = 5;
pub fn from_view_projection(vp: &Matrix4<f32>) -> Self {
let r0 = vp.row(0);
let r1 = vp.row(1);
let r2 = vp.row(2);
let r3 = vp.row(3);
let p_left = r3 + r0;
let p_right = r3 - r0;
let p_bottom = r3 + r1;
let p_top = r3 - r1;
let p_near = r3 + r2;
let p_far = r3 - r2;
Self {
planes: [
HalfSpace::new(p_left[0], p_left[1], p_left[2], p_left[3]),
HalfSpace::new(p_right[0], p_right[1], p_right[2], p_right[3]),
HalfSpace::new(p_bottom[0], p_bottom[1], p_bottom[2], p_bottom[3]),
HalfSpace::new(p_top[0], p_top[1], p_top[2], p_top[3]),
HalfSpace::new(p_near[0], p_near[1], p_near[2], p_near[3]),
HalfSpace::new(p_far[0], p_far[1], p_far[2], p_far[3]),
],
}
}
#[inline]
pub fn intersects_sphere(&self, center: &Vector3<f32>, radius: f32) -> bool {
for plane in &self.planes {
if plane.signed_distance(center) < -radius {
return false;
}
}
true
}
#[cfg(feature = "aabb-cull")]
#[inline]
pub fn intersects_aabb(&self, aabb: &Aabb) -> bool {
let min = aabb.min();
let max = aabb.max();
for plane in &self.planes {
let p_x = if plane.normal.x >= 0.0 { max.x } else { min.x };
let p_y = if plane.normal.y >= 0.0 { max.y } else { min.y };
let p_z = if plane.normal.z >= 0.0 { max.z } else { min.z };
if plane.signed_distance(&Vector3::new(p_x, p_y, p_z)) < 0.0 {
return false;
}
}
true
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_half_space_distance() {
let plane = HalfSpace::new(0.0, 1.0, 0.0, 5.0); assert_eq!(plane.signed_distance(&Vector3::new(0.0, 0.0, 0.0)), 5.0);
assert_eq!(plane.signed_distance(&Vector3::new(0.0, -5.0, 0.0)), 0.0);
assert_eq!(plane.signed_distance(&Vector3::new(0.0, -10.0, 0.0)), -5.0);
}
#[test]
fn test_frustum_sphere_culling() {
let frustum = ViewFrustum {
planes: [
HalfSpace::new(1.0, 0.0, 0.0, 10.0), HalfSpace::new(-1.0, 0.0, 0.0, 10.0), HalfSpace::new(0.0, 1.0, 0.0, 10.0), HalfSpace::new(0.0, -1.0, 0.0, 10.0), HalfSpace::new(0.0, 0.0, 1.0, 10.0), HalfSpace::new(0.0, 0.0, -1.0, 10.0), ],
};
assert!(frustum.intersects_sphere(&Vector3::zeros(), 1.0));
assert!(!frustum.intersects_sphere(&Vector3::new(15.0, 0.0, 0.0), 2.0));
assert!(frustum.intersects_sphere(&Vector3::new(11.0, 0.0, 0.0), 2.0));
}
#[cfg(feature = "aabb-cull")]
#[test]
fn test_frustum_aabb_culling() {
let frustum = ViewFrustum {
planes: [
HalfSpace::new(1.0, 0.0, 0.0, 10.0),
HalfSpace::new(-1.0, 0.0, 0.0, 10.0),
HalfSpace::new(0.0, 1.0, 0.0, 10.0),
HalfSpace::new(0.0, -1.0, 0.0, 10.0),
HalfSpace::new(0.0, 0.0, 1.0, 10.0),
HalfSpace::new(0.0, 0.0, -1.0, 10.0),
],
};
let inside =
Aabb::from_min_max(Vector3::new(-1.0, -1.0, -1.0), Vector3::new(1.0, 1.0, 1.0));
assert!(frustum.intersects_aabb(&inside));
let outside =
Aabb::from_min_max(Vector3::new(12.0, 0.0, 0.0), Vector3::new(15.0, 2.0, 2.0));
assert!(!frustum.intersects_aabb(&outside));
let straddling =
Aabb::from_min_max(Vector3::new(9.0, -1.0, -1.0), Vector3::new(12.0, 1.0, 1.0));
assert!(frustum.intersects_aabb(&straddling));
}
#[test]
fn test_half_space_degenerate_contains_and_from_vp() {
let degenerate = HalfSpace::new(0.0, 0.0, 0.0, 0.0);
assert_eq!(degenerate.normal, Vector3::new(0.0, 1.0, 0.0));
assert_eq!(degenerate.d, 0.0);
let plane = HalfSpace::new(0.0, 1.0, 0.0, 5.0);
assert!(plane.contains_point(&Vector3::new(0.0, 0.0, 0.0)));
assert!(!plane.contains_point(&Vector3::new(0.0, -10.0, 0.0)));
let vp = nalgebra::Matrix4::new_perspective(1.0, 1.2, 0.1, 100.0);
let frustum = ViewFrustum::from_view_projection(&vp);
assert_eq!(frustum.planes.len(), 6);
let center = Vector3::new(0.0, 0.0, -5.0);
assert!(frustum.intersects_sphere(¢er, 0.5));
assert!(!frustum.intersects_sphere(&Vector3::new(1000.0, 0.0, 0.0), 1.0));
}
}