Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
8670786
fix specular?
aevyrie Dec 31, 2025
bfb6384
Attempt to fix specular dimming and roughness on smooth materials
aevyrie Jan 4, 2026
c4a4fb1
Attempted pbr ssr following h3r3tic's example
aevyrie Jan 4, 2026
49768ab
Bump roughness cutoff now that rough materials are supported
aevyrie Jan 4, 2026
547b46f
Clean up comments
aevyrie Jan 4, 2026
e304987
Use STBN for SSR ray jitter
aevyrie Jan 4, 2026
a62b883
Merge branch 'specular-fix' into pbr-ssr
aevyrie Jan 4, 2026
3a9a55f
Fix crawling in fallback noise
aevyrie Jan 4, 2026
1319c47
Revert "Attempt to fix specular dimming and roughness on smooth mater…
aevyrie Jan 4, 2026
ac937a3
Revert "fix specular?"
aevyrie Jan 4, 2026
e28fa6e
Remove unwrap
aevyrie Jan 4, 2026
ce59be1
Update SSR docs
aevyrie Jan 4, 2026
b6a7643
Fix issue with pbr skipping both diffuse and specular for ssr'd mater…
aevyrie Jan 5, 2026
16f7a1e
Update related examples
aevyrie Jan 5, 2026
3ad15ac
Improve SSR roughness handling with fading ranges
aevyrie Jan 5, 2026
c62f290
Add edge fadeout configuration for SSR
aevyrie Jan 5, 2026
b358fa8
Merge remote-tracking branch 'origin/main' into pbr-ssr
aevyrie Jan 6, 2026
89ff31a
Review feedback
aevyrie Jan 6, 2026
4787f3f
Merge branch 'main' into pbr-ssr
aevyrie Jan 8, 2026
f104b5a
Merge branch 'main' into pbr-ssr
aevyrie Jan 8, 2026
f575205
Merge branch 'main' into pbr-ssr
aevyrie Jan 8, 2026
249d48b
Merge branch 'main' into pbr-ssr
aevyrie Jan 8, 2026
60637d3
Merge branch 'main' into pbr-ssr
aevyrie Jan 9, 2026
ddcd363
Merge branch 'main' into pbr-ssr
aevyrie Jan 10, 2026
0277fb2
Blue noise binding fixes
aevyrie Jan 11, 2026
4b9fcba
Blue noise binding fixes
aevyrie Jan 11, 2026
810a16f
Oops
aevyrie Jan 11, 2026
bab05fd
Oops
aevyrie Jan 11, 2026
33a96f5
Fix and document small bug in envmap sampling
aevyrie Jan 12, 2026
157f0a4
Update defaults to better match other engines and test in scenes.
aevyrie Jan 12, 2026
19110d6
Early exit SSR for too smooth materials
aevyrie Jan 12, 2026
ed5e6d3
Remove ssao from atmosphere example
aevyrie Jan 12, 2026
db68d34
Review feedback
aevyrie Jan 12, 2026
025577f
Merge branch 'main' into pbr-ssr
aevyrie Jan 12, 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
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4662,6 +4662,7 @@ name = "ssr"
path = "examples/3d/ssr.rs"
# Causes an ICE on docs.rs
doc-scrape-examples = false
required-features = ["bluenoise_texture"]

[package.metadata.example.ssr]
name = "Screen Space Reflections"
Expand Down
45 changes: 38 additions & 7 deletions crates/bevy_pbr/src/render/mesh_view_bindings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,11 +45,12 @@ use crate::{
},
prepass,
resources::{AtmosphereBuffer, AtmosphereData, AtmosphereSampler, AtmosphereTextures},
EnvironmentMapUniformBuffer, ExtractedAtmosphere, FogMeta, GlobalClusterableObjectMeta,
GpuClusterableObjects, GpuFog, GpuLights, LightMeta, LightProbesBuffer, LightProbesUniform,
MeshPipeline, MeshPipelineKey, RenderViewLightProbes, ScreenSpaceAmbientOcclusionResources,
ScreenSpaceReflectionsBuffer, ScreenSpaceReflectionsUniform, ShadowSamplers,
ViewClusterBindings, ViewShadowBindings, CLUSTERED_FORWARD_STORAGE_BUFFER_COUNT,
Bluenoise, EnvironmentMapUniformBuffer, ExtractedAtmosphere, FogMeta,
GlobalClusterableObjectMeta, GpuClusterableObjects, GpuFog, GpuLights, LightMeta,
LightProbesBuffer, LightProbesUniform, MeshPipeline, MeshPipelineKey, RenderViewLightProbes,
ScreenSpaceAmbientOcclusionResources, ScreenSpaceReflectionsBuffer,
ScreenSpaceReflectionsUniform, ShadowSamplers, ViewClusterBindings, ViewShadowBindings,
CLUSTERED_FORWARD_STORAGE_BUFFER_COUNT,
};

