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