prepare bevy_light for split (#19965)
# Objective - prepare bevy_light for split ## Solution - extract cascade module (this is not strictly necessary for bevy_light) - clean up imports to be less globby and tangled - move light specific stuff into light modules - move light system and type init from pbr into new LightPlugin ## Testing - 3d_scene, lighting NOTE TO REVIEWERS: it may help to review commits independently.
This commit is contained in:
parent
0b771d9f59
commit
dd57db44d9
@ -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::<StandardMaterial>()
|
||||
.register_type::<AmbientLight>()
|
||||
.register_type::<CascadeShadowConfig>()
|
||||
.register_type::<Cascades>()
|
||||
.register_type::<ClusterConfig>()
|
||||
.register_type::<DirectionalLight>()
|
||||
.register_type::<DirectionalLightShadowMap>()
|
||||
.register_type::<NotShadowCaster>()
|
||||
.register_type::<NotShadowReceiver>()
|
||||
.register_type::<PointLight>()
|
||||
.register_type::<PointLightShadowMap>()
|
||||
.register_type::<SpotLight>()
|
||||
.register_type::<ShadowFilteringMethod>()
|
||||
.init_resource::<AmbientLight>()
|
||||
.init_resource::<GlobalVisibleClusterableObjects>()
|
||||
.init_resource::<DirectionalLightShadowMap>()
|
||||
.init_resource::<PointLightShadowMap>()
|
||||
.register_type::<DefaultOpaqueRendererMethod>()
|
||||
.init_resource::<DefaultOpaqueRendererMethod>()
|
||||
.add_plugins((
|
||||
@ -243,7 +227,7 @@ impl Plugin for PbrPlugin {
|
||||
ExtractComponentPlugin::<ShadowFilteringMethod>::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),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -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)]
|
||||
|
333
crates/bevy_pbr/src/light/cascade.rs
Normal file
333
crates/bevy_pbr/src/light/cascade.rs
Normal file
@ -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<f32>,
|
||||
/// 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<f32> {
|
||||
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<CascadeShadowConfigBuilder> 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<Vec<Cascade>>,
|
||||
}
|
||||
|
||||
#[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<DirectionalLightShadowMap>,
|
||||
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::<Vec<_>>();
|
||||
|
||||
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,
|
||||
}
|
||||
}
|
@ -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::<LightVisibilityClass>)]
|
||||
#[component(on_add = visibility::add_visibility_class::<LightVisibilityClass>)]
|
||||
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<f32>,
|
||||
|
||||
@ -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<Camera>,
|
||||
),
|
||||
>,
|
||||
) {
|
||||
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::<Vec<_>>(),
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
}
|
||||
}
|
||||
|
@ -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::<AmbientLight>()
|
||||
.register_type::<CascadeShadowConfig>()
|
||||
.register_type::<Cascades>()
|
||||
.register_type::<DirectionalLight>()
|
||||
.register_type::<DirectionalLightShadowMap>()
|
||||
.register_type::<NotShadowCaster>()
|
||||
.register_type::<NotShadowReceiver>()
|
||||
.register_type::<PointLight>()
|
||||
.register_type::<PointLightShadowMap>()
|
||||
.register_type::<SpotLight>()
|
||||
.register_type::<ShadowFilteringMethod>()
|
||||
.init_resource::<AmbientLight>()
|
||||
.init_resource::<DirectionalLightShadowMap>()
|
||||
.init_resource::<PointLightShadowMap>()
|
||||
.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<DirectionalLight>)>`, for use with [`bevy_render::view::VisibleEntities`].
|
||||
pub type WithLight = Or<(With<PointLight>, With<SpotLight>, With<DirectionalLight>)>;
|
||||
|
||||
/// 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<f32>,
|
||||
/// 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<f32> {
|
||||
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<CascadeShadowConfigBuilder> 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<Vec<Cascade>>,
|
||||
}
|
||||
|
||||
#[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<DirectionalLightShadowMap>,
|
||||
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::<Vec<_>>();
|
||||
|
||||
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<Camera>,
|
||||
),
|
||||
>,
|
||||
) {
|
||||
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::<Vec<_>>(),
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
}
|
||||
}
|
||||
|
||||
// NOTE: Run this after assign_lights_to_clusters!
|
||||
pub fn update_point_light_frusta(
|
||||
global_lights: Res<GlobalVisibleClusterableObjects>,
|
||||
mut views: Query<(Entity, &GlobalTransform, &PointLight, &mut CubemapFrusta)>,
|
||||
changed_lights: Query<
|
||||
Entity,
|
||||
(
|
||||
With<PointLight>,
|
||||
Or<(Changed<GlobalTransform>, Changed<PointLight>)>,
|
||||
),
|
||||
>,
|
||||
) {
|
||||
let view_rotations = CUBE_MAP_FACES
|
||||
.iter()
|
||||
.map(|CubeMapFace { target, up }| Transform::IDENTITY.looking_at(*target, *up))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
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<GlobalVisibleClusterableObjects>,
|
||||
mut views: Query<
|
||||
(Entity, &GlobalTransform, &SpotLight, &mut Frustum),
|
||||
Or<(Changed<GlobalTransform>, Changed<SpotLight>)>,
|
||||
>,
|
||||
) {
|
||||
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<Entity>) {
|
||||
// Check that visible entities capacity() is no more than two times greater than len()
|
||||
let capacity = visible_entities.capacity();
|
||||
|
@ -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::<LightVisibilityClass>)]
|
||||
#[component(on_add = visibility::add_visibility_class::<LightVisibilityClass>)]
|
||||
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<GlobalVisibleClusterableObjects>,
|
||||
mut views: Query<(Entity, &GlobalTransform, &PointLight, &mut CubemapFrusta)>,
|
||||
changed_lights: Query<
|
||||
Entity,
|
||||
(
|
||||
With<PointLight>,
|
||||
Or<(Changed<GlobalTransform>, Changed<PointLight>)>,
|
||||
),
|
||||
>,
|
||||
) {
|
||||
let view_rotations = CUBE_MAP_FACES
|
||||
.iter()
|
||||
.map(|CubeMapFace { target, up }| Transform::IDENTITY.looking_at(*target, *up))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
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,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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::<LightVisibilityClass>)]
|
||||
#[component(on_add = visibility::add_visibility_class::<LightVisibilityClass>)]
|
||||
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<Image>,
|
||||
}
|
||||
|
||||
pub fn update_spot_light_frusta(
|
||||
global_lights: Res<GlobalVisibleClusterableObjects>,
|
||||
mut views: Query<
|
||||
(Entity, &GlobalTransform, &SpotLight, &mut Frustum),
|
||||
Or<(Changed<GlobalTransform>, Changed<SpotLight>)>,
|
||||
>,
|
||||
) {
|
||||
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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -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};
|
||||
|
Loading…
Reference in New Issue
Block a user