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 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 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(Component, Clone, Copy, Debug, Default, Reflect)]
195#[reflect(Component, Clone, Debug, Default)]
196pub struct Sphere {
197 pub center: Vec3A,
202
203 pub radius: f32,
208}
209
210impl Sphere {
211 #[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#[derive(Component, Clone, Copy, Debug, Default, Deref, DerefMut, Reflect)]
246#[reflect(Component, Default, Debug, Clone)]
247pub struct Frustum(pub ViewFrustum);
248
249impl Frustum {
250 #[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 #[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 #[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 #[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 #[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
338pub const CUBE_MAP_FACES: [CubeMapFace; 6] = [
346 CubeMapFace {
348 target: Vec3::X,
349 up: Vec3::Y,
350 },
351 CubeMapFace {
353 target: Vec3::NEG_X,
354 up: Vec3::Y,
355 },
356 CubeMapFace {
358 target: Vec3::Y,
359 up: Vec3::Z,
360 },
361 CubeMapFace {
363 target: Vec3::NEG_Y,
364 up: Vec3::NEG_Z,
365 },
366 CubeMapFace {
368 target: Vec3::NEG_Z,
369 up: Vec3::Y,
370 },
371 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#[derive(Default, Reflect, Debug, Clone, Copy)]
407pub enum CubemapLayout {
408 #[default]
416 CrossVertical = 0,
417 CrossHorizontal = 1,
424 SequenceVertical = 2,
434 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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, };
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 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 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 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 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}