From 74955d262f5b43f439eb8146a47df7a312cdaa61 Mon Sep 17 00:00:00 2001 From: Patrick Walton Date: Wed, 13 May 2026 17:54:32 -0700 Subject: [PATCH 1/3] Correctly handle visibility ranges in shadow maps. Right now, visibility ranges are always resolved relative to the view. This is incorrect for shadow maps in two ways: 1. Visibility ranges for meshes in directional light shadow maps should be resolved relative to the camera that the cascades are associated with. 2. Visibility ranges for meshes in point and spot light shadow maps should be resolved relative to *some* camera. To properly solve (2), this commit introduces the notion of a *primary camera*. The primary camera is the one that visibility ranges are relative to, when rendering views not associated with any camera. Point and spot light shadow maps are currently not associated with a camera, and therefore we need this extra notion in order to properly evaluate visibility ranges. (As a follow-up, we should introduce the notion of *own shadow maps*, which will allow each camera to have separate shadow maps for point and spot lights. That feature is however out of scope for *this* patch, which simply seeks to make the existing semantics consistent.) A new component, `PrimaryCamera`, has been added, which allows the developer to customize which camera is primary. In the absence of this component, this PR implements a simple heuristic to determine the primary camera: to prefer cameras that render to a window. This heuristic should suffice in the vast majority of cases, so developers will rarely have to manually use the `PrimaryCamera` component. A new field, `lod_view_world_position`, has been added to `View` to supply the position of the primary camera to the GPU. This is much simpler than introducing a new uniform or using immediates, as #24197 tried to do. This commit is the proper fix for #23991. PR #24252 attempted to fix this problem by reverting #23115. However, this didn't actually fix the issue, because the semantics were still inconsistent. This commit constitutes the correct fix for the issue. I verified that, after un-reverting #23115 on top of this patch and modifying it to use the new `lod_view_world_position`, that the issue reported in #23991 disappears. --- crates/bevy_camera/src/camera.rs | 41 ++++++++++++++ .../bevy_pbr/src/render/mesh_functions.wgsl | 2 +- crates/bevy_render/src/camera.rs | 45 ++++++++++++++-- crates/bevy_render/src/view/mod.rs | 53 +++++++++++++++++++ crates/bevy_render/src/view/view.wgsl | 6 +++ 5 files changed, 142 insertions(+), 5 deletions(-) diff --git a/crates/bevy_camera/src/camera.rs b/crates/bevy_camera/src/camera.rs index 9714caf5a1321..076c025d4b0e5 100644 --- a/crates/bevy_camera/src/camera.rs +++ b/crates/bevy_camera/src/camera.rs @@ -840,6 +840,47 @@ impl Default for CameraOutputMode { } } +/// The camera that Bevy uses to resolve visibility ranges when no specific +/// camera is applicable. +/// +/// For efficiency, Bevy currently renders point and spot light shadow maps only +/// once per frame, regardless of the number of cameras in use, rather than +/// rendering the shadow maps anew for each camera each frame. Most of the time, +/// this optimization doesn't change the result relative to a rendering that +/// rendered such shadow maps separately for each camera. However, there's one +/// exception: visibility ranges. Visibility ranges cause meshes to be visible +/// or invisible depending on the distance from the mesh to the camera. When +/// rendering a shadow map for a point or spot light, Bevy must therefore select +/// a camera as the reference camera for the purposes of visibility ranges. This +/// camera is known as the *primary camera*. +/// +/// Placing this component on a [`Camera`] makes that camera the primary one for +/// the purposes of shadow mapping of point and spot lights. +/// +/// The exact algorithm that Bevy uses to determine a primary camera is as +/// follows. Once a camera is chosen as the primary camera, all further steps +/// are skipped. +/// +/// 1. If a camera has this [`PrimaryCamera`] component, then it's the primary +/// camera. If there's more than one camera with the [`PrimaryCamera`] +/// component, one is chosen arbitrarily. +/// +/// 2. If a camera renders to a window (that is, the camera's [`RenderTarget`] +/// is [`RenderTarget::Window`]), then that camera is the primary one. If +/// there's more than one camera that renders to a window, then one is chosen +/// arbitrarily. +/// +/// 3. A primary camera is chosen arbitrarily from all cameras in the scene. +/// +/// This algorithm means that, in most cases, you don't need to add this +/// [`PrimaryCamera`] component explicitly to the scene; usually, Bevy chooses +/// the right camera automatically. You only need to use this component +/// explicitly if you have multiple cameras rendering to the window: e.g. a in a +/// split-screen game. +#[derive(Clone, Copy, Component, Default, Debug, Reflect)] +#[reflect(Clone, Component, Default)] +pub struct PrimaryCamera; + /// The "target" that a [`Camera`] will render to. For example, this could be a `Window` /// swapchain or an [`Image`]. #[derive(Component, Debug, Clone, Reflect, From)] diff --git a/crates/bevy_pbr/src/render/mesh_functions.wgsl b/crates/bevy_pbr/src/render/mesh_functions.wgsl index 4514f1270c163..9488d061f1524 100644 --- a/crates/bevy_pbr/src/render/mesh_functions.wgsl +++ b/crates/bevy_pbr/src/render/mesh_functions.wgsl @@ -146,7 +146,7 @@ fn get_visibility_range_dither_level(instance_index: u32, world_position: vec4, >, primary_window: Extract>>, + primary_camera: Extract>>, extracted_windows: Res, manual_texture_views: Res, images: Res>, @@ -520,6 +523,34 @@ pub fn extract_cameras( NoIndirectDrawing, ViewUniformOffset, ); + + // Determine the primary camera. The selection priority is, from highest to + // lowest: + // + // 1. A camera explicitly marked with the `PrimaryCamera` component. + // + // 2. A camera that renders to a window. + // + // 3. Any camera. + let mut primary_camera = primary_camera.iter().next(); + if primary_camera.is_none() { + primary_camera = + query + .iter() + .find_map( + |(main_entity, _, _, render_target, _, _, _, _, _)| match *render_target { + RenderTarget::Window(_) => Some(main_entity), + _ => None, + }, + ); + } + if primary_camera.is_none() { + primary_camera = query + .iter() + .map(|(main_entity, _, _, _, _, _, _, _, _)| main_entity) + .next(); + } + for ( main_entity, render_entity, @@ -682,6 +713,12 @@ pub fn extract_cameras( commands.remove::(); } + if primary_camera == Some(main_entity) { + commands.insert(ExtractedPrimaryCamera); + } else { + commands.remove::(); + } + if no_indirect_drawing || !matches!( gpu_preprocessing_support.max_supported_mode, diff --git a/crates/bevy_render/src/view/mod.rs b/crates/bevy_render/src/view/mod.rs index 6c55afaec7b80..fc9e0f018d18a 100644 --- a/crates/bevy_render/src/view/mod.rs +++ b/crates/bevy_render/src/view/mod.rs @@ -390,6 +390,16 @@ impl ExtractedView { } } +/// The render-world component that marks the primary camera. +/// +/// See [`bevy_camera::camera::PrimaryCamera`] for more information on primary +/// cameras. +/// +/// If any cameras are present in the render world, exactly one will have this +/// component. +#[derive(Clone, Copy, Component, Debug)] +pub struct ExtractedPrimaryCamera; + /// Configures filmic color grading parameters to adjust the image appearance. /// /// Color grading is applied just before tonemapping for a given @@ -651,6 +661,13 @@ pub struct ViewUniform { /// The normal vectors point towards the interior of the frustum. /// A half space contains `p` if `normal.dot(p) + distance > 0.` pub frustum: [Vec4; 6], + /// The world-space position of the camera used to resolve visibility + /// ranges. + /// + /// This is the position of the camera itself, unless this view isn't + /// associated with a camera, in which case it's the position of the primary + /// camera. + pub lod_view_world_position: Vec3, pub color_grading: ColorGradingUniform, pub mip_bias: f32, pub frame_count: u32, @@ -997,6 +1014,7 @@ pub fn prepare_view_uniforms( Option<&MipBias>, Option<&MainPassResolutionOverride>, )>, + primary_view: Query<&ExtractedView, With>, frame_count: Res, ) { let view_iter = views.iter(); @@ -1008,6 +1026,9 @@ pub fn prepare_view_uniforms( else { return; }; + let Some(primary_view) = primary_view.iter().next() else { + return; + }; for ( entity, extracted_camera, @@ -1049,6 +1070,37 @@ pub fn prepare_view_uniforms( .map(|frustum| frustum.half_spaces.map(|h| h.normal_d())) .unwrap_or([Vec4::ZERO; 6]); + // Determine the position of the camera used for resolving visibility + // ranges (LODs). + let lod_view_world_position = if extracted_camera.is_some() { + // If we're rendering a camera directly (i.e. we're not rendering a + // shadow map), we use this camera's position as the LOD view + // position. + extracted_view.world_from_view.translation() + } else if extracted_view.retained_view_entity.auxiliary_entity + == MainEntity::from(Entity::PLACEHOLDER) + { + // If we're rendering a shadow map that isn't associated with a + // camera, we use the position of the primary camera as the LOD view + // position. + primary_view.world_from_view.translation() + } else { + // Otherwise, if we're rendering a shadow map that is associated + // with a camera (i.e. a directional light shadow map, at present), + // we use the position of that camera as the LOD view position. This + // ensures that each rendered object has a shadow and that no + // invisible objects have shadows. + match views.get( + extracted_view + .retained_view_entity + .auxiliary_entity + .entity(), + ) { + Ok((_, _, camera_view, _, _, _, _)) => camera_view.world_from_view.translation(), + Err(_) => primary_view.world_from_view.translation(), + } + }; + let view_uniforms = ViewUniformOffset { offset: writer.write(&ViewUniform { clip_from_world, @@ -1065,6 +1117,7 @@ pub fn prepare_view_uniforms( viewport, main_pass_viewport, frustum, + lod_view_world_position, color_grading: extracted_view.color_grading.clone().into(), mip_bias: mip_bias.unwrap_or(&MipBias(0.0)).0, frame_count: frame_count.0, diff --git a/crates/bevy_render/src/view/view.wgsl b/crates/bevy_render/src/view/view.wgsl index 23ded53f40f91..308e9231369b8 100644 --- a/crates/bevy_render/src/view/view.wgsl +++ b/crates/bevy_render/src/view/view.wgsl @@ -61,6 +61,12 @@ struct View { // The normal vectors point towards the interior of the frustum. // A half space contains `p` if `normal.dot(p) + distance > 0.` frustum: array, 6>, + // The world-space position of the camera used to resolve visibility ranges. + // + // This is the position of the camera itself, unless this view isn't + // associated with a camera, in which case it's the position of the primary + // camera. + lod_view_world_position: vec3, color_grading: ColorGrading, mip_bias: f32, frame_count: u32, From 0f89ee25bfb3545371145e59c4783ad9222967f0 Mon Sep 17 00:00:00 2001 From: Patrick Walton Date: Thu, 14 May 2026 15:00:57 -0700 Subject: [PATCH 2/3] Rename `PrimaryCamera` to `ShadowLodOrigin` and make it not have to be a camera; fix CPU visibility range computation; pick the lowest entity ID in the case of a tie --- crates/bevy_camera/src/camera.rs | 85 +++++++++++----------- crates/bevy_camera/src/visibility/range.rs | 18 +---- crates/bevy_light/Cargo.toml | 1 + crates/bevy_light/src/lib.rs | 77 +++++++++++++++++++- crates/bevy_pbr/src/lib.rs | 7 +- crates/bevy_pbr/src/render/light.rs | 33 ++++++++- crates/bevy_render/src/camera.rs | 44 +---------- crates/bevy_render/src/view/mod.rs | 27 +++---- 8 files changed, 170 insertions(+), 122 deletions(-) diff --git a/crates/bevy_camera/src/camera.rs b/crates/bevy_camera/src/camera.rs index 076c025d4b0e5..f5d95dd5cc04c 100644 --- a/crates/bevy_camera/src/camera.rs +++ b/crates/bevy_camera/src/camera.rs @@ -811,6 +811,50 @@ impl Camera { } } +/// The entity that Bevy uses to resolve visibility ranges when no specific +/// camera is applicable. +/// +/// For efficiency, Bevy currently renders point and spot light shadow maps only +/// once per frame, regardless of the number of cameras in use, rather than +/// rendering the shadow maps anew for each camera each frame. Most of the time, +/// this optimization doesn't change the result relative to a rendering that +/// rendered such shadow maps separately for each camera. However, there's one +/// exception: visibility ranges. Visibility ranges cause meshes to be visible +/// or invisible depending on the distance from the mesh to the camera. When +/// rendering a shadow map for a point or spot light, Bevy must therefore select +/// an entity to use as the reference point for the purposes of visibility +/// ranges. This entity is called the *LOD origin*. +/// +/// Placing this component on an entity makes that entity the origin from which +/// LOD distances are computed for the purposes of shadow mapping of point and +/// spot lights. Typically, you place this component on a camera, but you may +/// place it on another entity if you wish. +/// +/// The exact algorithm that Bevy uses to determine the LOD origin is as +/// follows. Once the LOD origin is determined, all further steps are skipped. +/// +/// 1. If an entity has this [`ShadowLodOrigin`] component, then it's the LOD +/// origin. If there's more than one entity with the [`ShadowLodOrigin`] +/// component, one is chosen arbitrarily in a manner that's stable from frame to +/// frame. +/// +/// 2. If a camera renders to a window (that is, the camera's [`RenderTarget`] +/// is [`RenderTarget::Window`]), then that camera is the shadow LOD origin. If +/// there's more than one such camera that renders to a window, then one is +/// chosen arbitrarily in a manner that's stable from frame to frame. +/// +/// 3. A camera is chosen to be the LOD origin arbitrarily from all cameras in +/// the scene in a manner that's stable from frame to frame. +/// +/// This algorithm means that, in most cases, you don't need to add this +/// [`ShadowLodOrigin`] component explicitly to the scene; usually, Bevy chooses +/// the right origin automatically. You only need to use this component +/// explicitly if you have multiple cameras rendering to the window: e.g. a in a +/// split-screen game. +#[derive(Clone, Copy, Default, Component, Debug, Reflect)] +#[reflect(Clone, Default, Component)] +pub struct ShadowLodOrigin; + /// Control how this [`Camera`] outputs once rendering is completed. #[derive(Debug, Clone, Copy, Reflect)] pub enum CameraOutputMode { @@ -840,47 +884,6 @@ impl Default for CameraOutputMode { } } -/// The camera that Bevy uses to resolve visibility ranges when no specific -/// camera is applicable. -/// -/// For efficiency, Bevy currently renders point and spot light shadow maps only -/// once per frame, regardless of the number of cameras in use, rather than -/// rendering the shadow maps anew for each camera each frame. Most of the time, -/// this optimization doesn't change the result relative to a rendering that -/// rendered such shadow maps separately for each camera. However, there's one -/// exception: visibility ranges. Visibility ranges cause meshes to be visible -/// or invisible depending on the distance from the mesh to the camera. When -/// rendering a shadow map for a point or spot light, Bevy must therefore select -/// a camera as the reference camera for the purposes of visibility ranges. This -/// camera is known as the *primary camera*. -/// -/// Placing this component on a [`Camera`] makes that camera the primary one for -/// the purposes of shadow mapping of point and spot lights. -/// -/// The exact algorithm that Bevy uses to determine a primary camera is as -/// follows. Once a camera is chosen as the primary camera, all further steps -/// are skipped. -/// -/// 1. If a camera has this [`PrimaryCamera`] component, then it's the primary -/// camera. If there's more than one camera with the [`PrimaryCamera`] -/// component, one is chosen arbitrarily. -/// -/// 2. If a camera renders to a window (that is, the camera's [`RenderTarget`] -/// is [`RenderTarget::Window`]), then that camera is the primary one. If -/// there's more than one camera that renders to a window, then one is chosen -/// arbitrarily. -/// -/// 3. A primary camera is chosen arbitrarily from all cameras in the scene. -/// -/// This algorithm means that, in most cases, you don't need to add this -/// [`PrimaryCamera`] component explicitly to the scene; usually, Bevy chooses -/// the right camera automatically. You only need to use this component -/// explicitly if you have multiple cameras rendering to the window: e.g. a in a -/// split-screen game. -#[derive(Clone, Copy, Component, Default, Debug, Reflect)] -#[reflect(Clone, Component, Default)] -pub struct PrimaryCamera; - /// The "target" that a [`Camera`] will render to. For example, this could be a `Window` /// swapchain or an [`Image`]. #[derive(Component, Debug, Clone, Reflect, From)] diff --git a/crates/bevy_camera/src/visibility/range.rs b/crates/bevy_camera/src/visibility/range.rs index 9b9c3f8442c59..bd3b460744cad 100644 --- a/crates/bevy_camera/src/visibility/range.rs +++ b/crates/bevy_camera/src/visibility/range.rs @@ -10,7 +10,7 @@ use bevy_app::{App, Plugin, PostUpdate}; use bevy_ecs::{ component::Component, entity::{Entity, EntityHashMap}, - query::With, + query::{Or, With}, reflect::ReflectComponent, resource::Resource, schedule::IntoScheduleConfigs as _, @@ -22,7 +22,7 @@ use bevy_transform::components::GlobalTransform; use bevy_utils::Parallel; use super::{check_visibility_cpu_culling, VisibilitySystems}; -use crate::{camera::Camera, primitives::Aabb}; +use crate::{camera::Camera, primitives::Aabb, ShadowLodOrigin}; /// A plugin that enables [`VisibilityRange`]s, which allow entities to be /// hidden or shown based on distance to the camera. @@ -220,18 +220,6 @@ impl VisibleEntityRanges { }; (visibility_bitmask & (1 << view_index)) != 0 } - - /// Returns true if the entity is in range of any view. - /// - /// This only checks [`VisibilityRange`]s and doesn't perform any frustum or - /// occlusion culling. Thus the entity might not *actually* be visible. - /// - /// The entity is assumed to have a [`VisibilityRange`] component. If the - /// entity doesn't have that component, this method will return false. - #[inline] - pub fn entity_is_in_range_of_any_view(&self, entity: Entity) -> bool { - self.entities.contains_key(&entity) - } } /// Checks all entities against all views in order to determine which entities @@ -241,7 +229,7 @@ impl VisibleEntityRanges { /// cull. pub fn check_visibility_ranges( mut visible_entity_ranges: ResMut, - view_query: Query<(Entity, &GlobalTransform), With>, + view_query: Query<(Entity, &GlobalTransform), Or<(With, With)>>, mut par_local: Local>>, entity_query: Query<(Entity, &GlobalTransform, Option<&Aabb>, &VisibilityRange)>, ) { diff --git a/crates/bevy_light/Cargo.toml b/crates/bevy_light/Cargo.toml index 043601ec3be17..a6370e28a62a3 100644 --- a/crates/bevy_light/Cargo.toml +++ b/crates/bevy_light/Cargo.toml @@ -25,6 +25,7 @@ bevy_color = { path = "../bevy_color", version = "0.19.0-dev", features = [ "serialize", ] } bevy_gizmos = { path = "../bevy_gizmos", version = "0.19.0-dev", optional = true } +bevy_log = { path = "../bevy_log", version = "0.19.0-dev" } # other tracing = { version = "0.1", default-features = false } diff --git a/crates/bevy_light/src/lib.rs b/crates/bevy_light/src/lib.rs index bf0872dc6532b..bc2e099d9f8a8 100644 --- a/crates/bevy_light/src/lib.rs +++ b/crates/bevy_light/src/lib.rs @@ -7,23 +7,26 @@ extern crate alloc; use bevy_app::{App, Plugin, PostUpdate, Update}; use bevy_asset::{AssetApp, AssetEventSystems}; use bevy_camera::{ + self, primitives::{Aabb, CascadesFrusta, CubemapFrusta, Frustum, Sphere}, visibility::{ CascadesVisibleEntities, CubemapVisibleEntities, InheritedVisibility, NoCpuCulling, NoFrustumCulling, RenderLayers, ViewVisibility, VisibilityRange, VisibilitySystems, VisibleEntities, VisibleEntityRanges, VisibleMeshEntities, }, - Camera3d, CameraUpdateSystems, + Camera, Camera3d, CameraUpdateSystems, RenderTarget, ShadowLodOrigin, }; -use bevy_ecs::{entity::EntityHashSet, prelude::*}; +use bevy_ecs::{entity::EntityHashSet, prelude::*, system::QueryLens}; #[cfg(feature = "bevy_gizmos")] use bevy_gizmos::frustum::FrustumGizmoSystems; +use bevy_log::warn_once; use bevy_math::Vec3A; use bevy_mesh::Mesh3d; use bevy_reflect::prelude::*; use bevy_transform::{components::GlobalTransform, TransformSystems}; use bevy_utils::Parallel; use core::{any::TypeId, mem, ops::DerefMut}; +use smallvec::{smallvec, SmallVec}; pub mod cluster; use cluster::assign::assign_objects_to_clusters; @@ -533,6 +536,9 @@ pub fn check_point_light_mesh_visibility( With, ), >, + mut camera_query: Query<(Entity, &RenderTarget), With>, + mut shadow_lod_origin_query: Query>, + mut point_and_spot_light_query: Query, With)>>, visible_entity_ranges: Option>, mut cubemap_visible_entities_queue: Local; 6]>>, mut spot_visible_entities_queue: Local>>, @@ -540,6 +546,12 @@ pub fn check_point_light_mesh_visibility( ) { checked_lights.clear(); + let shadow_lod_origin = get_shadow_lod_origin( + camera_query.transmute_lens_filtered(), + shadow_lod_origin_query.transmute_lens_filtered(), + point_and_spot_light_query.transmute_lens_filtered(), + ); + let visible_entity_ranges = visible_entity_ranges.as_deref(); for visible_lights in &visible_point_lights { for &light_entity in visible_lights.get(TypeId::of::()) { @@ -589,7 +601,10 @@ pub fn check_point_light_mesh_visibility( } if has_visibility_range && visible_entity_ranges.is_some_and(|visible_entity_ranges| { - !visible_entity_ranges.entity_is_in_range_of_any_view(entity) + shadow_lod_origin.is_none_or(|shadow_lod_origin| { + !visible_entity_ranges + .entity_is_in_range_of_view(entity, shadow_lod_origin) + }) }) { return; @@ -679,7 +694,10 @@ pub fn check_point_light_mesh_visibility( // Check visibility ranges. if has_visibility_range && visible_entity_ranges.is_some_and(|visible_entity_ranges| { - !visible_entity_ranges.entity_is_in_range_of_any_view(entity) + shadow_lod_origin.is_none_or(|shadow_lod_origin| { + !visible_entity_ranges + .entity_is_in_range_of_view(entity, shadow_lod_origin) + }) }) { return; @@ -717,3 +735,54 @@ pub fn check_point_light_mesh_visibility( } } } + +/// Determines the LOD origin for spot and point light shadow maps. +/// +/// The selection priority is, from highest to lowest: +/// +/// 1. An entity explicitly marked with the [`ShadowLodOrigin`] component. +/// +/// 2. A camera that renders to a window. +/// +/// 3. Any camera. +pub fn get_shadow_lod_origin( + mut camera_query: QueryLens<(Entity, &RenderTarget), With>, + mut shadow_lod_origin_query: QueryLens>, + mut lights_query: QueryLens, With)>>, +) -> Option { + let (camera_query, shadow_lod_origin_query) = + (camera_query.query(), shadow_lod_origin_query.query()); + + let mut entities: SmallVec<[Entity; 4]> = smallvec![]; + entities.extend(shadow_lod_origin_query.iter()); + if let Some(lod_origin) = entities.iter().min() { + return Some(*lod_origin); + } + + entities.extend( + camera_query + .iter() + .filter_map(|(main_entity, render_target)| match *render_target { + RenderTarget::Window(_) => Some(main_entity), + _ => None, + }), + ); + if let Some(lod_origin) = entities.iter().min() { + return Some(*lod_origin); + }; + + entities.extend(camera_query.iter().map(|(main_entity, _)| main_entity)); + if let Some(lod_origin) = entities.iter().min() { + if !lights_query.query().is_empty() { + warn_once!( + "Point lights and/or spot lights are present, but no entity has \ + `ShadowLodOrigin`, and no camera that renders to the window has been found. \ + Consider using the `ShadowLodOrigin` component to set a LOD origin." + ); + } + + return Some(*lod_origin); + }; + + None +} diff --git a/crates/bevy_pbr/src/lib.rs b/crates/bevy_pbr/src/lib.rs index e7eaee6e2c958..c3ee7793dd3bb 100644 --- a/crates/bevy_pbr/src/lib.rs +++ b/crates/bevy_pbr/src/lib.rs @@ -32,7 +32,10 @@ mod gltf; use bevy_light::cluster::GlobalClusterSettings; use bevy_render::{ sync_component::SyncComponent, - view::{RenderExtractedShadowMapVisibleEntities, RenderShadowMapVisibleEntities}, + view::{ + RenderExtractedShadowMapVisibleEntities, RenderShadowLodOrigin, + RenderShadowMapVisibleEntities, + }, }; pub use contact_shadows::{ ContactShadows, ContactShadowsBuffer, ContactShadowsPlugin, ContactShadowsUniform, @@ -386,6 +389,7 @@ impl Plugin for PbrPlugin { extract_ambient_light_resource, extract_ambient_light, extract_shadow_filtering_method, + extract_shadow_lod_origin, late_sweep_material_instances, ), ) @@ -406,6 +410,7 @@ impl Plugin for PbrPlugin { ) .init_gpu_resource::() .init_gpu_resource::() + .init_resource::() .allow_ambiguous_resource::(); render_app.world_mut().add_observer(add_light_view_entities); diff --git a/crates/bevy_pbr/src/render/light.rs b/crates/bevy_pbr/src/render/light.rs index e99848ce860ac..fa35677f0f558 100644 --- a/crates/bevy_pbr/src/render/light.rs +++ b/crates/bevy_pbr/src/render/light.rs @@ -8,7 +8,7 @@ use bevy_camera::visibility::{ CascadesVisibleEntities, CubemapVisibleEntities, RenderLayers, ViewVisibility, VisibleMeshEntities, }; -use bevy_camera::Camera3d; +use bevy_camera::{Camera, Camera3d, RenderTarget, ShadowLodOrigin}; use bevy_color::ColorToComponents; use bevy_core_pipeline::core_3d::CORE_3D_DEPTH_FORMAT; use bevy_core_pipeline::schedule::RootNonCameraView; @@ -47,8 +47,8 @@ use bevy_render::occlusion_culling::{ }; use bevy_render::sync_world::{MainEntity, MainEntityHashMap, RenderEntity}; use bevy_render::view::{ - RenderExtractedShadowMapVisibleEntities, RenderShadowMapVisibleEntities, RenderVisibleEntities, - VisibilityExtractionSystemParam, + RenderExtractedShadowMapVisibleEntities, RenderShadowLodOrigin, RenderShadowMapVisibleEntities, + RenderVisibleEntities, VisibilityExtractionSystemParam, }; use bevy_render::{ batching::gpu_preprocessing::{GpuPreprocessingMode, GpuPreprocessingSupport}, @@ -2888,3 +2888,30 @@ fn get_shadow_map_visible_entities<'w, 's: 'w>( } } } + +/// An extraction system that determines the origin for LOD computation for +/// point and spot light shadow maps and updates the [`RenderShadowLodOrigin`] +/// with the result. +/// +/// See [`ShadowLodOrigin`] for more details on the algorithm that this system +/// uses. +pub fn extract_shadow_lod_origin( + global_transform_query: Extract>, + mut camera_query: Extract>>, + mut shadow_lod_origin_query: Extract>>, + mut lights_query: Extract, With)>>>, + mut render_shadow_lod_origin: ResMut, +) { + match bevy_light::get_shadow_lod_origin( + camera_query.transmute_lens_filtered(), + shadow_lod_origin_query.transmute_lens_filtered(), + lights_query.transmute_lens_filtered(), + ) + .and_then(|shadow_lod_origin_entity| global_transform_query.get(shadow_lod_origin_entity).ok()) + { + Some(global_transform) => { + render_shadow_lod_origin.0 = global_transform.translation(); + } + None => render_shadow_lod_origin.0 = Default::default(), + } +} diff --git a/crates/bevy_render/src/camera.rs b/crates/bevy_render/src/camera.rs index f17705e378188..29d15a00b0b97 100644 --- a/crates/bevy_render/src/camera.rs +++ b/crates/bevy_render/src/camera.rs @@ -10,10 +10,9 @@ use crate::{ sync_world::{MainEntity, MainEntityHashSet, RenderEntity, SyncToRenderWorld}, texture::{GpuImage, ManualTextureViews}, view::{ - ColorGrading, ExtractedPrimaryCamera, ExtractedView, ExtractedWindows, Msaa, - NoIndirectDrawing, RenderExtractedVisibleEntities, RenderVisibleEntities, - RenderVisibleEntitiesClass, RetainedViewEntity, ViewUniformOffset, - VisibilityExtractionSystemParam, + ColorGrading, ExtractedView, ExtractedWindows, Msaa, NoIndirectDrawing, + RenderExtractedVisibleEntities, RenderVisibleEntities, RenderVisibleEntitiesClass, + RetainedViewEntity, ViewUniformOffset, VisibilityExtractionSystemParam, }, Extract, ExtractSchedule, Render, RenderApp, RenderSystems, }; @@ -25,8 +24,7 @@ use bevy_camera::{ visibility::{self, RenderLayers, VisibleEntities}, Camera, Camera2d, Camera3d, CameraMainTextureUsages, CameraOutputMode, CameraUpdateSystems, ClearColor, ClearColorConfig, CompositingSpace, Exposure, Hdr, ManualTextureViewHandle, - MsaaWriteback, NormalizedRenderTarget, PrimaryCamera, Projection, RenderTarget, - RenderTargetInfo, Viewport, + MsaaWriteback, NormalizedRenderTarget, Projection, RenderTarget, RenderTargetInfo, Viewport, }; use bevy_derive::{Deref, DerefMut}; use bevy_ecs::{ @@ -499,7 +497,6 @@ pub fn extract_cameras( )>, >, primary_window: Extract>>, - primary_camera: Extract>>, extracted_windows: Res, manual_texture_views: Res, images: Res>, @@ -524,33 +521,6 @@ pub fn extract_cameras( ViewUniformOffset, ); - // Determine the primary camera. The selection priority is, from highest to - // lowest: - // - // 1. A camera explicitly marked with the `PrimaryCamera` component. - // - // 2. A camera that renders to a window. - // - // 3. Any camera. - let mut primary_camera = primary_camera.iter().next(); - if primary_camera.is_none() { - primary_camera = - query - .iter() - .find_map( - |(main_entity, _, _, render_target, _, _, _, _, _)| match *render_target { - RenderTarget::Window(_) => Some(main_entity), - _ => None, - }, - ); - } - if primary_camera.is_none() { - primary_camera = query - .iter() - .map(|(main_entity, _, _, _, _, _, _, _, _)| main_entity) - .next(); - } - for ( main_entity, render_entity, @@ -713,12 +683,6 @@ pub fn extract_cameras( commands.remove::(); } - if primary_camera == Some(main_entity) { - commands.insert(ExtractedPrimaryCamera); - } else { - commands.remove::(); - } - if no_indirect_drawing || !matches!( gpu_preprocessing_support.max_supported_mode, diff --git a/crates/bevy_render/src/view/mod.rs b/crates/bevy_render/src/view/mod.rs index fc9e0f018d18a..9e84c32508516 100644 --- a/crates/bevy_render/src/view/mod.rs +++ b/crates/bevy_render/src/view/mod.rs @@ -390,16 +390,6 @@ impl ExtractedView { } } -/// The render-world component that marks the primary camera. -/// -/// See [`bevy_camera::camera::PrimaryCamera`] for more information on primary -/// cameras. -/// -/// If any cameras are present in the render world, exactly one will have this -/// component. -#[derive(Clone, Copy, Component, Debug)] -pub struct ExtractedPrimaryCamera; - /// Configures filmic color grading parameters to adjust the image appearance. /// /// Color grading is applied just before tonemapping for a given @@ -612,6 +602,11 @@ impl ColorGrading { } } +/// A resource, part of the render world, that stores the resolved origin for +/// LOD selection for shadow maps of point and spot lights. +#[derive(Default, Resource, Debug)] +pub struct RenderShadowLodOrigin(pub Vec3); + #[derive(Clone, ShaderType)] pub struct ViewUniform { pub clip_from_world: Mat4, @@ -1014,8 +1009,8 @@ pub fn prepare_view_uniforms( Option<&MipBias>, Option<&MainPassResolutionOverride>, )>, - primary_view: Query<&ExtractedView, With>, frame_count: Res, + shadow_lod_origin: Res, ) { let view_iter = views.iter(); let view_count = view_iter.len(); @@ -1026,9 +1021,6 @@ pub fn prepare_view_uniforms( else { return; }; - let Some(primary_view) = primary_view.iter().next() else { - return; - }; for ( entity, extracted_camera, @@ -1081,9 +1073,8 @@ pub fn prepare_view_uniforms( == MainEntity::from(Entity::PLACEHOLDER) { // If we're rendering a shadow map that isn't associated with a - // camera, we use the position of the primary camera as the LOD view - // position. - primary_view.world_from_view.translation() + // camera, we use the shadow LOD origin. + shadow_lod_origin.0 } else { // Otherwise, if we're rendering a shadow map that is associated // with a camera (i.e. a directional light shadow map, at present), @@ -1097,7 +1088,7 @@ pub fn prepare_view_uniforms( .entity(), ) { Ok((_, _, camera_view, _, _, _, _)) => camera_view.world_from_view.translation(), - Err(_) => primary_view.world_from_view.translation(), + Err(_) => shadow_lod_origin.0, } }; From 34f7f245914c1de9940cd5f219f63d7abd6ba2f5 Mon Sep 17 00:00:00 2001 From: Patrick Walton Date: Fri, 15 May 2026 17:55:34 -0700 Subject: [PATCH 3/3] Address review comments --- crates/bevy_camera/src/camera.rs | 3 ++- crates/bevy_light/src/lib.rs | 1 - 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/bevy_camera/src/camera.rs b/crates/bevy_camera/src/camera.rs index f5d95dd5cc04c..38ecb39a016cb 100644 --- a/crates/bevy_camera/src/camera.rs +++ b/crates/bevy_camera/src/camera.rs @@ -849,10 +849,11 @@ impl Camera { /// This algorithm means that, in most cases, you don't need to add this /// [`ShadowLodOrigin`] component explicitly to the scene; usually, Bevy chooses /// the right origin automatically. You only need to use this component -/// explicitly if you have multiple cameras rendering to the window: e.g. a in a +/// explicitly if you have multiple cameras rendering to the window: e.g. in a /// split-screen game. #[derive(Clone, Copy, Default, Component, Debug, Reflect)] #[reflect(Clone, Default, Component)] +#[require(Transform)] pub struct ShadowLodOrigin; /// Control how this [`Camera`] outputs once rendering is completed. diff --git a/crates/bevy_light/src/lib.rs b/crates/bevy_light/src/lib.rs index bc2e099d9f8a8..566579faa4ffe 100644 --- a/crates/bevy_light/src/lib.rs +++ b/crates/bevy_light/src/lib.rs @@ -7,7 +7,6 @@ extern crate alloc; use bevy_app::{App, Plugin, PostUpdate, Update}; use bevy_asset::{AssetApp, AssetEventSystems}; use bevy_camera::{ - self, primitives::{Aabb, CascadesFrusta, CubemapFrusta, Frustum, Sphere}, visibility::{ CascadesVisibleEntities, CubemapVisibleEntities, InheritedVisibility, NoCpuCulling,