1use 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
25const LIGHT_RADIUS: f32 = 10.0;
27
28const POINT_LIGHT_INTENSITY: f32 = 1_000_000_000.0;
30
31const POINT_LIGHT_RANGE: f32 = 110.0;
33
34const DIRECTIONAL_SHADOW_DEPTH_BIAS: f32 = 0.20;
37
38const POINT_SHADOW_DEPTH_BIAS: f32 = 0.35;
45
46const SHADOW_MAP_NEAR_Z: f32 = 50.0;
50
51#[derive(Resource)]
54struct AppStatus {
55 light_type: LightType,
57 shadow_filter: ShadowFilter,
59 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#[derive(Clone, Copy, Default, PartialEq)]
75enum LightType {
76 #[default]
78 Directional,
79 Point,
81 Spot,
83}
84
85#[derive(Clone, Copy, Default, PartialEq)]
91enum ShadowFilter {
92 #[default]
95 NonTemporal,
96 Temporal,
99}
100
101#[derive(Clone, Copy, PartialEq)]
103enum AppSetting {
104 LightType(LightType),
106 ShadowFilter(ShadowFilter),
108 SoftShadows(bool),
110}
111
112fn 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
149fn 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
157fn 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 .insert(TemporalJitter::default())
172 .insert(Msaa::Off)
174 .insert(DepthPrepass)
176 .insert(MotionVectorPrepass)
178 .insert(Skybox {
180 image: Some(asset_server.load("environment_maps/sky_skybox.ktx2")),
181 brightness: 500.0,
182 rotation: Quat::IDENTITY,
183 });
184}
185
186fn spawn_light(commands: &mut Commands, app_status: &AppStatus) {
188 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 .insert(CubemapVisibleEntities::default())
204 .insert(CubemapFrusta::default())
205 .insert(VisibleMeshEntities::default())
207 .insert(Frustum::default());
208}
209
210fn 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
217fn 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
251fn 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
282fn 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
316fn 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
349fn 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 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
376fn 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
390fn 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
404fn 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}