Skip to main content

contact_shadows/
contact_shadows.rs

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