Skip to main content

bevy_camera/
primitives.rs

1use core::borrow::Borrow;
2
3use bevy_derive::{Deref, DerefMut};
4use bevy_ecs::{component::Component, entity::EntityHashMap, reflect::ReflectComponent};
5use bevy_math::{Affine3A, Mat3A, Vec3, Vec3A};
6use bevy_mesh::{Mesh, VertexAttributeValues};
7use bevy_reflect::prelude::*;
8use bevy_shape::{Aabb3d, BoundingVolume, HalfSpace, ViewFrustum};
9
10pub trait MeshAabb {
11    /// Compute the Axis-Aligned Bounding Box of the mesh vertices in model space,
12    /// or return [`Mesh::final_aabb`] if it is not `None`.
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 get_aabb(&self) -> Option<Aabb>;
17}
18
19impl MeshAabb for Mesh {
20    fn get_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/// A sphere, defined by a center and a radius.
182///
183/// This is typically used as a component on an entity to represent the local
184/// space occupied by this entity, as an alternative to [`Aabb`]. The *frustum
185/// culling* process uses this component to determine whether an entity is in
186/// the view of a [`crate::Camera`].
187///
188/// Bevy will automatically add this component to point and spot lights, as
189/// their ranges are most easily approximated by a sphere. The engine will keep
190/// this entity updated as the range and/or transform of such lights changes.
191///
192/// If both [`Aabb`] and [`Sphere`] are present on an entity, [`Aabb`] takes
193/// precedence.
194#[derive(Component, Clone, Copy, Debug, Default, Reflect)]
195#[reflect(Component, Clone, Debug, Default)]
196pub struct Sphere {
197    /// The center of the sphere.
198    ///
199    /// If this is used as a component, [`Self::center`] is in local space. That
200    /// is, it doesn't take the world-space position of the object into account.
201    pub center: Vec3A,
202
203    /// The radius of the sphere.
204    ///
205    /// If this is used as a component, [`Self::radius`] is in local space. That
206    /// is, it doesn't take the world-space scale of the object into account.
207    pub radius: f32,
208}
209
210impl Sphere {
211    /// Returns true if this sphere intersects the given oriented bounding box.
212    ///
213    /// The oriented bounding box (OBB) to test against is produced by
214    /// transforming the given AABB according to the supplied matrix.
215    #[inline]
216    pub fn intersects_obb(&self, aabb: &Aabb, world_from_local: &Affine3A) -> bool {
217        let aabb_center_world = world_from_local.transform_point3a(aabb.center);
218        let v = aabb_center_world - self.center;
219        let d_sq = v.length_squared();
220        let d = d_sq.sqrt();
221        let relative_radius_unscaled = aabb.relative_radius(&v, &world_from_local.matrix3);
222        d_sq <= self.radius * d + relative_radius_unscaled
223    }
224}
225
226/// A frustum component is used on an entity with a [`Camera`] component to
227/// determine which entities will be considered for rendering by this camera.
228/// All entities with an [`Aabb`] component that are not contained by (or crossing
229/// the boundary of) the frustum will not be rendered, and not be used in rendering computations.
230///
231/// This process is called frustum culling, and entities can opt out of it using
232/// the [`NoFrustumCulling`] component.
233///
234/// The frustum component is typically added automatically for cameras, either [`Camera2d`] or [`Camera3d`].
235/// It is usually updated automatically by [`update_frusta`] from the
236/// [`CameraProjection`] component and [`GlobalTransform`] of the camera entity.
237///
238/// [`Camera`]: crate::Camera
239/// [`NoFrustumCulling`]: crate::visibility::NoFrustumCulling
240/// [`update_frusta`]: crate::visibility::update_frusta
241/// [`CameraProjection`]: crate::CameraProjection
242/// [`GlobalTransform`]: bevy_transform::components::GlobalTransform
243/// [`Camera2d`]: crate::Camera2d
244/// [`Camera3d`]: crate::Camera3d
245#[derive(Component, Clone, Copy, Debug, Default, Deref, DerefMut, Reflect)]
246#[reflect(Component, Default, Debug, Clone)]
247pub struct Frustum(pub ViewFrustum);
248
249impl Frustum {
250    /// Checks if a sphere intersects the frustum.
251    #[inline]
252    pub fn intersects_sphere(&self, sphere: &Sphere, intersect_far: bool) -> bool {
253        let sphere_center = sphere.center.extend(1.0);
254        let max = if intersect_far {
255            ViewFrustum::FAR_PLANE_IDX
256        } else {
257            ViewFrustum::NEAR_PLANE_IDX
258        };
259        for half_space in &self.half_spaces[..=max] {
260            if half_space.normal_d().dot(sphere_center) + sphere.radius <= 0.0 {
261                return false;
262            }
263        }
264        true
265    }
266
267    /// Checks if an Oriented Bounding Box (obb) intersects the frustum.
268    #[inline]
269    pub fn intersects_obb(
270        &self,
271        aabb: &Aabb,
272        world_from_local: &Affine3A,
273        intersect_near: bool,
274        intersect_far: bool,
275    ) -> bool {
276        let aabb_center_world = world_from_local.transform_point3a(aabb.center).extend(1.0);
277
278        for (idx, half_space) in self.half_spaces.into_iter().enumerate() {
279            if (idx == ViewFrustum::NEAR_PLANE_IDX && !intersect_near)
280                || (idx == ViewFrustum::FAR_PLANE_IDX && !intersect_far)
281            {
282                continue;
283            }
284            let p_normal = half_space.normal();
285            let relative_radius = aabb.relative_radius(&p_normal, &world_from_local.matrix3);
286            if half_space.normal_d().dot(aabb_center_world) + relative_radius <= 0.0 {
287                return false;
288            }
289        }
290        true
291    }
292
293    /// Optimized version of [`Frustum::intersects_obb`]
294    /// where the transform is [`Affine3A::IDENTITY`] and both `intersect_near` and `intersect_far` are `true`.
295    #[inline]
296    pub fn intersects_obb_identity(&self, aabb: &Aabb) -> bool {
297        let aabb_center_world = aabb.center.extend(1.0);
298        for half_space in self.half_spaces.iter() {
299            let p_normal = half_space.normal();
300            let relative_radius = aabb.half_extents.abs().dot(p_normal.abs());
301            if half_space.normal_d().dot(aabb_center_world) + relative_radius <= 0.0 {
302                return false;
303            }
304        }
305        true
306    }
307
308    /// Check if the frustum contains the entire Axis-Aligned Bounding Box (AABB).
309    /// Referenced from: [Frustum Culling](https://learnopengl.com/Guest-Articles/2021/Scene/Frustum-Culling)
310    #[inline]
311    pub fn contains_aabb(&self, aabb: &Aabb, world_from_local: &Affine3A) -> bool {
312        for half_space in &self.half_spaces {
313            if !aabb.is_in_half_space(half_space, world_from_local) {
314                return false;
315            }
316        }
317        true
318    }
319
320    /// Optimized version of [`Self::contains_aabb`] when the AABB is already in world space.
321    /// Use this when `world_from_local` would be [`Affine3A::IDENTITY`].
322    #[inline]
323    pub fn contains_aabb_identity(&self, aabb: &Aabb) -> bool {
324        for half_space in &self.half_spaces {
325            if !aabb.is_in_half_space_identity(half_space) {
326                return false;
327            }
328        }
329        true
330    }
331}
332
333pub struct CubeMapFace {
334    pub target: Vec3,
335    pub up: Vec3,
336}
337
338// Cubemap faces are [+X, -X, +Y, -Y, +Z, -Z], per https://www.w3.org/TR/webgpu/#texture-view-creation
339// Note: Cubemap coordinates are left-handed y-up, unlike the rest of Bevy.
340// See https://registry.khronos.org/vulkan/specs/1.2/html/chap16.html#_cube_map_face_selection
341//
342// For each cubemap face, we take care to specify the appropriate target/up axis such that the rendered
343// texture using Bevy's right-handed y-up coordinate space matches the expected cubemap face in
344// left-handed y-up cubemap coordinates.
345pub const CUBE_MAP_FACES: [CubeMapFace; 6] = [
346    // +X
347    CubeMapFace {
348        target: Vec3::X,
349        up: Vec3::Y,
350    },
351    // -X
352    CubeMapFace {
353        target: Vec3::NEG_X,
354        up: Vec3::Y,
355    },
356    // +Y
357    CubeMapFace {
358        target: Vec3::Y,
359        up: Vec3::Z,
360    },
361    // -Y
362    CubeMapFace {
363        target: Vec3::NEG_Y,
364        up: Vec3::NEG_Z,
365    },
366    // +Z (with left-handed conventions, pointing forwards)
367    CubeMapFace {
368        target: Vec3::NEG_Z,
369        up: Vec3::Y,
370    },
371    // -Z (with left-handed conventions, pointing backwards)
372    CubeMapFace {
373        target: Vec3::Z,
374        up: Vec3::Y,
375    },
376];
377
378pub fn face_index_to_name(face_index: usize) -> &'static str {
379    match face_index {
380        0 => "+x",
381        1 => "-x",
382        2 => "+y",
383        3 => "-y",
384        4 => "+z",
385        5 => "-z",
386        _ => "invalid",
387    }
388}
389
390#[derive(Component, Clone, Debug, Default, Reflect)]
391#[reflect(Component, Default, Debug, Clone)]
392pub struct CubemapFrusta {
393    pub frusta: [Frustum; 6],
394}
395
396impl CubemapFrusta {
397    pub fn iter(&self) -> impl DoubleEndedIterator<Item = &Frustum> {
398        self.frusta.iter()
399    }
400    pub fn iter_mut(&mut self) -> impl DoubleEndedIterator<Item = &mut Frustum> {
401        self.frusta.iter_mut()
402    }
403}
404
405/// Cubemap layout defines the order of images in a packed cubemap image.
406#[derive(Default, Reflect, Debug, Clone, Copy)]
407pub enum CubemapLayout {
408    /// layout in a vertical cross format
409    /// ```text
410    ///    +y
411    /// -x -z +x
412    ///    -y
413    ///    +z
414    /// ```
415    #[default]
416    CrossVertical = 0,
417    /// layout in a horizontal cross format
418    /// ```text
419    ///    +y
420    /// -x -z +x +z
421    ///    -y
422    /// ```
423    CrossHorizontal = 1,
424    /// layout in a vertical sequence
425    /// ```text
426    ///   +x
427    ///   -x
428    ///   +y
429    ///   -y
430    ///   -z
431    ///   +z
432    /// ```
433    SequenceVertical = 2,
434    /// layout in a horizontal sequence
435    /// ```text
436    /// +x -x +y -y -z +z
437    /// ```
438    SequenceHorizontal = 3,
439}
440
441#[derive(Component, Debug, Default, Reflect, Clone)]
442#[reflect(Component, Default, Debug, Clone)]
443pub struct CascadesFrusta {
444    pub frusta: EntityHashMap<Vec<Frustum>>,
445}
446
447#[cfg(test)]
448mod tests {
449    use core::f32::consts::PI;
450
451    use bevy_math::{ops, Quat, Vec4};
452    use bevy_transform::components::GlobalTransform;
453
454    use crate::{CameraProjection, PerspectiveProjection};
455
456    use super::*;
457
458    // A big, offset frustum
459    fn big_frustum() -> Frustum {
460        Frustum(ViewFrustum {
461            half_spaces: [
462                HalfSpace::new(Vec4::new(-0.9701, -0.2425, -0.0000, 7.7611)),
463                HalfSpace::new(Vec4::new(-0.0000, 1.0000, -0.0000, 4.0000)),
464                HalfSpace::new(Vec4::new(-0.0000, -0.2425, -0.9701, 2.9104)),
465                HalfSpace::new(Vec4::new(-0.0000, -1.0000, -0.0000, 4.0000)),
466                HalfSpace::new(Vec4::new(-0.0000, -0.2425, 0.9701, 2.9104)),
467                HalfSpace::new(Vec4::new(0.9701, -0.2425, -0.0000, -1.9403)),
468            ],
469        })
470    }
471
472    #[test]
473    fn intersects_sphere_big_frustum_outside() {
474        // Sphere outside frustum
475        let frustum = big_frustum();
476        let sphere = Sphere {
477            center: Vec3A::new(0.9167, 0.0000, 0.0000),
478            radius: 0.7500,
479        };
480        assert!(!frustum.intersects_sphere(&sphere, true));
481    }
482
483    #[test]
484    fn intersects_sphere_big_frustum_intersect() {
485        // Sphere intersects frustum boundary
486        let frustum = big_frustum();
487        let sphere = Sphere {
488            center: Vec3A::new(7.9288, 0.0000, 2.9728),
489            radius: 2.0000,
490        };
491        assert!(frustum.intersects_sphere(&sphere, true));
492    }
493
494    // A frustum
495    fn frustum() -> Frustum {
496        Frustum(ViewFrustum {
497            half_spaces: [
498                HalfSpace::new(Vec4::new(-0.9701, -0.2425, -0.0000, 0.7276)),
499                HalfSpace::new(Vec4::new(-0.0000, 1.0000, -0.0000, 1.0000)),
500                HalfSpace::new(Vec4::new(-0.0000, -0.2425, -0.9701, 0.7276)),
501                HalfSpace::new(Vec4::new(-0.0000, -1.0000, -0.0000, 1.0000)),
502                HalfSpace::new(Vec4::new(-0.0000, -0.2425, 0.9701, 0.7276)),
503                HalfSpace::new(Vec4::new(0.9701, -0.2425, -0.0000, 0.7276)),
504            ],
505        })
506    }
507
508    #[test]
509    fn intersects_sphere_frustum_surrounding() {
510        // Sphere surrounds frustum
511        let frustum = frustum();
512        let sphere = Sphere {
513            center: Vec3A::new(0.0000, 0.0000, 0.0000),
514            radius: 3.0000,
515        };
516        assert!(frustum.intersects_sphere(&sphere, true));
517    }
518
519    #[test]
520    fn intersects_sphere_frustum_contained() {
521        // Sphere is contained in frustum
522        let frustum = frustum();
523        let sphere = Sphere {
524            center: Vec3A::new(0.0000, 0.0000, 0.0000),
525            radius: 0.7000,
526        };
527        assert!(frustum.intersects_sphere(&sphere, true));
528    }
529
530    #[test]
531    fn intersects_sphere_frustum_intersects_plane() {
532        // Sphere intersects a plane
533        let frustum = frustum();
534        let sphere = Sphere {
535            center: Vec3A::new(0.0000, 0.0000, 0.9695),
536            radius: 0.7000,
537        };
538        assert!(frustum.intersects_sphere(&sphere, true));
539    }
540
541    #[test]
542    fn intersects_sphere_frustum_intersects_2_planes() {
543        // Sphere intersects 2 planes
544        let frustum = frustum();
545        let sphere = Sphere {
546            center: Vec3A::new(1.2037, 0.0000, 0.9695),
547            radius: 0.7000,
548        };
549        assert!(frustum.intersects_sphere(&sphere, true));
550    }
551
552    #[test]
553    fn intersects_sphere_frustum_intersects_3_planes() {
554        // Sphere intersects 3 planes
555        let frustum = frustum();
556        let sphere = Sphere {
557            center: Vec3A::new(1.2037, -1.0988, 0.9695),
558            radius: 0.7000,
559        };
560        assert!(frustum.intersects_sphere(&sphere, true));
561    }
562
563    #[test]
564    fn intersects_sphere_frustum_dodges_1_plane() {
565        // Sphere avoids intersecting the frustum by 1 plane
566        let frustum = frustum();
567        let sphere = Sphere {
568            center: Vec3A::new(-1.7020, 0.0000, 0.0000),
569            radius: 0.7000,
570        };
571        assert!(!frustum.intersects_sphere(&sphere, true));
572    }
573
574    // A long frustum.
575    fn long_frustum() -> Frustum {
576        Frustum(ViewFrustum {
577            half_spaces: [
578                HalfSpace::new(Vec4::new(-0.9998, -0.0222, -0.0000, -1.9543)),
579                HalfSpace::new(Vec4::new(-0.0000, 1.0000, -0.0000, 45.1249)),
580                HalfSpace::new(Vec4::new(-0.0000, -0.0168, -0.9999, 2.2718)),
581                HalfSpace::new(Vec4::new(-0.0000, -1.0000, -0.0000, 45.1249)),
582                HalfSpace::new(Vec4::new(-0.0000, -0.0168, 0.9999, 2.2718)),
583                HalfSpace::new(Vec4::new(0.9998, -0.0222, -0.0000, 7.9528)),
584            ],
585        })
586    }
587
588    #[test]
589    fn intersects_sphere_long_frustum_outside() {
590        // Sphere outside frustum
591        let frustum = long_frustum();
592        let sphere = Sphere {
593            center: Vec3A::new(-4.4889, 46.9021, 0.0000),
594            radius: 0.7500,
595        };
596        assert!(!frustum.intersects_sphere(&sphere, true));
597    }
598
599    #[test]
600    fn intersects_sphere_long_frustum_intersect() {
601        // Sphere intersects frustum boundary
602        let frustum = long_frustum();
603        let sphere = Sphere {
604            center: Vec3A::new(-4.9957, 0.0000, -0.7396),
605            radius: 4.4094,
606        };
607        assert!(frustum.intersects_sphere(&sphere, true));
608    }
609
610    #[test]
611    fn sphere_intersects_obb_identical_center() {
612        let sphere_at_origin = Sphere {
613            center: Vec3A::ZERO,
614            radius: 1.0,
615        };
616        let aabb_at_origin = Aabb {
617            center: Vec3A::ZERO,
618            half_extents: Vec3A::splat(0.5),
619        };
620        assert!(
621            sphere_at_origin.intersects_obb(&aabb_at_origin, &Affine3A::IDENTITY),
622            "Should intersect when centers are exactly identical"
623        );
624    }
625
626    #[test]
627    fn sphere_intersects_obb_at_edge() {
628        // Zero-radius sphere (a point) exactly on the edge of an OBB
629        let point_sphere = Sphere {
630            center: Vec3A::new(1.0, 0.0, 0.0),
631            radius: 0.0,
632        };
633        let aabb = Aabb {
634            center: Vec3A::ZERO,
635            half_extents: Vec3A::X, // Width of 1, height/depth 0
636        };
637        assert!(
638            point_sphere.intersects_obb(&aabb, &Affine3A::IDENTITY),
639            "Zero radius sphere (point) on the boundary should count as an intersection"
640        );
641    }
642
643    #[test]
644    fn sphere_intersects_obb_zero_extents_inside() {
645        // OBB with zero extents (a point) inside a sphere
646        let sphere = Sphere {
647            center: Vec3A::ZERO,
648            radius: 10.0,
649        };
650        let point_aabb = Aabb {
651            center: Vec3A::new(1.0, 1.0, 1.0),
652            half_extents: Vec3A::ZERO,
653        };
654        assert!(
655            sphere.intersects_obb(&point_aabb, &Affine3A::IDENTITY),
656            "Sphere should intersect an AABB with zero extents if the point is inside"
657        );
658    }
659
660    #[test]
661    fn sphere_intersects_obb_rotated_zeros() {
662        // Rotated zero-extent OBB
663        let sphere = Sphere {
664            center: Vec3A::new(5.0, 0.0, 0.0),
665            radius: 1.0,
666        };
667        let point_aabb = Aabb {
668            center: Vec3A::ZERO,
669            half_extents: Vec3A::ZERO,
670        };
671
672        // Rotate and translate the "point" OBB so it sits inside the sphere
673        let transform = Affine3A::from_rotation_translation(
674            Quat::from_rotation_y(PI),
675            Vec3::new(5.0, 0.0, 0.0),
676        );
677
678        assert!(
679            sphere.intersects_obb(&point_aabb, &transform),
680            "Should intersect rotated point OBB"
681        );
682    }
683
684    #[test]
685    fn aabb_enclosing() {
686        assert_eq!(Aabb::enclosing([] as [Vec3; 0]), None);
687        assert_eq!(
688            Aabb::enclosing(vec![Vec3::ONE]).unwrap(),
689            Aabb::from_min_max(Vec3::ONE, Vec3::ONE)
690        );
691        assert_eq!(
692            Aabb::enclosing(&[Vec3::Y, Vec3::X, Vec3::Z][..]).unwrap(),
693            Aabb::from_min_max(Vec3::ZERO, Vec3::ONE)
694        );
695        assert_eq!(
696            Aabb::enclosing([
697                Vec3::NEG_X,
698                Vec3::X * 2.0,
699                Vec3::NEG_Y * 5.0,
700                Vec3::Z,
701                Vec3::ZERO
702            ])
703            .unwrap(),
704            Aabb::from_min_max(Vec3::new(-1.0, -5.0, 0.0), Vec3::new(2.0, 0.0, 1.0))
705        );
706    }
707
708    // A frustum with an offset for testing the [`Frustum::contains_aabb`] algorithm.
709    fn contains_aabb_test_frustum() -> Frustum {
710        let proj = PerspectiveProjection {
711            fov: 90.0_f32.to_radians(),
712            aspect_ratio: 1.0,
713            near: 1.0,
714            far: 100.0,
715            ..PerspectiveProjection::default()
716        };
717        proj.compute_frustum(&GlobalTransform::from_translation(Vec3::new(2.0, 2.0, 0.0)))
718    }
719
720    fn contains_aabb_test_frustum_with_rotation() -> Frustum {
721        let half_extent_world = (((49.5 * 49.5) * 0.5) as f32).sqrt() + 0.5f32.sqrt();
722        let near = 50.5 - half_extent_world;
723        let far = near + 2.0 * half_extent_world;
724        let fov = 2.0 * ops::atan(half_extent_world / near);
725        let proj = PerspectiveProjection {
726            aspect_ratio: 1.0,
727            near,
728            far,
729            fov,
730            ..PerspectiveProjection::default()
731        };
732        proj.compute_frustum(&GlobalTransform::IDENTITY)
733    }
734
735    #[test]
736    fn aabb_inside_frustum() {
737        let frustum = contains_aabb_test_frustum();
738        let aabb = Aabb {
739            center: Vec3A::ZERO,
740            half_extents: Vec3A::new(0.99, 0.99, 49.49),
741        };
742        let model = Affine3A::from_translation(Vec3::new(2.0, 2.0, -50.5));
743        assert!(frustum.contains_aabb(&aabb, &model));
744    }
745
746    #[test]
747    fn aabb_intersect_frustum() {
748        let frustum = contains_aabb_test_frustum();
749        let aabb = Aabb {
750            center: Vec3A::ZERO,
751            half_extents: Vec3A::new(0.99, 0.99, 49.6),
752        };
753        let model = Affine3A::from_translation(Vec3::new(2.0, 2.0, -50.5));
754        assert!(!frustum.contains_aabb(&aabb, &model));
755    }
756
757    #[test]
758    fn aabb_outside_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, 0.99),
763        };
764        let model = Affine3A::from_translation(Vec3::new(0.0, 0.0, 49.6));
765        assert!(!frustum.contains_aabb(&aabb, &model));
766    }
767
768    #[test]
769    fn aabb_inside_frustum_rotation() {
770        let frustum = contains_aabb_test_frustum_with_rotation();
771        let aabb = Aabb {
772            center: Vec3A::new(0.0, 0.0, 0.0),
773            half_extents: Vec3A::new(0.99, 0.99, 49.49),
774        };
775
776        let model = Affine3A::from_rotation_translation(
777            Quat::from_rotation_x(PI / 4.0),
778            Vec3::new(0.0, 0.0, -50.5),
779        );
780        assert!(frustum.contains_aabb(&aabb, &model));
781    }
782
783    #[test]
784    fn aabb_intersect_frustum_rotation() {
785        let frustum = contains_aabb_test_frustum_with_rotation();
786        let aabb = Aabb {
787            center: Vec3A::new(0.0, 0.0, 0.0),
788            half_extents: Vec3A::new(0.99, 0.99, 49.6),
789        };
790
791        let model = Affine3A::from_rotation_translation(
792            Quat::from_rotation_x(PI / 4.0),
793            Vec3::new(0.0, 0.0, -50.5),
794        );
795        assert!(!frustum.contains_aabb(&aabb, &model));
796    }
797
798    #[test]
799    fn test_identity_optimized_equivalence() {
800        let cases = vec![
801            (
802                Aabb {
803                    center: Vec3A::ZERO,
804                    half_extents: Vec3A::splat(1.0),
805                },
806                HalfSpace::new(Vec4::new(1.0, 0.0, 0.0, -0.5)),
807            ),
808            (
809                Aabb {
810                    center: Vec3A::new(2.0, -1.0, 0.5),
811                    half_extents: Vec3A::new(1.0, 2.0, 0.5),
812                },
813                HalfSpace::new(Vec4::new(1.0, 1.0, 1.0, -1.0).normalize()),
814            ),
815            (
816                Aabb {
817                    center: Vec3A::new(1.0, 1.0, 1.0),
818                    half_extents: Vec3A::ZERO,
819                },
820                HalfSpace::new(Vec4::new(0.0, 0.0, 1.0, -2.0)),
821            ),
822        ];
823        for (aabb, half_space) in cases {
824            let general = aabb.is_in_half_space(&half_space, &Affine3A::IDENTITY);
825            let identity = aabb.is_in_half_space_identity(&half_space);
826            assert_eq!(general, identity,);
827        }
828    }
829
830    #[test]
831    fn intersects_obb_identity_matches_standard_true_true() {
832        let frusta = [frustum(), long_frustum(), big_frustum()];
833        let aabbs = [
834            Aabb {
835                center: Vec3A::ZERO,
836                half_extents: Vec3A::new(0.5, 0.5, 0.5),
837            },
838            Aabb {
839                center: Vec3A::new(1.0, 0.0, 0.5),
840                half_extents: Vec3A::new(0.9, 0.9, 0.9),
841            },
842            Aabb {
843                center: Vec3A::new(100.0, 100.0, 100.0),
844                half_extents: Vec3A::new(1.0, 1.0, 1.0),
845            },
846        ];
847        for fr in &frusta {
848            for aabb in &aabbs {
849                let standard = fr.intersects_obb(aabb, &Affine3A::IDENTITY, true, true);
850                let optimized = fr.intersects_obb_identity(aabb);
851                assert_eq!(standard, optimized);
852            }
853        }
854    }
855}