3D Rendering / Contact Shadows

Back to examples View in GitHub

Demonstrates contact shadows, also known as screen-space shadows.

use crate::radio::{feathers_option_buttons, main_ui_node_scene, RadioButtonOptionValue};
use crate::theme::basic_example_theme;
use bevy::anti_alias::taa::TemporalAntiAliasing;
use bevy::core_pipeline::tonemapping::Tonemapping;
use bevy::light::Skybox;
use bevy::pbr::ScreenSpaceAmbientOcclusion;
use bevy::post_process::motion_blur::MotionBlur;
use bevy::window::{CursorIcon, PrimaryWindow, SystemCursorIcon};
use bevy::{
    camera::Hdr,
    feathers::{theme::UiTheme, FeathersPlugins},
    light::NotShadowReceiver,
    pbr::ContactShadows,
    post_process::bloom::Bloom,
    prelude::*,
    ui_widgets::{radio_self_update, ValueChange},
};

#[path = "../helpers/radio.rs"]
mod radio;

#[path = "../helpers/theme.rs"]
mod theme;

#[derive(Clone, Copy, PartialEq, Default, Debug)]
enum ContactShadowState {
    #[default]
    Enabled,
    Disabled,
}

#[derive(Clone, Copy, PartialEq, Default, Debug)]
enum ShadowMaps {
    #[default]
    Enabled,
    Disabled,
}

#[derive(Clone, Copy, PartialEq, Default, Debug)]
enum LightRotation {
    Stationary,
    #[default]
    Rotating,
}

#[derive(Clone, Copy, PartialEq, Default, Debug)]
enum LightType {
    Directional,
    #[default]
    Point,
    Spot,
}

#[derive(Clone, Copy, PartialEq, Default, Debug)]
enum ReceiveShadows {
    #[default]
    Enabled,
    Disabled,
}

/// Each example setting that can be toggled in the UI.
#[derive(Clone, Copy, PartialEq)]
enum ExampleSetting {
    ContactShadows(ContactShadowState),
    ShadowMaps(ShadowMaps),
    LightRotation(LightRotation),
    LightType(LightType),
    ReceiveShadows(ReceiveShadows),
}

impl Default for ExampleSetting {
    fn default() -> Self {
        Self::ContactShadows(ContactShadowState::default())
    }
}

const LIGHT_ROTATION_SPEED: f32 = 0.002;

#[derive(Resource, Default)]
struct AppStatus {
    contact_shadows: ContactShadowState,
    shadow_maps: ShadowMaps,
    light_rotation: LightRotation,
    light_type: LightType,
    receive_shadows: ReceiveShadows,
}

#[derive(Component)]
struct LightContainer;

#[derive(Component)]
struct GroundPlane;

fn main() {
    App::new()
        .add_plugins((
            DefaultPlugins.set(WindowPlugin {
                primary_window: Some(Window {
                    title: "Bevy Contact Shadows Example".into(),
                    ..default()
                }),
                ..default()
            }),
            MeshPickingPlugin,
            FeathersPlugins,
        ))
        .insert_resource(UiTheme(basic_example_theme(Color::WHITE)))
        .init_resource::<AppStatus>()
        .insert_resource(GlobalAmbientLight::NONE)
        .add_systems(Startup, setup)
        .add_systems(Update, rotate_light)
        .add_observer(handle_setting_change)
        .add_observer(radio_self_update)
        .run();
}

