Skip to main content

bevy_shape/
view_frustum.rs

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