Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
bevyengine
GitHub Repository: bevyengine/bevy
Path: blob/main/crates/bevy_math/src/primitives/view_frustum.rs
9367 views
1
use crate::{primitives::HalfSpace, Mat4, Vec3, Vec4};
2
3
#[cfg(feature = "bevy_reflect")]
4
use bevy_reflect::{std_traits::ReflectDefault, Reflect};
5
#[cfg(all(feature = "serialize", feature = "bevy_reflect"))]
6
use 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
)]
25
pub struct ViewFrustum {
26
/// The six half-spaces making up the frustum
27
pub half_spaces: [HalfSpace; 6],
28
}
29
30
impl 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)]
111
mod 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
}
252
253