Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Merged by Bors] - Faster view frustum culling #4181

Closed
wants to merge 6 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 12 additions & 12 deletions crates/bevy_pbr/src/light.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ use std::collections::HashSet;

use bevy_asset::Assets;
use bevy_ecs::prelude::*;
use bevy_math::{Mat4, UVec2, UVec3, Vec2, Vec3, Vec3Swizzles, Vec4, Vec4Swizzles};
use bevy_math::{Mat4, UVec2, UVec3, Vec2, Vec3, Vec3A, Vec3Swizzles, Vec4, Vec4Swizzles};
use bevy_reflect::Reflect;
use bevy_render::{
camera::{Camera, CameraProjection, OrthographicProjection},
Expand Down Expand Up @@ -640,8 +640,8 @@ fn cluster_space_light_aabb(
light_sphere: &Sphere,
) -> (Vec3, Vec3) {
let light_aabb_view = Aabb {
center: (inverse_view_transform * light_sphere.center.extend(1.0)).xyz(),
half_extents: Vec3::splat(light_sphere.radius),
center: Vec3A::from(inverse_view_transform * light_sphere.center.extend(1.0)),
half_extents: Vec3A::splat(light_sphere.radius),
};
let (mut light_aabb_view_min, mut light_aabb_view_max) =
(light_aabb_view.min(), light_aabb_view.max());
Expand Down Expand Up @@ -798,13 +798,13 @@ pub(crate) fn assign_lights_to_clusters(
false
} else {
let light_sphere = Sphere {
center: light.translation,
center: Vec3A::from(light.translation),
radius: light.range,
};

let light_in_view = frusta
.iter()
.any(|frustum| frustum.intersects_sphere(&light_sphere));
.any(|frustum| frustum.intersects_sphere(&light_sphere, true));

if light_in_view {
lights_in_view_count += 1;
Expand Down Expand Up @@ -875,12 +875,12 @@ pub(crate) fn assign_lights_to_clusters(
let mut cluster_index_estimate = 0.0;
for light in lights.iter() {
let light_sphere = Sphere {
center: light.translation,
center: Vec3A::from(light.translation),
radius: light.range,
};

// Check if the light is within the view frustum
if !frustum.intersects_sphere(&light_sphere) {
if !frustum.intersects_sphere(&light_sphere, true) {
continue;
}

Expand Down Expand Up @@ -965,12 +965,12 @@ pub(crate) fn assign_lights_to_clusters(

for light in lights.iter() {
let light_sphere = Sphere {
center: light.translation,
center: Vec3A::from(light.translation),
radius: light.range,
};

// Check if the light is within the view frustum
if !frustum.intersects_sphere(&light_sphere) {
if !frustum.intersects_sphere(&light_sphere, true) {
continue;
}

Expand Down Expand Up @@ -1174,7 +1174,7 @@ pub fn check_light_mesh_visibility(

// If we have an aabb and transform, do frustum culling
if let (Some(aabb), Some(transform)) = (maybe_aabb, maybe_transform) {
if !frustum.intersects_obb(aabb, &transform.compute_matrix()) {
if !frustum.intersects_obb(aabb, &transform.compute_matrix(), true) {
continue;
}
}
Expand Down Expand Up @@ -1209,7 +1209,7 @@ pub fn check_light_mesh_visibility(

let view_mask = maybe_view_mask.copied().unwrap_or_default();
let light_sphere = Sphere {
center: transform.translation,
center: Vec3A::from(transform.translation),
radius: point_light.range,
};

Expand Down Expand Up @@ -1242,7 +1242,7 @@ pub fn check_light_mesh_visibility(
.iter()
.zip(cubemap_visible_entities.iter_mut())
{
if frustum.intersects_obb(aabb, &model_to_world) {
if frustum.intersects_obb(aabb, &model_to_world, true) {
computed_visibility.is_visible = true;
visible_entities.entities.push(entity);
}
Expand Down
86 changes: 50 additions & 36 deletions crates/bevy_render/src/primitives/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,15 @@ use bevy_reflect::Reflect;
#[derive(Component, Clone, Debug, Default, Reflect)]
#[reflect(Component)]
pub struct Aabb {
pub center: Vec3,
pub half_extents: Vec3,
pub center: Vec3A,
pub half_extents: Vec3A,
}

impl Aabb {
#[inline]
pub fn from_min_max(minimum: Vec3, maximum: Vec3) -> Self {
let minimum = Vec3A::from(minimum);
let maximum = Vec3A::from(maximum);
let center = 0.5 * (maximum + minimum);
let half_extents = 0.5 * (maximum - minimum);
Self {
Expand All @@ -21,9 +24,10 @@ impl Aabb {
}

/// Calculate the relative radius of the AABB with respect to a plane
#[inline]
pub fn relative_radius(&self, p_normal: &Vec3A, axes: &[Vec3A]) -> f32 {
// NOTE: dot products on Vec3A use SIMD and even with the overhead of conversion are net faster than Vec3
let half_extents = Vec3A::from(self.half_extents);
let half_extents = self.half_extents;
Vec3A::new(
p_normal.dot(axes[0]),
p_normal.dot(axes[1]),
Expand All @@ -33,39 +37,43 @@ impl Aabb {
.dot(half_extents)
}

pub fn min(&self) -> Vec3 {
#[inline]
pub fn min(&self) -> Vec3A {
self.center - self.half_extents
}

pub fn max(&self) -> Vec3 {
#[inline]
pub fn max(&self) -> Vec3A {
self.center + self.half_extents
}
}

impl From<Sphere> for Aabb {
#[inline]
fn from(sphere: Sphere) -> Self {
Self {
center: sphere.center,
half_extents: Vec3::splat(sphere.radius),
half_extents: Vec3A::splat(sphere.radius),
}
}
}

#[derive(Debug, Default)]
pub struct Sphere {
pub center: Vec3,
pub center: Vec3A,
pub radius: f32,
}

impl Sphere {
#[inline]
pub fn intersects_obb(&self, aabb: &Aabb, local_to_world: &Mat4) -> bool {
let aabb_center_world = *local_to_world * aabb.center.extend(1.0);
let axes = [
Vec3A::from(local_to_world.x_axis),
Vec3A::from(local_to_world.y_axis),
Vec3A::from(local_to_world.z_axis),
];
let v = Vec3A::from(aabb_center_world) - Vec3A::from(self.center);
let v = Vec3A::from(aabb_center_world) - self.center;
let d = v.length();
let relative_radius = aabb.relative_radius(&(v / d), &axes);
d < self.radius + relative_radius
Expand Down Expand Up @@ -96,8 +104,8 @@ impl Plane {

/// `Plane` unit normal
#[inline]
pub fn normal(&self) -> Vec3 {
self.normal_d.xyz()
pub fn normal(&self) -> Vec3A {
Vec3A::from(self.normal_d)
}

/// Signed distance from the origin along the unit normal such that n.p + d = 0 for point p in
Expand Down Expand Up @@ -127,6 +135,7 @@ impl Frustum {
// projection matrix is from Foundations of Game Engine Development 2
// Rendering by Lengyel. Slight modification has been made for when
// the far plane is infinite but we still want to cull to a far plane.
#[inline]
pub fn from_view_projection(
view_projection: &Mat4,
view_translation: &Vec3,
Expand All @@ -148,24 +157,29 @@ impl Frustum {
Self { planes }
}

pub fn intersects_sphere(&self, sphere: &Sphere) -> bool {
for plane in &self.planes {
if plane.normal_d().dot(sphere.center.extend(1.0)) + sphere.radius <= 0.0 {
#[inline]
pub fn intersects_sphere(&self, sphere: &Sphere, intersect_far: bool) -> bool {
let sphere_center = sphere.center.extend(1.0);
let max = if intersect_far { 6 } else { 5 };
for plane in &self.planes[..max] {
if plane.normal_d().dot(sphere_center) + sphere.radius <= 0.0 {
return false;
}
}
true
}

pub fn intersects_obb(&self, aabb: &Aabb, model_to_world: &Mat4) -> bool {
let aabb_center_world = *model_to_world * aabb.center.extend(1.0);
#[inline]
pub fn intersects_obb(&self, aabb: &Aabb, model_to_world: &Mat4, intersect_far: bool) -> bool {
let aabb_center_world = model_to_world.transform_point3a(aabb.center).extend(1.0);
let axes = [
Vec3A::from(model_to_world.x_axis),
Vec3A::from(model_to_world.y_axis),
Vec3A::from(model_to_world.z_axis),
];

for plane in &self.planes {
let max = if intersect_far { 6 } else { 5 };
for plane in &self.planes[..max] {
let p_normal = Vec3A::from(plane.normal_d());
let relative_radius = aabb.relative_radius(&p_normal, &axes);
if plane.normal_d().dot(aabb_center_world) + relative_radius <= 0.0 {
Expand Down Expand Up @@ -215,21 +229,21 @@ mod tests {
// Sphere outside frustum
let frustum = big_frustum();
let sphere = Sphere {
center: Vec3::new(0.9167, 0.0000, 0.0000),
center: Vec3A::new(0.9167, 0.0000, 0.0000),
radius: 0.7500,
};
assert!(!frustum.intersects_sphere(&sphere));
assert!(!frustum.intersects_sphere(&sphere, true));
}

#[test]
fn intersects_sphere_big_frustum_intersect() {
// Sphere intersects frustum boundary
let frustum = big_frustum();
let sphere = Sphere {
center: Vec3::new(7.9288, 0.0000, 2.9728),
center: Vec3A::new(7.9288, 0.0000, 2.9728),
radius: 2.0000,
};
assert!(frustum.intersects_sphere(&sphere));
assert!(frustum.intersects_sphere(&sphere, true));
}

// A frustum
Expand All @@ -251,65 +265,65 @@ mod tests {
// Sphere surrounds frustum
let frustum = frustum();
let sphere = Sphere {
center: Vec3::new(0.0000, 0.0000, 0.0000),
center: Vec3A::new(0.0000, 0.0000, 0.0000),
radius: 3.0000,
};
assert!(frustum.intersects_sphere(&sphere));
assert!(frustum.intersects_sphere(&sphere, true));
}

#[test]
fn intersects_sphere_frustum_contained() {
// Sphere is contained in frustum
let frustum = frustum();
let sphere = Sphere {
center: Vec3::new(0.0000, 0.0000, 0.0000),
center: Vec3A::new(0.0000, 0.0000, 0.0000),
radius: 0.7000,
};
assert!(frustum.intersects_sphere(&sphere));
assert!(frustum.intersects_sphere(&sphere, true));
}

#[test]
fn intersects_sphere_frustum_intersects_plane() {
// Sphere intersects a plane
let frustum = frustum();
let sphere = Sphere {
center: Vec3::new(0.0000, 0.0000, 0.9695),
center: Vec3A::new(0.0000, 0.0000, 0.9695),
radius: 0.7000,
};
assert!(frustum.intersects_sphere(&sphere));
assert!(frustum.intersects_sphere(&sphere, true));
}

#[test]
fn intersects_sphere_frustum_intersects_2_planes() {
// Sphere intersects 2 planes
let frustum = frustum();
let sphere = Sphere {
center: Vec3::new(1.2037, 0.0000, 0.9695),
center: Vec3A::new(1.2037, 0.0000, 0.9695),
radius: 0.7000,
};
assert!(frustum.intersects_sphere(&sphere));
assert!(frustum.intersects_sphere(&sphere, true));
}

#[test]
fn intersects_sphere_frustum_intersects_3_planes() {
// Sphere intersects 3 planes
let frustum = frustum();
let sphere = Sphere {
center: Vec3::new(1.2037, -1.0988, 0.9695),
center: Vec3A::new(1.2037, -1.0988, 0.9695),
radius: 0.7000,
};
assert!(frustum.intersects_sphere(&sphere));
assert!(frustum.intersects_sphere(&sphere, true));
}

#[test]
fn intersects_sphere_frustum_dodges_1_plane() {
// Sphere avoids intersecting the frustum by 1 plane
let frustum = frustum();
let sphere = Sphere {
center: Vec3::new(-1.7020, 0.0000, 0.0000),
center: Vec3A::new(-1.7020, 0.0000, 0.0000),
radius: 0.7000,
};
assert!(!frustum.intersects_sphere(&sphere));
assert!(!frustum.intersects_sphere(&sphere, true));
}

// A long frustum.
Expand All @@ -331,20 +345,20 @@ mod tests {
// Sphere outside frustum
let frustum = long_frustum();
let sphere = Sphere {
center: Vec3::new(-4.4889, 46.9021, 0.0000),
center: Vec3A::new(-4.4889, 46.9021, 0.0000),
radius: 0.7500,
};
assert!(!frustum.intersects_sphere(&sphere));
assert!(!frustum.intersects_sphere(&sphere, true));
}

#[test]
fn intersects_sphere_long_frustum_intersect() {
// Sphere intersects frustum boundary
let frustum = long_frustum();
let sphere = Sphere {
center: Vec3::new(-4.9957, 0.0000, -0.7396),
center: Vec3A::new(-4.9957, 0.0000, -0.7396),
radius: 4.4094,
};
assert!(frustum.intersects_sphere(&sphere));
assert!(frustum.intersects_sphere(&sphere, true));
}
}
17 changes: 14 additions & 3 deletions crates/bevy_render/src/view/visibility/mod.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
mod render_layers;

use bevy_math::Vec3A;
pub use render_layers::*;

use bevy_app::{CoreStage, Plugin};
Expand All @@ -11,7 +12,7 @@ use bevy_transform::{components::GlobalTransform, TransformSystem};
use crate::{
camera::{Camera, CameraProjection, OrthographicProjection, PerspectiveProjection},
mesh::Mesh,
primitives::{Aabb, Frustum},
primitives::{Aabb, Frustum, Sphere},
};

/// User indication of whether an entity is visible
Expand Down Expand Up @@ -180,10 +181,20 @@ pub fn check_visibility(
}

// If we have an aabb and transform, do frustum culling
if let (Some(aabb), None, Some(transform)) =
if let (Some(model_aabb), None, Some(transform)) =
(maybe_aabb, maybe_no_frustum_culling, maybe_transform)
{
if !frustum.intersects_obb(aabb, &transform.compute_matrix()) {
let model = transform.compute_matrix();
let model_sphere = Sphere {
center: model.transform_point3a(model_aabb.center),
radius: (Vec3A::from(transform.scale) * model_aabb.half_extents).length(),
};
// Do quick sphere-based frustum culling
if !frustum.intersects_sphere(&model_sphere, false) {
continue;
}
// If we have an aabb, do aabb-based frustum culling
if !frustum.intersects_obb(model_aabb, &model, false) {
continue;
}
}
Expand Down