Implement percentage-closer soft shadows (PCSS). (#13497)

[*Percentage-closer soft shadows*] are a technique from 2004 that allow
shadows to become blurrier farther from the objects that cast them. It
works by introducing a *blocker search* step that runs before the normal
shadow map sampling. The blocker search step detects the difference
between the depth of the fragment being rasterized and the depth of the
nearby samples in the depth buffer. Larger depth differences result in a
larger penumbra and therefore a blurrier shadow.

To enable PCSS, fill in the `soft_shadow_size` value in
`DirectionalLight`, `PointLight`, or `SpotLight`, as appropriate. This
shadow size value represents the size of the light and should be tuned
as appropriate for your scene. Higher values result in a wider penumbra
(i.e. blurrier shadows).

When using PCSS, temporal shadow maps
(`ShadowFilteringMethod::Temporal`) are recommended. If you don't use
`ShadowFilteringMethod::Temporal` and instead use
`ShadowFilteringMethod::Gaussian`, Bevy will use the same technique as
`Temporal`, but the result won't vary over time. This produces a rather
noisy result. Doing better would likely require downsampling the shadow
map, which would be complex and slower (and would require PR #13003 to
land first).

In addition to PCSS, this commit makes the near Z plane for the shadow
map configurable on a per-light basis. Previously, it had been hardcoded
to 0.1 meters. This change was necessary to make the point light shadow
map in the example look reasonable, as otherwise the shadows appeared
far too aliased.

A new example, `pcss`, has been added. It demonstrates the
percentage-closer soft shadow technique with directional lights, point
lights, spot lights, non-temporal operation, and temporal operation. The
assets are my original work.

Both temporal and non-temporal shadows are rather noisy in the example,
and, as mentioned before, this is unavoidable without downsampling the
depth buffer, which we can't do yet. Note also that the shadows don't
look particularly great for point lights; the example simply isn't an
ideal scene for them. Nevertheless, I felt that the benefits of the
ability to do a side-by-side comparison of directional and point lights
outweighed the unsightliness of the point light shadows in that example,
so I kept the point light feature in.

Fixes #3631.

[*Percentage-closer soft shadows*]:
https://developer.download.nvidia.com/shaderlibrary/docs/shadow_PCSS.pdf

## Changelog

### Added

* Percentage-closer soft shadows (PCSS) are now supported, allowing
shadows to become blurrier as they stretch away from objects. To use
them, set the `soft_shadow_size` field in `DirectionalLight`,
`PointLight`, or `SpotLight`, as applicable.

* The near Z value for shadow maps is now customizable via the
`shadow_map_near_z` field in `DirectionalLight`, `PointLight`, and
`SpotLight`.

## Screenshots

PCSS off:
![Screenshot 2024-05-24
120012](https://github.com/bevyengine/bevy/assets/157897/0d35fe98-245b-44fb-8a43-8d0272a73b86)

PCSS on:
![Screenshot 2024-05-24
115959](https://github.com/bevyengine/bevy/assets/157897/83397ef8-1317-49dd-bfb3-f8286d7610cd)

---------

Co-authored-by: Alice Cecile <alice.i.cecile@gmail.com>
Co-authored-by: Torstein Grindvik <52322338+torsteingrindvik@users.noreply.github.com>
This commit is contained in:
Patrick Walton 2024-09-18 11:07:17 -07:00 committed by GitHub
parent bd489068c6
commit 2ae5a21009
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
23 changed files with 2346 additions and 158 deletions

View file

@ -3452,6 +3452,17 @@ description = "Demonstrates animation masks"
category = "Animation"
wasm = true
[[example]]
name = "pcss"
path = "examples/3d/pcss.rs"
doc-scrape-examples = true
[package.metadata.example.pcss]
name = "Percentage-closer soft shadows"
description = "Demonstrates percentage-closer soft shadows (PCSS)"
category = "3D Rendering"
wasm = false
[profile.wasm-release]
inherits = "release"
opt-level = "z"

Binary file not shown.

Binary file not shown.

File diff suppressed because it is too large Load diff

Binary file not shown.

After

Width:  |  Height:  |  Size: 540 KiB

View file

@ -152,6 +152,10 @@ pub struct GpuClusterableObject {
pub(crate) shadow_depth_bias: f32,
pub(crate) shadow_normal_bias: f32,
pub(crate) spot_light_tan_angle: f32,
pub(crate) soft_shadow_size: f32,
pub(crate) shadow_map_near_z: f32,
pub(crate) pad_a: f32,
pub(crate) pad_b: f32,
}
pub enum GpuClusterableObjects {

View file

@ -259,11 +259,11 @@ impl SpecializedRenderPipeline for DeferredLightingLayout {
shader_defs.push("TONEMAP_IN_SHADER".into());
shader_defs.push(ShaderDefVal::UInt(
"TONEMAPPING_LUT_TEXTURE_BINDING_INDEX".into(),
21,
22,
));
shader_defs.push(ShaderDefVal::UInt(
"TONEMAPPING_LUT_SAMPLER_BINDING_INDEX".into(),
22,
23,
));
let method = key.intersection(MeshPipelineKey::TONEMAP_METHOD_RESERVED_BITS);

View file

@ -50,7 +50,11 @@ use super::*;
#[derive(Component, Debug, Clone, Reflect)]
#[reflect(Component, Default, Debug)]
pub struct DirectionalLight {
/// The color of the light.
///
/// By default, this is white.
pub color: Color,
/// Illuminance in lux (lumens per square meter), representing the amount of
/// light projected onto surfaces by this light source. Lux is used here
/// instead of lumens because a directional light illuminates all surfaces
@ -58,10 +62,45 @@ pub struct DirectionalLight {
/// can only be specified for light sources which emit light from a specific
/// area.
pub illuminance: f32,
/// Whether this light casts shadows.
///
/// Note that shadows are rather expensive and become more so with every
/// light that casts them. In general, it's best to aggressively limit the
/// number of lights with shadows enabled to one or two at most.
pub shadows_enabled: bool,
/// Whether soft shadows are enabled, and if so, the size of the light.
///
/// Soft shadows, also known as *percentage-closer soft shadows* or PCSS,
/// cause shadows to become blurrier (i.e. their penumbra increases in
/// radius) as they extend away from objects. The blurriness of the shadow
/// depends on the size of the light; larger lights result in larger
/// penumbras and therefore blurrier shadows.
///
/// Currently, soft shadows are rather noisy if not using the temporal mode.
/// If you enable soft shadows, consider choosing
/// [`ShadowFilteringMethod::Temporal`] and enabling temporal antialiasing
/// (TAA) to smooth the noise out over time.
///
/// Note that soft shadows are significantly more expensive to render than
/// hard shadows.
pub soft_shadow_size: Option<f32>,
/// A value that adjusts the tradeoff between self-shadowing artifacts and
/// proximity of shadows to their casters.
///
/// This value frequently must be tuned to the specific scene; this is
/// normal and a well-known part of the shadow mapping workflow. If set too
/// low, unsightly shadow patterns appear on objects not in shadow as
/// objects incorrectly cast shadows on themselves, known as *shadow acne*.
/// If set too high, shadows detach from the objects casting them and seem
/// to "fly" off the objects, known as *Peter Panning*.
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.
/// 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.
pub shadow_normal_bias: f32,
}
@ -71,6 +110,7 @@ impl Default for DirectionalLight {
color: Color::WHITE,
illuminance: light_consts::lux::AMBIENT_DAYLIGHT,
shadows_enabled: false,
soft_shadow_size: None,
shadow_depth_bias: Self::DEFAULT_SHADOW_DEPTH_BIAS,
shadow_normal_bias: Self::DEFAULT_SHADOW_NORMAL_BIAS,
}

View file

@ -580,8 +580,6 @@ pub fn update_point_light_frusta(
Or<(Changed<GlobalTransform>, Changed<PointLight>)>,
>,
) {
let clip_from_view =
Mat4::perspective_infinite_reverse_rh(std::f32::consts::FRAC_PI_2, 1.0, POINT_LIGHT_NEAR_Z);
let view_rotations = CUBE_MAP_FACES
.iter()
.map(|CubeMapFace { target, up }| Transform::IDENTITY.looking_at(*target, *up))
@ -597,6 +595,12 @@ pub fn update_point_light_frusta(
continue;
}
let clip_from_view = Mat4::perspective_infinite_reverse_rh(
std::f32::consts::FRAC_PI_2,
1.0,
point_light.shadow_map_near_z,
);
// ignore scale because we don't want to effectively scale light radius and range
// by applying those as a view transform to shadow map rendering of objects
// and ignore rotation because we want the shadow map projections to align with the axes
@ -639,7 +643,8 @@ pub fn update_spot_light_frusta(
let view_backward = transform.back();
let spot_world_from_view = spot_light_world_from_view(transform);
let spot_clip_from_view = spot_light_clip_from_view(spot_light.outer_angle);
let spot_clip_from_view =
spot_light_clip_from_view(spot_light.outer_angle, spot_light.shadow_map_near_z);
let clip_from_world = spot_clip_from_view * spot_world_from_view.inverse();
*frustum = Frustum::from_clip_from_world_custom_far(

View file

@ -22,29 +22,63 @@ use super::*;
pub struct PointLight {
/// The color of this light source.
pub color: Color,
/// Luminous power in lumens, representing the amount of light emitted by this source in all directions.
pub intensity: f32,
/// Cut-off for the light's area-of-effect. Fragments outside this range will not be affected by
/// this light at all, so it's important to tune this together with `intensity` to prevent hard
/// lighting cut-offs.
pub range: f32,
/// Simulates a light source coming from a spherical volume with the given radius. Only affects
/// the size of specular highlights created by this light. Because of this, large values may not
/// produce the intended result -- for example, light radius does not affect shadow softness or
/// diffuse lighting.
/// Simulates a light source coming from a spherical volume with the given
/// radius.
///
/// This affects the size of specular highlights created by this light, as
/// well as the soft shadow penumbra size. Because of this, large values may
/// not produce the intended result -- for example, light radius does not
/// affect shadow softness or diffuse lighting.
pub radius: f32,
/// Whether this light casts shadows.
pub shadows_enabled: bool,
/// Whether soft shadows are enabled.
///
/// Soft shadows, also known as *percentage-closer soft shadows* or PCSS,
/// cause shadows to become blurrier (i.e. their penumbra increases in
/// radius) as they extend away from objects. The blurriness of the shadow
/// depends on the [`PointLight::radius`] of the light; larger lights result
/// in larger penumbras and therefore blurrier shadows.
///
/// Currently, soft shadows are rather noisy if not using the temporal mode.
/// If you enable soft shadows, consider choosing
/// [`ShadowFilteringMethod::Temporal`] and enabling temporal antialiasing
/// (TAA) to smooth the noise out over time.
///
/// Note that soft shadows are significantly more expensive to render than
/// hard shadows.
pub soft_shadows_enabled: bool,
/// A bias used when sampling shadow maps to avoid "shadow-acne", or false shadow occlusions
/// that happen as a result of shadow-map fragments not mapping 1:1 to screen-space fragments.
/// Too high of a depth bias can lead to shadows detaching from their casters, or
/// "peter-panning". This bias can be tuned together with `shadow_normal_bias` to correct shadow
/// artifacts for a given scene.
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
/// away.
pub shadow_normal_bias: f32,
/// The distance from the light to near Z plane in the shadow map.
///
/// Objects closer than this distance to the light won't cast shadows.
/// Setting this higher increases the shadow map's precision.
///
/// This only has an effect if shadows are enabled.
pub shadow_map_near_z: f32,
}
impl Default for PointLight {
@ -58,8 +92,10 @@ impl Default for PointLight {
range: 20.0,
radius: 0.0,
shadows_enabled: false,
soft_shadows_enabled: false,
shadow_depth_bias: Self::DEFAULT_SHADOW_DEPTH_BIAS,
shadow_normal_bias: Self::DEFAULT_SHADOW_NORMAL_BIAS,
shadow_map_near_z: Self::DEFAULT_SHADOW_MAP_NEAR_Z,
}
}
}
@ -67,4 +103,5 @@ impl Default for PointLight {
impl PointLight {
pub const DEFAULT_SHADOW_DEPTH_BIAS: f32 = 0.08;
pub const DEFAULT_SHADOW_NORMAL_BIAS: f32 = 0.6;
pub const DEFAULT_SHADOW_MAP_NEAR_Z: f32 = 0.1;
}

View file

@ -7,23 +7,85 @@ use super::*;
#[derive(Component, Debug, Clone, Copy, Reflect)]
#[reflect(Component, Default, Debug)]
pub struct SpotLight {
/// The color of the light.
///
/// By default, this is white.
pub color: Color,
/// Luminous power in lumens, representing the amount of light emitted by this source in all directions.
pub intensity: f32,
/// Range in meters that this light illuminates.
///
/// Note that this value affects resolution of the shadow maps; generally, the
/// higher you set it, the lower-resolution your shadow maps will be.
/// Consequently, you should set this value to be only the size that you need.
pub range: f32,
/// Simulates a light source coming from a spherical volume with the given
/// radius.
///
/// This affects the size of specular highlights created by this light, as
/// well as the soft shadow penumbra size. Because of this, large values may
/// not produce the intended result -- for example, light radius does not
/// affect shadow softness or diffuse lighting.
pub radius: f32,
/// Whether this light casts shadows.
///
/// Note that shadows are rather expensive and become more so with every
/// light that casts them. In general, it's best to aggressively limit the
/// number of lights with shadows enabled to one or two at most.
pub shadows_enabled: bool,
/// Whether soft shadows are enabled.
///
/// Soft shadows, also known as *percentage-closer soft shadows* or PCSS,
/// cause shadows to become blurrier (i.e. their penumbra increases in
/// radius) as they extend away from objects. The blurriness of the shadow
/// depends on the [`SpotLight::radius`] of the light; larger lights result in larger
/// penumbras and therefore blurrier shadows.
///
/// Currently, soft shadows are rather noisy if not using the temporal mode.
/// If you enable soft shadows, consider choosing
/// [`ShadowFilteringMethod::Temporal`] and enabling temporal antialiasing
/// (TAA) to smooth the noise out over time.
///
/// Note that soft shadows are significantly more expensive to render than
/// hard shadows.
pub soft_shadows_enabled: bool,
/// A value that adjusts the tradeoff between self-shadowing artifacts and
/// proximity of shadows to their casters.
///
/// This value frequently must be tuned to the specific scene; this is
/// normal and a well-known part of the shadow mapping workflow. If set too
/// low, unsightly shadow patterns appear on objects not in shadow as
/// objects incorrectly cast shadows on themselves, known as *shadow acne*.
/// If set too high, shadows detach from the objects casting them and seem
/// to "fly" off the objects, known as *Peter Panning*.
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
/// away.
pub shadow_normal_bias: f32,
/// The distance from the light to the near Z plane in the shadow map.
///
/// Objects closer than this distance to the light won't cast shadows.
/// Setting this higher increases the shadow map's precision.
///
/// This only has an effect if shadows are enabled.
pub shadow_map_near_z: f32,
/// Angle defining the distance from the spot light direction to the outer limit
/// of the light's cone of effect.
/// `outer_angle` should be < `PI / 2.0`.
/// `PI / 2.0` defines a hemispherical spot light, but shadows become very blocky as the angle
/// approaches this limit.
pub outer_angle: f32,
/// Angle defining the distance from the spot light direction to the inner limit
/// of the light's cone of effect.
/// Light is attenuated from `inner_angle` to `outer_angle` to give a smooth falloff.
@ -34,6 +96,7 @@ pub struct SpotLight {
impl SpotLight {
pub const DEFAULT_SHADOW_DEPTH_BIAS: f32 = 0.02;
pub const DEFAULT_SHADOW_NORMAL_BIAS: f32 = 1.8;
pub const DEFAULT_SHADOW_MAP_NEAR_Z: f32 = 0.1;
}
impl Default for SpotLight {
@ -48,8 +111,10 @@ impl Default for SpotLight {
range: 20.0,
radius: 0.0,
shadows_enabled: false,
soft_shadows_enabled: false,
shadow_depth_bias: Self::DEFAULT_SHADOW_DEPTH_BIAS,
shadow_normal_bias: Self::DEFAULT_SHADOW_NORMAL_BIAS,
shadow_map_near_z: Self::DEFAULT_SHADOW_MAP_NEAR_Z,
inner_angle: 0.0,
outer_angle: std::f32::consts::FRAC_PI_4,
}

View file

@ -19,6 +19,7 @@ use bevy_render::{
Extract,
};
use bevy_transform::{components::GlobalTransform, prelude::Transform};
use bevy_utils::prelude::default;
#[cfg(feature = "trace")]
use bevy_utils::tracing::info_span;
use bevy_utils::tracing::{error, warn};
@ -35,8 +36,10 @@ pub struct ExtractedPointLight {
pub radius: f32,
pub transform: GlobalTransform,
pub shadows_enabled: bool,
pub soft_shadows_enabled: bool,
pub shadow_depth_bias: f32,
pub shadow_normal_bias: f32,
pub shadow_map_near_z: f32,
pub spot_light_angles: Option<(f32, f32)>,
}
@ -47,6 +50,7 @@ pub struct ExtractedDirectionalLight {
pub transform: GlobalTransform,
pub shadows_enabled: bool,
pub volumetric: bool,
pub soft_shadow_size: Option<f32>,
pub shadow_depth_bias: f32,
pub shadow_normal_bias: f32,
pub cascade_shadow_config: CascadeShadowConfig,
@ -79,6 +83,7 @@ pub struct GpuDirectionalLight {
color: Vec4,
dir_to_light: Vec3,
flags: u32,
soft_shadow_size: f32,
shadow_depth_bias: f32,
shadow_normal_bias: f32,
num_cascades: u32,
@ -134,8 +139,10 @@ pub const MAX_CASCADES_PER_LIGHT: usize = 1;
#[derive(Resource, Clone)]
pub struct ShadowSamplers {
pub point_light_sampler: Sampler,
pub directional_light_sampler: Sampler,
pub point_light_comparison_sampler: Sampler,
pub point_light_linear_sampler: Sampler,
pub directional_light_comparison_sampler: Sampler,
pub directional_light_linear_sampler: Sampler,
}
// TODO: this pattern for initializing the shaders / pipeline isn't ideal. this should be handled by the asset system
@ -143,27 +150,30 @@ impl FromWorld for ShadowSamplers {
fn from_world(world: &mut World) -> Self {
let render_device = world.resource::<RenderDevice>();
let base_sampler_descriptor = SamplerDescriptor {
address_mode_u: AddressMode::ClampToEdge,
address_mode_v: AddressMode::ClampToEdge,
address_mode_w: AddressMode::ClampToEdge,
mag_filter: FilterMode::Linear,
min_filter: FilterMode::Linear,
mipmap_filter: FilterMode::Nearest,
..default()
};
ShadowSamplers {
point_light_sampler: render_device.create_sampler(&SamplerDescriptor {
address_mode_u: AddressMode::ClampToEdge,
address_mode_v: AddressMode::ClampToEdge,
address_mode_w: AddressMode::ClampToEdge,
mag_filter: FilterMode::Linear,
min_filter: FilterMode::Linear,
mipmap_filter: FilterMode::Nearest,
point_light_comparison_sampler: render_device.create_sampler(&SamplerDescriptor {
compare: Some(CompareFunction::GreaterEqual),
..Default::default()
..base_sampler_descriptor
}),
directional_light_sampler: render_device.create_sampler(&SamplerDescriptor {
address_mode_u: AddressMode::ClampToEdge,
address_mode_v: AddressMode::ClampToEdge,
address_mode_w: AddressMode::ClampToEdge,
mag_filter: FilterMode::Linear,
min_filter: FilterMode::Linear,
mipmap_filter: FilterMode::Nearest,
point_light_linear_sampler: render_device.create_sampler(&base_sampler_descriptor),
directional_light_comparison_sampler: render_device.create_sampler(
&SamplerDescriptor {
compare: Some(CompareFunction::GreaterEqual),
..Default::default()
}),
..base_sampler_descriptor
},
),
directional_light_linear_sampler: render_device
.create_sampler(&base_sampler_descriptor),
}
}
}
@ -252,11 +262,13 @@ pub fn extract_lights(
radius: point_light.radius,
transform: *transform,
shadows_enabled: point_light.shadows_enabled,
soft_shadows_enabled: point_light.soft_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
* point_light_texel_size
* std::f32::consts::SQRT_2,
shadow_map_near_z: point_light.shadow_map_near_z,
spot_light_angles: None,
};
point_lights_values.push((
@ -301,11 +313,13 @@ pub fn extract_lights(
radius: spot_light.radius,
transform: *transform,
shadows_enabled: spot_light.shadows_enabled,
soft_shadows_enabled: spot_light.soft_shadows_enabled,
shadow_depth_bias: spot_light.shadow_depth_bias,
// The factor of SQRT_2 is for the worst-case diagonal offset
shadow_normal_bias: spot_light.shadow_normal_bias
* texel_size
* std::f32::consts::SQRT_2,
shadow_map_near_z: spot_light.shadow_map_near_z,
spot_light_angles: Some((spot_light.inner_angle, spot_light.outer_angle)),
},
render_visible_entities,
@ -342,6 +356,7 @@ pub fn extract_lights(
illuminance: directional_light.illuminance,
transform: *transform,
volumetric: volumetric_light.is_some(),
soft_shadow_size: directional_light.soft_shadow_size,
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
@ -356,8 +371,6 @@ pub fn extract_lights(
}
}
pub(crate) const POINT_LIGHT_NEAR_Z: f32 = 0.1f32;
pub(crate) struct CubeMapFace {
pub(crate) target: Vec3,
pub(crate) up: Vec3,
@ -502,9 +515,9 @@ pub(crate) fn spot_light_world_from_view(transform: &GlobalTransform) -> Mat4 {
)
}
pub(crate) fn spot_light_clip_from_view(angle: f32) -> Mat4 {
pub(crate) fn spot_light_clip_from_view(angle: f32, near_z: f32) -> Mat4 {
// spot light projection FOV is 2x the angle from spot light center to outer edge
Mat4::perspective_infinite_reverse_rh(angle * 2.0, 1.0, POINT_LIGHT_NEAR_Z)
Mat4::perspective_infinite_reverse_rh(angle * 2.0, 1.0, near_z)
}
#[allow(clippy::too_many_arguments)]
@ -549,8 +562,6 @@ pub fn prepare_lights(
};
// Pre-calculate for PointLights
let cube_face_projection =
Mat4::perspective_infinite_reverse_rh(std::f32::consts::FRAC_PI_2, 1.0, POINT_LIGHT_NEAR_Z);
let cube_face_rotations = CUBE_MAP_FACES
.iter()
.map(|CubeMapFace { target, up }| Transform::IDENTITY.looking_at(*target, *up))
@ -685,6 +696,12 @@ pub fn prepare_lights(
flags |= PointLightFlags::SHADOWS_ENABLED;
}
let cube_face_projection = Mat4::perspective_infinite_reverse_rh(
std::f32::consts::FRAC_PI_2,
1.0,
light.shadow_map_near_z,
);
let (light_custom_data, spot_light_tan_angle) = match light.spot_light_angles {
Some((inner, outer)) => {
let light_direction = light.transform.forward();
@ -727,9 +744,17 @@ pub fn prepare_lights(
.extend(1.0 / (light.range * light.range)),
position_radius: light.transform.translation().extend(light.radius),
flags: flags.bits(),
soft_shadow_size: if light.soft_shadows_enabled {
light.radius
} else {
0.0
},
shadow_depth_bias: light.shadow_depth_bias,
shadow_normal_bias: light.shadow_normal_bias,
shadow_map_near_z: light.shadow_map_near_z,
spot_light_tan_angle,
pad_a: 0.0,
pad_b: 0.0,
});
global_light_meta.entity_to_index.insert(entity, index);
}
@ -771,6 +796,7 @@ pub fn prepare_lights(
// direction is negated to be ready for N.L
dir_to_light: light.transform.back().into(),
flags: flags.bits(),
soft_shadow_size: light.soft_shadow_size.unwrap_or_default(),
shadow_depth_bias: light.shadow_depth_bias,
shadow_normal_bias: light.shadow_normal_bias,
num_cascades: num_cascades as u32,
@ -878,6 +904,12 @@ pub fn prepare_lights(
// and ignore rotation because we want the shadow map projections to align with the axes
let view_translation = GlobalTransform::from_translation(light.transform.translation());
let cube_face_projection = Mat4::perspective_infinite_reverse_rh(
std::f32::consts::FRAC_PI_2,
1.0,
light.shadow_map_near_z,
);
for (face_index, (view_rotation, frustum)) in cube_face_rotations
.iter()
.zip(&point_light_frusta.unwrap().frusta)
@ -946,7 +978,7 @@ pub fn prepare_lights(
let angle = light.spot_light_angles.expect("lights should be sorted so that \
[point_light_count..point_light_count + spot_light_shadow_maps_count] are spot lights").1;
let spot_projection = spot_light_clip_from_view(angle);
let spot_projection = spot_light_clip_from_view(angle, light.shadow_map_near_z);
let depth_texture_view =
directional_light_depth_texture

View file

@ -1837,11 +1837,11 @@ impl SpecializedMeshPipeline for MeshPipeline {
shader_defs.push("TONEMAP_IN_SHADER".into());
shader_defs.push(ShaderDefVal::UInt(
"TONEMAPPING_LUT_TEXTURE_BINDING_INDEX".into(),
21,
23,
));
shader_defs.push(ShaderDefVal::UInt(
"TONEMAPPING_LUT_SAMPLER_BINDING_INDEX".into(),
22,
24,
));
let method = key.intersection(MeshPipelineKey::TONEMAP_METHOD_RESERVED_BITS);

View file

@ -213,11 +213,13 @@ fn layout_entries(
))]
texture_cube(TextureSampleType::Depth),
),
// Point Shadow Texture Array Sampler
// Point Shadow Texture Array Comparison Sampler
(3, sampler(SamplerBindingType::Comparison)),
// Point Shadow Texture Array Linear Sampler
(4, sampler(SamplerBindingType::Filtering)),
// Directional Shadow Texture Array
(
4,
5,
#[cfg(any(
not(feature = "webgl"),
not(target_arch = "wasm32"),
@ -227,11 +229,13 @@ fn layout_entries(
#[cfg(all(feature = "webgl", target_arch = "wasm32", not(feature = "webgpu")))]
texture_2d(TextureSampleType::Depth),
),
// Directional Shadow Texture Array Sampler
(5, sampler(SamplerBindingType::Comparison)),
// ClusterableObjects
// Directional Shadow Texture Array Comparison Sampler
(6, sampler(SamplerBindingType::Comparison)),
// Directional Shadow Texture Array Linear Sampler
(7, sampler(SamplerBindingType::Filtering)),
// PointLights
(
6,
8,
buffer_layout(
clustered_forward_buffer_binding_type,
false,
@ -242,7 +246,7 @@ fn layout_entries(
),
// ClusteredLightIndexLists
(
7,
9,
buffer_layout(
clustered_forward_buffer_binding_type,
false,
@ -255,7 +259,7 @@ fn layout_entries(
),
// ClusterOffsetsAndCounts
(
8,
10,
buffer_layout(
clustered_forward_buffer_binding_type,
false,
@ -266,16 +270,16 @@ fn layout_entries(
),
// Globals
(
9,
11,
uniform_buffer::<GlobalsUniform>(false).visibility(ShaderStages::VERTEX_FRAGMENT),
),
// Fog
(10, uniform_buffer::<GpuFog>(true)),
(12, uniform_buffer::<GpuFog>(true)),
// Light probes
(11, uniform_buffer::<LightProbesUniform>(true)),
(13, uniform_buffer::<LightProbesUniform>(true)),
// Visibility ranges
(
12,
14,
buffer_layout(
visibility_ranges_buffer_binding_type,
false,
@ -284,10 +288,10 @@ fn layout_entries(
.visibility(ShaderStages::VERTEX),
),
// Screen space reflection settings
(13, uniform_buffer::<ScreenSpaceReflectionsUniform>(true)),
(15, uniform_buffer::<ScreenSpaceReflectionsUniform>(true)),
// Screen space ambient occlusion texture
(
14,
16,
texture_2d(TextureSampleType::Float { filterable: false }),
),
),
@ -296,10 +300,10 @@ fn layout_entries(
// EnvironmentMapLight
let environment_map_entries = environment_map::get_bind_group_layout_entries(render_device);
entries = entries.extend_with_indices((
(15, environment_map_entries[0]),
(16, environment_map_entries[1]),
(17, environment_map_entries[2]),
(18, environment_map_entries[3]),
(17, environment_map_entries[0]),
(18, environment_map_entries[1]),
(19, environment_map_entries[2]),
(20, environment_map_entries[3]),
));
// Irradiance volumes
@ -307,16 +311,16 @@ fn layout_entries(
let irradiance_volume_entries =
irradiance_volume::get_bind_group_layout_entries(render_device);
entries = entries.extend_with_indices((
(19, irradiance_volume_entries[0]),
(20, irradiance_volume_entries[1]),
(21, irradiance_volume_entries[0]),
(22, irradiance_volume_entries[1]),
));
}
// Tonemapping
let tonemapping_lut_entries = get_lut_bind_group_layout_entries();
entries = entries.extend_with_indices((
(21, tonemapping_lut_entries[0]),
(22, tonemapping_lut_entries[1]),
(23, tonemapping_lut_entries[0]),
(24, tonemapping_lut_entries[1]),
));
// Prepass
@ -326,7 +330,7 @@ fn layout_entries(
{
for (entry, binding) in prepass::get_bind_group_layout_entries(layout_key)
.iter()
.zip([23, 24, 25, 26])
.zip([25, 26, 27, 28])
{
if let Some(entry) = entry {
entries = entries.extend_with_indices(((binding as u32, *entry),));
@ -337,10 +341,10 @@ fn layout_entries(
// View Transmission Texture
entries = entries.extend_with_indices((
(
27,
29,
texture_2d(TextureSampleType::Float { filterable: true }),
),
(28, sampler(SamplerBindingType::Filtering)),
(30, sampler(SamplerBindingType::Filtering)),
));
entries.to_vec()
@ -527,23 +531,25 @@ pub fn prepare_mesh_view_bind_groups(
(0, view_binding.clone()),
(1, light_binding.clone()),
(2, &shadow_bindings.point_light_depth_texture_view),
(3, &shadow_samplers.point_light_sampler),
(4, &shadow_bindings.directional_light_depth_texture_view),
(5, &shadow_samplers.directional_light_sampler),
(6, clusterable_objects_binding.clone()),
(3, &shadow_samplers.point_light_comparison_sampler),
(4, &shadow_samplers.point_light_linear_sampler),
(5, &shadow_bindings.directional_light_depth_texture_view),
(6, &shadow_samplers.directional_light_comparison_sampler),
(7, &shadow_samplers.directional_light_linear_sampler),
(8, clusterable_objects_binding.clone()),
(
7,
9,
cluster_bindings
.clusterable_object_index_lists_binding()
.unwrap(),
),
(8, cluster_bindings.offsets_and_counts_binding().unwrap()),
(9, globals.clone()),
(10, fog_binding.clone()),
(11, light_probes_binding.clone()),
(12, visibility_ranges_buffer.as_entire_binding()),
(13, ssr_binding.clone()),
(14, ssao_view),
(10, cluster_bindings.offsets_and_counts_binding().unwrap()),
(11, globals.clone()),
(12, fog_binding.clone()),
(13, light_probes_binding.clone()),
(14, visibility_ranges_buffer.as_entire_binding()),
(15, ssr_binding.clone()),
(16, ssao_view),
));
let environment_map_bind_group_entries = RenderViewEnvironmentMapBindGroupEntries::get(
@ -560,10 +566,10 @@ pub fn prepare_mesh_view_bind_groups(
sampler,
} => {
entries = entries.extend_with_indices((
(15, diffuse_texture_view),
(16, specular_texture_view),
(17, sampler),
(18, environment_map_binding.clone()),
(17, diffuse_texture_view),
(18, specular_texture_view),
(19, sampler),
(20, environment_map_binding.clone()),
));
}
RenderViewEnvironmentMapBindGroupEntries::Multiple {
@ -572,10 +578,10 @@ pub fn prepare_mesh_view_bind_groups(
sampler,
} => {
entries = entries.extend_with_indices((
(15, diffuse_texture_views.as_slice()),
(16, specular_texture_views.as_slice()),
(17, sampler),
(18, environment_map_binding.clone()),
(17, diffuse_texture_views.as_slice()),
(18, specular_texture_views.as_slice()),
(19, sampler),
(20, environment_map_binding.clone()),
));
}
}
@ -596,21 +602,21 @@ pub fn prepare_mesh_view_bind_groups(
texture_view,
sampler,
}) => {
entries = entries.extend_with_indices(((19, texture_view), (20, sampler)));
entries = entries.extend_with_indices(((21, texture_view), (22, sampler)));
}
Some(RenderViewIrradianceVolumeBindGroupEntries::Multiple {
ref texture_views,
sampler,
}) => {
entries = entries
.extend_with_indices(((19, texture_views.as_slice()), (20, sampler)));
.extend_with_indices(((21, texture_views.as_slice()), (22, sampler)));
}
None => {}
}
let lut_bindings =
get_lut_bindings(&images, &tonemapping_luts, tonemapping, &fallback_image);
entries = entries.extend_with_indices(((21, lut_bindings.0), (22, lut_bindings.1)));
entries = entries.extend_with_indices(((23, lut_bindings.0), (24, lut_bindings.1)));
// When using WebGL, we can't have a depth texture with multisampling
let prepass_bindings;
@ -620,7 +626,7 @@ pub fn prepare_mesh_view_bind_groups(
for (binding, index) in prepass_bindings
.iter()
.map(Option::as_ref)
.zip([23, 24, 25, 26])
.zip([25, 26, 27, 28])
.flat_map(|(b, i)| b.map(|b| (b, i)))
{
entries = entries.extend_with_indices(((index, binding),));
@ -636,7 +642,7 @@ pub fn prepare_mesh_view_bind_groups(
.unwrap_or(&fallback_image_zero.sampler);
entries =
entries.extend_with_indices(((27, transmission_view), (28, transmission_sampler)));
entries.extend_with_indices(((29, transmission_view), (30, transmission_sampler)));
commands.entity(entity).insert(MeshViewBindGroup {
value: render_device.create_bind_group("mesh_view_bind_group", layout, &entries),

View file

@ -13,88 +13,91 @@
#else
@group(0) @binding(2) var point_shadow_textures: texture_depth_cube_array;
#endif
@group(0) @binding(3) var point_shadow_textures_sampler: sampler_comparison;
@group(0) @binding(3) var point_shadow_textures_comparison_sampler: sampler_comparison;
@group(0) @binding(4) var point_shadow_textures_linear_sampler: sampler;
#ifdef NO_ARRAY_TEXTURES_SUPPORT
@group(0) @binding(4) var directional_shadow_textures: texture_depth_2d;
@group(0) @binding(5) var directional_shadow_textures: texture_depth_2d;
#else
@group(0) @binding(4) var directional_shadow_textures: texture_depth_2d_array;
@group(0) @binding(5) var directional_shadow_textures: texture_depth_2d_array;
#endif
@group(0) @binding(5) var directional_shadow_textures_sampler: sampler_comparison;
@group(0) @binding(6) var directional_shadow_textures_comparison_sampler: sampler_comparison;
@group(0) @binding(7) var directional_shadow_textures_linear_sampler: sampler;
#if AVAILABLE_STORAGE_BUFFER_BINDINGS >= 3
@group(0) @binding(6) var<storage> clusterable_objects: types::ClusterableObjects;
@group(0) @binding(7) var<storage> clusterable_object_index_lists: types::ClusterLightIndexLists;
@group(0) @binding(8) var<storage> cluster_offsets_and_counts: types::ClusterOffsetsAndCounts;
@group(0) @binding(8) var<storage> clusterable_objects: types::ClusterableObjects;
@group(0) @binding(9) var<storage> clusterable_object_index_lists: types::ClusterLightIndexLists;
@group(0) @binding(10) var<storage> cluster_offsets_and_counts: types::ClusterOffsetsAndCounts;
#else
@group(0) @binding(6) var<uniform> clusterable_objects: types::ClusterableObjects;
@group(0) @binding(7) var<uniform> clusterable_object_index_lists: types::ClusterLightIndexLists;
@group(0) @binding(8) var<uniform> cluster_offsets_and_counts: types::ClusterOffsetsAndCounts;
@group(0) @binding(8) var<uniform> clusterable_objects: types::ClusterableObjects;
@group(0) @binding(9) var<uniform> clusterable_object_index_lists: types::ClusterLightIndexLists;
@group(0) @binding(10) var<uniform> cluster_offsets_and_counts: types::ClusterOffsetsAndCounts;
#endif
@group(0) @binding(9) var<uniform> globals: Globals;
@group(0) @binding(10) var<uniform> fog: types::Fog;
@group(0) @binding(11) var<uniform> light_probes: types::LightProbes;
@group(0) @binding(11) var<uniform> globals: Globals;
@group(0) @binding(12) var<uniform> fog: types::Fog;
@group(0) @binding(13) var<uniform> light_probes: types::LightProbes;
const VISIBILITY_RANGE_UNIFORM_BUFFER_SIZE: u32 = 64u;
#if AVAILABLE_STORAGE_BUFFER_BINDINGS >= 6
@group(0) @binding(12) var<storage> visibility_ranges: array<vec4<f32>>;
@group(0) @binding(14) var<storage> visibility_ranges: array<vec4<f32>>;
#else
@group(0) @binding(12) var<uniform> visibility_ranges: array<vec4<f32>, VISIBILITY_RANGE_UNIFORM_BUFFER_SIZE>;
@group(0) @binding(14) var<uniform> visibility_ranges: array<vec4<f32>, VISIBILITY_RANGE_UNIFORM_BUFFER_SIZE>;
#endif
@group(0) @binding(13) var<uniform> ssr_settings: types::ScreenSpaceReflectionsSettings;
@group(0) @binding(14) var screen_space_ambient_occlusion_texture: texture_2d<f32>;
@group(0) @binding(15) var<uniform> ssr_settings: types::ScreenSpaceReflectionsSettings;
@group(0) @binding(16) var screen_space_ambient_occlusion_texture: texture_2d<f32>;
#ifdef MULTIPLE_LIGHT_PROBES_IN_ARRAY
@group(0) @binding(15) var diffuse_environment_maps: binding_array<texture_cube<f32>, 8u>;
@group(0) @binding(16) var specular_environment_maps: binding_array<texture_cube<f32>, 8u>;
@group(0) @binding(17) var diffuse_environment_maps: binding_array<texture_cube<f32>, 8u>;
@group(0) @binding(18) var specular_environment_maps: binding_array<texture_cube<f32>, 8u>;
#else
@group(0) @binding(15) var diffuse_environment_map: texture_cube<f32>;
@group(0) @binding(16) var specular_environment_map: texture_cube<f32>;
@group(0) @binding(17) var diffuse_environment_map: texture_cube<f32>;
@group(0) @binding(18) var specular_environment_map: texture_cube<f32>;
#endif
@group(0) @binding(17) var environment_map_sampler: sampler;
@group(0) @binding(18) var<uniform> environment_map_uniform: types::EnvironmentMapUniform;
@group(0) @binding(19) var environment_map_sampler: sampler;
@group(0) @binding(20) var<uniform> environment_map_uniform: types::EnvironmentMapUniform;
#ifdef IRRADIANCE_VOLUMES_ARE_USABLE
#ifdef MULTIPLE_LIGHT_PROBES_IN_ARRAY
@group(0) @binding(19) var irradiance_volumes: binding_array<texture_3d<f32>, 8u>;
@group(0) @binding(21) var irradiance_volumes: binding_array<texture_3d<f32>, 8u>;
#else
@group(0) @binding(19) var irradiance_volume: texture_3d<f32>;
@group(0) @binding(21) var irradiance_volume: texture_3d<f32>;
#endif
@group(0) @binding(20) var irradiance_volume_sampler: sampler;
@group(0) @binding(22) var irradiance_volume_sampler: sampler;
#endif
@group(0) @binding(21) var dt_lut_texture: texture_3d<f32>;
@group(0) @binding(22) var dt_lut_sampler: sampler;
// NB: If you change these, make sure to update `tonemapping_shared.wgsl` too.
@group(0) @binding(23) var dt_lut_texture: texture_3d<f32>;
@group(0) @binding(24) var dt_lut_sampler: sampler;
#ifdef MULTISAMPLED
#ifdef DEPTH_PREPASS
@group(0) @binding(23) var depth_prepass_texture: texture_depth_multisampled_2d;
@group(0) @binding(25) var depth_prepass_texture: texture_depth_multisampled_2d;
#endif // DEPTH_PREPASS
#ifdef NORMAL_PREPASS
@group(0) @binding(24) var normal_prepass_texture: texture_multisampled_2d<f32>;
@group(0) @binding(26) var normal_prepass_texture: texture_multisampled_2d<f32>;
#endif // NORMAL_PREPASS
#ifdef MOTION_VECTOR_PREPASS
@group(0) @binding(25) var motion_vector_prepass_texture: texture_multisampled_2d<f32>;
@group(0) @binding(27) var motion_vector_prepass_texture: texture_multisampled_2d<f32>;
#endif // MOTION_VECTOR_PREPASS
#else // MULTISAMPLED
#ifdef DEPTH_PREPASS
@group(0) @binding(23) var depth_prepass_texture: texture_depth_2d;
@group(0) @binding(25) var depth_prepass_texture: texture_depth_2d;
#endif // DEPTH_PREPASS
#ifdef NORMAL_PREPASS
@group(0) @binding(24) var normal_prepass_texture: texture_2d<f32>;
@group(0) @binding(26) var normal_prepass_texture: texture_2d<f32>;
#endif // NORMAL_PREPASS
#ifdef MOTION_VECTOR_PREPASS
@group(0) @binding(25) var motion_vector_prepass_texture: texture_2d<f32>;
@group(0) @binding(27) var motion_vector_prepass_texture: texture_2d<f32>;
#endif // MOTION_VECTOR_PREPASS
#endif // MULTISAMPLED
#ifdef DEFERRED_PREPASS
@group(0) @binding(26) var deferred_prepass_texture: texture_2d<u32>;
@group(0) @binding(28) var deferred_prepass_texture: texture_2d<u32>;
#endif // DEFERRED_PREPASS
@group(0) @binding(27) var view_transmission_texture: texture_2d<f32>;
@group(0) @binding(28) var view_transmission_sampler: sampler;
@group(0) @binding(29) var view_transmission_texture: texture_2d<f32>;
@group(0) @binding(30) var view_transmission_sampler: sampler;

View file

@ -11,6 +11,10 @@ struct ClusterableObject {
shadow_depth_bias: f32,
shadow_normal_bias: f32,
spot_light_tan_angle: f32,
soft_shadow_size: f32,
shadow_map_near_z: f32,
pad_a: f32,
pad_b: f32,
};
const POINT_LIGHT_FLAGS_SHADOWS_ENABLED_BIT: u32 = 1u;
@ -28,6 +32,7 @@ struct DirectionalLight {
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,
soft_shadow_size: f32,
shadow_depth_bias: f32,
shadow_normal_bias: f32,
num_cascades: u32,

View file

@ -447,8 +447,14 @@ fn apply_pbr_lighting(
var shadow: f32 = 1.0;
if ((in.flags & MESH_FLAGS_SHADOW_RECEIVER_BIT) != 0u
&& (view_bindings::clusterable_objects.data[light_id].flags & mesh_view_types::POINT_LIGHT_FLAGS_SHADOWS_ENABLED_BIT) != 0u) {
shadow = shadows::fetch_spot_shadow(light_id, in.world_position, in.world_normal);
&& (view_bindings::clusterable_objects.data[light_id].flags &
mesh_view_types::POINT_LIGHT_FLAGS_SHADOWS_ENABLED_BIT) != 0u) {
shadow = shadows::fetch_spot_shadow(
light_id,
in.world_position,
in.world_normal,
view_bindings::clusterable_objects.data[light_id].shadow_map_near_z,
);
}
let light_contrib = lighting::spot_light(light_id, &lighting_input);
@ -467,7 +473,12 @@ fn apply_pbr_lighting(
var transmitted_shadow: f32 = 1.0;
if ((in.flags & (MESH_FLAGS_SHADOW_RECEIVER_BIT | MESH_FLAGS_TRANSMITTED_SHADOW_RECEIVER_BIT)) == (MESH_FLAGS_SHADOW_RECEIVER_BIT | MESH_FLAGS_TRANSMITTED_SHADOW_RECEIVER_BIT)
&& (view_bindings::clusterable_objects.data[light_id].flags & mesh_view_types::POINT_LIGHT_FLAGS_SHADOWS_ENABLED_BIT) != 0u) {
transmitted_shadow = shadows::fetch_spot_shadow(light_id, diffuse_transmissive_lobe_world_position, -in.world_normal);
transmitted_shadow = shadows::fetch_spot_shadow(
light_id,
diffuse_transmissive_lobe_world_position,
-in.world_normal,
view_bindings::clusterable_objects.data[light_id].shadow_map_near_z,
);
}
let transmitted_light_contrib =

View file

@ -12,14 +12,14 @@ fn sample_shadow_map_hardware(light_local: vec2<f32>, depth: f32, array_index: i
#ifdef NO_ARRAY_TEXTURES_SUPPORT
return textureSampleCompare(
view_bindings::directional_shadow_textures,
view_bindings::directional_shadow_textures_sampler,
view_bindings::directional_shadow_textures_comparison_sampler,
light_local,
depth,
);
#else
return textureSampleCompareLevel(
view_bindings::directional_shadow_textures,
view_bindings::directional_shadow_textures_sampler,
view_bindings::directional_shadow_textures_comparison_sampler,
light_local,
array_index,
depth,
@ -27,6 +27,40 @@ fn sample_shadow_map_hardware(light_local: vec2<f32>, depth: f32, array_index: i
#endif
}
// Does a single sample of the blocker search, a part of the PCSS algorithm.
// This is the variant used for directional lights.
fn search_for_blockers_in_shadow_map_hardware(
light_local: vec2<f32>,
depth: f32,
array_index: i32,
) -> vec2<f32> {
#ifdef WEBGL2
// Make sure that the WebGL 2 compiler doesn't see `sampled_depth` sampled
// with different samplers, or it'll blow up.
return vec2(0.0);
#else // WEBGL2
#ifdef NO_ARRAY_TEXTURES_SUPPORT
let sampled_depth = textureSampleLevel(
view_bindings::directional_shadow_textures,
view_bindings::directional_shadow_textures_linear_sampler,
light_local,
0.0,
);
#else // NO_ARRAY_TEXTURES_SUPPORT
let sampled_depth = textureSampleLevel(
view_bindings::directional_shadow_textures,
view_bindings::directional_shadow_textures_linear_sampler,
light_local,
array_index,
0.0,
);
#endif // NO_ARRAY_TEXTURES_SUPPORT
return select(vec2(0.0), vec2(sampled_depth, 1.0), sampled_depth >= depth);
#endif // WEBGL2
}
// Numbers determined by trial and error that gave nice results.
const SPOT_SHADOW_TEXEL_SIZE: f32 = 0.0134277345;
const POINT_SHADOW_SCALE: f32 = 0.003;
@ -113,9 +147,9 @@ fn map(min1: f32, max1: f32, min2: f32, max2: f32, value: f32) -> f32 {
// Creates a random rotation matrix using interleaved gradient noise.
//
// See: https://www.iryoku.com/next-generation-post-processing-in-call-of-duty-advanced-warfare/
fn random_rotation_matrix(scale: vec2<f32>) -> mat2x2<f32> {
fn random_rotation_matrix(scale: vec2<f32>, temporal: bool) -> mat2x2<f32> {
let random_angle = 2.0 * PI * interleaved_gradient_noise(
scale, view_bindings::globals.frame_count);
scale, select(1u, view_bindings::globals.frame_count, temporal));
let m = vec2(sin(random_angle), cos(random_angle));
return mat2x2(
m.y, -m.x,
@ -123,13 +157,28 @@ fn random_rotation_matrix(scale: vec2<f32>) -> mat2x2<f32> {
);
}
fn sample_shadow_map_jimenez_fourteen(light_local: vec2<f32>, depth: f32, array_index: i32, texel_size: f32) -> f32 {
// Calculates the distance between spiral samples for the given texel size and
// penumbra size. This is used for the Jimenez '14 (i.e. temporal) variant of
// shadow sampling.
fn calculate_uv_offset_scale_jimenez_fourteen(texel_size: f32, blur_size: f32) -> vec2<f32> {
let shadow_map_size = vec2<f32>(textureDimensions(view_bindings::directional_shadow_textures));
let rotation_matrix = random_rotation_matrix(light_local * shadow_map_size);
// Empirically chosen fudge factor to make PCF look better across different CSM cascades
let f = map(0.00390625, 0.022949219, 0.015, 0.035, texel_size);
let uv_offset_scale = f / (texel_size * shadow_map_size);
return f * blur_size / (texel_size * shadow_map_size);
}
fn sample_shadow_map_jimenez_fourteen(
light_local: vec2<f32>,
depth: f32,
array_index: i32,
texel_size: f32,
blur_size: f32,
temporal: bool,
) -> f32 {
let shadow_map_size = vec2<f32>(textureDimensions(view_bindings::directional_shadow_textures));
let rotation_matrix = random_rotation_matrix(light_local * shadow_map_size, temporal);
let uv_offset_scale = calculate_uv_offset_scale_jimenez_fourteen(texel_size, blur_size);
// https://www.iryoku.com/next-generation-post-processing-in-call-of-duty-advanced-warfare (slides 120-135)
let sample_offset0 = (rotation_matrix * utils::SPIRAL_OFFSET_0_) * uv_offset_scale;
@ -153,11 +202,57 @@ fn sample_shadow_map_jimenez_fourteen(light_local: vec2<f32>, depth: f32, array_
return sum / 8.0;
}
// Performs the blocker search portion of percentage-closer soft shadows (PCSS).
// This is the variation used for directional lights.
//
// We can't use Castano '13 here because that has a hard-wired fixed size, while
// the PCSS algorithm requires a search size that varies based on the size of
// the light. So we instead use the D3D sample point positions, spaced according
// to the search size, to provide a sample pattern in a similar manner to the
// cubemap sampling approach we use for PCF.
//
// `search_size` is the size of the search region in texels.
fn search_for_blockers_in_shadow_map(
light_local: vec2<f32>,
depth: f32,
array_index: i32,
texel_size: f32,
search_size: f32,
) -> f32 {
let shadow_map_size = vec2<f32>(textureDimensions(view_bindings::directional_shadow_textures));
let uv_offset_scale = search_size / (texel_size * shadow_map_size);
let offset0 = D3D_SAMPLE_POINT_POSITIONS[0] * uv_offset_scale;
let offset1 = D3D_SAMPLE_POINT_POSITIONS[1] * uv_offset_scale;
let offset2 = D3D_SAMPLE_POINT_POSITIONS[2] * uv_offset_scale;
let offset3 = D3D_SAMPLE_POINT_POSITIONS[3] * uv_offset_scale;
let offset4 = D3D_SAMPLE_POINT_POSITIONS[4] * uv_offset_scale;
let offset5 = D3D_SAMPLE_POINT_POSITIONS[5] * uv_offset_scale;
let offset6 = D3D_SAMPLE_POINT_POSITIONS[6] * uv_offset_scale;
let offset7 = D3D_SAMPLE_POINT_POSITIONS[7] * uv_offset_scale;
var sum = vec2(0.0);
sum += search_for_blockers_in_shadow_map_hardware(light_local + offset0, depth, array_index);
sum += search_for_blockers_in_shadow_map_hardware(light_local + offset1, depth, array_index);
sum += search_for_blockers_in_shadow_map_hardware(light_local + offset2, depth, array_index);
sum += search_for_blockers_in_shadow_map_hardware(light_local + offset3, depth, array_index);
sum += search_for_blockers_in_shadow_map_hardware(light_local + offset4, depth, array_index);
sum += search_for_blockers_in_shadow_map_hardware(light_local + offset5, depth, array_index);
sum += search_for_blockers_in_shadow_map_hardware(light_local + offset6, depth, array_index);
sum += search_for_blockers_in_shadow_map_hardware(light_local + offset7, depth, array_index);
if (sum.y == 0.0) {
return 0.0;
}
return sum.x / sum.y;
}
fn sample_shadow_map(light_local: vec2<f32>, depth: f32, array_index: i32, texel_size: f32) -> f32 {
#ifdef SHADOW_FILTER_METHOD_GAUSSIAN
return sample_shadow_map_castano_thirteen(light_local, depth, array_index);
#else ifdef SHADOW_FILTER_METHOD_TEMPORAL
return sample_shadow_map_jimenez_fourteen(light_local, depth, array_index, texel_size);
return sample_shadow_map_jimenez_fourteen(
light_local, depth, array_index, texel_size, 1.0, true);
#else ifdef SHADOW_FILTER_METHOD_HARDWARE_2X2
return sample_shadow_map_hardware(light_local, depth, array_index);
#else
@ -169,6 +264,45 @@ fn sample_shadow_map(light_local: vec2<f32>, depth: f32, array_index: i32, texel
#endif
}
// Samples the shadow map for a directional light when percentage-closer soft
// shadows are being used.
//
// We first search for a *blocker*, which is the average depth value of any
// shadow map samples that are adjacent to the sample we're considering. That
// allows us to determine the penumbra size; a larger gap between the blocker
// and the depth of this sample results in a wider penumbra. Finally, we sample
// the shadow map the same way we do in PCF, using that penumbra width.
//
// A good overview of the technique:
// <https://medium.com/@varunm100/soft-shadows-for-mobile-ar-9e8da2e6f4ba>
fn sample_shadow_map_pcss(
light_local: vec2<f32>,
depth: f32,
array_index: i32,
texel_size: f32,
light_size: f32,
) -> f32 {
// Determine the average Z value of the closest blocker.
let z_blocker = search_for_blockers_in_shadow_map(
light_local, depth, array_index, texel_size, light_size);
// Don't let the blur size go below 0.5, or shadows will look unacceptably aliased.
let blur_size = max((z_blocker - depth) * light_size / depth, 0.5);
// FIXME: We can't use Castano '13 here because that has a hard-wired fixed
// size. So we instead use Jimenez '14 unconditionally. In the non-temporal
// variant this is unfortunately rather noisy. This may be improvable in the
// future by generating a mip chain of the shadow map and using that to
// provide better blurs.
#ifdef SHADOW_FILTER_METHOD_TEMPORAL
return sample_shadow_map_jimenez_fourteen(
light_local, depth, array_index, texel_size, blur_size, true);
#else // SHADOW_FILTER_METHOD_TEMPORAL
return sample_shadow_map_jimenez_fourteen(
light_local, depth, array_index, texel_size, blur_size, false);
#endif // SHADOW_FILTER_METHOD_TEMPORAL
}
// NOTE: Due to the non-uniform control flow in `shadows::fetch_point_shadow`,
// we must use the Level variant of textureSampleCompare to avoid undefined
// behavior due to some of the fragments in a quad (2x2 fragments) being
@ -176,12 +310,56 @@ fn sample_shadow_map(light_local: vec2<f32>, depth: f32, array_index: i32, texel
// The shadow maps have no mipmaps so Level just samples from LOD 0.
fn sample_shadow_cubemap_hardware(light_local: vec3<f32>, depth: f32, light_id: u32) -> f32 {
#ifdef NO_CUBE_ARRAY_TEXTURES_SUPPORT
return textureSampleCompare(view_bindings::point_shadow_textures, view_bindings::point_shadow_textures_sampler, light_local, depth);
return textureSampleCompare(
view_bindings::point_shadow_textures,
view_bindings::point_shadow_textures_comparison_sampler,
light_local,
depth
);
#else
return textureSampleCompareLevel(view_bindings::point_shadow_textures, view_bindings::point_shadow_textures_sampler, light_local, i32(light_id), depth);
return textureSampleCompareLevel(
view_bindings::point_shadow_textures,
view_bindings::point_shadow_textures_comparison_sampler,
light_local,
i32(light_id),
depth
);
#endif
}
// Performs one sample of the blocker search. This variation of the blocker
// search function is for point and spot lights.
fn search_for_blockers_in_shadow_cubemap_hardware(
light_local: vec3<f32>,
depth: f32,
light_id: u32,
) -> vec2<f32> {
#ifdef WEBGL2
// Make sure that the WebGL 2 compiler doesn't see `sampled_depth` sampled
// with different samplers, or it'll blow up.
return vec2(0.0);
#else // WEBGL2
#ifdef NO_CUBE_ARRAY_TEXTURES_SUPPORT
let sampled_depth = textureSample(
view_bindings::point_shadow_textures,
view_bindings::point_shadow_textures_linear_sampler,
light_local,
);
#else
let sampled_depth = textureSample(
view_bindings::point_shadow_textures,
view_bindings::point_shadow_textures_linear_sampler,
light_local,
i32(light_id),
);
#endif
return select(vec2(0.0), vec2(sampled_depth, 1.0), sampled_depth >= depth);
#endif // WEBGL2
}
fn sample_shadow_cubemap_at_offset(
position: vec2<f32>,
coeff: f32,
@ -198,6 +376,26 @@ fn sample_shadow_cubemap_at_offset(
) * coeff;
}
// Computes the search position and performs one sample of the blocker search.
// This variation of the blocker search function is for point and spot lights.
//
// `x_basis`, `y_basis`, and `light_local` form an orthonormal basis over which
// the blocker search happens.
fn search_for_blockers_in_shadow_cubemap_at_offset(
position: vec2<f32>,
x_basis: vec3<f32>,
y_basis: vec3<f32>,
light_local: vec3<f32>,
depth: f32,
light_id: u32,
) -> vec2<f32> {
return search_for_blockers_in_shadow_cubemap_hardware(
light_local + position.x * x_basis + position.y * y_basis,
depth,
light_id
);
}
// This more or less does what Castano13 does, but in 3D space. Castano13 is
// essentially an optimized 2D Gaussian filter that takes advantage of the
// bilinear filtering hardware to reduce the number of samples needed. This
@ -249,12 +447,13 @@ fn sample_shadow_cubemap_gaussian(
// This is a port of the Jimenez14 filter above to the 3D space. It jitters the
// points in the spiral pattern after first creating a 2D orthonormal basis
// along the principal light direction.
fn sample_shadow_cubemap_temporal(
fn sample_shadow_cubemap_jittered(
light_local: vec3<f32>,
depth: f32,
scale: f32,
distance_to_light: f32,
light_id: u32,
temporal: bool,
) -> f32 {
// Create an orthonormal basis so we can apply a 2D sampling pattern to a
// cubemap.
@ -264,7 +463,7 @@ fn sample_shadow_cubemap_temporal(
}
let basis = orthonormalize(light_local, up) * scale * distance_to_light;
let rotation_matrix = random_rotation_matrix(vec2(1.0));
let rotation_matrix = random_rotation_matrix(vec2(1.0), temporal);
let sample_offset0 = rotation_matrix * utils::SPIRAL_OFFSET_0_ *
POINT_SHADOW_TEMPORAL_OFFSET_SCALE;
@ -313,8 +512,8 @@ fn sample_shadow_cubemap(
return sample_shadow_cubemap_gaussian(
light_local, depth, POINT_SHADOW_SCALE, distance_to_light, light_id);
#else ifdef SHADOW_FILTER_METHOD_TEMPORAL
return sample_shadow_cubemap_temporal(
light_local, depth, POINT_SHADOW_SCALE, distance_to_light, light_id);
return sample_shadow_cubemap_jittered(
light_local, depth, POINT_SHADOW_SCALE, distance_to_light, light_id, true);
#else ifdef SHADOW_FILTER_METHOD_HARDWARE_2X2
return sample_shadow_cubemap_hardware(light_local, depth, light_id);
#else
@ -325,3 +524,76 @@ fn sample_shadow_cubemap(
return 0.0;
#endif
}
// Searches for PCSS blockers in a cubemap. This is the variant of the blocker
// search used for point and spot lights.
//
// This follows the logic in `sample_shadow_cubemap_gaussian`, but uses linear
// sampling instead of percentage-closer filtering.
//
// The `scale` parameter represents the size of the light.
fn search_for_blockers_in_shadow_cubemap(
light_local: vec3<f32>,
depth: f32,
scale: f32,
distance_to_light: f32,
light_id: u32,
) -> f32 {
// Create an orthonormal basis so we can apply a 2D sampling pattern to a
// cubemap.
var up = vec3(0.0, 1.0, 0.0);
if (dot(up, normalize(light_local)) > 0.99) {
up = vec3(1.0, 0.0, 0.0); // Avoid creating a degenerate basis.
}
let basis = orthonormalize(light_local, up) * scale * distance_to_light;
var sum: vec2<f32> = vec2(0.0);
sum += search_for_blockers_in_shadow_cubemap_at_offset(
D3D_SAMPLE_POINT_POSITIONS[0], basis[0], basis[1], light_local, depth, light_id);
sum += search_for_blockers_in_shadow_cubemap_at_offset(
D3D_SAMPLE_POINT_POSITIONS[1], basis[0], basis[1], light_local, depth, light_id);
sum += search_for_blockers_in_shadow_cubemap_at_offset(
D3D_SAMPLE_POINT_POSITIONS[2], basis[0], basis[1], light_local, depth, light_id);
sum += search_for_blockers_in_shadow_cubemap_at_offset(
D3D_SAMPLE_POINT_POSITIONS[3], basis[0], basis[1], light_local, depth, light_id);
sum += search_for_blockers_in_shadow_cubemap_at_offset(
D3D_SAMPLE_POINT_POSITIONS[4], basis[0], basis[1], light_local, depth, light_id);
sum += search_for_blockers_in_shadow_cubemap_at_offset(
D3D_SAMPLE_POINT_POSITIONS[5], basis[0], basis[1], light_local, depth, light_id);
sum += search_for_blockers_in_shadow_cubemap_at_offset(
D3D_SAMPLE_POINT_POSITIONS[6], basis[0], basis[1], light_local, depth, light_id);
sum += search_for_blockers_in_shadow_cubemap_at_offset(
D3D_SAMPLE_POINT_POSITIONS[7], basis[0], basis[1], light_local, depth, light_id);
if (sum.y == 0.0) {
return 0.0;
}
return sum.x / sum.y;
}
// Samples the shadow map for a point or spot light when percentage-closer soft
// shadows are being used.
//
// A good overview of the technique:
// <https://medium.com/@varunm100/soft-shadows-for-mobile-ar-9e8da2e6f4ba>
fn sample_shadow_cubemap_pcss(
light_local: vec3<f32>,
distance_to_light: f32,
depth: f32,
light_id: u32,
light_size: f32,
) -> f32 {
let z_blocker = search_for_blockers_in_shadow_cubemap(
light_local, depth, light_size, distance_to_light, light_id);
// Don't let the blur size go below 0.5, or shadows will look unacceptably aliased.
let blur_size = max((z_blocker - depth) * light_size / depth, 0.5);
#ifdef SHADOW_FILTER_METHOD_TEMPORAL
return sample_shadow_cubemap_jittered(
light_local, depth, POINT_SHADOW_SCALE * blur_size, distance_to_light, light_id, true);
#else
return sample_shadow_cubemap_jittered(
light_local, depth, POINT_SHADOW_SCALE * blur_size, distance_to_light, light_id, false);
#endif
}

View file

@ -3,7 +3,10 @@
#import bevy_pbr::{
mesh_view_types::POINT_LIGHT_FLAGS_SPOT_LIGHT_Y_NEGATIVE,
mesh_view_bindings as view_bindings,
shadow_sampling::{SPOT_SHADOW_TEXEL_SIZE, sample_shadow_cubemap, sample_shadow_map}
shadow_sampling::{
SPOT_SHADOW_TEXEL_SIZE, sample_shadow_cubemap, sample_shadow_cubemap_pcss,
sample_shadow_map, sample_shadow_map_pcss,
}
}
#import bevy_render::{
@ -41,12 +44,30 @@ fn fetch_point_shadow(light_id: u32, frag_position: vec4<f32>, surface_normal: v
let zw = -major_axis_magnitude * (*light).light_custom_data.xy + (*light).light_custom_data.zw;
let depth = zw.x / zw.y;
// Do the lookup, using HW PCF and comparison. Cubemaps assume a left-handed coordinate space,
// so we have to flip the z-axis when sampling.
// If soft shadows are enabled, use the PCSS path. Cubemaps assume a
// left-handed coordinate space, so we have to flip the z-axis when
// sampling.
if ((*light).soft_shadow_size > 0.0) {
return sample_shadow_cubemap_pcss(
frag_ls * flip_z,
distance_to_light,
depth,
light_id,
(*light).soft_shadow_size,
);
}
// Do the lookup, using HW PCF and comparison. Cubemaps assume a left-handed
// coordinate space, so we have to flip the z-axis when sampling.
return sample_shadow_cubemap(frag_ls * flip_z, distance_to_light, depth, light_id);
}
fn fetch_spot_shadow(light_id: u32, frag_position: vec4<f32>, surface_normal: vec3<f32>) -> f32 {
fn fetch_spot_shadow(
light_id: u32,
frag_position: vec4<f32>,
surface_normal: vec3<f32>,
near_z: f32,
) -> f32 {
let light = &view_bindings::clusterable_objects.data[light_id];
let surface_to_light = (*light).position_radius.xyz - frag_position.xyz;
@ -91,15 +112,16 @@ fn fetch_spot_shadow(light_id: u32, frag_position: vec4<f32>, surface_normal: ve
// convert to uv coordinates
let shadow_uv = shadow_xy_ndc * vec2<f32>(0.5, -0.5) + vec2<f32>(0.5, 0.5);
// 0.1 must match POINT_LIGHT_NEAR_Z
let depth = 0.1 / -projected_position.z;
let depth = near_z / -projected_position.z;
return sample_shadow_map(
shadow_uv,
depth,
i32(light_id) + view_bindings::lights.spot_light_shadowmap_offset,
SPOT_SHADOW_TEXEL_SIZE
);
// If soft shadows are enabled, use the PCSS path.
let array_index = i32(light_id) + view_bindings::lights.spot_light_shadowmap_offset;
if ((*light).soft_shadow_size > 0.0) {
return sample_shadow_map_pcss(
shadow_uv, depth, array_index, SPOT_SHADOW_TEXEL_SIZE, (*light).soft_shadow_size);
}
return sample_shadow_map(shadow_uv, depth, array_index, SPOT_SHADOW_TEXEL_SIZE);
}
fn get_cascade_index(light_id: u32, view_z: f32) -> u32 {
@ -146,7 +168,12 @@ fn world_to_directional_light_local(
return vec4(light_local, depth, 1.0);
}
fn sample_directional_cascade(light_id: u32, cascade_index: u32, frag_position: vec4<f32>, surface_normal: vec3<f32>) -> f32 {
fn sample_directional_cascade(
light_id: u32,
cascade_index: u32,
frag_position: vec4<f32>,
surface_normal: vec3<f32>,
) -> f32 {
let light = &view_bindings::lights.directional_lights[light_id];
let cascade = &(*light).cascades[cascade_index];
@ -161,7 +188,15 @@ fn sample_directional_cascade(light_id: u32, cascade_index: u32, frag_position:
}
let array_index = i32((*light).depth_texture_base_index + cascade_index);
return sample_shadow_map(light_local.xy, light_local.z, array_index, (*cascade).texel_size);
let texel_size = (*cascade).texel_size;
// If soft shadows are enabled, use the PCSS path.
if ((*light).soft_shadow_size > 0.0) {
return sample_shadow_map_pcss(
light_local.xy, light_local.z, array_index, texel_size, (*light).soft_shadow_size);
}
return sample_shadow_map(light_local.xy, light_local.z, array_index, texel_size);
}
fn fetch_directional_shadow(light_id: u32, frag_position: vec4<f32>, surface_normal: vec3<f32>, view_z: f32) -> f32 {

417
examples/3d/pcss.rs Normal file
View file

@ -0,0 +1,417 @@
//! Demonstrates percentage-closer soft shadows (PCSS).
use std::f32::consts::PI;
use bevy::{
core_pipeline::{
experimental::taa::{TemporalAntiAliasPlugin, TemporalAntiAliasing},
prepass::{DepthPrepass, MotionVectorPrepass},
Skybox,
},
math::vec3,
pbr::{CubemapVisibleEntities, ShadowFilteringMethod, VisibleMeshEntities},
prelude::*,
render::{
camera::TemporalJitter,
primitives::{CubemapFrusta, Frustum},
},
};
use crate::widgets::{RadioButton, RadioButtonText, WidgetClickEvent, WidgetClickSender};
#[path = "../helpers/widgets.rs"]
mod widgets;
/// The size of the light, which affects the size of the penumbras.
const LIGHT_RADIUS: f32 = 10.0;
/// The intensity of the point and spot lights.
const POINT_LIGHT_INTENSITY: f32 = 1_000_000_000.0;
/// The range in meters of the point and spot lights.
const POINT_LIGHT_RANGE: f32 = 110.0;
/// The depth bias for directional and spot lights. This value is set higher
/// than the default to avoid shadow acne.
const DIRECTIONAL_SHADOW_DEPTH_BIAS: f32 = 0.20;
/// The depth bias for point lights. This value is set higher than the default to
/// avoid shadow acne.
///
/// Unfortunately, there is a bit of Peter Panning with this value, because of
/// the distance and angle of the light. This can't be helped in this scene
/// without increasing the shadow map size beyond reasonable limits.
const POINT_SHADOW_DEPTH_BIAS: f32 = 0.35;
/// The near Z value for the shadow map, in meters. This is set higher than the
/// default in order to achieve greater resolution in the shadow map for point
/// and spot lights.
const SHADOW_MAP_NEAR_Z: f32 = 50.0;
/// The current application settings (light type, shadow filter, and the status
/// of PCSS).
#[derive(Resource)]
struct AppStatus {
/// The type of light presently in the scene: either directional or point.
light_type: LightType,
/// The type of shadow filter: Gaussian or temporal.
shadow_filter: ShadowFilter,
/// Whether soft shadows are enabled.
soft_shadows: bool,
}
impl Default for AppStatus {
fn default() -> Self {
Self {
light_type: default(),
shadow_filter: default(),
soft_shadows: true,
}
}
}
/// The type of light presently in the scene: directional, point, or spot.
#[derive(Clone, Copy, Default, PartialEq)]
enum LightType {
/// A directional light, with a cascaded shadow map.
#[default]
Directional,
/// A point light, with a cube shadow map.
Point,
/// A spot light, with a cube shadow map.
Spot,
}
/// The type of shadow filter.
///
/// Generally, `Gaussian` is preferred when temporal antialiasing isn't in use,
/// while `Temporal` is preferred when TAA is in use. In this example, this
/// setting also turns TAA on and off.
#[derive(Clone, Copy, Default, PartialEq)]
enum ShadowFilter {
/// The non-temporal Gaussian filter (Castano '13 for directional lights, an
/// analogous alternative for point and spot lights).
#[default]
NonTemporal,
/// The temporal Gaussian filter (Jimenez '14 for directional lights, an
/// analogous alternative for point and spot lights).
Temporal,
}
/// Each example setting that can be toggled in the UI.
#[derive(Clone, Copy, PartialEq)]
enum AppSetting {
/// The type of light presently in the scene: directional, point, or spot.
LightType(LightType),
/// The type of shadow filter.
ShadowFilter(ShadowFilter),
/// Whether PCSS is enabled or disabled.
SoftShadows(bool),
}
/// The example application entry point.
fn main() {
App::new()
.init_resource::<AppStatus>()
.add_plugins(DefaultPlugins.set(WindowPlugin {
primary_window: Some(Window {
title: "Bevy Percentage Closer Soft Shadows Example".into(),
..default()
}),
..default()
}))
.add_plugins(TemporalAntiAliasPlugin)
.add_event::<WidgetClickEvent<AppSetting>>()
.add_systems(Startup, setup)
.add_systems(Update, widgets::handle_ui_interactions::<AppSetting>)
.add_systems(
Update,
update_radio_buttons.after(widgets::handle_ui_interactions::<AppSetting>),
)
.add_systems(
Update,
(
handle_light_type_change,
handle_shadow_filter_change,
handle_pcss_toggle,
)
.after(widgets::handle_ui_interactions::<AppSetting>),
)
.run();
}
/// Creates all the objects in the scene.
fn setup(mut commands: Commands, asset_server: Res<AssetServer>, app_status: Res<AppStatus>) {
spawn_camera(&mut commands, &asset_server);
spawn_light(&mut commands, &app_status);
spawn_gltf_scene(&mut commands, &asset_server);
spawn_buttons(&mut commands);
}
/// Spawns the camera, with the initial shadow filtering method.
fn spawn_camera(commands: &mut Commands, asset_server: &AssetServer) {
commands
.spawn(Camera3dBundle {
transform: Transform::from_xyz(-12.912 * 0.7, 4.466 * 0.7, -10.624 * 0.7)
.with_rotation(Quat::from_euler(
EulerRot::YXZ,
-134.76 / 180.0 * PI,
-0.175,
0.0,
)),
..default()
})
.insert(ShadowFilteringMethod::Gaussian)
// `TemporalJitter` is needed for TAA. Note that it does nothing without
// `TemporalAntiAliasSettings`.
.insert(TemporalJitter::default())
// We want MSAA off for TAA to work properly.
.insert(Msaa::Off)
// The depth prepass is needed for TAA.
.insert(DepthPrepass)
// The motion vector prepass is needed for TAA.
.insert(MotionVectorPrepass)
// Add a nice skybox.
.insert(Skybox {
image: asset_server.load("environment_maps/sky_skybox.ktx2"),
brightness: 500.0,
rotation: Quat::IDENTITY,
});
}
/// Spawns the initial light.
fn spawn_light(commands: &mut Commands, app_status: &AppStatus) {
// Because this light can become a directional light, point light, or spot
// light depending on the settings, we add the union of the components
// necessary for this light to behave as all three of those.
commands
.spawn(DirectionalLightBundle {
directional_light: create_directional_light(app_status),
transform: Transform::from_rotation(Quat::from_array([
0.6539259,
-0.34646285,
0.36505926,
-0.5648683,
]))
.with_translation(vec3(57.693, 34.334, -6.422)),
..default()
})
// These two are needed for point lights.
.insert(CubemapVisibleEntities::default())
.insert(CubemapFrusta::default())
// These two are needed for spot lights.
.insert(VisibleMeshEntities::default())
.insert(Frustum::default());
}
/// Loads and spawns the glTF palm tree scene.
fn spawn_gltf_scene(commands: &mut Commands, asset_server: &AssetServer) {
commands.spawn(SceneBundle {
scene: asset_server.load("models/PalmTree/PalmTree.gltf#Scene0"),
..default()
});
}
/// Spawns all the buttons at the bottom of the screen.
fn spawn_buttons(commands: &mut Commands) {
commands
.spawn(NodeBundle {
style: widgets::main_ui_style(),
..default()
})
.with_children(|parent| {
widgets::spawn_option_buttons(
parent,
"Light Type",
&[
(AppSetting::LightType(LightType::Directional), "Directional"),
(AppSetting::LightType(LightType::Point), "Point"),
(AppSetting::LightType(LightType::Spot), "Spot"),
],
);
widgets::spawn_option_buttons(
parent,
"Shadow Filter",
&[
(AppSetting::ShadowFilter(ShadowFilter::Temporal), "Temporal"),
(
AppSetting::ShadowFilter(ShadowFilter::NonTemporal),
"Non-Temporal",
),
],
);
widgets::spawn_option_buttons(
parent,
"Soft Shadows",
&[
(AppSetting::SoftShadows(true), "On"),
(AppSetting::SoftShadows(false), "Off"),
],
);
});
}
/// Updates the style of the radio buttons that enable and disable soft shadows
/// to reflect whether PCSS is enabled.
fn update_radio_buttons(
mut widgets: Query<
(
Option<&mut BackgroundColor>,
Option<&mut Text>,
&WidgetClickSender<AppSetting>,
),
Or<(With<RadioButton>, With<RadioButtonText>)>,
>,
app_status: Res<AppStatus>,
) {
for (image, text, sender) in widgets.iter_mut() {
let selected = match **sender {
AppSetting::LightType(light_type) => light_type == app_status.light_type,
AppSetting::ShadowFilter(shadow_filter) => shadow_filter == app_status.shadow_filter,
AppSetting::SoftShadows(soft_shadows) => soft_shadows == app_status.soft_shadows,
};
if let Some(mut bg_color) = image {
widgets::update_ui_radio_button(&mut bg_color, selected);
}
if let Some(mut text) = text {
widgets::update_ui_radio_button_text(&mut text, selected);
}
}
}
/// Handles requests from the user to change the type of light.
fn handle_light_type_change(
mut commands: Commands,
mut lights: Query<Entity, Or<(With<DirectionalLight>, With<PointLight>, With<SpotLight>)>>,
mut events: EventReader<WidgetClickEvent<AppSetting>>,
mut app_status: ResMut<AppStatus>,
) {
for event in events.read() {
let AppSetting::LightType(light_type) = **event else {
continue;
};
app_status.light_type = light_type;
for light in lights.iter_mut() {
let light_commands = commands
.entity(light)
.remove::<DirectionalLight>()
.remove::<PointLight>()
.remove::<SpotLight>();
match light_type {
LightType::Point => {
light_commands.insert(create_point_light(&app_status));
}
LightType::Spot => {
light_commands.insert(create_spot_light(&app_status));
}
LightType::Directional => {
light_commands.insert(create_directional_light(&app_status));
}
}
}
}
}
/// Handles requests from the user to change the shadow filter method.
///
/// This system is also responsible for enabling and disabling TAA as
/// appropriate.
fn handle_shadow_filter_change(
mut commands: Commands,
mut cameras: Query<(Entity, &mut ShadowFilteringMethod)>,
mut events: EventReader<WidgetClickEvent<AppSetting>>,
mut app_status: ResMut<AppStatus>,
) {
for event in events.read() {
let AppSetting::ShadowFilter(shadow_filter) = **event else {
continue;
};
app_status.shadow_filter = shadow_filter;
for (camera, mut shadow_filtering_method) in cameras.iter_mut() {
match shadow_filter {
ShadowFilter::NonTemporal => {
*shadow_filtering_method = ShadowFilteringMethod::Gaussian;
commands.entity(camera).remove::<TemporalAntiAliasing>();
}
ShadowFilter::Temporal => {
*shadow_filtering_method = ShadowFilteringMethod::Temporal;
commands
.entity(camera)
.insert(TemporalAntiAliasing::default());
}
}
}
}
}
/// Handles requests from the user to toggle soft shadows on and off.
fn handle_pcss_toggle(
mut lights: Query<AnyOf<(&mut DirectionalLight, &mut PointLight, &mut SpotLight)>>,
mut events: EventReader<WidgetClickEvent<AppSetting>>,
mut app_status: ResMut<AppStatus>,
) {
for event in events.read() {
let AppSetting::SoftShadows(value) = **event else {
continue;
};
app_status.soft_shadows = value;
// Recreating the lights is the simplest way to toggle soft shadows.
for (directional_light, point_light, spot_light) in lights.iter_mut() {
if let Some(mut directional_light) = directional_light {
*directional_light = create_directional_light(&app_status);
}
if let Some(mut point_light) = point_light {
*point_light = create_point_light(&app_status);
}
if let Some(mut spot_light) = spot_light {
*spot_light = create_spot_light(&app_status);
}
}
}
}
/// Creates the [`DirectionalLight`] component with the appropriate settings.
fn create_directional_light(app_status: &AppStatus) -> DirectionalLight {
DirectionalLight {
shadows_enabled: true,
soft_shadow_size: if app_status.soft_shadows {
Some(LIGHT_RADIUS)
} else {
None
},
shadow_depth_bias: DIRECTIONAL_SHADOW_DEPTH_BIAS,
..default()
}
}
/// Creates the [`PointLight`] component with the appropriate settings.
fn create_point_light(app_status: &AppStatus) -> PointLight {
PointLight {
intensity: POINT_LIGHT_INTENSITY,
range: POINT_LIGHT_RANGE,
shadows_enabled: true,
radius: LIGHT_RADIUS,
soft_shadows_enabled: app_status.soft_shadows,
shadow_depth_bias: POINT_SHADOW_DEPTH_BIAS,
shadow_map_near_z: SHADOW_MAP_NEAR_Z,
..default()
}
}
/// Creates the [`SpotLight`] component with the appropriate settings.
fn create_spot_light(app_status: &AppStatus) -> SpotLight {
SpotLight {
intensity: POINT_LIGHT_INTENSITY,
range: POINT_LIGHT_RANGE,
radius: LIGHT_RADIUS,
shadows_enabled: true,
soft_shadows_enabled: app_status.soft_shadows,
shadow_depth_bias: DIRECTIONAL_SHADOW_DEPTH_BIAS,
shadow_map_near_z: SHADOW_MAP_NEAR_Z,
..default()
}
}

View file

@ -156,6 +156,7 @@ Example | Description
[Orthographic View](../examples/3d/orthographic.rs) | Shows how to create a 3D orthographic view (for isometric-look in games or CAD applications)
[Parallax Mapping](../examples/3d/parallax_mapping.rs) | Demonstrates use of a normal map and depth map for parallax mapping
[Parenting](../examples/3d/parenting.rs) | Demonstrates parent->child relationships and relative transformations
[Percentage-closer soft shadows](../examples/3d/pcss.rs) | Demonstrates percentage-closer soft shadows (PCSS)
[Physically Based Rendering](../examples/3d/pbr.rs) | Demonstrates use of Physically Based Rendering (PBR) properties
[Reflection Probes](../examples/3d/reflection_probes.rs) | Demonstrates reflection probes
[Render to Texture](../examples/3d/render_to_texture.rs) | Shows how to render to a texture, useful for mirrors, UI, or exporting images

177
examples/helpers/widgets.rs Normal file
View file

@ -0,0 +1,177 @@
//! Simple widgets for example UI.
use bevy::{ecs::system::EntityCommands, prelude::*};
/// An event that's sent whenever the user changes one of the settings by
/// clicking a radio button.
#[derive(Clone, Event, Deref, DerefMut)]
pub struct WidgetClickEvent<T>(T);
/// A marker component that we place on all widgets that send
/// [`WidgetClickEvent`]s of the given type.
#[derive(Clone, Component, Deref, DerefMut)]
pub struct WidgetClickSender<T>(T)
where
T: Clone + Send + Sync + 'static;
/// A marker component that we place on all radio `Button`s.
#[derive(Clone, Copy, Component)]
pub struct RadioButton;
/// A marker component that we place on all `Text` inside radio buttons.
#[derive(Clone, Copy, Component)]
pub struct RadioButtonText;
/// Returns a [`Style`] appropriate for the outer main UI node.
///
/// This UI is in the bottom left corner and has flex column support
pub fn main_ui_style() -> Style {
Style {
flex_direction: FlexDirection::Column,
position_type: PositionType::Absolute,
row_gap: Val::Px(6.0),
left: Val::Px(10.0),
bottom: Val::Px(10.0),
..default()
}
}
/// Spawns a single radio button that allows configuration of a setting.
///
/// The type parameter specifies the value that will be packaged up and sent in
/// a [`WidgetClickEvent`] when the radio button is clicked.
pub fn spawn_option_button<T>(
parent: &mut ChildBuilder,
option_value: T,
option_name: &str,
is_selected: bool,
is_first: bool,
is_last: bool,
) where
T: Clone + Send + Sync + 'static,
{
let (bg_color, fg_color) = if is_selected {
(Color::WHITE, Color::BLACK)
} else {
(Color::BLACK, Color::WHITE)
};
// Add the button node.
parent
.spawn(ButtonBundle {
style: Style {
border: UiRect::all(Val::Px(1.0)).with_left(if is_first {
Val::Px(1.0)
} else {
Val::Px(0.0)
}),
justify_content: JustifyContent::Center,
align_items: AlignItems::Center,
padding: UiRect::axes(Val::Px(12.0), Val::Px(6.0)),
..default()
},
border_color: BorderColor(Color::WHITE),
border_radius: BorderRadius::ZERO
.with_left(if is_first { Val::Px(6.0) } else { Val::Px(0.0) })
.with_right(if is_last { Val::Px(6.0) } else { Val::Px(0.0) }),
background_color: BackgroundColor(bg_color),
..default()
})
.insert(RadioButton)
.insert(WidgetClickSender(option_value.clone()))
.with_children(|parent| {
spawn_ui_text(parent, option_name, fg_color)
.insert(RadioButtonText)
.insert(WidgetClickSender(option_value));
});
}
/// Spawns the buttons that allow configuration of a setting.
///
/// The user may change the setting to any one of the labeled `options`. The
/// value of the given type parameter will be packaged up and sent as a
/// [`WidgetClickEvent`] when one of the radio buttons is clicked.
pub fn spawn_option_buttons<T>(parent: &mut ChildBuilder, title: &str, options: &[(T, &str)])
where
T: Clone + Send + Sync + 'static,
{
// Add the parent node for the row.
parent
.spawn(NodeBundle {
style: Style {
align_items: AlignItems::Center,
..default()
},
..default()
})
.with_children(|parent| {
spawn_ui_text(parent, title, Color::BLACK).insert(Style {
width: Val::Px(125.0),
..default()
});
for (option_index, (option_value, option_name)) in options.iter().cloned().enumerate() {
spawn_option_button(
parent,
option_value,
option_name,
option_index == 0,
option_index == 0,
option_index == options.len() - 1,
);
}
});
}
/// Spawns text for the UI.
///
/// Returns the `EntityCommands`, which allow further customization of the text
/// style.
pub fn spawn_ui_text<'a>(
parent: &'a mut ChildBuilder,
label: &str,
color: Color,
) -> EntityCommands<'a> {
parent.spawn(TextBundle::from_section(
label,
TextStyle {
font_size: 18.0,
color,
..default()
},
))
}
/// Checks for clicks on the radio buttons and sends `RadioButtonChangeEvent`s
/// as necessary.
pub fn handle_ui_interactions<T>(
mut interactions: Query<
(&Interaction, &WidgetClickSender<T>),
(With<Button>, With<RadioButton>),
>,
mut widget_click_events: EventWriter<WidgetClickEvent<T>>,
) where
T: Clone + Send + Sync + 'static,
{
for (interaction, click_event) in interactions.iter_mut() {
if *interaction == Interaction::Pressed {
widget_click_events.send(WidgetClickEvent((**click_event).clone()));
}
}
}
/// Updates the style of the button part of a radio button to reflect its
/// selected status.
pub fn update_ui_radio_button(background_color: &mut BackgroundColor, selected: bool) {
background_color.0 = if selected { Color::WHITE } else { Color::BLACK };
}
/// Updates the style of the label of a radio button to reflect its selected
/// status.
pub fn update_ui_radio_button_text(text: &mut Text, selected: bool) {
let text_color = if selected { Color::BLACK } else { Color::WHITE };
for section in &mut text.sections {
section.style.color = text_color;
}
}

View file

@ -2,6 +2,7 @@
extend-exclude = [
"*.pbxproj", # metadata file
"*.patch", # Automatically generated files that should not be manually modified.
"*.bin", # Binary files
]
# Corrections take the form of a key/value pair. The key is the incorrect word