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 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
28const LIGHT_RADIUS: f32 = 10.0;
30
31const POINT_LIGHT_INTENSITY: f32 = 1_000_000_000.0;
33
34const POINT_LIGHT_RANGE: f32 = 110.0;
36
37const DIRECTIONAL_SHADOW_DEPTH_BIAS: f32 = 0.20;
40
41const POINT_SHADOW_DEPTH_BIAS: f32 = 0.35;
48
49const SHADOW_MAP_NEAR_Z: f32 = 50.0;
53
54#[derive(Default, Resource)]
57struct AppStatus {
58 light_type: LightType,
60 shadow_filter: ShadowFilter,
62 soft_shadows: SoftShadows,
64}
65
66#[derive(Clone, Copy, Default, PartialEq)]
68enum LightType {
69 #[default]
71 Directional,
72 Point,
74 Spot,
76}
77
78#[derive(Clone, Copy, Default, PartialEq)]
84enum ShadowFilter {
85 #[default]
88 NonTemporal,
89 Temporal,
92}
93
94#[derive(Clone, Copy, Deref, PartialEq)]
96struct SoftShadows(bool);
97
98impl Default for SoftShadows {
99 fn default() -> Self {
100 Self(true)
101 }
102}
103
104fn 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
132fn 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
140fn 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 .insert(TemporalJitter::default())
155 .insert(Msaa::Off)
157 .insert(DepthPrepass)
159 .insert(MotionVectorPrepass)
161 .insert(Skybox {
163 image: Some(asset_server.load("environment_maps/sky_skybox.ktx2")),
164 brightness: 500.0,
165 rotation: Quat::IDENTITY,
166 });
167}
168
169fn spawn_light(commands: &mut Commands, app_status: &AppStatus) {
171 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 .insert(CubemapVisibleEntities::default())
187 .insert(CubemapFrusta::default())
188 .insert(VisibleMeshEntities::default())
190 .insert(Frustum::default());
191}
192
193fn 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
200fn 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
239fn 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
272fn 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
305fn 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 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
331fn 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
345fn 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
359fn 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}