1use 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
26const LIGHT_RADIUS: f32 = 10.0;
28
29const POINT_LIGHT_INTENSITY: f32 = 1_000_000_000.0;
31
32const POINT_LIGHT_RANGE: f32 = 110.0;
34
35const DIRECTIONAL_SHADOW_DEPTH_BIAS: f32 = 0.20;
38
39const POINT_SHADOW_DEPTH_BIAS: f32 = 0.35;
46
47const SHADOW_MAP_NEAR_Z: f32 = 50.0;
51
52#[derive(Resource)]
55struct AppStatus {
56 light_type: LightType,
58 shadow_filter: ShadowFilter,
60 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#[derive(Clone, Copy, Default, PartialEq)]
76enum LightType {
77 #[default]
79 Directional,
80 Point,
82 Spot,
84}
85
86#[derive(Clone, Copy, Default, PartialEq)]
92enum ShadowFilter {
93 #[default]
96 NonTemporal,
97 Temporal,
100}
101
102#[derive(Clone, Copy, PartialEq)]
104enum AppSetting {
105 LightType(LightType),
107 ShadowFilter(ShadowFilter),
109 SoftShadows(bool),
111}
112
113fn 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
143fn 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
151fn 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 .insert(TemporalJitter::default())
164 .insert(Msaa::Off)
166 .insert(DepthPrepass)
168 .insert(MotionVectorPrepass)
170 .insert(Skybox {
172 image: asset_server.load("environment_maps/sky_skybox.ktx2"),
173 brightness: 500.0,
174 rotation: Quat::IDENTITY,
175 });
176}
177
178fn spawn_light(commands: &mut Commands, app_status: &AppStatus) {
180 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 .insert(CubemapVisibleEntities::default())
196 .insert(CubemapFrusta::default())
197 .insert(VisibleMeshEntities::default())
199 .insert(Frustum::default());
200}
201
202fn 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
209fn 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
243fn 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
274fn 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
308fn 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
341fn 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 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
368fn 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
382fn 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
396fn 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}