Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
Show all changes
44 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
9e574ef
Merge remote-tracking branch 'origin/main' into pbr-ssr
aevyrie Jan 14, 2026
28f1949
Replace help text with interactive UI for managing SSR and app settings
aevyrie Jan 14, 2026
7593eae
add early exit for zero fade and adjust indirect light calculations
aevyrie Jan 14, 2026
c7137cf
Make skybox match envmap light
aevyrie Jan 14, 2026
e995a9a
Merge remote-tracking branch 'origin/main' into pbr-ssr
aevyrie Jan 15, 2026
7b14d72
Preserve fragment alpha in ssr
aevyrie Jan 15, 2026
b994af0
Fix pipeline key issues with ssr and atmosphere
aevyrie Jan 15, 2026
13a61dc
Fix edge fadeout effect on envmap
aevyrie Jan 15, 2026
32cb87e
Add different bases for debugging SSR output
aevyrie Jan 15, 2026
9966739
Lints
aevyrie Jan 15, 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
76 changes: 53 additions & 23 deletions crates/bevy_pbr/src/render/pbr_lighting.wgsl
Original file line number Diff line number Diff line change
Expand Up @@ -348,13 +348,14 @@ fn derive_lighting_input(N: vec3<f32>, V: vec3<f32>, L: vec3<f32>) -> DerivedLig
return input;
}

