Skip to content

Commit d34ecd7

Browse files
committed
bevy_pbr: Use a special first depth slice for clustered forward (#3545)
# Objective - Using plain exponential depth slicing for perspective projection cameras results in unnecessarily many slices very close together close to the camera. If the camera is then moved close to a collection of point lights, they will likely exhaust the available uniform buffer space for the lists of which lights affect which clusters. ## Solution - A simple solution to this is to use a different near plane value for the depth slicing and set it to where the first slice's far plane should be. The default value is 5 and works well. This results in the configured number of depth slices, maintains the exponential slicing beyond the initial slice, and no slices are too small such that they cause problems that are sensitive to the view position.
1 parent f781bfe commit d34ecd7

File tree

4 files changed

+48
-13
lines changed

4 files changed

+48
-13
lines changed

crates/bevy_pbr/src/light.rs

Lines changed: 29 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -210,6 +210,9 @@ pub struct Clusters {
210210
pub(crate) tile_size: UVec2,
211211
/// Number of clusters in x / y / z in the view frustum
212212
pub(crate) axis_slices: UVec3,
213+
/// Distance to the far plane of the first depth slice. The first depth slice is special
214+
/// and explicitly-configured to avoid having unnecessarily many slices close to the camera.
215+
pub(crate) near: f32,
213216
aabbs: Vec<Aabb>,
214217
pub(crate) lights: Vec<VisiblePointLights>,
215218
}
@@ -219,6 +222,7 @@ impl Clusters {
219222
let mut clusters = Self {
220223
tile_size,
221224
axis_slices: Default::default(),
225+
near: 5.0,
222226
aabbs: Default::default(),
223227
lights: Default::default(),
224228
};
@@ -246,6 +250,8 @@ impl Clusters {
246250
(screen_size.y + 1) / tile_size.y,
247251
z_slices,
248252
);
253+
// NOTE: Maximum 4096 clusters due to uniform buffer size constraints
254+
assert!(self.axis_slices.x * self.axis_slices.y * self.axis_slices.z <= 4096);
249255
}
250256
}
251257

