From 7fae8a0ae3f237b059fdd2d785e5ad772a67019d Mon Sep 17 00:00:00 2001 From: Kevin Chen Date: Tue, 5 May 2026 00:33:38 -0400 Subject: [PATCH 1/4] fix: use aux entity world pos in gpu visibility range culling --- crates/bevy_core_pipeline/src/core_2d/mod.rs | 2 +- crates/bevy_core_pipeline/src/core_3d/mod.rs | 4 +- crates/bevy_pbr/src/render/light.rs | 92 ++++++++++--------- .../bevy_pbr/src/render/mesh_preprocess.wgsl | 2 +- crates/bevy_pbr/src/transmission/phase.rs | 2 +- crates/bevy_pbr/src/wireframe.rs | 2 +- crates/bevy_render/src/camera.rs | 7 +- crates/bevy_render/src/view/mod.rs | 21 +++++ crates/bevy_render/src/view/view.wgsl | 1 + .../src/mesh2d/wireframe2d.rs | 2 +- crates/bevy_ui_render/src/lib.rs | 2 +- .../shader_advanced/custom_render_phase.rs | 2 +- 12 files changed, 88 insertions(+), 51 deletions(-) diff --git a/crates/bevy_core_pipeline/src/core_2d/mod.rs b/crates/bevy_core_pipeline/src/core_2d/mod.rs index 1bc693fac863d..b0593bbbe1c77 100644 --- a/crates/bevy_core_pipeline/src/core_2d/mod.rs +++ b/crates/bevy_core_pipeline/src/core_2d/mod.rs @@ -405,7 +405,7 @@ pub fn extract_core_2d_camera_phases( } // This is the main 2D camera, so we use the first subview index (0). - let retained_view_entity = RetainedViewEntity::new(main_entity.into(), None, 0); + let retained_view_entity = RetainedViewEntity::new(main_entity.into(), None, None, 0); transparent_2d_phases.prepare_for_new_frame(retained_view_entity); opaque_2d_phases.prepare_for_new_frame(retained_view_entity, GpuPreprocessingMode::None); diff --git a/crates/bevy_core_pipeline/src/core_3d/mod.rs b/crates/bevy_core_pipeline/src/core_3d/mod.rs index c24ae999d69f2..aed4768436507 100644 --- a/crates/bevy_core_pipeline/src/core_3d/mod.rs +++ b/crates/bevy_core_pipeline/src/core_3d/mod.rs @@ -517,7 +517,7 @@ pub fn extract_core_3d_camera_phases( }); // This is the main 3D camera, so use the first subview index (0). - let retained_view_entity = RetainedViewEntity::new(main_entity.into(), None, 0); + let retained_view_entity = RetainedViewEntity::new(main_entity.into(), None, None, 0); opaque_3d_phases.prepare_for_new_frame(retained_view_entity, gpu_preprocessing_mode); alpha_mask_3d_phases.prepare_for_new_frame(retained_view_entity, gpu_preprocessing_mode); @@ -587,7 +587,7 @@ pub fn extract_camera_prepass_phase( }); // This is the main 3D camera, so we use the first subview index (0). - let retained_view_entity = RetainedViewEntity::new(main_entity.into(), None, 0); + let retained_view_entity = RetainedViewEntity::new(main_entity.into(), None, None, 0); if depth_prepass || normal_prepass || motion_vector_prepass { opaque_3d_prepass_phases diff --git a/crates/bevy_pbr/src/render/light.rs b/crates/bevy_pbr/src/render/light.rs index bfd67faac1fc8..abe5f9f883200 100644 --- a/crates/bevy_pbr/src/render/light.rs +++ b/crates/bevy_pbr/src/render/light.rs @@ -399,6 +399,7 @@ pub fn extract_lights( ), >, >, + aux_render_entity: Extract>, rect_lights: Extract< Query< ( @@ -485,6 +486,7 @@ pub fn extract_lights( let retained_view_entity = RetainedViewEntity { main_entity: MainEntity::from(main_entity), auxiliary_entity: MainEntity::from(Entity::PLACEHOLDER), + render_auxiliary_entity: Entity::PLACEHOLDER, subview_index: face_index, }; render_shadow_map_visible_entities @@ -596,6 +598,7 @@ pub fn extract_lights( let retained_view_entity = RetainedViewEntity { main_entity: MainEntity::from(main_entity), auxiliary_entity: MainEntity::from(Entity::PLACEHOLDER), + render_auxiliary_entity: Entity::PLACEHOLDER, subview_index: 0, }; render_shadow_map_visible_entities @@ -738,43 +741,47 @@ pub fn extract_lights( for (main_auxiliary_entity, visible_mesh_entities_list) in visible_entities.entities.iter() { - for subview_index in 0..(cascade_config.bounds.len() as u32) { - let retained_view_entity = RetainedViewEntity { - main_entity: MainEntity::from(main_entity), - auxiliary_entity: MainEntity::from(*main_auxiliary_entity), - subview_index, - }; - all_cascades_seen.insert(retained_view_entity); - - existing_shadow_map_visible_entities - .subviews - .entry(retained_view_entity) - .or_default(); - - // Extract the visible CPU culled entities to the list. - let extracted_entities = &mut existing_extracted_shadow_map_visible_entities - .subviews - .entry(retained_view_entity) - .or_default() - .classes - .entry(TypeId::of::()) - .or_default() - .entities; - extracted_entities.clear(); - let Some(visible_mesh_entities) = - visible_mesh_entities_list.get(subview_index as usize) - else { - continue; - }; - extracted_entities.extend(visible_mesh_entities.entities.iter().map( - |main_entity| { - let render_entity = match mapper.get(*main_entity) { - Ok(render_entity) => **render_entity, - Err(_) => Entity::PLACEHOLDER, - }; - (render_entity, MainEntity::from(*main_entity)) - }, - )); + if let Ok(render_auxiliary_entity) = aux_render_entity.get(*main_auxiliary_entity) { + for subview_index in 0..(cascade_config.bounds.len() as u32) { + let retained_view_entity = RetainedViewEntity { + main_entity: MainEntity::from(main_entity), + auxiliary_entity: MainEntity::from(*main_auxiliary_entity), + render_auxiliary_entity, + subview_index, + }; + all_cascades_seen.insert(retained_view_entity); + + existing_shadow_map_visible_entities + .subviews + .entry(retained_view_entity) + .or_default(); + + // Extract the visible CPU culled entities to the list. + let extracted_entities = + &mut existing_extracted_shadow_map_visible_entities + .subviews + .entry(retained_view_entity) + .or_default() + .classes + .entry(TypeId::of::()) + .or_default() + .entities; + extracted_entities.clear(); + let Some(visible_mesh_entities) = + visible_mesh_entities_list.get(subview_index as usize) + else { + continue; + }; + extracted_entities.extend(visible_mesh_entities.entities.iter().map( + |main_entity| { + let render_entity = match mapper.get(*main_entity) { + Ok(render_entity) => **render_entity, + Err(_) => Entity::PLACEHOLDER, + }; + (render_entity, MainEntity::from(*main_entity)) + }, + )); + } } } @@ -1526,7 +1533,7 @@ pub fn prepare_lights( // Point light shadow maps are shared across all cameras, // so the retained view entity must not include the camera. let retained_view_entity = - RetainedViewEntity::new(*light_main_entity, None, face_index as u32); + RetainedViewEntity::new(*light_main_entity, None, None, face_index as u32); commands.entity(view_light_entity).insert(( ShadowView { @@ -1584,7 +1591,7 @@ pub fn prepare_lights( { let view_light_entity = point_and_spot_light_view_entities.0[face_index]; let retained_view_entity = - RetainedViewEntity::new(*light_main_entity, None, face_index as u32); + RetainedViewEntity::new(*light_main_entity, None, None, face_index as u32); commands.entity(view_light_entity).insert(( ExtractedView { retained_view_entity, @@ -1610,7 +1617,7 @@ pub fn prepare_lights( // already created the views in order to clear out old data. for face_index in 0..6 { let retained_view_entity = - RetainedViewEntity::new(*light_main_entity, None, face_index); + RetainedViewEntity::new(*light_main_entity, None, None, face_index); shadow_render_phases.prepare_for_new_frame( retained_view_entity, gpu_preprocessing_support.max_supported_mode, @@ -1644,7 +1651,7 @@ pub fn prepare_lights( // Spot light shadow maps are shared across all cameras, // so the retained view entity must not include the camera. - let retained_view_entity = RetainedViewEntity::new(*light_main_entity, None, 0); + let retained_view_entity = RetainedViewEntity::new(*light_main_entity, None, None, 0); if point_and_spot_light_view_entities.0.is_empty() { let spot_world_from_view = spot_light_world_from_view(&light.transform); @@ -2021,6 +2028,7 @@ pub fn prepare_lights( let retained_view_entity = RetainedViewEntity::new( *light_main_entity, Some(camera_main_entity.into()), + Some(entity), cascade_index as u32, ); @@ -2854,6 +2862,7 @@ fn get_shadow_map_visible_entities<'w, 's: 'w>( let retained_view_entity = RetainedViewEntity { main_entity: extracted_view_light.retained_view_entity.main_entity, auxiliary_entity: MainEntity::from(Entity::PLACEHOLDER), + render_auxiliary_entity: Entity::PLACEHOLDER, subview_index: *face_index as u32, }; shadow_map_visible_entities_query @@ -2870,6 +2879,7 @@ fn get_shadow_map_visible_entities<'w, 's: 'w>( let retained_view_entity = RetainedViewEntity { main_entity: extracted_view_light.retained_view_entity.main_entity, auxiliary_entity: MainEntity::from(Entity::PLACEHOLDER), + render_auxiliary_entity: Entity::PLACEHOLDER, subview_index: 0, }; shadow_map_visible_entities_query diff --git a/crates/bevy_pbr/src/render/mesh_preprocess.wgsl b/crates/bevy_pbr/src/render/mesh_preprocess.wgsl index 496cd6b6f84ab..2d8601ccc8ce8 100644 --- a/crates/bevy_pbr/src/render/mesh_preprocess.wgsl +++ b/crates/bevy_pbr/src/render/mesh_preprocess.wgsl @@ -224,7 +224,7 @@ fn main(@builtin(global_invocation_id) global_invocation_id: vec3) { world_pos = world_from_local[3].xyz; } - let camera_distance = length(position_world_to_view(world_pos)); + let camera_distance = length(view.primary_world_position - world_pos); // `x` is the minimum range; `w` is the largest range. if (camera_distance < lod_range.x || camera_distance >= lod_range.w) { return; diff --git a/crates/bevy_pbr/src/transmission/phase.rs b/crates/bevy_pbr/src/transmission/phase.rs index 69bcb62c82be9..09efd535287ec 100644 --- a/crates/bevy_pbr/src/transmission/phase.rs +++ b/crates/bevy_pbr/src/transmission/phase.rs @@ -133,7 +133,7 @@ pub fn extract_transmissive_camera_phases( } // This is the main camera, so use the first subview index (0). - let retained_view_entity = RetainedViewEntity::new(main_entity.into(), None, 0); + let retained_view_entity = RetainedViewEntity::new(main_entity.into(), None, None, 0); transmissive_3d_phases.prepare_for_new_frame(retained_view_entity); live_entities.insert(retained_view_entity); diff --git a/crates/bevy_pbr/src/wireframe.rs b/crates/bevy_pbr/src/wireframe.rs index 3b8581abfbcf6..146f5c0dab7bf 100644 --- a/crates/bevy_pbr/src/wireframe.rs +++ b/crates/bevy_pbr/src/wireframe.rs @@ -1268,7 +1268,7 @@ fn extract_wireframe_3d_camera( GpuPreprocessingMode::PreprocessingOnly }); - let retained_view_entity = RetainedViewEntity::new(main_entity.into(), None, 0); + let retained_view_entity = RetainedViewEntity::new(main_entity.into(), None, None, 0); wireframe_3d_phases.prepare_for_new_frame(retained_view_entity, gpu_preprocessing_mode); live_entities.insert(retained_view_entity); } diff --git a/crates/bevy_render/src/camera.rs b/crates/bevy_render/src/camera.rs index 047187e2a9c2a..3e2666fa38a0e 100644 --- a/crates/bevy_render/src/camera.rs +++ b/crates/bevy_render/src/camera.rs @@ -640,7 +640,12 @@ pub fn extract_cameras( compositing_space: compositing_space.copied(), }, ExtractedView { - retained_view_entity: RetainedViewEntity::new(main_entity.into(), None, 0), + retained_view_entity: RetainedViewEntity::new( + main_entity.into(), + None, + None, + 0, + ), clip_from_view: camera.clip_from_view(), world_from_view: *transform, clip_from_world: None, diff --git a/crates/bevy_render/src/view/mod.rs b/crates/bevy_render/src/view/mod.rs index 6c55afaec7b80..9779a9e7eea6f 100644 --- a/crates/bevy_render/src/view/mod.rs +++ b/crates/bevy_render/src/view/mod.rs @@ -286,6 +286,7 @@ pub struct RetainedViewEntity { /// /// If not present, this will be `MainEntity(Entity::PLACEHOLDER)`. pub auxiliary_entity: MainEntity, + pub render_auxiliary_entity: Entity, /// The index of the view corresponding to the entity. /// @@ -304,11 +305,13 @@ impl RetainedViewEntity { pub fn new( main_entity: MainEntity, auxiliary_entity: Option, + render_auxiliary_entity: Option, subview_index: u32, ) -> Self { Self { main_entity, auxiliary_entity: auxiliary_entity.unwrap_or(Entity::PLACEHOLDER.into()), + render_auxiliary_entity: render_auxiliary_entity.unwrap_or(Entity::PLACEHOLDER), subview_index, } } @@ -654,6 +657,14 @@ pub struct ViewUniform { pub color_grading: ColorGradingUniform, pub mip_bias: f32, pub frame_count: u32, + /// The world position of a camera view used for visibility range culling. + /// + /// This is useful for directional shadow views, where visibility range culling should + /// be executed in relation to its non-shadow camera's world position. + /// + /// If this ViewUniform already represents a camera view, this field will be set to world_position. + /// If this ViewUniform has no associated camera view, this field will be set to a Vec3 of NaN's. + pub primary_world_position: Vec3, } #[derive(Resource)] @@ -997,6 +1008,7 @@ pub fn prepare_view_uniforms( Option<&MipBias>, Option<&MainPassResolutionOverride>, )>, + primary_view: Query<&ExtractedView, With>, frame_count: Res, ) { let view_iter = views.iter(); @@ -1049,6 +1061,14 @@ pub fn prepare_view_uniforms( .map(|frustum| frustum.half_spaces.map(|h| h.normal_d())) .unwrap_or([Vec4::ZERO; 6]); + let primary_world_position = if let Ok(primary_extracted_view) = + primary_view.get(extracted_view.retained_view_entity.render_auxiliary_entity) + { + primary_extracted_view.world_from_view.translation() + } else { + extracted_view.world_from_view.translation() + }; + let view_uniforms = ViewUniformOffset { offset: writer.write(&ViewUniform { clip_from_world, @@ -1068,6 +1088,7 @@ pub fn prepare_view_uniforms( color_grading: extracted_view.color_grading.clone().into(), mip_bias: mip_bias.unwrap_or(&MipBias(0.0)).0, frame_count: frame_count.0, + primary_world_position, }), }; diff --git a/crates/bevy_render/src/view/view.wgsl b/crates/bevy_render/src/view/view.wgsl index 23ded53f40f91..dadfaeeb00235 100644 --- a/crates/bevy_render/src/view/view.wgsl +++ b/crates/bevy_render/src/view/view.wgsl @@ -64,6 +64,7 @@ struct View { color_grading: ColorGrading, mip_bias: f32, frame_count: u32, + primary_world_position: vec3, }; /// World space: diff --git a/crates/bevy_sprite_render/src/mesh2d/wireframe2d.rs b/crates/bevy_sprite_render/src/mesh2d/wireframe2d.rs index ec844c40e4ae5..3d0fa0e246ff2 100644 --- a/crates/bevy_sprite_render/src/mesh2d/wireframe2d.rs +++ b/crates/bevy_sprite_render/src/mesh2d/wireframe2d.rs @@ -662,7 +662,7 @@ fn extract_wireframe_2d_camera( if !camera.is_active { continue; } - let retained_view_entity = RetainedViewEntity::new(main_entity.into(), None, 0); + let retained_view_entity = RetainedViewEntity::new(main_entity.into(), None, None, 0); wireframe_2d_phases.prepare_for_new_frame(retained_view_entity, GpuPreprocessingMode::None); live_entities.insert(retained_view_entity); } diff --git a/crates/bevy_ui_render/src/lib.rs b/crates/bevy_ui_render/src/lib.rs index 21fa1f301da98..340ada3898ed8 100644 --- a/crates/bevy_ui_render/src/lib.rs +++ b/crates/bevy_ui_render/src/lib.rs @@ -803,7 +803,7 @@ pub fn extract_ui_camera_view( // We use `UI_CAMERA_SUBVIEW` here so as not to conflict with the // main 3D or 2D camera, which will have subview index 0. let retained_view_entity = - RetainedViewEntity::new(main_entity.into(), None, UI_CAMERA_SUBVIEW); + RetainedViewEntity::new(main_entity.into(), None, None, UI_CAMERA_SUBVIEW); // Creates the UI view. let ui_camera_view = commands .spawn(( diff --git a/examples/shader_advanced/custom_render_phase.rs b/examples/shader_advanced/custom_render_phase.rs index 73c3e64a27bca..9f373f2ecfa5c 100644 --- a/examples/shader_advanced/custom_render_phase.rs +++ b/examples/shader_advanced/custom_render_phase.rs @@ -508,7 +508,7 @@ fn extract_camera_phases( continue; } // This is the main camera, so we use the first subview index (0) - let retained_view_entity = RetainedViewEntity::new(main_entity.into(), None, 0); + let retained_view_entity = RetainedViewEntity::new(main_entity.into(), None, None, 0); stencil_phases.prepare_for_new_frame(retained_view_entity); live_entities.insert(retained_view_entity); From 6f1e8fcbf5a8db4ee192634981e0264f76e78306 Mon Sep 17 00:00:00 2001 From: Kevin Chen Date: Tue, 5 May 2026 01:20:49 -0400 Subject: [PATCH 2/4] fix: ensure point/spot light shadows views are not used for visibility range culling --- .../bevy_pbr/src/render/mesh_preprocess.wgsl | 8 +++-- crates/bevy_render/src/view/mod.rs | 33 ++++++++++++++++--- crates/bevy_render/src/view/view.wgsl | 3 ++ 3 files changed, 38 insertions(+), 6 deletions(-) diff --git a/crates/bevy_pbr/src/render/mesh_preprocess.wgsl b/crates/bevy_pbr/src/render/mesh_preprocess.wgsl index 2d8601ccc8ce8..bece93f01e1c7 100644 --- a/crates/bevy_pbr/src/render/mesh_preprocess.wgsl +++ b/crates/bevy_pbr/src/render/mesh_preprocess.wgsl @@ -29,7 +29,9 @@ position_world_to_prev_ndc, position_world_to_prev_view, prev_view_z_to_depth_ndc } #import bevy_render::maths -#import bevy_render::view::View +#import bevy_render::view::{ + View, VIEW_FLAGS_HAS_USABLE_PRIMARY_WORLD_POSITION +} // Information about each mesh instance needed to cull it on GPU. // @@ -211,7 +213,9 @@ fn main(@builtin(global_invocation_id) global_invocation_id: vec3) { let visibility_buffer_array_len = arrayLength(&visibility_ranges); let visibility_buffer_index = current_input[input_index].flags & MESH_FLAGS_VISIBILITY_RANGE_INDEX_BITS; - if (visibility_buffer_index < visibility_buffer_array_len) { + if (visibility_buffer_index < visibility_buffer_array_len + && (view.flags & VIEW_FLAGS_HAS_USABLE_PRIMARY_WORLD_POSITION) != 0u) + { let lod_range = visibility_ranges[visibility_buffer_index]; // If we're using the AABB as the mesh center, determine its world space position. diff --git a/crates/bevy_render/src/view/mod.rs b/crates/bevy_render/src/view/mod.rs index 9779a9e7eea6f..c4885da83b799 100644 --- a/crates/bevy_render/src/view/mod.rs +++ b/crates/bevy_render/src/view/mod.rs @@ -286,6 +286,11 @@ pub struct RetainedViewEntity { /// /// If not present, this will be `MainEntity(Entity::PLACEHOLDER)`. pub auxiliary_entity: MainEntity, + + /// The render entity corresponding to the `auxiliary_entity`. + /// + /// This is used to get the auxiliary entity's world position + /// for visibility range culling calculations. pub render_auxiliary_entity: Entity, /// The index of the view corresponding to the entity. @@ -298,10 +303,10 @@ pub struct RetainedViewEntity { impl RetainedViewEntity { /// Creates a new [`RetainedViewEntity`] from the given main world entity, - /// auxiliary main world entity, and subview index. + /// auxiliary main world entity, auxiliary render world entity, and subview index. /// /// See [`RetainedViewEntity::subview_index`] for an explanation of what - /// `auxiliary_entity` and `subview_index` are. + /// `auxiliary_entity`, `render_auxiliary_entity` and `subview_index` are. pub fn new( main_entity: MainEntity, auxiliary_entity: Option, @@ -662,9 +667,23 @@ pub struct ViewUniform { /// This is useful for directional shadow views, where visibility range culling should /// be executed in relation to its non-shadow camera's world position. /// - /// If this ViewUniform already represents a camera view, this field will be set to world_position. - /// If this ViewUniform has no associated camera view, this field will be set to a Vec3 of NaN's. + /// If this ViewUniform already represents a camera view or has no associated camera view, + /// this field will be set to world_position. pub primary_world_position: Vec3, + /// Flags associated with this View. + pub flags: u32, +} + +// NOTE: These must match the bit flags in bevy_pbr/src/view/view.wgsl! +bitflags::bitflags! { + /// Various flags and tightly-packed values on a View. + #[repr(transparent)] + pub struct ViewFlags: u32 { + /// Whether the primary_world_position can be used for visibility range cullling. + /// + /// If false, this view should not be used for visibility range culling. + const USABLE_PRIMARY_WORLD_POSITION = 1 << 0; + } } #[derive(Resource)] @@ -1061,11 +1080,16 @@ pub fn prepare_view_uniforms( .map(|frustum| frustum.half_spaces.map(|h| h.normal_d())) .unwrap_or([Vec4::ZERO; 6]); + let mut view_flags = ViewFlags::empty(); let primary_world_position = if let Ok(primary_extracted_view) = primary_view.get(extracted_view.retained_view_entity.render_auxiliary_entity) { + view_flags |= ViewFlags::USABLE_PRIMARY_WORLD_POSITION; primary_extracted_view.world_from_view.translation() } else { + if extracted_camera.is_some() { + view_flags |= ViewFlags::USABLE_PRIMARY_WORLD_POSITION; + } extracted_view.world_from_view.translation() }; @@ -1089,6 +1113,7 @@ pub fn prepare_view_uniforms( mip_bias: mip_bias.unwrap_or(&MipBias(0.0)).0, frame_count: frame_count.0, primary_world_position, + flags: view_flags.bits(), }), }; diff --git a/crates/bevy_render/src/view/view.wgsl b/crates/bevy_render/src/view/view.wgsl index dadfaeeb00235..52c15c0397785 100644 --- a/crates/bevy_render/src/view/view.wgsl +++ b/crates/bevy_render/src/view/view.wgsl @@ -65,8 +65,11 @@ struct View { mip_bias: f32, frame_count: u32, primary_world_position: vec3, + flags: u32, }; +const VIEW_FLAGS_HAS_USABLE_PRIMARY_WORLD_POSITION: u32 = 1u << 0u; + /// World space: /// +y is up From a31135030226aaf87554cbb6824b3992e4178197 Mon Sep 17 00:00:00 2001 From: Kevin Chen Date: Tue, 5 May 2026 01:41:01 -0400 Subject: [PATCH 3/4] backticks --- crates/bevy_render/src/view/mod.rs | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/crates/bevy_render/src/view/mod.rs b/crates/bevy_render/src/view/mod.rs index c4885da83b799..b49794213f88c 100644 --- a/crates/bevy_render/src/view/mod.rs +++ b/crates/bevy_render/src/view/mod.rs @@ -667,8 +667,8 @@ pub struct ViewUniform { /// This is useful for directional shadow views, where visibility range culling should /// be executed in relation to its non-shadow camera's world position. /// - /// If this ViewUniform already represents a camera view or has no associated camera view, - /// this field will be set to world_position. + /// If this `ViewUniform` already represents a camera view or has no associated camera view, + /// this field will be set to `world_position`. pub primary_world_position: Vec3, /// Flags associated with this View. pub flags: u32, @@ -679,9 +679,8 @@ bitflags::bitflags! { /// Various flags and tightly-packed values on a View. #[repr(transparent)] pub struct ViewFlags: u32 { - /// Whether the primary_world_position can be used for visibility range cullling. - /// - /// If false, this view should not be used for visibility range culling. + /// Whether the `primary_world_position` of this `ViewUniform` can be used + /// for visibility range cullling. const USABLE_PRIMARY_WORLD_POSITION = 1 << 0; } } From 15e7eb29249e2e3cb80295714ed0c8738c07752e Mon Sep 17 00:00:00 2001 From: Kevin Chen Date: Thu, 7 May 2026 17:26:21 -0400 Subject: [PATCH 4/4] Add TODO about point and spot light shadows --- crates/bevy_render/src/view/mod.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/crates/bevy_render/src/view/mod.rs b/crates/bevy_render/src/view/mod.rs index b49794213f88c..90e8f06ac04b9 100644 --- a/crates/bevy_render/src/view/mod.rs +++ b/crates/bevy_render/src/view/mod.rs @@ -1086,6 +1086,12 @@ pub fn prepare_view_uniforms( view_flags |= ViewFlags::USABLE_PRIMARY_WORLD_POSITION; primary_extracted_view.world_from_view.translation() } else { + // Point and Spot light shadows do not have a usable primary world position, + // because they are not associated with a camera. + // This means that they don't engage in visibility range culling. + // This is a problem if the original mesh also has the `NoCpuCulling` component, + // as this means that the shadow will never be culled via visibility range! + // TODO: How do we better handle this case? if extracted_camera.is_some() { view_flags |= ViewFlags::USABLE_PRIMARY_WORLD_POSITION; }