bevy_camera/
primitives.rs

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