@@ -320,11 +326,15 @@ fn compute_aabb_for_cluster(
320326
let p_max = screen_to_view(screen_size, inverse_projection, p_max, 1.0);
321327

322328
let z_far_over_z_near = -z_far / -z_near;
323-
let cluster_near = -z_near * z_far_over_z_near.powf(ijk.z / cluster_dimensions.z as f32);
329+
let cluster_near = if ijk.z == 0.0 {
330+
0.0
331+
} else {
332+
-z_near * z_far_over_z_near.powf((ijk.z - 1.0) / (cluster_dimensions.z - 1) as f32)
333+
};
324334
// NOTE: This could be simplified to:
325335
// cluster_far = cluster_near * z_far_over_z_near;
326336
let cluster_far =
327-
-z_near * z_far_over_z_near.powf((ijk.z + 1.0) / cluster_dimensions.z as f32);
337+
-z_near * z_far_over_z_near.powf(ijk.z / (cluster_dimensions.z - 1) as f32);
328338

329339
// Calculate the four intersection points of the min and max points with the cluster near and far planes
330340
let p_min_near = line_intersection_to_z_plane(Vec3::ZERO, p_min.xyz(), cluster_near);
@@ -387,7 +397,7 @@ pub fn update_clusters(windows: Res<Windows>, mut views: Query<(&Camera, &mut Cl
387397
for x in 0..clusters.axis_slices.x {
388398
for z in 0..clusters.axis_slices.z {
389399
aabbs.push(compute_aabb_for_cluster(
390-
camera.near,
400+
clusters.near,
391401
camera.far,
392402
tile_size,
393403
screen_size,
@@ -428,13 +438,19 @@ impl VisiblePointLights {
428438
}
429439
}
430440

431-
fn view_z_to_z_slice(cluster_factors: Vec2, view_z: f32, is_orthographic: bool) -> u32 {
441+
fn view_z_to_z_slice(
442+
cluster_factors: Vec2,
443+
z_slices: f32,
444+
view_z: f32,
445+
is_orthographic: bool,
446+
) -> u32 {
432447
if is_orthographic {
433448
// NOTE: view_z is correct in the orthographic case
434449
((view_z - cluster_factors.x) * cluster_factors.y).floor() as u32
435450
} else {
436451
// NOTE: had to use -view_z to make it positive else log(negative) is nan
437-
((-view_z).ln() * cluster_factors.x - cluster_factors.y).floor() as u32
452+
((-view_z).ln() * cluster_factors.x - cluster_factors.y + 1.0).clamp(0.0, z_slices - 1.0)
453+
as u32
438454
}
439455
}
440456

@@ -449,7 +465,12 @@ fn ndc_position_to_cluster(
449465
let frag_coord =
450466
(ndc_p.xy() * Vec2::new(0.5, -0.5) + Vec2::splat(0.5)).clamp(Vec2::ZERO, Vec2::ONE);
451467
let xy = (frag_coord * cluster_dimensions_f32.xy()).floor();
452-
let z_slice = view_z_to_z_slice(cluster_factors, view_z, is_orthographic);
468+
let z_slice = view_z_to_z_slice(
469+
cluster_factors,
470+
cluster_dimensions.z as f32,
471+
view_z,
472+
is_orthographic,
473+
);
453474
xy.as_uvec2()
454475
.extend(z_slice)
455476
.clamp(UVec3::ZERO, cluster_dimensions - UVec3::ONE)
@@ -474,7 +495,8 @@ pub fn assign_lights_to_clusters(
474495
let cluster_count = clusters.aabbs.len();
475496
let is_orthographic = camera.projection_matrix.w_axis.w == 1.0;
476497
let cluster_factors = calculate_cluster_factors(
477-
camera.near,
498+
// NOTE: Using the special cluster near value
499+
clusters.near,
478500
camera.far,
479501
clusters.axis_slices.z as f32,
480502
is_orthographic,

crates/bevy_pbr/src/render/light.rs

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,7 @@ pub struct GpuLights {
130130
// TODO: this comes first to work around a WGSL alignment issue. We need to solve this issue before releasing the renderer rework
131131
directional_lights: [GpuDirectionalLight; MAX_DIRECTIONAL_LIGHTS],
132132
ambient_color: Vec4,
133+
// xyz are x/y/z cluster dimensions and w is the number of clusters
133134
cluster_dimensions: UVec4,
134135
// xy are vec2<f32>(cluster_dimensions.xy) / vec2<f32>(view.width, view.height)
135136
// z is cluster_dimensions.z / log(far / near)
@@ -350,6 +351,8 @@ impl SpecializedPipeline for ShadowPipeline {
350351

351352
#[derive(Component)]
352353
pub struct ExtractedClusterConfig {
354+
/// Special near value for cluster calculations
355+
near: f32,
353356
/// Number of clusters in x / y / z in the view frustum
354357
axis_slices: UVec3,
355358
}
@@ -366,6 +369,7 @@ pub fn extract_clusters(mut commands: Commands, views: Query<(Entity, &Clusters)
366369
data: clusters.lights.clone(),
367370
},
368371
ExtractedClusterConfig {
372+
near: clusters.near,
369373
axis_slices: clusters.axis_slices,
370374
},
371375
));
@@ -578,7 +582,7 @@ pub fn calculate_cluster_factors(
578582
if is_orthographic {
579583
Vec2::new(-near, z_slices / (-far - -near))
580584
} else {
581-
let z_slices_of_ln_zfar_over_znear = z_slices / (far / near).ln();
585+
let z_slices_of_ln_zfar_over_znear = (z_slices - 1.0) / (far / near).ln();
582586
Vec2::new(
583587
z_slices_of_ln_zfar_over_znear,
584588
near.ln() * z_slices_of_ln_zfar_over_znear,
@@ -710,12 +714,13 @@ pub fn prepare_lights(
710714

711715
let is_orthographic = extracted_view.projection.w_axis.w == 1.0;
712716
let cluster_factors_zw = calculate_cluster_factors(
713-
extracted_view.near,
717+
clusters.near,
714718
extracted_view.far,
715719
clusters.axis_slices.z as f32,
716720
is_orthographic,
717721
);
718722

723+
let n_clusters = clusters.axis_slices.x * clusters.axis_slices.y * clusters.axis_slices.z;
719724
let mut gpu_lights = GpuLights {
720725
directional_lights: [GpuDirectionalLight::default(); MAX_DIRECTIONAL_LIGHTS],
721726
ambient_color: Vec4::from_slice(&ambient_light.color.as_linear_rgba_f32())
@@ -726,7 +731,7 @@ pub fn prepare_lights(
726731
cluster_factors_zw.x,
727732
cluster_factors_zw.y,
728733
),
729-
cluster_dimensions: clusters.axis_slices.extend(0),
734+
cluster_dimensions: clusters.axis_slices.extend(n_clusters),
730735
n_directional_lights: directional_lights.iter().len() as u32,
731736
};
732737

crates/bevy_pbr/src/render/mesh_view_bind_group.wgsl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ struct Lights {
3838
// NOTE: this array size must be kept in sync with the constants defined bevy_pbr2/src/render/light.rs
3939
directional_lights: array<DirectionalLight, 1u>;
4040
ambient_color: vec4<f32>;
41-
// x/y/z dimensions
41+
// x/y/z dimensions and n_clusters in w
4242
cluster_dimensions: vec4<u32>;
4343
// xy are vec2<f32>(cluster_dimensions.xy) / vec2<f32>(view.width, view.height)
4444
//

crates/bevy_pbr/src/render/pbr.wgsl

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -244,14 +244,22 @@ fn view_z_to_z_slice(view_z: f32, is_orthographic: bool) -> u32 {
244244
return u32(floor((view_z - lights.cluster_factors.z) * lights.cluster_factors.w));
245245
} else {
246246
// NOTE: had to use -view_z to make it positive else log(negative) is nan
247-
return u32(floor(log(-view_z) * lights.cluster_factors.z - lights.cluster_factors.w));
247+
return min(
248+
u32(log(-view_z) * lights.cluster_factors.z - lights.cluster_factors.w + 1.0),
249+
lights.cluster_dimensions.z - 1u
250+
);
248251
}
249252
}
250253

251254
fn fragment_cluster_index(frag_coord: vec2<f32>, view_z: f32, is_orthographic: bool) -> u32 {
252255
let xy = vec2<u32>(floor(frag_coord * lights.cluster_factors.xy));
253256
let z_slice = view_z_to_z_slice(view_z, is_orthographic);
254-
return (xy.y * lights.cluster_dimensions.x + xy.x) * lights.cluster_dimensions.z + z_slice;
257+
// NOTE: Restricting cluster index to avoid undefined behavior when accessing uniform buffer
258+
// arrays based on the cluster index.
259+
return min(
260+
(xy.y * lights.cluster_dimensions.x + xy.x) * lights.cluster_dimensions.z + z_slice,
261+
lights.cluster_dimensions.w - 1u
262+
);
255263
}
256264

257265
struct ClusterOffsetAndCount {

0 commit comments

Comments
 (0)