From 1bd3d8576941d09fc43fef901e1bbbf45b24c544 Mon Sep 17 00:00:00 2001 From: Marco Buono Date: Fri, 4 Nov 2022 20:12:26 +0000 Subject: [PATCH] Take DirectionalLight's GlobalTransform into account when calculating shadow map volume (not just direction) (#6384) # Objective This PR fixes #5789, by enabling movable (and scalable) directional light shadow volumes. ## Solution This PR changes `ExtractedDirectionalLight` to hold a copy of the `DirectionalLight` entity's `GlobalTransform`, instead of just a `direction` vector. This allows the shadow map volume (as defined by the light's `shadow_projection` field) to be transformed honoring translation _and_ scale transforms, and not just rotation. It also augments the texel size calculation (used to determine the `shadow_normal_bias`) so that it now takes into account the upper bound of the x/y/z scale of the `GlobalTransform`. This change makes the directional light extraction code more consistent with point and spot lights (that already use `transform`), and allows easily moving and scaling the shadow volume along with a player entity based on camera distance/angle, immediately enabling more real world use cases until we have a more sophisticated adaptive implementation, such as the one described in #3629. **Note:** While it was previously possible to update the projection achieving a similar effect, depending on the light direction and distance to the origin, the fact that the shadow map camera was always positioned at the origin with a hardcoded `Vec3::Y` up value meant you would get sub-optimal or inconsistent/incorrect results. --- ## Changelog ### Changed - `DirectionalLight` shadow volumes now honor translation and scale transforms ## Migration Guide - If your directional lights were positioned at the origin and not scaled (the default, most common scenario) no changes are needed on your part; it just works as before; - If you previously had a system for dynamically updating directional light shadow projections, you might now be able to simplify your code by updating the directional light entity's transform instead; - In the unlikely scenario that a scene with directional lights that previously rendered shadows correctly has missing shadows, make sure your directional lights are positioned at (0, 0, 0) and are not scaled to a size that's too large or too small. --- crates/bevy_pbr/src/light.rs | 39 +++++++++++++++++++++++++++++ crates/bevy_pbr/src/render/light.rs | 38 +++++++++++----------------- 2 files changed, 53 insertions(+), 24 deletions(-) diff --git a/crates/bevy_pbr/src/light.rs b/crates/bevy_pbr/src/light.rs index 39e8a6cf4a5c0..0d2efc2557e90 100644 --- a/crates/bevy_pbr/src/light.rs +++ b/crates/bevy_pbr/src/light.rs @@ -167,6 +167,43 @@ impl Default for SpotLight { /// | 32,000–100,000 | Direct sunlight | /// /// Source: [Wikipedia](https://en.wikipedia.org/wiki/Lux) +/// +/// ## Shadows +/// +/// 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. +/// +/// 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. +/// +/// 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: +/// +/// ``` +/// # use bevy_app::prelude::*; +/// # use bevy_pbr::DirectionalLightShadowMap; +/// 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 { @@ -174,6 +211,7 @@ 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 @@ -208,6 +246,7 @@ impl DirectionalLight { pub const DEFAULT_SHADOW_NORMAL_BIAS: f32 = 0.6; } +/// Controls the resolution of [`DirectionalLight`] shadow maps. #[derive(Resource, Clone, Debug, Reflect)] #[reflect(Resource)] pub struct DirectionalLightShadowMap { diff --git a/crates/bevy_pbr/src/render/light.rs b/crates/bevy_pbr/src/render/light.rs index d314116c83952..b11aecff4a6bc 100644 --- a/crates/bevy_pbr/src/render/light.rs +++ b/crates/bevy_pbr/src/render/light.rs @@ -10,7 +10,7 @@ use bevy_ecs::{ prelude::*, system::{lifetimeless::*, SystemParamItem}, }; -use bevy_math::{Mat4, UVec3, UVec4, Vec2, Vec3, Vec3Swizzles, Vec4, Vec4Swizzles}; +use bevy_math::{Mat4, UVec3, UVec4, Vec2, Vec3, Vec3A, Vec3Swizzles, Vec4, Vec4Swizzles}; use bevy_render::{ camera::{Camera, CameraProjection}, color::Color, @@ -66,7 +66,7 @@ pub struct ExtractedPointLight { pub struct ExtractedDirectionalLight { color: Color, illuminance: f32, - direction: Vec3, + transform: GlobalTransform, projection: Mat4, shadows_enabled: bool, shadow_depth_bias: f32, @@ -550,32 +550,27 @@ pub fn extract_lights( continue; } - // Calulate the directional light shadow map texel size using the largest x,y dimension of + // 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 largest_dimension = (directional_light.shadow_projection.right - - directional_light.shadow_projection.left) - .max( - directional_light.shadow_projection.top - - directional_light.shadow_projection.bottom, - ); - let directional_light_texel_size = - largest_dimension / directional_light_shadow_map.size as f32; + 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(( ExtractedDirectionalLight { color: directional_light.color, illuminance: directional_light.illuminance, - direction: transform.forward(), + transform: *transform, projection: directional_light.shadow_projection.get_projection_matrix(), shadows_enabled: directional_light.shadows_enabled, shadow_depth_bias: directional_light.shadow_depth_bias, - // The factor of SQRT_2 is for the worst-case diagonal offset shadow_normal_bias: directional_light.shadow_normal_bias - * directional_light_texel_size - * std::f32::consts::SQRT_2, + * directional_light_texel_size, }, render_visible_entities, )); @@ -947,7 +942,7 @@ pub fn prepare_lights( } // direction is negated to be ready for N.L - let dir_to_light = -light.direction; + let dir_to_light = light.transform.back(); // convert from illuminance (lux) to candelas // @@ -961,9 +956,8 @@ pub fn prepare_lights( let exposure = 1.0 / (f32::powf(2.0, ev100) * 1.2); let intensity = light.illuminance * exposure; - // NOTE: A directional light seems to have to have an eye position on the line along the direction of the light - // through the world origin. I (Rob Swain) do not yet understand why it cannot be translated away from this. - let view = Mat4::look_at_rh(Vec3::ZERO, light.direction, Vec3::Y); + // 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; @@ -1175,10 +1169,6 @@ pub fn prepare_lights( .enumerate() .take(directional_shadow_maps_count) { - // NOTE: A directional light seems to have to have an eye position on the line along the direction of the light - // through the world origin. I (Rob Swain) do not yet understand why it cannot be translated away from this. - let view = Mat4::look_at_rh(Vec3::ZERO, light.direction, Vec3::Y); - let depth_texture_view = directional_light_depth_texture .texture @@ -1206,7 +1196,7 @@ pub fn prepare_lights( directional_light_shadow_map.size as u32, directional_light_shadow_map.size as u32, ), - transform: GlobalTransform::from(view.inverse()), + transform: light.transform, projection: light.projection, hdr: false, },