bevy_camera/
primitives.rs

1use core::borrow::Borrow;
2
3use bevy_ecs::{component::Component, entity::EntityHashMap, reflect::ReflectComponent};
4use bevy_math::{Affine3A, Mat3A, Mat4, Vec3, Vec3A, Vec4, Vec4Swizzles};
5use bevy_mesh::{Mesh, VertexAttributeValues};
6use bevy_reflect::prelude::*;
7
8pub 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
16impl 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)]
54pub struct Aabb {
55    pub center: Vec3A,
56    pub half_extents: Vec3A,
57}
58
59impl 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
135impl 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)]
146pub struct Sphere {
147    pub center: Vec3A,
148    pub radius: f32,
149}
150
151impl 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)]
186pub struct HalfSpace {
187    normal_d: Vec4,
188}
189
190impl 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)]
251pub struct Frustum {
252    #[reflect(ignore, clone)]
253    pub half_spaces: [HalfSpace; 6],
254}
255
256impl 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
353pub 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.
365pub 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
398pub 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)]
412pub struct CubemapFrusta {
413    #[reflect(ignore, clone)]
414    pub frusta: [Frustum; 6],
415}
416
417impl 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)]
428pub 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)]
464pub struct CascadesFrusta {
465    #[reflect(ignore, clone)]
466    pub frusta: EntityHashMap<Vec<Frustum>>,
467}
468
469#[cfg(test)]
470mod 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}