Skip to content
Open
Show file tree
Hide file tree
Changes from 3 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
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);
10 changes: 7 additions & 3 deletions crates/bevy_pbr/src/deferred/mod.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
use crate::{
graph::NodePbr, MeshPipeline, MeshViewBindGroup, RenderViewLightProbes,
ScreenSpaceAmbientOcclusion, ScreenSpaceReflectionsUniform, ViewEnvironmentMapUniformOffset,
ViewLightProbesUniformOffset, ViewScreenSpaceReflectionsUniformOffset,
TONEMAPPING_LUT_SAMPLER_BINDING_INDEX, TONEMAPPING_LUT_TEXTURE_BINDING_INDEX,
ScreenSpaceAmbientOcclusion, ScreenSpaceReflectionsUniform, ViewContactShadowsUniformOffset,
ViewEnvironmentMapUniformOffset, ViewLightProbesUniformOffset,
ViewScreenSpaceReflectionsUniformOffset, TONEMAPPING_LUT_SAMPLER_BINDING_INDEX,
TONEMAPPING_LUT_TEXTURE_BINDING_INDEX,
};
use crate::{
DistanceFog, ExtractedAtmosphere, MeshPipelineKey, ViewFogUniformOffset,
Expand Down Expand Up @@ -139,6 +140,7 @@ impl ViewNode for DeferredOpaquePass3dPbrLightingNode {
&'static ViewFogUniformOffset,
&'static ViewLightProbesUniformOffset,
&'static ViewScreenSpaceReflectionsUniformOffset,
&'static ViewContactShadowsUniformOffset,
&'static ViewEnvironmentMapUniformOffset,
&'static MeshViewBindGroup,
&'static ViewTarget,
Expand All @@ -156,6 +158,7 @@ impl ViewNode for DeferredOpaquePass3dPbrLightingNode {
view_fog_offset,
view_light_probes_offset,
view_ssr_offset,
view_contact_shadows_offset,
view_environment_map_offset,
mesh_view_bind_group,
target,
Expand Down Expand Up @@ -215,6 +218,7 @@ impl ViewNode for DeferredOpaquePass3dPbrLightingNode {
view_fog_offset.offset,
**view_light_probes_offset,
**view_ssr_offset,
**view_contact_shadows_offset,
**view_environment_map_offset,
],
);
Expand Down
11 changes: 9 additions & 2 deletions crates/bevy_pbr/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,11 @@ pub mod experimental {
mod atmosphere;
mod cluster;
mod components;
pub mod contact_shadows;
pub use contact_shadows::{
ContactShadows, ContactShadowsBuffer, ContactShadowsPlugin, ContactShadowsUniform,
ViewContactShadowsUniformOffset,
};
pub mod decal;
pub mod deferred;
pub mod diagnostic;
Expand Down Expand Up @@ -79,6 +84,7 @@ pub use volumetric_fog::VolumetricFogPlugin;
pub mod prelude {
#[doc(hidden)]
pub use crate::{
contact_shadows::ContactShadowsPlugin,
fog::{DistanceFog, FogFalloff},
material::{Material, MaterialPlugin},
mesh_material::MeshMaterial3d,
Expand Down Expand Up @@ -153,8 +159,8 @@ fn shader_ref(path: PathBuf) -> ShaderRef {
ShaderRef::Path(AssetPath::from_path_buf(path).with_source("embedded"))
}

pub const TONEMAPPING_LUT_TEXTURE_BINDING_INDEX: u32 = 18;
pub const TONEMAPPING_LUT_SAMPLER_BINDING_INDEX: u32 = 19;
pub const TONEMAPPING_LUT_TEXTURE_BINDING_INDEX: u32 = 19;
pub const TONEMAPPING_LUT_SAMPLER_BINDING_INDEX: u32 = 20;

/// Sets up the entire PBR infrastructure of bevy.
pub struct PbrPlugin {
Expand Down Expand Up @@ -241,6 +247,7 @@ impl Plugin for PbrPlugin {
VolumetricFogPlugin,
ScreenSpaceReflectionsPlugin,
ClusteredDecalPlugin,
ContactShadowsPlugin,
))
.add_plugins((
decal::ForwardDecalPlugin,
Expand Down
4 changes: 4 additions & 0 deletions crates/bevy_pbr/src/render/mesh.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use crate::contact_shadows::ViewContactShadowsUniformOffset;
use crate::{
material_bind_groups::{MaterialBindGroupIndex, MaterialBindGroupSlot},
resources::write_atmosphere_buffer,
Expand Down Expand Up @@ -2997,6 +2998,7 @@ impl<P: PhaseItem, const I: usize> RenderCommand<P> for SetMeshViewBindGroup<I>
Read<ViewFogUniformOffset>,
Read<ViewLightProbesUniformOffset>,
Read<ViewScreenSpaceReflectionsUniformOffset>,
Read<ViewContactShadowsUniformOffset>,
Read<ViewEnvironmentMapUniformOffset>,
Read<MeshViewBindGroup>,
Option<Read<OrderIndependentTransparencySettingsOffset>>,
Expand All @@ -3012,6 +3014,7 @@ impl<P: PhaseItem, const I: usize> RenderCommand<P> for SetMeshViewBindGroup<I>
view_fog,
view_light_probes,
view_ssr,
view_contact_shadows,
view_environment_map,
mesh_view_bind_group,
maybe_oit_layers_count_offset,
Expand All @@ -3026,6 +3029,7 @@ impl<P: PhaseItem, const I: usize> RenderCommand<P> for SetMeshViewBindGroup<I>
view_fog.offset,
**view_light_probes,
**view_ssr,
**view_contact_shadows,
**view_environment_map,
];
if let Some(layers_count_offset) = maybe_oit_layers_count_offset {
Expand Down
Loading