diff --git a/crates/bevy_camera/src/camera.rs b/crates/bevy_camera/src/camera.rs index 9714caf5a1321..38ecb39a016cb 100644 --- a/crates/bevy_camera/src/camera.rs +++ b/crates/bevy_camera/src/camera.rs @@ -811,6 +811,51 @@ 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. 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. #[derive(Debug, Clone, Copy, Reflect)] pub enum CameraOutputMode { 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..566579faa4ffe 100644 --- a/crates/bevy_light/src/lib.rs +++ b/crates/bevy_light/src/lib.rs @@ -13,17 +13,19 @@ use bevy_camera::{ 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 +535,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 +545,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 +600,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 +693,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 +734,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_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 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, @@ -998,6 +1010,7 @@ pub fn prepare_view_uniforms( Option<&MainPassResolutionOverride>, )>, frame_count: Res, + shadow_lod_origin: Res, ) { let view_iter = views.iter(); let view_count = view_iter.len(); @@ -1049,6 +1062,36 @@ 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 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), + // 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(_) => shadow_lod_origin.0, + } + }; + let view_uniforms = ViewUniformOffset { offset: writer.write(&ViewUniform { clip_from_world, @@ -1065,6 +1108,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,