Cascaded shadow maps. (#7064)

Co-authored-by: Robert Swain <robert.swain@gmail.com>

# Objective

Implements cascaded shadow maps for directional lights, which produces better quality shadows without needing excessively large shadow maps.

Fixes #3629

Before
![image](https://user-images.githubusercontent.com/1222141/210061203-bbd965a4-8d11-4cec-9a88-67fc59d0819f.png)

After
![image](https://user-images.githubusercontent.com/1222141/210061334-2ff15334-e6d7-4a31-9314-f34a7805cac6.png)


## Solution

Rather than rendering a single shadow map for directional light, the view frustum is divided into a series of cascades, each of which gets its own shadow map. The correct cascade is then sampled for shadow determination.

---

## Changelog

Directional lights now use cascaded shadow maps for improved shadow quality.


## Migration Guide

You no longer have to manually specify a `shadow_projection` for a directional light, and these settings should be removed. If customization of how cascaded shadow maps work is desired, modify the `CascadeShadowConfig` component instead.
This commit is contained in:
Daniel Chia 2023-01-25 12:35:39 +00:00
parent f27e9b241d
commit c3a46822e1
23 changed files with 672 additions and 284 deletions

View File

@ -61,7 +61,7 @@ impl Camera2dBundle {
let transform = Transform::from_xyz(0.0, 0.0, far - 0.1); let transform = Transform::from_xyz(0.0, 0.0, far - 0.1);
let view_projection = let view_projection =
projection.get_projection_matrix() * transform.compute_matrix().inverse(); projection.get_projection_matrix() * transform.compute_matrix().inverse();
let frustum = Frustum::from_view_projection( let frustum = Frustum::from_view_projection_custom_far(
&view_projection, &view_projection,
&transform.translation, &transform.translation,
&transform.back(), &transform.back(),

View File

@ -1,13 +1,17 @@
use crate::{DirectionalLight, Material, PointLight, SpotLight, StandardMaterial}; use crate::{
CascadeShadowConfig, Cascades, DirectionalLight, Material, PointLight, SpotLight,
StandardMaterial,
};
use bevy_asset::Handle; use bevy_asset::Handle;
use bevy_ecs::{bundle::Bundle, component::Component, reflect::ReflectComponent}; use bevy_ecs::{bundle::Bundle, component::Component, prelude::Entity, reflect::ReflectComponent};
use bevy_reflect::Reflect; use bevy_reflect::Reflect;
use bevy_render::{ use bevy_render::{
mesh::Mesh, mesh::Mesh,
primitives::{CubemapFrusta, Frustum}, primitives::{CascadesFrusta, CubemapFrusta, Frustum},
view::{ComputedVisibility, Visibility, VisibleEntities}, view::{ComputedVisibility, Visibility, VisibleEntities},
}; };
use bevy_transform::components::{GlobalTransform, Transform}; use bevy_transform::components::{GlobalTransform, Transform};
use bevy_utils::HashMap;
/// A component bundle for PBR entities with a [`Mesh`] and a [`StandardMaterial`]. /// A component bundle for PBR entities with a [`Mesh`] and a [`StandardMaterial`].
pub type PbrBundle = MaterialMeshBundle<StandardMaterial>; pub type PbrBundle = MaterialMeshBundle<StandardMaterial>;
@ -63,6 +67,14 @@ impl CubemapVisibleEntities {
} }
} }
#[derive(Component, Clone, Debug, Default, Reflect)]
#[reflect(Component)]
pub struct CascadesVisibleEntities {
/// Map of view entity to the visible entities for each cascade frustum.
#[reflect(ignore)]
pub entities: HashMap<Entity, Vec<VisibleEntities>>,
}
/// A component bundle for [`PointLight`] entities. /// A component bundle for [`PointLight`] entities.
#[derive(Debug, Bundle, Default)] #[derive(Debug, Bundle, Default)]
pub struct PointLightBundle { pub struct PointLightBundle {
@ -95,8 +107,10 @@ pub struct SpotLightBundle {
#[derive(Debug, Bundle, Default)] #[derive(Debug, Bundle, Default)]
pub struct DirectionalLightBundle { pub struct DirectionalLightBundle {
pub directional_light: DirectionalLight, pub directional_light: DirectionalLight,
pub frustum: Frustum, pub frusta: CascadesFrusta,
pub visible_entities: VisibleEntities, pub cascades: Cascades,
pub cascade_shadow_config: CascadeShadowConfig,
pub visible_entities: CascadesVisibleEntities,
pub transform: Transform, pub transform: Transform,
pub global_transform: GlobalTransform, pub global_transform: GlobalTransform,
/// Enables or disables the light /// Enables or disables the light

View File

@ -145,17 +145,20 @@ impl Plugin for PbrPlugin {
Shader::from_wgsl Shader::from_wgsl
); );
app.register_type::<CubemapVisibleEntities>() app.register_asset_reflect::<StandardMaterial>()
.register_type::<DirectionalLight>()
.register_type::<PointLight>()
.register_type::<SpotLight>()
.register_asset_reflect::<StandardMaterial>()
.register_type::<AmbientLight>() .register_type::<AmbientLight>()
.register_type::<DirectionalLightShadowMap>() .register_type::<CascadeShadowConfig>()
.register_type::<Cascades>()
.register_type::<CascadesVisibleEntities>()
.register_type::<ClusterConfig>() .register_type::<ClusterConfig>()
.register_type::<ClusterZConfig>()
.register_type::<ClusterFarZMode>() .register_type::<ClusterFarZMode>()
.register_type::<ClusterZConfig>()
.register_type::<CubemapVisibleEntities>()
.register_type::<DirectionalLight>()
.register_type::<DirectionalLightShadowMap>()
.register_type::<PointLight>()
.register_type::<PointLightShadowMap>() .register_type::<PointLightShadowMap>()
.register_type::<SpotLight>()
.add_plugin(MeshRenderPlugin) .add_plugin(MeshRenderPlugin)
.add_plugin(MaterialPlugin::<StandardMaterial> { .add_plugin(MaterialPlugin::<StandardMaterial> {
prepass_enabled: self.prepass_enabled, prepass_enabled: self.prepass_enabled,
@ -183,6 +186,12 @@ impl Plugin for PbrPlugin {
.after(CameraUpdateSystem) .after(CameraUpdateSystem)
.after(ModifiesWindows), .after(ModifiesWindows),
) )
.add_system_to_stage(
CoreStage::PostUpdate,
update_directional_light_cascades
.label(SimulationLightSystems::UpdateDirectionalLightCascades)
.after(TransformSystem::TransformPropagate),
)
.add_system_to_stage( .add_system_to_stage(
CoreStage::PostUpdate, CoreStage::PostUpdate,
update_directional_light_frusta update_directional_light_frusta
@ -190,6 +199,7 @@ impl Plugin for PbrPlugin {
// This must run after CheckVisibility because it relies on ComputedVisibility::is_visible() // This must run after CheckVisibility because it relies on ComputedVisibility::is_visible()
.after(VisibilitySystems::CheckVisibility) .after(VisibilitySystems::CheckVisibility)
.after(TransformSystem::TransformPropagate) .after(TransformSystem::TransformPropagate)
.after(SimulationLightSystems::UpdateDirectionalLightCascades)
// We assume that no entity will be both a directional light and a spot light, // We assume that no entity will be both a directional light and a spot light,
// so these systems will run independently of one another. // so these systems will run independently of one another.
// FIXME: Add an archetype invariant for this https://github.com/bevyengine/bevy/issues/1481. // FIXME: Add an archetype invariant for this https://github.com/bevyengine/bevy/issues/1481.

View File

@ -4,21 +4,23 @@ use bevy_ecs::prelude::*;
use bevy_math::{Mat4, UVec2, UVec3, Vec2, Vec3, Vec3A, Vec3Swizzles, Vec4, Vec4Swizzles}; use bevy_math::{Mat4, UVec2, UVec3, Vec2, Vec3, Vec3A, Vec3Swizzles, Vec4, Vec4Swizzles};
use bevy_reflect::prelude::*; use bevy_reflect::prelude::*;
use bevy_render::{ use bevy_render::{
camera::{Camera, CameraProjection, OrthographicProjection}, camera::Camera,
color::Color, color::Color,
extract_resource::ExtractResource, extract_resource::ExtractResource,
primitives::{Aabb, CubemapFrusta, Frustum, Plane, Sphere}, prelude::Projection,
primitives::{Aabb, CascadesFrusta, CubemapFrusta, Frustum, Plane, Sphere},
render_resource::BufferBindingType, render_resource::BufferBindingType,
renderer::RenderDevice, renderer::RenderDevice,
view::{ComputedVisibility, RenderLayers, VisibleEntities}, view::{ComputedVisibility, RenderLayers, VisibleEntities},
}; };
use bevy_transform::{components::GlobalTransform, prelude::Transform}; use bevy_transform::{components::GlobalTransform, prelude::Transform};
use bevy_utils::tracing::warn; use bevy_utils::{tracing::warn, HashMap};
use crate::{ use crate::{
calculate_cluster_factors, spot_light_projection_matrix, spot_light_view_matrix, CubeMapFace, calculate_cluster_factors, spot_light_projection_matrix, spot_light_view_matrix,
CubemapVisibleEntities, ViewClusterBindings, CLUSTERED_FORWARD_STORAGE_BUFFER_COUNT, CascadesVisibleEntities, CubeMapFace, CubemapVisibleEntities, ViewClusterBindings,
CUBE_MAP_FACES, MAX_UNIFORM_BUFFER_POINT_LIGHTS, POINT_LIGHT_NEAR_Z, CLUSTERED_FORWARD_STORAGE_BUFFER_COUNT, CUBE_MAP_FACES, MAX_UNIFORM_BUFFER_POINT_LIGHTS,
POINT_LIGHT_NEAR_Z,
}; };
/// A light that emits light in all directions from a central point. /// A light that emits light in all directions from a central point.
@ -172,24 +174,11 @@ impl Default for SpotLight {
/// ///
/// To enable shadows, set the `shadows_enabled` property to `true`. /// To enable shadows, set the `shadows_enabled` property to `true`.
/// ///
/// While directional lights contribute to the illumination of meshes regardless /// Shadows are produced via [cascaded shadow maps](https://developer.download.nvidia.com/SDK/10.5/opengl/src/cascaded_shadow_maps/doc/cascaded_shadow_maps.pdf).
/// of their (or the meshes') positions, currently only a limited region of the scene
/// (the _shadow volume_) can cast and receive shadows for any given directional light.
/// ///
/// The shadow volume is a _rectangular cuboid_, with left/right/bottom/top/near/far /// To modify the cascade set up, such as the number of cascades or the maximum shadow distance,
/// planes controllable via the `shadow_projection` field. It is affected by the /// change the [`CascadeShadowConfig`] component of the [`crate::bundle::DirectionalLightBundle`].
/// directional light entity's [`GlobalTransform`], and as such can be freely repositioned in the
/// scene, (or even scaled!) without affecting illumination in any other way, by simply
/// moving (or scaling) the entity around. The shadow volume is always oriented towards the
/// light entity's forward direction.
/// ///
/// For smaller scenes, a static directional light with a preset volume is typically
/// sufficient. For larger scenes with movable cameras, you might want to introduce
/// a system that dynamically repositions and scales the light entity (and therefore
/// its shadow volume) based on the scene subject's position (e.g. a player character)
/// and its relative distance to the camera.
///
/// Shadows are produced via [shadow mapping](https://en.wikipedia.org/wiki/Shadow_mapping).
/// To control the resolution of the shadow maps, use the [`DirectionalLightShadowMap`] resource: /// To control the resolution of the shadow maps, use the [`DirectionalLightShadowMap`] resource:
/// ///
/// ``` /// ```
@ -198,12 +187,6 @@ impl Default for SpotLight {
/// App::new() /// App::new()
/// .insert_resource(DirectionalLightShadowMap { size: 2048 }); /// .insert_resource(DirectionalLightShadowMap { size: 2048 });
/// ``` /// ```
///
/// **Note:** Very large shadow map resolutions (> 4K) can have non-negligible performance and
/// memory impact, and not work properly under mobile or lower-end hardware. To improve the visual
/// fidelity of shadow maps, it's typically advisable to first reduce the `shadow_projection`
/// left/right/top/bottom to a scene-appropriate size, before ramping up the shadow map
/// resolution.
#[derive(Component, Debug, Clone, Reflect)] #[derive(Component, Debug, Clone, Reflect)]
#[reflect(Component, Default)] #[reflect(Component, Default)]
pub struct DirectionalLight { pub struct DirectionalLight {
@ -211,8 +194,6 @@ pub struct DirectionalLight {
/// Illuminance in lux /// Illuminance in lux
pub illuminance: f32, pub illuminance: f32,
pub shadows_enabled: bool, pub shadows_enabled: bool,
/// A projection that controls the volume in which shadow maps are rendered
pub shadow_projection: OrthographicProjection,
pub shadow_depth_bias: f32, pub shadow_depth_bias: f32,
/// A bias applied along the direction of the fragment's surface normal. It is scaled to the /// A bias applied along the direction of the fragment's surface normal. It is scaled to the
/// shadow map's texel size so that it is automatically adjusted to the orthographic projection. /// shadow map's texel size so that it is automatically adjusted to the orthographic projection.
@ -221,20 +202,10 @@ pub struct DirectionalLight {
impl Default for DirectionalLight { impl Default for DirectionalLight {
fn default() -> Self { fn default() -> Self {
let size = 100.0;
DirectionalLight { DirectionalLight {
color: Color::rgb(1.0, 1.0, 1.0), color: Color::rgb(1.0, 1.0, 1.0),
illuminance: 100000.0, illuminance: 100000.0,
shadows_enabled: false, shadows_enabled: false,
shadow_projection: OrthographicProjection {
left: -size,
right: size,
bottom: -size,
top: size,
near: -size,
far: size,
..Default::default()
},
shadow_depth_bias: Self::DEFAULT_SHADOW_DEPTH_BIAS, shadow_depth_bias: Self::DEFAULT_SHADOW_DEPTH_BIAS,
shadow_normal_bias: Self::DEFAULT_SHADOW_NORMAL_BIAS, shadow_normal_bias: Self::DEFAULT_SHADOW_NORMAL_BIAS,
} }
@ -256,9 +227,258 @@ pub struct DirectionalLightShadowMap {
impl Default for DirectionalLightShadowMap { impl Default for DirectionalLightShadowMap {
fn default() -> Self { fn default() -> Self {
#[cfg(feature = "webgl")] #[cfg(feature = "webgl")]
return Self { size: 2048 }; return Self { size: 1024 };
#[cfg(not(feature = "webgl"))] #[cfg(not(feature = "webgl"))]
return Self { size: 4096 }; return Self { size: 2048 };
}
}
/// Controls how cascaded shadow mapping works.
#[derive(Component, Clone, Debug, Reflect)]
#[reflect(Component)]
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,
}
impl Default for CascadeShadowConfig {
fn default() -> Self {
if cfg!(feature = "webgl") {
// Currently only support one cascade in webgl.
Self::new(1, 5.0, 100.0, 0.2)
} else {
Self::new(4, 5.0, 1000.0, 0.2)
}
}
}
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 = (shadow_maximum_distance / nearest_bound).powf(1.0 / (num_cascades - 1) as f32);
(0..num_cascades)
.map(|i| nearest_bound * base.powf(i as f32))
.collect()
}
impl CascadeShadowConfig {
/// Returns a cascade config for `num_cascades` cascades, with the first cascade
/// having far bound `nearest_bound` and the last cascade having far bound `shadow_maximum_distance`.
/// In-between cascades will be exponentially spaced.
pub fn new(
num_cascades: usize,
nearest_bound: f32,
shadow_maximum_distance: f32,
overlap_proportion: f32,
) -> Self {
assert!(
num_cascades > 0,
"num_cascades must be positive, but was {}",
num_cascades
);
assert!(
(0.0..1.0).contains(&overlap_proportion),
"overlap_proportion must be in [0.0, 1.0) but was {}",
overlap_proportion
);
Self {
bounds: calculate_cascade_bounds(num_cascades, nearest_bound, shadow_maximum_distance),
overlap_proportion,
}
}
}
#[derive(Component, Clone, Debug, Default, Reflect)]
#[reflect(Component)]
pub struct Cascades {
/// Map from a view to the configuration of each of its [`Cascade`]s.
pub(crate) cascades: HashMap<Entity, Vec<Cascade>>,
}
#[derive(Clone, Debug, Default, Reflect, FromReflect)]
pub struct Cascade {
/// The transform of the light, i.e. the view to world matrix.
pub(crate) view_transform: Mat4,
/// The orthographic projection for this cascade.
pub(crate) projection: Mat4,
/// The view-projection matrix for this cacade, converting world space into light clip space.
/// Importantly, this is derived and stored separately from `view_transform` and `projection` to
/// ensure shadow stability.
pub(crate) view_projection: Mat4,
/// Size of each shadow map texel in world units.
pub(crate) texel_size: f32,
}
pub fn update_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(|view| match view {
// TODO: orthographic camera projection support.
(entity, transform, Projection::Perspective(projection), camera)
if camera.is_active =>
{
Some((
entity,
projection.aspect_ratio,
(0.5 * projection.fov).tan(),
transform.compute_matrix(),
))
}
_ => None,
})
.collect::<Vec<_>>();
for (transform, directional_light, cascades_config, mut cascades) in lights.iter_mut() {
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.compute_matrix()` will give us a matrix with our desired properties.
// Instead, we directly create a good matrix from just the rotation.
let light_to_world = Mat4::from_quat(transform.compute_transform().rotation);
let light_to_world_inverse = light_to_world.inverse();
cascades.cascades.clear();
for (view_entity, aspect_ratio, tan_half_fov, 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)| {
calculate_cascade(
aspect_ratio,
tan_half_fov,
directional_light_shadow_map.size as f32,
light_to_world,
camera_to_light_view,
// Negate bounds as -z is camera forward direction.
if idx > 0 {
(1.0 - cascades_config.overlap_proportion)
* -cascades_config.bounds[idx - 1]
} else {
0.0
},
-far_bound,
)
})
.collect();
cascades.cascades.insert(view_entity, view_cascades);
}
}
}
fn calculate_cascade(
aspect_ratio: f32,
tan_half_fov: f32,
cascade_texture_size: f32,
light_to_world: Mat4,
camera_to_light: Mat4,
z_near: f32,
z_far: f32,
) -> Cascade {
debug_assert!(z_near <= 0.0, "z_near {} must be <= 0.0", z_near);
debug_assert!(z_far <= 0.0, "z_far {} must be <= 0.0", z_far);
// NOTE: This whole function is very sensitive to floating point precision and instability and
// has followed instructions to avoid view dependence from the section on cascade shadow maps in
// Eric Lengyel's Foundations of Game Engine Development 2: Rendering. Be very careful when
// modifying this code!
let a = z_near.abs() * tan_half_fov;
let b = z_far.abs() * tan_half_fov;
// NOTE: These vertices are in a specific order: bottom right, top right, top left, bottom left
// for near then for far
let frustum_corners = [
Vec3A::new(a * aspect_ratio, -a, z_near),
Vec3A::new(a * aspect_ratio, a, z_near),
Vec3A::new(-a * aspect_ratio, a, z_near),
Vec3A::new(-a * aspect_ratio, -a, z_near),
Vec3A::new(b * aspect_ratio, -b, z_far),
Vec3A::new(b * aspect_ratio, b, z_far),
Vec3A::new(-b * aspect_ratio, b, z_far),
Vec3A::new(-b * aspect_ratio, -b, z_far),
];
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 = camera_to_light.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_cascde` as the reference material suggests.
let light_to_world_transpose = light_to_world.transpose();
let world_to_cascade = 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 cascade_projection = 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 cascade_view_projection = cascade_projection * world_to_cascade;
Cascade {
view_transform: world_to_cascade.inverse(),
projection: cascade_projection,
view_projection: cascade_view_projection,
texel_size: cascade_texel_size,
} }
} }
@ -293,6 +513,7 @@ pub struct NotShadowReceiver;
pub enum SimulationLightSystems { pub enum SimulationLightSystems {
AddClusters, AddClusters,
AssignLightsToClusters, AssignLightsToClusters,
UpdateDirectionalLightCascades,
UpdateLightFrusta, UpdateLightFrusta,
CheckLightVisibility, CheckLightVisibility,
} }
@ -1462,19 +1683,18 @@ fn project_to_plane_y(y_light: Sphere, y_plane: Plane, is_orthographic: bool) ->
pub fn update_directional_light_frusta( pub fn update_directional_light_frusta(
mut views: Query< mut views: Query<
( (
&GlobalTransform, &Cascades,
&DirectionalLight, &DirectionalLight,
&mut Frustum,
&ComputedVisibility, &ComputedVisibility,
&mut CascadesFrusta,
), ),
( (
Or<(Changed<GlobalTransform>, Changed<DirectionalLight>)>,
// Prevents this query from conflicting with camera queries. // Prevents this query from conflicting with camera queries.
Without<Camera>, Without<Camera>,
), ),
>, >,
) { ) {
for (transform, directional_light, mut frustum, visibility) in &mut views { for (cascades, directional_light, visibility, mut frusta) in &mut views {
// The frustum is used for culling meshes to the light for shadow mapping // 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 // so if shadow mapping is disabled for this light, then the frustum is
// not needed. // not needed.
@ -1482,14 +1702,19 @@ pub fn update_directional_light_frusta(
continue; continue;
} }
let view_projection = directional_light.shadow_projection.get_projection_matrix() frusta.frusta = cascades
* transform.compute_matrix().inverse(); .cascades
*frustum = Frustum::from_view_projection( .iter()
&view_projection, .map(|(view, cascades)| {
&transform.translation(), (
&transform.back(), *view,
directional_light.shadow_projection.far(), cascades
); .iter()
.map(|c| Frustum::from_view_projection(&c.view_projection))
.collect::<Vec<_>>(),
)
})
.collect();
} }
} }
@ -1528,7 +1753,7 @@ pub fn update_point_light_frusta(
let view = view_translation * *view_rotation; let view = view_translation * *view_rotation;
let view_projection = projection * view.compute_matrix().inverse(); let view_projection = projection * view.compute_matrix().inverse();
*frustum = Frustum::from_view_projection( *frustum = Frustum::from_view_projection_custom_far(
&view_projection, &view_projection,
&transform.translation(), &transform.translation(),
&view_backward, &view_backward,
@ -1563,7 +1788,7 @@ pub fn update_spot_light_frusta(
let spot_projection = spot_light_projection_matrix(spot_light.outer_angle); let spot_projection = spot_light_projection_matrix(spot_light.outer_angle);
let view_projection = spot_projection * spot_view.inverse(); let view_projection = spot_projection * spot_view.inverse();
*frustum = Frustum::from_view_projection( *frustum = Frustum::from_view_projection_custom_far(
&view_projection, &view_projection,
&transform.translation(), &transform.translation(),
&view_backward, &view_backward,
@ -1591,10 +1816,10 @@ pub fn check_light_mesh_visibility(
mut directional_lights: Query< mut directional_lights: Query<
( (
&DirectionalLight, &DirectionalLight,
&Frustum, &CascadesFrusta,
&mut VisibleEntities, &mut CascadesVisibleEntities,
Option<&RenderLayers>, Option<&RenderLayers>,
&ComputedVisibility, &mut ComputedVisibility,
), ),
Without<SpotLight>, Without<SpotLight>,
>, >,
@ -1628,13 +1853,34 @@ pub fn check_light_mesh_visibility(
// Directional lights // Directional lights
for ( for (
directional_light, directional_light,
frustum, frusta,
mut visible_entities, mut visible_entities,
maybe_view_mask, maybe_view_mask,
light_computed_visibility, light_computed_visibility,
) in &mut directional_lights ) in &mut directional_lights
{ {
visible_entities.entities.clear(); // Re-use already allocated entries where possible.
let mut views_to_remove = Vec::new();
for (view, cascade_view_entities) in visible_entities.entities.iter_mut() {
match frusta.frusta.get(view) {
Some(view_frusta) => {
cascade_view_entities.resize(view_frusta.len(), Default::default());
cascade_view_entities
.iter_mut()
.for_each(|x| x.entities.clear());
}
None => views_to_remove.push(*view),
};
}
for (view, frusta) in frusta.frusta.iter() {
visible_entities
.entities
.entry(*view)
.or_insert_with(|| vec![VisibleEntities::default(); frusta.len()]);
}
for v in views_to_remove {
visible_entities.entities.remove(&v);
}
// NOTE: If shadow mapping is disabled for the light then it must have no visible entities // NOTE: If shadow mapping is disabled for the light then it must have no visible entities
if !directional_light.shadows_enabled || !light_computed_visibility.is_visible() { if !directional_light.shadows_enabled || !light_computed_visibility.is_visible() {
@ -1657,16 +1903,30 @@ pub fn check_light_mesh_visibility(
// If we have an aabb and transform, do frustum culling // If we have an aabb and transform, do frustum culling
if let (Some(aabb), Some(transform)) = (maybe_aabb, maybe_transform) { if let (Some(aabb), Some(transform)) = (maybe_aabb, maybe_transform) {
if !frustum.intersects_obb(aabb, &transform.compute_matrix(), true) { for (view, view_frusta) in frusta.frusta.iter() {
continue; let view_visible_entities = visible_entities
.entities
.get_mut(view)
.expect("Per-view visible entities should have been inserted already");
for (frustum, frustum_visible_entities) in
view_frusta.iter().zip(view_visible_entities)
{
// Disable near-plane culling, as a shadow caster could lie before the near plane.
if !frustum.intersects_obb(aabb, &transform.compute_matrix(), false, true) {
continue;
}
computed_visibility.set_visible_in_view();
frustum_visible_entities.entities.push(entity);
}
} }
} }
computed_visibility.set_visible_in_view();
visible_entities.entities.push(entity);
} }
shrink_entities(&mut visible_entities); for (_, cascade_view_entities) in visible_entities.entities.iter_mut() {
cascade_view_entities.iter_mut().for_each(shrink_entities);
}
} }
for visible_lights in &visible_point_lights { for visible_lights in &visible_point_lights {
@ -1724,7 +1984,7 @@ pub fn check_light_mesh_visibility(
.iter() .iter()
.zip(cubemap_visible_entities.iter_mut()) .zip(cubemap_visible_entities.iter_mut())
{ {
if frustum.intersects_obb(aabb, &model_to_world, true) { if frustum.intersects_obb(aabb, &model_to_world, true, true) {
computed_visibility.set_visible_in_view(); computed_visibility.set_visible_in_view();
visible_entities.entities.push(entity); visible_entities.entities.push(entity);
} }
@ -1784,7 +2044,7 @@ pub fn check_light_mesh_visibility(
continue; continue;
} }
if frustum.intersects_obb(aabb, &model_to_world, true) { if frustum.intersects_obb(aabb, &model_to_world, true, true) {
computed_visibility.set_visible_in_view(); computed_visibility.set_visible_in_view();
visible_entities.entities.push(entity); visible_entities.entities.push(entity);
} }

View File

@ -38,5 +38,9 @@ fn vertex(vertex: Vertex) -> VertexOutput {
var out: VertexOutput; var out: VertexOutput;
out.clip_position = mesh_position_local_to_clip(model, vec4<f32>(vertex.position, 1.0)); out.clip_position = mesh_position_local_to_clip(model, vec4<f32>(vertex.position, 1.0));
#ifdef DEPTH_CLAMP_ORTHO
out.clip_position.z = min(out.clip_position.z, 1.0);
#endif
return out; return out;
} }

View File

@ -1,8 +1,9 @@
use crate::{ use crate::{
directional_light_order, point_light_order, AmbientLight, Clusters, CubemapVisibleEntities, directional_light_order, point_light_order, AmbientLight, Cascade, CascadeShadowConfig,
DirectionalLight, DirectionalLightShadowMap, DrawMesh, GlobalVisiblePointLights, MeshPipeline, Cascades, CascadesVisibleEntities, Clusters, CubemapVisibleEntities, DirectionalLight,
NotShadowCaster, PointLight, PointLightShadowMap, SetMeshBindGroup, SpotLight, DirectionalLightShadowMap, DrawMesh, GlobalVisiblePointLights, MeshPipeline, NotShadowCaster,
VisiblePointLights, SHADOW_SHADER_HANDLE, PointLight, PointLightShadowMap, SetMeshBindGroup, SpotLight, VisiblePointLights,
SHADOW_SHADER_HANDLE,
}; };
use bevy_asset::Handle; use bevy_asset::Handle;
use bevy_core_pipeline::core_3d::Transparent3d; use bevy_core_pipeline::core_3d::Transparent3d;
@ -10,9 +11,9 @@ use bevy_ecs::{
prelude::*, prelude::*,
system::{lifetimeless::*, SystemParamItem}, system::{lifetimeless::*, SystemParamItem},
}; };
use bevy_math::{Mat4, UVec3, UVec4, Vec2, Vec3, Vec3A, Vec3Swizzles, Vec4, Vec4Swizzles}; use bevy_math::{Mat4, UVec3, UVec4, Vec2, Vec3, Vec3Swizzles, Vec4, Vec4Swizzles};
use bevy_render::{ use bevy_render::{
camera::{Camera, CameraProjection}, camera::Camera,
color::Color, color::Color,
mesh::{Mesh, MeshVertexBufferLayout}, mesh::{Mesh, MeshVertexBufferLayout},
render_asset::RenderAssets, render_asset::RenderAssets,
@ -61,15 +62,16 @@ pub struct ExtractedPointLight {
spot_light_angles: Option<(f32, f32)>, spot_light_angles: Option<(f32, f32)>,
} }
#[derive(Component)] #[derive(Component, Debug)]
pub struct ExtractedDirectionalLight { pub struct ExtractedDirectionalLight {
color: Color, color: Color,
illuminance: f32, illuminance: f32,
transform: GlobalTransform, transform: GlobalTransform,
projection: Mat4,
shadows_enabled: bool, shadows_enabled: bool,
shadow_depth_bias: f32, shadow_depth_bias: f32,
shadow_normal_bias: f32, shadow_normal_bias: f32,
cascade_shadow_config: CascadeShadowConfig,
cascades: HashMap<Entity, Vec<Cascade>>,
} }
#[derive(Copy, Clone, ShaderType, Default, Debug)] #[derive(Copy, Clone, ShaderType, Default, Debug)]
@ -174,13 +176,23 @@ bitflags::bitflags! {
} }
#[derive(Copy, Clone, ShaderType, Default, Debug)] #[derive(Copy, Clone, ShaderType, Default, Debug)]
pub struct GpuDirectionalLight { pub struct GpuDirectionalCascade {
view_projection: Mat4, view_projection: Mat4,
texel_size: f32,
far_bound: f32,
}
#[derive(Copy, Clone, ShaderType, Default, Debug)]
pub struct GpuDirectionalLight {
cascades: [GpuDirectionalCascade; MAX_CASCADES_PER_LIGHT],
color: Vec4, color: Vec4,
dir_to_light: Vec3, dir_to_light: Vec3,
flags: u32, flags: u32,
shadow_depth_bias: f32, shadow_depth_bias: f32,
shadow_normal_bias: f32, shadow_normal_bias: f32,
num_cascades: u32,
cascades_overlap_proportion: f32,
depth_texture_base_index: u32,
} }
// NOTE: These must match the bit flags in bevy_pbr/src/render/mesh_view_types.wgsl! // NOTE: These must match the bit flags in bevy_pbr/src/render/mesh_view_types.wgsl!
@ -211,6 +223,10 @@ pub struct GpuLights {
// NOTE: this must be kept in sync with the same constants in pbr.frag // NOTE: this must be kept in sync with the same constants in pbr.frag
pub const MAX_UNIFORM_BUFFER_POINT_LIGHTS: usize = 256; pub const MAX_UNIFORM_BUFFER_POINT_LIGHTS: usize = 256;
pub const MAX_DIRECTIONAL_LIGHTS: usize = 10; pub const MAX_DIRECTIONAL_LIGHTS: usize = 10;
#[cfg(not(feature = "webgl"))]
pub const MAX_CASCADES_PER_LIGHT: usize = 4;
#[cfg(feature = "webgl")]
pub const MAX_CASCADES_PER_LIGHT: usize = 1;
pub const SHADOW_FORMAT: TextureFormat = TextureFormat::Depth32Float; pub const SHADOW_FORMAT: TextureFormat = TextureFormat::Depth32Float;
#[derive(Resource, Clone)] #[derive(Resource, Clone)]
@ -279,6 +295,7 @@ bitflags::bitflags! {
#[repr(transparent)] #[repr(transparent)]
pub struct ShadowPipelineKey: u32 { pub struct ShadowPipelineKey: u32 {
const NONE = 0; const NONE = 0;
const DEPTH_CLAMP_ORTHO = 1;
const PRIMITIVE_TOPOLOGY_RESERVED_BITS = ShadowPipelineKey::PRIMITIVE_TOPOLOGY_MASK_BITS << ShadowPipelineKey::PRIMITIVE_TOPOLOGY_SHIFT_BITS; const PRIMITIVE_TOPOLOGY_RESERVED_BITS = ShadowPipelineKey::PRIMITIVE_TOPOLOGY_MASK_BITS << ShadowPipelineKey::PRIMITIVE_TOPOLOGY_SHIFT_BITS;
} }
} }
@ -324,6 +341,15 @@ impl SpecializedMeshPipeline for ShadowPipeline {
"MAX_DIRECTIONAL_LIGHTS".to_string(), "MAX_DIRECTIONAL_LIGHTS".to_string(),
MAX_DIRECTIONAL_LIGHTS as u32, MAX_DIRECTIONAL_LIGHTS as u32,
)); ));
shader_defs.push(ShaderDefVal::UInt(
"MAX_CASCADES_PER_LIGHT".to_string(),
MAX_CASCADES_PER_LIGHT as u32,
));
if key.contains(ShadowPipelineKey::DEPTH_CLAMP_ORTHO) {
// Avoid clipping shadow casters that are behind the near plane.
shader_defs.push("DEPTH_CLAMP_ORTHO".into());
}
if layout.contains(Mesh::ATTRIBUTE_JOINT_INDEX) if layout.contains(Mesh::ATTRIBUTE_JOINT_INDEX)
&& layout.contains(Mesh::ATTRIBUTE_JOINT_WEIGHT) && layout.contains(Mesh::ATTRIBUTE_JOINT_WEIGHT)
@ -437,7 +463,9 @@ pub fn extract_lights(
( (
Entity, Entity,
&DirectionalLight, &DirectionalLight,
&VisibleEntities, &CascadesVisibleEntities,
&Cascades,
&CascadeShadowConfig,
&GlobalTransform, &GlobalTransform,
&ComputedVisibility, &ComputedVisibility,
), ),
@ -546,22 +574,20 @@ pub fn extract_lights(
*previous_spot_lights_len = spot_lights_values.len(); *previous_spot_lights_len = spot_lights_values.len();
commands.insert_or_spawn_batch(spot_lights_values); commands.insert_or_spawn_batch(spot_lights_values);
for (entity, directional_light, visible_entities, transform, visibility) in for (
directional_lights.iter() entity,
directional_light,
visible_entities,
cascades,
cascade_config,
transform,
visibility,
) in directional_lights.iter()
{ {
if !visibility.is_visible() { if !visibility.is_visible() {
continue; continue;
} }
// Calculate the directional light shadow map texel size using the scaled x,y length of
// the orthographic projection divided by the shadow map resolution
// NOTE: When using various PCF kernel sizes, this will need to be adjusted, according to:
// https://catlikecoding.com/unity/tutorials/custom-srp/directional-shadows/
let directional_light_texel_size = transform.radius_vec3a(Vec3A::new(
directional_light.shadow_projection.right - directional_light.shadow_projection.left,
directional_light.shadow_projection.top - directional_light.shadow_projection.bottom,
0.,
)) / directional_light_shadow_map.size as f32;
// TODO: As above // TODO: As above
let render_visible_entities = visible_entities.clone(); let render_visible_entities = visible_entities.clone();
commands.get_or_spawn(entity).insert(( commands.get_or_spawn(entity).insert((
@ -569,11 +595,12 @@ pub fn extract_lights(
color: directional_light.color, color: directional_light.color,
illuminance: directional_light.illuminance, illuminance: directional_light.illuminance,
transform: *transform, transform: *transform,
projection: directional_light.shadow_projection.get_projection_matrix(),
shadows_enabled: directional_light.shadows_enabled, shadows_enabled: directional_light.shadows_enabled,
shadow_depth_bias: directional_light.shadow_depth_bias, shadow_depth_bias: directional_light.shadow_depth_bias,
shadow_normal_bias: directional_light.shadow_normal_bias // The factor of SQRT_2 is for the worst-case diagonal offset
* directional_light_texel_size, shadow_normal_bias: directional_light.shadow_normal_bias * std::f32::consts::SQRT_2,
cascade_shadow_config: cascade_config.clone(),
cascades: cascades.cascades.clone(),
}, },
render_visible_entities, render_visible_entities,
)); ));
@ -696,6 +723,7 @@ pub struct LightMeta {
pub enum LightEntity { pub enum LightEntity {
Directional { Directional {
light_entity: Entity, light_entity: Entity,
cascade_index: usize,
}, },
Point { Point {
light_entity: Entity, light_entity: Entity,
@ -770,6 +798,7 @@ pub fn prepare_lights(
point_light_shadow_map: Res<PointLightShadowMap>, point_light_shadow_map: Res<PointLightShadowMap>,
directional_light_shadow_map: Res<DirectionalLightShadowMap>, directional_light_shadow_map: Res<DirectionalLightShadowMap>,
mut max_directional_lights_warning_emitted: Local<bool>, mut max_directional_lights_warning_emitted: Local<bool>,
mut max_cascades_per_light_warning_emitted: Local<bool>,
point_lights: Query<(Entity, &ExtractedPointLight)>, point_lights: Query<(Entity, &ExtractedPointLight)>,
directional_lights: Query<(Entity, &ExtractedDirectionalLight)>, directional_lights: Query<(Entity, &ExtractedDirectionalLight)>,
) { ) {
@ -807,6 +836,18 @@ pub fn prepare_lights(
*max_directional_lights_warning_emitted = true; *max_directional_lights_warning_emitted = true;
} }
if !*max_cascades_per_light_warning_emitted
&& directional_lights
.iter()
.any(|(_, light)| light.cascade_shadow_config.bounds.len() > MAX_CASCADES_PER_LIGHT)
{
warn!(
"The number of cascades configured for a directional light exceeds the supported limit of {}.",
MAX_CASCADES_PER_LIGHT
);
*max_cascades_per_light_warning_emitted = true;
}
let point_light_count = point_lights let point_light_count = point_lights
.iter() .iter()
.filter(|light| light.1.spot_light_angles.is_none()) .filter(|light| light.1.spot_light_angles.is_none())
@ -818,18 +859,18 @@ pub fn prepare_lights(
.count() .count()
.min(max_texture_cubes); .min(max_texture_cubes);
let directional_shadow_maps_count = directional_lights let directional_shadow_enabled_count = directional_lights
.iter() .iter()
.take(MAX_DIRECTIONAL_LIGHTS) .take(MAX_DIRECTIONAL_LIGHTS)
.filter(|(_, light)| light.shadows_enabled) .filter(|(_, light)| light.shadows_enabled)
.count() .count()
.min(max_texture_array_layers); .min(max_texture_array_layers / MAX_CASCADES_PER_LIGHT);
let spot_light_shadow_maps_count = point_lights let spot_light_shadow_maps_count = point_lights
.iter() .iter()
.filter(|(_, light)| light.shadows_enabled && light.spot_light_angles.is_some()) .filter(|(_, light)| light.shadows_enabled && light.spot_light_angles.is_some())
.count() .count()
.min(max_texture_array_layers - directional_shadow_maps_count); .min(max_texture_array_layers - directional_shadow_enabled_count * MAX_CASCADES_PER_LIGHT);
// Sort lights by // Sort lights by
// - point-light vs spot-light, so that we can iterate point lights and spot lights in contiguous blocks in the fragment shader, // - point-light vs spot-light, so that we can iterate point lights and spot lights in contiguous blocks in the fragment shader,
@ -931,7 +972,7 @@ pub fn prepare_lights(
} }
let mut gpu_directional_lights = [GpuDirectionalLight::default(); MAX_DIRECTIONAL_LIGHTS]; let mut gpu_directional_lights = [GpuDirectionalLight::default(); MAX_DIRECTIONAL_LIGHTS];
let mut num_directional_cascades_enabled = 0usize;
for (index, (_light_entity, light)) in directional_lights for (index, (_light_entity, light)) in directional_lights
.iter() .iter()
.enumerate() .enumerate()
@ -940,13 +981,10 @@ pub fn prepare_lights(
let mut flags = DirectionalLightFlags::NONE; let mut flags = DirectionalLightFlags::NONE;
// Lights are sorted, shadow enabled lights are first // Lights are sorted, shadow enabled lights are first
if light.shadows_enabled && (index < directional_shadow_maps_count) { if light.shadows_enabled && (index < directional_shadow_enabled_count) {
flags |= DirectionalLightFlags::SHADOWS_ENABLED; flags |= DirectionalLightFlags::SHADOWS_ENABLED;
} }
// direction is negated to be ready for N.L
let dir_to_light = light.transform.back();
// convert from illuminance (lux) to candelas // convert from illuminance (lux) to candelas
// //
// exposure is hard coded at the moment but should be replaced // exposure is hard coded at the moment but should be replaced
@ -959,22 +997,29 @@ pub fn prepare_lights(
let exposure = 1.0 / (f32::powf(2.0, ev100) * 1.2); let exposure = 1.0 / (f32::powf(2.0, ev100) * 1.2);
let intensity = light.illuminance * exposure; let intensity = light.illuminance * exposure;
// NOTE: For the purpose of rendering shadow maps, we apply the directional light's transform to an orthographic camera let num_cascades = light
let view = light.transform.compute_matrix().inverse(); .cascade_shadow_config
// NOTE: This orthographic projection defines the volume within which shadows from a directional light can be cast .bounds
let projection = light.projection; .len()
.min(MAX_CASCADES_PER_LIGHT);
gpu_directional_lights[index] = GpuDirectionalLight { gpu_directional_lights[index] = GpuDirectionalLight {
// Filled in later.
cascades: [GpuDirectionalCascade::default(); MAX_CASCADES_PER_LIGHT],
// premultiply color by intensity // premultiply color by intensity
// we don't use the alpha at all, so no reason to multiply only [0..3] // we don't use the alpha at all, so no reason to multiply only [0..3]
color: Vec4::from_slice(&light.color.as_linear_rgba_f32()) * intensity, color: Vec4::from_slice(&light.color.as_linear_rgba_f32()) * intensity,
dir_to_light, // direction is negated to be ready for N.L
// NOTE: * view is correct, it should not be view.inverse() here dir_to_light: light.transform.back(),
view_projection: projection * view,
flags: flags.bits, flags: flags.bits,
shadow_depth_bias: light.shadow_depth_bias, shadow_depth_bias: light.shadow_depth_bias,
shadow_normal_bias: light.shadow_normal_bias, shadow_normal_bias: light.shadow_normal_bias,
num_cascades: num_cascades as u32,
cascades_overlap_proportion: light.cascade_shadow_config.overlap_proportion,
depth_texture_base_index: num_directional_cascades_enabled as u32,
}; };
if index < directional_shadow_enabled_count {
num_directional_cascades_enabled += num_cascades;
}
} }
global_light_meta.gpu_point_lights.set(gpu_point_lights); global_light_meta.gpu_point_lights.set(gpu_point_lights);
@ -1008,7 +1053,7 @@ pub fn prepare_lights(
.min(render_device.limits().max_texture_dimension_2d), .min(render_device.limits().max_texture_dimension_2d),
height: (directional_light_shadow_map.size as u32) height: (directional_light_shadow_map.size as u32)
.min(render_device.limits().max_texture_dimension_2d), .min(render_device.limits().max_texture_dimension_2d),
depth_or_array_layers: (directional_shadow_maps_count depth_or_array_layers: (num_directional_cascades_enabled
+ spot_light_shadow_maps_count) + spot_light_shadow_maps_count)
.max(1) as u32, .max(1) as u32,
}, },
@ -1031,7 +1076,7 @@ pub fn prepare_lights(
); );
let n_clusters = clusters.dimensions.x * clusters.dimensions.y * clusters.dimensions.z; let n_clusters = clusters.dimensions.x * clusters.dimensions.y * clusters.dimensions.z;
let gpu_lights = GpuLights { let mut gpu_lights = GpuLights {
directional_lights: gpu_directional_lights, directional_lights: gpu_directional_lights,
ambient_color: Vec4::from_slice(&ambient_light.color.as_linear_rgba_f32()) ambient_color: Vec4::from_slice(&ambient_light.color.as_linear_rgba_f32())
* ambient_light.brightness, * ambient_light.brightness,
@ -1043,10 +1088,10 @@ pub fn prepare_lights(
), ),
cluster_dimensions: clusters.dimensions.extend(n_clusters), cluster_dimensions: clusters.dimensions.extend(n_clusters),
n_directional_lights: directional_lights.iter().len() as u32, n_directional_lights: directional_lights.iter().len() as u32,
// spotlight shadow maps are stored in the directional light array, starting at directional_shadow_maps_count. // spotlight shadow maps are stored in the directional light array, starting at num_directional_cascades_enabled.
// the spot lights themselves start in the light array at point_light_count. so to go from light // the spot lights themselves start in the light array at point_light_count. so to go from light
// index to shadow map index, we need to subtract point light count and add directional shadowmap count. // index to shadow map index, we need to subtract point light count and add directional shadowmap count.
spot_light_shadowmap_offset: directional_shadow_maps_count as i32 spot_light_shadowmap_offset: num_directional_cascades_enabled as i32
- point_light_count as i32, - point_light_count as i32,
}; };
@ -1099,6 +1144,7 @@ pub fn prepare_lights(
point_light_shadow_map.size as u32, point_light_shadow_map.size as u32,
), ),
transform: view_translation * *view_rotation, transform: view_translation * *view_rotation,
view_projection: None,
projection: cube_face_projection, projection: cube_face_projection,
hdr: false, hdr: false,
}, },
@ -1137,7 +1183,7 @@ pub fn prepare_lights(
aspect: TextureAspect::All, aspect: TextureAspect::All,
base_mip_level: 0, base_mip_level: 0,
mip_level_count: None, mip_level_count: None,
base_array_layer: (directional_shadow_maps_count + light_index) as u32, base_array_layer: (num_directional_cascades_enabled + light_index) as u32,
array_layer_count: NonZeroU32::new(1), array_layer_count: NonZeroU32::new(1),
}); });
@ -1156,6 +1202,7 @@ pub fn prepare_lights(
), ),
transform: spot_view_transform, transform: spot_view_transform,
projection: spot_projection, projection: spot_projection,
view_projection: None,
hdr: false, hdr: false,
}, },
RenderPhase::<Shadow>::default(), RenderPhase::<Shadow>::default(),
@ -1167,47 +1214,71 @@ pub fn prepare_lights(
} }
// directional lights // directional lights
let mut directional_depth_texture_array_index = 0u32;
for (light_index, &(light_entity, light)) in directional_lights for (light_index, &(light_entity, light)) in directional_lights
.iter() .iter()
.enumerate() .enumerate()
.take(directional_shadow_maps_count) .take(directional_shadow_enabled_count)
{ {
let depth_texture_view = for (cascade_index, (cascade, bound)) in light
directional_light_depth_texture .cascades
.texture .get(&entity)
.create_view(&TextureViewDescriptor { .unwrap()
label: Some("directional_light_shadow_map_texture_view"), .iter()
format: None, .take(MAX_CASCADES_PER_LIGHT)
dimension: Some(TextureViewDimension::D2), .zip(&light.cascade_shadow_config.bounds)
aspect: TextureAspect::All, .enumerate()
base_mip_level: 0, {
mip_level_count: None, gpu_lights.directional_lights[light_index].cascades[cascade_index] =
base_array_layer: light_index as u32, GpuDirectionalCascade {
array_layer_count: NonZeroU32::new(1), view_projection: cascade.view_projection,
}); texel_size: cascade.texel_size,
far_bound: *bound,
};
let view_light_entity = commands let depth_texture_view =
.spawn(( directional_light_depth_texture
ShadowView { .texture
depth_texture_view, .create_view(&TextureViewDescriptor {
pass_name: format!("shadow pass directional light {light_index}"), label: Some("directional_light_shadow_map_array_texture_view"),
}, format: None,
ExtractedView { dimension: Some(TextureViewDimension::D2),
viewport: UVec4::new( aspect: TextureAspect::All,
0, base_mip_level: 0,
0, mip_level_count: None,
directional_light_shadow_map.size as u32, base_array_layer: directional_depth_texture_array_index,
directional_light_shadow_map.size as u32, array_layer_count: NonZeroU32::new(1),
), });
transform: light.transform, directional_depth_texture_array_index += 1;
projection: light.projection,
hdr: false, let view_light_entity = commands
}, .spawn((
RenderPhase::<Shadow>::default(), ShadowView {
LightEntity::Directional { light_entity }, depth_texture_view,
)) pass_name: format!(
.id(); "shadow pass directional light {light_index} cascade {cascade_index}"),
view_lights.push(view_light_entity); },
ExtractedView {
viewport: UVec4::new(
0,
0,
directional_light_shadow_map.size as u32,
directional_light_shadow_map.size as u32,
),
transform: GlobalTransform::from(cascade.view_transform),
projection: cascade.projection,
view_projection: Some(cascade.view_projection),
hdr: false,
},
RenderPhase::<Shadow>::default(),
LightEntity::Directional {
light_entity,
cascade_index,
},
))
.id();
view_lights.push(view_light_entity);
}
} }
let point_light_depth_texture_view = let point_light_depth_texture_view =
@ -1627,21 +1698,30 @@ pub fn queue_shadows(
render_meshes: Res<RenderAssets<Mesh>>, render_meshes: Res<RenderAssets<Mesh>>,
mut pipelines: ResMut<SpecializedMeshPipelines<ShadowPipeline>>, mut pipelines: ResMut<SpecializedMeshPipelines<ShadowPipeline>>,
pipeline_cache: Res<PipelineCache>, pipeline_cache: Res<PipelineCache>,
view_lights: Query<&ViewLightEntities>, view_lights: Query<(Entity, &ViewLightEntities)>,
mut view_light_shadow_phases: Query<(&LightEntity, &mut RenderPhase<Shadow>)>, mut view_light_shadow_phases: Query<(&LightEntity, &mut RenderPhase<Shadow>)>,
point_light_entities: Query<&CubemapVisibleEntities, With<ExtractedPointLight>>, point_light_entities: Query<&CubemapVisibleEntities, With<ExtractedPointLight>>,
directional_light_entities: Query<&VisibleEntities, With<ExtractedDirectionalLight>>, directional_light_entities: Query<&CascadesVisibleEntities, With<ExtractedDirectionalLight>>,
spot_light_entities: Query<&VisibleEntities, With<ExtractedPointLight>>, spot_light_entities: Query<&VisibleEntities, With<ExtractedPointLight>>,
) { ) {
for view_lights in &view_lights { for (entity, view_lights) in &view_lights {
let draw_shadow_mesh = shadow_draw_functions.read().id::<DrawShadowMesh>(); let draw_shadow_mesh = shadow_draw_functions.read().id::<DrawShadowMesh>();
for view_light_entity in view_lights.lights.iter().copied() { for view_light_entity in view_lights.lights.iter().copied() {
let (light_entity, mut shadow_phase) = let (light_entity, mut shadow_phase) =
view_light_shadow_phases.get_mut(view_light_entity).unwrap(); view_light_shadow_phases.get_mut(view_light_entity).unwrap();
let is_directional_light = matches!(light_entity, LightEntity::Directional { .. });
let visible_entities = match light_entity { let visible_entities = match light_entity {
LightEntity::Directional { light_entity } => directional_light_entities LightEntity::Directional {
light_entity,
cascade_index,
} => directional_light_entities
.get(*light_entity) .get(*light_entity)
.expect("Failed to get directional light visible entities"), .expect("Failed to get directional light visible entities")
.entities
.get(&entity)
.expect("Failed to get directional light visible entities for view")
.get(*cascade_index)
.expect("Failed to get directional light visible entities for cascade"),
LightEntity::Point { LightEntity::Point {
light_entity, light_entity,
face_index, face_index,
@ -1658,8 +1738,11 @@ pub fn queue_shadows(
for entity in visible_entities.iter().copied() { for entity in visible_entities.iter().copied() {
if let Ok(mesh_handle) = casting_meshes.get(entity) { if let Ok(mesh_handle) = casting_meshes.get(entity) {
if let Some(mesh) = render_meshes.get(mesh_handle) { if let Some(mesh) = render_meshes.get(mesh_handle) {
let key = let mut key =
ShadowPipelineKey::from_primitive_topology(mesh.primitive_topology); ShadowPipelineKey::from_primitive_topology(mesh.primitive_topology);
if is_directional_light {
key |= ShadowPipelineKey::DEPTH_CLAMP_ORTHO;
}
let pipeline_id = pipelines.specialize( let pipeline_id = pipelines.specialize(
&pipeline_cache, &pipeline_cache,
&shadow_pipeline, &shadow_pipeline,

View File

@ -1,7 +1,7 @@
use crate::{ use crate::{
GlobalLightMeta, GpuLights, GpuPointLights, LightMeta, NotShadowCaster, NotShadowReceiver, GlobalLightMeta, GpuLights, GpuPointLights, LightMeta, NotShadowCaster, NotShadowReceiver,
ShadowPipeline, ViewClusterBindings, ViewLightsUniformOffset, ViewShadowBindings, ShadowPipeline, ViewClusterBindings, ViewLightsUniformOffset, ViewShadowBindings,
CLUSTERED_FORWARD_STORAGE_BUFFER_COUNT, MAX_DIRECTIONAL_LIGHTS, CLUSTERED_FORWARD_STORAGE_BUFFER_COUNT, MAX_CASCADES_PER_LIGHT, MAX_DIRECTIONAL_LIGHTS,
}; };
use bevy_app::Plugin; use bevy_app::Plugin;
use bevy_asset::{load_internal_asset, Assets, Handle, HandleUntyped}; use bevy_asset::{load_internal_asset, Assets, Handle, HandleUntyped};
@ -646,6 +646,10 @@ impl SpecializedMeshPipeline for MeshPipeline {
"MAX_DIRECTIONAL_LIGHTS".to_string(), "MAX_DIRECTIONAL_LIGHTS".to_string(),
MAX_DIRECTIONAL_LIGHTS as u32, MAX_DIRECTIONAL_LIGHTS as u32,
)); ));
shader_defs.push(ShaderDefVal::UInt(
"MAX_CASCADES_PER_LIGHT".to_string(),
MAX_CASCADES_PER_LIGHT as u32,
));
if layout.contains(Mesh::ATTRIBUTE_UV_0) { if layout.contains(Mesh::ATTRIBUTE_UV_0) {
shader_defs.push("VERTEX_UVS".into()); shader_defs.push("VERTEX_UVS".into());

View File

@ -28,14 +28,23 @@ struct PointLight {
let POINT_LIGHT_FLAGS_SHADOWS_ENABLED_BIT: u32 = 1u; let POINT_LIGHT_FLAGS_SHADOWS_ENABLED_BIT: u32 = 1u;
let POINT_LIGHT_FLAGS_SPOT_LIGHT_Y_NEGATIVE: u32 = 2u; let POINT_LIGHT_FLAGS_SPOT_LIGHT_Y_NEGATIVE: u32 = 2u;
struct DirectionalLight { struct DirectionalCascade {
view_projection: mat4x4<f32>, view_projection: mat4x4<f32>,
texel_size: f32,
far_bound: f32,
}
struct DirectionalLight {
cascades: array<DirectionalCascade, #{MAX_CASCADES_PER_LIGHT}>,
color: vec4<f32>, color: vec4<f32>,
direction_to_light: vec3<f32>, direction_to_light: vec3<f32>,
// 'flags' is a bit field indicating various options. u32 is 32 bits so we have up to 32 options. // 'flags' is a bit field indicating various options. u32 is 32 bits so we have up to 32 options.
flags: u32, flags: u32,
shadow_depth_bias: f32, shadow_depth_bias: f32,
shadow_normal_bias: f32, shadow_normal_bias: f32,
num_cascades: u32,
cascades_overlap_proportion: f32,
depth_texture_base_index: u32,
}; };
let DIRECTIONAL_LIGHT_FLAGS_SHADOWS_ENABLED_BIT: u32 = 1u; let DIRECTIONAL_LIGHT_FLAGS_SHADOWS_ENABLED_BIT: u32 = 1u;

View File

@ -224,7 +224,7 @@ fn pbr(
var shadow: f32 = 1.0; var shadow: f32 = 1.0;
if ((mesh.flags & MESH_FLAGS_SHADOW_RECEIVER_BIT) != 0u if ((mesh.flags & MESH_FLAGS_SHADOW_RECEIVER_BIT) != 0u
&& (lights.directional_lights[i].flags & DIRECTIONAL_LIGHT_FLAGS_SHADOWS_ENABLED_BIT) != 0u) { && (lights.directional_lights[i].flags & DIRECTIONAL_LIGHT_FLAGS_SHADOWS_ENABLED_BIT) != 0u) {
shadow = fetch_directional_shadow(i, in.world_position, in.world_normal); shadow = fetch_directional_shadow(i, in.world_position, in.world_normal, view_z);
} }
let light_contrib = directional_light(i, roughness, NdotV, in.N, in.V, R, F0, diffuse_color); let light_contrib = directional_light(i, roughness, NdotV, in.N, in.V, R, F0, diffuse_color);
light_accum = light_accum + light_contrib * shadow; light_accum = light_accum + light_contrib * shadow;

View File

@ -98,15 +98,27 @@ fn fetch_spot_shadow(light_id: u32, frag_position: vec4<f32>, surface_normal: ve
#endif #endif
} }
fn fetch_directional_shadow(light_id: u32, frag_position: vec4<f32>, surface_normal: vec3<f32>) -> f32 { fn get_cascade_index(light_id: u32, view_z: f32) -> u32 {
let light = &lights.directional_lights[light_id]; let light = &lights.directional_lights[light_id];
for (var i: u32 = 0u; i < (*light).num_cascades; i = i + 1u) {
if (-view_z < (*light).cascades[i].far_bound) {
return i;
}
}
return (*light).num_cascades;
}
fn sample_cascade(light_id: u32, cascade_index: u32, frag_position: vec4<f32>, surface_normal: vec3<f32>) -> f32 {
let light = &lights.directional_lights[light_id];
let cascade = &(*light).cascades[cascade_index];
// The normal bias is scaled to the texel size. // The normal bias is scaled to the texel size.
let normal_offset = (*light).shadow_normal_bias * surface_normal.xyz; let normal_offset = (*light).shadow_normal_bias * (*cascade).texel_size * surface_normal.xyz;
let depth_offset = (*light).shadow_depth_bias * (*light).direction_to_light.xyz; let depth_offset = (*light).shadow_depth_bias * (*light).direction_to_light.xyz;
let offset_position = vec4<f32>(frag_position.xyz + normal_offset + depth_offset, frag_position.w); let offset_position = vec4<f32>(frag_position.xyz + normal_offset + depth_offset, frag_position.w);
let offset_position_clip = (*light).view_projection * offset_position; let offset_position_clip = (*cascade).view_projection * offset_position;
if (offset_position_clip.w <= 0.0) { if (offset_position_clip.w <= 0.0) {
return 1.0; return 1.0;
} }
@ -127,8 +139,42 @@ fn fetch_directional_shadow(light_id: u32, frag_position: vec4<f32>, surface_nor
// NOTE: Due to non-uniform control flow above, we must use the level variant of the texture // NOTE: Due to non-uniform control flow above, we must use the level variant of the texture
// sampler to avoid use of implicit derivatives causing possible undefined behavior. // sampler to avoid use of implicit derivatives causing possible undefined behavior.
#ifdef NO_ARRAY_TEXTURES_SUPPORT #ifdef NO_ARRAY_TEXTURES_SUPPORT
return textureSampleCompareLevel(directional_shadow_textures, directional_shadow_textures_sampler, light_local, depth); return textureSampleCompareLevel(
directional_shadow_textures,
directional_shadow_textures_sampler,
light_local,
depth
);
#else #else
return textureSampleCompareLevel(directional_shadow_textures, directional_shadow_textures_sampler, light_local, i32(light_id), depth); return textureSampleCompareLevel(
directional_shadow_textures,
directional_shadow_textures_sampler,
light_local,
i32((*light).depth_texture_base_index + cascade_index),
depth
);
#endif #endif
} }
fn fetch_directional_shadow(light_id: u32, frag_position: vec4<f32>, surface_normal: vec3<f32>, view_z: f32) -> f32 {
let light = &lights.directional_lights[light_id];
let cascade_index = get_cascade_index(light_id, view_z);
if (cascade_index >= (*light).num_cascades) {
return 1.0;
}
var shadow = sample_cascade(light_id, cascade_index, frag_position, surface_normal);
// Blend with the next cascade, if there is one.
let next_cascade_index = cascade_index + 1u;
if (next_cascade_index < (*light).num_cascades) {
let this_far_bound = (*light).cascades[cascade_index].far_bound;
let next_near_bound = (1.0 - (*light).cascades_overlap_proportion) * this_far_bound;
if (-view_z >= next_near_bound) {
let next_shadow = sample_cascade(light_id, next_cascade_index, frag_position, surface_normal);
shadow = mix(shadow, next_shadow, (-view_z - next_near_bound) / (this_far_bound - next_near_bound));
}
}
return shadow;
}

View File

@ -559,6 +559,7 @@ pub fn extract_cameras(
ExtractedView { ExtractedView {
projection: camera.projection_matrix(), projection: camera.projection_matrix(),
transform: *transform, transform: *transform,
view_projection: None,
hdr: camera.hdr, hdr: camera.hdr,
viewport: UVec4::new( viewport: UVec4::new(
viewport_origin.x, viewport_origin.x,

View File

@ -279,6 +279,7 @@ impl Plugin for RenderPlugin {
app.register_type::<color::Color>() app.register_type::<color::Color>()
.register_type::<primitives::Aabb>() .register_type::<primitives::Aabb>()
.register_type::<primitives::CascadesFrusta>()
.register_type::<primitives::CubemapFrusta>() .register_type::<primitives::CubemapFrusta>()
.register_type::<primitives::Frustum>(); .register_type::<primitives::Frustum>();
} }

View File

@ -1,6 +1,7 @@
use bevy_ecs::{component::Component, reflect::ReflectComponent}; use bevy_ecs::{component::Component, prelude::Entity, reflect::ReflectComponent};
use bevy_math::{Mat4, Vec3, Vec3A, Vec4, Vec4Swizzles}; use bevy_math::{Mat4, Vec3, Vec3A, Vec4, Vec4Swizzles};
use bevy_reflect::Reflect; use bevy_reflect::Reflect;
use bevy_utils::HashMap;
/// An Axis-Aligned Bounding Box /// An Axis-Aligned Bounding Box
#[derive(Component, Clone, Debug, Default, Reflect)] #[derive(Component, Clone, Debug, Default, Reflect)]
@ -134,17 +135,33 @@ pub struct Frustum {
} }
impl Frustum { impl Frustum {
// NOTE: This approach of extracting the frustum planes from the view /// Returns a frustum derived from `view_projection`.
// projection matrix is from Foundations of Game Engine Development 2
// Rendering by Lengyel. Slight modification has been made for when
// the far plane is infinite but we still want to cull to a far plane.
#[inline] #[inline]
pub fn from_view_projection( pub fn from_view_projection(view_projection: &Mat4) -> Self {
let mut frustum = Frustum::from_view_projection_no_far(view_projection);
frustum.planes[5] = Plane::new(view_projection.row(2));
frustum
}
/// Returns a frustum derived from `view_projection`, but with a custom
/// far plane.
#[inline]
pub fn from_view_projection_custom_far(
view_projection: &Mat4, view_projection: &Mat4,
view_translation: &Vec3, view_translation: &Vec3,
view_backward: &Vec3, view_backward: &Vec3,
far: f32, far: f32,
) -> Self { ) -> Self {
let mut frustum = Frustum::from_view_projection_no_far(view_projection);
let far_center = *view_translation - far * *view_backward;
frustum.planes[5] = Plane::new(view_backward.extend(-view_backward.dot(far_center)));
frustum
}
// NOTE: This approach of extracting the frustum planes from the view
// projection matrix is from Foundations of Game Engine Development 2
// Rendering by Lengyel.
fn from_view_projection_no_far(view_projection: &Mat4) -> Self {
let row3 = view_projection.row(3); let row3 = view_projection.row(3);
let mut planes = [Plane::default(); 6]; let mut planes = [Plane::default(); 6];
for (i, plane) in planes.iter_mut().enumerate().take(5) { for (i, plane) in planes.iter_mut().enumerate().take(5) {
@ -155,8 +172,6 @@ impl Frustum {
row3 - row row3 - row
}); });
} }
let far_center = *view_translation - far * *view_backward;
planes[5] = Plane::new(view_backward.extend(-view_backward.dot(far_center)));
Self { planes } Self { planes }
} }
@ -173,7 +188,13 @@ impl Frustum {
} }
#[inline] #[inline]
pub fn intersects_obb(&self, aabb: &Aabb, model_to_world: &Mat4, intersect_far: bool) -> bool { pub fn intersects_obb(
&self,
aabb: &Aabb,
model_to_world: &Mat4,
intersect_near: bool,
intersect_far: bool,
) -> bool {
let aabb_center_world = model_to_world.transform_point3a(aabb.center).extend(1.0); let aabb_center_world = model_to_world.transform_point3a(aabb.center).extend(1.0);
let axes = [ let axes = [
Vec3A::from(model_to_world.x_axis), Vec3A::from(model_to_world.x_axis),
@ -181,8 +202,13 @@ impl Frustum {
Vec3A::from(model_to_world.z_axis), Vec3A::from(model_to_world.z_axis),
]; ];
let max = if intersect_far { 6 } else { 5 }; for (idx, plane) in self.planes.into_iter().enumerate() {
for plane in &self.planes[..max] { if idx == 4 && !intersect_near {
continue;
}
if idx == 5 && !intersect_far {
continue;
}
let p_normal = Vec3A::from(plane.normal_d()); let p_normal = Vec3A::from(plane.normal_d());
let relative_radius = aabb.relative_radius(&p_normal, &axes); let relative_radius = aabb.relative_radius(&p_normal, &axes);
if plane.normal_d().dot(aabb_center_world) + relative_radius <= 0.0 { if plane.normal_d().dot(aabb_center_world) + relative_radius <= 0.0 {
@ -209,6 +235,13 @@ impl CubemapFrusta {
} }
} }
#[derive(Component, Debug, Default, Reflect)]
#[reflect(Component)]
pub struct CascadesFrusta {
#[reflect(ignore)]
pub frusta: HashMap<Entity, Vec<Frustum>>,
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;

View File

@ -91,6 +91,10 @@ impl Msaa {
pub struct ExtractedView { pub struct ExtractedView {
pub projection: Mat4, pub projection: Mat4,
pub transform: GlobalTransform, pub transform: GlobalTransform,
// The view-projection matrix. When provided it is used instead of deriving it from
// `projection` and `transform` fields, which can be helpful in cases where numerical
// stability matters and there is a more direct way to derive the view-projection matrix.
pub view_projection: Option<Mat4>,
pub hdr: bool, pub hdr: bool,
// uvec4(origin.x, origin.y, width, height) // uvec4(origin.x, origin.y, width, height)
pub viewport: UVec4, pub viewport: UVec4,
@ -251,7 +255,9 @@ fn prepare_view_uniforms(
let inverse_view = view.inverse(); let inverse_view = view.inverse();
let view_uniforms = ViewUniformOffset { let view_uniforms = ViewUniformOffset {
offset: view_uniforms.uniforms.push(ViewUniform { offset: view_uniforms.uniforms.push(ViewUniform {
view_proj: projection * inverse_view, view_proj: camera
.view_projection
.unwrap_or_else(|| projection * inverse_view),
inverse_view_proj: view * inverse_projection, inverse_view_proj: view * inverse_projection,
view, view,
inverse_view, inverse_view,

View File

@ -281,7 +281,7 @@ pub fn update_frusta<T: Component + CameraProjection + Send + Sync + 'static>(
for (transform, projection, mut frustum) in &mut views { for (transform, projection, mut frustum) in &mut views {
let view_projection = let view_projection =
projection.get_projection_matrix() * transform.compute_matrix().inverse(); projection.get_projection_matrix() * transform.compute_matrix().inverse();
*frustum = Frustum::from_view_projection( *frustum = Frustum::from_view_projection_custom_far(
&view_projection, &view_projection,
&transform.translation(), &transform.translation(),
&transform.back(), &transform.back(),
@ -407,7 +407,7 @@ pub fn check_visibility(
return; return;
} }
// If we have an aabb, do aabb-based frustum culling // If we have an aabb, do aabb-based frustum culling
if !frustum.intersects_obb(model_aabb, &model, false) { if !frustum.intersects_obb(model_aabb, &model, true, false) {
return; return;
} }
} }

View File

@ -275,6 +275,7 @@ pub fn extract_default_ui_camera_view<T: Component>(
0.0, 0.0,
UI_CAMERA_FAR + UI_CAMERA_TRANSFORM_OFFSET, UI_CAMERA_FAR + UI_CAMERA_TRANSFORM_OFFSET,
), ),
view_projection: None,
hdr: camera.hdr, hdr: camera.hdr,
viewport: UVec4::new( viewport: UVec4::new(
physical_origin.x, physical_origin.x,

View File

@ -70,18 +70,8 @@ fn setup(
}); });
// light // light
const HALF_SIZE: f32 = 2.0;
commands.spawn(DirectionalLightBundle { commands.spawn(DirectionalLightBundle {
directional_light: DirectionalLight { directional_light: DirectionalLight {
shadow_projection: OrthographicProjection {
left: -HALF_SIZE,
right: HALF_SIZE,
bottom: -HALF_SIZE,
top: HALF_SIZE,
near: -10.0 * HALF_SIZE,
far: 10.0 * HALF_SIZE,
..default()
},
shadows_enabled: true, shadows_enabled: true,
..default() ..default()
}, },

View File

@ -3,7 +3,7 @@
use std::f32::consts::PI; use std::f32::consts::PI;
use bevy::prelude::*; use bevy::{pbr::CascadeShadowConfig, prelude::*};
fn main() { fn main() {
App::new() App::new()
@ -186,19 +186,8 @@ fn setup(
}); });
// directional 'sun' light // directional 'sun' light
const HALF_SIZE: f32 = 10.0;
commands.spawn(DirectionalLightBundle { commands.spawn(DirectionalLightBundle {
directional_light: DirectionalLight { directional_light: DirectionalLight {
// Configure the projection to better fit the scene
shadow_projection: OrthographicProjection {
left: -HALF_SIZE,
right: HALF_SIZE,
bottom: -HALF_SIZE,
top: HALF_SIZE,
near: -10.0 * HALF_SIZE,
far: 10.0 * HALF_SIZE,
..default()
},
shadows_enabled: true, shadows_enabled: true,
..default() ..default()
}, },
@ -207,6 +196,10 @@ fn setup(
rotation: Quat::from_rotation_x(-PI / 4.), rotation: Quat::from_rotation_x(-PI / 4.),
..default() ..default()
}, },
// The default cascade config is designed to handle large scenes.
// As this example has a much smaller world, we can tighten the shadow
// far bound for better visual quality.
cascade_shadow_config: CascadeShadowConfig::new(4, 5.0, 30.0, 0.2),
..default() ..default()
}); });

View File

@ -2,7 +2,7 @@
use std::f32::consts::*; use std::f32::consts::*;
use bevy::prelude::*; use bevy::{pbr::CascadeShadowConfig, prelude::*};
fn main() { fn main() {
App::new() App::new()
@ -21,21 +21,14 @@ fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
transform: Transform::from_xyz(0.7, 0.7, 1.0).looking_at(Vec3::new(0.0, 0.3, 0.0), Vec3::Y), transform: Transform::from_xyz(0.7, 0.7, 1.0).looking_at(Vec3::new(0.0, 0.3, 0.0), Vec3::Y),
..default() ..default()
}); });
const HALF_SIZE: f32 = 1.0;
commands.spawn(DirectionalLightBundle { commands.spawn(DirectionalLightBundle {
directional_light: DirectionalLight { directional_light: DirectionalLight {
shadow_projection: OrthographicProjection {
left: -HALF_SIZE,
right: HALF_SIZE,
bottom: -HALF_SIZE,
top: HALF_SIZE,
near: -10.0 * HALF_SIZE,
far: 10.0 * HALF_SIZE,
..default()
},
shadows_enabled: true, shadows_enabled: true,
..default() ..default()
}, },
// This is a relatively small scene, so use tighter shadow
// cascade bounds than the default for better quality.
cascade_shadow_config: CascadeShadowConfig::new(1, 1.1, 1.5, 0.3),
..default() ..default()
}); });
commands.spawn(SceneBundle { commands.spawn(SceneBundle {

View File

@ -69,15 +69,6 @@ fn setup(
commands.spawn(DirectionalLightBundle { commands.spawn(DirectionalLightBundle {
directional_light: DirectionalLight { directional_light: DirectionalLight {
illuminance: 100000.0, illuminance: 100000.0,
shadow_projection: OrthographicProjection {
left: -0.35,
right: 500.35,
bottom: -0.1,
top: 5.0,
near: -5.0,
far: 5.0,
..default()
},
shadow_depth_bias: 0.0, shadow_depth_bias: 0.0,
shadow_normal_bias: 0.0, shadow_normal_bias: 0.0,
shadows_enabled: true, shadows_enabled: true,

View File

@ -100,15 +100,6 @@ fn setup(
commands.spawn(DirectionalLightBundle { commands.spawn(DirectionalLightBundle {
directional_light: DirectionalLight { directional_light: DirectionalLight {
illuminance: 100000.0, illuminance: 100000.0,
shadow_projection: OrthographicProjection {
left: -10.0,
right: 10.0,
bottom: -10.0,
top: 10.0,
near: -50.0,
far: 50.0,
..default()
},
shadows_enabled: true, shadows_enabled: true,
..default() ..default()
}, },

View File

@ -132,26 +132,9 @@ fn setup_scene_after_load(
// Spawn a default light if the scene does not have one // Spawn a default light if the scene does not have one
if !scene_handle.has_light { if !scene_handle.has_light {
let sphere = Sphere {
center: aabb.center,
radius: aabb.half_extents.length(),
};
let aabb = Aabb::from(sphere);
let min = aabb.min();
let max = aabb.max();
info!("Spawning a directional light"); info!("Spawning a directional light");
commands.spawn(DirectionalLightBundle { commands.spawn(DirectionalLightBundle {
directional_light: DirectionalLight { directional_light: DirectionalLight {
shadow_projection: OrthographicProjection {
left: min.x,
right: max.x,
bottom: min.y,
top: max.y,
near: min.z,
far: max.z,
..default()
},
shadows_enabled: false, shadows_enabled: false,
..default() ..default()
}, },

View File

@ -44,9 +44,6 @@ Scene Controls:
L - animate light direction L - animate light direction
U - toggle shadows U - toggle shadows
C - cycle through the camera controller and any cameras loaded from the scene C - cycle through the camera controller and any cameras loaded from the scene
5/6 - decrease/increase shadow projection width
7/8 - decrease/increase shadow projection height
9/0 - decrease/increase shadow projection near/far
Space - Play/Pause animation Space - Play/Pause animation
Enter - Cycle through animations Enter - Cycle through animations
@ -198,35 +195,13 @@ fn keyboard_animation_control(
} }
} }
const SCALE_STEP: f32 = 0.1;
fn update_lights( fn update_lights(
key_input: Res<Input<KeyCode>>, key_input: Res<Input<KeyCode>>,
time: Res<Time>, time: Res<Time>,
mut query: Query<(&mut Transform, &mut DirectionalLight)>, mut query: Query<(&mut Transform, &mut DirectionalLight)>,
mut animate_directional_light: Local<bool>, mut animate_directional_light: Local<bool>,
) { ) {
let mut projection_adjustment = Vec3::ONE;
if key_input.just_pressed(KeyCode::Key5) {
projection_adjustment.x -= SCALE_STEP;
} else if key_input.just_pressed(KeyCode::Key6) {
projection_adjustment.x += SCALE_STEP;
} else if key_input.just_pressed(KeyCode::Key7) {
projection_adjustment.y -= SCALE_STEP;
} else if key_input.just_pressed(KeyCode::Key8) {
projection_adjustment.y += SCALE_STEP;
} else if key_input.just_pressed(KeyCode::Key9) {
projection_adjustment.z -= SCALE_STEP;
} else if key_input.just_pressed(KeyCode::Key0) {
projection_adjustment.z += SCALE_STEP;
}
for (_, mut light) in &mut query { for (_, mut light) in &mut query {
light.shadow_projection.left *= projection_adjustment.x;
light.shadow_projection.right *= projection_adjustment.x;
light.shadow_projection.bottom *= projection_adjustment.y;
light.shadow_projection.top *= projection_adjustment.y;
light.shadow_projection.near *= projection_adjustment.z;
light.shadow_projection.far *= projection_adjustment.z;
if key_input.just_pressed(KeyCode::U) { if key_input.just_pressed(KeyCode::U) {
light.shadows_enabled = !light.shadows_enabled; light.shadows_enabled = !light.shadows_enabled;
} }