fn setup(mut commands: Commands, asset_server: Res<AssetServer>, app_status: Res<AppStatus>) {
    commands.spawn((
        Camera3d::default(),
        Transform::from_xyz(-0.8, 0.6, -0.8).looking_at(Vec3::new(0.0, 0.35, 0.0), Vec3::Y),
        ContactShadows::default(),
        TemporalAntiAliasing::default(), // Contact shadows and AO benefit from TAA
        // Everything past this point is extra to look pretty.
        Bloom::default(),
        Hdr,
        Skybox {
            brightness: 1000.0,
            image: Some(asset_server.load("environment_maps/pisa_diffuse_rgb9e5_zstd.ktx2")),
            ..default()
        },
        EnvironmentMapLight {
            diffuse_map: asset_server.load("environment_maps/pisa_diffuse_rgb9e5_zstd.ktx2"),
            specular_map: asset_server.load("environment_maps/pisa_specular_rgb9e5_zstd.ktx2"),
            intensity: 1000.0,
            ..default()
        },
        ScreenSpaceAmbientOcclusion::default(),
        Msaa::Off,
        Tonemapping::AcesFitted,
        MotionBlur {
            shutter_angle: 2.0, // This is really just for fun when spinning the model
            ..default()
        },
    ));

    let directional_light = commands
        .spawn((
            DirectionalLight {
                shadow_maps_enabled: true,
                contact_shadows_enabled: true,
                ..default()
            },
            Visibility::Hidden,
        ))
        .id();

    let point_light = commands
        .spawn((
            PointLight {
                intensity: light_consts::lumens::VERY_LARGE_CINEMA_LIGHT * 0.4,
                shadow_maps_enabled: true,
                contact_shadows_enabled: true,
                ..default()
            },
            Visibility::Visible,
        ))
        .id();

    let spot_light = commands
        .spawn((
            SpotLight {
                intensity: light_consts::lumens::VERY_LARGE_CINEMA_LIGHT * 0.4,
                shadow_maps_enabled: true,
                contact_shadows_enabled: true,
                ..default()
            },
            Visibility::Hidden,
        ))
        .id();

    commands
        .spawn((
            Transform::from_xyz(-0.8, 1.5, 1.2).looking_at(Vec3::ZERO, Vec3::Y),
            Visibility::default(),
            LightContainer,
        ))
        .add_child(directional_light)
        .add_child(point_light)
        .add_child(spot_light);

    commands
        .spawn((
            WorldAssetRoot(asset_server.load(
                GltfAssetLabel::Scene(0).from_asset("models/FlightHelmet/FlightHelmet.gltf"),
            )),
            Transform::from_rotation(Quat::from_rotation_y(std::f32::consts::PI)),
        ))
        .observe(
            |event: On<PointerDrag>,
             mut query: Query<&mut Transform, With<WorldAssetRoot>>,
             mut commands: Commands,
             mut window: Query<Entity, With<PrimaryWindow>>| {
                for mut transform in query.iter_mut() {
                    transform.rotate_y(event.delta.x * 0.01);
                }
                commands
                    .entity(window.single_mut().unwrap())
                    .insert(CursorIcon::System(SystemCursorIcon::Grabbing));
            },
        )
        .observe(
            |_: On<PointerOver>,
             mut commands: Commands,
             mut window: Query<Entity, With<PrimaryWindow>>| {
                commands
                    .entity(window.single_mut().unwrap())
                    .insert(CursorIcon::System(SystemCursorIcon::Grab));
            },
        )
        .observe(
            |_: On<PointerOut>,
             mut commands: Commands,
             mut window: Query<Entity, With<PrimaryWindow>>| {
                commands
                    .entity(window.single_mut().unwrap())
                    .insert(CursorIcon::System(SystemCursorIcon::Default));
            },
        )
        .observe(
            |_: On<PointerDragEnd>,
             mut commands: Commands,
             mut window: Query<Entity, With<PrimaryWindow>>| {
                commands
                    .entity(window.single_mut().unwrap())
                    .insert(CursorIcon::System(SystemCursorIcon::Default));
            },
        );

    commands.spawn((
        Mesh3d(asset_server.add(Circle::default().mesh().into())),
        MeshMaterial3d(asset_server.add(StandardMaterial {
            base_color: Color::srgb(0.06, 0.06, 0.06),
            ..default()
        })),
        Transform::from_rotation(Quat::from_axis_angle(Vec3::X, -std::f32::consts::FRAC_PI_2)),
        GroundPlane,
    ));

    spawn_buttons(&mut commands, &app_status);

    commands.spawn((
        Node {
            position_type: PositionType::Absolute,
            top: px(12.0),
            left: px(0.0),
            right: px(0.0),
            justify_content: JustifyContent::Center,
            ..default()
        },
        children![(
            Text::new("Drag model to spin"),
            TextFont {
                font_size: FontSize::Px(18.0),
                ..default()
            },
        )],
    ));
}

fn rotate_light(
    mut lights: Query<&mut Transform, With<LightContainer>>,
    app_status: Res<AppStatus>,
) {
    if app_status.light_rotation != LightRotation::Rotating {
        return;
    }

    for mut transform in lights.iter_mut() {
        transform.rotate_around(Vec3::ZERO, Quat::from_rotation_y(LIGHT_ROTATION_SPEED));
    }
}

