Per-light toggleable shadow mapping (#3126)

# Objective

Allow shadow mapping to be enabled/disabled per-light.

## Solution

- NOTE: This PR is on top of https://github.com/bevyengine/bevy/pull/3072
- Add `shadows_enabled` boolean property to `PointLight` and `DirectionalLight` components.
- Do not update the frusta for the light if shadows are disabled.
- Do not check for visible entities for the light if shadows are disabled.
- Do not fetch shadows for lights with shadows disabled.
- I reworked a few types for clarity: `ViewLight` -> `ShadowView`, the bulk of `ViewLights` members -> `ViewShadowBindings`, the entities Vec in `ViewLights` -> `ViewLightEntities`, the uniform offset in `ViewLights` for `GpuLights` -> `ViewLightsUniformOffset`

Co-authored-by: Carter Anderson <mcanders1@gmail.com>
This commit is contained in:
Robert Swain 2021-11-19 21:16:58 +00:00
parent 2e79951659
commit a7729319cc
5 changed files with 204 additions and 103 deletions

View file

@ -33,6 +33,7 @@ pub struct PointLight {
pub intensity: f32,
pub range: f32,
pub radius: f32,
pub shadows_enabled: bool,
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 can be small close to the camera and gets larger further
@ -48,6 +49,7 @@ impl Default for PointLight {
intensity: 800.0, // Roughly a 60W non-halogen incandescent bulb
range: 20.0,
radius: 0.0,
shadows_enabled: false,
shadow_depth_bias: Self::DEFAULT_SHADOW_DEPTH_BIAS,
shadow_normal_bias: Self::DEFAULT_SHADOW_NORMAL_BIAS,
}
@ -101,6 +103,7 @@ pub struct DirectionalLight {
pub color: Color,
/// Illuminance in lux
pub illuminance: f32,
pub shadows_enabled: bool,
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
@ -114,6 +117,7 @@ impl Default for DirectionalLight {
DirectionalLight {
color: Color::rgb(1.0, 1.0, 1.0),
illuminance: 100000.0,
shadows_enabled: false,
shadow_projection: OrthographicProjection {
left: -size,
right: size,
@ -178,6 +182,13 @@ pub fn update_directional_light_frusta(
mut views: Query<(&GlobalTransform, &DirectionalLight, &mut Frustum)>,
) {
for (transform, directional_light, mut frustum) in views.iter_mut() {
// The frustum is used for culling meshes to the light for shadow mapping
// so if shadow mapping is disabled for this light, then the frustum is
// not needed.
if !directional_light.shadows_enabled {
continue;
}
let view_projection = directional_light.shadow_projection.get_projection_matrix()
* transform.compute_matrix().inverse();
*frustum = Frustum::from_view_projection(
@ -199,6 +210,13 @@ pub fn update_point_light_frusta(
.collect::<Vec<_>>();
for (transform, point_light, mut cubemap_frusta) in views.iter_mut() {
// The frusta are used for culling meshes to the light for shadow mapping
// so if shadow mapping is disabled for this light, then the frusta are
// not needed.
if !point_light.shadows_enabled {
continue;
}
// ignore scale because we don't want to effectively scale light radius and range
// by applying those as a view transform to shadow map rendering of objects
// and ignore rotation because we want the shadow map projections to align with the axes
@ -227,10 +245,12 @@ pub fn check_light_visibility(
&mut CubemapVisibleEntities,
Option<&RenderLayers>,
)>,
mut directional_lights: Query<
(&Frustum, &mut VisibleEntities, Option<&RenderLayers>),
With<DirectionalLight>,
>,
mut directional_lights: Query<(
&DirectionalLight,
&Frustum,
&mut VisibleEntities,
Option<&RenderLayers>,
)>,
mut visible_entity_query: Query<
(
Entity,
@ -244,8 +264,16 @@ pub fn check_light_visibility(
>,
) {
// Directonal lights
for (frustum, mut visible_entities, maybe_view_mask) in directional_lights.iter_mut() {
for (directional_light, frustum, mut visible_entities, maybe_view_mask) in
directional_lights.iter_mut()
{
visible_entities.entities.clear();
// NOTE: If shadow mapping is disabled for the light then it must have no visible entities
if !directional_light.shadows_enabled {
continue;
}
let view_mask = maybe_view_mask.copied().unwrap_or_default();
for (
@ -288,6 +316,12 @@ pub fn check_light_visibility(
for visible_entities in cubemap_visible_entities.iter_mut() {
visible_entities.entities.clear();
}
// NOTE: If shadow mapping is disabled for the light then it must have no visible entities
if !point_light.shadows_enabled {
continue;
}
let view_mask = maybe_view_mask.copied().unwrap_or_default();
let light_sphere = Sphere {
center: transform.translation,

View file

@ -50,6 +50,7 @@ pub struct ExtractedPointLight {
range: f32,
radius: f32,
transform: GlobalTransform,
shadows_enabled: bool,
shadow_depth_bias: f32,
shadow_normal_bias: f32,
}
@ -61,6 +62,7 @@ pub struct ExtractedDirectionalLight {
illuminance: f32,
direction: Vec3,
projection: Mat4,
shadows_enabled: bool,
shadow_depth_bias: f32,
shadow_normal_bias: f32,
}
@ -77,20 +79,42 @@ pub struct GpuPointLight {
radius: f32,
near: f32,
far: f32,
flags: u32,
shadow_depth_bias: f32,
shadow_normal_bias: f32,
}
// NOTE: These must match the bit flags in bevy_pbr2/src/render/pbr.frag!
bitflags::bitflags! {
#[repr(transparent)]
struct PointLightFlags: u32 {
const SHADOWS_ENABLED = (1 << 0);
const NONE = 0;
const UNINITIALIZED = 0xFFFF;
}
}
#[repr(C)]
#[derive(Copy, Clone, AsStd140, Default, Debug)]
pub struct GpuDirectionalLight {
view_projection: Mat4,
color: Vec4,
dir_to_light: Vec3,
flags: u32,
shadow_depth_bias: f32,
shadow_normal_bias: f32,
}
// NOTE: These must match the bit flags in bevy_pbr2/src/render/pbr.frag!
bitflags::bitflags! {
#[repr(transparent)]
struct DirectionalLightFlags: u32 {
const SHADOWS_ENABLED = (1 << 0);
const NONE = 0;
const UNINITIALIZED = 0xFFFF;
}
}
#[repr(C)]
#[derive(Copy, Clone, Debug, AsStd140)]
pub struct GpuLights {
@ -328,6 +352,7 @@ pub fn extract_lights(
range: point_light.range,
radius: point_light.radius,
transform: *transform,
shadows_enabled: point_light.shadows_enabled,
shadow_depth_bias: point_light.shadow_depth_bias,
// The factor of SQRT_2 is for the worst-case diagonal offset
shadow_normal_bias: point_light.shadow_normal_bias
@ -357,6 +382,7 @@ pub fn extract_lights(
illuminance: directional_light.illuminance,
direction: transform.forward(),
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
@ -424,18 +450,24 @@ fn face_index_to_name(face_index: usize) -> &'static str {
}
}
pub struct ViewLight {
pub struct ShadowView {
pub depth_texture_view: TextureView,
pub pass_name: String,
}
pub struct ViewLights {
pub struct ViewShadowBindings {
pub point_light_depth_texture: Texture,
pub point_light_depth_texture_view: TextureView,
pub directional_light_depth_texture: Texture,
pub directional_light_depth_texture_view: TextureView,
}
pub struct ViewLightEntities {
pub lights: Vec<Entity>,
pub gpu_light_binding_index: u32,
}
pub struct ViewLightsUniformOffset {
pub offset: u32,
}
#[derive(Default)]
@ -524,54 +556,59 @@ pub fn prepare_lights(
};
// TODO: this should select lights based on relevance to the view instead of the first ones that show up in a query
for (light_index, (light_entity, light)) in
point_lights.iter().enumerate().take(MAX_POINT_LIGHTS)
{
for (light_index, (light_entity, light)) in point_lights.iter().enumerate() {
// ignore scale because we don't want to effectively scale light radius and range
// by applying those as a view transform to shadow map rendering of objects
// and ignore rotation because we want the shadow map projections to align with the axes
let view_translation = GlobalTransform::from_translation(light.transform.translation);
for (face_index, view_rotation) in cube_face_rotations.iter().enumerate() {
let depth_texture_view =
point_light_depth_texture
.texture
.create_view(&TextureViewDescriptor {
label: Some("point_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 * 6 + face_index) as u32,
array_layer_count: NonZeroU32::new(1),
});
if light.shadows_enabled {
for (face_index, view_rotation) in cube_face_rotations.iter().enumerate() {
let depth_texture_view =
point_light_depth_texture
.texture
.create_view(&TextureViewDescriptor {
label: Some("point_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 * 6 + face_index) as u32,
array_layer_count: NonZeroU32::new(1),
});
let view_light_entity = commands
.spawn()
.insert_bundle((
ViewLight {
depth_texture_view,
pass_name: format!(
"shadow pass point light {} {}",
light_index,
face_index_to_name(face_index)
),
},
ExtractedView {
width: point_light_shadow_map.size as u32,
height: point_light_shadow_map.size as u32,
transform: view_translation * *view_rotation,
projection: cube_face_projection,
},
RenderPhase::<Shadow>::default(),
LightEntity::Point {
light_entity,
face_index,
},
))
.id();
view_lights.push(view_light_entity);
let view_light_entity = commands
.spawn()
.insert_bundle((
ShadowView {
depth_texture_view,
pass_name: format!(
"shadow pass point light {} {}",
light_index,
face_index_to_name(face_index)
),
},
ExtractedView {
width: point_light_shadow_map.size as u32,
height: point_light_shadow_map.size as u32,
transform: view_translation * *view_rotation,
projection: cube_face_projection,
},
RenderPhase::<Shadow>::default(),
LightEntity::Point {
light_entity,
face_index,
},
))
.id();
view_lights.push(view_light_entity);
}
}
let mut flags = PointLightFlags::NONE;
if light.shadows_enabled {
flags |= PointLightFlags::SHADOWS_ENABLED;
}
gpu_lights.point_lights[light_index] = GpuPointLight {
@ -584,6 +621,7 @@ pub fn prepare_lights(
inverse_square_range: 1.0 / (light.range * light.range),
near: 0.1,
far: light.range,
flags: flags.bits,
shadow_depth_bias: light.shadow_depth_bias,
shadow_normal_bias: light.shadow_normal_bias,
};
@ -616,6 +654,11 @@ pub fn prepare_lights(
// NOTE: This orthographic projection defines the volume within which shadows from a directional light can be cast
let projection = light.projection;
let mut flags = DirectionalLightFlags::NONE;
if light.shadows_enabled {
flags |= DirectionalLightFlags::SHADOWS_ENABLED;
}
gpu_lights.directional_lights[i] = GpuDirectionalLight {
// premultiply color by intensity
// we don't use the alpha at all, so no reason to multiply only [0..3]
@ -623,42 +666,45 @@ pub fn prepare_lights(
dir_to_light,
// NOTE: * view is correct, it should not be view.inverse() here
view_projection: projection * view,
flags: flags.bits,
shadow_depth_bias: light.shadow_depth_bias,
shadow_normal_bias: light.shadow_normal_bias,
};
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: i as u32,
array_layer_count: NonZeroU32::new(1),
});
if light.shadows_enabled {
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: i as u32,
array_layer_count: NonZeroU32::new(1),
});
let view_light_entity = commands
.spawn()
.insert_bundle((
ViewLight {
depth_texture_view,
pass_name: format!("shadow pass directional light {}", i),
},
ExtractedView {
width: directional_light_shadow_map.size as u32,
height: directional_light_shadow_map.size as u32,
transform: GlobalTransform::from_matrix(view.inverse()),
projection,
},
RenderPhase::<Shadow>::default(),
LightEntity::Directional { light_entity },
))
.id();
view_lights.push(view_light_entity);
let view_light_entity = commands
.spawn()
.insert_bundle((
ShadowView {
depth_texture_view,
pass_name: format!("shadow pass directional light {}", i),
},
ExtractedView {
width: directional_light_shadow_map.size as u32,
height: directional_light_shadow_map.size as u32,
transform: GlobalTransform::from_matrix(view.inverse()),
projection,
},
RenderPhase::<Shadow>::default(),
LightEntity::Directional { light_entity },
))
.id();
view_lights.push(view_light_entity);
}
}
let point_light_depth_texture_view =
point_light_depth_texture
@ -686,14 +732,20 @@ pub fn prepare_lights(
array_layer_count: None,
});
commands.entity(entity).insert(ViewLights {
point_light_depth_texture: point_light_depth_texture.texture,
point_light_depth_texture_view,
directional_light_depth_texture: directional_light_depth_texture.texture,
directional_light_depth_texture_view,
lights: view_lights,
gpu_light_binding_index: light_meta.view_gpu_lights.push(gpu_lights),
});
commands.entity(entity).insert_bundle((
ViewShadowBindings {
point_light_depth_texture: point_light_depth_texture.texture,
point_light_depth_texture_view,
directional_light_depth_texture: directional_light_depth_texture.texture,
directional_light_depth_texture_view,
},
ViewLightEntities {
lights: view_lights,
},
ViewLightsUniformOffset {
offset: light_meta.view_gpu_lights.push(gpu_lights),
},
));
}
light_meta
@ -728,12 +780,12 @@ pub fn queue_shadows(
render_meshes: Res<RenderAssets<Mesh>>,
mut pipelines: ResMut<SpecializedPipelines<ShadowPipeline>>,
mut pipeline_cache: ResMut<RenderPipelineCache>,
mut view_lights: Query<&ViewLights>,
view_lights: Query<&ViewLightEntities>,
mut view_light_shadow_phases: Query<(&LightEntity, &mut RenderPhase<Shadow>)>,
point_light_entities: Query<&CubemapVisibleEntities, With<ExtractedPointLight>>,
directional_light_entities: Query<&VisibleEntities, With<ExtractedDirectionalLight>>,
) {
for view_lights in view_lights.iter_mut() {
for view_lights in view_lights.iter() {
let draw_shadow_mesh = shadow_draw_functions
.read()
.get_id::<DrawShadowMesh>()
@ -753,6 +805,8 @@ pub fn queue_shadows(
.expect("Failed to get point light visible entities")
.get(*face_index),
};
// NOTE: Lights with shadow mapping disabled will have no visible entities
// so no meshes will be queued
for VisibleEntity { entity, .. } in visible_entities.iter() {
let mut key = ShadowPipelineKey::empty();
if let Ok(mesh_handle) = casting_meshes.get(*entity) {
@ -811,8 +865,8 @@ impl CachedPipelinePhaseItem for Shadow {
}
pub struct ShadowPassNode {
main_view_query: QueryState<&'static ViewLights>,
view_light_query: QueryState<(&'static ViewLight, &'static RenderPhase<Shadow>)>,
main_view_query: QueryState<&'static ViewLightEntities>,
view_light_query: QueryState<(&'static ShadowView, &'static RenderPhase<Shadow>)>,
}
impl ShadowPassNode {

View file

@ -1,4 +1,7 @@
use crate::{LightMeta, NotShadowCaster, NotShadowReceiver, ShadowPipeline, ViewLights};
use crate::{
LightMeta, NotShadowCaster, NotShadowReceiver, ShadowPipeline, ViewLightsUniformOffset,
ViewShadowBindings,
};
use bevy_app::Plugin;
use bevy_asset::{Assets, Handle, HandleUntyped};
use bevy_ecs::{
@ -523,13 +526,13 @@ pub fn queue_mesh_view_bind_groups(
shadow_pipeline: Res<ShadowPipeline>,
light_meta: Res<LightMeta>,
view_uniforms: Res<ViewUniforms>,
mut views: Query<(Entity, &ViewLights)>,
mut views: Query<(Entity, &ViewShadowBindings)>,
) {
if let (Some(view_binding), Some(light_binding)) = (
view_uniforms.uniforms.binding(),
light_meta.view_gpu_lights.binding(),
) {
for (entity, view_lights) in views.iter_mut() {
for (entity, view_shadow_bindings) in views.iter_mut() {
let view_bind_group = render_device.create_bind_group(&BindGroupDescriptor {
entries: &[
BindGroupEntry {
@ -543,7 +546,7 @@ pub fn queue_mesh_view_bind_groups(
BindGroupEntry {
binding: 2,
resource: BindingResource::TextureView(
&view_lights.point_light_depth_texture_view,
&view_shadow_bindings.point_light_depth_texture_view,
),
},
BindGroupEntry {
@ -553,7 +556,7 @@ pub fn queue_mesh_view_bind_groups(
BindGroupEntry {
binding: 4,
resource: BindingResource::TextureView(
&view_lights.directional_light_depth_texture_view,
&view_shadow_bindings.directional_light_depth_texture_view,
),
},
BindGroupEntry {
@ -578,7 +581,7 @@ pub struct SetMeshViewBindGroup<const I: usize>;
impl<const I: usize> EntityRenderCommand for SetMeshViewBindGroup<I> {
type Param = SQuery<(
Read<ViewUniformOffset>,
Read<ViewLights>,
Read<ViewLightsUniformOffset>,
Read<MeshViewBindGroup>,
)>;
#[inline]
@ -592,7 +595,7 @@ impl<const I: usize> EntityRenderCommand for SetMeshViewBindGroup<I> {
pass.set_bind_group(
I,
&mesh_view_bind_group.value,
&[view_uniform.offset, view_lights.gpu_light_binding_index],
&[view_uniform.offset, view_lights.offset],
);
RenderCommandResult::Success

View file

@ -13,18 +13,26 @@ struct PointLight {
radius: f32;
near: f32;
far: f32;
// '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;
};
let POINT_LIGHT_FLAGS_SHADOWS_ENABLED_BIT: u32 = 1u;
struct DirectionalLight {
view_projection: mat4x4<f32>;
color: vec4<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: u32;
shadow_depth_bias: f32;
shadow_normal_bias: f32;
};
let DIRECTIONAL_LIGHT_FLAGS_SHADOWS_ENABLED_BIT: u32 = 1u;
[[block]]
struct Lights {
// NOTE: this array size must be kept in sync with the constants defined bevy_pbr2/src/render/light.rs

View file

@ -500,7 +500,8 @@ fn fragment(in: FragmentInput) -> [[location(0)]] vec4<f32> {
for (var i: i32 = 0; i < n_point_lights; i = i + 1) {
let light = lights.point_lights[i];
var shadow: f32;
if ((mesh.flags & MESH_FLAGS_SHADOW_RECEIVER_BIT) != 0u) {
if ((mesh.flags & MESH_FLAGS_SHADOW_RECEIVER_BIT) != 0u
|| (light.flags & POINT_LIGHT_FLAGS_SHADOWS_ENABLED_BIT) != 0u) {
shadow = fetch_point_shadow(i, in.world_position, in.world_normal);
} else {
shadow = 1.0;
@ -511,7 +512,8 @@ fn fragment(in: FragmentInput) -> [[location(0)]] vec4<f32> {
for (var i: i32 = 0; i < n_directional_lights; i = i + 1) {
let light = lights.directional_lights[i];
var shadow: f32;
if ((mesh.flags & MESH_FLAGS_SHADOW_RECEIVER_BIT) != 0u) {
if ((mesh.flags & MESH_FLAGS_SHADOW_RECEIVER_BIT) != 0u
|| (light.flags & DIRECTIONAL_LIGHT_FLAGS_SHADOWS_ENABLED_BIT) != 0u) {
shadow = fetch_directional_shadow(i, in.world_position, in.world_normal);
} else {
shadow = 1.0;