#[cfg(all(feature = "webgl", target_arch = "wasm32", not(feature = "webgpu")))]
Expand Down Expand Up @@ -84,6 +85,7 @@ bitflags::bitflags! {
const DEFERRED_PREPASS = 1 << 4;
const OIT_ENABLED = 1 << 5;
const ATMOSPHERE = 1 << 6;
const BLUE_NOISE_TEXTURE = 1 << 7;
}
}

Expand All @@ -96,7 +98,7 @@ impl MeshPipelineViewLayoutKey {
use MeshPipelineViewLayoutKey as Key;

format!(
"mesh_view_layout{}{}{}{}{}{}{}",
"mesh_view_layout{}{}{}{}{}{}{}{}",
if self.contains(Key::MULTISAMPLED) {
"_multisampled"
} else {
Expand Down Expand Up @@ -132,6 +134,11 @@ impl MeshPipelineViewLayoutKey {
} else {
Default::default()
},
if self.contains(Key::BLUE_NOISE_TEXTURE) {
"_stbn"
} else {
Default::default()
},
)
}
}
Expand Down Expand Up @@ -162,6 +169,10 @@ impl From<MeshPipelineKey> for MeshPipelineViewLayoutKey {
result |= MeshPipelineViewLayoutKey::ATMOSPHERE;
}

if cfg!(feature = "bluenoise_texture") {
result |= MeshPipelineViewLayoutKey::BLUE_NOISE_TEXTURE;
}

result
}
}
Expand Down Expand Up @@ -410,6 +421,14 @@ fn layout_entries(
));
}

// Blue noise
if layout_key.contains(MeshPipelineViewLayoutKey::BLUE_NOISE_TEXTURE) {
entries = entries.extend_with_indices(((
33,
texture_2d_array(TextureSampleType::Float { filterable: false }),
),));
}

