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
This commit is contained in:
parent
e7a9420443
commit
ac8bbafc5c
@ -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);
|
||||
}
|
||||
|
||||
@ -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<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 = [
|
||||
@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user