fn spawn_buttons(commands: &mut Commands, app_status: &AppStatus) {
    commands.spawn_scene(bsn! {
        @main_ui_node_scene()
        Children [
            @feathers_option_buttons(
                "Contact Shadows",
                &[
                    (
                        ExampleSetting::ContactShadows(ContactShadowState::Enabled),
                        "On"
                    ),
                    (
                        ExampleSetting::ContactShadows(ContactShadowState::Disabled),
                        "Off"
                    ),
                ],
                {
                    if app_status.contact_shadows == ContactShadowState::Enabled {
                        0
                    }
                    else {
                        1
                    }
                },
            )
            --
            @feathers_option_buttons(
                "Shadow Maps",
                &[
                    (ExampleSetting::ShadowMaps(ShadowMaps::Enabled), "On"),
                    (ExampleSetting::ShadowMaps(ShadowMaps::Disabled), "Off"),
                ],
                {
                    if app_status.shadow_maps == ShadowMaps::Enabled {
                        0
                    }
                    else {
                        1
                    }
                },
            )
            --
            @feathers_option_buttons(
                "Light Rotation",
                &[
                    (ExampleSetting::LightRotation(LightRotation::Rotating), "On"),
                    (
                        ExampleSetting::LightRotation(LightRotation::Stationary),
                        "Off"
                    ),
                ],
                {
                    if app_status.light_rotation == LightRotation::Rotating {
                        0
                    }
                    else {
                        1
                    }
                },
            )
            --
            @feathers_option_buttons(
                "Light Type",
                &[
                    (
                        ExampleSetting::LightType(LightType::Directional),
                        "Directional"
                    ),
                    (ExampleSetting::LightType(LightType::Point), "Point"),
                    (ExampleSetting::LightType(LightType::Spot), "Spot"),
                ],
                {
                    match app_status.light_type {
                        LightType::Directional => 0,
                        LightType::Point => 1,
                        LightType::Spot => 2,
                    }
                },
            )
            --
            @feathers_option_buttons(
                "Receive Shadows",
                &[
                    (
                        ExampleSetting::ReceiveShadows(ReceiveShadows::Enabled),
                        "On"
                    ),
                    (
                        ExampleSetting::ReceiveShadows(ReceiveShadows::Disabled),
                        "Off"
                    ),
                ],
                {
                    if app_status.receive_shadows == ReceiveShadows::Enabled {
                        0
                    }
                    else {
                        1
                    }
                },
            )
        ]
    });
}

fn handle_setting_change(
    event: On<ValueChange<Entity>>,
    new_value_q: Query<&RadioButtonOptionValue<ExampleSetting>>,
    mut lights: Query<
        (
            &mut Visibility,
            Option<&mut DirectionalLight>,
            Option<&mut PointLight>,
            Option<&mut SpotLight>,
        ),
        Or<(With<DirectionalLight>, With<PointLight>, With<SpotLight>)>,
    >,
    mut ground_plane: Query<Entity, With<GroundPlane>>,
    mut app_status: ResMut<AppStatus>,
    mut commands: Commands,
) {
    let Ok(RadioButtonOptionValue(setting)) = new_value_q.get(event.value) else {
        return;
    };

    match *setting {
        ExampleSetting::ContactShadows(value) => {
            app_status.contact_shadows = value;
            for (_, maybe_directional_light, maybe_point_light, maybe_spot_light) in
                lights.iter_mut()
            {
                if let Some(mut directional_light) = maybe_directional_light {
                    directional_light.contact_shadows_enabled =
                        value == ContactShadowState::Enabled;
                }
                if let Some(mut point_light) = maybe_point_light {
                    point_light.contact_shadows_enabled = value == ContactShadowState::Enabled;
                }
                if let Some(mut spot_light) = maybe_spot_light {
                    spot_light.contact_shadows_enabled = value == ContactShadowState::Enabled;
                }
            }
        }
        ExampleSetting::ShadowMaps(value) => {
            app_status.shadow_maps = value;
            for (_, maybe_directional_light, maybe_point_light, maybe_spot_light) in
                lights.iter_mut()
            {
                if let Some(mut directional_light) = maybe_directional_light {
                    directional_light.shadow_maps_enabled = value == ShadowMaps::Enabled;
                }
                if let Some(mut point_light) = maybe_point_light {
                    point_light.shadow_maps_enabled = value == ShadowMaps::Enabled;
                }
                if let Some(mut spot_light) = maybe_spot_light {
                    spot_light.shadow_maps_enabled = value == ShadowMaps::Enabled;
                }
            }
        }
        ExampleSetting::LightRotation(value) => {
            app_status.light_rotation = value;
        }
        ExampleSetting::LightType(value) => {
            app_status.light_type = value;
            for (mut visibility, maybe_directional_light, maybe_point_light, maybe_spot_light) in
                lights.iter_mut()
            {
                let is_visible = match value {
                    LightType::Directional => maybe_directional_light.is_some(),
                    LightType::Point => maybe_point_light.is_some(),
                    LightType::Spot => maybe_spot_light.is_some(),
                };
                *visibility = if is_visible {
                    Visibility::Visible
                } else {
                    Visibility::Hidden
                };
            }
        }
        ExampleSetting::ReceiveShadows(value) => {
            app_status.receive_shadows = value;
            for entity in ground_plane.iter_mut() {
                match value {
                    ReceiveShadows::Enabled => {
                        commands.entity(entity).remove::<NotShadowReceiver>();
                    }
                    ReceiveShadows::Disabled => {
                        commands.entity(entity).insert(NotShadowReceiver);
                    }
                }
            }
        }
    }
}