
# Objective - Fixes #4234 - Fixes #4473 - Built on top of #3989 - Improve performance of `assign_lights_to_clusters` ## Solution - Remove the OBB-based cluster light assignment algorithm and calculation of view space AABBs - Implement the 'iterative sphere refinement' algorithm used in Just Cause 3 by Emil Persson as documented in the Siggraph 2015 Practical Clustered Shading talk by Persson, on pages 42-44 http://newq.net/dl/pub/s2015_practical.pdf - Adapt to also support orthographic projections - Add `many_lights -- orthographic` for testing many lights using an orthographic projection ## Results - `assign_lights_to_clusters` in `many_lights` before this PR on an M1 Max over 1500 frames had a median execution time of 1.71ms. With this PR it is 1.51ms, a reduction of 0.2ms or 11.7% for this system. --- ## Changelog - Changed: Improved cluster light assignment performance Co-authored-by: robtfm <50659922+robtfm@users.noreply.github.com> Co-authored-by: Carter Anderson <mcanders1@gmail.com>
365 lines
11 KiB
Rust
365 lines
11 KiB
Rust
use bevy_ecs::{component::Component, reflect::ReflectComponent};
|
|
use bevy_math::{Mat4, Vec3, Vec3A, Vec4, Vec4Swizzles};
|
|
use bevy_reflect::Reflect;
|
|
|
|
/// An Axis-Aligned Bounding Box
|
|
#[derive(Component, Clone, Debug, Default, Reflect)]
|
|
#[reflect(Component)]
|
|
pub struct Aabb {
|
|
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 {
|
|
center,
|
|
half_extents,
|
|
}
|
|
}
|
|
|
|
/// 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 = self.half_extents;
|
|
Vec3A::new(
|
|
p_normal.dot(axes[0]),
|
|
p_normal.dot(axes[1]),
|
|
p_normal.dot(axes[2]),
|
|
)
|
|
.abs()
|
|
.dot(half_extents)
|
|
}
|
|
|
|
#[inline]
|
|
pub fn min(&self) -> Vec3A {
|
|
self.center - self.half_extents
|
|
}
|
|
|
|
#[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: Vec3A::splat(sphere.radius),
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Debug, Default)]
|
|
pub struct Sphere {
|
|
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) - self.center;
|
|
let d = v.length();
|
|
let relative_radius = aabb.relative_radius(&(v / d), &axes);
|
|
d < self.radius + relative_radius
|
|
}
|
|
}
|
|
|
|
/// A plane defined by a unit normal and distance from the origin along the normal
|
|
/// Any point p is in the plane if n.p + d = 0
|
|
/// For planes defining half-spaces such as for frusta, if n.p + d > 0 then p is on
|
|
/// the positive side (inside) of the plane.
|
|
#[derive(Clone, Copy, Debug, Default)]
|
|
pub struct Plane {
|
|
normal_d: Vec4,
|
|
}
|
|
|
|
impl Plane {
|
|
/// Constructs a `Plane` from a 4D vector whose first 3 components
|
|
/// are the normal and whose last component is the distance along the normal
|
|
/// from the origin.
|
|
/// This constructor ensures that the normal is normalized and the distance is
|
|
/// scaled accordingly so it represents the signed distance from the origin.
|
|
#[inline]
|
|
pub fn new(normal_d: Vec4) -> Self {
|
|
Self {
|
|
normal_d: normal_d * normal_d.xyz().length_recip(),
|
|
}
|
|
}
|
|
|
|
/// `Plane` unit normal
|
|
#[inline]
|
|
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
|
|
/// the `Plane`
|
|
#[inline]
|
|
pub fn d(&self) -> f32 {
|
|
self.normal_d.w
|
|
}
|
|
|
|
/// `Plane` unit normal and signed distance from the origin such that n.p + d = 0 for point p
|
|
/// in the `Plane`
|
|
#[inline]
|
|
pub fn normal_d(&self) -> Vec4 {
|
|
self.normal_d
|
|
}
|
|
}
|
|
|
|
#[derive(Component, Clone, Copy, Debug, Default, Reflect)]
|
|
#[reflect(Component)]
|
|
pub struct Frustum {
|
|
#[reflect(ignore)]
|
|
pub planes: [Plane; 6],
|
|
}
|
|
|
|
impl Frustum {
|
|
// NOTE: This approach of extracting the frustum planes from the view
|
|
// 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,
|
|
view_backward: &Vec3,
|
|
far: f32,
|
|
) -> Self {
|
|
let row3 = view_projection.row(3);
|
|
let mut planes = [Plane::default(); 6];
|
|
for (i, plane) in planes.iter_mut().enumerate().take(5) {
|
|
let row = view_projection.row(i / 2);
|
|
*plane = Plane::new(if (i & 1) == 0 && i != 4 {
|
|
row3 + row
|
|
} else {
|
|
row3 - row
|
|
});
|
|
}
|
|
let far_center = *view_translation - far * *view_backward;
|
|
planes[5] = Plane::new(view_backward.extend(-view_backward.dot(far_center)));
|
|
Self { planes }
|
|
}
|
|
|
|
#[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
|
|
}
|
|
|
|
#[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),
|
|
];
|
|
|
|
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 {
|
|
return false;
|
|
}
|
|
}
|
|
true
|
|
}
|
|
}
|
|
|
|
#[derive(Component, Debug, Default, Reflect)]
|
|
#[reflect(Component)]
|
|
pub struct CubemapFrusta {
|
|
#[reflect(ignore)]
|
|
pub frusta: [Frustum; 6],
|
|
}
|
|
|
|
impl CubemapFrusta {
|
|
pub fn iter(&self) -> impl DoubleEndedIterator<Item = &Frustum> {
|
|
self.frusta.iter()
|
|
}
|
|
pub fn iter_mut(&mut self) -> impl DoubleEndedIterator<Item = &mut Frustum> {
|
|
self.frusta.iter_mut()
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
// A big, offset frustum
|
|
fn big_frustum() -> Frustum {
|
|
Frustum {
|
|
planes: [
|
|
Plane::new(Vec4::new(-0.9701, -0.2425, -0.0000, 7.7611)),
|
|
Plane::new(Vec4::new(-0.0000, 1.0000, -0.0000, 4.0000)),
|
|
Plane::new(Vec4::new(-0.0000, -0.2425, -0.9701, 2.9104)),
|
|
Plane::new(Vec4::new(-0.0000, -1.0000, -0.0000, 4.0000)),
|
|
Plane::new(Vec4::new(-0.0000, -0.2425, 0.9701, 2.9104)),
|
|
Plane::new(Vec4::new(0.9701, -0.2425, -0.0000, -1.9403)),
|
|
],
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn intersects_sphere_big_frustum_outside() {
|
|
// Sphere outside frustum
|
|
let frustum = big_frustum();
|
|
let sphere = Sphere {
|
|
center: Vec3A::new(0.9167, 0.0000, 0.0000),
|
|
radius: 0.7500,
|
|
};
|
|
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: Vec3A::new(7.9288, 0.0000, 2.9728),
|
|
radius: 2.0000,
|
|
};
|
|
assert!(frustum.intersects_sphere(&sphere, true));
|
|
}
|
|
|
|
// A frustum
|
|
fn frustum() -> Frustum {
|
|
Frustum {
|
|
planes: [
|
|
Plane::new(Vec4::new(-0.9701, -0.2425, -0.0000, 0.7276)),
|
|
Plane::new(Vec4::new(-0.0000, 1.0000, -0.0000, 1.0000)),
|
|
Plane::new(Vec4::new(-0.0000, -0.2425, -0.9701, 0.7276)),
|
|
Plane::new(Vec4::new(-0.0000, -1.0000, -0.0000, 1.0000)),
|
|
Plane::new(Vec4::new(-0.0000, -0.2425, 0.9701, 0.7276)),
|
|
Plane::new(Vec4::new(0.9701, -0.2425, -0.0000, 0.7276)),
|
|
],
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn intersects_sphere_frustum_surrounding() {
|
|
// Sphere surrounds frustum
|
|
let frustum = frustum();
|
|
let sphere = Sphere {
|
|
center: Vec3A::new(0.0000, 0.0000, 0.0000),
|
|
radius: 3.0000,
|
|
};
|
|
assert!(frustum.intersects_sphere(&sphere, true));
|
|
}
|
|
|
|
#[test]
|
|
fn intersects_sphere_frustum_contained() {
|
|
// Sphere is contained in frustum
|
|
let frustum = frustum();
|
|
let sphere = Sphere {
|
|
center: Vec3A::new(0.0000, 0.0000, 0.0000),
|
|
radius: 0.7000,
|
|
};
|
|
assert!(frustum.intersects_sphere(&sphere, true));
|
|
}
|
|
|
|
#[test]
|
|
fn intersects_sphere_frustum_intersects_plane() {
|
|
// Sphere intersects a plane
|
|
let frustum = frustum();
|
|
let sphere = Sphere {
|
|
center: Vec3A::new(0.0000, 0.0000, 0.9695),
|
|
radius: 0.7000,
|
|
};
|
|
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: Vec3A::new(1.2037, 0.0000, 0.9695),
|
|
radius: 0.7000,
|
|
};
|
|
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: Vec3A::new(1.2037, -1.0988, 0.9695),
|
|
radius: 0.7000,
|
|
};
|
|
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: Vec3A::new(-1.7020, 0.0000, 0.0000),
|
|
radius: 0.7000,
|
|
};
|
|
assert!(!frustum.intersects_sphere(&sphere, true));
|
|
}
|
|
|
|
// A long frustum.
|
|
fn long_frustum() -> Frustum {
|
|
Frustum {
|
|
planes: [
|
|
Plane::new(Vec4::new(-0.9998, -0.0222, -0.0000, -1.9543)),
|
|
Plane::new(Vec4::new(-0.0000, 1.0000, -0.0000, 45.1249)),
|
|
Plane::new(Vec4::new(-0.0000, -0.0168, -0.9999, 2.2718)),
|
|
Plane::new(Vec4::new(-0.0000, -1.0000, -0.0000, 45.1249)),
|
|
Plane::new(Vec4::new(-0.0000, -0.0168, 0.9999, 2.2718)),
|
|
Plane::new(Vec4::new(0.9998, -0.0222, -0.0000, 7.9528)),
|
|
],
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn intersects_sphere_long_frustum_outside() {
|
|
// Sphere outside frustum
|
|
let frustum = long_frustum();
|
|
let sphere = Sphere {
|
|
center: Vec3A::new(-4.4889, 46.9021, 0.0000),
|
|
radius: 0.7500,
|
|
};
|
|
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: Vec3A::new(-4.9957, 0.0000, -0.7396),
|
|
radius: 4.4094,
|
|
};
|
|
assert!(frustum.intersects_sphere(&sphere, true));
|
|
}
|
|
}
|