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 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 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#[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 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 #[inline]
106 pub fn relative_radius(&self, p_normal: &Vec3A, world_from_local: &Mat3A) -> f32 {
107 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 #[inline]
131 pub fn is_in_half_space(&self, half_space: &HalfSpace, world_from_local: &Affine3A) -> bool {
132 let half_extents_world = world_from_local.matrix3.abs() * self.half_extents.abs();
134 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 #[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#[derive(Clone, Copy, Debug, Default)]
222pub struct HalfSpace {
223 normal_d: Vec4,
224}
225
226impl HalfSpace {
227 #[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 #[inline]
240 pub fn normal(&self) -> Vec3A {
241 Vec3A::from_vec4(self.normal_d)
242 }
243
244 #[inline]
247 pub fn d(&self) -> f32 {
248 self.normal_d.w
249 }
250
251 #[inline]
254 pub fn normal_d(&self) -> Vec4 {
255 self.normal_d
256 }
257}
258
259#[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 #[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 #[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 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 #[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 #[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 #[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 #[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 #[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
432pub const CUBE_MAP_FACES: [CubeMapFace; 6] = [
440 CubeMapFace {
442 target: Vec3::X,
443 up: Vec3::Y,
444 },
445 CubeMapFace {
447 target: Vec3::NEG_X,
448 up: Vec3::Y,
449 },
450 CubeMapFace {
452 target: Vec3::Y,
453 up: Vec3::Z,
454 },
455 CubeMapFace {
457 target: Vec3::NEG_Y,
458 up: Vec3::NEG_Z,
459 },
460 CubeMapFace {
462 target: Vec3::NEG_Z,
463 up: Vec3::Y,
464 },
465 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#[derive(Default, Reflect, Debug, Clone, Copy)]
502pub enum CubemapLayout {
503 #[default]
511 CrossVertical = 0,
512 CrossHorizontal = 1,
519 SequenceVertical = 2,
529 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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}