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