Skip to main content

pcss/
pcss.rs

1//! Demonstrates percentage-closer soft shadows (PCSS).
2
3use std::f32::consts::PI;
4
5#[cfg(feature = "free_camera")]
6use bevy::camera_controller::free_camera::{FreeCamera, FreeCameraPlugin};
7use bevy::{
8    anti_alias::taa::TemporalAntiAliasing,
9    camera::{
10        primitives::{CubemapFrusta, Frustum},
11        visibility::{CubemapVisibleEntities, VisibleMeshEntities},
12    },
13    core_pipeline::prepass::{DepthPrepass, MotionVectorPrepass},
14    light::{ShadowFilteringMethod, Skybox},
15    math::vec3,
16    prelude::*,
17    render::camera::TemporalJitter,
18};
19
20use crate::widgets::{RadioButton, RadioButtonText, WidgetClickEvent, WidgetClickSender};
21
22#[path = "../helpers/widgets.rs"]
23mod widgets;
24
25/// The size of the light, which affects the size of the penumbras.
26const LIGHT_RADIUS: f32 = 10.0;
27
28/// The intensity of the point and spot lights.
29const POINT_LIGHT_INTENSITY: f32 = 1_000_000_000.0;
30
31/// The range in meters of the point and spot lights.
32const POINT_LIGHT_RANGE: f32 = 110.0;
33
34/// The depth bias for directional and spot lights. This value is set higher
35/// than the default to avoid shadow acne.
36const DIRECTIONAL_SHADOW_DEPTH_BIAS: f32 = 0.20;
37
38/// The depth bias for point lights. This value is set higher than the default to
39/// avoid shadow acne.
40///
41/// Unfortunately, there is a bit of Peter Panning with this value, because of
42/// the distance and angle of the light. This can't be helped in this scene
43/// without increasing the shadow map size beyond reasonable limits.
44const POINT_SHADOW_DEPTH_BIAS: f32 = 0.35;
45
46/// The near Z value for the shadow map, in meters. This is set higher than the
47/// default in order to achieve greater resolution in the shadow map for point
48/// and spot lights.
49const SHADOW_MAP_NEAR_Z: f32 = 50.0;
50
51/// The current application settings (light type, shadow filter, and the status
52/// of PCSS).
53#[derive(Resource)]
54struct AppStatus {
55    /// The type of light presently in the scene: either directional or point.
56    light_type: LightType,
57    /// The type of shadow filter: Gaussian or temporal.
58    shadow_filter: ShadowFilter,
59    /// Whether soft shadows are enabled.
60    soft_shadows: bool,
61}
62
63impl Default for AppStatus {
64    fn default() -> Self {
65        Self {
66            light_type: default(),
67            shadow_filter: default(),
68            soft_shadows: true,
69        }
70    }
71}
72
73/// The type of light presently in the scene: directional, point, or spot.
74#[derive(Clone, Copy, Default, PartialEq)]
75enum LightType {
76    /// A directional light, with a cascaded shadow map.
77    #[default]
78    Directional,
79    /// A point light, with a cube shadow map.
80    Point,
81    /// A spot light, with a cube shadow map.
82    Spot,
83}
84
85/// The type of shadow filter.
86///
87/// Generally, `Gaussian` is preferred when temporal antialiasing isn't in use,
88/// while `Temporal` is preferred when TAA is in use. In this example, this
89/// setting also turns TAA on and off.
90#[derive(Clone, Copy, Default, PartialEq)]
91enum ShadowFilter {
92    /// The non-temporal Gaussian filter (Castano '13 for directional lights, an
93    /// analogous alternative for point and spot lights).
94    #[default]
95    NonTemporal,
96    /// The temporal Gaussian filter (Jimenez '14 for directional lights, an
97    /// analogous alternative for point and spot lights).
98    Temporal,
99}
100
101/// Each example setting that can be toggled in the UI.
102#[derive(Clone, Copy, PartialEq)]
103enum AppSetting {
104    /// The type of light presently in the scene: directional, point, or spot.
105    LightType(LightType),
106    /// The type of shadow filter.
107    ShadowFilter(ShadowFilter),
108    /// Whether PCSS is enabled or disabled.
109    SoftShadows(bool),
110}
111
112/// The example application entry point.
113fn main() {
114    #[cfg(not(feature = "free_camera"))]
115    println!("Enable feature free_camera to add a free camera to this example");
116
117    App::new()
118        .init_resource::<AppStatus>()
119        .add_plugins((
120            DefaultPlugins.set(WindowPlugin {
121                primary_window: Some(Window {
122                    title: "Bevy Percentage Closer Soft Shadows Example".into(),
123                    ..default()
124                }),
125                ..default()
126            }),
127            #[cfg(feature = "free_camera")]
128            FreeCameraPlugin,
129        ))
130        .add_message::<WidgetClickEvent<AppSetting>>()
131        .add_systems(Startup, setup)
132        .add_systems(Update, widgets::handle_ui_interactions::<AppSetting>)
133        .add_systems(
134            Update,
135            update_radio_buttons.after(widgets::handle_ui_interactions::<AppSetting>),
136        )
137        .add_systems(
138            Update,
139            (
140                handle_light_type_change,
141                handle_shadow_filter_change,
142                handle_pcss_toggle,
143            )
144                .after(widgets::handle_ui_interactions::<AppSetting>),
145        )
146        .run();
147}
148
149/// Creates all the objects in the scene.
150fn setup(mut commands: Commands, asset_server: Res<AssetServer>, app_status: Res<AppStatus>) {
151    spawn_camera(&mut commands, &asset_server);
152    spawn_light(&mut commands, &app_status);
153    spawn_gltf_scene(&mut commands, &asset_server);
154    spawn_buttons(&mut commands);
155}
156
157/// Spawns the camera, with the initial shadow filtering method.
158fn spawn_camera(commands: &mut Commands, asset_server: &AssetServer) {
159    commands
160        .spawn((
161            Camera3d::default(),
162            Transform::from_xyz(-12.912 * 0.7, 4.466 * 0.7, -10.624 * 0.7).with_rotation(
163                Quat::from_euler(EulerRot::YXZ, -134.76 / 180.0 * PI, -0.175, 0.0),
164            ),
165            #[cfg(feature = "free_camera")]
166            FreeCamera::default(),
167        ))
168        .insert(ShadowFilteringMethod::Gaussian)
169        // `TemporalJitter` is needed for TAA. Note that it does nothing without
170        // `TemporalAntiAliasSettings`.
171        .insert(TemporalJitter::default())
172        // We want MSAA off for TAA to work properly.
173        .insert(Msaa::Off)
174        // The depth prepass is needed for TAA.
175        .insert(DepthPrepass)
176        // The motion vector prepass is needed for TAA.
177        .insert(MotionVectorPrepass)
178        // Add a nice skybox.
179        .insert(Skybox {
180            image: Some(asset_server.load("environment_maps/sky_skybox.ktx2")),
181            brightness: 500.0,
182            rotation: Quat::IDENTITY,
183        });
184}
185
186/// Spawns the initial light.
187fn spawn_light(commands: &mut Commands, app_status: &AppStatus) {
188    // Because this light can become a directional light, point light, or spot
189    // light depending on the settings, we add the union of the components
190    // necessary for this light to behave as all three of those.
191    commands
192        .spawn((
193            create_directional_light(app_status),
194            Transform::from_rotation(Quat::from_array([
195                0.6539259,
196                -0.34646285,
197                0.36505926,
198                -0.5648683,
199            ]))
200            .with_translation(vec3(57.693, 34.334, -6.422)),
201        ))
202        // These two are needed for point lights.
203        .insert(CubemapVisibleEntities::default())
204        .insert(CubemapFrusta::default())
205        // These two are needed for spot lights.
206        .insert(VisibleMeshEntities::default())
207        .insert(Frustum::default());
208}
209
210/// Loads and spawns the glTF palm tree scene.
211fn spawn_gltf_scene(commands: &mut Commands, asset_server: &AssetServer) {
212    commands.spawn(WorldAssetRoot(
213        asset_server.load("models/PalmTree/PalmTree.gltf#Scene0"),
214    ));
215}
216
217/// Spawns all the buttons at the bottom of the screen.
218fn spawn_buttons(commands: &mut Commands) {
219    commands.spawn((
220        widgets::main_ui_node(),
221        children![
222            widgets::option_buttons(
223                "Light Type",
224                &[
225                    (AppSetting::LightType(LightType::Directional), "Directional"),
226                    (AppSetting::LightType(LightType::Point), "Point"),
227                    (AppSetting::LightType(LightType::Spot), "Spot"),
228                ],
229            ),
230            widgets::option_buttons(
231                "Shadow Filter",
232                &[
233                    (AppSetting::ShadowFilter(ShadowFilter::Temporal), "Temporal"),
234                    (
235                        AppSetting::ShadowFilter(ShadowFilter::NonTemporal),
236                        "Non-Temporal",
237                    ),
238                ],
239            ),
240            widgets::option_buttons(
241                "Soft Shadows",
242                &[
243                    (AppSetting::SoftShadows(true), "On"),
244                    (AppSetting::SoftShadows(false), "Off"),
245                ],
246            ),
247        ],
248    ));
249}
250
251/// Updates the style of the radio buttons that enable and disable soft shadows
252/// to reflect whether PCSS is enabled.
253fn update_radio_buttons(
254    mut widgets: Query<
255        (
256            Entity,
257            Option<&mut BackgroundColor>,
258            Has<Text>,
259            &WidgetClickSender<AppSetting>,
260        ),
261        Or<(With<RadioButton>, With<RadioButtonText>)>,
262    >,
263    app_status: Res<AppStatus>,
264    mut writer: TextUiWriter,
265) {
266    for (entity, image, has_text, sender) in widgets.iter_mut() {
267        let selected = match **sender {
268            AppSetting::LightType(light_type) => light_type == app_status.light_type,
269            AppSetting::ShadowFilter(shadow_filter) => shadow_filter == app_status.shadow_filter,
270            AppSetting::SoftShadows(soft_shadows) => soft_shadows == app_status.soft_shadows,
271        };
272
273        if let Some(mut bg_color) = image {
274            widgets::update_ui_radio_button(&mut bg_color, selected);
275        }
276        if has_text {
277            widgets::update_ui_radio_button_text(entity, &mut writer, selected);
278        }
279    }
280}
281
282/// Handles requests from the user to change the type of light.
283fn handle_light_type_change(
284    mut commands: Commands,
285    mut lights: Query<Entity, Or<(With<DirectionalLight>, With<PointLight>, With<SpotLight>)>>,
286    mut events: MessageReader<WidgetClickEvent<AppSetting>>,
287    mut app_status: ResMut<AppStatus>,
288) {
289    for event in events.read() {
290        let AppSetting::LightType(light_type) = **event else {
291            continue;
292        };
293        app_status.light_type = light_type;
294
295        for light in lights.iter_mut() {
296            let mut light_commands = commands.entity(light);
297            light_commands
298                .remove::<DirectionalLight>()
299                .remove::<PointLight>()
300                .remove::<SpotLight>();
301            match light_type {
302                LightType::Point => {
303                    light_commands.insert(create_point_light(&app_status));
304                }
305                LightType::Spot => {
306                    light_commands.insert(create_spot_light(&app_status));
307                }
308                LightType::Directional => {
309                    light_commands.insert(create_directional_light(&app_status));
310                }
311            }
312        }
313    }
314}
315
316/// Handles requests from the user to change the shadow filter method.
317///
318/// This system is also responsible for enabling and disabling TAA as
319/// appropriate.
320fn handle_shadow_filter_change(
321    mut commands: Commands,
322    mut cameras: Query<(Entity, &mut ShadowFilteringMethod)>,
323    mut events: MessageReader<WidgetClickEvent<AppSetting>>,
324    mut app_status: ResMut<AppStatus>,
325) {
326    for event in events.read() {
327        let AppSetting::ShadowFilter(shadow_filter) = **event else {
328            continue;
329        };
330        app_status.shadow_filter = shadow_filter;
331
332        for (camera, mut shadow_filtering_method) in cameras.iter_mut() {
333            match shadow_filter {
334                ShadowFilter::NonTemporal => {
335                    *shadow_filtering_method = ShadowFilteringMethod::Gaussian;
336                    commands.entity(camera).remove::<TemporalAntiAliasing>();
337                }
338                ShadowFilter::Temporal => {
339                    *shadow_filtering_method = ShadowFilteringMethod::Temporal;
340                    commands
341                        .entity(camera)
342                        .insert(TemporalAntiAliasing::default());
343                }
344            }
345        }
346    }
347}
348
349/// Handles requests from the user to toggle soft shadows on and off.
350fn handle_pcss_toggle(
351    mut lights: Query<AnyOf<(&mut DirectionalLight, &mut PointLight, &mut SpotLight)>>,
352    mut events: MessageReader<WidgetClickEvent<AppSetting>>,
353    mut app_status: ResMut<AppStatus>,
354) {
355    for event in events.read() {
356        let AppSetting::SoftShadows(value) = **event else {
357            continue;
358        };
359        app_status.soft_shadows = value;
360
361        // Recreating the lights is the simplest way to toggle soft shadows.
362        for (directional_light, point_light, spot_light) in lights.iter_mut() {
363            if let Some(mut directional_light) = directional_light {
364                *directional_light = create_directional_light(&app_status);
365            }
366            if let Some(mut point_light) = point_light {
367                *point_light = create_point_light(&app_status);
368            }
369            if let Some(mut spot_light) = spot_light {
370                *spot_light = create_spot_light(&app_status);
371            }
372        }
373    }
374}
375
376/// Creates the [`DirectionalLight`] component with the appropriate settings.
377fn create_directional_light(app_status: &AppStatus) -> DirectionalLight {
378    DirectionalLight {
379        shadow_maps_enabled: true,
380        soft_shadow_size: if app_status.soft_shadows {
381            Some(LIGHT_RADIUS)
382        } else {
383            None
384        },
385        shadow_depth_bias: DIRECTIONAL_SHADOW_DEPTH_BIAS,
386        ..default()
387    }
388}
389
390/// Creates the [`PointLight`] component with the appropriate settings.
391fn create_point_light(app_status: &AppStatus) -> PointLight {
392    PointLight {
393        intensity: POINT_LIGHT_INTENSITY,
394        range: POINT_LIGHT_RANGE,
395        shadow_maps_enabled: true,
396        radius: LIGHT_RADIUS,
397        soft_shadows_enabled: app_status.soft_shadows,
398        shadow_depth_bias: POINT_SHADOW_DEPTH_BIAS,
399        shadow_map_near_z: SHADOW_MAP_NEAR_Z,
400        ..default()
401    }
402}
403
404/// Creates the [`SpotLight`] component with the appropriate settings.
405fn create_spot_light(app_status: &AppStatus) -> SpotLight {
406    SpotLight {
407        intensity: POINT_LIGHT_INTENSITY,
408        range: POINT_LIGHT_RANGE,
409        radius: LIGHT_RADIUS,
410        shadow_maps_enabled: true,
411        soft_shadows_enabled: app_status.soft_shadows,
412        shadow_depth_bias: DIRECTIONAL_SHADOW_DEPTH_BIAS,
413        shadow_map_near_z: SHADOW_MAP_NEAR_Z,
414        ..default()
415    }
416}