1use core::borrow::Borrow;
2
3use bevy_ecs::{component::Component, entity::EntityHashMap, reflect::ReflectComponent};
4use bevy_math::{Affine3A, Mat3A, Mat4, Vec3, Vec3A, Vec4, Vec4Swizzles};
5use bevy_mesh::{Mesh, VertexAttributeValues};
6use bevy_reflect::prelude::*;
7
8pub trait MeshAabb {
9 fn compute_aabb(&self) -> Option<Aabb>;
14}
15
16impl MeshAabb for Mesh {
17 fn compute_aabb(&self) -> Option<Aabb> {
18 let Some(VertexAttributeValues::Float32x3(values)) =
19 self.attribute(Mesh::ATTRIBUTE_POSITION)
20 else {
21 return None;
22 };
23
24 Aabb::enclosing(values.iter().map(|p| Vec3::from_slice(p)))
25 }
26}
27
28#[derive(Component, Clone, Copy, Debug, Default, Reflect, PartialEq)]
53#[reflect(Component, Default, Debug, PartialEq, Clone)]
54pub struct Aabb {
55 pub center: Vec3A,
56 pub half_extents: Vec3A,
57}
58
59impl Aabb {
60 #[inline]
61 pub fn from_min_max(minimum: Vec3, maximum: Vec3) -> Self {
62 let minimum = Vec3A::from(minimum);
63 let maximum = Vec3A::from(maximum);
64 let center = 0.5 * (maximum + minimum);
65 let half_extents = 0.5 * (maximum - minimum);
66 Self {
67 center,
68 half_extents,
69 }
70 }
71
72 pub fn enclosing<T: Borrow<Vec3>>(iter: impl IntoIterator<Item = T>) -> Option<Self> {
86 let mut iter = iter.into_iter().map(|p| *p.borrow());
87 let mut min = iter.next()?;
88 let mut max = min;
89 for v in iter {
90 min = Vec3::min(min, v);
91 max = Vec3::max(max, v);
92 }
93 Some(Self::from_min_max(min, max))
94 }
95
96 #[inline]
98 pub fn relative_radius(&self, p_normal: &Vec3A, world_from_local: &Mat3A) -> f32 {
99 let half_extents = self.half_extents;
101 Vec3A::new(
102 p_normal.dot(world_from_local.x_axis),
103 p_normal.dot(world_from_local.y_axis),
104 p_normal.dot(world_from_local.z_axis),
105 )
106 .abs()
107 .dot(half_extents)
108 }
109
110 #[inline]
111 pub fn min(&self) -> Vec3A {
112 self.center - self.half_extents
113 }
114
115 #[inline]
116 pub fn max(&self) -> Vec3A {
117 self.center + self.half_extents
118 }
119
120 #[inline]
123 pub fn is_in_half_space(&self, half_space: &HalfSpace, world_from_local: &Affine3A) -> bool {
124 let half_extents_world = world_from_local.matrix3.abs() * self.half_extents.abs();
126 let p_normal = half_space.normal();
128 let r = half_extents_world.dot(p_normal.abs());
129 let aabb_center_world = world_from_local.transform_point3a(self.center);
130 let signed_distance = p_normal.dot(aabb_center_world) + half_space.d();
131 signed_distance > r
132 }
133}
134
135impl From<Sphere> for Aabb {
136 #[inline]
137 fn from(sphere: Sphere) -> Self {
138 Self {
139 center: sphere.center,
140 half_extents: Vec3A::splat(sphere.radius),
141 }
142 }
143}
144
145#[derive(Clone, Debug, Default)]
146pub struct Sphere {
147 pub center: Vec3A,
148 pub radius: f32,
149}
150
151impl Sphere {
152 #[inline]
153 pub fn intersects_obb(&self, aabb: &Aabb, world_from_local: &Affine3A) -> bool {
154 let aabb_center_world = world_from_local.transform_point3a(aabb.center);
155 let v = aabb_center_world - self.center;
156 let d = v.length();
157 let relative_radius = aabb.relative_radius(&(v / d), &world_from_local.matrix3);
158 d < self.radius + relative_radius
159 }
160}
161
162#[derive(Clone, Copy, Debug, Default)]
186pub struct HalfSpace {
187 normal_d: Vec4,
188}
189
190impl HalfSpace {
191 #[inline]
196 pub fn new(normal_d: Vec4) -> Self {
197 Self {
198 normal_d: normal_d * normal_d.xyz().length_recip(),
199 }
200 }
201
202 #[inline]
204 pub fn normal(&self) -> Vec3A {
205 Vec3A::from_vec4(self.normal_d)
206 }
207
208 #[inline]
211 pub fn d(&self) -> f32 {
212 self.normal_d.w
213 }
214
215 #[inline]
218 pub fn normal_d(&self) -> Vec4 {
219 self.normal_d
220 }
221}
222
223#[derive(Component, Clone, Copy, Debug, Default, Reflect)]
250#[reflect(Component, Default, Debug, Clone)]
251pub struct Frustum {
252 #[reflect(ignore, clone)]
253 pub half_spaces: [HalfSpace; 6],
254}
255
256impl Frustum {
257 #[inline]
259 pub fn from_clip_from_world(clip_from_world: &Mat4) -> Self {
260 let mut frustum = Frustum::from_clip_from_world_no_far(clip_from_world);
261 frustum.half_spaces[5] = HalfSpace::new(clip_from_world.row(2));
262 frustum
263 }
264
265 #[inline]
268 pub fn from_clip_from_world_custom_far(
269 clip_from_world: &Mat4,
270 view_translation: &Vec3,
271 view_backward: &Vec3,
272 far: f32,
273 ) -> Self {
274 let mut frustum = Frustum::from_clip_from_world_no_far(clip_from_world);
275 let far_center = *view_translation - far * *view_backward;
276 frustum.half_spaces[5] =
277 HalfSpace::new(view_backward.extend(-view_backward.dot(far_center)));
278 frustum
279 }
280
281 fn from_clip_from_world_no_far(clip_from_world: &Mat4) -> Self {
287 let row3 = clip_from_world.row(3);
288 let mut half_spaces = [HalfSpace::default(); 6];
289 for (i, half_space) in half_spaces.iter_mut().enumerate().take(5) {
290 let row = clip_from_world.row(i / 2);
291 *half_space = HalfSpace::new(if (i & 1) == 0 && i != 4 {
292 row3 + row
293 } else {
294 row3 - row
295 });
296 }
297 half_spaces[5] = HalfSpace::new(Vec4::new(0.0, 0.0, 0.0, f32::MAX));
298 Self { half_spaces }
299 }
300
301 #[inline]
303 pub fn intersects_sphere(&self, sphere: &Sphere, intersect_far: bool) -> bool {
304 let sphere_center = sphere.center.extend(1.0);
305 let max = if intersect_far { 6 } else { 5 };
306 for half_space in &self.half_spaces[..max] {
307 if half_space.normal_d().dot(sphere_center) + sphere.radius <= 0.0 {
308 return false;
309 }
310 }
311 true
312 }
313
314 #[inline]
316 pub fn intersects_obb(
317 &self,
318 aabb: &Aabb,
319 world_from_local: &Affine3A,
320 intersect_near: bool,
321 intersect_far: bool,
322 ) -> bool {
323 let aabb_center_world = world_from_local.transform_point3a(aabb.center).extend(1.0);
324 for (idx, half_space) in self.half_spaces.into_iter().enumerate() {
325 if idx == 4 && !intersect_near {
326 continue;
327 }
328 if idx == 5 && !intersect_far {
329 continue;
330 }
331 let p_normal = half_space.normal();
332 let relative_radius = aabb.relative_radius(&p_normal, &world_from_local.matrix3);
333 if half_space.normal_d().dot(aabb_center_world) + relative_radius <= 0.0 {
334 return false;
335 }
336 }
337 true
338 }
339
340 #[inline]
343 pub fn contains_aabb(&self, aabb: &Aabb, world_from_local: &Affine3A) -> bool {
344 for half_space in &self.half_spaces {
345 if !aabb.is_in_half_space(half_space, world_from_local) {
346 return false;
347 }
348 }
349 true
350 }
351}
352
353pub struct CubeMapFace {
354 pub target: Vec3,
355 pub up: Vec3,
356}
357
358pub const CUBE_MAP_FACES: [CubeMapFace; 6] = [
366 CubeMapFace {
368 target: Vec3::X,
369 up: Vec3::Y,
370 },
371 CubeMapFace {
373 target: Vec3::NEG_X,
374 up: Vec3::Y,
375 },
376 CubeMapFace {
378 target: Vec3::Y,
379 up: Vec3::Z,
380 },
381 CubeMapFace {
383 target: Vec3::NEG_Y,
384 up: Vec3::NEG_Z,
385 },
386 CubeMapFace {
388 target: Vec3::NEG_Z,
389 up: Vec3::Y,
390 },
391 CubeMapFace {
393 target: Vec3::Z,
394 up: Vec3::Y,
395 },
396];
397
398pub fn face_index_to_name(face_index: usize) -> &'static str {
399 match face_index {
400 0 => "+x",
401 1 => "-x",
402 2 => "+y",
403 3 => "-y",
404 4 => "+z",
405 5 => "-z",
406 _ => "invalid",
407 }
408}
409
410#[derive(Component, Clone, Debug, Default, Reflect)]
411#[reflect(Component, Default, Debug, Clone)]
412pub struct CubemapFrusta {
413 #[reflect(ignore, clone)]
414 pub frusta: [Frustum; 6],
415}
416
417impl CubemapFrusta {
418 pub fn iter(&self) -> impl DoubleEndedIterator<Item = &Frustum> {
419 self.frusta.iter()
420 }
421 pub fn iter_mut(&mut self) -> impl DoubleEndedIterator<Item = &mut Frustum> {
422 self.frusta.iter_mut()
423 }
424}
425
426#[derive(Default, Reflect, Debug, Clone, Copy)]
428pub enum CubemapLayout {
429 #[default]
437 CrossVertical = 0,
438 CrossHorizontal = 1,
445 SequenceVertical = 2,
455 SequenceHorizontal = 3,
460}
461
462#[derive(Component, Debug, Default, Reflect, Clone)]
463#[reflect(Component, Default, Debug, Clone)]
464pub struct CascadesFrusta {
465 #[reflect(ignore, clone)]
466 pub frusta: EntityHashMap<Vec<Frustum>>,
467}
468
469#[cfg(test)]
470mod tests {
471 use core::f32::consts::PI;
472
473 use bevy_math::{ops, Quat};
474 use bevy_transform::components::GlobalTransform;
475
476 use crate::{CameraProjection, PerspectiveProjection};
477
478 use super::*;
479
480 fn big_frustum() -> Frustum {
482 Frustum {
483 half_spaces: [
484 HalfSpace::new(Vec4::new(-0.9701, -0.2425, -0.0000, 7.7611)),
485 HalfSpace::new(Vec4::new(-0.0000, 1.0000, -0.0000, 4.0000)),
486 HalfSpace::new(Vec4::new(-0.0000, -0.2425, -0.9701, 2.9104)),
487 HalfSpace::new(Vec4::new(-0.0000, -1.0000, -0.0000, 4.0000)),
488 HalfSpace::new(Vec4::new(-0.0000, -0.2425, 0.9701, 2.9104)),
489 HalfSpace::new(Vec4::new(0.9701, -0.2425, -0.0000, -1.9403)),
490 ],
491 }
492 }
493
494 #[test]
495 fn intersects_sphere_big_frustum_outside() {
496 let frustum = big_frustum();
498 let sphere = Sphere {
499 center: Vec3A::new(0.9167, 0.0000, 0.0000),
500 radius: 0.7500,
501 };
502 assert!(!frustum.intersects_sphere(&sphere, true));
503 }
504
505 #[test]
506 fn intersects_sphere_big_frustum_intersect() {
507 let frustum = big_frustum();
509 let sphere = Sphere {
510 center: Vec3A::new(7.9288, 0.0000, 2.9728),
511 radius: 2.0000,
512 };
513 assert!(frustum.intersects_sphere(&sphere, true));
514 }
515
516 fn frustum() -> Frustum {
518 Frustum {
519 half_spaces: [
520 HalfSpace::new(Vec4::new(-0.9701, -0.2425, -0.0000, 0.7276)),
521 HalfSpace::new(Vec4::new(-0.0000, 1.0000, -0.0000, 1.0000)),
522 HalfSpace::new(Vec4::new(-0.0000, -0.2425, -0.9701, 0.7276)),
523 HalfSpace::new(Vec4::new(-0.0000, -1.0000, -0.0000, 1.0000)),
524 HalfSpace::new(Vec4::new(-0.0000, -0.2425, 0.9701, 0.7276)),
525 HalfSpace::new(Vec4::new(0.9701, -0.2425, -0.0000, 0.7276)),
526 ],
527 }
528 }
529
530 #[test]
531 fn intersects_sphere_frustum_surrounding() {
532 let frustum = frustum();
534 let sphere = Sphere {
535 center: Vec3A::new(0.0000, 0.0000, 0.0000),
536 radius: 3.0000,
537 };
538 assert!(frustum.intersects_sphere(&sphere, true));
539 }
540
541 #[test]
542 fn intersects_sphere_frustum_contained() {
543 let frustum = frustum();
545 let sphere = Sphere {
546 center: Vec3A::new(0.0000, 0.0000, 0.0000),
547 radius: 0.7000,
548 };
549 assert!(frustum.intersects_sphere(&sphere, true));
550 }
551
552 #[test]
553 fn intersects_sphere_frustum_intersects_plane() {
554 let frustum = frustum();
556 let sphere = Sphere {
557 center: Vec3A::new(0.0000, 0.0000, 0.9695),
558 radius: 0.7000,
559 };
560 assert!(frustum.intersects_sphere(&sphere, true));
561 }
562
563 #[test]
564 fn intersects_sphere_frustum_intersects_2_planes() {
565 let frustum = frustum();
567 let sphere = Sphere {
568 center: Vec3A::new(1.2037, 0.0000, 0.9695),
569 radius: 0.7000,
570 };
571 assert!(frustum.intersects_sphere(&sphere, true));
572 }
573
574 #[test]
575 fn intersects_sphere_frustum_intersects_3_planes() {
576 let frustum = frustum();
578 let sphere = Sphere {
579 center: Vec3A::new(1.2037, -1.0988, 0.9695),
580 radius: 0.7000,
581 };
582 assert!(frustum.intersects_sphere(&sphere, true));
583 }
584
585 #[test]
586 fn intersects_sphere_frustum_dodges_1_plane() {
587 let frustum = frustum();
589 let sphere = Sphere {
590 center: Vec3A::new(-1.7020, 0.0000, 0.0000),
591 radius: 0.7000,
592 };
593 assert!(!frustum.intersects_sphere(&sphere, true));
594 }
595
596 fn long_frustum() -> Frustum {
598 Frustum {
599 half_spaces: [
600 HalfSpace::new(Vec4::new(-0.9998, -0.0222, -0.0000, -1.9543)),
601 HalfSpace::new(Vec4::new(-0.0000, 1.0000, -0.0000, 45.1249)),
602 HalfSpace::new(Vec4::new(-0.0000, -0.0168, -0.9999, 2.2718)),
603 HalfSpace::new(Vec4::new(-0.0000, -1.0000, -0.0000, 45.1249)),
604 HalfSpace::new(Vec4::new(-0.0000, -0.0168, 0.9999, 2.2718)),
605 HalfSpace::new(Vec4::new(0.9998, -0.0222, -0.0000, 7.9528)),
606 ],
607 }
608 }
609
610 #[test]
611 fn intersects_sphere_long_frustum_outside() {
612 let frustum = long_frustum();
614 let sphere = Sphere {
615 center: Vec3A::new(-4.4889, 46.9021, 0.0000),
616 radius: 0.7500,
617 };
618 assert!(!frustum.intersects_sphere(&sphere, true));
619 }
620
621 #[test]
622 fn intersects_sphere_long_frustum_intersect() {
623 let frustum = long_frustum();
625 let sphere = Sphere {
626 center: Vec3A::new(-4.9957, 0.0000, -0.7396),
627 radius: 4.4094,
628 };
629 assert!(frustum.intersects_sphere(&sphere, true));
630 }
631
632 #[test]
633 fn aabb_enclosing() {
634 assert_eq!(Aabb::enclosing(<[Vec3; 0]>::default()), None);
635 assert_eq!(
636 Aabb::enclosing(vec![Vec3::ONE]).unwrap(),
637 Aabb::from_min_max(Vec3::ONE, Vec3::ONE)
638 );
639 assert_eq!(
640 Aabb::enclosing(&[Vec3::Y, Vec3::X, Vec3::Z][..]).unwrap(),
641 Aabb::from_min_max(Vec3::ZERO, Vec3::ONE)
642 );
643 assert_eq!(
644 Aabb::enclosing([
645 Vec3::NEG_X,
646 Vec3::X * 2.0,
647 Vec3::NEG_Y * 5.0,
648 Vec3::Z,
649 Vec3::ZERO
650 ])
651 .unwrap(),
652 Aabb::from_min_max(Vec3::new(-1.0, -5.0, 0.0), Vec3::new(2.0, 0.0, 1.0))
653 );
654 }
655
656 fn contains_aabb_test_frustum() -> Frustum {
658 let proj = PerspectiveProjection {
659 fov: 90.0_f32.to_radians(),
660 aspect_ratio: 1.0,
661 near: 1.0,
662 far: 100.0,
663 };
664 proj.compute_frustum(&GlobalTransform::from_translation(Vec3::new(2.0, 2.0, 0.0)))
665 }
666
667 fn contains_aabb_test_frustum_with_rotation() -> Frustum {
668 let half_extent_world = (((49.5 * 49.5) * 0.5) as f32).sqrt() + 0.5f32.sqrt();
669 let near = 50.5 - half_extent_world;
670 let far = near + 2.0 * half_extent_world;
671 let fov = 2.0 * ops::atan(half_extent_world / near);
672 let proj = PerspectiveProjection {
673 aspect_ratio: 1.0,
674 near,
675 far,
676 fov,
677 };
678 proj.compute_frustum(&GlobalTransform::IDENTITY)
679 }
680
681 #[test]
682 fn aabb_inside_frustum() {
683 let frustum = contains_aabb_test_frustum();
684 let aabb = Aabb {
685 center: Vec3A::ZERO,
686 half_extents: Vec3A::new(0.99, 0.99, 49.49),
687 };
688 let model = Affine3A::from_translation(Vec3::new(2.0, 2.0, -50.5));
689 assert!(frustum.contains_aabb(&aabb, &model));
690 }
691
692 #[test]
693 fn aabb_intersect_frustum() {
694 let frustum = contains_aabb_test_frustum();
695 let aabb = Aabb {
696 center: Vec3A::ZERO,
697 half_extents: Vec3A::new(0.99, 0.99, 49.6),
698 };
699 let model = Affine3A::from_translation(Vec3::new(2.0, 2.0, -50.5));
700 assert!(!frustum.contains_aabb(&aabb, &model));
701 }
702
703 #[test]
704 fn aabb_outside_frustum() {
705 let frustum = contains_aabb_test_frustum();
706 let aabb = Aabb {
707 center: Vec3A::ZERO,
708 half_extents: Vec3A::new(0.99, 0.99, 0.99),
709 };
710 let model = Affine3A::from_translation(Vec3::new(0.0, 0.0, 49.6));
711 assert!(!frustum.contains_aabb(&aabb, &model));
712 }
713
714 #[test]
715 fn aabb_inside_frustum_rotation() {
716 let frustum = contains_aabb_test_frustum_with_rotation();
717 let aabb = Aabb {
718 center: Vec3A::new(0.0, 0.0, 0.0),
719 half_extents: Vec3A::new(0.99, 0.99, 49.49),
720 };
721
722 let model = Affine3A::from_rotation_translation(
723 Quat::from_rotation_x(PI / 4.0),
724 Vec3::new(0.0, 0.0, -50.5),
725 );
726 assert!(frustum.contains_aabb(&aabb, &model));
727 }
728
729 #[test]
730 fn aabb_intersect_frustum_rotation() {
731 let frustum = contains_aabb_test_frustum_with_rotation();
732 let aabb = Aabb {
733 center: Vec3A::new(0.0, 0.0, 0.0),
734 half_extents: Vec3A::new(0.99, 0.99, 49.6),
735 };
736
737 let model = Affine3A::from_rotation_translation(
738 Quat::from_rotation_x(PI / 4.0),
739 Vec3::new(0.0, 0.0, -50.5),
740 );
741 assert!(!frustum.contains_aabb(&aabb, &model));
742 }
743}