docs/bevy/examples/3d/pcss.rs 12.6 K · 371 lines · raw · history

1 //! Demonstrates percentage-closer soft shadows (PCSS).
2
3 use std::f32::consts::PI;
4
5 #[cfg(feature = "free_camera")]
6 use bevy::camera_controller::free_camera::{FreeCamera, FreeCameraPlugin};
7 use 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"]
23 mod radio;
24
25 #[path = "../helpers/theme.rs"]
26 mod theme;
27
28 /// The size of the light, which affects the size of the penumbras.
29 const LIGHT_RADIUS: f32 = 10.0;
30
31 /// The intensity of the point and spot lights.
32 const POINT_LIGHT_INTENSITY: f32 = 1_000_000_000.0;
33
34 /// The range in meters of the point and spot lights.
35 const 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.
39 const 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.
47 const 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.
52 const 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)]
57 struct 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)]
68 enum 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)]
84 enum 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)]
96 struct SoftShadows(bool);
97
98 impl Default for SoftShadows {
99 fn default() -> Self {
100 Self(true)
101 }
102 }
103
104 /// The example application entry point.
105 fn 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.
133 fn 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.
141 fn 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.
170 fn 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.
194 fn 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.
201 fn 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.
240 fn 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.
276 fn 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.
306 fn 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.
332 fn 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.
346 fn 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.
360 fn 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 }