Skip to main content

contact_shadows/
contact_shadows.rs

1//! Demonstrates contact shadows, also known as screen-space shadows.
2
3use 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/// Each example setting that can be toggled in the UI.
55#[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(), // Contact shadows and AO benefit from TAA
115        // Everything past this point is extra to look pretty.
116        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, // This is really just for fun when spinning the model
134            ..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}