Path: blob/main/crates/bevy_math/src/primitives/view_frustum.rs
9367 views
use crate::{primitives::HalfSpace, Mat4, Vec3, Vec4};12#[cfg(feature = "bevy_reflect")]3use bevy_reflect::{std_traits::ReflectDefault, Reflect};4#[cfg(all(feature = "serialize", feature = "bevy_reflect"))]5use bevy_reflect::{ReflectDeserialize, ReflectSerialize};67/// A region of 3D space defined by the intersection of 6 [`HalfSpace`]s.8///9/// View Frustums are typically an apex-truncated square pyramid (a pyramid without the top) or a cuboid.10///11/// Half spaces are ordered left, right, top, bottom, near, far. The normal vectors12/// of the half-spaces point towards the interior of the frustum.13#[derive(Clone, Copy, Debug, Default, PartialEq)]14#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]15#[cfg_attr(16feature = "bevy_reflect",17derive(Reflect),18reflect(Clone, Debug, Default, PartialEq)19)]20#[cfg_attr(21all(feature = "serialize", feature = "bevy_reflect"),22reflect(Serialize, Deserialize)23)]24pub struct ViewFrustum {25/// The six half-spaces making up the frustum26pub half_spaces: [HalfSpace; 6],27}2829impl ViewFrustum {30/// The index for the near plane in `half_spaces`31pub const NEAR_PLANE_IDX: usize = 4;32/// The index for the far plane in `half_spaces`33pub const FAR_PLANE_IDX: usize = 5;34/// Vec4 representing an inactive half space.35/// The bisecting plane's unit normal is set to (0, 0, 0).36/// The signed distance along the normal from the plane to the origin is set to `f32::INFINITY`.37const INACTIVE_HALF_SPACE: Vec4 = Vec4::new(0.0, 0.0, 0.0, f32::INFINITY);3839/// Returns a view frustum derived from `clip_from_world`.40#[inline]41pub fn from_clip_from_world(clip_from_world: &Mat4) -> Self {42let mut frustum = ViewFrustum::from_clip_from_world_no_far(clip_from_world);43frustum.half_spaces[Self::FAR_PLANE_IDX] = HalfSpace::new(clip_from_world.row(2));44frustum45}4647/// Returns a view frustum derived from `clip_from_world`,48/// but with a custom far plane.49#[inline]50pub fn from_clip_from_world_custom_far(51clip_from_world: &Mat4,52view_translation: &Vec3,53view_backward: &Vec3,54far: f32,55) -> Self {56let mut frustum = ViewFrustum::from_clip_from_world_no_far(clip_from_world);57let far_center = *view_translation - far * *view_backward;58frustum.half_spaces[Self::FAR_PLANE_IDX] =59HalfSpace::new(view_backward.extend(-view_backward.dot(far_center)));60frustum61}6263/// Calculates the corners of this frustum. Returns `None` if the frustum isn't properly defined.64///65/// If `Some`, the corners are returned in the following order:66/// near top left, near top right, near bottom right, near bottom left,67/// far top left, far top right, far bottom right, far bottom left.68/// If the far plane is an inactive half space, the intersection points69/// that include the far plane will be `Vec3::NAN`.70#[inline]71pub fn corners(&self) -> Option<[Vec3; 8]> {72let [left, right, top, bottom, near, far] = self.half_spaces;73Some([74HalfSpace::intersection_point(top, left, near)?,75HalfSpace::intersection_point(top, right, near)?,76HalfSpace::intersection_point(bottom, right, near)?,77HalfSpace::intersection_point(bottom, left, near)?,78HalfSpace::intersection_point(top, left, far)?,79HalfSpace::intersection_point(top, right, far)?,80HalfSpace::intersection_point(bottom, right, far)?,81HalfSpace::intersection_point(bottom, left, far)?,82])83}8485// NOTE: This approach of extracting the frustum half-space from the view86// projection matrix is from Foundations of Game Engine Development 287// Rendering by Lengyel.88/// Returns a view frustum derived from `view_projection`,89/// without a far plane.90fn from_clip_from_world_no_far(clip_from_world: &Mat4) -> Self {91let row0 = clip_from_world.row(0);92let row1 = clip_from_world.row(1);93let row2 = clip_from_world.row(2);94let row3 = clip_from_world.row(3);9596Self {97half_spaces: [98HalfSpace::new(row3 + row0),99HalfSpace::new(row3 - row0),100HalfSpace::new(row3 + row1),101HalfSpace::new(row3 - row1),102HalfSpace::new(row3 + row2),103HalfSpace::new(Self::INACTIVE_HALF_SPACE),104],105}106}107}108109#[cfg(test)]110mod view_frustum_tests {111use core::f32::consts::FRAC_1_SQRT_2;112113use approx::assert_relative_eq;114115use super::ViewFrustum;116use crate::{primitives::HalfSpace, Vec3, Vec4};117118#[test]119fn cuboid_frustum_corners() {120let cuboid_frustum = ViewFrustum {121// left: x = -5; right: x = 4122// near: y = 0; far: y = 6123// top: z = 3; bottom: z = -2124half_spaces: [125// left: yz plane at x = -5126HalfSpace::new(Vec4::new(1., 0., 0., 5.)),127// right: yz plane at x = 4128HalfSpace::new(Vec4::new(-1., 0., 0., 4.)),129// top: xy plane at z = 3130HalfSpace::new(Vec4::new(0., 0., -1., 3.)),131// bottom: xy plane at z = -2132HalfSpace::new(Vec4::new(0., 0., 1., 2.)),133// near: xz plane at origin (y = 0)134HalfSpace::new(Vec4::new(0., 1., 0., 0.)),135// far: xz plane at y = 6136HalfSpace::new(Vec4::new(0., -1., 0., 6.)),137],138};139let corners = cuboid_frustum.corners().unwrap();140// near top left141assert_relative_eq!(corners[0], Vec3::new(-5., 0., 3.), epsilon = 2e-7);142// near top right143assert_relative_eq!(corners[1], Vec3::new(4., 0., 3.), epsilon = 2e-7);144// near bottom right145assert_relative_eq!(corners[2], Vec3::new(4., 0., -2.), epsilon = 2e-7);146// near bottom left147assert_relative_eq!(corners[3], Vec3::new(-5., 0., -2.), epsilon = 2e-7);148// far top left149assert_relative_eq!(corners[4], Vec3::new(-5., 6., 3.), epsilon = 2e-7);150// far top right151assert_relative_eq!(corners[5], Vec3::new(4., 6., 3.), epsilon = 2e-7);152// far bottom right153assert_relative_eq!(corners[6], Vec3::new(4., 6., -2.), epsilon = 2e-7);154// far bottom left155assert_relative_eq!(corners[7], Vec3::new(-5., 6., -2.), epsilon = 2e-7);156}157158#[test]159fn pyramid_frustum_corners() {160// a frustum where the near plane intersects the left right top and bottom planes161// at a single point162let pyramid_frustum = ViewFrustum {163half_spaces: [164// left165HalfSpace::new(Vec4::new(FRAC_1_SQRT_2, FRAC_1_SQRT_2, 0., FRAC_1_SQRT_2)),166// right167HalfSpace::new(Vec4::new(-FRAC_1_SQRT_2, FRAC_1_SQRT_2, 0., FRAC_1_SQRT_2)),168// top169HalfSpace::new(Vec4::new(0., FRAC_1_SQRT_2, -FRAC_1_SQRT_2, FRAC_1_SQRT_2)),170// bottom171HalfSpace::new(Vec4::new(0., FRAC_1_SQRT_2, FRAC_1_SQRT_2, FRAC_1_SQRT_2)),172// near: xz plane at y = -1173HalfSpace::new(Vec4::new(0., 1., 0., 1.)),174// far: xz plane at y = 3175HalfSpace::new(Vec4::new(0., -1., 0., 3.)),176],177};178let corners = pyramid_frustum.corners().unwrap();179// near top left180assert_relative_eq!(corners[0], Vec3::new(0., -1., 0.), epsilon = 2e-7);181// near top right182assert_relative_eq!(corners[1], Vec3::new(0., -1., 0.), epsilon = 2e-7);183// near bottom right184assert_relative_eq!(corners[2], Vec3::new(0., -1., 0.), epsilon = 2e-7);185// near bottom left186assert_relative_eq!(corners[3], Vec3::new(0., -1., 0.), epsilon = 2e-7);187// far top left188assert_relative_eq!(corners[4], Vec3::new(-4., 3., 4.), epsilon = 2e-7);189// far top right190assert_relative_eq!(corners[5], Vec3::new(4., 3., 4.), epsilon = 2e-7);191// far bottom right192assert_relative_eq!(corners[6], Vec3::new(4., 3., -4.), epsilon = 2e-7);193// far bottom left194assert_relative_eq!(corners[7], Vec3::new(-4., 3., -4.), epsilon = 2e-7);195}196197#[test]198fn frustum_with_some_nan_corners() {199// frustum with no far plane has NAN far corners200let no_far = ViewFrustum {201half_spaces: [202// left: a yz plane rotated outwards203HalfSpace::new(Vec4::new(FRAC_1_SQRT_2, FRAC_1_SQRT_2, 0., FRAC_1_SQRT_2)),204// right: a yz plane rotated outwards205HalfSpace::new(Vec4::new(-FRAC_1_SQRT_2, FRAC_1_SQRT_2, 0., FRAC_1_SQRT_2)),206// top: an xz plane rotated outwards207HalfSpace::new(Vec4::new(0., FRAC_1_SQRT_2, -FRAC_1_SQRT_2, FRAC_1_SQRT_2)),208// bottom: xz plane rotated outwards209HalfSpace::new(Vec4::new(0., FRAC_1_SQRT_2, FRAC_1_SQRT_2, FRAC_1_SQRT_2)),210// near: xz plane at origin (y = 0)211HalfSpace::new(Vec4::new(0., 1., 0., 0.)),212// far213HalfSpace::new(ViewFrustum::INACTIVE_HALF_SPACE),214],215};216let corners = no_far.corners().unwrap();217// near top left218assert_relative_eq!(corners[0], Vec3::new(-1., 0., 1.), epsilon = 2e-7);219// near top right220assert_relative_eq!(corners[1], Vec3::new(1., 0., 1.), epsilon = 2e-7);221// near bottom right222assert_relative_eq!(corners[2], Vec3::new(1., 0., -1.), epsilon = 2e-7);223// near bottom left224assert_relative_eq!(corners[3], Vec3::new(-1., 0., -1.), epsilon = 2e-7);225// far top left226assert!(corners[4].is_nan());227// far top right228assert!(corners[5].is_nan());229// far bottom right230assert!(corners[6].is_nan());231// far bottom left232assert!(corners[7].is_nan());233}234235#[test]236fn invalid_frustum_corners() {237let invalid = ViewFrustum {238half_spaces: [239// the left and the top half spaces are the same, resulting in no intersection point240HalfSpace::new(Vec4::new(FRAC_1_SQRT_2, FRAC_1_SQRT_2, 0., FRAC_1_SQRT_2)),241HalfSpace::new(Vec4::new(-FRAC_1_SQRT_2, FRAC_1_SQRT_2, 0., -FRAC_1_SQRT_2)),242HalfSpace::new(Vec4::new(FRAC_1_SQRT_2, FRAC_1_SQRT_2, 0., FRAC_1_SQRT_2)),243HalfSpace::new(Vec4::new(0., FRAC_1_SQRT_2, FRAC_1_SQRT_2, FRAC_1_SQRT_2)),244HalfSpace::new(Vec4::new(0., 1., 0., 0.)),245HalfSpace::new(Vec4::new(0., -1., 0., 3.)),246],247};248assert!(invalid.corners().is_none());249}250}251252253