From 206f4f7f5f3e99f772cb2ec68ad78f60da2eff7d Mon Sep 17 00:00:00 2001 From: Sou1gh0st Date: Mon, 2 Dec 2024 05:30:01 +0800 Subject: [PATCH] Add .contains_aabb for Frustum (#16022) # Objective - Fixes: #15663 ## Solution - Add an `is_forward_plane` method to `Aabb`, and a `contains_aabb` method to `Frustum`. ## Test - I have created a frustum with an offset along with three unit tests to evaluate the `contains_aabb` algorithm. ## Explanation for the Test Cases - To facilitate the code review, I will explain how the frustum is created. Initially, we create a frustum without any offset and then create a cuboid that is just contained within it. image - Secondly, we move the cuboid by 2 units along both the x-axis and the y-axis to make it more general. ## Reference - [Frustum Culling](https://learnopengl.com/Guest-Articles/2021/Scene/Frustum-Culling#) - [AABB Plane intersection](https://gdbooks.gitbooks.io/3dcollisions/content/Chapter2/static_aabb_plane.html) --------- Co-authored-by: IQuick 143 --- crates/bevy_render/src/primitives/mod.rs | 121 +++++++++++++++++++++++ 1 file changed, 121 insertions(+) diff --git a/crates/bevy_render/src/primitives/mod.rs b/crates/bevy_render/src/primitives/mod.rs index 70a06ae635840..48c0652fef3b9 100644 --- a/crates/bevy_render/src/primitives/mod.rs +++ b/crates/bevy_render/src/primitives/mod.rs @@ -95,6 +95,20 @@ impl Aabb { pub fn max(&self) -> Vec3A { self.center + self.half_extents } + + /// Check if the AABB is at the front side of the bisecting plane. + /// Referenced from: [AABB Plane intersection](https://gdbooks.gitbooks.io/3dcollisions/content/Chapter2/static_aabb_plane.html) + #[inline] + pub fn is_in_half_space(&self, half_space: &HalfSpace, world_from_local: &Affine3A) -> bool { + // transform the half-extents into world space. + let half_extents_world = world_from_local.matrix3.abs() * self.half_extents.abs(); + // collapse the half-extents onto the plane normal. + let p_normal = half_space.normal(); + let r = half_extents_world.dot(p_normal.abs()); + let aabb_center_world = world_from_local.transform_point3a(self.center); + let signed_distance = p_normal.dot(aabb_center_world) + half_space.d(); + signed_distance > r + } } impl From for Aabb { @@ -298,6 +312,18 @@ impl Frustum { } true } + + /// Check if the frustum contains the Axis-Aligned Bounding Box (AABB). + /// Referenced from: [Frustum Culling](https://learnopengl.com/Guest-Articles/2021/Scene/Frustum-Culling) + #[inline] + pub fn contains_aabb(&self, aabb: &Aabb, world_from_local: &Affine3A) -> bool { + for half_space in &self.half_spaces { + if !aabb.is_in_half_space(half_space, world_from_local) { + return false; + } + } + true + } } #[derive(Component, Clone, Debug, Default, Reflect)] @@ -325,6 +351,13 @@ pub struct CascadesFrusta { #[cfg(test)] mod tests { + use core::f32::consts::PI; + + use bevy_math::{ops, Quat}; + use bevy_transform::components::GlobalTransform; + + use crate::camera::{CameraProjection, PerspectiveProjection}; + use super::*; // A big, offset frustum @@ -502,4 +535,92 @@ mod tests { Aabb::from_min_max(Vec3::new(-1.0, -5.0, 0.0), Vec3::new(2.0, 0.0, 1.0)) ); } + + // A frustum with an offset for testing the [`Frustum::contains_aabb`] algorithm. + fn contains_aabb_test_frustum() -> Frustum { + let proj = PerspectiveProjection { + fov: 90.0_f32.to_radians(), + aspect_ratio: 1.0, + near: 1.0, + far: 100.0, + }; + proj.compute_frustum(&GlobalTransform::from_translation(Vec3::new(2.0, 2.0, 0.0))) + } + + fn contains_aabb_test_frustum_with_rotation() -> Frustum { + let half_extent_world = (((49.5 * 49.5) * 0.5) as f32).sqrt() + 0.5f32.sqrt(); + let near = 50.5 - half_extent_world; + let far = near + 2.0 * half_extent_world; + let fov = 2.0 * ops::atan(half_extent_world / near); + let proj = PerspectiveProjection { + aspect_ratio: 1.0, + near, + far, + fov, + }; + proj.compute_frustum(&GlobalTransform::IDENTITY) + } + + #[test] + fn aabb_inside_frustum() { + let frustum = contains_aabb_test_frustum(); + let aabb = Aabb { + center: Vec3A::ZERO, + half_extents: Vec3A::new(0.99, 0.99, 49.49), + }; + let model = Affine3A::from_translation(Vec3::new(2.0, 2.0, -50.5)); + assert!(frustum.contains_aabb(&aabb, &model)); + } + + #[test] + fn aabb_intersect_frustum() { + let frustum = contains_aabb_test_frustum(); + let aabb = Aabb { + center: Vec3A::ZERO, + half_extents: Vec3A::new(0.99, 0.99, 49.6), + }; + let model = Affine3A::from_translation(Vec3::new(2.0, 2.0, -50.5)); + assert!(!frustum.contains_aabb(&aabb, &model)); + } + + #[test] + fn aabb_outside_frustum() { + let frustum = contains_aabb_test_frustum(); + let aabb = Aabb { + center: Vec3A::ZERO, + half_extents: Vec3A::new(0.99, 0.99, 0.99), + }; + let model = Affine3A::from_translation(Vec3::new(0.0, 0.0, 49.6)); + assert!(!frustum.contains_aabb(&aabb, &model)); + } + + #[test] + fn aabb_inside_frustum_rotation() { + let frustum = contains_aabb_test_frustum_with_rotation(); + let aabb = Aabb { + center: Vec3A::new(0.0, 0.0, 0.0), + half_extents: Vec3A::new(0.99, 0.99, 49.49), + }; + + let model = Affine3A::from_rotation_translation( + Quat::from_rotation_x(PI / 4.0), + Vec3::new(0.0, 0.0, -50.5), + ); + assert!(frustum.contains_aabb(&aabb, &model)); + } + + #[test] + fn aabb_intersect_frustum_rotation() { + let frustum = contains_aabb_test_frustum_with_rotation(); + let aabb = Aabb { + center: Vec3A::new(0.0, 0.0, 0.0), + half_extents: Vec3A::new(0.99, 0.99, 49.6), + }; + + let model = Affine3A::from_rotation_translation( + Quat::from_rotation_x(PI / 4.0), + Vec3::new(0.0, 0.0, -50.5), + ); + assert!(!frustum.contains_aabb(&aabb, &model)); + } }