1use crate::radio::{feathers_option_buttons, main_ui_node_scene, RadioButtonOptionValue};
4use crate::theme::basic_example_theme;
5use bevy::anti_alias::taa::TemporalAntiAliasing;
6use bevy::core_pipeline::tonemapping::Tonemapping;
7use bevy::light::Skybox;
8use bevy::pbr::ScreenSpaceAmbientOcclusion;
9use bevy::post_process::motion_blur::MotionBlur;
10use bevy::window::{CursorIcon, PrimaryWindow, SystemCursorIcon};
11use bevy::{
12 camera::Hdr,
13 feathers::{theme::UiTheme, FeathersPlugins},
14 light::NotShadowReceiver,
15 pbr::ContactShadows,
16 post_process::bloom::Bloom,
17 prelude::*,
18 ui_widgets::{radio_self_update, ValueChange},
19};
20
21#[path = "../helpers/radio.rs"]
22mod radio;
23
24#[path = "../helpers/theme.rs"]
25mod theme;
26
27#[derive(Clone, Copy, PartialEq, Default, Debug)]
28enum ContactShadowState {
29 #[default]
30 Enabled,
31 Disabled,
32}
33
34#[derive(Clone, Copy, PartialEq, Default, Debug)]
35enum ShadowMaps {
36 #[default]
37 Enabled,
38 Disabled,
39}
40
41#[derive(Clone, Copy, PartialEq, Default, Debug)]
42enum LightRotation {
43 Stationary,
44 #[default]
45 Rotating,
46}
47
48#[derive(Clone, Copy, PartialEq, Default, Debug)]
49enum LightType {
50 Directional,
51 #[default]
52 Point,
53 Spot,
54}
55
56#[derive(Clone, Copy, PartialEq, Default, Debug)]
57enum ReceiveShadows {
58 #[default]
59 Enabled,
60 Disabled,
61}
62
63#[derive(Clone, Copy, PartialEq)]
65enum ExampleSetting {
66 ContactShadows(ContactShadowState),
67 ShadowMaps(ShadowMaps),
68 LightRotation(LightRotation),
69 LightType(LightType),
70 ReceiveShadows(ReceiveShadows),
71}
72
73impl Default for ExampleSetting {
74 fn default() -> Self {
75 Self::ContactShadows(ContactShadowState::default())
76 }
77}
78
79const LIGHT_ROTATION_SPEED: f32 = 0.002;
80
81#[derive(Resource, Default)]
82struct AppStatus {
83 contact_shadows: ContactShadowState,
84 shadow_maps: ShadowMaps,
85 light_rotation: LightRotation,
86 light_type: LightType,
87 receive_shadows: ReceiveShadows,
88}
89
90#[derive(Component)]
91struct LightContainer;
92
93#[derive(Component)]
94struct GroundPlane;
95
96fn main() {
97 App::new()
98 .add_plugins((
99 DefaultPlugins.set(WindowPlugin {
100 primary_window: Some(Window {
101 title: "Bevy Contact Shadows Example".into(),
102 ..default()
103 }),
104 ..default()
105 }),
106 MeshPickingPlugin,
107 FeathersPlugins,
108 ))
109 .insert_resource(UiTheme(basic_example_theme(Color::WHITE)))
110 .init_resource::<AppStatus>()
111 .insert_resource(GlobalAmbientLight::NONE)
112 .add_systems(Startup, setup)
113 .add_systems(Update, rotate_light)
114 .add_observer(handle_setting_change)
115 .add_observer(radio_self_update)
116 .run();
117}
118
119fn setup(mut commands: Commands, asset_server: Res<AssetServer>, app_status: Res<AppStatus>) {
120 commands.spawn((
121 Camera3d::default(),
122 Transform::from_xyz(-0.8, 0.6, -0.8).looking_at(Vec3::new(0.0, 0.35, 0.0), Vec3::Y),
123 ContactShadows::default(),
124 TemporalAntiAliasing::default(), Bloom::default(),
127 Hdr,
128 Skybox {
129 brightness: 1000.0,
130 image: Some(asset_server.load("environment_maps/pisa_diffuse_rgb9e5_zstd.ktx2")),
131 ..default()
132 },
133 EnvironmentMapLight {
134 diffuse_map: asset_server.load("environment_maps/pisa_diffuse_rgb9e5_zstd.ktx2"),
135 specular_map: asset_server.load("environment_maps/pisa_specular_rgb9e5_zstd.ktx2"),
136 intensity: 1000.0,
137 ..default()
138 },
139 ScreenSpaceAmbientOcclusion::default(),
140 Msaa::Off,
141 Tonemapping::AcesFitted,
142 MotionBlur {
143 shutter_angle: 2.0, ..default()
145 },
146 ));
147
148 let directional_light = commands
149 .spawn((
150 DirectionalLight {
151 shadow_maps_enabled: true,
152 contact_shadows_enabled: true,
153 ..default()
154 },
155 Visibility::Hidden,
156 ))
157 .id();
158
159 let point_light = commands
160 .spawn((
161 PointLight {
162 intensity: light_consts::lumens::VERY_LARGE_CINEMA_LIGHT * 0.4,
163 shadow_maps_enabled: true,
164 contact_shadows_enabled: true,
165 ..default()
166 },
167 Visibility::Visible,
168 ))
169 .id();
170
171 let spot_light = commands
172 .spawn((
173 SpotLight {
174 intensity: light_consts::lumens::VERY_LARGE_CINEMA_LIGHT * 0.4,
175 shadow_maps_enabled: true,
176 contact_shadows_enabled: true,
177 ..default()
178 },
179 Visibility::Hidden,
180 ))
181 .id();
182
183 commands
184 .spawn((
185 Transform::from_xyz(-0.8, 1.5, 1.2).looking_at(Vec3::ZERO, Vec3::Y),
186 Visibility::default(),
187 LightContainer,
188 ))
189 .add_child(directional_light)
190 .add_child(point_light)
191 .add_child(spot_light);
192
193 commands
194 .spawn((
195 WorldAssetRoot(asset_server.load(
196 GltfAssetLabel::Scene(0).from_asset("models/FlightHelmet/FlightHelmet.gltf"),
197 )),
198 Transform::from_rotation(Quat::from_rotation_y(std::f32::consts::PI)),
199 ))
200 .observe(
201 |event: On<PointerDrag>,
202 mut query: Query<&mut Transform, With<WorldAssetRoot>>,
203 mut commands: Commands,
204 mut window: Query<Entity, With<PrimaryWindow>>| {
205 for mut transform in query.iter_mut() {
206 transform.rotate_y(event.delta.x * 0.01);
207 }
208 commands
209 .entity(window.single_mut().unwrap())
210 .insert(CursorIcon::System(SystemCursorIcon::Grabbing));
211 },
212 )
213 .observe(
214 |_: On<PointerOver>,
215 mut commands: Commands,
216 mut window: Query<Entity, With<PrimaryWindow>>| {
217 commands
218 .entity(window.single_mut().unwrap())
219 .insert(CursorIcon::System(SystemCursorIcon::Grab));
220 },
221 )
222 .observe(
223 |_: On<PointerOut>,
224 mut commands: Commands,
225 mut window: Query<Entity, With<PrimaryWindow>>| {
226 commands
227 .entity(window.single_mut().unwrap())
228 .insert(CursorIcon::System(SystemCursorIcon::Default));
229 },
230 )
231 .observe(
232 |_: On<PointerDragEnd>,
233 mut commands: Commands,
234 mut window: Query<Entity, With<PrimaryWindow>>| {
235 commands
236 .entity(window.single_mut().unwrap())
237 .insert(CursorIcon::System(SystemCursorIcon::Default));
238 },
239 );
240
241 commands.spawn((
242 Mesh3d(asset_server.add(Circle::default().mesh().into())),
243 MeshMaterial3d(asset_server.add(StandardMaterial {
244 base_color: Color::srgb(0.06, 0.06, 0.06),
245 ..default()
246 })),
247 Transform::from_rotation(Quat::from_axis_angle(Vec3::X, -std::f32::consts::FRAC_PI_2)),
248 GroundPlane,
249 ));
250
251 spawn_buttons(&mut commands, &app_status);
252
253 commands.spawn((
254 Node {
255 position_type: PositionType::Absolute,
256 top: px(12.0),
257 left: px(0.0),
258 right: px(0.0),
259 justify_content: JustifyContent::Center,
260 ..default()
261 },
262 children![(
263 Text::new("Drag model to spin"),
264 TextFont {
265 font_size: FontSize::Px(18.0),
266 ..default()
267 },
268 )],
269 ));
270}
271
272fn rotate_light(
273 mut lights: Query<&mut Transform, With<LightContainer>>,
274 app_status: Res<AppStatus>,
275) {
276 if app_status.light_rotation != LightRotation::Rotating {
277 return;
278 }
279
280 for mut transform in lights.iter_mut() {
281 transform.rotate_around(Vec3::ZERO, Quat::from_rotation_y(LIGHT_ROTATION_SPEED));
282 }
283}
284
285fn spawn_buttons(commands: &mut Commands, app_status: &AppStatus) {
286 commands.spawn_scene(bsn! {
287 @main_ui_node_scene()
288 Children [
289 @feathers_option_buttons(
290 "Contact Shadows",
291 &[
292 (
293 ExampleSetting::ContactShadows(ContactShadowState::Enabled),
294 "On"
295 ),
296 (
297 ExampleSetting::ContactShadows(ContactShadowState::Disabled),
298 "Off"
299 ),
300 ],
301 {
302 if app_status.contact_shadows == ContactShadowState::Enabled {
303 0
304 }
305 else {
306 1
307 }
308 },
309 )
310 --
311 @feathers_option_buttons(
312 "Shadow Maps",
313 &[
314 (ExampleSetting::ShadowMaps(ShadowMaps::Enabled), "On"),
315 (ExampleSetting::ShadowMaps(ShadowMaps::Disabled), "Off"),
316 ],
317 {
318 if app_status.shadow_maps == ShadowMaps::Enabled {
319 0
320 }
321 else {
322 1
323 }
324 },
325 )
326 --
327 @feathers_option_buttons(
328 "Light Rotation",
329 &[
330 (ExampleSetting::LightRotation(LightRotation::Rotating), "On"),
331 (
332 ExampleSetting::LightRotation(LightRotation::Stationary),
333 "Off"
334 ),
335 ],
336 {
337 if app_status.light_rotation == LightRotation::Rotating {
338 0
339 }
340 else {
341 1
342 }
343 },
344 )
345 --
346 @feathers_option_buttons(
347 "Light Type",
348 &[
349 (
350 ExampleSetting::LightType(LightType::Directional),
351 "Directional"
352 ),
353 (ExampleSetting::LightType(LightType::Point), "Point"),
354 (ExampleSetting::LightType(LightType::Spot), "Spot"),
355 ],
356 {
357 match app_status.light_type {
358 LightType::Directional => 0,
359 LightType::Point => 1,
360 LightType::Spot => 2,
361 }
362 },
363 )
364 --
365 @feathers_option_buttons(
366 "Receive Shadows",
367 &[
368 (
369 ExampleSetting::ReceiveShadows(ReceiveShadows::Enabled),
370 "On"
371 ),
372 (
373 ExampleSetting::ReceiveShadows(ReceiveShadows::Disabled),
374 "Off"
375 ),
376 ],
377 {
378 if app_status.receive_shadows == ReceiveShadows::Enabled {
379 0
380 }
381 else {
382 1
383 }
384 },
385 )
386 ]
387 });
388}
389
390fn handle_setting_change(
391 event: On<ValueChange<Entity>>,
392 new_value_q: Query<&RadioButtonOptionValue<ExampleSetting>>,
393 mut lights: Query<
394 (
395 &mut Visibility,
396 Option<&mut DirectionalLight>,
397 Option<&mut PointLight>,
398 Option<&mut SpotLight>,
399 ),
400 Or<(With<DirectionalLight>, With<PointLight>, With<SpotLight>)>,
401 >,
402 mut ground_plane: Query<Entity, With<GroundPlane>>,
403 mut app_status: ResMut<AppStatus>,
404 mut commands: Commands,
405) {
406 let Ok(RadioButtonOptionValue(setting)) = new_value_q.get(event.value) else {
407 return;
408 };
409
410 match *setting {
411 ExampleSetting::ContactShadows(value) => {
412 app_status.contact_shadows = value;
413 for (_, maybe_directional_light, maybe_point_light, maybe_spot_light) in
414 lights.iter_mut()
415 {
416 if let Some(mut directional_light) = maybe_directional_light {
417 directional_light.contact_shadows_enabled =
418 value == ContactShadowState::Enabled;
419 }
420 if let Some(mut point_light) = maybe_point_light {
421 point_light.contact_shadows_enabled = value == ContactShadowState::Enabled;
422 }
423 if let Some(mut spot_light) = maybe_spot_light {
424 spot_light.contact_shadows_enabled = value == ContactShadowState::Enabled;
425 }
426 }
427 }
428 ExampleSetting::ShadowMaps(value) => {
429 app_status.shadow_maps = value;
430 for (_, maybe_directional_light, maybe_point_light, maybe_spot_light) in
431 lights.iter_mut()
432 {
433 if let Some(mut directional_light) = maybe_directional_light {
434 directional_light.shadow_maps_enabled = value == ShadowMaps::Enabled;
435 }
436 if let Some(mut point_light) = maybe_point_light {
437 point_light.shadow_maps_enabled = value == ShadowMaps::Enabled;
438 }
439 if let Some(mut spot_light) = maybe_spot_light {
440 spot_light.shadow_maps_enabled = value == ShadowMaps::Enabled;
441 }
442 }
443 }
444 ExampleSetting::LightRotation(value) => {
445 app_status.light_rotation = value;
446 }
447 ExampleSetting::LightType(value) => {
448 app_status.light_type = value;
449 for (mut visibility, maybe_directional_light, maybe_point_light, maybe_spot_light) in
450 lights.iter_mut()
451 {
452 let is_visible = match value {
453 LightType::Directional => maybe_directional_light.is_some(),
454 LightType::Point => maybe_point_light.is_some(),
455 LightType::Spot => maybe_spot_light.is_some(),
456 };
457 *visibility = if is_visible {
458 Visibility::Visible
459 } else {
460 Visibility::Hidden
461 };
462 }
463 }
464 ExampleSetting::ReceiveShadows(value) => {
465 app_status.receive_shadows = value;
466 for entity in ground_plane.iter_mut() {
467 match value {
468 ReceiveShadows::Enabled => {
469 commands.entity(entity).remove::<NotShadowReceiver>();
470 }
471 ReceiveShadows::Disabled => {
472 commands.entity(entity).insert(NotShadowReceiver);
473 }
474 }
475 }
476 }
477 }
478}