From ac8bbafc5caae72e6dcf78fe012294ccf6a0f160 Mon Sep 17 00:00:00 2001 From: Robert Swain Date: Sat, 19 Mar 2022 04:41:28 +0000 Subject: [PATCH] Faster view frustum culling (#4181) # Objective - Reduce time spent in the `check_visibility` system ## Solution - Use `Vec3A` for all bounding volume types to leverage SIMD optimisations and to avoid repeated runtime conversions from `Vec3` to `Vec3A` - Inline all bounding volume intersection methods - Add on-the-fly calculated `Aabb` -> `Sphere` and do `Sphere`-`Frustum` intersection tests before `Aabb`-`Frustum` tests. This is faster for `many_cubes` but could be slower in other cases where the sphere test gives a false-positive that the `Aabb` test discards. Also, I tested precalculating the `Sphere`s and inserting them alongside the `Aabb` but this was slower. - Do not test meshes against the far plane. Apparently games don't do this anymore with infinite projections, and it's one fewer plane to test against. I made it optional and still do the test for culling lights but that is up for discussion. - These collectively reduce `check_visibility` execution time in `many_cubes -- sphere` from 2.76ms to 1.48ms and increase frame rate from ~42fps to ~44fps --- crates/bevy_pbr/src/light.rs | 24 +++--- crates/bevy_render/src/primitives/mod.rs | 86 +++++++++++-------- crates/bevy_render/src/view/visibility/mod.rs | 17 +++- 3 files changed, 76 insertions(+), 51 deletions(-) diff --git a/crates/bevy_pbr/src/light.rs b/crates/bevy_pbr/src/light.rs index ec0988f8d6ef6..8eae0068a2db2 100644 --- a/crates/bevy_pbr/src/light.rs +++ b/crates/bevy_pbr/src/light.rs @@ -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}, @@ -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()); @@ -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; @@ -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; } @@ -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; } @@ -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; } } @@ -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, }; @@ -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); } diff --git a/crates/bevy_render/src/primitives/mod.rs b/crates/bevy_render/src/primitives/mod.rs index 161e8c24fd75a..76c44d2ff0d97 100644 --- a/crates/bevy_render/src/primitives/mod.rs +++ b/crates/bevy_render/src/primitives/mod.rs @@ -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 { @@ -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]), @@ -33,31 +37,35 @@ 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 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 = [ @@ -65,7 +73,7 @@ impl Sphere { 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 @@ -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 @@ -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, @@ -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 { @@ -215,10 +229,10 @@ 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] @@ -226,10 +240,10 @@ mod tests { // 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 @@ -251,10 +265,10 @@ 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] @@ -262,10 +276,10 @@ mod tests { // 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] @@ -273,10 +287,10 @@ mod tests { // 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] @@ -284,10 +298,10 @@ mod tests { // 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] @@ -295,10 +309,10 @@ mod tests { // 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] @@ -306,10 +320,10 @@ mod tests { // 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. @@ -331,10 +345,10 @@ 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] @@ -342,9 +356,9 @@ mod tests { // 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)); } } diff --git a/crates/bevy_render/src/view/visibility/mod.rs b/crates/bevy_render/src/view/visibility/mod.rs index 860ebb2142dae..6d2a57c6e10c2 100644 --- a/crates/bevy_render/src/view/visibility/mod.rs +++ b/crates/bevy_render/src/view/visibility/mod.rs @@ -1,5 +1,6 @@ mod render_layers; +use bevy_math::Vec3A; pub use render_layers::*; use bevy_app::{CoreStage, Plugin}; @@ -12,7 +13,7 @@ use bevy_transform::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 @@ -181,10 +182,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; } }