Skip to main content

bevy_pbr/ssr/
mod.rs

1//! Screen space reflections implemented via raymarching.
2
3use core::ops::Range;
4
5use bevy_app::{App, Plugin};
6use bevy_asset::{load_embedded_asset, AssetServer, Handle};
7use bevy_core_pipeline::{
8    core_3d::{main_opaque_pass_3d, DEPTH_PREPASS_TEXTURE_SUPPORTED},
9    prepass::{DeferredPrepass, DepthPrepass},
10    schedule::{Core3d, Core3dSystems},
11    FullscreenShader,
12};
13use bevy_derive::{Deref, DerefMut};
14use bevy_ecs::{
15    component::Component,
16    entity::Entity,
17    query::{QueryItem, With},
18    reflect::ReflectComponent,
19    resource::Resource,
20    schedule::IntoScheduleConfigs as _,
21    system::{lifetimeless::Read, Commands, Query, Res, ResMut},
22};
23use bevy_reflect::{std_traits::ReflectDefault, Reflect};
24use bevy_render::{
25    diagnostic::RecordDiagnostics,
26    extract_component::{ExtractComponent, ExtractComponentPlugin},
27    render_asset::RenderAssets,
28    render_resource::{
29        binding_types, AddressMode, BindGroupEntries, BindGroupLayoutDescriptor,
30        BindGroupLayoutEntries, CachedRenderPipelineId, ColorTargetState, ColorWrites,
31        DynamicUniformBuffer, FilterMode, FragmentState, Operations, PipelineCache,
32        RenderPassColorAttachment, RenderPassDescriptor, RenderPipelineDescriptor, Sampler,
33        SamplerBindingType, SamplerDescriptor, ShaderStages, ShaderType, SpecializedRenderPipeline,
34        SpecializedRenderPipelines, TextureFormat, TextureSampleType, TextureViewDescriptor,
35        TextureViewDimension,
36    },
37    renderer::{RenderAdapter, RenderContext, RenderDevice, RenderQueue, ViewQuery},
38    sync_component::SyncComponent,
39    texture::GpuImage,
40    view::{ExtractedView, ViewTarget},
41    GpuResourceAppExt, Render, RenderApp, RenderStartup, RenderSystems,
42};
43use bevy_shader::{load_shader_library, Shader};
44use bevy_utils::{once, prelude::default};
45use tracing::info;
46
47use crate::{
48    binding_arrays_are_usable, deferred::deferred_lighting, Bluenoise, MeshPipelineSystems,
49    MeshPipelineViewLayoutKey, MeshPipelineViewLayouts, MeshViewBindGroup, ViewKeyCache,
50};
51
52/// Enables screen-space reflections for a camera.
53///
54/// Screen-space reflections are currently only supported with deferred rendering.
55pub struct ScreenSpaceReflectionsPlugin;
56
57/// Add this component to a camera to enable *screen-space reflections* (SSR).
58///
59/// Screen-space reflections currently require deferred rendering in order to
60/// appear. Therefore, they also need the [`DepthPrepass`] and [`DeferredPrepass`]
61/// components, which are inserted automatically,
62/// but deferred rendering itself is not automatically enabled.
63///
64/// Enable the `bluenoise_texture` feature to improve the quality of noise on rough reflections.
65///
66/// As with all screen-space techniques, SSR can only reflect objects on screen.
67/// When objects leave the camera, they will disappear from reflections.
68/// An alternative that doesn't suffer from this problem is the combination of
69/// a [`LightProbe`](bevy_light::LightProbe) and [`EnvironmentMapLight`](bevy_light::EnvironmentMapLight).
70/// The advantage of SSR is that it can reflect all objects, not just static ones.
71///
72/// SSR is an approximation technique and produces artifacts in some situations.
73/// Hand-tuning the settings in this component will likely be useful.
74///
75/// Screen-space reflections are presently unsupported on WebGL 2 because of a
76/// bug whereby Naga doesn't generate correct GLSL when sampling depth buffers,
77/// which is required for screen-space raymarching.
78#[derive(Clone, Component, Reflect)]
79#[reflect(Component, Default, Clone)]
80#[require(DepthPrepass, DeferredPrepass)]
81#[doc(alias = "Ssr")]
82pub struct ScreenSpaceReflections {
83    /// The perceptual roughness range over which SSR begins to fade in.
84    ///
85    /// The first value is the roughness at which SSR begins to appear; the
86    /// second value is the roughness at which SSR is fully active.
87    pub min_perceptual_roughness: Range<f32>,
88
89    /// The perceptual roughness range over which SSR begins to fade out.
90    ///
91    /// The first value is the roughness at which SSR begins to fade out; the
92    /// second value is the roughness at which SSR is no longer active.
93    pub max_perceptual_roughness: Range<f32>,
94
95    /// When marching the depth buffer, we only have 2.5D information and don't
96    /// know how thick surfaces are. We shall assume that the depth buffer
97    /// fragments are cuboids with a constant thickness defined by this
98    /// parameter.
99    pub thickness: f32,
100
101    /// The number of steps to be taken at regular intervals to find an initial
102    /// intersection. Must not be zero.
103    ///
104    /// Higher values result in higher-quality reflections, because the
105    /// raymarching shader is less likely to miss objects. However, they take
106    /// more GPU time.
107    pub linear_steps: u32,
108
109    /// Exponent to be applied in the linear part of the march.
110    ///
111    /// A value of 1.0 will result in equidistant steps, and higher values will
112    /// compress the earlier steps, and expand the later ones. This might be
113    /// desirable in order to get more detail close to objects.
114    ///
115    /// For optimal performance, this should be a small unsigned integer, such
116    /// as 1 or 2.
117    pub linear_march_exponent: f32,
118
119    /// The range over which SSR begins to fade out at the edges of the screen,
120    /// in terms of a percentage of the screen dimensions.
121    ///
122    /// The first value is the percentage from the edge at which SSR is no
123    /// longer active; the second value is the percentage at which SSR is fully
124    /// active.
125    pub edge_fadeout: Range<f32>,
126
127    /// Number of steps in a bisection (binary search) to perform once the
128    /// linear search has found an intersection. Helps narrow down the hit,
129    /// increasing the chance of the secant method finding an accurate hit
130    /// point.
131    pub bisection_steps: u32,
132
133    /// Approximate the root position using the secant method—by solving for
134    /// line-line intersection between the ray approach rate and the surface
135    /// gradient.
136    pub use_secant: bool,
137}
138
139/// A version of [`ScreenSpaceReflections`] for upload to the GPU.
140///
141/// For more information on these fields, see the corresponding documentation in
142/// [`ScreenSpaceReflections`].
143#[derive(Clone, Copy, Component, ShaderType)]
144pub struct ScreenSpaceReflectionsUniform {
145    min_perceptual_roughness: f32,
146    min_perceptual_roughness_fully_active: f32,
147    max_perceptual_roughness_starts_to_fade: f32,
148    max_perceptual_roughness: f32,
149    edge_fadeout_fully_active: f32,
150    edge_fadeout_no_longer_active: f32,
151    thickness: f32,
152    linear_steps: u32,
153    linear_march_exponent: f32,
154    bisection_steps: u32,
155    /// A boolean converted to a `u32`.
156    use_secant: u32,
157}
158
159/// Identifies which screen space reflections render pipeline a view needs.
160#[derive(Component, Deref, DerefMut)]
161pub struct ScreenSpaceReflectionsPipelineId(pub CachedRenderPipelineId);
162
163/// Information relating to the render pipeline for the screen space reflections
164/// shader.
165#[derive(Resource)]
166pub struct ScreenSpaceReflectionsPipeline {
167    mesh_view_layouts: MeshPipelineViewLayouts,
168    color_sampler: Sampler,
169    depth_linear_sampler: Sampler,
170    depth_nearest_sampler: Sampler,
171    bind_group_layout: BindGroupLayoutDescriptor,
172    binding_arrays_are_usable: bool,
173    fullscreen_shader: FullscreenShader,
174    fragment_shader: Handle<Shader>,
175}
176
177/// A GPU buffer that stores the screen space reflection settings for each view.
178#[derive(Resource, Default, Deref, DerefMut)]
179pub struct ScreenSpaceReflectionsBuffer(pub DynamicUniformBuffer<ScreenSpaceReflectionsUniform>);
180
181/// A component that stores the offset within the
182/// [`ScreenSpaceReflectionsBuffer`] for each view.
183#[derive(Component, Default, Deref, DerefMut)]
184pub struct ViewScreenSpaceReflectionsUniformOffset(u32);
185
186/// Identifies a specific configuration of the SSR pipeline shader.
187#[derive(Clone, Copy, PartialEq, Eq, Hash)]
188pub struct ScreenSpaceReflectionsPipelineKey {
189    mesh_pipeline_view_key: MeshPipelineViewLayoutKey,
190    target_format: TextureFormat,
191}
192
193impl Plugin for ScreenSpaceReflectionsPlugin {
194    fn build(&self, app: &mut App) {
195        load_shader_library!(app, "ssr.wgsl");
196        load_shader_library!(app, "raymarch.wgsl");
197
198        app.add_plugins(ExtractComponentPlugin::<ScreenSpaceReflections>::default());
199
200        let Some(render_app) = app.get_sub_app_mut(RenderApp) else {
201            return;
202        };
203
204        render_app
205            .init_gpu_resource::<ScreenSpaceReflectionsBuffer>()
206            .init_gpu_resource::<SpecializedRenderPipelines<ScreenSpaceReflectionsPipeline>>()
207            .add_systems(
208                RenderStartup,
209                init_screen_space_reflections_pipeline.after(MeshPipelineSystems),
210            )
211            .add_systems(Render, prepare_ssr_pipelines.in_set(RenderSystems::Prepare))
212            .add_systems(
213                Render,
214                prepare_ssr_settings.in_set(RenderSystems::PrepareResources),
215            )
216            .add_systems(
217                Core3d,
218                screen_space_reflections
219                    .after(deferred_lighting)
220                    .before(main_opaque_pass_3d)
221                    .in_set(Core3dSystems::MainPass),
222            );
223    }
224}
225
226impl Default for ScreenSpaceReflections {
227    // Reasonable default values.
228    //
229    // These are from
230    // <https://gist.github.com/h3r2tic/9c8356bdaefbe80b1a22ae0aaee192db?permalink_comment_id=4552149#gistcomment-4552149>.
231    fn default() -> Self {
232        Self {
233            min_perceptual_roughness: 0.08..0.12,
234            max_perceptual_roughness: 0.55..0.6,
235            linear_steps: 10,
236            bisection_steps: 5,
237            use_secant: true,
238            thickness: 0.25,
239            linear_march_exponent: 1.0,
240            edge_fadeout: 0.0..0.0,
241        }
242    }
243}
244
245pub fn screen_space_reflections(
246    view: ViewQuery<(
247        &ViewTarget,
248        &MeshViewBindGroup,
249        &ScreenSpaceReflectionsPipelineId,
250    )>,
251    pipeline_cache: Res<PipelineCache>,
252    ssr_pipeline: Res<ScreenSpaceReflectionsPipeline>,
253    bluenoise: Res<Bluenoise>,
254    render_images: Res<RenderAssets<GpuImage>>,
255    mut ctx: RenderContext,
256) {
257    let (view_target, view_bind_group, ssr_pipeline_id) = view.into_inner();
258
259    // Grab the render pipeline.
260    let Some(render_pipeline) = pipeline_cache.get_render_pipeline(**ssr_pipeline_id) else {
261        return;
262    };
263
264    // Set up a standard pair of postprocessing textures.
265    let postprocess = view_target.post_process_write();
266
267    // Get blue noise texture for SSR.
268    let Some(stbn_texture) = render_images.get(&bluenoise.texture) else {
269        return;
270    };
271    let stbn_view = stbn_texture.texture.create_view(&TextureViewDescriptor {
272        label: Some("ssr_stbn_view"),
273        dimension: Some(TextureViewDimension::D2Array),
274        ..default()
275    });
276
277    // Create the bind group for this view.
278    let ssr_bind_group = ctx.render_device().create_bind_group(
279        "SSR bind group",
280        &pipeline_cache.get_bind_group_layout(&ssr_pipeline.bind_group_layout),
281        &BindGroupEntries::sequential((
282            postprocess.source,
283            &ssr_pipeline.color_sampler,
284            &ssr_pipeline.depth_linear_sampler,
285            &ssr_pipeline.depth_nearest_sampler,
286            &stbn_view,
287        )),
288    );
289
290    let diagnostics = ctx.diagnostic_recorder();
291    let diagnostics = diagnostics.as_deref();
292
293    // Build the SSR render pass.
294    let mut render_pass = ctx.begin_tracked_render_pass(RenderPassDescriptor {
295        label: Some("ssr"),
296        color_attachments: &[Some(RenderPassColorAttachment {
297            view: postprocess.destination,
298            depth_slice: None,
299            resolve_target: None,
300            ops: Operations::default(),
301        })],
302        depth_stencil_attachment: None,
303        timestamp_writes: None,
304        occlusion_query_set: None,
305        multiview_mask: None,
306    });
307    let pass_span = diagnostics.pass_span(&mut render_pass, "ssr");
308
309    // Set bind groups.
310    render_pass.set_render_pipeline(render_pipeline);
311
312    render_pass.set_bind_group(0, &view_bind_group.main, &view_bind_group.main_offsets);
313    render_pass.set_bind_group(1, &view_bind_group.binding_array, &[]);
314
315    // Perform the SSR render pass.
316    render_pass.set_bind_group(2, &ssr_bind_group, &[]);
317    render_pass.draw(0..3, 0..1);
318
319    pass_span.end(&mut render_pass);
320}
321
322pub fn init_screen_space_reflections_pipeline(
323    mut commands: Commands,
324    render_device: Res<RenderDevice>,
325    render_adapter: Res<RenderAdapter>,
326    mesh_view_layouts: Res<MeshPipelineViewLayouts>,
327    fullscreen_shader: Res<FullscreenShader>,
328    asset_server: Res<AssetServer>,
329) {
330    // Create the bind group layout.
331    let bind_group_layout = BindGroupLayoutDescriptor::new(
332        "SSR bind group layout",
333        &BindGroupLayoutEntries::sequential(
334            ShaderStages::FRAGMENT,
335            (
336                binding_types::texture_2d(TextureSampleType::Float { filterable: true }),
337                binding_types::sampler(SamplerBindingType::Filtering),
338                binding_types::sampler(SamplerBindingType::Filtering),
339                binding_types::sampler(SamplerBindingType::NonFiltering),
340                binding_types::texture_2d_array(TextureSampleType::Float { filterable: false }),
341            ),
342        ),
343    );
344
345    // Create the samplers we need.
346
347    let color_sampler = render_device.create_sampler(&SamplerDescriptor {
348        label: "SSR color sampler".into(),
349        address_mode_u: AddressMode::ClampToEdge,
350        address_mode_v: AddressMode::ClampToEdge,
351        mag_filter: FilterMode::Linear,
352        min_filter: FilterMode::Linear,
353        ..default()
354    });
355
356    let depth_linear_sampler = render_device.create_sampler(&SamplerDescriptor {
357        label: "SSR depth linear sampler".into(),
358        address_mode_u: AddressMode::ClampToEdge,
359        address_mode_v: AddressMode::ClampToEdge,
360        mag_filter: FilterMode::Linear,
361        min_filter: FilterMode::Linear,
362        ..default()
363    });
364
365    let depth_nearest_sampler = render_device.create_sampler(&SamplerDescriptor {
366        label: "SSR depth nearest sampler".into(),
367        address_mode_u: AddressMode::ClampToEdge,
368        address_mode_v: AddressMode::ClampToEdge,
369        mag_filter: FilterMode::Nearest,
370        min_filter: FilterMode::Nearest,
371        ..default()
372    });
373
374    commands.insert_resource(ScreenSpaceReflectionsPipeline {
375        mesh_view_layouts: mesh_view_layouts.clone(),
376        color_sampler,
377        depth_linear_sampler,
378        depth_nearest_sampler,
379        bind_group_layout,
380        binding_arrays_are_usable: binding_arrays_are_usable(&render_device, &render_adapter),
381        fullscreen_shader: fullscreen_shader.clone(),
382        // Even though ssr was loaded using load_shader_library, we can still access it like a
383        // normal embedded asset (so we can use it as both a library or a kernel).
384        fragment_shader: load_embedded_asset!(asset_server.as_ref(), "ssr.wgsl"),
385    });
386}
387
388/// Sets up screen space reflection pipelines for each applicable view.
389pub fn prepare_ssr_pipelines(
390    mut commands: Commands,
391    pipeline_cache: Res<PipelineCache>,
392    view_key_cache: Res<ViewKeyCache>,
393    mut pipelines: ResMut<SpecializedRenderPipelines<ScreenSpaceReflectionsPipeline>>,
394    ssr_pipeline: Res<ScreenSpaceReflectionsPipeline>,
395    views: Query<
396        (Entity, &ExtractedView),
397        (
398            With<ScreenSpaceReflectionsUniform>,
399            With<DepthPrepass>,
400            With<DeferredPrepass>,
401        ),
402    >,
403) {
404    for (entity, extracted_view) in &views {
405        let Some(view_key) = view_key_cache.get(&extracted_view.retained_view_entity) else {
406            continue;
407        };
408        // Build the pipeline.
409        let pipeline_id = pipelines.specialize(
410            &pipeline_cache,
411            &ssr_pipeline,
412            ScreenSpaceReflectionsPipelineKey {
413                mesh_pipeline_view_key: (*view_key).into(),
414                target_format: extracted_view.target_format,
415            },
416        );
417
418        // Note which pipeline ID was used.
419        commands
420            .entity(entity)
421            .insert(ScreenSpaceReflectionsPipelineId(pipeline_id));
422    }
423}
424
425/// Gathers up screen space reflection settings for each applicable view and
426/// writes them into a GPU buffer.
427pub fn prepare_ssr_settings(
428    mut commands: Commands,
429    views: Query<(Entity, &ScreenSpaceReflectionsUniform), With<ExtractedView>>,
430    mut ssr_settings_buffer: ResMut<ScreenSpaceReflectionsBuffer>,
431    render_device: Res<RenderDevice>,
432    render_queue: Res<RenderQueue>,
433) {
434    let Some(mut writer) =
435        ssr_settings_buffer.get_writer(views.iter().len(), &render_device, &render_queue)
436    else {
437        return;
438    };
439
440    for (view, ssr_uniform) in views.iter() {
441        let uniform_offset = writer.write(ssr_uniform);
442        commands
443            .entity(view)
444            .insert(ViewScreenSpaceReflectionsUniformOffset(uniform_offset));
445    }
446}
447
448impl SyncComponent for ScreenSpaceReflections {
449    type Target = (
450        ScreenSpaceReflectionsUniform,
451        ViewScreenSpaceReflectionsUniformOffset,
452        ScreenSpaceReflectionsPipelineId,
453    );
454}
455
456impl ExtractComponent for ScreenSpaceReflections {
457    type QueryData = Read<ScreenSpaceReflections>;
458    type QueryFilter = ();
459    type Out = ScreenSpaceReflectionsUniform;
460
461    fn extract_component(settings: QueryItem<'_, '_, Self::QueryData>) -> Option<Self::Out> {
462        if !DEPTH_PREPASS_TEXTURE_SUPPORTED {
463            once!(info!(
464                "Disabling screen-space reflections on this platform because depth textures \
465                aren't supported correctly"
466            ));
467            return None;
468        }
469
470        Some(settings.clone().into())
471    }
472}
473
474impl SpecializedRenderPipeline for ScreenSpaceReflectionsPipeline {
475    type Key = ScreenSpaceReflectionsPipelineKey;
476
477    fn specialize(&self, key: Self::Key) -> RenderPipelineDescriptor {
478        let layout = self
479            .mesh_view_layouts
480            .get_view_layout(key.mesh_pipeline_view_key);
481        let layout = vec![
482            layout.main_layout,
483            layout.binding_array_layout,
484            self.bind_group_layout.clone(),
485        ];
486
487        let mut shader_defs = vec![
488            "DEPTH_PREPASS".into(),
489            "DEFERRED_PREPASS".into(),
490            "SCREEN_SPACE_REFLECTIONS".into(),
491        ];
492
493        if key
494            .mesh_pipeline_view_key
495            .contains(MeshPipelineViewLayoutKey::ENVIRONMENT_MAP)
496        {
497            shader_defs.push("ENVIRONMENT_MAP".into());
498        }
499
500        if self.binding_arrays_are_usable {
501            shader_defs.push("MULTIPLE_LIGHT_PROBES_IN_ARRAY".into());
502        }
503
504        if key
505            .mesh_pipeline_view_key
506            .contains(MeshPipelineViewLayoutKey::ATMOSPHERE)
507        {
508            shader_defs.push("ATMOSPHERE".into());
509        }
510
511        if cfg!(feature = "bluenoise_texture") {
512            shader_defs.push("BLUE_NOISE_TEXTURE".into());
513        }
514        if cfg!(feature = "dfg_lut") {
515            shader_defs.push("DFG_LUT".into());
516        }
517        if cfg!(feature = "area_light_luts") {
518            shader_defs.push("AREA_LIGHT_LUTS".into());
519        }
520
521        #[cfg(not(target_arch = "wasm32"))]
522        shader_defs.push("USE_DEPTH_SAMPLERS".into());
523
524        RenderPipelineDescriptor {
525            label: Some("SSR pipeline".into()),
526            layout,
527            vertex: self.fullscreen_shader.to_vertex_state(),
528            fragment: Some(FragmentState {
529                shader: self.fragment_shader.clone(),
530                shader_defs,
531                targets: vec![Some(ColorTargetState {
532                    format: key.target_format,
533                    blend: None,
534                    write_mask: ColorWrites::ALL,
535                })],
536                ..default()
537            }),
538            ..default()
539        }
540    }
541}
542
543impl From<ScreenSpaceReflections> for ScreenSpaceReflectionsUniform {
544    fn from(settings: ScreenSpaceReflections) -> Self {
545        Self {
546            min_perceptual_roughness: settings.min_perceptual_roughness.start,
547            min_perceptual_roughness_fully_active: settings.min_perceptual_roughness.end,
548            max_perceptual_roughness_starts_to_fade: settings.max_perceptual_roughness.start,
549            max_perceptual_roughness: settings.max_perceptual_roughness.end,
550            edge_fadeout_no_longer_active: settings.edge_fadeout.start,
551            edge_fadeout_fully_active: settings.edge_fadeout.end,
552            thickness: settings.thickness,
553            linear_steps: settings.linear_steps,
554            linear_march_exponent: settings.linear_march_exponent,
555            bisection_steps: settings.bisection_steps,
556            use_secant: settings.use_secant as u32,
557        }
558    }
559}