use crate::aabb::Aabb;
use glam::{Mat4, Vec3A, Vec4};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Intersection {
Outside,
Partial,
Inside,
}
#[derive(Debug, Clone, Copy)]
pub struct Plane {
pub normal: Vec3A,
pub distance: f32,
}
impl Plane {
#[inline]
pub fn from_coefficients(x: f32, y: f32, z: f32, w: f32) -> Self {
let len_sq = x * x + y * y + z * z;
if len_sq < 1e-10 {
return Self {
normal: Vec3A::Z,
distance: 0.0,
};
}
let inv_len = len_sq.sqrt().recip();
Self {
normal: Vec3A::new(x * inv_len, y * inv_len, z * inv_len),
distance: w * inv_len,
}
}
#[inline]
fn from_vec4(v: Vec4) -> Self {
Self::from_coefficients(v.x, v.y, v.z, v.w)
}
#[inline]
pub fn signed_distance(self, pt: Vec3A) -> f32 {
self.normal.dot(pt) + self.distance
}
#[inline]
fn positive_vertex(self, aabb: Aabb) -> Vec3A {
Vec3A::select(self.normal.cmpgt(Vec3A::ZERO), aabb.max, aabb.min)
}
#[inline]
fn negative_vertex(self, aabb: Aabb) -> Vec3A {
Vec3A::select(self.normal.cmpgt(Vec3A::ZERO), aabb.min, aabb.max)
}
}
#[derive(Debug, Clone, Copy)]
pub struct Frustum {
pub planes: [Plane; 6],
}
impl Frustum {
#[inline]
pub fn from_matrix(vp: &Mat4) -> Self {
let r0 = vp.row(0); let r1 = vp.row(1); let r2 = vp.row(2); let r3 = vp.row(3);
Self {
planes: [
Plane::from_vec4(r3 + r0), Plane::from_vec4(r3 - r0), Plane::from_vec4(r3 + r1), Plane::from_vec4(r3 - r1), Plane::from_vec4(r2), Plane::from_vec4(r3 - r2), ],
}
}
#[inline]
pub fn planes(&self) -> &[Plane; 6] {
&self.planes
}
#[inline]
pub fn intersects_sphere(&self, center: impl Into<Vec3A>, radius: f32) -> bool {
let c = center.into();
self.planes.iter().all(|p| p.signed_distance(c) >= -radius)
}
#[inline]
pub fn intersects_obb(
&self,
center: impl Into<Vec3A>,
half_extents: impl Into<Vec3A>,
rotation: glam::Quat,
) -> bool {
self.test_obb(center, half_extents, rotation) != Intersection::Outside
}
#[inline]
pub fn test_obb(
&self,
center: impl Into<Vec3A>,
half_extents: impl Into<Vec3A>,
rotation: glam::Quat,
) -> Intersection {
let center = center.into();
let extents = half_extents.into();
let rot_mat = glam::Mat3A::from_quat(rotation);
let axis_x = rot_mat.x_axis;
let axis_y = rot_mat.y_axis;
let axis_z = rot_mat.z_axis;
let mut all_inside = true;
for plane in &self.planes {
let r = extents.x * plane.normal.dot(axis_x).abs()
+ extents.y * plane.normal.dot(axis_y).abs()
+ extents.z * plane.normal.dot(axis_z).abs();
let d = plane.signed_distance(center);
if d < -r {
return Intersection::Outside;
}
if d < r {
all_inside = false;
}
}
if all_inside {
Intersection::Inside
} else {
Intersection::Partial
}
}
#[inline]
pub fn test_aabb(&self, aabb: Aabb) -> Intersection {
if aabb.is_empty() {
return Intersection::Outside;
}
let mut all_inside = true;
for plane in &self.planes {
if plane.signed_distance(plane.positive_vertex(aabb)) < 0.0 {
return Intersection::Outside;
}
if plane.signed_distance(plane.negative_vertex(aabb)) < 0.0 {
all_inside = false;
}
}
if all_inside {
Intersection::Inside
} else {
Intersection::Partial
}
}
#[inline]
pub fn intersects_aabb(&self, aabb: Aabb) -> bool {
self.test_aabb(aabb) != Intersection::Outside
}
#[inline]
pub fn test_aabb_masked(&self, aabb: Aabb, plane_mask: u8) -> (Intersection, u8) {
if aabb.is_empty() {
return (Intersection::Outside, 0);
}
let mut all_inside = true;
let mut out_mask = 0u8;
for (i, plane) in self.planes.iter().enumerate() {
let bit = 1u8 << i;
if plane_mask & bit == 0 {
continue;
}
if plane.signed_distance(plane.positive_vertex(aabb)) < 0.0 {
return (Intersection::Outside, 0);
}
if plane.signed_distance(plane.negative_vertex(aabb)) < 0.0 {
all_inside = false;
out_mask |= bit; }
}
let result = if all_inside {
Intersection::Inside
} else {
Intersection::Partial
};
(result, out_mask)
}
pub const FULL_MASK: u8 = 0b0011_1111;
}
#[cfg(test)]
mod tests {
use super::{Frustum, Intersection, Plane};
use crate::aabb::Aabb;
use glam::{Mat4, Vec3, Vec3A};
fn make_frustum() -> Frustum {
let view = Mat4::look_at_rh(Vec3::new(0.0, 0.0, 8.0), Vec3::ZERO, Vec3::Y);
let proj = Mat4::perspective_rh(std::f32::consts::FRAC_PI_4, 1.0, 0.1, 100.0);
Frustum::from_matrix(&(proj * view))
}
#[test]
fn plane_signed_distance() {
let p = Plane::from_coefficients(0.0, 0.0, 1.0, 0.0);
assert!((p.signed_distance(Vec3A::new(0.0, 0.0, 5.0)) - 5.0).abs() < 1e-5);
assert!((p.signed_distance(Vec3A::new(0.0, 0.0, -3.0)) + 3.0).abs() < 1e-5);
}
#[test]
fn plane_degenerate_normal() {
let p = Plane::from_coefficients(0.0, 0.0, 0.0, 1.0);
assert!((p.normal - Vec3A::Z).length() < 1e-5);
}
#[test]
fn aabb_fully_inside() {
let frustum = make_frustum();
let cube = Aabb::new(Vec3::splat(-0.5), Vec3::splat(0.5));
assert_eq!(frustum.test_aabb(cube), Intersection::Inside);
}
#[test]
fn aabb_behind_camera_outside() {
let frustum = make_frustum();
let behind = Aabb::new(Vec3::new(-1.0, -1.0, 10.0), Vec3::new(1.0, 1.0, 12.0));
assert_eq!(frustum.test_aabb(behind), Intersection::Outside);
}
#[test]
fn aabb_beyond_far_plane_outside() {
let frustum = make_frustum();
let far = Aabb::new(Vec3::new(-1.0, -1.0, -105.0), Vec3::new(1.0, 1.0, -95.0));
assert_eq!(frustum.test_aabb(far), Intersection::Outside);
}
#[test]
fn aabb_enclosing_frustum_is_partial() {
let frustum = make_frustum();
let huge = Aabb::new(Vec3::splat(-1000.0), Vec3::splat(1000.0));
assert_eq!(frustum.test_aabb(huge), Intersection::Partial);
}
#[test]
fn aabb_degenerate_point_inside() {
let frustum = make_frustum();
let pt = Aabb::new(Vec3::ZERO, Vec3::ZERO);
assert_eq!(frustum.test_aabb(pt), Intersection::Inside);
}
#[test]
fn aabb_degenerate_point_outside() {
let frustum = make_frustum();
let pt = Aabb::new(Vec3::splat(1000.0), Vec3::splat(1000.0));
assert_eq!(frustum.test_aabb(pt), Intersection::Outside);
}
#[test]
fn aabb_empty_is_outside() {
let frustum = make_frustum();
assert_eq!(frustum.test_aabb(Aabb::empty()), Intersection::Outside);
}
#[test]
fn intersects_aabb_convenience() {
let frustum = make_frustum();
let inside = Aabb::new(Vec3::splat(-0.5), Vec3::splat(0.5));
let outside = Aabb::new(Vec3::new(-1.0, -1.0, 10.0), Vec3::new(1.0, 1.0, 12.0));
assert!(frustum.intersects_aabb(inside));
assert!(!frustum.intersects_aabb(outside));
}
#[test]
fn sphere_inside_frustum() {
let frustum = make_frustum();
assert!(frustum.intersects_sphere(Vec3::ZERO, 0.5));
}
#[test]
fn sphere_outside_frustum() {
let frustum = make_frustum();
assert!(!frustum.intersects_sphere(Vec3::new(0.0, 0.0, 50.0), 0.1));
}
#[test]
fn sphere_straddles_plane() {
let frustum = make_frustum();
assert!(frustum.intersects_sphere(Vec3::new(0.0, 0.0, 9.0), 5.0));
}
#[test]
fn masked_test_skip_all_planes() {
let frustum = make_frustum();
let outside = Aabb::new(Vec3::splat(1000.0), Vec3::splat(2000.0));
let (result, _) = frustum.test_aabb_masked(outside, 0);
assert_eq!(result, Intersection::Inside, "zero mask skips all tests");
}
#[test]
fn masked_test_full_mask_matches_unmasked() {
let frustum = make_frustum();
let cube = Aabb::new(Vec3::splat(-0.5), Vec3::splat(0.5));
let unmasked = frustum.test_aabb(cube);
let (masked, _) = frustum.test_aabb_masked(cube, Frustum::FULL_MASK);
assert_eq!(unmasked, masked);
}
}