Skip to main content

bevy_math/primitives/
view_frustum.rs

1use crate::{primitives::HalfSpace, Mat4, Vec3, Vec4};
2
3#[cfg(feature = "bevy_reflect")]
4use bevy_reflect::{std_traits::ReflectDefault, Reflect};
5#[cfg(all(feature = "serialize", feature = "bevy_reflect"))]
6use bevy_reflect::{ReflectDeserialize, ReflectSerialize};
7
8/// A region of 3D space defined by the intersection of 6 [`HalfSpace`]s.
9///
10/// View Frustums are typically an apex-truncated square pyramid (a pyramid without the top) or a cuboid.
11///
12/// Half spaces are ordered left, right, top, bottom, near, far. The normal vectors
13/// of the half-spaces point towards the interior of the frustum.
14#[derive(Clone, Copy, Debug, Default, PartialEq)]
15#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
16#[cfg_attr(
17    feature = "bevy_reflect",
18    derive(Reflect),
19    reflect(Clone, Debug, Default, PartialEq)
20)]
21#[cfg_attr(
22    all(feature = "serialize", feature = "bevy_reflect"),
23    reflect(Serialize, Deserialize)
24)]
25pub struct ViewFrustum {
26    /// The six half-spaces making up the frustum
27    pub half_spaces: [HalfSpace; 6],
28}
29
30impl ViewFrustum {
31    /// The index for the near plane in `half_spaces`
32    pub const NEAR_PLANE_IDX: usize = 4;
33    /// The index for the far plane in `half_spaces`
34    pub const FAR_PLANE_IDX: usize = 5;
35    /// Vec4 representing an inactive half space.
36    /// The bisecting plane's unit normal is set to (0, 0, 0).
37    /// The signed distance along the normal from the plane to the origin is set to `f32::INFINITY`.
38    const INACTIVE_HALF_SPACE: Vec4 = Vec4::new(0.0, 0.0, 0.0, f32::INFINITY);
39
40    /// Returns a view frustum derived from `clip_from_world`.
41    #[inline]
42    pub fn from_clip_from_world(clip_from_world: &Mat4) -> Self {
43        let mut frustum = ViewFrustum::from_clip_from_world_no_far(clip_from_world);
44        frustum.half_spaces[Self::FAR_PLANE_IDX] = HalfSpace::new(clip_from_world.row(2));
45        frustum
46    }
47
48    /// Returns a view frustum derived from `clip_from_world`,
49    /// but with a custom far plane.
50    #[inline]
51    pub fn from_clip_from_world_custom_far(
52        clip_from_world: &Mat4,
53        view_translation: &Vec3,
54        view_backward: &Vec3,
55        far: f32,
56    ) -> Self {
57        let mut frustum = ViewFrustum::from_clip_from_world_no_far(clip_from_world);
58        let far_center = *view_translation - far * *view_backward;
59        frustum.half_spaces[Self::FAR_PLANE_IDX] =
60            HalfSpace::new(view_backward.extend(-view_backward.dot(far_center)));
61        frustum
62    }
63
64    /// Calculates the corners of this frustum. Returns `None` if the frustum isn't properly defined.
65    ///
66    /// If `Some`, the corners are returned in the following order:
67    /// near top left, near top right, near bottom right, near bottom left,
68    /// far top left, far top right, far bottom right, far bottom left.
69    /// If the far plane is an inactive half space, the intersection points
70    /// that include the far plane will be `Vec3::NAN`.
71    #[inline]
72    pub fn corners(&self) -> Option<[Vec3; 8]> {
73        let [left, right, top, bottom, near, far] = self.half_spaces;
74        Some([
75            HalfSpace::intersection_point(top, left, near)?,
76            HalfSpace::intersection_point(top, right, near)?,
77            HalfSpace::intersection_point(bottom, right, near)?,
78            HalfSpace::intersection_point(bottom, left, near)?,
79            HalfSpace::intersection_point(top, left, far)?,
80            HalfSpace::intersection_point(top, right, far)?,
81            HalfSpace::intersection_point(bottom, right, far)?,
82            HalfSpace::intersection_point(bottom, left, far)?,
83        ])
84    }
85
86    // NOTE: This approach of extracting the frustum half-space from the view
87    // projection matrix is from Foundations of Game Engine Development 2
88    // Rendering by Lengyel.
89    /// Returns a view frustum derived from `view_projection`,
90    /// without a far plane.
91    fn from_clip_from_world_no_far(clip_from_world: &Mat4) -> Self {
92        let row0 = clip_from_world.row(0);
93        let row1 = clip_from_world.row(1);
94        let row2 = clip_from_world.row(2);
95        let row3 = clip_from_world.row(3);
96
97        Self {
98            half_spaces: [
99                HalfSpace::new(row3 + row0),
100                HalfSpace::new(row3 - row0),
101                HalfSpace::new(row3 + row1),
102                HalfSpace::new(row3 - row1),
103                HalfSpace::new(row3 + row2),
104                HalfSpace::new(Self::INACTIVE_HALF_SPACE),
105            ],
106        }
107    }
108}
109
110#[cfg(test)]
111mod view_frustum_tests {
112    use core::f32::consts::FRAC_1_SQRT_2;
113
114    use approx::assert_relative_eq;
115
116    use super::ViewFrustum;
117    use crate::{primitives::HalfSpace, Vec3, Vec4};
118
119    #[test]
120    fn cuboid_frustum_corners() {
121        let cuboid_frustum = ViewFrustum {
122            // left: x = -5; right: x = 4
123            // near: y = 0; far: y = 6
124            // top: z = 3; bottom: z = -2
125            half_spaces: [
126                // left: yz plane at x = -5
127                HalfSpace::new(Vec4::new(1., 0., 0., 5.)),
128                // right: yz plane at x = 4
129                HalfSpace::new(Vec4::new(-1., 0., 0., 4.)),
130                // top: xy plane at z = 3
131                HalfSpace::new(Vec4::new(0., 0., -1., 3.)),
132                // bottom: xy plane at z = -2
133                HalfSpace::new(Vec4::new(0., 0., 1., 2.)),
134                // near: xz plane at origin (y = 0)
135                HalfSpace::new(Vec4::new(0., 1., 0., 0.)),
136                // far: xz plane at y = 6
137                HalfSpace::new(Vec4::new(0., -1., 0., 6.)),
138            ],
139        };
140        let corners = cuboid_frustum.corners().unwrap();
141        // near top left
142        assert_relative_eq!(corners[0], Vec3::new(-5., 0., 3.), epsilon = 2e-7);
143        // near top right
144        assert_relative_eq!(corners[1], Vec3::new(4., 0., 3.), epsilon = 2e-7);
145        // near bottom right
146        assert_relative_eq!(corners[2], Vec3::new(4., 0., -2.), epsilon = 2e-7);
147        // near bottom left
148        assert_relative_eq!(corners[3], Vec3::new(-5., 0., -2.), epsilon = 2e-7);
149        // far top left
150        assert_relative_eq!(corners[4], Vec3::new(-5., 6., 3.), epsilon = 2e-7);
151        // far top right
152        assert_relative_eq!(corners[5], Vec3::new(4., 6., 3.), epsilon = 2e-7);
153        // far bottom right
154        assert_relative_eq!(corners[6], Vec3::new(4., 6., -2.), epsilon = 2e-7);
155        // far bottom left
156        assert_relative_eq!(corners[7], Vec3::new(-5., 6., -2.), epsilon = 2e-7);
157    }
158
159    #[test]
160    fn pyramid_frustum_corners() {
161        // a frustum where the near plane intersects the left right top and bottom planes
162        // at a single point
163        let pyramid_frustum = ViewFrustum {
164            half_spaces: [
165                // left
166                HalfSpace::new(Vec4::new(FRAC_1_SQRT_2, FRAC_1_SQRT_2, 0., FRAC_1_SQRT_2)),
167                // right
168                HalfSpace::new(Vec4::new(-FRAC_1_SQRT_2, FRAC_1_SQRT_2, 0., FRAC_1_SQRT_2)),
169                // top
170                HalfSpace::new(Vec4::new(0., FRAC_1_SQRT_2, -FRAC_1_SQRT_2, FRAC_1_SQRT_2)),
171                // bottom
172                HalfSpace::new(Vec4::new(0., FRAC_1_SQRT_2, FRAC_1_SQRT_2, FRAC_1_SQRT_2)),
173                // near: xz plane at y = -1
174                HalfSpace::new(Vec4::new(0., 1., 0., 1.)),
175                // far: xz plane at y = 3
176                HalfSpace::new(Vec4::new(0., -1., 0., 3.)),
177            ],
178        };
179        let corners = pyramid_frustum.corners().unwrap();
180        // near top left
181        assert_relative_eq!(corners[0], Vec3::new(0., -1., 0.), epsilon = 2e-7);
182        // near top right
183        assert_relative_eq!(corners[1], Vec3::new(0., -1., 0.), epsilon = 2e-7);
184        // near bottom right
185        assert_relative_eq!(corners[2], Vec3::new(0., -1., 0.), epsilon = 2e-7);
186        // near bottom left
187        assert_relative_eq!(corners[3], Vec3::new(0., -1., 0.), epsilon = 2e-7);
188        // far top left
189        assert_relative_eq!(corners[4], Vec3::new(-4., 3., 4.), epsilon = 2e-7);
190        // far top right
191        assert_relative_eq!(corners[5], Vec3::new(4., 3., 4.), epsilon = 2e-7);
192        // far bottom right
193        assert_relative_eq!(corners[6], Vec3::new(4., 3., -4.), epsilon = 2e-7);
194        // far bottom left
195        assert_relative_eq!(corners[7], Vec3::new(-4., 3., -4.), epsilon = 2e-7);
196    }
197
198    #[test]
199    fn frustum_with_some_nan_corners() {
200        // frustum with no far plane has NAN far corners
201        let no_far = ViewFrustum {
202            half_spaces: [
203                // left: a yz plane rotated outwards
204                HalfSpace::new(Vec4::new(FRAC_1_SQRT_2, FRAC_1_SQRT_2, 0., FRAC_1_SQRT_2)),
205                // right: a yz plane rotated outwards
206                HalfSpace::new(Vec4::new(-FRAC_1_SQRT_2, FRAC_1_SQRT_2, 0., FRAC_1_SQRT_2)),
207                // top: an xz plane rotated outwards
208                HalfSpace::new(Vec4::new(0., FRAC_1_SQRT_2, -FRAC_1_SQRT_2, FRAC_1_SQRT_2)),
209                // bottom: xz plane rotated outwards
210                HalfSpace::new(Vec4::new(0., FRAC_1_SQRT_2, FRAC_1_SQRT_2, FRAC_1_SQRT_2)),
211                // near: xz plane at origin (y = 0)
212                HalfSpace::new(Vec4::new(0., 1., 0., 0.)),
213                // far
214                HalfSpace::new(ViewFrustum::INACTIVE_HALF_SPACE),
215            ],
216        };
217        let corners = no_far.corners().unwrap();
218        // near top left
219        assert_relative_eq!(corners[0], Vec3::new(-1., 0., 1.), epsilon = 2e-7);
220        // near top right
221        assert_relative_eq!(corners[1], Vec3::new(1., 0., 1.), epsilon = 2e-7);
222        // near bottom right
223        assert_relative_eq!(corners[2], Vec3::new(1., 0., -1.), epsilon = 2e-7);
224        // near bottom left
225        assert_relative_eq!(corners[3], Vec3::new(-1., 0., -1.), epsilon = 2e-7);
226        // far top left
227        assert!(corners[4].is_nan());
228        // far top right
229        assert!(corners[5].is_nan());
230        // far bottom right
231        assert!(corners[6].is_nan());
232        // far bottom left
233        assert!(corners[7].is_nan());
234    }
235
236    #[test]
237    fn invalid_frustum_corners() {
238        let invalid = ViewFrustum {
239            half_spaces: [
240                // the left and the top half spaces are the same, resulting in no intersection point
241                HalfSpace::new(Vec4::new(FRAC_1_SQRT_2, FRAC_1_SQRT_2, 0., FRAC_1_SQRT_2)),
242                HalfSpace::new(Vec4::new(-FRAC_1_SQRT_2, FRAC_1_SQRT_2, 0., -FRAC_1_SQRT_2)),
243                HalfSpace::new(Vec4::new(FRAC_1_SQRT_2, FRAC_1_SQRT_2, 0., FRAC_1_SQRT_2)),
244                HalfSpace::new(Vec4::new(0., FRAC_1_SQRT_2, FRAC_1_SQRT_2, FRAC_1_SQRT_2)),
245                HalfSpace::new(Vec4::new(0., 1., 0., 0.)),
246                HalfSpace::new(Vec4::new(0., -1., 0., 3.)),
247            ],
248        };
249        assert!(invalid.corners().is_none());
250    }
251}