Skip to content
Open
Show file tree
Hide file tree
Changes from 18 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
331ba66
Implement contact shadows
aevyrie Jan 5, 2026
375b2b1
Formatting
aevyrie Jan 5, 2026
835ecd6
Bump contact shadow steps to match ssr and improve appearance
aevyrie Jan 5, 2026
d96c7d6
Review feedback
aevyrie Jan 8, 2026
bae7d05
Factor out contact shadow logic
aevyrie Jan 8, 2026
00b02e5
Add STBN support
aevyrie Jan 8, 2026
3fa5a25
Update example defaults
aevyrie Jan 8, 2026
20151bd
Example fixes
aevyrie Jan 8, 2026
07f2b89
Add light setting `contact_shadows_enabled` and rename `shadows_enabl…
aevyrie Jan 8, 2026
8f45d15
Require the stbn feature for the contact shadows example
aevyrie Jan 8, 2026
f2fe279
CI lints
aevyrie Jan 8, 2026
8297194
Merge branch 'main' into contact-shadows
aevyrie Jan 8, 2026
3c0f881
Update example appearance
aevyrie Jan 8, 2026
06b4f1e
Merge remote-tracking branch 'origin/main' into contact-shadows
aevyrie Jan 8, 2026
ce20572
Equalize light intensity
aevyrie Jan 8, 2026
3e5530c
Fix lint
aevyrie Jan 8, 2026
e04c02a
Merge branch 'main' into contact-shadows
aevyrie Jan 8, 2026
246c803
Add TAA and SSAO to example
aevyrie Jan 8, 2026
45756a2
Include STBN to mesh view layout key
aevyrie Jan 9, 2026
4a92edc
Juicing the example
aevyrie Jan 9, 2026
3b9568b
Fix refactor errors
aevyrie Jan 9, 2026
0d27653
Add migration guide.
aevyrie Jan 9, 2026
a16a2dc
Merge branch 'main' into contact-shadows
aevyrie Jan 9, 2026
b3d9bf6
Fix atmospheric shader not working when no directional light present
aevyrie Jan 9, 2026
11f35a1
Merge fixes
aevyrie Jan 9, 2026
99e8480
Respect material shadow receiver settings for contact shadows.
aevyrie Jan 9, 2026
0c28f00
Markdown lint
aevyrie Jan 9, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -1245,6 +1245,19 @@ description = "Showcases different blend modes"
category = "3D Rendering"
wasm = true

[[example]]
name = "contact_shadows"
path = "examples/3d/contact_shadows.rs"
# Causes an ICE on docs.rs
doc-scrape-examples = false
required-features = ["bluenoise_texture"]

[package.metadata.example.contact_shadows]
name = "Contact Shadows"
description = "Showcases how contact shadows add shadow detail"
category = "3D Rendering"
wasm = true

[[example]]
name = "lighting"
path = "examples/3d/lighting.rs"
Expand Down
4 changes: 2 additions & 2 deletions crates/bevy_light/src/cascade.rs
Original file line number Diff line number Diff line change
Expand Up @@ -185,7 +185,7 @@ pub struct Cascade {

pub fn clear_directional_light_cascades(mut lights: Query<(&DirectionalLight, &mut Cascades)>) {
for (directional_light, mut cascades) in lights.iter_mut() {
if !directional_light.shadows_enabled {
if !directional_light.shadow_maps_enabled {
continue;
}
cascades.cascades.clear();
Expand Down Expand Up @@ -214,7 +214,7 @@ pub fn build_directional_light_cascades(
.collect::<Vec<_>>();

for (transform, directional_light, cascades_config, mut cascades) in &mut lights {
if !directional_light.shadows_enabled {
if !directional_light.shadow_maps_enabled {
continue;
}

Expand Down
16 changes: 8 additions & 8 deletions crates/bevy_light/src/cluster/assign.rs
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ pub enum ClusterableObjectType {
/// Whether shadows are enabled for this point light.
///
/// This is used for sorting the light list.
shadows_enabled: bool,
shadow_maps_enabled: bool,

/// Whether this light interacts with volumetrics.
///
Expand All @@ -73,7 +73,7 @@ pub enum ClusterableObjectType {
/// Whether shadows are enabled for this spot light.
///
/// This is used for sorting the light list.
shadows_enabled: bool,
shadow_maps_enabled: bool,

/// Whether this light interacts with volumetrics.
///
Expand Down Expand Up @@ -105,14 +105,14 @@ impl ClusterableObjectType {
pub fn ordering(&self) -> (u8, bool, bool) {
match *self {
ClusterableObjectType::PointLight {
shadows_enabled,
shadow_maps_enabled,
volumetric,
} => (0, !shadows_enabled, !volumetric),
} => (0, !shadow_maps_enabled, !volumetric),
ClusterableObjectType::SpotLight {
shadows_enabled,
shadow_maps_enabled,
volumetric,
..
} => (1, !shadows_enabled, !volumetric),
} => (1, !shadow_maps_enabled, !volumetric),
ClusterableObjectType::ReflectionProbe => (2, false, false),
ClusterableObjectType::IrradianceVolume => (3, false, false),
ClusterableObjectType::Decal => (4, false, false),
Expand Down Expand Up @@ -178,7 +178,7 @@ pub(crate) fn assign_objects_to_clusters(
transform: GlobalTransform::from_translation(transform.translation()),
range: point_light.range,
object_type: ClusterableObjectType::PointLight {
shadows_enabled: point_light.shadows_enabled,
shadow_maps_enabled: point_light.shadow_maps_enabled,
volumetric: volumetric.is_some(),
},
render_layers: maybe_layers.unwrap_or_default().clone(),
Expand All @@ -198,7 +198,7 @@ pub(crate) fn assign_objects_to_clusters(
range: spot_light.range,
object_type: ClusterableObjectType::SpotLight {
outer_angle: spot_light.outer_angle,
shadows_enabled: spot_light.shadows_enabled,
shadow_maps_enabled: spot_light.shadow_maps_enabled,
volumetric: volumetric.is_some(),
},
render_layers: maybe_layers.unwrap_or_default().clone(),
Expand Down
12 changes: 8 additions & 4 deletions crates/bevy_light/src/directional_light.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ use super::{
///
/// ## Shadows
///
/// To enable shadows, set the `shadows_enabled` property to `true`.
/// To enable shadows, set the `shadow_maps_enabled` property to `true`.
///
/// Shadows are produced via [cascaded shadow maps](https://developer.download.nvidia.com/SDK/10.5/opengl/src/cascaded_shadow_maps/doc/cascaded_shadow_maps.pdf).
///
Expand Down Expand Up @@ -86,7 +86,10 @@ pub struct DirectionalLight {
/// Note that shadows are rather expensive and become more so with every
/// light that casts them. In general, it's best to aggressively limit the
/// number of lights with shadows enabled to one or two at most.
pub shadows_enabled: bool,
pub shadow_maps_enabled: bool,

/// Whether this light casts contact shadows.
pub contact_shadows_enabled: bool,

/// Whether soft shadows are enabled, and if so, the size of the light.
///
Expand Down Expand Up @@ -142,7 +145,8 @@ impl Default for DirectionalLight {
DirectionalLight {
color: Color::WHITE,
illuminance: light_consts::lux::AMBIENT_DAYLIGHT,
shadows_enabled: false,
shadow_maps_enabled: false,
contact_shadows_enabled: false,
shadow_depth_bias: Self::DEFAULT_SHADOW_DEPTH_BIAS,
shadow_normal_bias: Self::DEFAULT_SHADOW_NORMAL_BIAS,
affects_lightmapped_mesh_diffuse: true,
Expand Down Expand Up @@ -221,7 +225,7 @@ pub fn update_directional_light_frusta(
// The frustum is used for culling meshes to the light for shadow mapping
// so if shadow mapping is disabled for this light, then the frustum is
// not needed.
if !directional_light.shadows_enabled || !visibility.get() {
if !directional_light.shadow_maps_enabled || !visibility.get() {
continue;
}

Expand Down
6 changes: 3 additions & 3 deletions crates/bevy_light/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -358,7 +358,7 @@ pub fn check_dir_light_mesh_visibility(
}

// NOTE: If shadow mapping is disabled for the light then it must have no visible entities
if !directional_light.shadows_enabled || !light_view_visibility.get() {
if !directional_light.shadow_maps_enabled || !light_view_visibility.get() {
continue;
}

Expand Down Expand Up @@ -524,7 +524,7 @@ pub fn check_point_light_mesh_visibility(
}

// NOTE: If shadow mapping is disabled for the light then it must have no visible entities
if !point_light.shadows_enabled {
if !point_light.shadow_maps_enabled {
continue;
}

Expand Down Expand Up @@ -613,7 +613,7 @@ pub fn check_point_light_mesh_visibility(
visible_entities.clear();

// NOTE: If shadow mapping is disabled for the light then it must have no visible entities
if !point_light.shadows_enabled {
if !point_light.shadow_maps_enabled {
continue;
}

Expand Down
12 changes: 8 additions & 4 deletions crates/bevy_light/src/point_light.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ use crate::{
///
/// ## Shadows
///
/// To enable shadows, set the `shadows_enabled` property to `true`.
/// To enable shadows, set the `shadow_maps_enabled` property to `true`.
///
/// To control the resolution of the shadow maps, use the [`PointLightShadowMap`] resource.
#[derive(Component, Debug, Clone, Copy, Reflect)]
Expand Down Expand Up @@ -70,7 +70,10 @@ pub struct PointLight {
pub radius: f32,

/// Whether this light casts shadows.
pub shadows_enabled: bool,
pub shadow_maps_enabled: bool,

/// Whether this light casts contact shadows.
pub contact_shadows_enabled: bool,

/// Whether soft shadows are enabled.
///
Expand Down Expand Up @@ -132,7 +135,8 @@ impl Default for PointLight {
intensity: light_consts::lumens::VERY_LARGE_CINEMA_LIGHT,
range: 20.0,
radius: 0.0,
shadows_enabled: false,
shadow_maps_enabled: false,
contact_shadows_enabled: false,
affects_lightmapped_mesh_diffuse: true,
shadow_depth_bias: Self::DEFAULT_SHADOW_DEPTH_BIAS,
shadow_normal_bias: Self::DEFAULT_SHADOW_NORMAL_BIAS,
Expand Down Expand Up @@ -214,7 +218,7 @@ pub fn update_point_light_frusta(
// not needed.
// Also, if the light is not relevant for any cluster, it will not be in the
// global lights set and so there is no need to update its frusta.
if !point_light.shadows_enabled || !global_lights.entities.contains(&entity) {
if !point_light.shadow_maps_enabled || !global_lights.entities.contains(&entity) {
continue;
}

Expand Down
11 changes: 8 additions & 3 deletions crates/bevy_light/src/spot_light.rs
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,11 @@ pub struct SpotLight {
/// Note that shadows are rather expensive and become more so with every
/// light that casts them. In general, it's best to aggressively limit the
/// number of lights with shadows enabled to one or two at most.
pub shadows_enabled: bool,
pub shadow_maps_enabled: bool,

/// Whether this light casts contact shadows. Cameras must also have the `ContactShadows`
/// component.
pub contact_shadows_enabled: bool,

/// Whether soft shadows are enabled.
///
Expand Down Expand Up @@ -142,7 +146,8 @@ impl Default for SpotLight {
intensity: 1_000_000.0,
range: 20.0,
radius: 0.0,
shadows_enabled: false,
shadow_maps_enabled: false,
contact_shadows_enabled: false,
affects_lightmapped_mesh_diffuse: true,
shadow_depth_bias: Self::DEFAULT_SHADOW_DEPTH_BIAS,
shadow_normal_bias: Self::DEFAULT_SHADOW_NORMAL_BIAS,
Expand Down Expand Up @@ -214,7 +219,7 @@ pub fn update_spot_light_frusta(
// not needed.
// Also, if the light is not relevant for any cluster, it will not be in the
// global lights set and so there is no need to update its frusta.
if !spot_light.shadows_enabled || !global_lights.entities.contains(&entity) {
if !spot_light.shadow_maps_enabled || !global_lights.entities.contains(&entity) {
continue;
}

Expand Down
2 changes: 1 addition & 1 deletion crates/bevy_light/src/volumetric.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ use bevy_reflect::prelude::*;
use bevy_transform::components::Transform;

/// Add this component to a [`DirectionalLight`](crate::DirectionalLight) with a shadow map
/// (`shadows_enabled: true`) to make volumetric fog interact with it.
/// (`shadow_maps_enabled: true`) to make volumetric fog interact with it.
///
/// This allows the light to generate light shafts/god rays.
#[derive(Clone, Copy, Component, Default, Debug, Reflect)]
Expand Down
140 changes: 140 additions & 0 deletions crates/bevy_pbr/src/contact_shadows.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
//! Contact shadows implemented via screenspace raymarching.

use bevy_app::{App, Plugin};
use bevy_derive::{Deref, DerefMut};
use bevy_ecs::{
component::Component,
entity::Entity,
query::{QueryItem, With},
reflect::ReflectComponent,
resource::Resource,
schedule::IntoScheduleConfigs,
system::{Commands, Query, Res, ResMut},
};
use bevy_reflect::{std_traits::ReflectDefault, Reflect};
use bevy_render::{
extract_component::{ExtractComponent, ExtractComponentPlugin},
render_resource::{DynamicUniformBuffer, ShaderType},
renderer::{RenderDevice, RenderQueue},
view::ExtractedView,
Render, RenderApp, RenderSystems,
};
use bevy_utils::default;

/// Enables contact shadows for a camera.
pub struct ContactShadowsPlugin;

/// Add this component to a camera to enable contact shadows.
///
/// Contact shadows are a screen-space technique that adds small-scale shadows
/// in areas where traditional shadow maps may lack detail, such as where
/// objects touch the ground.
///
/// This can be used in forward or deferred rendering, but the depth prepass is required.
#[derive(Clone, Copy, Component, Reflect)]
#[reflect(Component, Default, Clone)]
#[require(bevy_core_pipeline::prepass::DepthPrepass)]
pub struct ContactShadows {
/// The number of steps to be taken at regular intervals to find an initial
/// intersection.
pub linear_steps: u32,
/// When marching the depth buffer, we only have 2.5D information and don't
/// know how thick surfaces are. We shall assume that the depth buffer
/// fragments are cuboids with a constant thickness defined by this
/// parameter.
pub thickness: f32,
/// The length of the contact shadow ray in world space.
pub length: f32,
}

impl Default for ContactShadows {
fn default() -> Self {
Self {
linear_steps: 16,
thickness: 0.1,
length: 0.3,
}
}
}

/// A version of [`ContactShadows`] for upload to the GPU.
#[derive(Clone, Copy, Component, ShaderType, Default)]
pub struct ContactShadowsUniform {
pub linear_steps: u32,
pub thickness: f32,
pub length: f32,
}

impl From<ContactShadows> for ContactShadowsUniform {
fn from(settings: ContactShadows) -> Self {
Self {
linear_steps: settings.linear_steps,
thickness: settings.thickness,
length: settings.length,
}
}
}

impl ExtractComponent for ContactShadows {
type QueryData = &'static ContactShadows;
type QueryFilter = ();
type Out = ContactShadows;

fn extract_component(settings: QueryItem<'_, '_, Self::QueryData>) -> Option<Self::Out> {
Some(*settings)
}
}

/// A GPU buffer that stores the contact shadow settings for each view.
#[derive(Resource, Default)]
pub struct ContactShadowsBuffer(pub DynamicUniformBuffer<ContactShadowsUniform>);

impl Plugin for ContactShadowsPlugin {
fn build(&self, app: &mut App) {
app.register_type::<ContactShadows>()
.add_plugins(ExtractComponentPlugin::<ContactShadows>::default());

let Some(render_app) = app.get_sub_app_mut(RenderApp) else {
return;
};

render_app
.init_resource::<ContactShadowsBuffer>()
.add_systems(
Render,
prepare_contact_shadows_settings.in_set(RenderSystems::PrepareResources),
);
}
}

fn prepare_contact_shadows_settings(
mut commands: Commands,
views: Query<(Entity, Option<&ContactShadows>), With<ExtractedView>>,
mut contact_shadows_buffer: ResMut<ContactShadowsBuffer>,
render_device: Res<RenderDevice>,
render_queue: Res<RenderQueue>,
) {
contact_shadows_buffer.0.clear();
for (entity, settings) in &views {
let uniform = if let Some(settings) = settings {
ContactShadowsUniform::from(*settings)
} else {
ContactShadowsUniform {
linear_steps: 0,
..default()
}
};
let offset = contact_shadows_buffer.0.push(&uniform);
commands
.entity(entity)
.insert(ViewContactShadowsUniformOffset(offset));
}
contact_shadows_buffer
.0
.write_buffer(&render_device, &render_queue);
}

/// A component that stores the offset within the [`ContactShadowsBuffer`] for
/// each view.
#[derive(Component, Default, Deref, DerefMut)]
pub struct ViewContactShadowsUniformOffset(pub u32);
Loading