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