Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
bevyengine
GitHub Repository: bevyengine/bevy
Path: blob/main/crates/bevy_camera/src/primitives.rs
6598 views
1
use core::borrow::Borrow;
2
3
use bevy_ecs::{component::Component, entity::EntityHashMap, reflect::ReflectComponent};
4
use bevy_math::{Affine3A, Mat3A, Mat4, Vec3, Vec3A, Vec4, Vec4Swizzles};
5
use bevy_mesh::{Mesh, VertexAttributeValues};
6
use bevy_reflect::prelude::*;
7
8
pub trait MeshAabb {
9
/// Compute the Axis-Aligned Bounding Box of the mesh vertices in model space
10
///
11
/// Returns `None` if `self` doesn't have [`Mesh::ATTRIBUTE_POSITION`] of
12
/// type [`VertexAttributeValues::Float32x3`], or if `self` doesn't have any vertices.
13
fn compute_aabb(&self) -> Option<Aabb>;
14
}
15
16
impl MeshAabb for Mesh {
17
fn compute_aabb(&self) -> Option<Aabb> {
18
let Some(VertexAttributeValues::Float32x3(values)) =
19
self.attribute(Mesh::ATTRIBUTE_POSITION)
20
else {
21
return None;
22
};
23
24
Aabb::enclosing(values.iter().map(|p| Vec3::from_slice(p)))
25
}
26
}
27
28
/// An axis-aligned bounding box, defined by:
29
/// - a center,
30
/// - the distances from the center to each faces along the axis,
31
/// the faces are orthogonal to the axis.
32
///
33
/// It is typically used as a component on an entity to represent the local space
34
/// occupied by this entity, with faces orthogonal to its local axis.
35
///
36
/// This component is notably used during "frustum culling", a process to determine
37
/// if an entity should be rendered by a [`Camera`] if its bounding box intersects
38
/// with the camera's [`Frustum`].
39
///
40
/// It will be added automatically by the systems in [`CalculateBounds`] to entities that:
41
/// - could be subject to frustum culling, for example with a [`Mesh3d`]
42
/// or `Sprite` component,
43
/// - don't have the [`NoFrustumCulling`] component.
44
///
45
/// It won't be updated automatically if the space occupied by the entity changes,
46
/// for example if the vertex positions of a [`Mesh3d`] are updated.
47
///
48
/// [`Camera`]: crate::Camera
49
/// [`NoFrustumCulling`]: crate::visibility::NoFrustumCulling
50
/// [`CalculateBounds`]: crate::visibility::VisibilitySystems::CalculateBounds
51
/// [`Mesh3d`]: bevy_mesh::Mesh
52
#[derive(Component, Clone, Copy, Debug, Default, Reflect, PartialEq)]
53
#[reflect(Component, Default, Debug, PartialEq, Clone)]
54
pub struct Aabb {
55
pub center: Vec3A,
56
pub half_extents: Vec3A,
57
}
58
59
impl Aabb {
60
#[inline]
61
pub fn from_min_max(minimum: Vec3, maximum: Vec3) -> Self {
62
let minimum = Vec3A::from(minimum);
63
let maximum = Vec3A::from(maximum);
64
let center = 0.5 * (maximum + minimum);
65
let half_extents = 0.5 * (maximum - minimum);
66
Self {
67
center,
68
half_extents,
69
}
70
}
71
72
/// Returns a bounding box enclosing the specified set of points.
73
///
74
/// Returns `None` if the iterator is empty.
75
///
76
/// # Examples
77
///
78
/// ```
79
/// # use bevy_math::{Vec3, Vec3A};
80
/// # use bevy_camera::primitives::Aabb;
81
/// let bb = Aabb::enclosing([Vec3::X, Vec3::Z * 2.0, Vec3::Y * -0.5]).unwrap();
82
/// assert_eq!(bb.min(), Vec3A::new(0.0, -0.5, 0.0));
83
/// assert_eq!(bb.max(), Vec3A::new(1.0, 0.0, 2.0));
84
/// ```
85
pub fn enclosing<T: Borrow<Vec3>>(iter: impl IntoIterator<Item = T>) -> Option<Self> {
86
let mut iter = iter.into_iter().map(|p| *p.borrow());
87
let mut min = iter.next()?;
88
let mut max = min;
89
for v in iter {
90
min = Vec3::min(min, v);
91
max = Vec3::max(max, v);
92
}
93
Some(Self::from_min_max(min, max))
94
}
95
96
/// Calculate the relative radius of the AABB with respect to a plane
97
#[inline]
98
pub fn relative_radius(&self, p_normal: &Vec3A, world_from_local: &Mat3A) -> f32 {
99
// NOTE: dot products on Vec3A use SIMD and even with the overhead of conversion are net faster than Vec3
100
let half_extents = self.half_extents;
101
Vec3A::new(
102
p_normal.dot(world_from_local.x_axis),
103
p_normal.dot(world_from_local.y_axis),
104
p_normal.dot(world_from_local.z_axis),
105
)
106
.abs()
107
.dot(half_extents)
108
}
109
110
#[inline]
111
pub fn min(&self) -> Vec3A {
112
self.center - self.half_extents
113
}
114
115
#[inline]
116
pub fn max(&self) -> Vec3A {
117
self.center + self.half_extents
118
}
119
120
/// Check if the AABB is at the front side of the bisecting plane.
121
/// Referenced from: [AABB Plane intersection](https://gdbooks.gitbooks.io/3dcollisions/content/Chapter2/static_aabb_plane.html)
122
#[inline]
123
pub fn is_in_half_space(&self, half_space: &HalfSpace, world_from_local: &Affine3A) -> bool {
124
// transform the half-extents into world space.
125
let half_extents_world = world_from_local.matrix3.abs() * self.half_extents.abs();
126
// collapse the half-extents onto the plane normal.
127
let p_normal = half_space.normal();
128
let r = half_extents_world.dot(p_normal.abs());
129
let aabb_center_world = world_from_local.transform_point3a(self.center);
130
let signed_distance = p_normal.dot(aabb_center_world) + half_space.d();
131
signed_distance > r
132
}
133
}
134
135
impl From<Sphere> for Aabb {
136
#[inline]
137
fn from(sphere: Sphere) -> Self {
138
Self {
139
center: sphere.center,
140
half_extents: Vec3A::splat(sphere.radius),
141
}
142
}
143
}
144
145
#[derive(Clone, Debug, Default)]
146
pub struct Sphere {
147
pub center: Vec3A,
148
pub radius: f32,
149
}
150
151
impl Sphere {
152
#[inline]
153
pub fn intersects_obb(&self, aabb: &Aabb, world_from_local: &Affine3A) -> bool {
154
let aabb_center_world = world_from_local.transform_point3a(aabb.center);
155
let v = aabb_center_world - self.center;
156
let d = v.length();
157
let relative_radius = aabb.relative_radius(&(v / d), &world_from_local.matrix3);
158
d < self.radius + relative_radius
159
}
160
}
161
162
/// A region of 3D space, specifically an open set whose border is a bisecting 2D plane.
163
///
164
/// This bisecting plane partitions 3D space into two infinite regions,
165
/// the half-space is one of those regions and excludes the bisecting plane.
166
///
167
/// Each instance of this type is characterized by:
168
/// - the bisecting plane's unit normal, normalized and pointing "inside" the half-space,
169
/// - the signed distance along the normal from the bisecting plane to the origin of 3D space.
170
///
171
/// The distance can also be seen as:
172
/// - the distance along the inverse of the normal from the origin of 3D space to the bisecting plane,
173
/// - the opposite of the distance along the normal from the origin of 3D space to the bisecting plane.
174
///
175
/// Any point `p` is considered to be within the `HalfSpace` when the length of the projection
176
/// of p on the normal is greater or equal than the opposite of the distance,
177
/// meaning: if the equation `normal.dot(p) + distance > 0.` is satisfied.
178
///
179
/// For example, the half-space containing all the points with a z-coordinate lesser
180
/// or equal than `8.0` would be defined by: `HalfSpace::new(Vec3::NEG_Z.extend(-8.0))`.
181
/// It includes all the points from the bisecting plane towards `NEG_Z`, and the distance
182
/// from the plane to the origin is `-8.0` along `NEG_Z`.
183
///
184
/// It is used to define a [`Frustum`], but is also a useful mathematical primitive for rendering tasks such as light computation.
185
#[derive(Clone, Copy, Debug, Default)]
186
pub struct HalfSpace {
187
normal_d: Vec4,
188
}
189
190
impl HalfSpace {
191
/// Constructs a `HalfSpace` from a 4D vector whose first 3 components
192
/// represent the bisecting plane's unit normal, and the last component is
193
/// the signed distance along the normal from the plane to the origin.
194
/// The constructor ensures the normal vector is normalized and the distance is appropriately scaled.
195
#[inline]
196
pub fn new(normal_d: Vec4) -> Self {
197
Self {
198
normal_d: normal_d * normal_d.xyz().length_recip(),
199
}
200
}
201
202
/// Returns the unit normal vector of the bisecting plane that characterizes the `HalfSpace`.
203
#[inline]
204
pub fn normal(&self) -> Vec3A {
205
Vec3A::from_vec4(self.normal_d)
206
}
207
208
/// Returns the signed distance from the bisecting plane to the origin along
209
/// the plane's unit normal vector.
210
#[inline]
211
pub fn d(&self) -> f32 {
212
self.normal_d.w
213
}
214
215
/// Returns the bisecting plane's unit normal vector and the signed distance
216
/// from the plane to the origin.
217
#[inline]
218
pub fn normal_d(&self) -> Vec4 {
219
self.normal_d
220
}
221
}
222
223
/// A region of 3D space defined by the intersection of 6 [`HalfSpace`]s.
224
///
225
/// Frustums are typically an apex-truncated square pyramid (a pyramid without the top) or a cuboid.
226
///
227
/// Half spaces are ordered left, right, top, bottom, near, far. The normal vectors
228
/// of the half-spaces point towards the interior of the frustum.
229
///
230
/// A frustum component is used on an entity with a [`Camera`] component to
231
/// determine which entities will be considered for rendering by this camera.
232
/// All entities with an [`Aabb`] component that are not contained by (or crossing
233
/// the boundary of) the frustum will not be rendered, and not be used in rendering computations.
234
///
235
/// This process is called frustum culling, and entities can opt out of it using
236
/// the [`NoFrustumCulling`] component.
237
///
238
/// The frustum component is typically added automatically for cameras, either [`Camera2d`] or [`Camera3d`].
239
/// It is usually updated automatically by [`update_frusta`] from the
240
/// [`CameraProjection`] component and [`GlobalTransform`] of the camera entity.
241
///
242
/// [`Camera`]: crate::Camera
243
/// [`NoFrustumCulling`]: crate::visibility::NoFrustumCulling
244
/// [`update_frusta`]: crate::visibility::update_frusta
245
/// [`CameraProjection`]: crate::CameraProjection
246
/// [`GlobalTransform`]: bevy_transform::components::GlobalTransform
247
/// [`Camera2d`]: crate::Camera2d
248
/// [`Camera3d`]: crate::Camera3d
249
#[derive(Component, Clone, Copy, Debug, Default, Reflect)]
250
#[reflect(Component, Default, Debug, Clone)]
251
pub struct Frustum {
252
#[reflect(ignore, clone)]
253
pub half_spaces: [HalfSpace; 6],
254
}
255
256
impl Frustum {
257
/// Returns a frustum derived from `clip_from_world`.
258
#[inline]
259
pub fn from_clip_from_world(clip_from_world: &Mat4) -> Self {
260
let mut frustum = Frustum::from_clip_from_world_no_far(clip_from_world);
261
frustum.half_spaces[5] = HalfSpace::new(clip_from_world.row(2));
262
frustum
263
}
264
265
/// Returns a frustum derived from `clip_from_world`,
266
/// but with a custom far plane.
267
#[inline]
268
pub fn from_clip_from_world_custom_far(
269
clip_from_world: &Mat4,
270
view_translation: &Vec3,
271
view_backward: &Vec3,
272
far: f32,
273
) -> Self {
274
let mut frustum = Frustum::from_clip_from_world_no_far(clip_from_world);
275
let far_center = *view_translation - far * *view_backward;
276
frustum.half_spaces[5] =
277
HalfSpace::new(view_backward.extend(-view_backward.dot(far_center)));
278
frustum
279
}
280
281
// NOTE: This approach of extracting the frustum half-space from the view
282
// projection matrix is from Foundations of Game Engine Development 2
283
// Rendering by Lengyel.
284
/// Returns a frustum derived from `view_projection`,
285
/// without a far plane.
286
fn from_clip_from_world_no_far(clip_from_world: &Mat4) -> Self {
287
let row3 = clip_from_world.row(3);
288
let mut half_spaces = [HalfSpace::default(); 6];
289
for (i, half_space) in half_spaces.iter_mut().enumerate().take(5) {
290
let row = clip_from_world.row(i / 2);
291
*half_space = HalfSpace::new(if (i & 1) == 0 && i != 4 {
292
row3 + row
293
} else {
294
row3 - row
295
});
296
}
297
half_spaces[5] = HalfSpace::new(Vec4::new(0.0, 0.0, 0.0, f32::MAX));
298
Self { half_spaces }
299
}
300
301
/// Checks if a sphere intersects the frustum.
302
#[inline]
303
pub fn intersects_sphere(&self, sphere: &Sphere, intersect_far: bool) -> bool {
304
let sphere_center = sphere.center.extend(1.0);
305
let max = if intersect_far { 6 } else { 5 };
306
for half_space in &self.half_spaces[..max] {
307
if half_space.normal_d().dot(sphere_center) + sphere.radius <= 0.0 {
308
return false;
309
}
310
}
311
true
312
}
313
314
/// Checks if an Oriented Bounding Box (obb) intersects the frustum.
315
#[inline]
316
pub fn intersects_obb(
317
&self,
318
aabb: &Aabb,
319
world_from_local: &Affine3A,
320
intersect_near: bool,
321
intersect_far: bool,
322
) -> bool {
323
let aabb_center_world = world_from_local.transform_point3a(aabb.center).extend(1.0);
324
for (idx, half_space) in self.half_spaces.into_iter().enumerate() {
325
if idx == 4 && !intersect_near {
326
continue;
327
}
328
if idx == 5 && !intersect_far {
329
continue;
330
}
331
let p_normal = half_space.normal();
332
let relative_radius = aabb.relative_radius(&p_normal, &world_from_local.matrix3);
333
if half_space.normal_d().dot(aabb_center_world) + relative_radius <= 0.0 {
334
return false;
335
}
336
}
337
true
338
}
339
340
/// Check if the frustum contains the Axis-Aligned Bounding Box (AABB).
341
/// Referenced from: [Frustum Culling](https://learnopengl.com/Guest-Articles/2021/Scene/Frustum-Culling)
342
#[inline]
343
pub fn contains_aabb(&self, aabb: &Aabb, world_from_local: &Affine3A) -> bool {
344
for half_space in &self.half_spaces {
345
if !aabb.is_in_half_space(half_space, world_from_local) {
346
return false;
347
}
348
}
349
true
350
}
351
}
352
353
pub struct CubeMapFace {
354
pub target: Vec3,
355
pub up: Vec3,
356
}
357
358
// Cubemap faces are [+X, -X, +Y, -Y, +Z, -Z], per https://www.w3.org/TR/webgpu/#texture-view-creation
359
// Note: Cubemap coordinates are left-handed y-up, unlike the rest of Bevy.
360
// See https://registry.khronos.org/vulkan/specs/1.2/html/chap16.html#_cube_map_face_selection
361
//
362
// For each cubemap face, we take care to specify the appropriate target/up axis such that the rendered
363
// texture using Bevy's right-handed y-up coordinate space matches the expected cubemap face in
364
// left-handed y-up cubemap coordinates.
365
pub const CUBE_MAP_FACES: [CubeMapFace; 6] = [
366
// +X
367
CubeMapFace {
368
target: Vec3::X,
369
up: Vec3::Y,
370
},
371
// -X
372
CubeMapFace {
373
target: Vec3::NEG_X,
374
up: Vec3::Y,
375
},
376
// +Y
377
CubeMapFace {
378
target: Vec3::Y,
379
up: Vec3::Z,
380
},
381
// -Y
382
CubeMapFace {
383
target: Vec3::NEG_Y,
384
up: Vec3::NEG_Z,
385
},
386
// +Z (with left-handed conventions, pointing forwards)
387
CubeMapFace {
388
target: Vec3::NEG_Z,
389
up: Vec3::Y,
390
},
391
// -Z (with left-handed conventions, pointing backwards)
392
CubeMapFace {
393
target: Vec3::Z,
394
up: Vec3::Y,
395
},
396
];
397
398
pub fn face_index_to_name(face_index: usize) -> &'static str {
399
match face_index {
400
0 => "+x",
401
1 => "-x",
402
2 => "+y",
403
3 => "-y",
404
4 => "+z",
405
5 => "-z",
406
_ => "invalid",
407
}
408
}
409
410
#[derive(Component, Clone, Debug, Default, Reflect)]
411
#[reflect(Component, Default, Debug, Clone)]
412
pub struct CubemapFrusta {
413
#[reflect(ignore, clone)]
414
pub frusta: [Frustum; 6],
415
}
416
417
impl CubemapFrusta {
418
pub fn iter(&self) -> impl DoubleEndedIterator<Item = &Frustum> {
419
self.frusta.iter()
420
}
421
pub fn iter_mut(&mut self) -> impl DoubleEndedIterator<Item = &mut Frustum> {
422
self.frusta.iter_mut()
423
}
424
}
425
426
/// Cubemap layout defines the order of images in a packed cubemap image.
427
#[derive(Default, Reflect, Debug, Clone, Copy)]
428
pub enum CubemapLayout {
429
/// layout in a vertical cross format
430
/// ```text
431
/// +y
432
/// -x -z +x
433
/// -y
434
/// +z
435
/// ```
436
#[default]
437
CrossVertical = 0,
438
/// layout in a horizontal cross format
439
/// ```text
440
/// +y
441
/// -x -z +x +z
442
/// -y
443
/// ```
444
CrossHorizontal = 1,
445
/// layout in a vertical sequence
446
/// ```text
447
/// +x
448
/// -x
449
/// +y
450
/// -y
451
/// -z
452
/// +z
453
/// ```
454
SequenceVertical = 2,
455
/// layout in a horizontal sequence
456
/// ```text
457
/// +x -x +y -y -z +z
458
/// ```
459
SequenceHorizontal = 3,
460
}
461
462
#[derive(Component, Debug, Default, Reflect, Clone)]
463
#[reflect(Component, Default, Debug, Clone)]
464
pub struct CascadesFrusta {
465
#[reflect(ignore, clone)]
466
pub frusta: EntityHashMap<Vec<Frustum>>,
467
}
468
469
#[cfg(test)]
470
mod tests {
471
use core::f32::consts::PI;
472
473
use bevy_math::{ops, Quat};
474
use bevy_transform::components::GlobalTransform;
475
476
use crate::{CameraProjection, PerspectiveProjection};
477
478
use super::*;
479
480
// A big, offset frustum
481
fn big_frustum() -> Frustum {
482
Frustum {
483
half_spaces: [
484
HalfSpace::new(Vec4::new(-0.9701, -0.2425, -0.0000, 7.7611)),
485
HalfSpace::new(Vec4::new(-0.0000, 1.0000, -0.0000, 4.0000)),
486
HalfSpace::new(Vec4::new(-0.0000, -0.2425, -0.9701, 2.9104)),
487
HalfSpace::new(Vec4::new(-0.0000, -1.0000, -0.0000, 4.0000)),
488
HalfSpace::new(Vec4::new(-0.0000, -0.2425, 0.9701, 2.9104)),
489
HalfSpace::new(Vec4::new(0.9701, -0.2425, -0.0000, -1.9403)),
490
],
491
}
492
}
493
494
#[test]
495
fn intersects_sphere_big_frustum_outside() {
496
// Sphere outside frustum
497
let frustum = big_frustum();
498
let sphere = Sphere {
499
center: Vec3A::new(0.9167, 0.0000, 0.0000),
500
radius: 0.7500,
501
};
502
assert!(!frustum.intersects_sphere(&sphere, true));
503
}
504
505
#[test]
506
fn intersects_sphere_big_frustum_intersect() {
507
// Sphere intersects frustum boundary
508
let frustum = big_frustum();
509
let sphere = Sphere {
510
center: Vec3A::new(7.9288, 0.0000, 2.9728),
511
radius: 2.0000,
512
};
513
assert!(frustum.intersects_sphere(&sphere, true));
514
}
515
516
// A frustum
517
fn frustum() -> Frustum {
518
Frustum {
519
half_spaces: [
520
HalfSpace::new(Vec4::new(-0.9701, -0.2425, -0.0000, 0.7276)),
521
HalfSpace::new(Vec4::new(-0.0000, 1.0000, -0.0000, 1.0000)),
522
HalfSpace::new(Vec4::new(-0.0000, -0.2425, -0.9701, 0.7276)),
523
HalfSpace::new(Vec4::new(-0.0000, -1.0000, -0.0000, 1.0000)),
524
HalfSpace::new(Vec4::new(-0.0000, -0.2425, 0.9701, 0.7276)),
525
HalfSpace::new(Vec4::new(0.9701, -0.2425, -0.0000, 0.7276)),
526
],
527
}
528
}
529
530
#[test]
531
fn intersects_sphere_frustum_surrounding() {
532
// Sphere surrounds frustum
533
let frustum = frustum();
534
let sphere = Sphere {
535
center: Vec3A::new(0.0000, 0.0000, 0.0000),
536
radius: 3.0000,
537
};
538
assert!(frustum.intersects_sphere(&sphere, true));
539
}
540
541
#[test]
542
fn intersects_sphere_frustum_contained() {
543
// Sphere is contained in frustum
544
let frustum = frustum();
545
let sphere = Sphere {
546
center: Vec3A::new(0.0000, 0.0000, 0.0000),
547
radius: 0.7000,
548
};
549
assert!(frustum.intersects_sphere(&sphere, true));
550
}
551
552
#[test]
553
fn intersects_sphere_frustum_intersects_plane() {
554
// Sphere intersects a plane
555
let frustum = frustum();
556
let sphere = Sphere {
557
center: Vec3A::new(0.0000, 0.0000, 0.9695),
558
radius: 0.7000,
559
};
560
assert!(frustum.intersects_sphere(&sphere, true));
561
}
562
563
#[test]
564
fn intersects_sphere_frustum_intersects_2_planes() {
565
// Sphere intersects 2 planes
566
let frustum = frustum();
567
let sphere = Sphere {
568
center: Vec3A::new(1.2037, 0.0000, 0.9695),
569
radius: 0.7000,
570
};
571
assert!(frustum.intersects_sphere(&sphere, true));
572
}
573
574
#[test]
575
fn intersects_sphere_frustum_intersects_3_planes() {
576
// Sphere intersects 3 planes
577
let frustum = frustum();
578
let sphere = Sphere {
579
center: Vec3A::new(1.2037, -1.0988, 0.9695),
580
radius: 0.7000,
581
};
582
assert!(frustum.intersects_sphere(&sphere, true));
583
}
584
585
#[test]
586
fn intersects_sphere_frustum_dodges_1_plane() {
587
// Sphere avoids intersecting the frustum by 1 plane
588
let frustum = frustum();
589
let sphere = Sphere {
590
center: Vec3A::new(-1.7020, 0.0000, 0.0000),
591
radius: 0.7000,
592
};
593
assert!(!frustum.intersects_sphere(&sphere, true));
594
}
595
596
// A long frustum.
597
fn long_frustum() -> Frustum {
598
Frustum {
599
half_spaces: [
600
HalfSpace::new(Vec4::new(-0.9998, -0.0222, -0.0000, -1.9543)),
601
HalfSpace::new(Vec4::new(-0.0000, 1.0000, -0.0000, 45.1249)),
602
HalfSpace::new(Vec4::new(-0.0000, -0.0168, -0.9999, 2.2718)),
603
HalfSpace::new(Vec4::new(-0.0000, -1.0000, -0.0000, 45.1249)),
604
HalfSpace::new(Vec4::new(-0.0000, -0.0168, 0.9999, 2.2718)),
605
HalfSpace::new(Vec4::new(0.9998, -0.0222, -0.0000, 7.9528)),
606
],
607
}
608
}
609
610
#[test]
611
fn intersects_sphere_long_frustum_outside() {
612
// Sphere outside frustum
613
let frustum = long_frustum();
614
let sphere = Sphere {
615
center: Vec3A::new(-4.4889, 46.9021, 0.0000),
616
radius: 0.7500,
617
};
618
assert!(!frustum.intersects_sphere(&sphere, true));
619
}
620
621
#[test]
622
fn intersects_sphere_long_frustum_intersect() {
623
// Sphere intersects frustum boundary
624
let frustum = long_frustum();
625
let sphere = Sphere {
626
center: Vec3A::new(-4.9957, 0.0000, -0.7396),
627
radius: 4.4094,
628
};
629
assert!(frustum.intersects_sphere(&sphere, true));
630
}
631
632
#[test]
633
fn aabb_enclosing() {
634
assert_eq!(Aabb::enclosing(<[Vec3; 0]>::default()), None);
635
assert_eq!(
636
Aabb::enclosing(vec![Vec3::ONE]).unwrap(),
637
Aabb::from_min_max(Vec3::ONE, Vec3::ONE)
638
);
639
assert_eq!(
640
Aabb::enclosing(&[Vec3::Y, Vec3::X, Vec3::Z][..]).unwrap(),
641
Aabb::from_min_max(Vec3::ZERO, Vec3::ONE)
642
);
643
assert_eq!(
644
Aabb::enclosing([
645
Vec3::NEG_X,
646
Vec3::X * 2.0,
647
Vec3::NEG_Y * 5.0,
648
Vec3::Z,
649
Vec3::ZERO
650
])
651
.unwrap(),
652
Aabb::from_min_max(Vec3::new(-1.0, -5.0, 0.0), Vec3::new(2.0, 0.0, 1.0))
653
);
654
}
655
656
// A frustum with an offset for testing the [`Frustum::contains_aabb`] algorithm.
657
fn contains_aabb_test_frustum() -> Frustum {
658
let proj = PerspectiveProjection {
659
fov: 90.0_f32.to_radians(),
660
aspect_ratio: 1.0,
661
near: 1.0,
662
far: 100.0,
663
};
664
proj.compute_frustum(&GlobalTransform::from_translation(Vec3::new(2.0, 2.0, 0.0)))
665
}
666
667
fn contains_aabb_test_frustum_with_rotation() -> Frustum {
668
let half_extent_world = (((49.5 * 49.5) * 0.5) as f32).sqrt() + 0.5f32.sqrt();
669
let near = 50.5 - half_extent_world;
670
let far = near + 2.0 * half_extent_world;
671
let fov = 2.0 * ops::atan(half_extent_world / near);
672
let proj = PerspectiveProjection {
673
aspect_ratio: 1.0,
674
near,
675
far,
676
fov,
677
};
678
proj.compute_frustum(&GlobalTransform::IDENTITY)
679
}
680
681
#[test]
682
fn aabb_inside_frustum() {
683
let frustum = contains_aabb_test_frustum();
684
let aabb = Aabb {
685
center: Vec3A::ZERO,
686
half_extents: Vec3A::new(0.99, 0.99, 49.49),
687
};
688
let model = Affine3A::from_translation(Vec3::new(2.0, 2.0, -50.5));
689
assert!(frustum.contains_aabb(&aabb, &model));
690
}
691
692
#[test]
693
fn aabb_intersect_frustum() {
694
let frustum = contains_aabb_test_frustum();
695
let aabb = Aabb {
696
center: Vec3A::ZERO,
697
half_extents: Vec3A::new(0.99, 0.99, 49.6),
698
};
699
let model = Affine3A::from_translation(Vec3::new(2.0, 2.0, -50.5));
700
assert!(!frustum.contains_aabb(&aabb, &model));
701
}
702
703
#[test]
704
fn aabb_outside_frustum() {
705
let frustum = contains_aabb_test_frustum();
706
let aabb = Aabb {
707
center: Vec3A::ZERO,
708
half_extents: Vec3A::new(0.99, 0.99, 0.99),
709
};
710
let model = Affine3A::from_translation(Vec3::new(0.0, 0.0, 49.6));
711
assert!(!frustum.contains_aabb(&aabb, &model));
712
}
713
714
#[test]
715
fn aabb_inside_frustum_rotation() {
716
let frustum = contains_aabb_test_frustum_with_rotation();
717
let aabb = Aabb {
718
center: Vec3A::new(0.0, 0.0, 0.0),
719
half_extents: Vec3A::new(0.99, 0.99, 49.49),
720
};
721
722
let model = Affine3A::from_rotation_translation(
723
Quat::from_rotation_x(PI / 4.0),
724
Vec3::new(0.0, 0.0, -50.5),
725
);
726
assert!(frustum.contains_aabb(&aabb, &model));
727
}
728
729
#[test]
730
fn aabb_intersect_frustum_rotation() {
731
let frustum = contains_aabb_test_frustum_with_rotation();
732
let aabb = Aabb {
733
center: Vec3A::new(0.0, 0.0, 0.0),
734
half_extents: Vec3A::new(0.99, 0.99, 49.6),
735
};
736
737
let model = Affine3A::from_rotation_translation(
738
Quat::from_rotation_x(PI / 4.0),
739
Vec3::new(0.0, 0.0, -50.5),
740
);
741
assert!(!frustum.contains_aabb(&aabb, &model));
742
}
743
}
744
745