use crate::{primitives::HalfSpace, Mat4, Vec3, Vec4};
#[cfg(feature = "bevy_reflect")]
use bevy_reflect::{std_traits::ReflectDefault, Reflect};
#[cfg(all(feature = "serialize", feature = "bevy_reflect"))]
use bevy_reflect::{ReflectDeserialize, ReflectSerialize};
#[derive(Clone, Copy, Debug, Default, PartialEq)]
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(
feature = "bevy_reflect",
derive(Reflect),
reflect(Clone, Debug, Default, PartialEq)
)]
#[cfg_attr(
all(feature = "serialize", feature = "bevy_reflect"),
reflect(Serialize, Deserialize)
)]
pub struct ViewFrustum {
pub half_spaces: [HalfSpace; 6],
}
impl ViewFrustum {
pub const NEAR_PLANE_IDX: usize = 4;
pub const FAR_PLANE_IDX: usize = 5;
const INACTIVE_HALF_SPACE: Vec4 = Vec4::new(0.0, 0.0, 0.0, f32::INFINITY);
#[inline]
pub fn from_clip_from_world(clip_from_world: &Mat4) -> Self {
let mut frustum = ViewFrustum::from_clip_from_world_no_far(clip_from_world);
frustum.half_spaces[Self::FAR_PLANE_IDX] = HalfSpace::new(clip_from_world.row(2));
frustum
}
#[inline]
pub fn from_clip_from_world_custom_far(
clip_from_world: &Mat4,
view_translation: &Vec3,
view_backward: &Vec3,
far: f32,
) -> Self {
let mut frustum = ViewFrustum::from_clip_from_world_no_far(clip_from_world);
let far_center = *view_translation - far * *view_backward;
frustum.half_spaces[Self::FAR_PLANE_IDX] =
HalfSpace::new(view_backward.extend(-view_backward.dot(far_center)));
frustum
}
#[inline]
pub fn corners(&self) -> Option<[Vec3; 8]> {
let [left, right, top, bottom, near, far] = self.half_spaces;
Some([
HalfSpace::intersection_point(top, left, near)?,
HalfSpace::intersection_point(top, right, near)?,
HalfSpace::intersection_point(bottom, right, near)?,
HalfSpace::intersection_point(bottom, left, near)?,
HalfSpace::intersection_point(top, left, far)?,
HalfSpace::intersection_point(top, right, far)?,
HalfSpace::intersection_point(bottom, right, far)?,
HalfSpace::intersection_point(bottom, left, far)?,
])
}
fn from_clip_from_world_no_far(clip_from_world: &Mat4) -> Self {
let row0 = clip_from_world.row(0);
let row1 = clip_from_world.row(1);
let row2 = clip_from_world.row(2);
let row3 = clip_from_world.row(3);
Self {
half_spaces: [
HalfSpace::new(row3 + row0),
HalfSpace::new(row3 - row0),
HalfSpace::new(row3 + row1),
HalfSpace::new(row3 - row1),
HalfSpace::new(row3 + row2),
HalfSpace::new(Self::INACTIVE_HALF_SPACE),
],
}
}
}
#[cfg(test)]
mod view_frustum_tests {
use core::f32::consts::FRAC_1_SQRT_2;
use approx::assert_relative_eq;
use super::ViewFrustum;
use crate::{primitives::HalfSpace, Vec3, Vec4};
#[test]
fn cuboid_frustum_corners() {
let cuboid_frustum = ViewFrustum {
half_spaces: [
HalfSpace::new(Vec4::new(1., 0., 0., 5.)),
HalfSpace::new(Vec4::new(-1., 0., 0., 4.)),
HalfSpace::new(Vec4::new(0., 0., -1., 3.)),
HalfSpace::new(Vec4::new(0., 0., 1., 2.)),
HalfSpace::new(Vec4::new(0., 1., 0., 0.)),
HalfSpace::new(Vec4::new(0., -1., 0., 6.)),
],
};
let corners = cuboid_frustum.corners().unwrap();
assert_relative_eq!(corners[0], Vec3::new(-5., 0., 3.), epsilon = 2e-7);
assert_relative_eq!(corners[1], Vec3::new(4., 0., 3.), epsilon = 2e-7);
assert_relative_eq!(corners[2], Vec3::new(4., 0., -2.), epsilon = 2e-7);
assert_relative_eq!(corners[3], Vec3::new(-5., 0., -2.), epsilon = 2e-7);
assert_relative_eq!(corners[4], Vec3::new(-5., 6., 3.), epsilon = 2e-7);
assert_relative_eq!(corners[5], Vec3::new(4., 6., 3.), epsilon = 2e-7);
assert_relative_eq!(corners[6], Vec3::new(4., 6., -2.), epsilon = 2e-7);
assert_relative_eq!(corners[7], Vec3::new(-5., 6., -2.), epsilon = 2e-7);
}
#[test]
fn pyramid_frustum_corners() {
let pyramid_frustum = ViewFrustum {
half_spaces: [
HalfSpace::new(Vec4::new(FRAC_1_SQRT_2, FRAC_1_SQRT_2, 0., FRAC_1_SQRT_2)),
HalfSpace::new(Vec4::new(-FRAC_1_SQRT_2, FRAC_1_SQRT_2, 0., FRAC_1_SQRT_2)),
HalfSpace::new(Vec4::new(0., FRAC_1_SQRT_2, -FRAC_1_SQRT_2, FRAC_1_SQRT_2)),
HalfSpace::new(Vec4::new(0., FRAC_1_SQRT_2, FRAC_1_SQRT_2, FRAC_1_SQRT_2)),
HalfSpace::new(Vec4::new(0., 1., 0., 1.)),
HalfSpace::new(Vec4::new(0., -1., 0., 3.)),
],
};
let corners = pyramid_frustum.corners().unwrap();
assert_relative_eq!(corners[0], Vec3::new(0., -1., 0.), epsilon = 2e-7);
assert_relative_eq!(corners[1], Vec3::new(0., -1., 0.), epsilon = 2e-7);
assert_relative_eq!(corners[2], Vec3::new(0., -1., 0.), epsilon = 2e-7);
assert_relative_eq!(corners[3], Vec3::new(0., -1., 0.), epsilon = 2e-7);
assert_relative_eq!(corners[4], Vec3::new(-4., 3., 4.), epsilon = 2e-7);
assert_relative_eq!(corners[5], Vec3::new(4., 3., 4.), epsilon = 2e-7);
assert_relative_eq!(corners[6], Vec3::new(4., 3., -4.), epsilon = 2e-7);
assert_relative_eq!(corners[7], Vec3::new(-4., 3., -4.), epsilon = 2e-7);
}
#[test]
fn frustum_with_some_nan_corners() {
let no_far = ViewFrustum {
half_spaces: [
HalfSpace::new(Vec4::new(FRAC_1_SQRT_2, FRAC_1_SQRT_2, 0., FRAC_1_SQRT_2)),
HalfSpace::new(Vec4::new(-FRAC_1_SQRT_2, FRAC_1_SQRT_2, 0., FRAC_1_SQRT_2)),
HalfSpace::new(Vec4::new(0., FRAC_1_SQRT_2, -FRAC_1_SQRT_2, FRAC_1_SQRT_2)),
HalfSpace::new(Vec4::new(0., FRAC_1_SQRT_2, FRAC_1_SQRT_2, FRAC_1_SQRT_2)),
HalfSpace::new(Vec4::new(0., 1., 0., 0.)),
HalfSpace::new(ViewFrustum::INACTIVE_HALF_SPACE),
],
};
let corners = no_far.corners().unwrap();
assert_relative_eq!(corners[0], Vec3::new(-1., 0., 1.), epsilon = 2e-7);
assert_relative_eq!(corners[1], Vec3::new(1., 0., 1.), epsilon = 2e-7);
assert_relative_eq!(corners[2], Vec3::new(1., 0., -1.), epsilon = 2e-7);
assert_relative_eq!(corners[3], Vec3::new(-1., 0., -1.), epsilon = 2e-7);
assert!(corners[4].is_nan());
assert!(corners[5].is_nan());
assert!(corners[6].is_nan());
assert!(corners[7].is_nan());
}
#[test]
fn invalid_frustum_corners() {
let invalid = ViewFrustum {
half_spaces: [
HalfSpace::new(Vec4::new(FRAC_1_SQRT_2, FRAC_1_SQRT_2, 0., FRAC_1_SQRT_2)),
HalfSpace::new(Vec4::new(-FRAC_1_SQRT_2, FRAC_1_SQRT_2, 0., -FRAC_1_SQRT_2)),
HalfSpace::new(Vec4::new(FRAC_1_SQRT_2, FRAC_1_SQRT_2, 0., FRAC_1_SQRT_2)),
HalfSpace::new(Vec4::new(0., FRAC_1_SQRT_2, FRAC_1_SQRT_2, FRAC_1_SQRT_2)),
HalfSpace::new(Vec4::new(0., 1., 0., 0.)),
HalfSpace::new(Vec4::new(0., -1., 0., 3.)),
],
};
assert!(invalid.corners().is_none());
}
}