Skip to content

Commit c061ec3

Browse files
committed
bevy_pbr2: Fix clustering for orthographic projections (#3316)
# Objective PBR lighting was broken in the new renderer when using orthographic projections due to the way the depth slicing works for the clusters. Fix it. ## Solution - The default orthographic projection near plane is 0.0. The perspective projection depth slicing does a division by the near plane which gives a floating point NaN and the clustering all breaks down. - Orthographic projections have a linear depth mapping, so it made intuitive sense to me to do depth slicing with a linear mapping too. The alternative I saw was to try to handle the near plane being at 0.0 and using the exponential depth slicing, but that felt like a hack that didn't make sense. - As such, I have added code that detects whether the projection is orthographic based on `projection[3][3] == 1.0` and then implemented the orthographic mapping case throughout (when computing cluster AABBs, and when mapping a view space position (or light) to a cluster id in both the rust and shader code). ## Screenshots Before: ![before](https://user-images.githubusercontent.com/302146/145847278-5b1bca74-fbad-4cc5-8b49-384f6a377fdc.png) After: <img width="1392" alt="Screenshot 2021-12-13 at 16 36 53" src="https://user-images.githubusercontent.com/302146/145847314-6f3a2035-5d87-4896-8032-0c3e35e15b7d.png"> Old renderer (slightly lighter due to slight difference in configured intensity): <img width="1392" alt="Screenshot 2021-12-13 at 16 42 23" src="https://user-images.githubusercontent.com/302146/145847391-6a5e6fe0-22da-4fc1-a6c7-440543689a63.png">
1 parent c825fda commit c061ec3

File tree

5 files changed

+168
-78
lines changed

5 files changed

+168
-78
lines changed

crates/bevy_pbr/src/light.rs

Lines changed: 70 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,8 @@ use bevy_transform::components::GlobalTransform;
1212
use bevy_window::Windows;
1313

1414
use crate::{
15-
CubeMapFace, CubemapVisibleEntities, ViewClusterBindings, CUBE_MAP_FACES, POINT_LIGHT_NEAR_Z,
15+
calculate_cluster_factors, CubeMapFace, CubemapVisibleEntities, ViewClusterBindings,
16+
CUBE_MAP_FACES, POINT_LIGHT_NEAR_Z,
1617
};
1718

1819
/// A light that emits light in all directions from a central point.
@@ -265,12 +266,14 @@ fn line_intersection_to_z_plane(origin: Vec3, p: Vec3, z: f32) -> Vec3 {
265266
origin + t * v
266267
}
267268

269+
#[allow(clippy::too_many_arguments)]
268270
fn compute_aabb_for_cluster(
269271
z_near: f32,
270272
z_far: f32,
271273
tile_size: Vec2,
272274
screen_size: Vec2,
273275
inverse_projection: Mat4,
276+
is_orthographic: bool,
274277
cluster_dimensions: UVec3,
275278
ijk: UVec3,
276279
) -> Aabb {
@@ -280,25 +283,52 @@ fn compute_aabb_for_cluster(
280283
let p_min = ijk.xy() * tile_size;
281284
let p_max = p_min + tile_size;
282285

283-
// Convert to view space at the near plane
284-
// NOTE: 1.0 is the near plane due to using reverse z projections
285-
let p_min = screen_to_view(screen_size, inverse_projection, p_min, 1.0);
286-
let p_max = screen_to_view(screen_size, inverse_projection, p_max, 1.0);
287-
288-
let z_far_over_z_near = -z_far / -z_near;
289-
let cluster_near = -z_near * z_far_over_z_near.powf(ijk.z / cluster_dimensions.z as f32);
290-
// NOTE: This could be simplified to:
291-
// let cluster_far = cluster_near * z_far_over_z_near;
292-
let cluster_far = -z_near * z_far_over_z_near.powf((ijk.z + 1.0) / cluster_dimensions.z as f32);
293-
294-
// Calculate the four intersection points of the min and max points with the cluster near and far planes
295-
let p_min_near = line_intersection_to_z_plane(Vec3::ZERO, p_min.xyz(), cluster_near);
296-
let p_min_far = line_intersection_to_z_plane(Vec3::ZERO, p_min.xyz(), cluster_far);
297-
let p_max_near = line_intersection_to_z_plane(Vec3::ZERO, p_max.xyz(), cluster_near);
298-
let p_max_far = line_intersection_to_z_plane(Vec3::ZERO, p_max.xyz(), cluster_far);
299-
300-
let cluster_min = p_min_near.min(p_min_far).min(p_max_near.min(p_max_far));
301-
let cluster_max = p_min_near.max(p_min_far).max(p_max_near.max(p_max_far));
286+
let cluster_min;
287+
let cluster_max;
288+
if is_orthographic {
289+
// Use linear depth slicing for orthographic
290+
291+
// Convert to view space at the cluster near and far planes
292+
// NOTE: 1.0 is the near plane due to using reverse z projections
293+
let p_min = screen_to_view(
294+
screen_size,
295+
inverse_projection,
296+
p_min,
297+
1.0 - (ijk.z / cluster_dimensions.z as f32),
298+
)
299+
.xyz();
300+
let p_max = screen_to_view(
301+
screen_size,
302+
inverse_projection,
303+
p_max,
304+
1.0 - ((ijk.z + 1.0) / cluster_dimensions.z as f32),
305+
)
306+
.xyz();
307+
308+
cluster_min = p_min.min(p_max);
309+
cluster_max = p_min.max(p_max);
310+
} else {
311+
// Convert to view space at the near plane
312+
// NOTE: 1.0 is the near plane due to using reverse z projections
313+
let p_min = screen_to_view(screen_size, inverse_projection, p_min, 1.0);
314+
let p_max = screen_to_view(screen_size, inverse_projection, p_max, 1.0);
315+
316+
let z_far_over_z_near = -z_far / -z_near;
317+
let cluster_near = -z_near * z_far_over_z_near.powf(ijk.z / cluster_dimensions.z as f32);
318+
// NOTE: This could be simplified to:
319+
// cluster_far = cluster_near * z_far_over_z_near;
320+
let cluster_far =
321+
-z_near * z_far_over_z_near.powf((ijk.z + 1.0) / cluster_dimensions.z as f32);
322+
323+
// Calculate the four intersection points of the min and max points with the cluster near and far planes
324+
let p_min_near = line_intersection_to_z_plane(Vec3::ZERO, p_min.xyz(), cluster_near);
325+
let p_min_far = line_intersection_to_z_plane(Vec3::ZERO, p_min.xyz(), cluster_far);
326+
let p_max_near = line_intersection_to_z_plane(Vec3::ZERO, p_max.xyz(), cluster_near);
327+
let p_max_far = line_intersection_to_z_plane(Vec3::ZERO, p_max.xyz(), cluster_far);
328+
329+
cluster_min = p_min_near.min(p_min_far).min(p_max_near.min(p_max_far));
330+
cluster_max = p_min_near.max(p_min_far).max(p_max_near.max(p_max_far));
331+
}
302332

303333
Aabb::from_min_max(cluster_min, cluster_max)
304334
}
@@ -322,6 +352,7 @@ pub fn add_clusters(
322352

323353
pub fn update_clusters(windows: Res<Windows>, mut views: Query<(&Camera, &mut Clusters)>) {
324354
for (camera, mut clusters) in views.iter_mut() {
355+
let is_orthographic = camera.projection_matrix.w_axis.w == 1.0;
325356
let inverse_projection = camera.projection_matrix.inverse();
326357
let window = windows.get(camera.window).unwrap();
327358
let screen_size_u32 = UVec2::new(window.physical_width(), window.physical_height());
@@ -348,6 +379,7 @@ pub fn update_clusters(windows: Res<Windows>, mut views: Query<(&Camera, &mut Cl
348379
tile_size,
349380
screen_size,
350381
inverse_projection,
382+
is_orthographic,
351383
clusters.axis_slices,
352384
UVec3::new(x, y, z),
353385
));
@@ -383,22 +415,28 @@ impl VisiblePointLights {
383415
}
384416
}
385417

386-
fn view_z_to_z_slice(cluster_factors: Vec2, view_z: f32) -> u32 {
387-
// NOTE: had to use -view_z to make it positive else log(negative) is nan
388-
((-view_z).ln() * cluster_factors.x - cluster_factors.y).floor() as u32
418+
fn view_z_to_z_slice(cluster_factors: Vec2, view_z: f32, is_orthographic: bool) -> u32 {
419+
if is_orthographic {
420+
// NOTE: view_z is correct in the orthographic case
421+
((view_z - cluster_factors.x) * cluster_factors.y).floor() as u32
422+
} else {
423+
// NOTE: had to use -view_z to make it positive else log(negative) is nan
424+
((-view_z).ln() * cluster_factors.x - cluster_factors.y).floor() as u32
425+
}
389426
}
390427

391428
fn ndc_position_to_cluster(
392429
cluster_dimensions: UVec3,
393430
cluster_factors: Vec2,
431+
is_orthographic: bool,
394432
ndc_p: Vec3,
395433
view_z: f32,
396434
) -> UVec3 {
397435
let cluster_dimensions_f32 = cluster_dimensions.as_vec3();
398436
let frag_coord =
399437
(ndc_p.xy() * Vec2::new(0.5, -0.5) + Vec2::splat(0.5)).clamp(Vec2::ZERO, Vec2::ONE);
400438
let xy = (frag_coord * cluster_dimensions_f32.xy()).floor();
401-
let z_slice = view_z_to_z_slice(cluster_factors, view_z);
439+
let z_slice = view_z_to_z_slice(cluster_factors, view_z, is_orthographic);
402440
xy.as_uvec2()
403441
.extend(z_slice)
404442
.clamp(UVec3::ZERO, cluster_dimensions - UVec3::ONE)
@@ -421,11 +459,12 @@ pub fn assign_lights_to_clusters(
421459
let view_transform = view_transform.compute_matrix();
422460
let inverse_view_transform = view_transform.inverse();
423461
let cluster_count = clusters.aabbs.len();
424-
let z_slices_of_ln_zfar_over_znear =
425-
clusters.axis_slices.z as f32 / (camera.far / camera.near).ln();
426-
let cluster_factors = Vec2::new(
427-
z_slices_of_ln_zfar_over_znear,
428-
camera.near.ln() * z_slices_of_ln_zfar_over_znear,
462+
let is_orthographic = camera.projection_matrix.w_axis.w == 1.0;
463+
let cluster_factors = calculate_cluster_factors(
464+
camera.near,
465+
camera.far,
466+
clusters.axis_slices.z as f32,
467+
is_orthographic,
429468
);
430469

431470
let mut clusters_lights =
@@ -501,12 +540,14 @@ pub fn assign_lights_to_clusters(
501540
let min_cluster = ndc_position_to_cluster(
502541
clusters.axis_slices,
503542
cluster_factors,
543+
is_orthographic,
504544
light_aabb_ndc_min,
505545
light_aabb_view_min.z,
506546
);
507547
let max_cluster = ndc_position_to_cluster(
508548
clusters.axis_slices,
509549
cluster_factors,
550+
is_orthographic,
510551
light_aabb_ndc_max,
511552
light_aabb_view_max.z,
512553
);

crates/bevy_pbr/src/render/light.rs

Lines changed: 33 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ use bevy_ecs::{
1010
prelude::*,
1111
system::{lifetimeless::*, SystemParamItem},
1212
};
13-
use bevy_math::{const_vec3, Mat4, UVec3, UVec4, Vec3, Vec4, Vec4Swizzles};
13+
use bevy_math::{const_vec3, Mat4, UVec3, UVec4, Vec2, Vec3, Vec4, Vec4Swizzles};
1414
use bevy_render::{
1515
camera::{Camera, CameraProjection},
1616
color::Color,
@@ -540,6 +540,22 @@ pub enum LightEntity {
540540
face_index: usize,
541541
},
542542
}
543+
pub fn calculate_cluster_factors(
544+
near: f32,
545+
far: f32,
546+
z_slices: f32,
547+
is_orthographic: bool,
548+
) -> Vec2 {
549+
if is_orthographic {
550+
Vec2::new(-near, z_slices / (-far - -near))
551+
} else {
552+
let z_slices_of_ln_zfar_over_znear = z_slices / (far / near).ln();
553+
Vec2::new(
554+
z_slices_of_ln_zfar_over_znear,
555+
near.ln() * z_slices_of_ln_zfar_over_znear,
556+
)
557+
}
558+
}
543559

544560
#[allow(clippy::too_many_arguments)]
545561
pub fn prepare_lights(
@@ -644,17 +660,23 @@ pub fn prepare_lights(
644660
);
645661
let mut view_lights = Vec::new();
646662

647-
let z_times_ln_far_over_near =
648-
clusters.axis_slices.z as f32 / (extracted_view.far / extracted_view.near).ln();
663+
let is_orthographic = extracted_view.projection.w_axis.w == 1.0;
664+
let cluster_factors_zw = calculate_cluster_factors(
665+
extracted_view.near,
666+
extracted_view.far,
667+
clusters.axis_slices.z as f32,
668+
is_orthographic,
669+
);
670+
649671
let mut gpu_lights = GpuLights {
650672
directional_lights: [GpuDirectionalLight::default(); MAX_DIRECTIONAL_LIGHTS],
651673
ambient_color: Vec4::from_slice(&ambient_light.color.as_linear_rgba_f32())
652674
* ambient_light.brightness,
653675
cluster_factors: Vec4::new(
654676
clusters.axis_slices.x as f32 / extracted_view.width as f32,
655677
clusters.axis_slices.y as f32 / extracted_view.height as f32,
656-
z_times_ln_far_over_near,
657-
extracted_view.near.ln() * z_times_ln_far_over_near,
678+
cluster_factors_zw.x,
679+
cluster_factors_zw.y,
658680
),
659681
cluster_dimensions: clusters.axis_slices.extend(0),
660682
n_directional_lights: directional_lights.iter().len() as u32,
@@ -855,15 +877,16 @@ const CLUSTER_COUNT_MASK: u32 = (1 << 8) - 1;
855877
const POINT_LIGHT_INDEX_MASK: u32 = (1 << 8) - 1;
856878

857879
// NOTE: With uniform buffer max binding size as 16384 bytes
858-
// that means we can fit say 128 point lights in one uniform
859-
// buffer, which means the count can be at most 128 so it
860-
// needs 7 bits, use 8 for convenience.
880+
// that means we can fit say 256 point lights in one uniform
881+
// buffer, which means the count can be at most 256 so it
882+
// needs 8 bits.
861883
// The array of indices can also use u8 and that means the
862884
// offset in to the array of indices needs to be able to address
863-
// 16384 values. lod2(16384) = 21 bits.
885+
// 16384 values. log2(16384) = 14 bits.
864886
// This means we can pack the offset into the upper 24 bits of a u32
865887
// and the count into the lower 8 bits.
866-
// FIXME: Probably there are endianness concerns here????!!!!!
888+
// NOTE: This assumes CPU and GPU endianness are the same which is true
889+
// for all common and tested x86/ARM CPUs and AMD/NVIDIA/Intel/Apple/etc GPUs
867890
fn pack_offset_and_count(offset: usize, count: usize) -> u32 {
868891
((offset as u32 & CLUSTER_OFFSET_MASK) << CLUSTER_COUNT_SIZE)
869892
| (count as u32 & CLUSTER_COUNT_MASK)

crates/bevy_pbr/src/render/mesh.rs

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -245,7 +245,7 @@ impl FromWorld for MeshPipeline {
245245
ty: BufferBindingType::Uniform,
246246
has_dynamic_offset: false,
247247
// NOTE: Static size for uniform buffers. GpuPointLight has a padded
248-
// size of 128 bytes, so 16384 / 128 = 128 point lights max
248+
// size of 64 bytes, so 16384 / 64 = 256 point lights max
249249
min_binding_size: BufferSize::new(16384),
250250
},
251251
count: None,
@@ -257,8 +257,7 @@ impl FromWorld for MeshPipeline {
257257
ty: BindingType::Buffer {
258258
ty: BufferBindingType::Uniform,
259259
has_dynamic_offset: false,
260-
// NOTE: With 128 point lights max, indices need 7 bits. Use u8 for
261-
// convenience.
260+
// NOTE: With 256 point lights max, indices need 8 bits so use u8
262261
min_binding_size: BufferSize::new(16384),
263262
},
264263
count: None,
@@ -270,10 +269,10 @@ impl FromWorld for MeshPipeline {
270269
ty: BindingType::Buffer {
271270
ty: BufferBindingType::Uniform,
272271
has_dynamic_offset: false,
273-
// NOTE: The offset needs to address 16384 indices, which needs 21 bits.
274-
// The count can be at most all 128 lights so 7 bits.
272+
// NOTE: The offset needs to address 16384 indices, which needs 14 bits.
273+
// The count can be at most all 256 lights so 8 bits.
275274
// Pack the offset into the upper 24 bits and the count into the
276-
// lower 8 bits for convenience.
275+
// lower 8 bits.
277276
min_binding_size: BufferSize::new(16384),
278277
},
279278
count: None,

crates/bevy_pbr/src/render/mesh_view_bind_group.wgsl

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,8 +43,15 @@ struct Lights {
4343
// x/y/z dimensions
4444
cluster_dimensions: vec4<u32>;
4545
// xy are vec2<f32>(cluster_dimensions.xy) / vec2<f32>(view.width, view.height)
46+
//
47+
// For perspective projections:
4648
// z is cluster_dimensions.z / log(far / near)
4749
// w is cluster_dimensions.z * log(near) / log(far / near)
50+
//
51+
// For orthographic projections:
52+
// NOTE: near and far are +ve but -z is infront of the camera
53+
// z is -near
54+
// w is cluster_dimensions.z / (-far - -near)
4855
cluster_factors: vec4<f32>;
4956
n_directional_lights: u32;
5057
};

0 commit comments

Comments
 (0)