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 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 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#[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 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 #[inline]
108 pub fn relative_radius(&self, p_normal: &Vec3A, world_from_local: &Mat3A) -> f32 {
109 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 #[inline]
133 pub fn is_in_half_space(&self, half_space: &HalfSpace, world_from_local: &Affine3A) -> bool {
134 let half_extents_world = world_from_local.matrix3.abs() * self.half_extents.abs();
136 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 #[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#[derive(Component, Clone, Copy, Debug, Default, Reflect)]
197#[reflect(Component, Clone, Debug, Default)]
198pub struct Sphere {
199 pub center: Vec3A,
204
205 pub radius: f32,
210}
211
212impl Sphere {
213 #[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#[derive(Component, Clone, Copy, Debug, Default, Deref, DerefMut, Reflect)]
248#[reflect(Component, Default, Debug, Clone)]
249pub struct Frustum(pub ViewFrustum);
250
251impl Frustum {
252 #[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 #[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 #[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 #[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 #[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
340pub const CUBE_MAP_FACES: [CubeMapFace; 6] = [
348 CubeMapFace {
350 target: Vec3::X,
351 up: Vec3::Y,
352 },
353 CubeMapFace {
355 target: Vec3::NEG_X,
356 up: Vec3::Y,
357 },
358 CubeMapFace {
360 target: Vec3::Y,
361 up: Vec3::Z,
362 },
363 CubeMapFace {
365 target: Vec3::NEG_Y,
366 up: Vec3::NEG_Z,
367 },
368 CubeMapFace {
370 target: Vec3::NEG_Z,
371 up: Vec3::Y,
372 },
373 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#[derive(Default, Reflect, Debug, Clone, Copy)]
409pub enum CubemapLayout {
410 #[default]
418 CrossVertical = 0,
419 CrossHorizontal = 1,
426 SequenceVertical = 2,
436 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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, };
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 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 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 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 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}