bevy/examples/3d/pcss.rs
Patrick Walton 2ae5a21009
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>
2024-09-18 18:07:17 +00:00

417 lines
14 KiB
Rust

//! 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()
}
}