Skip to content

Commit

Permalink
Add .contains_aabb for Frustum (bevyengine#16022)
Browse files Browse the repository at this point in the history
# Objective

- Fixes: bevyengine#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.

<img width="714" alt="image"
src="https://github.com/user-attachments/assets/a9ac53a2-f8a3-4e09-b20b-4ee71b27a099">

- 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 <IQuick143cz@gmail.com>
  • Loading branch information
Soulghost and IQuick143 authored Dec 1, 2024
1 parent fcfb685 commit 206f4f7
Showing 1 changed file with 121 additions and 0 deletions.
121 changes: 121 additions & 0 deletions crates/bevy_render/src/primitives/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Sphere> for Aabb {
Expand Down Expand Up @@ -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)]
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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));
}
}

0 comments on commit 206f4f7

Please sign in to comment.