From e1c8f7f8998be5433b25ede254332229324914e8 Mon Sep 17 00:00:00 2001 From: Daniel Chia Date: Mon, 12 Dec 2022 00:38:51 -0800 Subject: [PATCH 01/16] Cascaded shadow maps. Co-authored-by: Robert Swain Implements cascaded shadow maps for directional lights, which produces better quality shadows without needing excessively large shadow maps. --- .../src/core_2d/camera_2d.rs | 2 +- crates/bevy_pbr/src/bundle.rs | 24 +- crates/bevy_pbr/src/lib.rs | 7 + crates/bevy_pbr/src/light.rs | 387 ++++++++++++++---- crates/bevy_pbr/src/render/depth.wgsl | 4 + crates/bevy_pbr/src/render/light.rs | 230 +++++++---- crates/bevy_pbr/src/render/mesh.rs | 6 +- .../bevy_pbr/src/render/mesh_view_types.wgsl | 10 +- crates/bevy_pbr/src/render/pbr_functions.wgsl | 2 +- crates/bevy_pbr/src/render/shadows.wgsl | 62 ++- crates/bevy_render/src/camera/camera.rs | 1 + crates/bevy_render/src/primitives/mod.rs | 55 ++- crates/bevy_render/src/view/mod.rs | 5 +- crates/bevy_render/src/view/visibility/mod.rs | 4 +- crates/bevy_ui/src/render/mod.rs | 1 + examples/3d/fxaa.rs | 9 - examples/3d/lighting.rs | 11 - examples/3d/load_gltf.rs | 10 - examples/3d/shadow_biases.rs | 9 - examples/3d/shadow_caster_receiver.rs | 9 - 20 files changed, 615 insertions(+), 233 deletions(-) diff --git a/crates/bevy_core_pipeline/src/core_2d/camera_2d.rs b/crates/bevy_core_pipeline/src/core_2d/camera_2d.rs index 1f52beeec8aef..dce52de82b375 100644 --- a/crates/bevy_core_pipeline/src/core_2d/camera_2d.rs +++ b/crates/bevy_core_pipeline/src/core_2d/camera_2d.rs @@ -61,7 +61,7 @@ impl Camera2dBundle { let transform = Transform::from_xyz(0.0, 0.0, far - 0.1); let view_projection = projection.get_projection_matrix() * transform.compute_matrix().inverse(); - let frustum = Frustum::from_view_projection( + let frustum = Frustum::from_view_projection_custom_far( &view_projection, &transform.translation, &transform.back(), diff --git a/crates/bevy_pbr/src/bundle.rs b/crates/bevy_pbr/src/bundle.rs index 0bd720d11ea06..e2ef3d047cdda 100644 --- a/crates/bevy_pbr/src/bundle.rs +++ b/crates/bevy_pbr/src/bundle.rs @@ -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_ecs::{bundle::Bundle, component::Component, reflect::ReflectComponent}; +use bevy_ecs::{bundle::Bundle, component::Component, prelude::Entity, reflect::ReflectComponent}; use bevy_reflect::Reflect; use bevy_render::{ mesh::Mesh, - primitives::{CubemapFrusta, Frustum}, + primitives::{CascadesFrusta, CubemapFrusta, Frustum}, view::{ComputedVisibility, Visibility, VisibleEntities}, }; use bevy_transform::components::{GlobalTransform, Transform}; +use bevy_utils::HashMap; /// A component bundle for PBR entities with a [`Mesh`] and a [`StandardMaterial`]. pub type PbrBundle = MaterialMeshBundle; @@ -63,6 +67,14 @@ impl CubemapVisibleEntities { } } +#[derive(Component, Clone, Debug, Default, Reflect)] +#[reflect(Component)] +pub struct CascadesVisibleEntities { + /// The visible entities for each cascade frustrum, for each view. + #[reflect(ignore)] + pub entities: HashMap>, +} + /// A component bundle for [`PointLight`] entities. #[derive(Debug, Bundle, Default)] pub struct PointLightBundle { @@ -95,8 +107,10 @@ pub struct SpotLightBundle { #[derive(Debug, Bundle, Default)] pub struct DirectionalLightBundle { pub directional_light: DirectionalLight, - pub frustum: Frustum, - pub visible_entities: VisibleEntities, + pub frusta: CascadesFrusta, + pub cascades: Cascades, + pub cascade_shadow_config: CascadeShadowConfig, + pub visible_entities: CascadesVisibleEntities, pub transform: Transform, pub global_transform: GlobalTransform, /// Enables or disables the light diff --git a/crates/bevy_pbr/src/lib.rs b/crates/bevy_pbr/src/lib.rs index 992d2504f1d6d..95d09f3d6d6a2 100644 --- a/crates/bevy_pbr/src/lib.rs +++ b/crates/bevy_pbr/src/lib.rs @@ -183,6 +183,12 @@ impl Plugin for PbrPlugin { .after(CameraUpdateSystem) .after(ModifiesWindows), ) + .add_system_to_stage( + CoreStage::PostUpdate, + update_directional_light_cascades + .label(SimulationLightSystems::UpdateDirectionalLightCascades) + .after(TransformSystem::TransformPropagate), + ) .add_system_to_stage( CoreStage::PostUpdate, update_directional_light_frusta @@ -190,6 +196,7 @@ impl Plugin for PbrPlugin { // This must run after CheckVisibility because it relies on ComputedVisibility::is_visible() .after(VisibilitySystems::CheckVisibility) .after(TransformSystem::TransformPropagate) + .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. diff --git a/crates/bevy_pbr/src/light.rs b/crates/bevy_pbr/src/light.rs index abad3c9b395d8..07c0cd288b235 100644 --- a/crates/bevy_pbr/src/light.rs +++ b/crates/bevy_pbr/src/light.rs @@ -4,21 +4,23 @@ use bevy_ecs::prelude::*; use bevy_math::{Mat4, UVec2, UVec3, Vec2, Vec3, Vec3A, Vec3Swizzles, Vec4, Vec4Swizzles}; use bevy_reflect::prelude::*; use bevy_render::{ - camera::{Camera, CameraProjection, OrthographicProjection}, + camera::Camera, color::Color, extract_resource::ExtractResource, - primitives::{Aabb, CubemapFrusta, Frustum, Plane, Sphere}, + prelude::Projection, + primitives::{Aabb, CascadesFrusta, CubemapFrusta, Frustum, Plane, Sphere}, render_resource::BufferBindingType, renderer::RenderDevice, view::{ComputedVisibility, RenderLayers, VisibleEntities}, }; use bevy_transform::{components::GlobalTransform, prelude::Transform}; -use bevy_utils::tracing::warn; +use bevy_utils::{tracing::warn, HashMap}; use crate::{ - calculate_cluster_factors, spot_light_projection_matrix, spot_light_view_matrix, CubeMapFace, - CubemapVisibleEntities, ViewClusterBindings, CLUSTERED_FORWARD_STORAGE_BUFFER_COUNT, - CUBE_MAP_FACES, MAX_UNIFORM_BUFFER_POINT_LIGHTS, POINT_LIGHT_NEAR_Z, + calculate_cluster_factors, spot_light_projection_matrix, spot_light_view_matrix, + CascadesVisibleEntities, CubeMapFace, CubemapVisibleEntities, ViewClusterBindings, + 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. @@ -172,24 +174,11 @@ impl Default for SpotLight { /// /// To enable shadows, set the `shadows_enabled` property to `true`. /// -/// While directional lights contribute to the illumination of meshes regardless -/// 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. +/// 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). /// -/// The shadow volume is a _rectangular cuboid_, with left/right/bottom/top/near/far -/// planes controllable via the `shadow_projection` field. It is affected by the -/// 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. +/// To modify the cascade set up, such as the number of cascades or the maximum shadow distance, +/// change the [`CascadeShadowConfig`] component of the `[`DirectionalLightBundle`]. /// -/// 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: /// /// ``` @@ -198,12 +187,6 @@ impl Default for SpotLight { /// App::new() /// .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)] #[reflect(Component, Default)] pub struct DirectionalLight { @@ -211,8 +194,6 @@ pub struct DirectionalLight { /// Illuminance in lux pub illuminance: f32, 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, /// 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. @@ -221,20 +202,10 @@ pub struct DirectionalLight { impl Default for DirectionalLight { fn default() -> Self { - let size = 100.0; DirectionalLight { color: Color::rgb(1.0, 1.0, 1.0), illuminance: 100000.0, 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_normal_bias: Self::DEFAULT_SHADOW_NORMAL_BIAS, } @@ -256,9 +227,243 @@ pub struct DirectionalLightShadowMap { impl Default for DirectionalLightShadowMap { fn default() -> Self { #[cfg(feature = "webgl")] - return Self { size: 2048 }; + return Self { size: 1024 }; #[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, + /// The proportion of overlap each cascade has with the previous cascade. + pub overlap_proportion: f32, +} + +impl Default for CascadeShadowConfig { + fn default() -> Self { + Self::new(4, 5.0, 1000.0, 0.2) + } +} + +fn calculate_cascade_bounds( + num_cascades: usize, + nearest_bound: f32, + shadow_maximum_distance: f32, +) -> Vec { + if num_cascades == 1 { + return vec![shadow_maximum_distance]; + } + let base = (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. + 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"); + 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 { + /// Cascade configuration, per-view. + pub(crate) cascades: HashMap>, +} + +#[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. + /// Derived and stored separately from `view_transform` and `projection` for better 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, + views: Query<(Entity, &GlobalTransform, &Projection, &Camera)>, + mut lights: Query<( + &GlobalTransform, + &DirectionalLight, + &CascadeShadowConfig, + &mut Cascades, + )>, +) { + let views = views + .iter() + .filter_map(|x| match x { + // 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::>(); + + 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` + // an inverting it, which risks instability due to numerical precision, we directly form + // `world_to_cascde` as the text 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 orthogographic projection, centered at `near_plane_center`. + // NOTE: This is different from the text, 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 +498,7 @@ pub struct NotShadowReceiver; pub enum SimulationLightSystems { AddClusters, AssignLightsToClusters, + UpdateDirectionalLightCascades, UpdateLightFrusta, CheckLightVisibility, } @@ -1462,19 +1668,18 @@ fn project_to_plane_y(y_light: Sphere, y_plane: Plane, is_orthographic: bool) -> pub fn update_directional_light_frusta( mut views: Query< ( - &GlobalTransform, + &Cascades, &DirectionalLight, - &mut Frustum, &ComputedVisibility, + &mut CascadesFrusta, ), ( - Or<(Changed, Changed)>, // Prevents this query from conflicting with camera queries. Without, ), >, ) { - 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 // so if shadow mapping is disabled for this light, then the frustum is // not needed. @@ -1482,14 +1687,19 @@ pub fn update_directional_light_frusta( continue; } - let view_projection = directional_light.shadow_projection.get_projection_matrix() - * transform.compute_matrix().inverse(); - *frustum = Frustum::from_view_projection( - &view_projection, - &transform.translation(), - &transform.back(), - directional_light.shadow_projection.far(), - ); + frusta.frusta = cascades + .cascades + .iter() + .map(|(view, cascades)| { + ( + *view, + cascades + .iter() + .map(|c| Frustum::from_view_projection(&c.view_projection)) + .collect::>(), + ) + }) + .collect(); } } @@ -1528,7 +1738,7 @@ pub fn update_point_light_frusta( let view = view_translation * *view_rotation; let view_projection = projection * view.compute_matrix().inverse(); - *frustum = Frustum::from_view_projection( + *frustum = Frustum::from_view_projection_custom_far( &view_projection, &transform.translation(), &view_backward, @@ -1563,7 +1773,7 @@ pub fn update_spot_light_frusta( let spot_projection = spot_light_projection_matrix(spot_light.outer_angle); let view_projection = spot_projection * spot_view.inverse(); - *frustum = Frustum::from_view_projection( + *frustum = Frustum::from_view_projection_custom_far( &view_projection, &transform.translation(), &view_backward, @@ -1591,10 +1801,10 @@ pub fn check_light_mesh_visibility( mut directional_lights: Query< ( &DirectionalLight, - &Frustum, - &mut VisibleEntities, + &CascadesFrusta, + &mut CascadesVisibleEntities, Option<&RenderLayers>, - &ComputedVisibility, + &mut ComputedVisibility, ), Without, >, @@ -1628,13 +1838,34 @@ pub fn check_light_mesh_visibility( // Directional lights for ( directional_light, - frustum, + frusta, mut visible_entities, maybe_view_mask, light_computed_visibility, ) 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 if !directional_light.shadows_enabled || !light_computed_visibility.is_visible() { @@ -1655,18 +1886,32 @@ pub fn check_light_mesh_visibility( continue; } - // If we have an aabb and transform, do frustum culling - if let (Some(aabb), Some(transform)) = (maybe_aabb, maybe_transform) { - if !frustum.intersects_obb(aabb, &transform.compute_matrix(), true) { - continue; + for (view, view_frustra) in frusta.frusta.iter() { + let view_visible_entities = visible_entities + .entities + .get_mut(view) + .expect("per-view should have been inserted already"); + + for (frustum, frustum_visible_entities) in + view_frustra.iter().zip(view_visible_entities) + { + // If we have an aabb and transform, do frustum culling + if let (Some(aabb), Some(transform)) = (maybe_aabb, maybe_transform) { + // 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 { @@ -1724,7 +1969,7 @@ pub fn check_light_mesh_visibility( .iter() .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(); visible_entities.entities.push(entity); } @@ -1784,7 +2029,7 @@ pub fn check_light_mesh_visibility( 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(); visible_entities.entities.push(entity); } diff --git a/crates/bevy_pbr/src/render/depth.wgsl b/crates/bevy_pbr/src/render/depth.wgsl index 5a238cb1e5f21..d488a947e56a0 100644 --- a/crates/bevy_pbr/src/render/depth.wgsl +++ b/crates/bevy_pbr/src/render/depth.wgsl @@ -38,5 +38,9 @@ fn vertex(vertex: Vertex) -> VertexOutput { var out: VertexOutput; out.clip_position = mesh_position_local_to_clip(model, vec4(vertex.position, 1.0)); + #ifdef DEPTH_CLAMP + out.clip_position.z = min(out.clip_position.z, out.clip_position.w); + #endif + return out; } diff --git a/crates/bevy_pbr/src/render/light.rs b/crates/bevy_pbr/src/render/light.rs index 657534c30c220..e0204f2afeabc 100644 --- a/crates/bevy_pbr/src/render/light.rs +++ b/crates/bevy_pbr/src/render/light.rs @@ -1,8 +1,9 @@ use crate::{ - directional_light_order, point_light_order, AmbientLight, Clusters, CubemapVisibleEntities, - DirectionalLight, DirectionalLightShadowMap, DrawMesh, GlobalVisiblePointLights, MeshPipeline, - NotShadowCaster, PointLight, PointLightShadowMap, SetMeshBindGroup, SpotLight, - VisiblePointLights, SHADOW_SHADER_HANDLE, + directional_light_order, point_light_order, AmbientLight, Cascade, CascadeShadowConfig, + Cascades, CascadesVisibleEntities, Clusters, CubemapVisibleEntities, DirectionalLight, + DirectionalLightShadowMap, DrawMesh, GlobalVisiblePointLights, MeshPipeline, NotShadowCaster, + PointLight, PointLightShadowMap, SetMeshBindGroup, SpotLight, VisiblePointLights, + SHADOW_SHADER_HANDLE, }; use bevy_asset::Handle; use bevy_core_pipeline::core_3d::Transparent3d; @@ -10,9 +11,9 @@ use bevy_ecs::{ prelude::*, 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::{ - camera::{Camera, CameraProjection}, + camera::Camera, color::Color, mesh::{Mesh, MeshVertexBufferLayout}, render_asset::RenderAssets, @@ -61,15 +62,16 @@ pub struct ExtractedPointLight { spot_light_angles: Option<(f32, f32)>, } -#[derive(Component)] +#[derive(Component, Debug)] pub struct ExtractedDirectionalLight { color: Color, illuminance: f32, transform: GlobalTransform, - projection: Mat4, shadows_enabled: bool, shadow_depth_bias: f32, shadow_normal_bias: f32, + cascade_shadow_config: CascadeShadowConfig, + cascades: HashMap>, } #[derive(Copy, Clone, ShaderType, Default, Debug)] @@ -174,13 +176,22 @@ bitflags::bitflags! { } #[derive(Copy, Clone, ShaderType, Default, Debug)] -pub struct GpuDirectionalLight { +pub struct GpuDirectionalCascade { view_projection: Mat4, + texel_size: Vec4, + far_bound: f32, +} + +#[derive(Copy, Clone, ShaderType, Default, Debug)] +pub struct GpuDirectionalLight { + cascades: [GpuDirectionalCascade; MAX_CASCADES_PER_LIGHT], color: Vec4, dir_to_light: Vec3, flags: u32, shadow_depth_bias: f32, shadow_normal_bias: f32, + num_cascades: u32, + cascades_overlap_proportion: f32, } // NOTE: These must match the bit flags in bevy_pbr/src/render/mesh_view_types.wgsl! @@ -211,6 +222,10 @@ pub struct GpuLights { // 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_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; #[derive(Resource, Clone)] @@ -324,6 +339,12 @@ impl SpecializedMeshPipeline for ShadowPipeline { "MAX_DIRECTIONAL_LIGHTS".to_string(), MAX_DIRECTIONAL_LIGHTS as u32, )); + shader_defs.push(ShaderDefVal::UInt( + "MAX_CASCADES_PER_LIGHT".to_string(), + MAX_CASCADES_PER_LIGHT as u32, + )); + // Avoid clipping shadow casters that are behind the near plane. + shader_defs.push("DEPTH_CLAMP".into()); if layout.contains(Mesh::ATTRIBUTE_JOINT_INDEX) && layout.contains(Mesh::ATTRIBUTE_JOINT_WEIGHT) @@ -437,7 +458,9 @@ pub fn extract_lights( ( Entity, &DirectionalLight, - &VisibleEntities, + &CascadesVisibleEntities, + &Cascades, + &CascadeShadowConfig, &GlobalTransform, &ComputedVisibility, ), @@ -546,22 +569,20 @@ pub fn extract_lights( *previous_spot_lights_len = spot_lights_values.len(); commands.insert_or_spawn_batch(spot_lights_values); - for (entity, directional_light, visible_entities, transform, visibility) in - directional_lights.iter() + for ( + entity, + directional_light, + visible_entities, + cascades, + cascade_config, + transform, + visibility, + ) in directional_lights.iter() { if !visibility.is_visible() { 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 let render_visible_entities = visible_entities.clone(); commands.get_or_spawn(entity).insert(( @@ -569,11 +590,12 @@ pub fn extract_lights( color: directional_light.color, illuminance: directional_light.illuminance, transform: *transform, - projection: directional_light.shadow_projection.get_projection_matrix(), shadows_enabled: directional_light.shadows_enabled, shadow_depth_bias: directional_light.shadow_depth_bias, - shadow_normal_bias: directional_light.shadow_normal_bias - * directional_light_texel_size, + // The factor of SQRT_2 is for the worst-case diagonal offset + 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, )); @@ -696,6 +718,7 @@ pub struct LightMeta { pub enum LightEntity { Directional { light_entity: Entity, + cascade_index: usize, }, Point { light_entity: Entity, @@ -818,18 +841,18 @@ pub fn prepare_lights( .count() .min(max_texture_cubes); - let directional_shadow_maps_count = directional_lights + let directional_shadow_enabled_count = directional_lights .iter() .take(MAX_DIRECTIONAL_LIGHTS) .filter(|(_, light)| light.shadows_enabled) .count() - .min(max_texture_array_layers); + .min(max_texture_array_layers / MAX_CASCADES_PER_LIGHT); let spot_light_shadow_maps_count = point_lights .iter() .filter(|(_, light)| light.shadows_enabled && light.spot_light_angles.is_some()) .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 // - point-light vs spot-light, so that we can iterate point lights and spot lights in contiguous blocks in the fragment shader, @@ -931,7 +954,7 @@ pub fn prepare_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 .iter() .enumerate() @@ -940,13 +963,10 @@ pub fn prepare_lights( let mut flags = DirectionalLightFlags::NONE; // 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; } - // direction is negated to be ready for N.L - let dir_to_light = light.transform.back(); - // convert from illuminance (lux) to candelas // // exposure is hard coded at the moment but should be replaced @@ -959,21 +979,28 @@ pub fn prepare_lights( let exposure = 1.0 / (f32::powf(2.0, ev100) * 1.2); 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 view = light.transform.compute_matrix().inverse(); - // NOTE: This orthographic projection defines the volume within which shadows from a directional light can be cast - let projection = light.projection; + let num_cascades = light + .cascade_shadow_config + .bounds + .len() + .min(MAX_CASCADES_PER_LIGHT); + if index < directional_shadow_enabled_count { + num_directional_cascades_enabled += num_cascades; + } gpu_directional_lights[index] = GpuDirectionalLight { + // Filled in later. + cascades: [GpuDirectionalCascade::default(); MAX_CASCADES_PER_LIGHT], // premultiply color by intensity // 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, - dir_to_light, - // NOTE: * view is correct, it should not be view.inverse() here - view_projection: projection * view, + // direction is negated to be ready for N.L + dir_to_light: light.transform.back(), flags: flags.bits, shadow_depth_bias: light.shadow_depth_bias, shadow_normal_bias: light.shadow_normal_bias, + num_cascades: num_cascades as u32, + cascades_overlap_proportion: light.cascade_shadow_config.overlap_proportion, }; } @@ -1008,7 +1035,7 @@ pub fn prepare_lights( .min(render_device.limits().max_texture_dimension_2d), height: (directional_light_shadow_map.size as u32) .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) .max(1) as u32, }, @@ -1031,7 +1058,7 @@ pub fn prepare_lights( ); 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, ambient_color: Vec4::from_slice(&ambient_light.color.as_linear_rgba_f32()) * ambient_light.brightness, @@ -1046,7 +1073,8 @@ pub fn prepare_lights( // spotlight shadow maps are stored in the directional light array, starting at directional_shadow_maps_count. // 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. - spot_light_shadowmap_offset: directional_shadow_maps_count as i32 + spot_light_shadowmap_offset: (directional_shadow_enabled_count * MAX_CASCADES_PER_LIGHT) + as i32 - point_light_count as i32, }; @@ -1099,6 +1127,7 @@ pub fn prepare_lights( point_light_shadow_map.size as u32, ), transform: view_translation * *view_rotation, + view_projection: None, projection: cube_face_projection, hdr: false, }, @@ -1137,7 +1166,7 @@ pub fn prepare_lights( aspect: TextureAspect::All, base_mip_level: 0, 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), }); @@ -1156,6 +1185,7 @@ pub fn prepare_lights( ), transform: spot_view_transform, projection: spot_projection, + view_projection: None, hdr: false, }, RenderPhase::::default(), @@ -1167,47 +1197,71 @@ pub fn prepare_lights( } // directional lights + let mut directional_depth_texture_array_index = 0u32; for (light_index, &(light_entity, light)) in directional_lights .iter() .enumerate() - .take(directional_shadow_maps_count) + .take(directional_shadow_enabled_count) { - let depth_texture_view = - directional_light_depth_texture - .texture - .create_view(&TextureViewDescriptor { - label: Some("directional_light_shadow_map_texture_view"), - format: None, - dimension: Some(TextureViewDimension::D2), - aspect: TextureAspect::All, - base_mip_level: 0, - mip_level_count: None, - base_array_layer: light_index as u32, - array_layer_count: NonZeroU32::new(1), - }); + for (cascade_index, (cascade, bound)) in light + .cascades + .get(&entity) + .unwrap() + .iter() + .take(MAX_CASCADES_PER_LIGHT) + .zip(&light.cascade_shadow_config.bounds) + .enumerate() + { + gpu_lights.directional_lights[light_index].cascades[cascade_index] = + GpuDirectionalCascade { + view_projection: cascade.view_projection, + texel_size: Vec4::splat(cascade.texel_size), + far_bound: *bound, + }; - let view_light_entity = commands - .spawn(( - ShadowView { - depth_texture_view, - pass_name: format!("shadow pass directional light {light_index}"), - }, - ExtractedView { - viewport: UVec4::new( - 0, - 0, - directional_light_shadow_map.size as u32, - directional_light_shadow_map.size as u32, - ), - transform: light.transform, - projection: light.projection, - hdr: false, - }, - RenderPhase::::default(), - LightEntity::Directional { light_entity }, - )) - .id(); - view_lights.push(view_light_entity); + let depth_texture_view = + directional_light_depth_texture + .texture + .create_view(&TextureViewDescriptor { + label: Some("directional_light_shadow_map_array_texture_view"), + format: None, + dimension: Some(TextureViewDimension::D2), + aspect: TextureAspect::All, + base_mip_level: 0, + mip_level_count: None, + base_array_layer: directional_depth_texture_array_index, + array_layer_count: NonZeroU32::new(1), + }); + directional_depth_texture_array_index += 1; + + let view_light_entity = commands + .spawn(( + ShadowView { + depth_texture_view, + pass_name: format!( + "shadow pass directional light {light_index} cascade {cascade_index}"), + }, + 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::::default(), + LightEntity::Directional { + light_entity, + cascade_index, + }, + )) + .id(); + view_lights.push(view_light_entity); + } } let point_light_depth_texture_view = @@ -1627,21 +1681,29 @@ pub fn queue_shadows( render_meshes: Res>, mut pipelines: ResMut>, pipeline_cache: Res, - view_lights: Query<&ViewLightEntities>, + view_lights: Query<(Entity, &ViewLightEntities)>, mut view_light_shadow_phases: Query<(&LightEntity, &mut RenderPhase)>, point_light_entities: Query<&CubemapVisibleEntities, With>, - directional_light_entities: Query<&VisibleEntities, With>, + directional_light_entities: Query<&CascadesVisibleEntities, With>, spot_light_entities: Query<&VisibleEntities, With>, ) { - for view_lights in &view_lights { + for (entity, view_lights) in &view_lights { let draw_shadow_mesh = shadow_draw_functions.read().id::(); for view_light_entity in view_lights.lights.iter().copied() { let (light_entity, mut shadow_phase) = view_light_shadow_phases.get_mut(view_light_entity).unwrap(); 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) - .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 { light_entity, face_index, diff --git a/crates/bevy_pbr/src/render/mesh.rs b/crates/bevy_pbr/src/render/mesh.rs index d1fb3a3c62162..1b14b300ad9c5 100644 --- a/crates/bevy_pbr/src/render/mesh.rs +++ b/crates/bevy_pbr/src/render/mesh.rs @@ -1,7 +1,7 @@ use crate::{ GlobalLightMeta, GpuLights, GpuPointLights, LightMeta, NotShadowCaster, NotShadowReceiver, 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_asset::{load_internal_asset, Assets, Handle, HandleUntyped}; @@ -646,6 +646,10 @@ impl SpecializedMeshPipeline for MeshPipeline { "MAX_DIRECTIONAL_LIGHTS".to_string(), 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) { shader_defs.push("VERTEX_UVS".into()); diff --git a/crates/bevy_pbr/src/render/mesh_view_types.wgsl b/crates/bevy_pbr/src/render/mesh_view_types.wgsl index c6316f59652dc..86074800ce62a 100644 --- a/crates/bevy_pbr/src/render/mesh_view_types.wgsl +++ b/crates/bevy_pbr/src/render/mesh_view_types.wgsl @@ -28,14 +28,22 @@ struct PointLight { let POINT_LIGHT_FLAGS_SHADOWS_ENABLED_BIT: u32 = 1u; let POINT_LIGHT_FLAGS_SPOT_LIGHT_Y_NEGATIVE: u32 = 2u; -struct DirectionalLight { +struct DirectionalCascade { view_projection: mat4x4, + texel_size: vec4, + far_bound: f32, +} + +struct DirectionalLight { + cascades: array, color: vec4, direction_to_light: vec3, // 'flags' is a bit field indicating various options. u32 is 32 bits so we have up to 32 options. flags: u32, shadow_depth_bias: f32, shadow_normal_bias: f32, + num_cascades: u32, + cascades_overlap_proportion: f32, }; let DIRECTIONAL_LIGHT_FLAGS_SHADOWS_ENABLED_BIT: u32 = 1u; diff --git a/crates/bevy_pbr/src/render/pbr_functions.wgsl b/crates/bevy_pbr/src/render/pbr_functions.wgsl index bb638e6ea2979..f4a4f947310b6 100644 --- a/crates/bevy_pbr/src/render/pbr_functions.wgsl +++ b/crates/bevy_pbr/src/render/pbr_functions.wgsl @@ -224,7 +224,7 @@ fn pbr( var shadow: f32 = 1.0; if ((mesh.flags & MESH_FLAGS_SHADOW_RECEIVER_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); light_accum = light_accum + light_contrib * shadow; diff --git a/crates/bevy_pbr/src/render/shadows.wgsl b/crates/bevy_pbr/src/render/shadows.wgsl index 34f4c3b627f48..b2422af0e2b98 100644 --- a/crates/bevy_pbr/src/render/shadows.wgsl +++ b/crates/bevy_pbr/src/render/shadows.wgsl @@ -98,15 +98,27 @@ fn fetch_spot_shadow(light_id: u32, frag_position: vec4, surface_normal: ve #endif } -fn fetch_directional_shadow(light_id: u32, frag_position: vec4, surface_normal: vec3) -> f32 { - let light = &lights.directional_lights[light_id]; +fn get_cascade_index(light_id: u32, view_z: f32) -> u32 { + var 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, surface_normal: vec3) -> f32 { + var light = lights.directional_lights[light_id]; + let cascade = light.cascades[cascade_index]; // The normal bias is scaled to the texel size. - let normal_offset = (*light).shadow_normal_bias * surface_normal.xyz; - let depth_offset = (*light).shadow_depth_bias * (*light).direction_to_light.xyz; + let normal_offset = light.shadow_normal_bias * cascade.texel_size.x * surface_normal.xyz; + let depth_offset = light.shadow_depth_bias * light.direction_to_light.xyz; let offset_position = vec4(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) { return 1.0; } @@ -127,8 +139,44 @@ fn fetch_directional_shadow(light_id: u32, frag_position: vec4, surface_nor // 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. #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 - 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_id * #{MAX_CASCADES_PER_LIGHT}u + cascade_index), + depth + ); #endif } + +fn fetch_directional_shadow(light_id: u32, frag_position: vec4, surface_normal: vec3, view_z: f32) -> f32 { + var 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; + } + + // return clamp(f32(cascade_index) * 0.3, 0.0, 1.0); + + var shadow = sample_cascade(light_id, cascade_index, frag_position, surface_normal); + + // Blend with the next cascade, if there's 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; +} diff --git a/crates/bevy_render/src/camera/camera.rs b/crates/bevy_render/src/camera/camera.rs index 53252ff453588..27715d749236c 100644 --- a/crates/bevy_render/src/camera/camera.rs +++ b/crates/bevy_render/src/camera/camera.rs @@ -559,6 +559,7 @@ pub fn extract_cameras( ExtractedView { projection: camera.projection_matrix(), transform: *transform, + view_projection: None, hdr: camera.hdr, viewport: UVec4::new( viewport_origin.x, diff --git a/crates/bevy_render/src/primitives/mod.rs b/crates/bevy_render/src/primitives/mod.rs index ae82c844be6ce..1c1f9d5f0d565 100644 --- a/crates/bevy_render/src/primitives/mod.rs +++ b/crates/bevy_render/src/primitives/mod.rs @@ -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_reflect::Reflect; +use bevy_utils::HashMap; /// An Axis-Aligned Bounding Box #[derive(Component, Clone, Debug, Default, Reflect)] @@ -134,17 +135,33 @@ pub struct Frustum { } impl 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. Slight modification has been made for when - // the far plane is infinite but we still want to cull to a far plane. + /// Returns a frustrum derived from `view_projection`. + #[inline] + pub fn from_view_projection(view_projection: &Mat4) -> Self { + let mut frustrum = Frustum::from_view_projection_no_far(view_projection); + frustrum.planes[5] = Plane::new(view_projection.row(2)); + frustrum + } + + /// Returns a frustrum derived from `view_projection`, but with a custom + /// far plane. #[inline] - pub fn from_view_projection( + pub fn from_view_projection_custom_far( view_projection: &Mat4, view_translation: &Vec3, view_backward: &Vec3, far: f32, ) -> Self { + let mut frustrum = Frustum::from_view_projection_no_far(view_projection); + let far_center = *view_translation - far * *view_backward; + frustrum.planes[5] = Plane::new(view_backward.extend(-view_backward.dot(far_center))); + frustrum + } + + // 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 mut planes = [Plane::default(); 6]; for (i, plane) in planes.iter_mut().enumerate().take(5) { @@ -155,8 +172,6 @@ impl Frustum { row3 - row }); } - let far_center = *view_translation - far * *view_backward; - planes[5] = Plane::new(view_backward.extend(-view_backward.dot(far_center))); Self { planes } } @@ -173,7 +188,13 @@ impl Frustum { } #[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 axes = [ Vec3A::from(model_to_world.x_axis), @@ -181,8 +202,13 @@ impl Frustum { Vec3A::from(model_to_world.z_axis), ]; - let max = if intersect_far { 6 } else { 5 }; - for plane in &self.planes[..max] { + for (idx, plane) in self.planes.into_iter().enumerate() { + if idx == 4 && !intersect_near { + continue; + } + if idx == 5 && !intersect_far { + continue; + } let p_normal = Vec3A::from(plane.normal_d()); let relative_radius = aabb.relative_radius(&p_normal, &axes); 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>, +} + #[cfg(test)] mod tests { use super::*; diff --git a/crates/bevy_render/src/view/mod.rs b/crates/bevy_render/src/view/mod.rs index 4584f5359f6e8..9dcc87c5f4709 100644 --- a/crates/bevy_render/src/view/mod.rs +++ b/crates/bevy_render/src/view/mod.rs @@ -91,6 +91,7 @@ impl Msaa { pub struct ExtractedView { pub projection: Mat4, pub transform: GlobalTransform, + pub view_projection: Option, pub hdr: bool, // uvec4(origin.x, origin.y, width, height) pub viewport: UVec4, @@ -251,7 +252,9 @@ fn prepare_view_uniforms( let inverse_view = view.inverse(); let view_uniforms = ViewUniformOffset { 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, view, inverse_view, diff --git a/crates/bevy_render/src/view/visibility/mod.rs b/crates/bevy_render/src/view/visibility/mod.rs index 30f28b8638c4c..d6e6e43594b96 100644 --- a/crates/bevy_render/src/view/visibility/mod.rs +++ b/crates/bevy_render/src/view/visibility/mod.rs @@ -281,7 +281,7 @@ pub fn update_frusta( for (transform, projection, mut frustum) in &mut views { let view_projection = projection.get_projection_matrix() * transform.compute_matrix().inverse(); - *frustum = Frustum::from_view_projection( + *frustum = Frustum::from_view_projection_custom_far( &view_projection, &transform.translation(), &transform.back(), @@ -407,7 +407,7 @@ pub fn check_visibility( return; } // 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; } } diff --git a/crates/bevy_ui/src/render/mod.rs b/crates/bevy_ui/src/render/mod.rs index 17d6719b4a083..ee70cf5377f83 100644 --- a/crates/bevy_ui/src/render/mod.rs +++ b/crates/bevy_ui/src/render/mod.rs @@ -275,6 +275,7 @@ pub fn extract_default_ui_camera_view( 0.0, UI_CAMERA_FAR + UI_CAMERA_TRANSFORM_OFFSET, ), + view_projection: None, hdr: camera.hdr, viewport: UVec4::new( physical_origin.x, diff --git a/examples/3d/fxaa.rs b/examples/3d/fxaa.rs index fd1ca6be2e327..c5a60db917ba1 100644 --- a/examples/3d/fxaa.rs +++ b/examples/3d/fxaa.rs @@ -73,15 +73,6 @@ fn setup( const HALF_SIZE: f32 = 2.0; commands.spawn(DirectionalLightBundle { 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, ..default() }, diff --git a/examples/3d/lighting.rs b/examples/3d/lighting.rs index a5a1c2b215523..5ea53405e27ff 100644 --- a/examples/3d/lighting.rs +++ b/examples/3d/lighting.rs @@ -186,19 +186,8 @@ fn setup( }); // directional 'sun' light - const HALF_SIZE: f32 = 10.0; commands.spawn(DirectionalLightBundle { 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, ..default() }, diff --git a/examples/3d/load_gltf.rs b/examples/3d/load_gltf.rs index 1a075e70b0ec9..3981a32d73a8d 100644 --- a/examples/3d/load_gltf.rs +++ b/examples/3d/load_gltf.rs @@ -21,18 +21,8 @@ fn setup(mut commands: Commands, asset_server: Res) { transform: Transform::from_xyz(0.7, 0.7, 1.0).looking_at(Vec3::new(0.0, 0.3, 0.0), Vec3::Y), ..default() }); - const HALF_SIZE: f32 = 1.0; commands.spawn(DirectionalLightBundle { 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, ..default() }, diff --git a/examples/3d/shadow_biases.rs b/examples/3d/shadow_biases.rs index 4ddbde7d9711e..9297900216a3e 100644 --- a/examples/3d/shadow_biases.rs +++ b/examples/3d/shadow_biases.rs @@ -69,15 +69,6 @@ fn setup( commands.spawn(DirectionalLightBundle { directional_light: DirectionalLight { 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_normal_bias: 0.0, shadows_enabled: true, diff --git a/examples/3d/shadow_caster_receiver.rs b/examples/3d/shadow_caster_receiver.rs index d779db77aae3f..3488ed48b383e 100644 --- a/examples/3d/shadow_caster_receiver.rs +++ b/examples/3d/shadow_caster_receiver.rs @@ -100,15 +100,6 @@ fn setup( commands.spawn(DirectionalLightBundle { directional_light: DirectionalLight { 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, ..default() }, From dfc7b472f3103efc7cc8920f496ef221cf45e3ae Mon Sep 17 00:00:00 2001 From: Daniel Chia Date: Fri, 30 Dec 2022 15:46:12 -0800 Subject: [PATCH 02/16] fix shader indexing bug + make config ctor pub --- crates/bevy_pbr/src/light.rs | 2 +- crates/bevy_pbr/src/render/light.rs | 9 +++++---- crates/bevy_pbr/src/render/mesh_view_types.wgsl | 1 + crates/bevy_pbr/src/render/shadows.wgsl | 2 +- 4 files changed, 8 insertions(+), 6 deletions(-) diff --git a/crates/bevy_pbr/src/light.rs b/crates/bevy_pbr/src/light.rs index 07c0cd288b235..211a9e98fda1f 100644 --- a/crates/bevy_pbr/src/light.rs +++ b/crates/bevy_pbr/src/light.rs @@ -267,7 +267,7 @@ 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. - fn new( + pub fn new( num_cascades: usize, nearest_bound: f32, shadow_maximum_distance: f32, diff --git a/crates/bevy_pbr/src/render/light.rs b/crates/bevy_pbr/src/render/light.rs index e0204f2afeabc..919b10607eff4 100644 --- a/crates/bevy_pbr/src/render/light.rs +++ b/crates/bevy_pbr/src/render/light.rs @@ -192,6 +192,7 @@ pub struct GpuDirectionalLight { 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! @@ -984,10 +985,6 @@ pub fn prepare_lights( .bounds .len() .min(MAX_CASCADES_PER_LIGHT); - if index < directional_shadow_enabled_count { - num_directional_cascades_enabled += num_cascades; - } - gpu_directional_lights[index] = GpuDirectionalLight { // Filled in later. cascades: [GpuDirectionalCascade::default(); MAX_CASCADES_PER_LIGHT], @@ -1001,7 +998,11 @@ pub fn prepare_lights( 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); diff --git a/crates/bevy_pbr/src/render/mesh_view_types.wgsl b/crates/bevy_pbr/src/render/mesh_view_types.wgsl index 86074800ce62a..d0d9321262e62 100644 --- a/crates/bevy_pbr/src/render/mesh_view_types.wgsl +++ b/crates/bevy_pbr/src/render/mesh_view_types.wgsl @@ -44,6 +44,7 @@ struct DirectionalLight { 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; diff --git a/crates/bevy_pbr/src/render/shadows.wgsl b/crates/bevy_pbr/src/render/shadows.wgsl index b2422af0e2b98..ae28affe04f77 100644 --- a/crates/bevy_pbr/src/render/shadows.wgsl +++ b/crates/bevy_pbr/src/render/shadows.wgsl @@ -150,7 +150,7 @@ fn sample_cascade(light_id: u32, cascade_index: u32, frag_position: vec4, s directional_shadow_textures, directional_shadow_textures_sampler, light_local, - i32(light_id * #{MAX_CASCADES_PER_LIGHT}u + cascade_index), + i32(light.depth_texture_base_index + cascade_index), depth ); #endif From ac0f989dc0e33ae5f4c88ff4f008b1b8784f40ac Mon Sep 17 00:00:00 2001 From: Daniel Chia Date: Fri, 30 Dec 2022 16:00:14 -0800 Subject: [PATCH 03/16] clippy, fix compile errors --- crates/bevy_pbr/src/light.rs | 2 +- examples/3d/fxaa.rs | 1 - examples/tools/scene_viewer/main.rs | 17 -------------- .../tools/scene_viewer/scene_viewer_plugin.rs | 22 ------------------- 4 files changed, 1 insertion(+), 41 deletions(-) diff --git a/crates/bevy_pbr/src/light.rs b/crates/bevy_pbr/src/light.rs index 211a9e98fda1f..dfb661494f37a 100644 --- a/crates/bevy_pbr/src/light.rs +++ b/crates/bevy_pbr/src/light.rs @@ -177,7 +177,7 @@ impl Default for SpotLight { /// 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). /// /// To modify the cascade set up, such as the number of cascades or the maximum shadow distance, -/// change the [`CascadeShadowConfig`] component of the `[`DirectionalLightBundle`]. +/// change the [`CascadeShadowConfig`] component of the [`DirectionalLightBundle`]. /// /// To control the resolution of the shadow maps, use the [`DirectionalLightShadowMap`] resource: /// diff --git a/examples/3d/fxaa.rs b/examples/3d/fxaa.rs index c5a60db917ba1..cc09d08ab41ef 100644 --- a/examples/3d/fxaa.rs +++ b/examples/3d/fxaa.rs @@ -70,7 +70,6 @@ fn setup( }); // light - const HALF_SIZE: f32 = 2.0; commands.spawn(DirectionalLightBundle { directional_light: DirectionalLight { shadows_enabled: true, diff --git a/examples/tools/scene_viewer/main.rs b/examples/tools/scene_viewer/main.rs index a87101dd5f63f..d747f66638753 100644 --- a/examples/tools/scene_viewer/main.rs +++ b/examples/tools/scene_viewer/main.rs @@ -132,26 +132,9 @@ fn setup_scene_after_load( // Spawn a default light if the scene does not have one 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"); commands.spawn(DirectionalLightBundle { 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, ..default() }, diff --git a/examples/tools/scene_viewer/scene_viewer_plugin.rs b/examples/tools/scene_viewer/scene_viewer_plugin.rs index 7c23de67e88b0..3e5236464562d 100644 --- a/examples/tools/scene_viewer/scene_viewer_plugin.rs +++ b/examples/tools/scene_viewer/scene_viewer_plugin.rs @@ -198,35 +198,13 @@ fn keyboard_animation_control( } } -const SCALE_STEP: f32 = 0.1; - fn update_lights( key_input: Res>, time: Res