use crate::bounds::WorldBounds;
use crate::coord::WorldCoord;
use glam::DVec4;
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Plane {
normal: [f64; 3],
d: f64,
}
impl Plane {
#[inline]
pub fn new(normal: [f64; 3], d: f64) -> Self {
Self { normal, d }
}
#[inline]
pub fn normal(&self) -> [f64; 3] {
self.normal
}
#[inline]
pub fn d(&self) -> f64 {
self.d
}
#[inline]
pub fn distance_to_point(&self, x: f64, y: f64, z: f64) -> f64 {
self.normal[0] * x + self.normal[1] * y + self.normal[2] * z + self.d
}
}
pub const PLANE_LEFT: usize = 0;
pub const PLANE_RIGHT: usize = 1;
pub const PLANE_BOTTOM: usize = 2;
pub const PLANE_TOP: usize = 3;
pub const PLANE_NEAR: usize = 4;
pub const PLANE_FAR: usize = 5;
#[derive(Debug, Clone)]
pub struct Frustum {
planes: [Plane; 6],
}
impl Frustum {
pub fn from_view_projection(vp: &glam::DMat4) -> Self {
let row0 = DVec4::new(vp.col(0).x, vp.col(1).x, vp.col(2).x, vp.col(3).x);
let row1 = DVec4::new(vp.col(0).y, vp.col(1).y, vp.col(2).y, vp.col(3).y);
let row2 = DVec4::new(vp.col(0).z, vp.col(1).z, vp.col(2).z, vp.col(3).z);
let row3 = DVec4::new(vp.col(0).w, vp.col(1).w, vp.col(2).w, vp.col(3).w);
let raw_planes = [
row3 + row0, row3 - row0, row3 + row1, row3 - row1, row3 + row2, row3 - row2, ];
let mut planes = [Plane {
normal: [0.0; 3],
d: 0.0,
}; 6];
for (i, p) in raw_planes.iter().enumerate() {
let len = (p.x * p.x + p.y * p.y + p.z * p.z).sqrt();
if len > 1e-15 {
planes[i] = Plane {
normal: [p.x / len, p.y / len, p.z / len],
d: p.w / len,
};
}
}
Self { planes }
}
#[inline]
pub fn planes(&self) -> &[Plane; 6] {
&self.planes
}
pub fn contains_point(&self, point: &WorldCoord) -> bool {
let (x, y, z) = (point.position.x, point.position.y, point.position.z);
for plane in &self.planes {
if plane.distance_to_point(x, y, z) < 0.0 {
return false;
}
}
true
}
pub fn intersects_sphere(&self, center: &WorldCoord, radius: f64) -> bool {
let (x, y, z) = (center.position.x, center.position.y, center.position.z);
for plane in &self.planes {
if plane.distance_to_point(x, y, z) < -radius {
return false;
}
}
true
}
pub fn intersects_aabb(&self, bounds: &WorldBounds) -> bool {
let min = bounds.min.position;
let max = bounds.max.position;
for plane in &self.planes {
let px = if plane.normal[0] >= 0.0 { max.x } else { min.x };
let py = if plane.normal[1] >= 0.0 { max.y } else { min.y };
let pz = if plane.normal[2] >= 0.0 { max.z } else { min.z };
if plane.distance_to_point(px, py, pz) < 0.0 {
return false;
}
}
true
}
}
#[cfg(test)]
mod tests {
use super::*;
use glam::DMat4;
#[test]
fn plane_new_and_accessors() {
let p = Plane::new([0.0, 1.0, 0.0], -5.0);
assert_eq!(p.normal(), [0.0, 1.0, 0.0]);
assert_eq!(p.d(), -5.0);
}
#[test]
fn identity_frustum_contains_origin() {
let vp = DMat4::IDENTITY;
let frustum = Frustum::from_view_projection(&vp);
let bounds = WorldBounds::new(
WorldCoord::new(-0.5, -0.5, -0.5),
WorldCoord::new(0.5, 0.5, 0.5),
);
assert!(frustum.intersects_aabb(&bounds));
}
#[test]
fn ortho_frustum_culls_outside() {
let vp = DMat4::orthographic_rh(-100.0, 100.0, -100.0, 100.0, -100.0, 100.0);
let frustum = Frustum::from_view_projection(&vp);
let inside = WorldBounds::new(
WorldCoord::new(-50.0, -50.0, -50.0),
WorldCoord::new(50.0, 50.0, 50.0),
);
let outside = WorldBounds::new(
WorldCoord::new(200.0, 200.0, 200.0),
WorldCoord::new(300.0, 300.0, 300.0),
);
assert!(frustum.intersects_aabb(&inside));
assert!(!frustum.intersects_aabb(&outside));
}
#[test]
fn extracted_normals_are_unit_length() {
let proj = DMat4::perspective_rh(std::f64::consts::FRAC_PI_4, 1.0, 0.1, 1000.0);
let view = DMat4::look_at_rh(
glam::DVec3::new(0.0, 0.0, 10.0),
glam::DVec3::ZERO,
glam::DVec3::Y,
);
let frustum = Frustum::from_view_projection(&(proj * view));
for plane in frustum.planes() {
let n = plane.normal();
let len = (n[0] * n[0] + n[1] * n[1] + n[2] * n[2]).sqrt();
assert!(
(len - 1.0).abs() < 1e-12,
"plane normal not unit-length: {len}"
);
}
}
#[test]
fn degenerate_matrix_passes_all_tests() {
let frustum = Frustum::from_view_projection(&DMat4::ZERO);
assert!(frustum.contains_point(&WorldCoord::new(999.0, 999.0, 999.0)));
assert!(frustum.intersects_sphere(&WorldCoord::new(0.0, 0.0, 0.0), 1.0));
let bounds = WorldBounds::new(
WorldCoord::new(-1.0, -1.0, -1.0),
WorldCoord::new(1.0, 1.0, 1.0),
);
assert!(frustum.intersects_aabb(&bounds));
}
#[test]
fn plane_index_constants() {
let vp = DMat4::orthographic_rh(-100.0, 100.0, -100.0, 100.0, -100.0, 100.0);
let frustum = Frustum::from_view_projection(&vp);
let planes = frustum.planes();
assert!(planes[PLANE_LEFT].normal()[0] > 0.0);
assert!(planes[PLANE_RIGHT].normal()[0] < 0.0);
}
#[test]
fn perspective_frustum() {
let proj = DMat4::perspective_rh(std::f64::consts::FRAC_PI_4, 1.0, 0.1, 1000.0);
let view = DMat4::look_at_rh(
glam::DVec3::new(0.0, 0.0, 10.0),
glam::DVec3::ZERO,
glam::DVec3::Y,
);
let vp = proj * view;
let frustum = Frustum::from_view_projection(&vp);
let visible = WorldBounds::new(
WorldCoord::new(-1.0, -1.0, -1.0),
WorldCoord::new(1.0, 1.0, 1.0),
);
let behind = WorldBounds::new(
WorldCoord::new(-1.0, -1.0, 20.0),
WorldCoord::new(1.0, 1.0, 30.0),
);
assert!(frustum.intersects_aabb(&visible));
assert!(!frustum.intersects_aabb(&behind));
}
#[test]
fn pitched_camera_frustum() {
let proj = DMat4::perspective_rh(std::f64::consts::FRAC_PI_4, 1.5, 1.0, 5000.0);
let eye = glam::DVec3::new(0.0, -500.0, 500.0);
let target = glam::DVec3::ZERO;
let view = DMat4::look_at_rh(eye, target, glam::DVec3::Z);
let vp = proj * view;
let frustum = Frustum::from_view_projection(&vp);
let ahead = WorldBounds::new(
WorldCoord::new(-100.0, 100.0, 0.0),
WorldCoord::new(100.0, 300.0, 0.0),
);
assert!(frustum.intersects_aabb(&ahead));
let behind = WorldBounds::new(
WorldCoord::new(-100.0, -2000.0, 0.0),
WorldCoord::new(100.0, -1500.0, 0.0),
);
assert!(!frustum.intersects_aabb(&behind));
let far_left = WorldBounds::new(
WorldCoord::new(-5000.0, 0.0, 0.0),
WorldCoord::new(-4000.0, 100.0, 0.0),
);
assert!(!frustum.intersects_aabb(&far_left));
}
#[test]
fn contains_point_inside() {
let proj = DMat4::perspective_rh(std::f64::consts::FRAC_PI_4, 1.0, 0.1, 1000.0);
let view = DMat4::look_at_rh(
glam::DVec3::new(0.0, 0.0, 10.0),
glam::DVec3::ZERO,
glam::DVec3::Y,
);
let frustum = Frustum::from_view_projection(&(proj * view));
assert!(frustum.contains_point(&WorldCoord::new(0.0, 0.0, 0.0)));
}
#[test]
fn contains_point_outside() {
let proj = DMat4::perspective_rh(std::f64::consts::FRAC_PI_4, 1.0, 0.1, 1000.0);
let view = DMat4::look_at_rh(
glam::DVec3::new(0.0, 0.0, 10.0),
glam::DVec3::ZERO,
glam::DVec3::Y,
);
let frustum = Frustum::from_view_projection(&(proj * view));
assert!(!frustum.contains_point(&WorldCoord::new(0.0, 0.0, 50.0)));
assert!(!frustum.contains_point(&WorldCoord::new(1000.0, 0.0, 0.0)));
}
#[test]
fn intersects_sphere_inside() {
let proj = DMat4::perspective_rh(std::f64::consts::FRAC_PI_4, 1.0, 0.1, 1000.0);
let view = DMat4::look_at_rh(
glam::DVec3::new(0.0, 0.0, 10.0),
glam::DVec3::ZERO,
glam::DVec3::Y,
);
let frustum = Frustum::from_view_projection(&(proj * view));
assert!(frustum.intersects_sphere(&WorldCoord::new(0.0, 0.0, 0.0), 1.0));
}
#[test]
fn intersects_sphere_partially_outside() {
let vp = DMat4::orthographic_rh(-100.0, 100.0, -100.0, 100.0, -100.0, 100.0);
let frustum = Frustum::from_view_projection(&vp);
assert!(frustum.intersects_sphere(&WorldCoord::new(105.0, 0.0, 0.0), 10.0));
assert!(!frustum.intersects_sphere(&WorldCoord::new(200.0, 0.0, 0.0), 10.0));
}
#[test]
fn intersects_sphere_zero_radius_degrades_to_point() {
let proj = DMat4::perspective_rh(std::f64::consts::FRAC_PI_4, 1.0, 0.1, 1000.0);
let view = DMat4::look_at_rh(
glam::DVec3::new(0.0, 0.0, 10.0),
glam::DVec3::ZERO,
glam::DVec3::Y,
);
let frustum = Frustum::from_view_projection(&(proj * view));
let inside = WorldCoord::new(0.0, 0.0, 0.0);
let outside = WorldCoord::new(0.0, 0.0, 50.0);
assert_eq!(
frustum.intersects_sphere(&inside, 0.0),
frustum.contains_point(&inside),
);
assert_eq!(
frustum.intersects_sphere(&outside, 0.0),
frustum.contains_point(&outside),
);
}
#[test]
fn plane_partial_eq() {
let a = Plane::new([1.0, 0.0, 0.0], 5.0);
let b = Plane::new([1.0, 0.0, 0.0], 5.0);
assert_eq!(a, b);
}
}