let mut binding_array_entries = DynamicBindGroupLayoutEntries::new(ShaderStages::FRAGMENT);
binding_array_entries = binding_array_entries.extend_with_indices((
(0, environment_map_entries[0]),
Expand Down Expand Up @@ -599,11 +618,12 @@ pub fn prepare_mesh_view_bind_groups(
visibility_ranges: Res<RenderVisibilityRanges>,
ssr_buffer: Res<ScreenSpaceReflectionsBuffer>,
oit_buffers: Res<OitBuffers>,
(decals_buffer, render_decals, atmosphere_buffer, atmosphere_sampler): (
(decals_buffer, render_decals, atmosphere_buffer, atmosphere_sampler, blue_noise): (
Res<DecalsBuffer>,
Res<RenderClusteredDecals>,
Option<Res<AtmosphereBuffer>>,
Option<Res<AtmosphereSampler>>,
Res<Bluenoise>,
),
) {
if let (
Expand Down Expand Up @@ -659,6 +679,9 @@ pub fn prepare_mesh_view_bind_groups(
if has_atmosphere {
layout_key |= MeshPipelineViewLayoutKey::ATMOSPHERE;
}
if cfg!(feature = "bluenoise_texture") {
layout_key |= MeshPipelineViewLayoutKey::BLUE_NOISE_TEXTURE;
}

let layout = mesh_pipeline.get_view_layout(layout_key);

Expand Down Expand Up @@ -752,6 +775,14 @@ pub fn prepare_mesh_view_bind_groups(
));
}

if layout_key.contains(MeshPipelineViewLayoutKey::BLUE_NOISE_TEXTURE) {
let stbn_view = &images
.get(&blue_noise.texture)
.expect("STBN texture is added unconditionally with at least a placeholder")
.texture_view;
entries = entries.extend_with_indices(((33, stbn_view),));
}

let mut entries_binding_array = DynamicBindGroupEntries::new();

let environment_map_bind_group_entries = RenderViewEnvironmentMapBindGroupEntries::get(
Expand Down
7 changes: 6 additions & 1 deletion crates/bevy_pbr/src/render/mesh_view_types.wgsl
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,12 @@ struct LightProbes {
// For more information on these settings, see the documentation for
// `bevy_pbr::ssr::ScreenSpaceReflections`.
struct ScreenSpaceReflectionsSettings {
perceptual_roughness_threshold: f32,
min_perceptual_roughness: f32,
min_perceptual_roughness_fully_active: f32,
max_perceptual_roughness_starts_to_fade: f32,
max_perceptual_roughness: f32,
edge_fadeout_fully_active: f32,
edge_fadeout_no_longer_active: f32,
thickness: f32,
linear_steps: u32,
linear_march_exponent: f32,
Expand Down
34 changes: 18 additions & 16 deletions crates/bevy_pbr/src/render/pbr_functions.wgsl
Original file line number Diff line number Diff line change
Expand Up @@ -607,32 +607,34 @@ fn apply_pbr_lighting(

// Environment map light (indirect)
#ifdef ENVIRONMENT_MAP
// If screen space reflections are going to be used for this material, don't
// accumulate environment map light yet. The SSR shader will do it.
// If screen space reflections are going to be used for this material, only
// accumulate the diffuse part of the environment map light. The SSR shader
// will accumulate the specular part (including the environment map fallback
// if SSR misses).
#ifdef SCREEN_SPACE_REFLECTIONS
let use_ssr = perceptual_roughness <=
view_bindings::ssr_settings.perceptual_roughness_threshold;
let use_ssr = perceptual_roughness <= view_bindings::ssr_settings.max_perceptual_roughness
&& perceptual_roughness >= view_bindings::ssr_settings.min_perceptual_roughness;
#else // SCREEN_SPACE_REFLECTIONS
let use_ssr = false;
#endif // SCREEN_SPACE_REFLECTIONS

if (!use_ssr) {
#ifdef STANDARD_MATERIAL_ANISOTROPY
var bent_normal_lighting_input = lighting_input;
bend_normal_for_anisotropy(&bent_normal_lighting_input);
let environment_map_lighting_input = &bent_normal_lighting_input;
var bent_normal_lighting_input = lighting_input;
bend_normal_for_anisotropy(&bent_normal_lighting_input);
let environment_map_lighting_input = &bent_normal_lighting_input;
#else // STANDARD_MATERIAL_ANISOTROPY
let environment_map_lighting_input = &lighting_input;
let environment_map_lighting_input = &lighting_input;
#endif // STANDARD_MATERIAL_ANISOTROPY

let environment_light = environment_map::environment_map_light(
environment_map_lighting_input,
&clusterable_object_index_ranges,
found_diffuse_indirect,
);
let environment_light = environment_map::environment_map_light(
environment_map_lighting_input,
&clusterable_object_index_ranges,
found_diffuse_indirect,
);

indirect_light += environment_light.diffuse * diffuse_occlusion +
environment_light.specular * specular_occlusion;
indirect_light += environment_light.diffuse * diffuse_occlusion;
if (!use_ssr) {
indirect_light += environment_light.specular * specular_occlusion;
}
#endif // ENVIRONMENT_MAP

Expand Down
84 changes: 67 additions & 17 deletions crates/bevy_pbr/src/ssr/mod.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
//! Screen space reflections implemented via raymarching.

use core::ops::Range;

use bevy_app::{App, Plugin};
use bevy_asset::{load_embedded_asset, AssetServer, Handle};
use bevy_core_pipeline::{
Expand Down Expand Up @@ -27,6 +29,7 @@ use bevy_reflect::{std_traits::ReflectDefault, Reflect};
use bevy_render::{
diagnostic::RecordDiagnostics,
extract_component::{ExtractComponent, ExtractComponentPlugin},
render_asset::RenderAssets,
render_graph::{
NodeRunError, RenderGraph, RenderGraphContext, RenderGraphExt, ViewNode, ViewNodeRunner,
},
Expand All @@ -36,9 +39,11 @@ use bevy_render::{
DynamicUniformBuffer, FilterMode, FragmentState, Operations, PipelineCache,
RenderPassColorAttachment, RenderPassDescriptor, RenderPipelineDescriptor, Sampler,
SamplerBindingType, SamplerDescriptor, ShaderStages, ShaderType, SpecializedRenderPipeline,
SpecializedRenderPipelines, TextureFormat, TextureSampleType,
SpecializedRenderPipelines, TextureFormat, TextureSampleType, TextureViewDescriptor,
TextureViewDimension,
},
renderer::{RenderAdapter, RenderContext, RenderDevice, RenderQueue},
texture::GpuImage,
view::{ExtractedView, Msaa, ViewTarget, ViewUniformOffset},
Render, RenderApp, RenderStartup, RenderSystems,
};
Expand All @@ -47,8 +52,8 @@ use bevy_utils::{once, prelude::default};
use tracing::info;

use crate::{
binding_arrays_are_usable, graph::NodePbr, ExtractedAtmosphere, MeshPipelineViewLayoutKey,
MeshPipelineViewLayouts, MeshViewBindGroup, RenderViewLightProbes,
binding_arrays_are_usable, graph::NodePbr, Bluenoise, ExtractedAtmosphere,
MeshPipelineViewLayoutKey, MeshPipelineViewLayouts, MeshViewBindGroup, RenderViewLightProbes,
ViewEnvironmentMapUniformOffset, ViewFogUniformOffset, ViewLightProbesUniformOffset,
ViewLightsUniformOffset,
};
Expand All @@ -65,10 +70,7 @@ pub struct ScreenSpaceReflectionsPlugin;
/// components, which are inserted automatically,
/// but deferred rendering itself is not automatically enabled.
///
/// SSR currently performs no roughness filtering for glossy reflections, so
/// only very smooth surfaces will reflect objects in screen space. You can
/// adjust the `perceptual_roughness_threshold` in order to tune the threshold
/// below which screen-space reflections will be traced.
/// Enable the `bluenoise_texture` feature to improve the quality of noise on rough reflections.
///
/// As with all screen-space techniques, SSR can only reflect objects on screen.
/// When objects leave the camera, they will disappear from reflections.
Expand All @@ -82,14 +84,22 @@ pub struct ScreenSpaceReflectionsPlugin;
/// Screen-space reflections are presently unsupported on WebGL 2 because of a
/// bug whereby Naga doesn't generate correct GLSL when sampling depth buffers,
/// which is required for screen-space raymarching.
#[derive(Clone, Copy, Component, Reflect)]
#[derive(Clone, Component, Reflect)]
#[reflect(Component, Default, Clone)]
#[require(DepthPrepass, DeferredPrepass)]
#[doc(alias = "Ssr")]
pub struct ScreenSpaceReflections {
/// The maximum PBR roughness level that will enable screen space
/// reflections.
pub perceptual_roughness_threshold: f32,
/// The perceptual roughness range over which SSR begins to fade in.
///
/// The first value is the roughness at which SSR begins to appear; the
/// second value is the roughness at which SSR is fully active.
pub min_perceptual_roughness: Range<f32>,

/// The perceptual roughness range over which SSR begins to fade out.
///
/// The first value is the roughness at which SSR begins to fade out; the
/// second value is the roughness at which SSR is no longer active.
pub max_perceptual_roughness: Range<f32>,

/// 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
Expand All @@ -115,6 +125,14 @@ pub struct ScreenSpaceReflections {
/// as 1 or 2.
pub linear_march_exponent: f32,

/// The range over which SSR begins to fade out at the edges of the screen,
/// in terms of a percentage of the screen dimensions.
///
/// The first value is the percentage from the edge at which SSR is no
/// longer active; the second value is the percentage at which SSR is fully
/// active.
pub edge_fadeout: Range<f32>,

/// Number of steps in a bisection (binary search) to perform once the
/// linear search has found an intersection. Helps narrow down the hit,
/// increasing the chance of the secant method finding an accurate hit
Expand All @@ -133,7 +151,12 @@ pub struct ScreenSpaceReflections {
/// [`ScreenSpaceReflections`].
#[derive(Clone, Copy, Component, ShaderType)]
pub struct ScreenSpaceReflectionsUniform {
perceptual_roughness_threshold: f32,
min_perceptual_roughness: f32,
min_perceptual_roughness_fully_active: f32,
max_perceptual_roughness_starts_to_fade: f32,
max_perceptual_roughness: f32,
edge_fadeout_fully_active: f32,
edge_fadeout_no_longer_active: f32,
thickness: f32,
linear_steps: u32,
linear_march_exponent: f32,
Expand Down Expand Up @@ -240,12 +263,14 @@ impl Default for ScreenSpaceReflections {
// <https://gist.github.com/h3r2tic/9c8356bdaefbe80b1a22ae0aaee192db?permalink_comment_id=4552149#gistcomment-4552149>.
fn default() -> Self {
Self {
perceptual_roughness_threshold: 0.1,
linear_steps: 16,
bisection_steps: 4,
min_perceptual_roughness: 0.08..0.12,
max_perceptual_roughness: 0.55..0.6,
linear_steps: 10,
bisection_steps: 5,
use_secant: true,
thickness: 0.25,
linear_march_exponent: 1.0,
edge_fadeout: 0.0..0.0,
}
}
}
Expand Down Expand Up @@ -293,6 +318,17 @@ impl ViewNode for ScreenSpaceReflectionsNode {

// Create the bind group for this view.
let ssr_pipeline = world.resource::<ScreenSpaceReflectionsPipeline>();
let bluenoise = world.resource::<Bluenoise>();
let render_images = world.resource::<RenderAssets<GpuImage>>();
let Some(stbn_texture) = render_images.get(&bluenoise.texture) else {
return Ok(());
};
let stbn_view = stbn_texture.texture.create_view(&TextureViewDescriptor {
label: Some("ssr_stbn_view"),
dimension: Some(TextureViewDimension::D2Array),
..default()
});

let ssr_bind_group = render_context.render_device().create_bind_group(
"SSR bind group",
&pipeline_cache.get_bind_group_layout(&ssr_pipeline.bind_group_layout),
Expand All @@ -301,6 +337,7 @@ impl ViewNode for ScreenSpaceReflectionsNode {
&ssr_pipeline.color_sampler,
&ssr_pipeline.depth_linear_sampler,
&ssr_pipeline.depth_nearest_sampler,
&stbn_view,
)),
);

Expand Down Expand Up @@ -363,6 +400,7 @@ pub fn init_screen_space_reflections_pipeline(
binding_types::sampler(SamplerBindingType::Filtering),
binding_types::sampler(SamplerBindingType::Filtering),
binding_types::sampler(SamplerBindingType::NonFiltering),
binding_types::texture_2d_array(TextureSampleType::Float { filterable: false }),
),
),
);
Expand Down Expand Up @@ -455,6 +493,9 @@ pub fn prepare_ssr_pipelines(
has_motion_vector_prepass,
);
mesh_pipeline_view_key.set(MeshPipelineViewLayoutKey::ATMOSPHERE, has_atmosphere);
if cfg!(feature = "bluenoise_texture") {
mesh_pipeline_view_key |= MeshPipelineViewLayoutKey::BLUE_NOISE_TEXTURE;
}

// Build the pipeline.
let pipeline_id = pipelines.specialize(
Expand Down Expand Up @@ -517,7 +558,7 @@ impl ExtractComponent for ScreenSpaceReflections {
return None;
}

Some((*settings).into())
Some(settings.clone().into())
}
}

Expand Down Expand Up @@ -552,6 +593,10 @@ impl SpecializedRenderPipeline for ScreenSpaceReflectionsPipeline {
shader_defs.push("ATMOSPHERE".into());
}

if cfg!(feature = "bluenoise_texture") {
shader_defs.push("BLUE_NOISE_TEXTURE".into());
}

#[cfg(not(target_arch = "wasm32"))]
shader_defs.push("USE_DEPTH_SAMPLERS".into());

Expand Down Expand Up @@ -581,7 +626,12 @@ impl SpecializedRenderPipeline for ScreenSpaceReflectionsPipeline {
impl From<ScreenSpaceReflections> for ScreenSpaceReflectionsUniform {
fn from(settings: ScreenSpaceReflections) -> Self {
Self {
perceptual_roughness_threshold: settings.perceptual_roughness_threshold,
min_perceptual_roughness: settings.min_perceptual_roughness.start,
min_perceptual_roughness_fully_active: settings.min_perceptual_roughness.end,
max_perceptual_roughness_starts_to_fade: settings.max_perceptual_roughness.start,
max_perceptual_roughness: settings.max_perceptual_roughness.end,
edge_fadeout_no_longer_active: settings.edge_fadeout.start,
edge_fadeout_fully_active: settings.edge_fadeout.end,
thickness: settings.thickness,
linear_steps: settings.linear_steps,
linear_march_exponent: settings.linear_march_exponent,
Expand Down
Loading