// Returns L in the `xyz` components and the specular intensity in the `w` component.
// Returns L in the `xyz` components and the modified roughness in the `w` component.
fn compute_specular_layer_values_for_point_light(
input: ptr<function, LightingInput>,
layer: u32,
V: vec3<f32>,
light_to_frag: vec3<f32>,
light_position_radius: f32,
distance: f32,
) -> vec4<f32> {
// Unpack.
let R = (*input).layers[layer].R;
Expand Down Expand Up @@ -382,22 +383,24 @@ fn compute_specular_layer_values_for_point_light(
let closestPoint = light_to_frag + centerToRay * saturate(
light_position_radius * inverseSqrt(dot(centerToRay, centerToRay)));
let LspecLengthInverse = inverseSqrt(dot(closestPoint, closestPoint));
let normalizationFactor = a / saturate(a + (light_position_radius * 0.5 * LspecLengthInverse));
let intensity = normalizationFactor * normalizationFactor;

// a' = saturate( a + sourceRadius / (2 * distance) )
// see Karis 2013
let a_prime = saturate(a + light_position_radius / (2.0 * distance));

let L: vec3<f32> = closestPoint * LspecLengthInverse; // normalize() equivalent?
return vec4(L, intensity);
return vec4(L, a_prime);
}

// Cook-Torrance approximation of the microfacet model integration using Fresnel law F to model f_m
// f_r(v,l) = { D(h,α) G(v,l,α) F(v,h,f0) } / { 4 (n⋅v) (n⋅l) }
fn specular(
input: ptr<function, LightingInput>,
derived_input: ptr<function, DerivedLightingInput>,
roughness: f32,
specular_intensity: f32,
) -> vec3<f32> {
// Unpack.
let roughness = (*input).layers[LAYER_BASE].roughness;
let NdotV = (*input).layers[LAYER_BASE].NdotV;
let F0 = (*input).F0_;
let NdotL = (*derived_input).NdotL;
Expand Down Expand Up @@ -425,10 +428,10 @@ fn specular_clearcoat(
input: ptr<function, LightingInput>,
derived_input: ptr<function, DerivedLightingInput>,
clearcoat_strength: f32,
roughness: f32,
specular_intensity: f32,
) -> vec2<f32> {
// Unpack.
let roughness = (*input).layers[LAYER_CLEARCOAT].roughness;
let NdotH = (*derived_input).NdotH;
let LdotH = (*derived_input).LdotH;

Expand All @@ -449,10 +452,10 @@ fn specular_anisotropy(
input: ptr<function, LightingInput>,
derived_input: ptr<function, DerivedLightingInput>,
L: vec3<f32>,
roughness: f32,
specular_intensity: f32,
) -> vec3<f32> {
// Unpack.
let roughness = (*input).layers[LAYER_BASE].roughness;
let NdotV = (*input).layers[LAYER_BASE].NdotV;
let V = (*input).V;
let F0 = (*input).F0_;
Expand Down Expand Up @@ -620,25 +623,37 @@ fn point_light(
let light_to_frag = (*light).position_radius.xyz - P;
let L = normalize(light_to_frag);
let distance_square = dot(light_to_frag, light_to_frag);
let distance = sqrt(distance_square);
let rangeAttenuation = getDistanceAttenuation(distance_square, (*light).color_inverse_square_range.w);

// Base layer

let specular_L_intensity = compute_specular_layer_values_for_point_light(
let a = (*input).layers[LAYER_BASE].roughness;
let specular_L_a_prime = compute_specular_layer_values_for_point_light(
input,
LAYER_BASE,
V,
light_to_frag,
(*light).position_radius.w,
distance,
);
var specular_derived_input = derive_lighting_input(N, V, specular_L_intensity.xyz);
let L_spec = specular_L_a_prime.xyz;
let a_prime = specular_L_a_prime.w;
var specular_derived_input = derive_lighting_input(N, V, L_spec);

let specular_intensity = specular_L_intensity.w;
let normalizationFactor = a / a_prime;
let specular_intensity = normalizationFactor * normalizationFactor;

// This is a modification to Karis 2013 for area lights to fix an issue where the specular reflection on smooth materials
// looks too rough and dim. We lerp between the base roughness and Karis2013 roughness with a lerp factor tuned by looking at reference renders.
// The goal is to preserve sharp specular highlights on smooth materials, without blowing out specular highlights on rough materials.
let lerp = 1.0 - (1.0 - a) * (1.0 - a) * (1.0 - a) * (1.0 - a);
let brdf_roughness = mix(a, a_prime, lerp);

#ifdef STANDARD_MATERIAL_ANISOTROPY
let specular_light = specular_anisotropy(input, &specular_derived_input, L, specular_intensity);
let specular_light = specular_anisotropy(input, &specular_derived_input, L, brdf_roughness, specular_intensity);
#else // STANDARD_MATERIAL_ANISOTROPY
let specular_light = specular(input, &specular_derived_input, specular_intensity);
let specular_light = specular(input, &specular_derived_input, brdf_roughness, specular_intensity);
#endif // STANDARD_MATERIAL_ANISOTROPY

// Clearcoat
Expand All @@ -647,26 +662,40 @@ fn point_light(
// Unpack.
let clearcoat_N = (*input).layers[LAYER_CLEARCOAT].N;
let clearcoat_strength = (*input).clearcoat_strength;
let clearcoat_a = (*input).layers[LAYER_CLEARCOAT].roughness;

// Perform specular input calculations again for the clearcoat layer. We
// can't reuse the above because the clearcoat normal might be different
// from the main layer normal.
let clearcoat_specular_L_intensity = compute_specular_layer_values_for_point_light(
let clearcoat_specular_L_a_prime = compute_specular_layer_values_for_point_light(
input,
LAYER_CLEARCOAT,
V,
light_to_frag,
(*light).position_radius.w,
distance,
);
let L_clearcoat_spec = clearcoat_specular_L_a_prime.xyz;
let clearcoat_a_prime = clearcoat_specular_L_a_prime.w;
var clearcoat_specular_derived_input =
derive_lighting_input(clearcoat_N, V, clearcoat_specular_L_intensity.xyz);
derive_lighting_input(clearcoat_N, V, L_clearcoat_spec);

// Calculate the specular light.
let clearcoat_specular_intensity = clearcoat_specular_L_intensity.w;
let clearcoat_normalizationFactor = clearcoat_a / clearcoat_a_prime;

let clearcoat_specular_intensity = clearcoat_normalizationFactor * clearcoat_normalizationFactor;

// This is a modification to Karis 2013 for area lights to fix an issue where the specular reflection on smooth materials
// looks too rough and dim. We lerp between the base roughness and Karis2013 roughness with a lerp factor tuned by looking at reference renders.
// The goal is to preserve sharp specular highlights on smooth materials, without blowing out specular highlights on rough materials.
let cc_lerp = 1.0 - (1.0 - clearcoat_a) * (1.0 - clearcoat_a) * (1.0 - clearcoat_a) * (1.0 - clearcoat_a);
let clearcoat_brdf_roughness = mix(clearcoat_a, clearcoat_a_prime, cc_lerp);

let Fc_Frc = specular_clearcoat(
input,
&clearcoat_specular_derived_input,
clearcoat_strength,
clearcoat_brdf_roughness,
clearcoat_specular_intensity
);
let inv_Fc = 1.0 - Fc_Frc.r; // Inverse Fresnel term.
Expand Down Expand Up @@ -694,14 +723,14 @@ fn point_light(

// NOTE: (*light).color.rgb is premultiplied with (*light).intensity / 4 π (which would be the luminous intensity) on the CPU

var color: vec3<f32>;
var color_times_NdotL: vec3<f32>;
#ifdef STANDARD_MATERIAL_CLEARCOAT
// Account for the Fresnel term from the clearcoat darkening the main layer.
//
// <https://google.github.io/filament/Filament.html#materialsystem/clearcoatmodel/integrationinthesurfaceresponse>
color = (diffuse + specular_light * inv_Fc) * inv_Fc + Frc;
color_times_NdotL = (diffuse * derived_input.NdotL + specular_light * specular_derived_input.NdotL * inv_Fc) * inv_Fc + Frc * clearcoat_specular_derived_input.NdotL;
#else // STANDARD_MATERIAL_CLEARCOAT
color = diffuse + specular_light;
color_times_NdotL = diffuse * derived_input.NdotL + specular_light * specular_derived_input.NdotL;
#endif // STANDARD_MATERIAL_CLEARCOAT

var texture_sample = 1f;
Expand All @@ -722,8 +751,8 @@ fn point_light(
}
#endif

return color * (*light).color_inverse_square_range.rgb *
(rangeAttenuation * derived_input.NdotL) * texture_sample;
return color_times_NdotL * (*light).color_inverse_square_range.rgb *
rangeAttenuation * texture_sample;
}

fn spot_light(
Expand Down Expand Up @@ -797,22 +826,23 @@ fn directional_light(
}

#ifdef STANDARD_MATERIAL_ANISOTROPY
let specular_light = specular_anisotropy(input, &derived_input, L, 1.0);
let specular_light = specular_anisotropy(input, &derived_input, L, roughness, 1.0);
#else // STANDARD_MATERIAL_ANISOTROPY
let specular_light = specular(input, &derived_input, 1.0);
let specular_light = specular(input, &derived_input, roughness, 1.0);
#endif // STANDARD_MATERIAL_ANISOTROPY

#ifdef STANDARD_MATERIAL_CLEARCOAT
let clearcoat_N = (*input).layers[LAYER_CLEARCOAT].N;
let clearcoat_strength = (*input).clearcoat_strength;
let clearcoat_roughness = (*input).layers[LAYER_CLEARCOAT].roughness;

// Perform specular input calculations again for the clearcoat layer. We
// can't reuse the above because the clearcoat normal might be different
// from the main layer normal.
var derived_clearcoat_input = derive_lighting_input(clearcoat_N, V, L);

let Fc_Frc =
specular_clearcoat(input, &derived_clearcoat_input, clearcoat_strength, 1.0);
specular_clearcoat(input, &derived_clearcoat_input, clearcoat_strength, clearcoat_roughness, 1.0);
let inv_Fc = 1.0 - Fc_Frc.r;
let Frc = Fc_Frc.g;
#endif // STANDARD_MATERIAL_CLEARCOAT
Expand Down
22 changes: 18 additions & 4 deletions crates/bevy_pbr/src/ssr/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,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 +37,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 +50,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 Down Expand Up @@ -240,7 +243,7 @@ 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,
perceptual_roughness_threshold: 1.0,
linear_steps: 16,
bisection_steps: 4,
use_secant: true,
Expand Down Expand Up @@ -293,6 +296,15 @@ 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 stbn_texture = render_images.get(&bluenoise.texture).unwrap();
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 +313,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 +376,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
Loading
Loading