pcss/
pcss.rs

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