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::{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, "raymarch.wesl");
196
197        app.add_plugins(ExtractComponentPlugin::<ScreenSpaceReflections>::default());
198
199        let Some(render_app) = app.get_sub_app_mut(RenderApp) else {
200            return;
201        };
202
203        render_app
204            .init_gpu_resource::<ScreenSpaceReflectionsBuffer>()
205            .init_gpu_resource::<SpecializedRenderPipelines<ScreenSpaceReflectionsPipeline>>()
206            .add_systems(
207                RenderStartup,
208                init_screen_space_reflections_pipeline.after(MeshPipelineSystems),
209            )
210            .add_systems(Render, prepare_ssr_pipelines.in_set(RenderSystems::Prepare))
211            .add_systems(
212                Render,
213                prepare_ssr_settings.in_set(RenderSystems::PrepareResources),
214            )
215            .add_systems(
216                Core3d,
217                screen_space_reflections
218                    .after(deferred_lighting)
219                    .before(main_opaque_pass_3d)
220                    .in_set(Core3dSystems::MainPass),
221            );
222    }
223}
224
225impl Default for ScreenSpaceReflections {
226    // Reasonable default values.
227    //
228    // These are from
229    // <https://gist.github.com/h3r2tic/9c8356bdaefbe80b1a22ae0aaee192db?permalink_comment_id=4552149#gistcomment-4552149>.
230    fn default() -> Self {
231        Self {
232            min_perceptual_roughness: 0.08..0.12,
233            max_perceptual_roughness: 0.55..0.6,
234            linear_steps: 10,
235            bisection_steps: 5,
236            use_secant: true,
237            thickness: 0.25,
238            linear_march_exponent: 1.0,
239            edge_fadeout: 0.0..0.0,
240        }
241    }
242}
243
244pub fn screen_space_reflections(
245    view: ViewQuery<(
246        &ViewTarget,
247        &MeshViewBindGroup,
248        &ScreenSpaceReflectionsPipelineId,
249    )>,
250    pipeline_cache: Res<PipelineCache>,
251    ssr_pipeline: Res<ScreenSpaceReflectionsPipeline>,
252    bluenoise: Res<Bluenoise>,
253    render_images: Res<RenderAssets<GpuImage>>,
254    mut ctx: RenderContext,
255) {
256    let (view_target, view_bind_group, ssr_pipeline_id) = view.into_inner();
257
258    // Grab the render pipeline.
259    let Some(render_pipeline) = pipeline_cache.get_render_pipeline(**ssr_pipeline_id) else {
260        return;
261    };
262
263    // Set up a standard pair of postprocessing textures.
264    let postprocess = view_target.post_process_write();
265
266    // Get blue noise texture for SSR.
267    let Some(stbn_texture) = render_images.get(&bluenoise.texture) else {
268        return;
269    };
270    let stbn_view = stbn_texture.texture.create_view(&TextureViewDescriptor {
271        label: Some("ssr_stbn_view"),
272        dimension: Some(TextureViewDimension::D2Array),
273        ..default()
274    });
275
276    // Create the bind group for this view.
277    let ssr_bind_group = ctx.render_device().create_bind_group(
278        "SSR bind group",
279        &pipeline_cache.get_bind_group_layout(&ssr_pipeline.bind_group_layout),
280        &BindGroupEntries::sequential((
281            postprocess.source,
282            &ssr_pipeline.color_sampler,
283            &ssr_pipeline.depth_linear_sampler,
284            &ssr_pipeline.depth_nearest_sampler,
285            &stbn_view,
286        )),
287    );
288
289    let diagnostics = ctx.diagnostic_recorder();
290    let diagnostics = diagnostics.as_deref();
291
292    // Build the SSR render pass.
293    let mut render_pass = ctx.begin_tracked_render_pass(RenderPassDescriptor {
294        label: Some("ssr"),
295        color_attachments: &[Some(RenderPassColorAttachment {
296            view: postprocess.destination,
297            depth_slice: None,
298            resolve_target: None,
299            ops: Operations::default(),
300        })],
301        depth_stencil_attachment: None,
302        timestamp_writes: None,
303        occlusion_query_set: None,
304        multiview_mask: None,
305    });
306    let pass_span = diagnostics.pass_span(&mut render_pass, "ssr");
307
308    // Set bind groups.
309    render_pass.set_render_pipeline(render_pipeline);
310
311    render_pass.set_bind_group(0, &view_bind_group.main, &view_bind_group.main_offsets);
312    render_pass.set_bind_group(1, &view_bind_group.binding_array, &[]);
313
314    // Perform the SSR render pass.
315    render_pass.set_bind_group(2, &ssr_bind_group, &[]);
316    render_pass.draw(0..3, 0..1);
317
318    pass_span.end(&mut render_pass);
319}
320
321pub fn init_screen_space_reflections_pipeline(
322    mut commands: Commands,
323    render_device: Res<RenderDevice>,
324    render_adapter: Res<RenderAdapter>,
325    mesh_view_layouts: Res<MeshPipelineViewLayouts>,
326    fullscreen_shader: Res<FullscreenShader>,
327    asset_server: Res<AssetServer>,
328) {
329    // Create the bind group layout.
330    let bind_group_layout = BindGroupLayoutDescriptor::new(
331        "SSR bind group layout",
332        &BindGroupLayoutEntries::sequential(
333            ShaderStages::FRAGMENT,
334            (
335                binding_types::texture_2d(TextureSampleType::Float { filterable: true }),
336                binding_types::sampler(SamplerBindingType::Filtering),
337                binding_types::sampler(SamplerBindingType::Filtering),
338                binding_types::sampler(SamplerBindingType::NonFiltering),
339                binding_types::texture_2d_array(TextureSampleType::Float { filterable: false }),
340            ),
341        ),
342    );
343
344    // Create the samplers we need.
345
346    let color_sampler = render_device.create_sampler(&SamplerDescriptor {
347        label: "SSR color sampler".into(),
348        address_mode_u: AddressMode::ClampToEdge,
349        address_mode_v: AddressMode::ClampToEdge,
350        mag_filter: FilterMode::Linear,
351        min_filter: FilterMode::Linear,
352        ..default()
353    });
354
355    let depth_linear_sampler = render_device.create_sampler(&SamplerDescriptor {
356        label: "SSR depth linear sampler".into(),
357        address_mode_u: AddressMode::ClampToEdge,
358        address_mode_v: AddressMode::ClampToEdge,
359        mag_filter: FilterMode::Linear,
360        min_filter: FilterMode::Linear,
361        ..default()
362    });
363
364    let depth_nearest_sampler = render_device.create_sampler(&SamplerDescriptor {
365        label: "SSR depth nearest sampler".into(),
366        address_mode_u: AddressMode::ClampToEdge,
367        address_mode_v: AddressMode::ClampToEdge,
368        mag_filter: FilterMode::Nearest,
369        min_filter: FilterMode::Nearest,
370        ..default()
371    });
372
373    commands.insert_resource(ScreenSpaceReflectionsPipeline {
374        mesh_view_layouts: mesh_view_layouts.clone(),
375        color_sampler,
376        depth_linear_sampler,
377        depth_nearest_sampler,
378        bind_group_layout,
379        binding_arrays_are_usable: binding_arrays_are_usable(&render_device, &render_adapter),
380        fullscreen_shader: fullscreen_shader.clone(),
381        // Even though ssr was loaded using load_shader_library, we can still access it like a
382        // normal embedded asset (so we can use it as both a library or a kernel).
383        fragment_shader: asset_server.load("embedded://bevy_pbr/ssr.wesl"),
384    });
385}
386
387/// Sets up screen space reflection pipelines for each applicable view.
388pub fn prepare_ssr_pipelines(
389    mut commands: Commands,
390    pipeline_cache: Res<PipelineCache>,
391    view_key_cache: Res<ViewKeyCache>,
392    mut pipelines: ResMut<SpecializedRenderPipelines<ScreenSpaceReflectionsPipeline>>,
393    ssr_pipeline: Res<ScreenSpaceReflectionsPipeline>,
394    views: Query<
395        (Entity, &ExtractedView),
396        (
397            With<ScreenSpaceReflectionsUniform>,
398            With<DepthPrepass>,
399            With<DeferredPrepass>,
400        ),
401    >,
402) {
403    for (entity, extracted_view) in &views {
404        let Some(view_key) = view_key_cache.get(&extracted_view.retained_view_entity) else {
405            continue;
406        };
407        // Build the pipeline.
408        let pipeline_id = pipelines.specialize(
409            &pipeline_cache,
410            &ssr_pipeline,
411            ScreenSpaceReflectionsPipelineKey {
412                mesh_pipeline_view_key: (*view_key).into(),
413                target_format: extracted_view.target_format,
414            },
415        );
416
417        // Note which pipeline ID was used.
418        commands
419            .entity(entity)
420            .insert(ScreenSpaceReflectionsPipelineId(pipeline_id));
421    }
422}
423
424/// Gathers up screen space reflection settings for each applicable view and
425/// writes them into a GPU buffer.
426pub fn prepare_ssr_settings(
427    mut commands: Commands,
428    views: Query<(Entity, &ScreenSpaceReflectionsUniform), With<ExtractedView>>,
429    mut ssr_settings_buffer: ResMut<ScreenSpaceReflectionsBuffer>,
430    render_device: Res<RenderDevice>,
431    render_queue: Res<RenderQueue>,
432) {
433    let Some(mut writer) =
434        ssr_settings_buffer.get_writer(views.iter().len(), &render_device, &render_queue)
435    else {
436        return;
437    };
438
439    for (view, ssr_uniform) in views.iter() {
440        let uniform_offset = writer.write(ssr_uniform);
441        commands
442            .entity(view)
443            .insert(ViewScreenSpaceReflectionsUniformOffset(uniform_offset));
444    }
445}
446
447impl SyncComponent<RenderApp> for ScreenSpaceReflections {
448    type Target = (
449        ScreenSpaceReflectionsUniform,
450        ViewScreenSpaceReflectionsUniformOffset,
451        ScreenSpaceReflectionsPipelineId,
452    );
453}
454
455impl ExtractComponent<RenderApp> for ScreenSpaceReflections {
456    type QueryData = Read<ScreenSpaceReflections>;
457    type QueryFilter = ();
458    type Out = ScreenSpaceReflectionsUniform;
459
460    fn extract_component(settings: QueryItem<'_, '_, Self::QueryData>) -> Option<Self::Out> {
461        if !DEPTH_PREPASS_TEXTURE_SUPPORTED {
462            once!(info!(
463                "Disabling screen-space reflections on this platform because depth textures \
464                aren't supported correctly"
465            ));
466            return None;
467        }
468
469        Some(settings.clone().into())
470    }
471}
472
473impl SpecializedRenderPipeline for ScreenSpaceReflectionsPipeline {
474    type Key = ScreenSpaceReflectionsPipelineKey;
475
476    fn specialize(&self, key: Self::Key) -> RenderPipelineDescriptor {
477        let layout = self
478            .mesh_view_layouts
479            .get_view_layout(key.mesh_pipeline_view_key);
480        let layout = vec![
481            layout.main_layout,
482            layout.binding_array_layout,
483            self.bind_group_layout.clone(),
484        ];
485
486        let mut shader_defs = vec![
487            "DEPTH_PREPASS".into(),
488            "DEFERRED_PREPASS".into(),
489            "SCREEN_SPACE_REFLECTIONS".into(),
490        ];
491
492        if key
493            .mesh_pipeline_view_key
494            .contains(MeshPipelineViewLayoutKey::ENVIRONMENT_MAP)
495        {
496            shader_defs.push("ENVIRONMENT_MAP".into());
497        }
498
499        if self.binding_arrays_are_usable {
500            shader_defs.push("MULTIPLE_LIGHT_PROBES_IN_ARRAY".into());
501        }
502
503        if key
504            .mesh_pipeline_view_key
505            .contains(MeshPipelineViewLayoutKey::ATMOSPHERE)
506        {
507            shader_defs.push("ATMOSPHERE".into());
508        }
509
510        if cfg!(feature = "bluenoise_texture") {
511            shader_defs.push("BLUE_NOISE_TEXTURE".into());
512        }
513        if cfg!(feature = "dfg_lut") {
514            shader_defs.push("DFG_LUT".into());
515        }
516        if cfg!(feature = "area_light_luts") {
517            shader_defs.push("AREA_LIGHT_LUTS".into());
518        }
519
520        #[cfg(not(target_arch = "wasm32"))]
521        shader_defs.push("USE_DEPTH_SAMPLERS".into());
522
523        RenderPipelineDescriptor {
524            label: Some("SSR pipeline".into()),
525            layout,
526            vertex: self.fullscreen_shader.to_vertex_state(),
527            fragment: Some(FragmentState {
528                shader: self.fragment_shader.clone(),
529                shader_defs,
530                targets: vec![Some(ColorTargetState {
531                    format: key.target_format,
532                    blend: None,
533                    write_mask: ColorWrites::ALL,
534                })],
535                ..default()
536            }),
537            ..default()
538        }
539    }
540}
541
542impl From<ScreenSpaceReflections> for ScreenSpaceReflectionsUniform {
543    fn from(settings: ScreenSpaceReflections) -> Self {
544        Self {
545            min_perceptual_roughness: settings.min_perceptual_roughness.start,
546            min_perceptual_roughness_fully_active: settings.min_perceptual_roughness.end,
547            max_perceptual_roughness_starts_to_fade: settings.max_perceptual_roughness.start,
548            max_perceptual_roughness: settings.max_perceptual_roughness.end,
549            edge_fadeout_no_longer_active: settings.edge_fadeout.start,
550            edge_fadeout_fully_active: settings.edge_fadeout.end,
551            thickness: settings.thickness,
552            linear_steps: settings.linear_steps,
553            linear_march_exponent: settings.linear_march_exponent,
554            bisection_steps: settings.bisection_steps,
555            use_secant: settings.use_secant as u32,
556        }
557    }
558}