Skip to content

Commit edde626

Browse files
authored
Add support for SSR on web (bevyengine#22193)
# Objective - Implemented WebGPU safe SSR (screen-space reflections) while preserving native behavior - Fixes bevyengine#21700 ## Solution - Kept native SSR unchanged by defining `USE_DEPTH_SAMPLERS` shader def on native - the pipeline still binds filtered depth samplers and uses `textureSampleLevel` on depth - added manual depth sampling in `ssr/raymarch.wgsl` via `textureLoad` with clamped bilinear and nearest helpers - This avoids the validation error previously happening on web (documented in the linked issue above) ## Testing - Ran the ssr example on native and web using the bevy CLI - Ran the atmosphere example on native and web --- ## Showcase Atmosphere example running (fixed) using deferred rendering + SSR on web browser (chrome) <img width="1388" height="1140" alt="Screenshot 2025-12-18 at 3 26 21 PM" src="https://github.com/user-attachments/assets/bc9484c1-e3f2-4adf-a740-c0a7c3312a8d" />
1 parent a8f19a2 commit edde626

File tree

2 files changed

+54
-4
lines changed

2 files changed

+54
-4
lines changed

crates/bevy_pbr/src/ssr/mod.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -552,6 +552,9 @@ impl SpecializedRenderPipeline for ScreenSpaceReflectionsPipeline {
552552
shader_defs.push("ATMOSPHERE".into());
553553
}
554554

555+
#[cfg(not(target_arch = "wasm32"))]
556+
shader_defs.push("USE_DEPTH_SAMPLERS".into());
557+
555558
RenderPipelineDescriptor {
556559
label: Some("SSR pipeline".into()),
557560
layout,

crates/bevy_pbr/src/ssr/raymarch.wgsl

Lines changed: 51 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,11 +24,60 @@
2424
position_world_to_ndc,
2525
}
2626

27+
#ifdef USE_DEPTH_SAMPLERS
2728
// Allows us to sample from the depth buffer with bilinear filtering.
2829
@group(2) @binding(2) var depth_linear_sampler: sampler;
2930

3031
// Allows us to sample from the depth buffer with nearest-neighbor filtering.
3132
@group(2) @binding(3) var depth_nearest_sampler: sampler;
33+
#endif
34+
35+
// Manual depth fetch helpers used on WebGPU where depth + filtering sampler is invalid.
36+
#ifndef USE_DEPTH_SAMPLERS
37+
fn depth_texel_clamped(texel: vec2<i32>) -> f32 {
38+
let dims = textureDimensions(depth_prepass_texture);
39+
let max_coord = vec2<i32>(i32(dims.x) - 1, i32(dims.y) - 1);
40+
let clamped = clamp(texel, vec2<i32>(0), max_coord);
41+
return textureLoad(depth_prepass_texture, clamped, 0);
42+
}
43+
44+
fn depth_sample_nearest_clamped(uv: vec2<f32>, tex_size: vec2<f32>) -> f32 {
45+
// Match nearest sampling by snapping to the closest texel center.
46+
let coord = uv * tex_size - vec2(0.5);
47+
return depth_texel_clamped(vec2<i32>(floor(coord + vec2(0.5))));
48+
}
49+
50+
fn depth_sample_bilinear_clamped(uv: vec2<f32>, tex_size: vec2<f32>) -> f32 {
51+
let coord = uv * tex_size - vec2(0.5);
52+
let base = vec2<i32>(floor(coord));
53+
let frac = coord - floor(coord);
54+
55+
let d00 = depth_texel_clamped(base);
56+
let d10 = depth_texel_clamped(base + vec2(1, 0));
57+
let d01 = depth_texel_clamped(base + vec2(0, 1));
58+
let d11 = depth_texel_clamped(base + vec2(1, 1));
59+
60+
let d0 = mix(d00, d10, frac.x);
61+
let d1 = mix(d01, d11, frac.x);
62+
return mix(d0, d1, frac.y);
63+
}
64+
#endif
65+
66+
fn depth_sample_linear(uv: vec2<f32>, tex_size: vec2<f32>) -> f32 {
67+
#ifdef USE_DEPTH_SAMPLERS
68+
return textureSampleLevel(depth_prepass_texture, depth_linear_sampler, uv, 0u);
69+
#else
70+
return depth_sample_bilinear_clamped(uv, tex_size);
71+
#endif
72+
}
73+
74+
fn depth_sample_nearest(uv: vec2<f32>, tex_size: vec2<f32>) -> f32 {
75+
#ifdef USE_DEPTH_SAMPLERS
76+
return textureSampleLevel(depth_prepass_texture, depth_nearest_sampler, uv, 0u);
77+
#else
78+
return depth_sample_nearest_clamped(uv, tex_size);
79+
#endif
80+
}
3281

3382
// Main code
3483

@@ -241,10 +290,8 @@ fn depth_raymarch_distance_fn_evaluate(
241290
// * The false occlusions due to duplo land are rejected because the ray stays above the smooth surface.
242291
// * The shrink-wrap surface is no longer continuous, so it's possible for rays to miss it.
243292

244-
let linear_depth =
245-
1.0 / textureSampleLevel(depth_prepass_texture, depth_linear_sampler, interp_uv, 0u);
246-
let unfiltered_depth =
247-
1.0 / textureSampleLevel(depth_prepass_texture, depth_nearest_sampler, interp_uv, 0u);
293+
let linear_depth = 1.0 / depth_sample_linear(interp_uv, (*distance_fn).depth_tex_size);
294+
let unfiltered_depth = 1.0 / depth_sample_nearest(interp_uv, (*distance_fn).depth_tex_size);
248295

249296
var max_depth: f32;
250297
var min_depth: f32;

0 commit comments

Comments
 (0)