diff --git a/crates/bevy_pbr/src/lib.rs b/crates/bevy_pbr/src/lib.rs index 0604c06328..cad7b8979c 100644 --- a/crates/bevy_pbr/src/lib.rs +++ b/crates/bevy_pbr/src/lib.rs @@ -122,6 +122,7 @@ pub mod graph { } } +pub use crate::cascade::{CascadeShadowConfig, CascadeShadowConfigBuilder, Cascades}; use crate::{deferred::DeferredPbrLightingPlugin, graph::NodePbr}; use bevy_app::prelude::*; use bevy_asset::{AssetApp, AssetPath, Assets, Handle}; @@ -130,19 +131,16 @@ use bevy_ecs::prelude::*; use bevy_image::Image; use bevy_render::{ alpha::AlphaMode, - camera::{sort_cameras, CameraUpdateSystems, Projection}, + camera::{sort_cameras, Projection}, extract_component::ExtractComponentPlugin, extract_resource::ExtractResourcePlugin, load_shader_library, render_graph::RenderGraph, render_resource::ShaderRef, sync_component::SyncComponentPlugin, - view::VisibilitySystems, ExtractSchedule, Render, RenderApp, RenderDebugFlags, RenderSystems, }; -use bevy_transform::TransformSystems; - use std::path::PathBuf; fn shader_ref(path: PathBuf) -> ShaderRef { @@ -205,22 +203,8 @@ impl Plugin for PbrPlugin { load_shader_library!(app, "meshlet/dummy_visibility_buffer_resolve.wgsl"); app.register_asset_reflect::() - .register_type::() - .register_type::() - .register_type::() .register_type::() - .register_type::() - .register_type::() - .register_type::() - .register_type::() - .register_type::() - .register_type::() - .register_type::() - .register_type::() - .init_resource::() .init_resource::() - .init_resource::() - .init_resource::() .register_type::() .init_resource::() .add_plugins(( @@ -243,7 +227,7 @@ impl Plugin for PbrPlugin { ExtractComponentPlugin::::default(), LightmapPlugin, LightProbePlugin, - PbrProjectionPlugin, + LightPlugin, GpuMeshPreprocessPlugin { use_gpu_instance_buffer_builder: self.use_gpu_instance_buffer_builder, }, @@ -266,64 +250,6 @@ impl Plugin for PbrPlugin { SimulationLightSystems::AssignLightsToClusters, ) .chain(), - ) - .configure_sets( - PostUpdate, - SimulationLightSystems::UpdateDirectionalLightCascades - .ambiguous_with(SimulationLightSystems::UpdateDirectionalLightCascades), - ) - .configure_sets( - PostUpdate, - SimulationLightSystems::CheckLightVisibility - .ambiguous_with(SimulationLightSystems::CheckLightVisibility), - ) - .add_systems( - PostUpdate, - ( - add_clusters - .in_set(SimulationLightSystems::AddClusters) - .after(CameraUpdateSystems), - assign_objects_to_clusters - .in_set(SimulationLightSystems::AssignLightsToClusters) - .after(TransformSystems::Propagate) - .after(VisibilitySystems::CheckVisibility) - .after(CameraUpdateSystems), - clear_directional_light_cascades - .in_set(SimulationLightSystems::UpdateDirectionalLightCascades) - .after(TransformSystems::Propagate) - .after(CameraUpdateSystems), - update_directional_light_frusta - .in_set(SimulationLightSystems::UpdateLightFrusta) - // This must run after CheckVisibility because it relies on `ViewVisibility` - .after(VisibilitySystems::CheckVisibility) - .after(TransformSystems::Propagate) - .after(SimulationLightSystems::UpdateDirectionalLightCascades) - // We assume that no entity will be both a directional light and a spot light, - // so these systems will run independently of one another. - // FIXME: Add an archetype invariant for this https://github.com/bevyengine/bevy/issues/1481. - .ambiguous_with(update_spot_light_frusta), - update_point_light_frusta - .in_set(SimulationLightSystems::UpdateLightFrusta) - .after(TransformSystems::Propagate) - .after(SimulationLightSystems::AssignLightsToClusters), - update_spot_light_frusta - .in_set(SimulationLightSystems::UpdateLightFrusta) - .after(TransformSystems::Propagate) - .after(SimulationLightSystems::AssignLightsToClusters), - ( - check_dir_light_mesh_visibility, - check_point_light_mesh_visibility, - ) - .in_set(SimulationLightSystems::CheckLightVisibility) - .after(VisibilitySystems::CalculateBounds) - .after(TransformSystems::Propagate) - .after(SimulationLightSystems::UpdateLightFrusta) - // NOTE: This MUST be scheduled AFTER the core renderer visibility check - // because that resets entity `ViewVisibility` for the first view - // which would override any results from this otherwise - .after(VisibilitySystems::CheckVisibility) - .before(VisibilitySystems::MarkNewlyHiddenEntitiesInvisible), - ), ); if self.add_default_deferred_lighting_plugin { @@ -401,17 +327,3 @@ impl Plugin for PbrPlugin { app.insert_resource(global_cluster_settings); } } - -/// Camera projection PBR functionality. -#[derive(Default)] -pub struct PbrProjectionPlugin; -impl Plugin for PbrProjectionPlugin { - fn build(&self, app: &mut App) { - app.add_systems( - PostUpdate, - build_directional_light_cascades - .in_set(SimulationLightSystems::UpdateDirectionalLightCascades) - .after(clear_directional_light_cascades), - ); - } -} diff --git a/crates/bevy_pbr/src/light/ambient_light.rs b/crates/bevy_pbr/src/light/ambient_light.rs index cfbe99963b..dfb9cdfecc 100644 --- a/crates/bevy_pbr/src/light/ambient_light.rs +++ b/crates/bevy_pbr/src/light/ambient_light.rs @@ -1,8 +1,12 @@ -use super::*; +use bevy_camera::Camera; +use bevy_color::Color; +use bevy_ecs::prelude::*; +use bevy_reflect::prelude::*; +use bevy_render::{extract_component::ExtractComponent, extract_resource::ExtractResource}; /// An ambient light, which lights the entire scene equally. /// -/// This resource is inserted by the [`PbrPlugin`] and by default it is set to a low ambient light. +/// This resource is inserted by the [`LightPlugin`] and by default it is set to a low ambient light. /// /// It can also be added to a camera to override the resource (or default) ambient for that camera only. /// @@ -17,6 +21,8 @@ use super::*; /// ambient_light.brightness = 100.0; /// } /// ``` +/// +/// [`LightPlugin`]: crate::LightPlugin #[derive(Resource, Component, Clone, Debug, ExtractResource, ExtractComponent, Reflect)] #[reflect(Resource, Component, Debug, Default, Clone)] #[require(Camera)] diff --git a/crates/bevy_pbr/src/light/cascade.rs b/crates/bevy_pbr/src/light/cascade.rs new file mode 100644 index 0000000000..a6ebe5a89b --- /dev/null +++ b/crates/bevy_pbr/src/light/cascade.rs @@ -0,0 +1,333 @@ +pub use bevy_camera::primitives::{face_index_to_name, CubeMapFace, CUBE_MAP_FACES}; +use bevy_camera::{Camera, Projection}; +use bevy_ecs::{entity::EntityHashMap, prelude::*}; +use bevy_math::{ops, Mat4, Vec3A, Vec4}; +use bevy_reflect::prelude::*; +use bevy_transform::components::GlobalTransform; + +use crate::{DirectionalLight, DirectionalLightShadowMap}; + +/// Controls how cascaded shadow mapping works. +/// Prefer using [`CascadeShadowConfigBuilder`] to construct an instance. +/// +/// ``` +/// # use bevy_pbr::CascadeShadowConfig; +/// # use bevy_pbr::CascadeShadowConfigBuilder; +/// # use bevy_utils::default; +/// # +/// let config: CascadeShadowConfig = CascadeShadowConfigBuilder { +/// maximum_distance: 100.0, +/// ..default() +/// }.into(); +/// ``` +#[derive(Component, Clone, Debug, Reflect)] +#[reflect(Component, Default, Debug, Clone)] +pub struct CascadeShadowConfig { + /// The (positive) distance to the far boundary of each cascade. + pub bounds: Vec, + /// The proportion of overlap each cascade has with the previous cascade. + pub overlap_proportion: f32, + /// The (positive) distance to the near boundary of the first cascade. + pub minimum_distance: f32, +} + +impl Default for CascadeShadowConfig { + fn default() -> Self { + CascadeShadowConfigBuilder::default().into() + } +} + +fn calculate_cascade_bounds( + num_cascades: usize, + nearest_bound: f32, + shadow_maximum_distance: f32, +) -> Vec { + if num_cascades == 1 { + return vec![shadow_maximum_distance]; + } + let base = ops::powf( + shadow_maximum_distance / nearest_bound, + 1.0 / (num_cascades - 1) as f32, + ); + (0..num_cascades) + .map(|i| nearest_bound * ops::powf(base, i as f32)) + .collect() +} + +/// Builder for [`CascadeShadowConfig`]. +pub struct CascadeShadowConfigBuilder { + /// The number of shadow cascades. + /// More cascades increases shadow quality by mitigating perspective aliasing - a phenomenon where areas + /// nearer the camera are covered by fewer shadow map texels than areas further from the camera, causing + /// blocky looking shadows. + /// + /// This does come at the cost increased rendering overhead, however this overhead is still less + /// than if you were to use fewer cascades and much larger shadow map textures to achieve the + /// same quality level. + /// + /// In case rendered geometry covers a relatively narrow and static depth relative to camera, it may + /// make more sense to use fewer cascades and a higher resolution shadow map texture as perspective aliasing + /// is not as much an issue. Be sure to adjust `minimum_distance` and `maximum_distance` appropriately. + pub num_cascades: usize, + /// The minimum shadow distance, which can help improve the texel resolution of the first cascade. + /// Areas nearer to the camera than this will likely receive no shadows. + /// + /// NOTE: Due to implementation details, this usually does not impact shadow quality as much as + /// `first_cascade_far_bound` and `maximum_distance`. At many view frustum field-of-views, the + /// texel resolution of the first cascade is dominated by the width / height of the view frustum plane + /// at `first_cascade_far_bound` rather than the depth of the frustum from `minimum_distance` to + /// `first_cascade_far_bound`. + pub minimum_distance: f32, + /// The maximum shadow distance. + /// Areas further from the camera than this will likely receive no shadows. + pub maximum_distance: f32, + /// Sets the far bound of the first cascade, relative to the view origin. + /// In-between cascades will be exponentially spaced relative to the maximum shadow distance. + /// NOTE: This is ignored if there is only one cascade, the maximum distance takes precedence. + pub first_cascade_far_bound: f32, + /// Sets the overlap proportion between cascades. + /// The overlap is used to make the transition from one cascade's shadow map to the next + /// less abrupt by blending between both shadow maps. + pub overlap_proportion: f32, +} + +impl CascadeShadowConfigBuilder { + /// Returns the cascade config as specified by this builder. + pub fn build(&self) -> CascadeShadowConfig { + assert!( + self.num_cascades > 0, + "num_cascades must be positive, but was {}", + self.num_cascades + ); + assert!( + self.minimum_distance >= 0.0, + "maximum_distance must be non-negative, but was {}", + self.minimum_distance + ); + assert!( + self.num_cascades == 1 || self.minimum_distance < self.first_cascade_far_bound, + "minimum_distance must be less than first_cascade_far_bound, but was {}", + self.minimum_distance + ); + assert!( + self.maximum_distance > self.minimum_distance, + "maximum_distance must be greater than minimum_distance, but was {}", + self.maximum_distance + ); + assert!( + (0.0..1.0).contains(&self.overlap_proportion), + "overlap_proportion must be in [0.0, 1.0) but was {}", + self.overlap_proportion + ); + CascadeShadowConfig { + bounds: calculate_cascade_bounds( + self.num_cascades, + self.first_cascade_far_bound, + self.maximum_distance, + ), + overlap_proportion: self.overlap_proportion, + minimum_distance: self.minimum_distance, + } + } +} + +impl Default for CascadeShadowConfigBuilder { + fn default() -> Self { + // The defaults are chosen to be similar to be Unity, Unreal, and Godot. + // Unity: first cascade far bound = 10.05, maximum distance = 150.0 + // Unreal Engine 5: maximum distance = 200.0 + // Godot: first cascade far bound = 10.0, maximum distance = 100.0 + Self { + // Currently only support one cascade in WebGL 2. + num_cascades: if cfg!(all( + feature = "webgl", + target_arch = "wasm32", + not(feature = "webgpu") + )) { + 1 + } else { + 4 + }, + minimum_distance: 0.1, + maximum_distance: 150.0, + first_cascade_far_bound: 10.0, + overlap_proportion: 0.2, + } + } +} + +impl From for CascadeShadowConfig { + fn from(builder: CascadeShadowConfigBuilder) -> Self { + builder.build() + } +} + +#[derive(Component, Clone, Debug, Default, Reflect)] +#[reflect(Component, Debug, Default, Clone)] +pub struct Cascades { + /// Map from a view to the configuration of each of its [`Cascade`]s. + pub cascades: EntityHashMap>, +} + +#[derive(Clone, Debug, Default, Reflect)] +#[reflect(Clone, Default)] +pub struct Cascade { + /// The transform of the light, i.e. the view to world matrix. + pub world_from_cascade: Mat4, + /// The orthographic projection for this cascade. + pub clip_from_cascade: Mat4, + /// The view-projection matrix for this cascade, converting world space into light clip space. + /// Importantly, this is derived and stored separately from `view_transform` and `projection` to + /// ensure shadow stability. + pub clip_from_world: Mat4, + /// Size of each shadow map texel in world units. + pub texel_size: f32, +} + +pub fn clear_directional_light_cascades(mut lights: Query<(&DirectionalLight, &mut Cascades)>) { + for (directional_light, mut cascades) in lights.iter_mut() { + if !directional_light.shadows_enabled { + continue; + } + cascades.cascades.clear(); + } +} + +pub fn build_directional_light_cascades( + directional_light_shadow_map: Res, + views: Query<(Entity, &GlobalTransform, &Projection, &Camera)>, + mut lights: Query<( + &GlobalTransform, + &DirectionalLight, + &CascadeShadowConfig, + &mut Cascades, + )>, +) { + let views = views + .iter() + .filter_map(|(entity, transform, projection, camera)| { + if camera.is_active { + Some((entity, projection, transform.to_matrix())) + } else { + None + } + }) + .collect::>(); + + for (transform, directional_light, cascades_config, mut cascades) in &mut lights { + if !directional_light.shadows_enabled { + continue; + } + + // It is very important to the numerical and thus visual stability of shadows that + // light_to_world has orthogonal upper-left 3x3 and zero translation. + // Even though only the direction (i.e. rotation) of the light matters, we don't constrain + // users to not change any other aspects of the transform - there's no guarantee + // `transform.to_matrix()` will give us a matrix with our desired properties. + // Instead, we directly create a good matrix from just the rotation. + let world_from_light = Mat4::from_quat(transform.compute_transform().rotation); + let light_to_world_inverse = world_from_light.inverse(); + + for (view_entity, projection, view_to_world) in views.iter().copied() { + let camera_to_light_view = light_to_world_inverse * view_to_world; + let view_cascades = cascades_config + .bounds + .iter() + .enumerate() + .map(|(idx, far_bound)| { + // Negate bounds as -z is camera forward direction. + let z_near = if idx > 0 { + (1.0 - cascades_config.overlap_proportion) + * -cascades_config.bounds[idx - 1] + } else { + -cascades_config.minimum_distance + }; + let z_far = -far_bound; + + let corners = projection.get_frustum_corners(z_near, z_far); + + calculate_cascade( + corners, + directional_light_shadow_map.size as f32, + world_from_light, + camera_to_light_view, + ) + }) + .collect(); + cascades.cascades.insert(view_entity, view_cascades); + } + } +} + +/// Returns a [`Cascade`] for the frustum defined by `frustum_corners`. +/// +/// The corner vertices should be specified in the following order: +/// first the bottom right, top right, top left, bottom left for the near plane, then similar for the far plane. +fn calculate_cascade( + frustum_corners: [Vec3A; 8], + cascade_texture_size: f32, + world_from_light: Mat4, + light_from_camera: Mat4, +) -> Cascade { + let mut min = Vec3A::splat(f32::MAX); + let mut max = Vec3A::splat(f32::MIN); + for corner_camera_view in frustum_corners { + let corner_light_view = light_from_camera.transform_point3a(corner_camera_view); + min = min.min(corner_light_view); + max = max.max(corner_light_view); + } + + // NOTE: Use the larger of the frustum slice far plane diagonal and body diagonal lengths as this + // will be the maximum possible projection size. Use the ceiling to get an integer which is + // very important for floating point stability later. It is also important that these are + // calculated using the original camera space corner positions for floating point precision + // as even though the lengths using corner_light_view above should be the same, precision can + // introduce small but significant differences. + // NOTE: The size remains the same unless the view frustum or cascade configuration is modified. + let cascade_diameter = (frustum_corners[0] - frustum_corners[6]) + .length() + .max((frustum_corners[4] - frustum_corners[6]).length()) + .ceil(); + + // NOTE: If we ensure that cascade_texture_size is a power of 2, then as we made cascade_diameter an + // integer, cascade_texel_size is then an integer multiple of a power of 2 and can be + // exactly represented in a floating point value. + let cascade_texel_size = cascade_diameter / cascade_texture_size; + // NOTE: For shadow stability it is very important that the near_plane_center is at integer + // multiples of the texel size to be exactly representable in a floating point value. + let near_plane_center = Vec3A::new( + (0.5 * (min.x + max.x) / cascade_texel_size).floor() * cascade_texel_size, + (0.5 * (min.y + max.y) / cascade_texel_size).floor() * cascade_texel_size, + // NOTE: max.z is the near plane for right-handed y-up + max.z, + ); + + // It is critical for `world_to_cascade` to be stable. So rather than forming `cascade_to_world` + // and inverting it, which risks instability due to numerical precision, we directly form + // `world_to_cascade` as the reference material suggests. + let light_to_world_transpose = world_from_light.transpose(); + let cascade_from_world = Mat4::from_cols( + light_to_world_transpose.x_axis, + light_to_world_transpose.y_axis, + light_to_world_transpose.z_axis, + (-near_plane_center).extend(1.0), + ); + + // Right-handed orthographic projection, centered at `near_plane_center`. + // NOTE: This is different from the reference material, as we use reverse Z. + let r = (max.z - min.z).recip(); + let clip_from_cascade = Mat4::from_cols( + Vec4::new(2.0 / cascade_diameter, 0.0, 0.0, 0.0), + Vec4::new(0.0, 2.0 / cascade_diameter, 0.0, 0.0), + Vec4::new(0.0, 0.0, r, 0.0), + Vec4::new(0.0, 0.0, 1.0, 1.0), + ); + + let clip_from_world = clip_from_cascade * cascade_from_world; + Cascade { + world_from_cascade: cascade_from_world.inverse(), + clip_from_cascade, + clip_from_world, + texel_size: cascade_texel_size, + } +} diff --git a/crates/bevy_pbr/src/light/directional_light.rs b/crates/bevy_pbr/src/light/directional_light.rs index 2d182c1c83..dd2da1d975 100644 --- a/crates/bevy_pbr/src/light/directional_light.rs +++ b/crates/bevy_pbr/src/light/directional_light.rs @@ -1,6 +1,16 @@ -use bevy_render::view::{self, Visibility}; +use bevy_asset::Handle; +use bevy_camera::{ + primitives::{CascadesFrusta, Frustum}, + visibility::{self, CascadesVisibleEntities, ViewVisibility, Visibility, VisibilityClass}, + Camera, +}; +use bevy_color::Color; +use bevy_ecs::prelude::*; +use bevy_image::Image; +use bevy_reflect::prelude::*; +use bevy_transform::components::Transform; -use super::*; +use crate::{cascade::CascadeShadowConfig, light_consts, Cascades, LightVisibilityClass}; /// A Directional light. /// @@ -53,7 +63,7 @@ use super::*; Visibility, VisibilityClass )] -#[component(on_add = view::add_visibility_class::)] +#[component(on_add = visibility::add_visibility_class::)] pub struct DirectionalLight { /// The color of the light. /// @@ -90,6 +100,8 @@ pub struct DirectionalLight { /// /// Note that soft shadows are significantly more expensive to render than /// hard shadows. + /// + /// [`ShadowFilteringMethod::Temporal`]: crate::ShadowFilteringMethod::Temporal #[cfg(feature = "experimental_pbr_pcss")] pub soft_shadow_size: Option, @@ -154,3 +166,64 @@ pub struct DirectionalLightTexture { /// Whether to tile the image infinitely, or use only a single tile centered at the light's translation pub tiled: bool, } + +/// Controls the resolution of [`DirectionalLight`] shadow maps. +/// +/// ``` +/// # use bevy_app::prelude::*; +/// # use bevy_pbr::DirectionalLightShadowMap; +/// App::new() +/// .insert_resource(DirectionalLightShadowMap { size: 4096 }); +/// ``` +#[derive(Resource, Clone, Debug, Reflect)] +#[reflect(Resource, Debug, Default, Clone)] +pub struct DirectionalLightShadowMap { + // The width and height of each cascade. + /// + /// Defaults to `2048`. + pub size: usize, +} + +impl Default for DirectionalLightShadowMap { + fn default() -> Self { + Self { size: 2048 } + } +} + +pub fn update_directional_light_frusta( + mut views: Query< + ( + &Cascades, + &DirectionalLight, + &ViewVisibility, + &mut CascadesFrusta, + ), + ( + // Prevents this query from conflicting with camera queries. + Without, + ), + >, +) { + for (cascades, directional_light, visibility, mut frusta) in &mut views { + // The frustum is used for culling meshes to the light for shadow mapping + // so if shadow mapping is disabled for this light, then the frustum is + // not needed. + if !directional_light.shadows_enabled || !visibility.get() { + continue; + } + + frusta.frusta = cascades + .cascades + .iter() + .map(|(view, cascades)| { + ( + *view, + cascades + .iter() + .map(|c| Frustum::from_clip_from_world(&c.clip_from_world)) + .collect::>(), + ) + }) + .collect(); + } +} diff --git a/crates/bevy_pbr/src/light/mod.rs b/crates/bevy_pbr/src/light/mod.rs index f73cebdba3..53199d39f1 100644 --- a/crates/bevy_pbr/src/light/mod.rs +++ b/crates/bevy_pbr/src/light/mod.rs @@ -1,36 +1,43 @@ -use bevy_ecs::{ - entity::{EntityHashMap, EntityHashSet}, - prelude::*, -}; -use bevy_math::{ops, Mat4, Vec3A, Vec4}; -use bevy_reflect::prelude::*; -use bevy_render::{ - camera::{Camera, Projection}, - extract_component::ExtractComponent, - extract_resource::ExtractResource, - mesh::Mesh3d, +use bevy_app::{App, Plugin, PostUpdate}; +use bevy_camera::{ primitives::{Aabb, CascadesFrusta, CubemapFrusta, Frustum, Sphere}, - view::{ - InheritedVisibility, NoFrustumCulling, PreviousVisibleEntities, RenderLayers, - ViewVisibility, VisibilityClass, VisibilityRange, VisibleEntityRanges, + visibility::{ + CascadesVisibleEntities, CubemapVisibleEntities, InheritedVisibility, NoFrustumCulling, + PreviousVisibleEntities, RenderLayers, ViewVisibility, VisibilityRange, VisibilitySystems, + VisibleEntityRanges, VisibleMeshEntities, }, + CameraUpdateSystems, }; -use bevy_transform::components::{GlobalTransform, Transform}; +use bevy_ecs::{entity::EntityHashSet, prelude::*}; +use bevy_math::Vec3A; +use bevy_reflect::prelude::*; +use bevy_render::{extract_component::ExtractComponent, mesh::Mesh3d}; +use bevy_transform::{components::GlobalTransform, TransformSystems}; use bevy_utils::Parallel; use core::ops::DerefMut; -use crate::*; -pub use light::spot_light::{spot_light_clip_from_view, spot_light_world_from_view}; +pub use crate::light::spot_light::{spot_light_clip_from_view, spot_light_world_from_view}; +use crate::{ + add_clusters, assign_objects_to_clusters, + cascade::{build_directional_light_cascades, clear_directional_light_cascades}, + CascadeShadowConfig, Cascades, VisibleClusterableObjects, +}; mod ambient_light; pub use ambient_light::AmbientLight; +pub mod cascade; mod point_light; -pub use point_light::{PointLight, PointLightTexture}; +pub use point_light::{ + update_point_light_frusta, PointLight, PointLightShadowMap, PointLightTexture, +}; mod spot_light; -pub use spot_light::{SpotLight, SpotLightTexture}; +pub use spot_light::{update_spot_light_frusta, SpotLight, SpotLightTexture}; mod directional_light; -pub use directional_light::{DirectionalLight, DirectionalLightTexture}; +pub use directional_light::{ + update_directional_light_frusta, DirectionalLight, DirectionalLightShadowMap, + DirectionalLightTexture, +}; /// Constants for operating with the light units: lumens, and lux. pub mod light_consts { @@ -91,26 +98,85 @@ pub mod light_consts { } } -/// Controls the resolution of [`PointLight`] shadow maps. -/// -/// ``` -/// # use bevy_app::prelude::*; -/// # use bevy_pbr::PointLightShadowMap; -/// App::new() -/// .insert_resource(PointLightShadowMap { size: 2048 }); -/// ``` -#[derive(Resource, Clone, Debug, Reflect)] -#[reflect(Resource, Debug, Default, Clone)] -pub struct PointLightShadowMap { - /// The width and height of each of the 6 faces of the cubemap. - /// - /// Defaults to `1024`. - pub size: usize, -} +pub struct LightPlugin; -impl Default for PointLightShadowMap { - fn default() -> Self { - Self { size: 1024 } +impl Plugin for LightPlugin { + fn build(&self, app: &mut App) { + app.register_type::() + .register_type::() + .register_type::() + .register_type::() + .register_type::() + .register_type::() + .register_type::() + .register_type::() + .register_type::() + .register_type::() + .register_type::() + .init_resource::() + .init_resource::() + .init_resource::() + .configure_sets( + PostUpdate, + SimulationLightSystems::UpdateDirectionalLightCascades + .ambiguous_with(SimulationLightSystems::UpdateDirectionalLightCascades), + ) + .configure_sets( + PostUpdate, + SimulationLightSystems::CheckLightVisibility + .ambiguous_with(SimulationLightSystems::CheckLightVisibility), + ) + .add_systems( + PostUpdate, + ( + add_clusters + .in_set(SimulationLightSystems::AddClusters) + .after(CameraUpdateSystems), + assign_objects_to_clusters + .in_set(SimulationLightSystems::AssignLightsToClusters) + .after(TransformSystems::Propagate) + .after(VisibilitySystems::CheckVisibility) + .after(CameraUpdateSystems), + clear_directional_light_cascades + .in_set(SimulationLightSystems::UpdateDirectionalLightCascades) + .after(TransformSystems::Propagate) + .after(CameraUpdateSystems), + update_directional_light_frusta + .in_set(SimulationLightSystems::UpdateLightFrusta) + // This must run after CheckVisibility because it relies on `ViewVisibility` + .after(VisibilitySystems::CheckVisibility) + .after(TransformSystems::Propagate) + .after(SimulationLightSystems::UpdateDirectionalLightCascades) + // We assume that no entity will be both a directional light and a spot light, + // so these systems will run independently of one another. + // FIXME: Add an archetype invariant for this https://github.com/bevyengine/bevy/issues/1481. + .ambiguous_with(update_spot_light_frusta), + update_point_light_frusta + .in_set(SimulationLightSystems::UpdateLightFrusta) + .after(TransformSystems::Propagate) + .after(SimulationLightSystems::AssignLightsToClusters), + update_spot_light_frusta + .in_set(SimulationLightSystems::UpdateLightFrusta) + .after(TransformSystems::Propagate) + .after(SimulationLightSystems::AssignLightsToClusters), + ( + check_dir_light_mesh_visibility, + check_point_light_mesh_visibility, + ) + .in_set(SimulationLightSystems::CheckLightVisibility) + .after(VisibilitySystems::CalculateBounds) + .after(TransformSystems::Propagate) + .after(SimulationLightSystems::UpdateLightFrusta) + // NOTE: This MUST be scheduled AFTER the core renderer visibility check + // because that resets entity `ViewVisibility` for the first view + // which would override any results from this otherwise + .after(VisibilitySystems::CheckVisibility) + .before(VisibilitySystems::MarkNewlyHiddenEntitiesInvisible), + build_directional_light_cascades + .in_set(SimulationLightSystems::UpdateDirectionalLightCascades) + .after(clear_directional_light_cascades), + ), + ); } } @@ -118,353 +184,6 @@ impl Default for PointLightShadowMap { /// With)>`, for use with [`bevy_render::view::VisibleEntities`]. pub type WithLight = Or<(With, With, With)>; -/// Controls the resolution of [`DirectionalLight`] shadow maps. -/// -/// ``` -/// # use bevy_app::prelude::*; -/// # use bevy_pbr::DirectionalLightShadowMap; -/// App::new() -/// .insert_resource(DirectionalLightShadowMap { size: 4096 }); -/// ``` -#[derive(Resource, Clone, Debug, Reflect)] -#[reflect(Resource, Debug, Default, Clone)] -pub struct DirectionalLightShadowMap { - // The width and height of each cascade. - /// - /// Defaults to `2048`. - pub size: usize, -} - -impl Default for DirectionalLightShadowMap { - fn default() -> Self { - Self { size: 2048 } - } -} - -/// Controls how cascaded shadow mapping works. -/// Prefer using [`CascadeShadowConfigBuilder`] to construct an instance. -/// -/// ``` -/// # use bevy_pbr::CascadeShadowConfig; -/// # use bevy_pbr::CascadeShadowConfigBuilder; -/// # use bevy_utils::default; -/// # -/// let config: CascadeShadowConfig = CascadeShadowConfigBuilder { -/// maximum_distance: 100.0, -/// ..default() -/// }.into(); -/// ``` -#[derive(Component, Clone, Debug, Reflect)] -#[reflect(Component, Default, Debug, Clone)] -pub struct CascadeShadowConfig { - /// The (positive) distance to the far boundary of each cascade. - pub bounds: Vec, - /// The proportion of overlap each cascade has with the previous cascade. - pub overlap_proportion: f32, - /// The (positive) distance to the near boundary of the first cascade. - pub minimum_distance: f32, -} - -impl Default for CascadeShadowConfig { - fn default() -> Self { - CascadeShadowConfigBuilder::default().into() - } -} - -fn calculate_cascade_bounds( - num_cascades: usize, - nearest_bound: f32, - shadow_maximum_distance: f32, -) -> Vec { - if num_cascades == 1 { - return vec![shadow_maximum_distance]; - } - let base = ops::powf( - shadow_maximum_distance / nearest_bound, - 1.0 / (num_cascades - 1) as f32, - ); - (0..num_cascades) - .map(|i| nearest_bound * ops::powf(base, i as f32)) - .collect() -} - -/// Builder for [`CascadeShadowConfig`]. -pub struct CascadeShadowConfigBuilder { - /// The number of shadow cascades. - /// More cascades increases shadow quality by mitigating perspective aliasing - a phenomenon where areas - /// nearer the camera are covered by fewer shadow map texels than areas further from the camera, causing - /// blocky looking shadows. - /// - /// This does come at the cost increased rendering overhead, however this overhead is still less - /// than if you were to use fewer cascades and much larger shadow map textures to achieve the - /// same quality level. - /// - /// In case rendered geometry covers a relatively narrow and static depth relative to camera, it may - /// make more sense to use fewer cascades and a higher resolution shadow map texture as perspective aliasing - /// is not as much an issue. Be sure to adjust `minimum_distance` and `maximum_distance` appropriately. - pub num_cascades: usize, - /// The minimum shadow distance, which can help improve the texel resolution of the first cascade. - /// Areas nearer to the camera than this will likely receive no shadows. - /// - /// NOTE: Due to implementation details, this usually does not impact shadow quality as much as - /// `first_cascade_far_bound` and `maximum_distance`. At many view frustum field-of-views, the - /// texel resolution of the first cascade is dominated by the width / height of the view frustum plane - /// at `first_cascade_far_bound` rather than the depth of the frustum from `minimum_distance` to - /// `first_cascade_far_bound`. - pub minimum_distance: f32, - /// The maximum shadow distance. - /// Areas further from the camera than this will likely receive no shadows. - pub maximum_distance: f32, - /// Sets the far bound of the first cascade, relative to the view origin. - /// In-between cascades will be exponentially spaced relative to the maximum shadow distance. - /// NOTE: This is ignored if there is only one cascade, the maximum distance takes precedence. - pub first_cascade_far_bound: f32, - /// Sets the overlap proportion between cascades. - /// The overlap is used to make the transition from one cascade's shadow map to the next - /// less abrupt by blending between both shadow maps. - pub overlap_proportion: f32, -} - -impl CascadeShadowConfigBuilder { - /// Returns the cascade config as specified by this builder. - pub fn build(&self) -> CascadeShadowConfig { - assert!( - self.num_cascades > 0, - "num_cascades must be positive, but was {}", - self.num_cascades - ); - assert!( - self.minimum_distance >= 0.0, - "maximum_distance must be non-negative, but was {}", - self.minimum_distance - ); - assert!( - self.num_cascades == 1 || self.minimum_distance < self.first_cascade_far_bound, - "minimum_distance must be less than first_cascade_far_bound, but was {}", - self.minimum_distance - ); - assert!( - self.maximum_distance > self.minimum_distance, - "maximum_distance must be greater than minimum_distance, but was {}", - self.maximum_distance - ); - assert!( - (0.0..1.0).contains(&self.overlap_proportion), - "overlap_proportion must be in [0.0, 1.0) but was {}", - self.overlap_proportion - ); - CascadeShadowConfig { - bounds: calculate_cascade_bounds( - self.num_cascades, - self.first_cascade_far_bound, - self.maximum_distance, - ), - overlap_proportion: self.overlap_proportion, - minimum_distance: self.minimum_distance, - } - } -} - -impl Default for CascadeShadowConfigBuilder { - fn default() -> Self { - // The defaults are chosen to be similar to be Unity, Unreal, and Godot. - // Unity: first cascade far bound = 10.05, maximum distance = 150.0 - // Unreal Engine 5: maximum distance = 200.0 - // Godot: first cascade far bound = 10.0, maximum distance = 100.0 - Self { - // Currently only support one cascade in WebGL 2. - num_cascades: if cfg!(all( - feature = "webgl", - target_arch = "wasm32", - not(feature = "webgpu") - )) { - 1 - } else { - 4 - }, - minimum_distance: 0.1, - maximum_distance: 150.0, - first_cascade_far_bound: 10.0, - overlap_proportion: 0.2, - } - } -} - -impl From for CascadeShadowConfig { - fn from(builder: CascadeShadowConfigBuilder) -> Self { - builder.build() - } -} - -#[derive(Component, Clone, Debug, Default, Reflect)] -#[reflect(Component, Debug, Default, Clone)] -pub struct Cascades { - /// Map from a view to the configuration of each of its [`Cascade`]s. - pub cascades: EntityHashMap>, -} - -#[derive(Clone, Debug, Default, Reflect)] -#[reflect(Clone, Default)] -pub struct Cascade { - /// The transform of the light, i.e. the view to world matrix. - pub world_from_cascade: Mat4, - /// The orthographic projection for this cascade. - pub clip_from_cascade: Mat4, - /// The view-projection matrix for this cascade, converting world space into light clip space. - /// Importantly, this is derived and stored separately from `view_transform` and `projection` to - /// ensure shadow stability. - pub clip_from_world: Mat4, - /// Size of each shadow map texel in world units. - pub texel_size: f32, -} - -pub fn clear_directional_light_cascades(mut lights: Query<(&DirectionalLight, &mut Cascades)>) { - for (directional_light, mut cascades) in lights.iter_mut() { - if !directional_light.shadows_enabled { - continue; - } - cascades.cascades.clear(); - } -} - -pub fn build_directional_light_cascades( - directional_light_shadow_map: Res, - views: Query<(Entity, &GlobalTransform, &Projection, &Camera)>, - mut lights: Query<( - &GlobalTransform, - &DirectionalLight, - &CascadeShadowConfig, - &mut Cascades, - )>, -) { - let views = views - .iter() - .filter_map(|(entity, transform, projection, camera)| { - if camera.is_active { - Some((entity, projection, transform.to_matrix())) - } else { - None - } - }) - .collect::>(); - - for (transform, directional_light, cascades_config, mut cascades) in &mut lights { - if !directional_light.shadows_enabled { - continue; - } - - // It is very important to the numerical and thus visual stability of shadows that - // light_to_world has orthogonal upper-left 3x3 and zero translation. - // Even though only the direction (i.e. rotation) of the light matters, we don't constrain - // users to not change any other aspects of the transform - there's no guarantee - // `transform.to_matrix()` will give us a matrix with our desired properties. - // Instead, we directly create a good matrix from just the rotation. - let world_from_light = Mat4::from_quat(transform.compute_transform().rotation); - let light_to_world_inverse = world_from_light.inverse(); - - for (view_entity, projection, view_to_world) in views.iter().copied() { - let camera_to_light_view = light_to_world_inverse * view_to_world; - let view_cascades = cascades_config - .bounds - .iter() - .enumerate() - .map(|(idx, far_bound)| { - // Negate bounds as -z is camera forward direction. - let z_near = if idx > 0 { - (1.0 - cascades_config.overlap_proportion) - * -cascades_config.bounds[idx - 1] - } else { - -cascades_config.minimum_distance - }; - let z_far = -far_bound; - - let corners = projection.get_frustum_corners(z_near, z_far); - - calculate_cascade( - corners, - directional_light_shadow_map.size as f32, - world_from_light, - camera_to_light_view, - ) - }) - .collect(); - cascades.cascades.insert(view_entity, view_cascades); - } - } -} - -/// Returns a [`Cascade`] for the frustum defined by `frustum_corners`. -/// -/// The corner vertices should be specified in the following order: -/// first the bottom right, top right, top left, bottom left for the near plane, then similar for the far plane. -fn calculate_cascade( - frustum_corners: [Vec3A; 8], - cascade_texture_size: f32, - world_from_light: Mat4, - light_from_camera: Mat4, -) -> Cascade { - let mut min = Vec3A::splat(f32::MAX); - let mut max = Vec3A::splat(f32::MIN); - for corner_camera_view in frustum_corners { - let corner_light_view = light_from_camera.transform_point3a(corner_camera_view); - min = min.min(corner_light_view); - max = max.max(corner_light_view); - } - - // NOTE: Use the larger of the frustum slice far plane diagonal and body diagonal lengths as this - // will be the maximum possible projection size. Use the ceiling to get an integer which is - // very important for floating point stability later. It is also important that these are - // calculated using the original camera space corner positions for floating point precision - // as even though the lengths using corner_light_view above should be the same, precision can - // introduce small but significant differences. - // NOTE: The size remains the same unless the view frustum or cascade configuration is modified. - let cascade_diameter = (frustum_corners[0] - frustum_corners[6]) - .length() - .max((frustum_corners[4] - frustum_corners[6]).length()) - .ceil(); - - // NOTE: If we ensure that cascade_texture_size is a power of 2, then as we made cascade_diameter an - // integer, cascade_texel_size is then an integer multiple of a power of 2 and can be - // exactly represented in a floating point value. - let cascade_texel_size = cascade_diameter / cascade_texture_size; - // NOTE: For shadow stability it is very important that the near_plane_center is at integer - // multiples of the texel size to be exactly representable in a floating point value. - let near_plane_center = Vec3A::new( - (0.5 * (min.x + max.x) / cascade_texel_size).floor() * cascade_texel_size, - (0.5 * (min.y + max.y) / cascade_texel_size).floor() * cascade_texel_size, - // NOTE: max.z is the near plane for right-handed y-up - max.z, - ); - - // It is critical for `world_to_cascade` to be stable. So rather than forming `cascade_to_world` - // and inverting it, which risks instability due to numerical precision, we directly form - // `world_to_cascade` as the reference material suggests. - let light_to_world_transpose = world_from_light.transpose(); - let cascade_from_world = Mat4::from_cols( - light_to_world_transpose.x_axis, - light_to_world_transpose.y_axis, - light_to_world_transpose.z_axis, - (-near_plane_center).extend(1.0), - ); - - // Right-handed orthographic projection, centered at `near_plane_center`. - // NOTE: This is different from the reference material, as we use reverse Z. - let r = (max.z - min.z).recip(); - let clip_from_cascade = Mat4::from_cols( - Vec4::new(2.0 / cascade_diameter, 0.0, 0.0, 0.0), - Vec4::new(0.0, 2.0 / cascade_diameter, 0.0, 0.0), - Vec4::new(0.0, 0.0, r, 0.0), - Vec4::new(0.0, 0.0, 1.0, 1.0), - ); - - let clip_from_world = clip_from_cascade * cascade_from_world; - Cascade { - world_from_cascade: cascade_from_world.inverse(), - clip_from_cascade, - clip_from_world, - texel_size: cascade_texel_size, - } -} /// Add this component to make a [`Mesh3d`] not cast shadows. #[derive(Debug, Component, Reflect, Default)] #[reflect(Component, Default, Debug)] @@ -525,6 +244,8 @@ pub enum ShadowFilteringMethod { } /// The [`VisibilityClass`] used for all lights (point, directional, and spot). +/// +/// [`VisibilityClass`]: bevy_camera::visibility::VisibilityClass pub struct LightVisibilityClass; /// System sets used to run light-related systems. @@ -543,138 +264,6 @@ pub enum SimulationLightSystems { CheckLightVisibility, } -pub fn update_directional_light_frusta( - mut views: Query< - ( - &Cascades, - &DirectionalLight, - &ViewVisibility, - &mut CascadesFrusta, - ), - ( - // Prevents this query from conflicting with camera queries. - Without, - ), - >, -) { - for (cascades, directional_light, visibility, mut frusta) in &mut views { - // The frustum is used for culling meshes to the light for shadow mapping - // so if shadow mapping is disabled for this light, then the frustum is - // not needed. - if !directional_light.shadows_enabled || !visibility.get() { - continue; - } - - frusta.frusta = cascades - .cascades - .iter() - .map(|(view, cascades)| { - ( - *view, - cascades - .iter() - .map(|c| Frustum::from_clip_from_world(&c.clip_from_world)) - .collect::>(), - ) - }) - .collect(); - } -} - -// NOTE: Run this after assign_lights_to_clusters! -pub fn update_point_light_frusta( - global_lights: Res, - mut views: Query<(Entity, &GlobalTransform, &PointLight, &mut CubemapFrusta)>, - changed_lights: Query< - Entity, - ( - With, - Or<(Changed, Changed)>, - ), - >, -) { - let view_rotations = CUBE_MAP_FACES - .iter() - .map(|CubeMapFace { target, up }| Transform::IDENTITY.looking_at(*target, *up)) - .collect::>(); - - for (entity, transform, point_light, mut cubemap_frusta) in &mut views { - // If this light hasn't changed, and neither has the set of global_lights, - // then we can skip this calculation. - if !global_lights.is_changed() && !changed_lights.contains(entity) { - continue; - } - - // The frusta are used for culling meshes to the light for shadow mapping - // so if shadow mapping is disabled for this light, then the frusta are - // not needed. - // Also, if the light is not relevant for any cluster, it will not be in the - // global lights set and so there is no need to update its frusta. - if !point_light.shadows_enabled || !global_lights.entities.contains(&entity) { - continue; - } - - let clip_from_view = Mat4::perspective_infinite_reverse_rh( - core::f32::consts::FRAC_PI_2, - 1.0, - point_light.shadow_map_near_z, - ); - - // ignore scale because we don't want to effectively scale light radius and range - // by applying those as a view transform to shadow map rendering of objects - // and ignore rotation because we want the shadow map projections to align with the axes - let view_translation = Transform::from_translation(transform.translation()); - let view_backward = transform.back(); - - for (view_rotation, frustum) in view_rotations.iter().zip(cubemap_frusta.iter_mut()) { - let world_from_view = view_translation * *view_rotation; - let clip_from_world = clip_from_view * world_from_view.to_matrix().inverse(); - - *frustum = Frustum::from_clip_from_world_custom_far( - &clip_from_world, - &transform.translation(), - &view_backward, - point_light.range, - ); - } - } -} - -pub fn update_spot_light_frusta( - global_lights: Res, - mut views: Query< - (Entity, &GlobalTransform, &SpotLight, &mut Frustum), - Or<(Changed, Changed)>, - >, -) { - for (entity, transform, spot_light, mut frustum) in &mut views { - // The frusta are used for culling meshes to the light for shadow mapping - // so if shadow mapping is disabled for this light, then the frusta are - // not needed. - // Also, if the light is not relevant for any cluster, it will not be in the - // global lights set and so there is no need to update its frusta. - if !spot_light.shadows_enabled || !global_lights.entities.contains(&entity) { - continue; - } - - // ignore scale because we don't want to effectively scale light radius and range - // by applying those as a view transform to shadow map rendering of objects - let view_backward = transform.back(); - - let spot_world_from_view = spot_light_world_from_view(transform); - let spot_clip_from_view = - spot_light_clip_from_view(spot_light.outer_angle, spot_light.shadow_map_near_z); - let clip_from_world = spot_clip_from_view * spot_world_from_view.inverse(); - - *frustum = Frustum::from_clip_from_world_custom_far( - &clip_from_world, - &transform.translation(), - &view_backward, - spot_light.range, - ); - } -} - fn shrink_entities(visible_entities: &mut Vec) { // Check that visible entities capacity() is no more than two times greater than len() let capacity = visible_entities.capacity(); diff --git a/crates/bevy_pbr/src/light/point_light.rs b/crates/bevy_pbr/src/light/point_light.rs index c977d0be33..8ba108adcc 100644 --- a/crates/bevy_pbr/src/light/point_light.rs +++ b/crates/bevy_pbr/src/light/point_light.rs @@ -1,8 +1,16 @@ -use bevy_render::view::{self, Visibility}; +use bevy_asset::Handle; +use bevy_camera::{ + primitives::{CubeMapFace, CubemapFrusta, CubemapLayout, Frustum, CUBE_MAP_FACES}, + visibility::{self, CubemapVisibleEntities, Visibility, VisibilityClass}, +}; +use bevy_color::Color; +use bevy_ecs::prelude::*; +use bevy_image::Image; +use bevy_math::Mat4; +use bevy_reflect::prelude::*; +use bevy_transform::components::{GlobalTransform, Transform}; -use crate::decal::clustered::CubemapLayout; - -use super::*; +use crate::{GlobalVisibleClusterableObjects, LightVisibilityClass}; /// A light that emits light in all directions from a central point. /// @@ -36,7 +44,7 @@ use super::*; Visibility, VisibilityClass )] -#[component(on_add = view::add_visibility_class::)] +#[component(on_add = visibility::add_visibility_class::)] pub struct PointLight { /// The color of this light source. pub color: Color, @@ -76,6 +84,8 @@ pub struct PointLight { /// /// Note that soft shadows are significantly more expensive to render than /// hard shadows. + /// + /// [`ShadowFilteringMethod::Temporal`]: crate::ShadowFilteringMethod::Temporal #[cfg(feature = "experimental_pbr_pcss")] pub soft_shadows_enabled: bool, @@ -151,3 +161,85 @@ pub struct PointLightTexture { /// The cubemap layout. The image should be a packed cubemap in one of the formats described by the [`CubemapLayout`] enum. pub cubemap_layout: CubemapLayout, } + +/// Controls the resolution of [`PointLight`] shadow maps. +/// +/// ``` +/// # use bevy_app::prelude::*; +/// # use bevy_pbr::PointLightShadowMap; +/// App::new() +/// .insert_resource(PointLightShadowMap { size: 2048 }); +/// ``` +#[derive(Resource, Clone, Debug, Reflect)] +#[reflect(Resource, Debug, Default, Clone)] +pub struct PointLightShadowMap { + /// The width and height of each of the 6 faces of the cubemap. + /// + /// Defaults to `1024`. + pub size: usize, +} + +impl Default for PointLightShadowMap { + fn default() -> Self { + Self { size: 1024 } + } +} + +// NOTE: Run this after assign_lights_to_clusters! +pub fn update_point_light_frusta( + global_lights: Res, + mut views: Query<(Entity, &GlobalTransform, &PointLight, &mut CubemapFrusta)>, + changed_lights: Query< + Entity, + ( + With, + Or<(Changed, Changed)>, + ), + >, +) { + let view_rotations = CUBE_MAP_FACES + .iter() + .map(|CubeMapFace { target, up }| Transform::IDENTITY.looking_at(*target, *up)) + .collect::>(); + + for (entity, transform, point_light, mut cubemap_frusta) in &mut views { + // If this light hasn't changed, and neither has the set of global_lights, + // then we can skip this calculation. + if !global_lights.is_changed() && !changed_lights.contains(entity) { + continue; + } + + // The frusta are used for culling meshes to the light for shadow mapping + // so if shadow mapping is disabled for this light, then the frusta are + // not needed. + // Also, if the light is not relevant for any cluster, it will not be in the + // global lights set and so there is no need to update its frusta. + if !point_light.shadows_enabled || !global_lights.entities.contains(&entity) { + continue; + } + + let clip_from_view = Mat4::perspective_infinite_reverse_rh( + core::f32::consts::FRAC_PI_2, + 1.0, + point_light.shadow_map_near_z, + ); + + // ignore scale because we don't want to effectively scale light radius and range + // by applying those as a view transform to shadow map rendering of objects + // and ignore rotation because we want the shadow map projections to align with the axes + let view_translation = Transform::from_translation(transform.translation()); + let view_backward = transform.back(); + + for (view_rotation, frustum) in view_rotations.iter().zip(cubemap_frusta.iter_mut()) { + let world_from_view = view_translation * *view_rotation; + let clip_from_world = clip_from_view * world_from_view.to_matrix().inverse(); + + *frustum = Frustum::from_clip_from_world_custom_far( + &clip_from_world, + &transform.translation(), + &view_backward, + point_light.range, + ); + } + } +} diff --git a/crates/bevy_pbr/src/light/spot_light.rs b/crates/bevy_pbr/src/light/spot_light.rs index 393e9efc0c..0e09b1c509 100644 --- a/crates/bevy_pbr/src/light/spot_light.rs +++ b/crates/bevy_pbr/src/light/spot_light.rs @@ -1,6 +1,17 @@ -use bevy_render::view::{self, Visibility}; +use bevy_asset::Handle; +use bevy_camera::{ + primitives::Frustum, + visibility::{self, Visibility, VisibilityClass}, +}; +use bevy_color::Color; +use bevy_ecs::prelude::*; +use bevy_image::Image; +use bevy_math::{Mat4, Vec4}; +use bevy_reflect::prelude::*; +use bevy_render::view::VisibleMeshEntities; +use bevy_transform::components::{GlobalTransform, Transform}; -use super::*; +use crate::{GlobalVisibleClusterableObjects, LightVisibilityClass}; /// A light that emits light in a given direction from a central point. /// @@ -10,7 +21,7 @@ use super::*; #[derive(Component, Debug, Clone, Copy, Reflect)] #[reflect(Component, Default, Debug, Clone)] #[require(Frustum, VisibleMeshEntities, Transform, Visibility, VisibilityClass)] -#[component(on_add = view::add_visibility_class::)] +#[component(on_add = visibility::add_visibility_class::)] pub struct SpotLight { /// The color of the light. /// @@ -58,6 +69,8 @@ pub struct SpotLight { /// /// Note that soft shadows are significantly more expensive to render than /// hard shadows. + /// + /// [`ShadowFilteringMethod::Temporal`]: crate::ShadowFilteringMethod::Temporal #[cfg(feature = "experimental_pbr_pcss")] pub soft_shadows_enabled: bool, @@ -184,3 +197,38 @@ pub struct SpotLightTexture { /// Note the border of the image should be entirely black to avoid leaking light. pub image: Handle, } + +pub fn update_spot_light_frusta( + global_lights: Res, + mut views: Query< + (Entity, &GlobalTransform, &SpotLight, &mut Frustum), + Or<(Changed, Changed)>, + >, +) { + for (entity, transform, spot_light, mut frustum) in &mut views { + // The frusta are used for culling meshes to the light for shadow mapping + // so if shadow mapping is disabled for this light, then the frusta are + // not needed. + // Also, if the light is not relevant for any cluster, it will not be in the + // global lights set and so there is no need to update its frusta. + if !spot_light.shadows_enabled || !global_lights.entities.contains(&entity) { + continue; + } + + // ignore scale because we don't want to effectively scale light radius and range + // by applying those as a view transform to shadow map rendering of objects + let view_backward = transform.back(); + + let spot_world_from_view = spot_light_world_from_view(transform); + let spot_clip_from_view = + spot_light_clip_from_view(spot_light.outer_angle, spot_light.shadow_map_near_z); + let clip_from_world = spot_clip_from_view * spot_world_from_view.inverse(); + + *frustum = Frustum::from_clip_from_world_custom_far( + &clip_from_world, + &transform.translation(), + &view_backward, + spot_light.range, + ); + } +} diff --git a/crates/bevy_pbr/src/render/light.rs b/crates/bevy_pbr/src/render/light.rs index 1f36d45fd6..883147acc3 100644 --- a/crates/bevy_pbr/src/render/light.rs +++ b/crates/bevy_pbr/src/render/light.rs @@ -1,5 +1,6 @@ use self::assign::ClusterableObjectType; use crate::assign::calculate_cluster_factors; +use crate::cascade::{Cascade, CascadeShadowConfig, Cascades}; use crate::*; use bevy_asset::UntypedAssetId; pub use bevy_camera::primitives::{face_index_to_name, CubeMapFace, CUBE_MAP_FACES};