From 14050ff25ba5f3780482a717eee1a9a0a9d68ccd Mon Sep 17 00:00:00 2001 From: Robert Swain Date: Sat, 26 Mar 2022 06:52:28 +0100 Subject: [PATCH 01/10] WIP: Faster assign_lights_to_clusters Using the Iterative Sphere Refinement algorithm by Persson et al in Practical Clustered Shading: http://newq.net/dl/pub/s2015_practical.pdf --- crates/bevy_pbr/src/light.rs | 474 ++++++++++++++++------- crates/bevy_render/src/primitives/mod.rs | 2 +- examples/stress_tests/many_lights.rs | 20 +- 3 files changed, 363 insertions(+), 133 deletions(-) diff --git a/crates/bevy_pbr/src/light.rs b/crates/bevy_pbr/src/light.rs index 90eda98082ae0..711a1961dda42 100644 --- a/crates/bevy_pbr/src/light.rs +++ b/crates/bevy_pbr/src/light.rs @@ -8,7 +8,7 @@ use bevy_render::{ camera::{Camera, CameraProjection, OrthographicProjection}, color::Color, prelude::Image, - primitives::{Aabb, CubemapFrusta, Frustum, Sphere}, + primitives::{Aabb, CubemapFrusta, Frustum, Plane, Sphere}, render_resource::BufferBindingType, renderer::RenderDevice, view::{ComputedVisibility, RenderLayers, Visibility, VisibleEntities}, @@ -366,7 +366,7 @@ pub struct Clusters { /// and explicitly-configured to avoid having unnecessarily many slices close to the camera. pub(crate) near: f32, pub(crate) far: f32, - aabbs: Vec, + // aabbs: Vec, pub(crate) lights: Vec, } @@ -397,97 +397,97 @@ fn clip_to_view(inverse_projection: Mat4, clip: Vec4) -> Vec4 { view / view.w } -fn screen_to_view(screen_size: Vec2, inverse_projection: Mat4, screen: Vec2, ndc_z: f32) -> Vec4 { - let tex_coord = screen / screen_size; - let clip = Vec4::new( - tex_coord.x * 2.0 - 1.0, - (1.0 - tex_coord.y) * 2.0 - 1.0, - ndc_z, - 1.0, - ); - clip_to_view(inverse_projection, clip) -} - -// Calculate the intersection of a ray from the eye through the view space position to a z plane -fn line_intersection_to_z_plane(origin: Vec3, p: Vec3, z: f32) -> Vec3 { - let v = p - origin; - let t = (z - Vec3::Z.dot(origin)) / Vec3::Z.dot(v); - origin + t * v -} - -#[allow(clippy::too_many_arguments)] -fn compute_aabb_for_cluster( - z_near: f32, - z_far: f32, - tile_size: Vec2, - screen_size: Vec2, - inverse_projection: Mat4, - is_orthographic: bool, - cluster_dimensions: UVec3, - ijk: UVec3, -) -> Aabb { - let ijk = ijk.as_vec3(); - - // Calculate the minimum and maximum points in screen space - let p_min = ijk.xy() * tile_size; - let p_max = p_min + tile_size; - - let cluster_min; - let cluster_max; - if is_orthographic { - // Use linear depth slicing for orthographic - - // Convert to view space at the cluster near and far planes - // NOTE: 1.0 is the near plane due to using reverse z projections - let p_min = screen_to_view( - screen_size, - inverse_projection, - p_min, - 1.0 - (ijk.z / cluster_dimensions.z as f32), - ) - .xyz(); - let p_max = screen_to_view( - screen_size, - inverse_projection, - p_max, - 1.0 - ((ijk.z + 1.0) / cluster_dimensions.z as f32), - ) - .xyz(); - - cluster_min = p_min.min(p_max); - cluster_max = p_min.max(p_max); - } else { - // Convert to view space at the near plane - // NOTE: 1.0 is the near plane due to using reverse z projections - let p_min = screen_to_view(screen_size, inverse_projection, p_min, 1.0); - let p_max = screen_to_view(screen_size, inverse_projection, p_max, 1.0); - - let z_far_over_z_near = -z_far / -z_near; - let cluster_near = if ijk.z == 0.0 { - 0.0 - } else { - -z_near * z_far_over_z_near.powf((ijk.z - 1.0) / (cluster_dimensions.z - 1) as f32) - }; - // NOTE: This could be simplified to: - // cluster_far = cluster_near * z_far_over_z_near; - let cluster_far = if cluster_dimensions.z == 1 { - -z_far - } else { - -z_near * z_far_over_z_near.powf(ijk.z / (cluster_dimensions.z - 1) as f32) - }; - - // Calculate the four intersection points of the min and max points with the cluster near and far planes - let p_min_near = line_intersection_to_z_plane(Vec3::ZERO, p_min.xyz(), cluster_near); - let p_min_far = line_intersection_to_z_plane(Vec3::ZERO, p_min.xyz(), cluster_far); - let p_max_near = line_intersection_to_z_plane(Vec3::ZERO, p_max.xyz(), cluster_near); - let p_max_far = line_intersection_to_z_plane(Vec3::ZERO, p_max.xyz(), cluster_far); - - cluster_min = p_min_near.min(p_min_far).min(p_max_near.min(p_max_far)); - cluster_max = p_min_near.max(p_min_far).max(p_max_near.max(p_max_far)); - } - - Aabb::from_min_max(cluster_min, cluster_max) -} +// fn screen_to_view(screen_size: Vec2, inverse_projection: Mat4, screen: Vec2, ndc_z: f32) -> Vec4 { +// let tex_coord = screen / screen_size; +// let clip = Vec4::new( +// tex_coord.x * 2.0 - 1.0, +// (1.0 - tex_coord.y) * 2.0 - 1.0, +// ndc_z, +// 1.0, +// ); +// clip_to_view(inverse_projection, clip) +// } + +// // Calculate the intersection of a ray from the eye through the view space position to a z plane +// fn line_intersection_to_z_plane(origin: Vec3, p: Vec3, z: f32) -> Vec3 { +// let v = p - origin; +// let t = (z - Vec3::Z.dot(origin)) / Vec3::Z.dot(v); +// origin + t * v +// } + +// #[allow(clippy::too_many_arguments)] +// fn compute_aabb_for_cluster( +// z_near: f32, +// z_far: f32, +// tile_size: Vec2, +// screen_size: Vec2, +// inverse_projection: Mat4, +// is_orthographic: bool, +// cluster_dimensions: UVec3, +// ijk: UVec3, +// ) -> Aabb { +// let ijk = ijk.as_vec3(); + +// // Calculate the minimum and maximum points in screen space +// let p_min = ijk.xy() * tile_size; +// let p_max = p_min + tile_size; + +// let cluster_min; +// let cluster_max; +// if is_orthographic { +// // Use linear depth slicing for orthographic + +// // Convert to view space at the cluster near and far planes +// // NOTE: 1.0 is the near plane due to using reverse z projections +// let p_min = screen_to_view( +// screen_size, +// inverse_projection, +// p_min, +// 1.0 - (ijk.z / cluster_dimensions.z as f32), +// ) +// .xyz(); +// let p_max = screen_to_view( +// screen_size, +// inverse_projection, +// p_max, +// 1.0 - ((ijk.z + 1.0) / cluster_dimensions.z as f32), +// ) +// .xyz(); + +// cluster_min = p_min.min(p_max); +// cluster_max = p_min.max(p_max); +// } else { +// // Convert to view space at the near plane +// // NOTE: 1.0 is the near plane due to using reverse z projections +// let p_min = screen_to_view(screen_size, inverse_projection, p_min, 1.0); +// let p_max = screen_to_view(screen_size, inverse_projection, p_max, 1.0); + +// let z_far_over_z_near = -z_far / -z_near; +// let cluster_near = if ijk.z == 0.0 { +// 0.0 +// } else { +// -z_near * z_far_over_z_near.powf((ijk.z - 1.0) / (cluster_dimensions.z - 1) as f32) +// }; +// // NOTE: This could be simplified to: +// // cluster_far = cluster_near * z_far_over_z_near; +// let cluster_far = if cluster_dimensions.z == 1 { +// -z_far +// } else { +// -z_near * z_far_over_z_near.powf(ijk.z / (cluster_dimensions.z - 1) as f32) +// }; + +// // Calculate the four intersection points of the min and max points with the cluster near and far planes +// let p_min_near = line_intersection_to_z_plane(Vec3::ZERO, p_min.xyz(), cluster_near); +// let p_min_far = line_intersection_to_z_plane(Vec3::ZERO, p_min.xyz(), cluster_far); +// let p_max_near = line_intersection_to_z_plane(Vec3::ZERO, p_max.xyz(), cluster_near); +// let p_max_far = line_intersection_to_z_plane(Vec3::ZERO, p_max.xyz(), cluster_far); + +// cluster_min = p_min_near.min(p_min_far).min(p_max_near.min(p_max_far)); +// cluster_max = p_min_near.max(p_min_far).max(p_max_near.max(p_max_far)); +// } + +// Aabb::from_min_max(cluster_min, cluster_max) +// } pub fn add_clusters( mut commands: Commands, @@ -795,7 +795,7 @@ pub(crate) fn assign_lights_to_clusters( let clusters = clusters.into_inner(); let screen_size = camera.target.get_physical_size(&windows, &images); - clusters.aabbs.clear(); + // clusters.aabbs.clear(); clusters.lights.clear(); let screen_size = screen_size.unwrap_or_default(); @@ -920,41 +920,137 @@ pub(crate) fn assign_lights_to_clusters( let inverse_projection = camera.projection_matrix.inverse(); let screen_size = screen_size.as_vec2(); - let tile_size_u32 = clusters.tile_size; - let tile_size = tile_size_u32.as_vec2(); - // Calculate view space AABBs - // NOTE: It is important that these are iterated in a specific order - // so that we can calculate the cluster index in the fragment shader! - // I (Rob Swain) choose to scan along rows of tiles in x,y, and for each tile then scan - // along z - for y in 0..clusters.dimensions.y { - for x in 0..clusters.dimensions.x { - for z in 0..clusters.dimensions.z { - clusters.aabbs.push(compute_aabb_for_cluster( - clusters.near, - clusters.far, - tile_size, - screen_size, - inverse_projection, - is_orthographic, - clusters.dimensions, - UVec3::new(x, y, z), - )); - } - } - } + // let tile_size_u32 = clusters.tile_size; + // let tile_size = tile_size_u32.as_vec2(); + // // Calculate view space AABBs + // // NOTE: It is important that these are iterated in a specific order + // // so that we can calculate the cluster index in the fragment shader! + // // I (Rob Swain) choose to scan along rows of tiles in x,y, and for each tile then scan + // // along z + // for y in 0..clusters.dimensions.y { + // for x in 0..clusters.dimensions.x { + // for z in 0..clusters.dimensions.z { + // clusters.aabbs.push(compute_aabb_for_cluster( + // clusters.near, + // clusters.far, + // tile_size, + // screen_size, + // inverse_projection, + // is_orthographic, + // clusters.dimensions, + // UVec3::new(x, y, z), + // )); + // } + // } + // } for lights in clusters.lights.iter_mut() { lights.entities.clear(); } - clusters - .lights - .resize_with(clusters.aabbs.len(), VisiblePointLights::default); + clusters.lights.resize_with( + (clusters.dimensions.x * clusters.dimensions.y * clusters.dimensions.z) as usize, + VisiblePointLights::default, + ); if screen_size.x == 0.0 || screen_size.y == 0.0 { continue; } + // Calculate the x/y/z cluster frustum planes in view space + let mut x_planes = Vec::with_capacity(clusters.dimensions.x as usize + 1); + let mut y_planes = Vec::with_capacity(clusters.dimensions.y as usize + 1); + let mut z_planes = Vec::with_capacity(clusters.dimensions.z as usize + 1); + + if is_orthographic { + let x_slices = clusters.dimensions.x as f32; + for x in 0..=clusters.dimensions.x { + let x_proportion = x as f32 / x_slices; + let x_pos = x_proportion * 2.0 - 1.0; + // let nb = clip_to_view(inverse_projection, Vec4::new(x_pos, -1.0, 1.0, 1.0)).xyz(); + // let nt = clip_to_view(inverse_projection, Vec4::new(x_pos, 1.0, 1.0, 1.0)).xyz(); + // let normal = nb.cross(nt).normalize(); + // let d = nb.dot(normal); + // x_planes.push(Plane::new(normal.extend(d))); + let view_x = clip_to_view(inverse_projection, Vec4::new(x_pos, 0.0, 1.0, 1.0)).x; + + let normal = Vec3::X; + let d = view_x * normal.x; + x_planes.push(Plane::new(normal.extend(d))); + } + + let y_slices = clusters.dimensions.y as f32; + for y in 0..=clusters.dimensions.y { + let y_proportion = 1.0 - y as f32 / y_slices; + let y_pos = y_proportion * 2.0 - 1.0; + // let nl = clip_to_view(inverse_projection, Vec4::new(-1.0, y_pos, 1.0, 1.0)).xyz(); + // let nr = clip_to_view(inverse_projection, Vec4::new(1.0, y_pos, 1.0, 1.0)).xyz(); + // let normal = nr.cross(nl).normalize(); + // let d = nr.dot(normal); + // y_planes.push(Plane::new(normal.extend(d))); + let view_y = clip_to_view(inverse_projection, Vec4::new(0.0, y_pos, 1.0, 1.0)).y; + + let normal = -Vec3::Y; + let d = view_y * normal.y; + y_planes.push(Plane::new(normal.extend(d))); + } + + for z in 0..=clusters.dimensions.z { + // Use linear depth slicing for orthographic + let view_z = clip_to_view( + inverse_projection, + Vec4::new( + 0.0, + 0.0, + 1.0 - (z as f32 / clusters.dimensions.z as f32), + 1.0, + ), + ) + .z; + + let normal = -Vec3::Z; + let d = view_z * normal.z; + z_planes.push(Plane::new(normal.extend(d))); + } + } else { + let x_slices = clusters.dimensions.x as f32; + for x in 0..=clusters.dimensions.x { + let x_proportion = x as f32 / x_slices; + let x_pos = x_proportion * 2.0 - 1.0; + let nb = clip_to_view(inverse_projection, Vec4::new(x_pos, -1.0, 1.0, 1.0)).xyz(); + let nt = clip_to_view(inverse_projection, Vec4::new(x_pos, 1.0, 1.0, 1.0)).xyz(); + let normal = nb.cross(nt).normalize(); + let d = nb.dot(normal); + x_planes.push(Plane::new(normal.extend(d))); + } + + let y_slices = clusters.dimensions.y as f32; + for y in 0..=clusters.dimensions.y { + let y_proportion = 1.0 - y as f32 / y_slices; + let y_pos = y_proportion * 2.0 - 1.0; + let nl = clip_to_view(inverse_projection, Vec4::new(-1.0, y_pos, 1.0, 1.0)).xyz(); + let nr = clip_to_view(inverse_projection, Vec4::new(1.0, y_pos, 1.0, 1.0)).xyz(); + let normal = nr.cross(nl).normalize(); + let d = nr.dot(normal); + y_planes.push(Plane::new(normal.extend(d))); + } + + let z_far_over_z_near = -far_z / -first_slice_depth; + for z in 0..=clusters.dimensions.z { + // Perspective + let view_z = if z == 0 { + 0.0 + } else { + -first_slice_depth + * z_far_over_z_near + .powf((z - 1) as f32 / (clusters.dimensions.z - 1) as f32) + }; + + let normal = -Vec3::Z; + let d = view_z * normal.z; + z_planes.push(Plane::new(normal.extend(d))); + } + } + let mut visible_lights_scratch = Vec::new(); { @@ -1005,17 +1101,92 @@ pub(crate) fn assign_lights_to_clusters( let (min_cluster, max_cluster) = (min_cluster.min(max_cluster), min_cluster.max(max_cluster)); - for y in min_cluster.y..=max_cluster.y { - let row_offset = y * clusters.dimensions.x; - for x in min_cluster.x..=max_cluster.x { - let col_offset = (row_offset + x) * clusters.dimensions.z; - for z in min_cluster.z..=max_cluster.z { - // NOTE: cluster_index = (y * dim.x + x) * dim.z + z - let cluster_index = (col_offset + z) as usize; - let cluster_aabb = &clusters.aabbs[cluster_index]; - if light_sphere.intersects_obb(cluster_aabb, &view_transform) { - clusters.lights[cluster_index].entities.push(light.entity); + // What follows is the Iterative Sphere Refinement algorithm from Just Cause 3 + // Persson et al, Practical Clustered Shading + // http://newq.net/dl/pub/s2015_practical.pdf + let view_light_sphere = Sphere { + center: Vec3A::from(inverse_view_transform * light_sphere.center.extend(1.0)), + radius: light_sphere.radius, + }; + // FIXME: Calculate the screen space and depth extents of the light in terms of clusters + let light_center_clip = + camera.projection_matrix * view_light_sphere.center.extend(1.0); + let light_center_ndc = light_center_clip.xyz() / light_center_clip.w; + let cluster_coordinates = ndc_position_to_cluster( + clusters.dimensions, + cluster_factors, + is_orthographic, + light_center_ndc, + view_light_sphere.center.z, + ); + let z_center = if light_center_ndc.z <= 1.0 { + Some(cluster_coordinates.z) + } else { + None + }; + let y_center = if light_center_ndc.y > 1.0 { + None + } else if light_center_ndc.y < -1.0 { + Some(clusters.dimensions.y + 1) + } else { + Some(cluster_coordinates.y) + }; + for z in min_cluster.z..=max_cluster.z { + let mut z_light = view_light_sphere.clone(); + if z_center.is_none() || z != z_center.unwrap() { + let z_plane = if z_center.is_some() && z < z_center.unwrap() { + z_planes[(z + 1) as usize] + } else { + z_planes[z as usize] + }; + if let Some(projected) = project_to_plane_z(z_light, z_plane) { + z_light = projected; + } else { + continue; + } + } + for y in min_cluster.y..=max_cluster.y { + let mut y_light = z_light.clone(); + if y_center.is_none() || y != y_center.unwrap() { + let y_plane = if y_center.is_some() && y < y_center.unwrap() { + y_planes[(y + 1) as usize] + } else { + y_planes[y as usize] + }; + if let Some(projected) = project_to_plane_y(y_light, y_plane) { + y_light = projected; + } else { + continue; + } + } + let mut min_x = min_cluster.x; + loop { + if min_x >= max_cluster.x + || -get_distance_x(x_planes[(min_x + 1) as usize], y_light.center) + + y_light.radius + > 0.0 + { + break; + } + min_x += 1; + } + let mut max_x = max_cluster.x; + loop { + if max_x <= min_x + || get_distance_x(x_planes[max_x as usize], y_light.center) + + y_light.radius + > 0.0 + { + break; } + max_x -= 1; + } + let mut cluster_index = ((y * clusters.dimensions.x + min_x) + * clusters.dimensions.z + + z) as usize; + for _ in min_x..=max_x { + clusters.lights[cluster_index].entities.push(light.entity); + cluster_index += clusters.dimensions.z as usize; } } } @@ -1030,6 +1201,49 @@ pub(crate) fn assign_lights_to_clusters( } } +// NOTE: This exploits the fact that a x-plane normal has only x and z components +fn get_distance_x(plane: Plane, point: Vec3A) -> f32 { + // Distance from a point to a plane: + // signed distance to plane = (nx * px + ny * py + nz * pz + d) / n.length() + // NOTE: For a x-plane, ny and d are 0 and we have a unit normal + // = nx * px + nz * pz + plane.normal_d().xz().dot(point.xz()) +} + +// NOTE: This exploits the fact that a z-plane normal has only a z component +fn project_to_plane_z(z_light: Sphere, z_plane: Plane) -> Option { + // p = sphere center + // n = plane normal + // d = n.p if p is in the plane + // NOTE: For a z-plane, nx and ny are both 0 + // d = px * nx + py * ny + pz * nz + // = pz * nz + // => pz = d / nz + let z = z_plane.d() / z_plane.normal_d().z; + let distance_to_plane = z - z_light.center.z; + if distance_to_plane.abs() > z_light.radius { + return None; + } + Some(Sphere { + center: Vec3A::from(z_light.center.xy().extend(z)), + // hypotenuse length = radius + // pythagorus = (distance to plane)^2 + b^2 = radius^2 + radius: (z_light.radius * z_light.radius - distance_to_plane * distance_to_plane).sqrt(), + }) +} + +// NOTE: This exploits the fact that a y-plane normal has only y and z components +fn project_to_plane_y(y_light: Sphere, y_plane: Plane) -> Option { + let distance_to_plane = -y_light.center.yz().dot(y_plane.normal_d().yz()); + if distance_to_plane.abs() > y_light.radius { + return None; + } + Some(Sphere { + center: y_light.center + distance_to_plane * y_plane.normal(), + radius: (y_light.radius * y_light.radius - distance_to_plane * distance_to_plane).sqrt(), + }) +} + pub fn update_directional_light_frusta( mut views: Query< ( diff --git a/crates/bevy_render/src/primitives/mod.rs b/crates/bevy_render/src/primitives/mod.rs index 76c44d2ff0d97..4f1b67555be64 100644 --- a/crates/bevy_render/src/primitives/mod.rs +++ b/crates/bevy_render/src/primitives/mod.rs @@ -58,7 +58,7 @@ impl From for Aabb { } } -#[derive(Debug, Default)] +#[derive(Clone, Debug, Default)] pub struct Sphere { pub center: Vec3A, pub radius: f32, diff --git a/examples/stress_tests/many_lights.rs b/examples/stress_tests/many_lights.rs index 6134b459de082..ccf665d52b56c 100644 --- a/examples/stress_tests/many_lights.rs +++ b/examples/stress_tests/many_lights.rs @@ -3,7 +3,7 @@ use bevy::{ math::{DVec2, DVec3}, pbr::{ExtractedPointLight, GlobalLightMeta}, prelude::*, - render::{RenderApp, RenderStage}, + render::{camera::CameraProjection, primitives::Frustum, RenderApp, RenderStage}, }; fn main() { @@ -70,7 +70,23 @@ fn setup( } // camera - commands.spawn_bundle(PerspectiveCameraBundle::default()); + match std::env::args().nth(1).as_deref() { + Some("orthographic") => { + let mut orthographic_camera_bundle = OrthographicCameraBundle::new_3d(); + orthographic_camera_bundle.orthographic_projection.scale = 20.0; + let view_projection = orthographic_camera_bundle + .orthographic_projection + .get_projection_matrix(); + orthographic_camera_bundle.frustum = Frustum::from_view_projection( + &view_projection, + &Vec3::ZERO, + &Vec3::Z, + orthographic_camera_bundle.orthographic_projection.far(), + ); + commands.spawn_bundle(orthographic_camera_bundle) + } + _ => commands.spawn_bundle(PerspectiveCameraBundle::default()), + }; // add one cube, the only one with strong handles // also serves as a reference point during rotation From bf29844744b45f5be23dc7b62234e310f3f63579 Mon Sep 17 00:00:00 2001 From: robtfm <50659922+robtfm@users.noreply.github.com> Date: Sun, 27 Mar 2022 05:12:39 +0200 Subject: [PATCH 02/10] bevy_pbr: Fix cluster light assignment for orthographic projections --- crates/bevy_pbr/src/light.rs | 66 +++++++++++++++++------------ crates/bevy_pbr/src/render/pbr.wgsl | 5 ++- 2 files changed, 44 insertions(+), 27 deletions(-) diff --git a/crates/bevy_pbr/src/light.rs b/crates/bevy_pbr/src/light.rs index 711a1961dda42..c22e7449e8a46 100644 --- a/crates/bevy_pbr/src/light.rs +++ b/crates/bevy_pbr/src/light.rs @@ -819,8 +819,9 @@ pub(crate) fn assign_lights_to_clusters( } ClusterFarZMode::Constant(far) => far, }; - let first_slice_depth = match requested_cluster_dimensions.z { - 1 => config.first_slice_depth().max(far_z), + let first_slice_depth = match (is_orthographic, requested_cluster_dimensions.z) { + (true, _) => camera.near, + (false, 1) => config.first_slice_depth().max(far_z), _ => config.first_slice_depth(), }; // NOTE: Ensure the far_z is at least as far as the first_depth_slice to avoid clustering problems. @@ -989,23 +990,19 @@ pub(crate) fn assign_lights_to_clusters( // y_planes.push(Plane::new(normal.extend(d))); let view_y = clip_to_view(inverse_projection, Vec4::new(0.0, y_pos, 1.0, 1.0)).y; - let normal = -Vec3::Y; + let normal = Vec3::Y; let d = view_y * normal.y; y_planes.push(Plane::new(normal.extend(d))); } for z in 0..=clusters.dimensions.z { // Use linear depth slicing for orthographic - let view_z = clip_to_view( - inverse_projection, - Vec4::new( - 0.0, - 0.0, - 1.0 - (z as f32 / clusters.dimensions.z as f32), - 1.0, - ), - ) - .z; + let view_z = if z == 0 { + -first_slice_depth + } else { + -first_slice_depth + - (far_z - first_slice_depth) * z as f32 / clusters.dimensions.z as f32 + }; let normal = -Vec3::Z; let d = view_z * normal.z; @@ -1153,7 +1150,9 @@ pub(crate) fn assign_lights_to_clusters( } else { y_planes[y as usize] }; - if let Some(projected) = project_to_plane_y(y_light, y_plane) { + if let Some(projected) = + project_to_plane_y(y_light, y_plane, is_orthographic) + { y_light = projected; } else { continue; @@ -1162,8 +1161,11 @@ pub(crate) fn assign_lights_to_clusters( let mut min_x = min_cluster.x; loop { if min_x >= max_cluster.x - || -get_distance_x(x_planes[(min_x + 1) as usize], y_light.center) - + y_light.radius + || -get_distance_x( + x_planes[(min_x + 1) as usize], + y_light.center, + is_orthographic, + ) + y_light.radius > 0.0 { break; @@ -1173,8 +1175,11 @@ pub(crate) fn assign_lights_to_clusters( let mut max_x = max_cluster.x; loop { if max_x <= min_x - || get_distance_x(x_planes[max_x as usize], y_light.center) - + y_light.radius + || get_distance_x( + x_planes[max_x as usize], + y_light.center, + is_orthographic, + ) + y_light.radius > 0.0 { break; @@ -1202,12 +1207,16 @@ pub(crate) fn assign_lights_to_clusters( } // NOTE: This exploits the fact that a x-plane normal has only x and z components -fn get_distance_x(plane: Plane, point: Vec3A) -> f32 { - // Distance from a point to a plane: - // signed distance to plane = (nx * px + ny * py + nz * pz + d) / n.length() - // NOTE: For a x-plane, ny and d are 0 and we have a unit normal - // = nx * px + nz * pz - plane.normal_d().xz().dot(point.xz()) +fn get_distance_x(plane: Plane, point: Vec3A, is_orthographic: bool) -> f32 { + if is_orthographic { + point.x - plane.d() + } else { + // Distance from a point to a plane: + // signed distance to plane = (nx * px + ny * py + nz * pz + d) / n.length() + // NOTE: For a x-plane, ny and d are 0 and we have a unit normal + // = nx * px + nz * pz + plane.normal_d().xz().dot(point.xz()) + } } // NOTE: This exploits the fact that a z-plane normal has only a z component @@ -1233,8 +1242,13 @@ fn project_to_plane_z(z_light: Sphere, z_plane: Plane) -> Option { } // NOTE: This exploits the fact that a y-plane normal has only y and z components -fn project_to_plane_y(y_light: Sphere, y_plane: Plane) -> Option { - let distance_to_plane = -y_light.center.yz().dot(y_plane.normal_d().yz()); +fn project_to_plane_y(y_light: Sphere, y_plane: Plane, is_orthographic: bool) -> Option { + let distance_to_plane = if is_orthographic { + y_plane.d() - y_light.center.y + } else { + -y_light.center.yz().dot(y_plane.normal_d().yz()) + }; + if distance_to_plane.abs() > y_light.radius { return None; } diff --git a/crates/bevy_pbr/src/render/pbr.wgsl b/crates/bevy_pbr/src/render/pbr.wgsl index 8cb483173c9b0..def42f186964b 100644 --- a/crates/bevy_pbr/src/render/pbr.wgsl +++ b/crates/bevy_pbr/src/render/pbr.wgsl @@ -243,7 +243,10 @@ fn reinhard_extended_luminance(color: vec3, max_white_l: f32) -> vec3 fn view_z_to_z_slice(view_z: f32, is_orthographic: bool) -> u32 { if (is_orthographic) { // NOTE: view_z is correct in the orthographic case - return u32(floor((view_z - lights.cluster_factors.z) * lights.cluster_factors.w)); + return min( + u32(floor((view_z - lights.cluster_factors.z) * lights.cluster_factors.w)), + lights.cluster_dimensions.z - 1u + ); } else { // NOTE: had to use -view_z to make it positive else log(negative) is nan return min( From 59dec5f465a0f000916efb503603a4d4f1752bf4 Mon Sep 17 00:00:00 2001 From: Robert Swain Date: Sun, 27 Mar 2022 06:14:14 +0200 Subject: [PATCH 03/10] bevy_pbr: Remove unused cluster AABB calculation code --- crates/bevy_pbr/src/light.rs | 131 +---------------------------------- 1 file changed, 1 insertion(+), 130 deletions(-) diff --git a/crates/bevy_pbr/src/light.rs b/crates/bevy_pbr/src/light.rs index c22e7449e8a46..f82af92d2703d 100644 --- a/crates/bevy_pbr/src/light.rs +++ b/crates/bevy_pbr/src/light.rs @@ -397,98 +397,6 @@ fn clip_to_view(inverse_projection: Mat4, clip: Vec4) -> Vec4 { view / view.w } -// fn screen_to_view(screen_size: Vec2, inverse_projection: Mat4, screen: Vec2, ndc_z: f32) -> Vec4 { -// let tex_coord = screen / screen_size; -// let clip = Vec4::new( -// tex_coord.x * 2.0 - 1.0, -// (1.0 - tex_coord.y) * 2.0 - 1.0, -// ndc_z, -// 1.0, -// ); -// clip_to_view(inverse_projection, clip) -// } - -// // Calculate the intersection of a ray from the eye through the view space position to a z plane -// fn line_intersection_to_z_plane(origin: Vec3, p: Vec3, z: f32) -> Vec3 { -// let v = p - origin; -// let t = (z - Vec3::Z.dot(origin)) / Vec3::Z.dot(v); -// origin + t * v -// } - -// #[allow(clippy::too_many_arguments)] -// fn compute_aabb_for_cluster( -// z_near: f32, -// z_far: f32, -// tile_size: Vec2, -// screen_size: Vec2, -// inverse_projection: Mat4, -// is_orthographic: bool, -// cluster_dimensions: UVec3, -// ijk: UVec3, -// ) -> Aabb { -// let ijk = ijk.as_vec3(); - -// // Calculate the minimum and maximum points in screen space -// let p_min = ijk.xy() * tile_size; -// let p_max = p_min + tile_size; - -// let cluster_min; -// let cluster_max; -// if is_orthographic { -// // Use linear depth slicing for orthographic - -// // Convert to view space at the cluster near and far planes -// // NOTE: 1.0 is the near plane due to using reverse z projections -// let p_min = screen_to_view( -// screen_size, -// inverse_projection, -// p_min, -// 1.0 - (ijk.z / cluster_dimensions.z as f32), -// ) -// .xyz(); -// let p_max = screen_to_view( -// screen_size, -// inverse_projection, -// p_max, -// 1.0 - ((ijk.z + 1.0) / cluster_dimensions.z as f32), -// ) -// .xyz(); - -// cluster_min = p_min.min(p_max); -// cluster_max = p_min.max(p_max); -// } else { -// // Convert to view space at the near plane -// // NOTE: 1.0 is the near plane due to using reverse z projections -// let p_min = screen_to_view(screen_size, inverse_projection, p_min, 1.0); -// let p_max = screen_to_view(screen_size, inverse_projection, p_max, 1.0); - -// let z_far_over_z_near = -z_far / -z_near; -// let cluster_near = if ijk.z == 0.0 { -// 0.0 -// } else { -// -z_near * z_far_over_z_near.powf((ijk.z - 1.0) / (cluster_dimensions.z - 1) as f32) -// }; -// // NOTE: This could be simplified to: -// // cluster_far = cluster_near * z_far_over_z_near; -// let cluster_far = if cluster_dimensions.z == 1 { -// -z_far -// } else { -// -z_near * z_far_over_z_near.powf(ijk.z / (cluster_dimensions.z - 1) as f32) -// }; - -// // Calculate the four intersection points of the min and max points with the cluster near and far planes -// let p_min_near = line_intersection_to_z_plane(Vec3::ZERO, p_min.xyz(), cluster_near); -// let p_min_far = line_intersection_to_z_plane(Vec3::ZERO, p_min.xyz(), cluster_far); -// let p_max_near = line_intersection_to_z_plane(Vec3::ZERO, p_max.xyz(), cluster_near); -// let p_max_far = line_intersection_to_z_plane(Vec3::ZERO, p_max.xyz(), cluster_far); - -// cluster_min = p_min_near.min(p_min_far).min(p_max_near.min(p_max_far)); -// cluster_max = p_min_near.max(p_min_far).max(p_max_near.max(p_max_far)); -// } - -// Aabb::from_min_max(cluster_min, cluster_max) -// } - pub fn add_clusters( mut commands: Commands, cameras: Query<(Entity, Option<&ClusterConfig>), (With, Without)>, @@ -920,31 +828,6 @@ pub(crate) fn assign_lights_to_clusters( let inverse_projection = camera.projection_matrix.inverse(); - let screen_size = screen_size.as_vec2(); - // let tile_size_u32 = clusters.tile_size; - // let tile_size = tile_size_u32.as_vec2(); - // // Calculate view space AABBs - // // NOTE: It is important that these are iterated in a specific order - // // so that we can calculate the cluster index in the fragment shader! - // // I (Rob Swain) choose to scan along rows of tiles in x,y, and for each tile then scan - // // along z - // for y in 0..clusters.dimensions.y { - // for x in 0..clusters.dimensions.x { - // for z in 0..clusters.dimensions.z { - // clusters.aabbs.push(compute_aabb_for_cluster( - // clusters.near, - // clusters.far, - // tile_size, - // screen_size, - // inverse_projection, - // is_orthographic, - // clusters.dimensions, - // UVec3::new(x, y, z), - // )); - // } - // } - // } - for lights in clusters.lights.iter_mut() { lights.entities.clear(); } @@ -953,7 +836,7 @@ pub(crate) fn assign_lights_to_clusters( VisiblePointLights::default, ); - if screen_size.x == 0.0 || screen_size.y == 0.0 { + if screen_size.x == 0 || screen_size.y == 0 { continue; } @@ -967,13 +850,7 @@ pub(crate) fn assign_lights_to_clusters( for x in 0..=clusters.dimensions.x { let x_proportion = x as f32 / x_slices; let x_pos = x_proportion * 2.0 - 1.0; - // let nb = clip_to_view(inverse_projection, Vec4::new(x_pos, -1.0, 1.0, 1.0)).xyz(); - // let nt = clip_to_view(inverse_projection, Vec4::new(x_pos, 1.0, 1.0, 1.0)).xyz(); - // let normal = nb.cross(nt).normalize(); - // let d = nb.dot(normal); - // x_planes.push(Plane::new(normal.extend(d))); let view_x = clip_to_view(inverse_projection, Vec4::new(x_pos, 0.0, 1.0, 1.0)).x; - let normal = Vec3::X; let d = view_x * normal.x; x_planes.push(Plane::new(normal.extend(d))); @@ -983,13 +860,7 @@ pub(crate) fn assign_lights_to_clusters( for y in 0..=clusters.dimensions.y { let y_proportion = 1.0 - y as f32 / y_slices; let y_pos = y_proportion * 2.0 - 1.0; - // let nl = clip_to_view(inverse_projection, Vec4::new(-1.0, y_pos, 1.0, 1.0)).xyz(); - // let nr = clip_to_view(inverse_projection, Vec4::new(1.0, y_pos, 1.0, 1.0)).xyz(); - // let normal = nr.cross(nl).normalize(); - // let d = nr.dot(normal); - // y_planes.push(Plane::new(normal.extend(d))); let view_y = clip_to_view(inverse_projection, Vec4::new(0.0, y_pos, 1.0, 1.0)).y; - let normal = Vec3::Y; let d = view_y * normal.y; y_planes.push(Plane::new(normal.extend(d))); From da41507785b71491feca96acb439da89dc473d8f Mon Sep 17 00:00:00 2001 From: Robert Swain Date: Sun, 27 Mar 2022 06:14:38 +0200 Subject: [PATCH 04/10] bevy_pbr: Remove unnecessary normalization The normal is normalized and d scale in the Plane constructor --- crates/bevy_pbr/src/light.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/bevy_pbr/src/light.rs b/crates/bevy_pbr/src/light.rs index f82af92d2703d..329f6bd420ecb 100644 --- a/crates/bevy_pbr/src/light.rs +++ b/crates/bevy_pbr/src/light.rs @@ -886,7 +886,7 @@ pub(crate) fn assign_lights_to_clusters( let x_pos = x_proportion * 2.0 - 1.0; let nb = clip_to_view(inverse_projection, Vec4::new(x_pos, -1.0, 1.0, 1.0)).xyz(); let nt = clip_to_view(inverse_projection, Vec4::new(x_pos, 1.0, 1.0, 1.0)).xyz(); - let normal = nb.cross(nt).normalize(); + let normal = nb.cross(nt); let d = nb.dot(normal); x_planes.push(Plane::new(normal.extend(d))); } @@ -897,7 +897,7 @@ pub(crate) fn assign_lights_to_clusters( let y_pos = y_proportion * 2.0 - 1.0; let nl = clip_to_view(inverse_projection, Vec4::new(-1.0, y_pos, 1.0, 1.0)).xyz(); let nr = clip_to_view(inverse_projection, Vec4::new(1.0, y_pos, 1.0, 1.0)).xyz(); - let normal = nr.cross(nl).normalize(); + let normal = nr.cross(nl); let d = nr.dot(normal); y_planes.push(Plane::new(normal.extend(d))); } From 20414919de2377a4f383ed394cfaf1c505a53085 Mon Sep 17 00:00:00 2001 From: Robert Swain Date: Sun, 27 Mar 2022 06:16:01 +0200 Subject: [PATCH 05/10] bevy_pbr: Simplifications and cleanup of the clustering code --- crates/bevy_pbr/src/light.rs | 73 +++++++++++++++-------------- crates/bevy_pbr/src/render/pbr.wgsl | 15 +++--- 2 files changed, 44 insertions(+), 44 deletions(-) diff --git a/crates/bevy_pbr/src/light.rs b/crates/bevy_pbr/src/light.rs index 329f6bd420ecb..49c14d4a995bc 100644 --- a/crates/bevy_pbr/src/light.rs +++ b/crates/bevy_pbr/src/light.rs @@ -432,19 +432,42 @@ impl VisiblePointLights { } } +// NOTE: Keep in sync with bevy_pbr/src/render/pbr.wgsl fn view_z_to_z_slice( cluster_factors: Vec2, - z_slices: f32, + z_slices: u32, view_z: f32, is_orthographic: bool, ) -> u32 { - if is_orthographic { + let z_slice = if is_orthographic { // NOTE: view_z is correct in the orthographic case ((view_z - cluster_factors.x) * cluster_factors.y).floor() as u32 } else { // NOTE: had to use -view_z to make it positive else log(negative) is nan - ((-view_z).ln() * cluster_factors.x - cluster_factors.y + 1.0).clamp(0.0, z_slices - 1.0) - as u32 + ((-view_z).ln() * cluster_factors.x - cluster_factors.y + 1.0) as u32 + }; + // NOTE: We use min as we may limit the far z plane used for clustering to be closeer than + // the furthest thing being drawn. This means that we need to limit to the maximum cluster. + z_slice.min(z_slices - 1) +} + +// NOTE: Keep in sync as the inverse of view_z_to_z_slice above +fn z_slice_to_view_z( + near: f32, + far: f32, + z_slices: u32, + z_slice: u32, + is_orthographic: bool, +) -> f32 { + if is_orthographic { + return -near - (far - near) * z_slice as f32 / z_slices as f32; + } + + // Perspective + if z_slice == 0 { + 0.0 + } else { + -near * (far / near).powf((z_slice - 1) as f32 / (z_slices - 1) as f32) } } @@ -461,7 +484,7 @@ fn ndc_position_to_cluster( let xy = (frag_coord * cluster_dimensions_f32.xy()).floor(); let z_slice = view_z_to_z_slice( cluster_factors, - cluster_dimensions.z as f32, + cluster_dimensions.z, view_z, is_orthographic, ); @@ -766,13 +789,13 @@ pub(crate) fn assign_lights_to_clusters( // since we won't adjust z slices we can calculate exact number of slices required in z dimension let z_cluster_min = view_z_to_z_slice( cluster_factors, - requested_cluster_dimensions.z as f32, + requested_cluster_dimensions.z, light_aabb_min.z, is_orthographic, ); let z_cluster_max = view_z_to_z_slice( cluster_factors, - requested_cluster_dimensions.z as f32, + requested_cluster_dimensions.z, light_aabb_max.z, is_orthographic, ); @@ -865,20 +888,6 @@ pub(crate) fn assign_lights_to_clusters( let d = view_y * normal.y; y_planes.push(Plane::new(normal.extend(d))); } - - for z in 0..=clusters.dimensions.z { - // Use linear depth slicing for orthographic - let view_z = if z == 0 { - -first_slice_depth - } else { - -first_slice_depth - - (far_z - first_slice_depth) * z as f32 / clusters.dimensions.z as f32 - }; - - let normal = -Vec3::Z; - let d = view_z * normal.z; - z_planes.push(Plane::new(normal.extend(d))); - } } else { let x_slices = clusters.dimensions.x as f32; for x in 0..=clusters.dimensions.x { @@ -901,22 +910,14 @@ pub(crate) fn assign_lights_to_clusters( let d = nr.dot(normal); y_planes.push(Plane::new(normal.extend(d))); } + } - let z_far_over_z_near = -far_z / -first_slice_depth; - for z in 0..=clusters.dimensions.z { - // Perspective - let view_z = if z == 0 { - 0.0 - } else { - -first_slice_depth - * z_far_over_z_near - .powf((z - 1) as f32 / (clusters.dimensions.z - 1) as f32) - }; - - let normal = -Vec3::Z; - let d = view_z * normal.z; - z_planes.push(Plane::new(normal.extend(d))); - } + let z_slices = clusters.dimensions.z; + for z in 0..=z_slices { + let view_z = z_slice_to_view_z(first_slice_depth, far_z, z_slices, z, is_orthographic); + let normal = -Vec3::Z; + let d = view_z * normal.z; + z_planes.push(Plane::new(normal.extend(d))); } let mut visible_lights_scratch = Vec::new(); diff --git a/crates/bevy_pbr/src/render/pbr.wgsl b/crates/bevy_pbr/src/render/pbr.wgsl index def42f186964b..8e365cdd3885b 100644 --- a/crates/bevy_pbr/src/render/pbr.wgsl +++ b/crates/bevy_pbr/src/render/pbr.wgsl @@ -240,20 +240,19 @@ fn reinhard_extended_luminance(color: vec3, max_white_l: f32) -> vec3 return change_luminance(color, l_new); } +// NOTE: Keep in sync with bevy_pbr/src/light.rs fn view_z_to_z_slice(view_z: f32, is_orthographic: bool) -> u32 { + var z_slice: u32 = 0u; if (is_orthographic) { // NOTE: view_z is correct in the orthographic case - return min( - u32(floor((view_z - lights.cluster_factors.z) * lights.cluster_factors.w)), - lights.cluster_dimensions.z - 1u - ); + z_slice = u32(floor((view_z - lights.cluster_factors.z) * lights.cluster_factors.w)); } else { // NOTE: had to use -view_z to make it positive else log(negative) is nan - return min( - u32(log(-view_z) * lights.cluster_factors.z - lights.cluster_factors.w + 1.0), - lights.cluster_dimensions.z - 1u - ); + z_slice = u32(log(-view_z) * lights.cluster_factors.z - lights.cluster_factors.w + 1.0); } + // NOTE: We use min as we may limit the far z plane used for clustering to be closeer than + // the furthest thing being drawn. This means that we need to limit to the maximum cluster. + return min(z_slice, lights.cluster_dimensions.z - 1u); } fn fragment_cluster_index(frag_coord: vec2, view_z: f32, is_orthographic: bool) -> u32 { From 0ab5f2517cf64ff4839c0b5229d647f2a56cb814 Mon Sep 17 00:00:00 2001 From: Robert Swain Date: Sun, 27 Mar 2022 07:46:20 +0200 Subject: [PATCH 06/10] bevy_pbr: Cleanup --- crates/bevy_pbr/src/light.rs | 3 --- 1 file changed, 3 deletions(-) diff --git a/crates/bevy_pbr/src/light.rs b/crates/bevy_pbr/src/light.rs index 49c14d4a995bc..bf21938854dfc 100644 --- a/crates/bevy_pbr/src/light.rs +++ b/crates/bevy_pbr/src/light.rs @@ -366,7 +366,6 @@ pub struct Clusters { /// and explicitly-configured to avoid having unnecessarily many slices close to the camera. pub(crate) near: f32, pub(crate) far: f32, - // aabbs: Vec, pub(crate) lights: Vec, } @@ -726,7 +725,6 @@ pub(crate) fn assign_lights_to_clusters( let clusters = clusters.into_inner(); let screen_size = camera.target.get_physical_size(&windows, &images); - // clusters.aabbs.clear(); clusters.lights.clear(); let screen_size = screen_size.unwrap_or_default(); @@ -977,7 +975,6 @@ pub(crate) fn assign_lights_to_clusters( center: Vec3A::from(inverse_view_transform * light_sphere.center.extend(1.0)), radius: light_sphere.radius, }; - // FIXME: Calculate the screen space and depth extents of the light in terms of clusters let light_center_clip = camera.projection_matrix * view_light_sphere.center.extend(1.0); let light_center_ndc = light_center_clip.xyz() / light_center_clip.w; From e6c50969e5dbcac46019910878213783e411d199 Mon Sep 17 00:00:00 2001 From: Robert Swain Date: Sun, 27 Mar 2022 07:46:38 +0200 Subject: [PATCH 07/10] bevy_pbr: Document how the iterative sphere refinement algorithm works In case the PDF is taken down. --- crates/bevy_pbr/src/light.rs | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/crates/bevy_pbr/src/light.rs b/crates/bevy_pbr/src/light.rs index bf21938854dfc..1d7f9cba0c9c1 100644 --- a/crates/bevy_pbr/src/light.rs +++ b/crates/bevy_pbr/src/light.rs @@ -971,6 +971,10 @@ pub(crate) fn assign_lights_to_clusters( // What follows is the Iterative Sphere Refinement algorithm from Just Cause 3 // Persson et al, Practical Clustered Shading // http://newq.net/dl/pub/s2015_practical.pdf + // NOTE: A sphere under perspective projection is no longer a sphere. It gets + // stretched and warped, which prevents simpler algorithms from being correct + // as they often assume that the widest part of the sphere under projection is the + // center point on the axis of interest plus the radius, and that is not true! let view_light_sphere = Sphere { center: Vec3A::from(inverse_view_transform * light_sphere.center.extend(1.0)), radius: light_sphere.radius, @@ -1000,11 +1004,15 @@ pub(crate) fn assign_lights_to_clusters( for z in min_cluster.z..=max_cluster.z { let mut z_light = view_light_sphere.clone(); if z_center.is_none() || z != z_center.unwrap() { + // The z plane closer to the light has the larger radius circle where the + // light sphere intersects the z plane. let z_plane = if z_center.is_some() && z < z_center.unwrap() { z_planes[(z + 1) as usize] } else { z_planes[z as usize] }; + // Project the sphere to this z plane and use its radius as the radius of a + // new, refined sphere. if let Some(projected) = project_to_plane_z(z_light, z_plane) { z_light = projected; } else { @@ -1014,11 +1022,15 @@ pub(crate) fn assign_lights_to_clusters( for y in min_cluster.y..=max_cluster.y { let mut y_light = z_light.clone(); if y_center.is_none() || y != y_center.unwrap() { + // The y plane closer to the light has the larger radius circle where the + // light sphere intersects the y plane. let y_plane = if y_center.is_some() && y < y_center.unwrap() { y_planes[(y + 1) as usize] } else { y_planes[y as usize] }; + // Project the refined sphere to this y plane and use its radius as the + // radius of a new, even more refined sphere. if let Some(projected) = project_to_plane_y(y_light, y_plane, is_orthographic) { @@ -1027,6 +1039,7 @@ pub(crate) fn assign_lights_to_clusters( continue; } } + // Loop from the left to find the first affected cluster let mut min_x = min_cluster.x; loop { if min_x >= max_cluster.x @@ -1041,6 +1054,7 @@ pub(crate) fn assign_lights_to_clusters( } min_x += 1; } + // Loop from the right to find the last affected cluster let mut max_x = max_cluster.x; loop { if max_x <= min_x @@ -1058,6 +1072,7 @@ pub(crate) fn assign_lights_to_clusters( let mut cluster_index = ((y * clusters.dimensions.x + min_x) * clusters.dimensions.z + z) as usize; + // Mark the clusters in the range as affected for _ in min_x..=max_x { clusters.lights[cluster_index].entities.push(light.entity); cluster_index += clusters.dimensions.z as usize; From b89c3b268938f61eb61de4dddf3cdb1a1b550b0e Mon Sep 17 00:00:00 2001 From: Robert Swain Date: Sun, 27 Mar 2022 08:16:53 +0200 Subject: [PATCH 08/10] many_lights: Tweak the cluster configuration for much better performance --- examples/stress_tests/many_lights.rs | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/examples/stress_tests/many_lights.rs b/examples/stress_tests/many_lights.rs index ccf665d52b56c..eee2ecb8cea3c 100644 --- a/examples/stress_tests/many_lights.rs +++ b/examples/stress_tests/many_lights.rs @@ -1,7 +1,7 @@ use bevy::{ diagnostic::{FrameTimeDiagnosticsPlugin, LogDiagnosticsPlugin}, math::{DVec2, DVec3}, - pbr::{ExtractedPointLight, GlobalLightMeta}, + pbr::{ClusterConfig, ClusterZConfig, ExtractedPointLight, GlobalLightMeta}, prelude::*, render::{camera::CameraProjection, primitives::Frustum, RenderApp, RenderStage}, }; @@ -70,7 +70,7 @@ fn setup( } // camera - match std::env::args().nth(1).as_deref() { + let mut camera = match std::env::args().nth(1).as_deref() { Some("orthographic") => { let mut orthographic_camera_bundle = OrthographicCameraBundle::new_3d(); orthographic_camera_bundle.orthographic_projection.scale = 20.0; @@ -88,6 +88,21 @@ fn setup( _ => commands.spawn_bundle(PerspectiveCameraBundle::default()), }; + // NOTE: The lights in this example are distributed primarily in x and y across the + // screen. Using only 1 z-slice and letting the x,y slicing be calculated + // automatically produces much better light distribution across the clusters (i.e. + // fewer maximum lights in any one cluster). The consequences of this versus the + // default on an M1 Max means going from ~65fps to ~130fps!!! + camera.insert(ClusterConfig::FixedZ { + total: 4096, + z_slices: 1, + z_config: ClusterZConfig { + first_slice_depth: 5.0, + far_z_mode: bevy::pbr::ClusterFarZMode::MaxLightRange, + }, + dynamic_resizing: true, + }); + // add one cube, the only one with strong handles // also serves as a reference point during rotation commands.spawn_bundle(PbrBundle { From e45da4eac64231b937e4bfc9dbafa352c3f2364b Mon Sep 17 00:00:00 2001 From: Robert Swain Date: Thu, 7 Apr 2022 21:01:30 +0200 Subject: [PATCH 09/10] examples: Randomize hue of light color in many_lights MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit For François. <3 --- examples/stress_tests/many_lights.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/examples/stress_tests/many_lights.rs b/examples/stress_tests/many_lights.rs index eee2ecb8cea3c..f8307f07ce182 100644 --- a/examples/stress_tests/many_lights.rs +++ b/examples/stress_tests/many_lights.rs @@ -5,6 +5,7 @@ use bevy::{ prelude::*, render::{camera::CameraProjection, primitives::Frustum, RenderApp, RenderStage}, }; +use rand::{thread_rng, Rng}; fn main() { App::new() @@ -55,6 +56,7 @@ fn setup( // the same number of visible meshes regardless of the viewing angle. // NOTE: f64 is used to avoid precision issues that produce visual artifacts in the distribution let golden_ratio = 0.5f64 * (1.0f64 + 5.0f64.sqrt()); + let mut rng = thread_rng(); for i in 0..N_LIGHTS { let spherical_polar_theta_phi = fibonacci_spiral_on_sphere(golden_ratio, i, N_LIGHTS); let unit_sphere_p = spherical_polar_to_cartesian(spherical_polar_theta_phi); @@ -62,6 +64,7 @@ fn setup( point_light: PointLight { range: LIGHT_RADIUS, intensity: LIGHT_INTENSITY, + color: Color::hsl(rng.gen_range(0.0..360.0), 1.0, 0.5), ..default() }, transform: Transform::from_translation((RADIUS as f64 * unit_sphere_p).as_vec3()), From e01591d11042d026475644e6cf3e9443b4e79210 Mon Sep 17 00:00:00 2001 From: Carter Anderson Date: Thu, 14 Apr 2022 19:41:58 -0700 Subject: [PATCH 10/10] remove custom clustering config --- examples/stress_tests/many_lights.rs | 19 ++----------------- 1 file changed, 2 insertions(+), 17 deletions(-) diff --git a/examples/stress_tests/many_lights.rs b/examples/stress_tests/many_lights.rs index f8307f07ce182..6fe6f91297e8a 100644 --- a/examples/stress_tests/many_lights.rs +++ b/examples/stress_tests/many_lights.rs @@ -1,7 +1,7 @@ use bevy::{ diagnostic::{FrameTimeDiagnosticsPlugin, LogDiagnosticsPlugin}, math::{DVec2, DVec3}, - pbr::{ClusterConfig, ClusterZConfig, ExtractedPointLight, GlobalLightMeta}, + pbr::{ExtractedPointLight, GlobalLightMeta}, prelude::*, render::{camera::CameraProjection, primitives::Frustum, RenderApp, RenderStage}, }; @@ -73,7 +73,7 @@ fn setup( } // camera - let mut camera = match std::env::args().nth(1).as_deref() { + match std::env::args().nth(1).as_deref() { Some("orthographic") => { let mut orthographic_camera_bundle = OrthographicCameraBundle::new_3d(); orthographic_camera_bundle.orthographic_projection.scale = 20.0; @@ -91,21 +91,6 @@ fn setup( _ => commands.spawn_bundle(PerspectiveCameraBundle::default()), }; - // NOTE: The lights in this example are distributed primarily in x and y across the - // screen. Using only 1 z-slice and letting the x,y slicing be calculated - // automatically produces much better light distribution across the clusters (i.e. - // fewer maximum lights in any one cluster). The consequences of this versus the - // default on an M1 Max means going from ~65fps to ~130fps!!! - camera.insert(ClusterConfig::FixedZ { - total: 4096, - z_slices: 1, - z_config: ClusterZConfig { - first_slice_depth: 5.0, - far_z_mode: bevy::pbr::ClusterFarZMode::MaxLightRange, - }, - dynamic_resizing: true, - }); - // add one cube, the only one with strong handles // also serves as a reference point during rotation commands.spawn_bundle(PbrBundle {