Skip to content

Commit 3f6068d

Browse files
committed
fix issues with too many point lights (#3916)
# Objective fix #3915 ## Solution the issues are caused by - lights are assigned to clusters before being filtered down to MAX_POINT_LIGHTS, leading to cluster counts potentially being too high - after fixing the above, packing the count into 8 bits still causes overflow with exactly 256 lights affecting a cluster to fix: ```assign_lights_to_clusters``` - limit extracted lights to MAX_POINT_LIGHTS, selecting based on shadow-caster & intensity (if required) - warn if MAX_POINT_LIGHT count is exceeded ```prepare_lights``` - limit the lights assigned to a cluster to CLUSTER_COUNT_MASK (which is 1 less than MAX_POINT_LIGHTS) to avoid overflowing into the offset bits notes: - a better solution to the overflow may be to use more than 8 bits for cluster_count (the comment states only 14 of the remaining 24 bits are used for the offset). this would touch more of the code base but i'm happy to try if it has some benefit. - intensity is only one way to select lights. it may be worth allowing user configuration of the light filtering, but i can't see a clean way to do that
1 parent b21c69c commit 3f6068d

File tree

3 files changed

+111
-26
lines changed

3 files changed

+111
-26
lines changed

crates/bevy_pbr/src/light.rs

Lines changed: 88 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,12 @@ use bevy_render::{
1212
view::{ComputedVisibility, RenderLayers, Visibility, VisibleEntities},
1313
};
1414
use bevy_transform::components::GlobalTransform;
15+
use bevy_utils::tracing::warn;
1516
use bevy_window::Windows;
1617

1718
use crate::{
1819
calculate_cluster_factors, CubeMapFace, CubemapVisibleEntities, ViewClusterBindings,
19-
CUBE_MAP_FACES, POINT_LIGHT_NEAR_Z,
20+
CUBE_MAP_FACES, MAX_POINT_LIGHTS, POINT_LIGHT_NEAR_Z,
2021
};
2122

2223
/// A light that emits light in all directions from a central point.
@@ -478,14 +479,91 @@ fn ndc_position_to_cluster(
478479
.clamp(UVec3::ZERO, cluster_dimensions - UVec3::ONE)
479480
}
480481

482+
// Sort point lights with shadows enabled first, then by a stable key so that the index
483+
// can be used to render at most `MAX_POINT_LIGHT_SHADOW_MAPS` point light shadows, and
484+
// we keep a stable set of lights visible
485+
pub(crate) fn point_light_order(
486+
(entity_1, shadows_enabled_1): (&Entity, &bool),
487+
(entity_2, shadows_enabled_2): (&Entity, &bool),
488+
) -> std::cmp::Ordering {
489+
shadows_enabled_1
490+
.cmp(shadows_enabled_2)
491+
.reverse()
492+
.then_with(|| entity_1.cmp(entity_2))
493+
}
494+
495+
#[derive(Clone, Copy)]
496+
// data required for assigning lights to clusters
497+
pub(crate) struct PointLightAssignmentData {
498+
entity: Entity,
499+
translation: Vec3,
500+
range: f32,
501+
shadows_enabled: bool,
502+
}
503+
481504
// NOTE: Run this before update_point_light_frusta!
482-
pub fn assign_lights_to_clusters(
505+
pub(crate) fn assign_lights_to_clusters(
483506
mut commands: Commands,
484507
mut global_lights: ResMut<VisiblePointLights>,
485508
mut views: Query<(Entity, &GlobalTransform, &Camera, &Frustum, &mut Clusters)>,
486-
lights: Query<(Entity, &GlobalTransform, &PointLight)>,
509+
lights_query: Query<(Entity, &GlobalTransform, &PointLight)>,
510+
mut lights: Local<Vec<PointLightAssignmentData>>,
511+
mut max_point_lights_warning_emitted: Local<bool>,
487512
) {
488-
let light_count = lights.iter().count();
513+
// collect just the relevant light query data into a persisted vec to avoid reallocating each frame
514+
lights.extend(
515+
lights_query
516+
.iter()
517+
.map(|(entity, transform, light)| PointLightAssignmentData {
518+
entity,
519+
translation: transform.translation,
520+
shadows_enabled: light.shadows_enabled,
521+
range: light.range,
522+
}),
523+
);
524+
525+
if lights.len() > MAX_POINT_LIGHTS {
526+
lights.sort_by(|light_1, light_2| {
527+
point_light_order(
528+
(&light_1.entity, &light_1.shadows_enabled),
529+
(&light_2.entity, &light_2.shadows_enabled),
530+
)
531+
});
532+
533+
// check each light against each view's frustum, keep only those that affect at least one of our views
534+
let frusta: Vec<_> = views.iter().map(|(_, _, _, frustum, _)| *frustum).collect();
535+
let mut lights_in_view_count = 0;
536+
lights.retain(|light| {
537+
// take one extra light to check if we should emit the warning
538+
if lights_in_view_count == MAX_POINT_LIGHTS + 1 {
539+
false
540+
} else {
541+
let light_sphere = Sphere {
542+
center: light.translation,
543+
radius: light.range,
544+
};
545+
546+
let light_in_view = frusta
547+
.iter()
548+
.any(|frustum| frustum.intersects_sphere(&light_sphere));
549+
550+
if light_in_view {
551+
lights_in_view_count += 1;
552+
}
553+
554+
light_in_view
555+
}
556+
});
557+
558+
if lights.len() > MAX_POINT_LIGHTS && !*max_point_lights_warning_emitted {
559+
warn!("MAX_POINT_LIGHTS ({}) exceeded", MAX_POINT_LIGHTS);
560+
*max_point_lights_warning_emitted = true;
561+
}
562+
563+
lights.truncate(MAX_POINT_LIGHTS);
564+
}
565+
566+
let light_count = lights.len();
489567
let mut global_lights_set = HashSet::with_capacity(light_count);
490568
for (view_entity, view_transform, camera, frustum, mut clusters) in views.iter_mut() {
491569
let view_transform = view_transform.compute_matrix();
@@ -504,9 +582,9 @@ pub fn assign_lights_to_clusters(
504582
vec![VisiblePointLights::from_light_count(light_count); cluster_count];
505583
let mut visible_lights = Vec::with_capacity(light_count);
506584

507-
for (light_entity, light_transform, light) in lights.iter() {
585+
for light in lights.iter() {
508586
let light_sphere = Sphere {
509-
center: light_transform.translation,
587+
center: light.translation,
510588
radius: light.range,
511589
};
512590

@@ -516,8 +594,8 @@ pub fn assign_lights_to_clusters(
516594
}
517595

518596
// NOTE: The light intersects the frustum so it must be visible and part of the global set
519-
global_lights_set.insert(light_entity);
520-
visible_lights.push(light_entity);
597+
global_lights_set.insert(light.entity);
598+
visible_lights.push(light.entity);
521599

522600
// Calculate an AABB for the light in view space, find the corresponding clusters for the min and max
523601
// points of the AABB, then iterate over just those clusters for this light
@@ -599,7 +677,7 @@ pub fn assign_lights_to_clusters(
599677
let cluster_index = (col_offset + z) as usize;
600678
let cluster_aabb = &clusters.aabbs[cluster_index];
601679
if light_sphere.intersects_obb(cluster_aabb, &view_transform) {
602-
clusters_lights[cluster_index].entities.push(light_entity);
680+
clusters_lights[cluster_index].entities.push(light.entity);
603681
}
604682
}
605683
}
@@ -617,6 +695,7 @@ pub fn assign_lights_to_clusters(
617695
});
618696
}
619697
global_lights.entities = global_lights_set.into_iter().collect();
698+
lights.clear();
620699
}
621700

622701
pub fn update_directional_light_frusta(

crates/bevy_pbr/src/render/light.rs

Lines changed: 19 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
use crate::{
2-
AmbientLight, Clusters, CubemapVisibleEntities, DirectionalLight, DirectionalLightShadowMap,
3-
DrawMesh, MeshPipeline, NotShadowCaster, PointLight, PointLightShadowMap, SetMeshBindGroup,
4-
VisiblePointLights, SHADOW_SHADER_HANDLE,
2+
point_light_order, AmbientLight, Clusters, CubemapVisibleEntities, DirectionalLight,
3+
DirectionalLightShadowMap, DrawMesh, MeshPipeline, NotShadowCaster, PointLight,
4+
PointLightShadowMap, SetMeshBindGroup, VisiblePointLights, SHADOW_SHADER_HANDLE,
55
};
66
use bevy_asset::Handle;
77
use bevy_core::FloatOrd;
@@ -574,11 +574,10 @@ pub fn prepare_lights(
574574
// Sort point lights with shadows enabled first, then by a stable key so that the index can be used
575575
// to render at most `MAX_POINT_LIGHT_SHADOW_MAPS` point light shadows.
576576
point_lights.sort_by(|(entity_1, light_1), (entity_2, light_2)| {
577-
light_1
578-
.shadows_enabled
579-
.cmp(&light_2.shadows_enabled)
580-
.reverse()
581-
.then_with(|| entity_1.cmp(entity_2))
577+
point_light_order(
578+
(entity_1, &light_1.shadows_enabled),
579+
(entity_2, &light_2.shadows_enabled),
580+
)
582581
});
583582

584583
if global_light_meta.entity_to_index.capacity() < point_lights.len() {
@@ -588,7 +587,7 @@ pub fn prepare_lights(
588587
}
589588

590589
let mut gpu_point_lights = [GpuPointLight::default(); MAX_POINT_LIGHTS];
591-
for (index, &(entity, light)) in point_lights.iter().enumerate().take(MAX_POINT_LIGHTS) {
590+
for (index, &(entity, light)) in point_lights.iter().enumerate() {
592591
let mut flags = PointLightFlags::NONE;
593592
// Lights are sorted, shadow enabled lights are first
594593
if light.shadows_enabled && index < MAX_POINT_LIGHT_SHADOW_MAPS {
@@ -877,20 +876,25 @@ pub fn prepare_lights(
877876
.write_buffer(&render_device, &render_queue);
878877
}
879878

880-
const CLUSTER_OFFSET_MASK: u32 = (1 << 24) - 1;
881-
const CLUSTER_COUNT_SIZE: u32 = 8;
882-
const CLUSTER_COUNT_MASK: u32 = (1 << 8) - 1;
879+
// this must match CLUSTER_COUNT_SIZE in pbr.wgsl
880+
// and must be large enough to contain MAX_POINT_LIGHTS
881+
const CLUSTER_COUNT_SIZE: u32 = 13;
882+
883+
const CLUSTER_OFFSET_MASK: u32 = (1 << (32 - CLUSTER_COUNT_SIZE)) - 1;
884+
const CLUSTER_COUNT_MASK: u32 = (1 << CLUSTER_COUNT_SIZE) - 1;
883885
const POINT_LIGHT_INDEX_MASK: u32 = (1 << 8) - 1;
884886

885887
// NOTE: With uniform buffer max binding size as 16384 bytes
886888
// that means we can fit say 256 point lights in one uniform
887889
// buffer, which means the count can be at most 256 so it
888-
// needs 8 bits.
890+
// needs 9 bits.
889891
// The array of indices can also use u8 and that means the
890892
// offset in to the array of indices needs to be able to address
891893
// 16384 values. log2(16384) = 14 bits.
892-
// This means we can pack the offset into the upper 24 bits of a u32
893-
// and the count into the lower 8 bits.
894+
// We use 32 bits to store the pair, so we choose to divide the
895+
// remaining 9 bits proportionally to give some future room.
896+
// This means we can pack the offset into the upper 19 bits of a u32
897+
// and the count into the lower 13 bits.
894898
// NOTE: This assumes CPU and GPU endianness are the same which is true
895899
// for all common and tested x86/ARM CPUs and AMD/NVIDIA/Intel/Apple/etc GPUs
896900
fn pack_offset_and_count(offset: usize, count: usize) -> u32 {

crates/bevy_pbr/src/render/pbr.wgsl

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -267,13 +267,15 @@ struct ClusterOffsetAndCount {
267267
count: u32;
268268
};
269269

270+
// this must match CLUSTER_COUNT_SIZE in light.rs
271+
let CLUSTER_COUNT_SIZE = 13u;
270272
fn unpack_offset_and_count(cluster_index: u32) -> ClusterOffsetAndCount {
271273
let offset_and_count = cluster_offsets_and_counts.data[cluster_index >> 2u][cluster_index & ((1u << 2u) - 1u)];
272274
var output: ClusterOffsetAndCount;
273275
// The offset is stored in the upper 24 bits
274-
output.offset = (offset_and_count >> 8u) & ((1u << 24u) - 1u);
276+
output.offset = (offset_and_count >> CLUSTER_COUNT_SIZE) & ((1u << 32u - CLUSTER_COUNT_SIZE) - 1u);
275277
// The count is stored in the lower 8 bits
276-
output.count = offset_and_count & ((1u << 8u) - 1u);
278+
output.count = offset_and_count & ((1u << CLUSTER_COUNT_SIZE) - 1u);
277279
return output;
278280
}
279281

0 commit comments

Comments
 (0)