bevy_pbr/ssr/
mod.rs

1//! Screen space reflections implemented via raymarching.
2
3use bevy_app::{App, Plugin};
4use bevy_asset::{load_internal_asset, weak_handle, Handle};
5use bevy_core_pipeline::{
6    core_3d::{
7        graph::{Core3d, Node3d},
8        DEPTH_TEXTURE_SAMPLING_SUPPORTED,
9    },
10    fullscreen_vertex_shader,
11    prepass::{DeferredPrepass, DepthPrepass, MotionVectorPrepass, NormalPrepass},
12};
13use bevy_derive::{Deref, DerefMut};
14use bevy_ecs::{
15    component::Component,
16    entity::Entity,
17    query::{Has, QueryItem, With},
18    reflect::ReflectComponent,
19    resource::Resource,
20    schedule::IntoScheduleConfigs as _,
21    system::{lifetimeless::Read, Commands, Query, Res, ResMut},
22    world::{FromWorld, World},
23};
24use bevy_image::BevyDefault as _;
25use bevy_reflect::{std_traits::ReflectDefault, Reflect};
26use bevy_render::render_graph::RenderGraph;
27use bevy_render::{
28    extract_component::{ExtractComponent, ExtractComponentPlugin},
29    render_graph::{NodeRunError, RenderGraphApp, RenderGraphContext, ViewNode, ViewNodeRunner},
30    render_resource::{
31        binding_types, AddressMode, BindGroupEntries, BindGroupLayout, BindGroupLayoutEntries,
32        CachedRenderPipelineId, ColorTargetState, ColorWrites, DynamicUniformBuffer, FilterMode,
33        FragmentState, Operations, PipelineCache, RenderPassColorAttachment, RenderPassDescriptor,
34        RenderPipelineDescriptor, Sampler, SamplerBindingType, SamplerDescriptor, Shader,
35        ShaderStages, ShaderType, SpecializedRenderPipeline, SpecializedRenderPipelines,
36        TextureFormat, TextureSampleType,
37    },
38    renderer::{RenderAdapter, RenderContext, RenderDevice, RenderQueue},
39    view::{ExtractedView, Msaa, ViewTarget, ViewUniformOffset},
40    Render, RenderApp, RenderSet,
41};
42use bevy_utils::{once, prelude::default};
43use tracing::info;
44
45use crate::{
46    binding_arrays_are_usable, graph::NodePbr, prelude::EnvironmentMapLight,
47    MeshPipelineViewLayoutKey, MeshPipelineViewLayouts, MeshViewBindGroup, RenderViewLightProbes,
48    ViewEnvironmentMapUniformOffset, ViewFogUniformOffset, ViewLightProbesUniformOffset,
49    ViewLightsUniformOffset,
50};
51
52const SSR_SHADER_HANDLE: Handle<Shader> = weak_handle!("0b559df2-0d61-4f53-bf62-aea16cf32787");
53const RAYMARCH_SHADER_HANDLE: Handle<Shader> = weak_handle!("798cc6fc-6072-4b6c-ab4f-83905fa4a19e");
54
55/// Enables screen-space reflections for a camera.
56///
57/// Screen-space reflections are currently only supported with deferred rendering.
58pub struct ScreenSpaceReflectionsPlugin;
59
60/// Add this component to a camera to enable *screen-space reflections* (SSR).
61///
62/// Screen-space reflections currently require deferred rendering in order to
63/// appear. Therefore, they also need the [`DepthPrepass`] and [`DeferredPrepass`]
64/// components, which are inserted automatically.
65///
66/// SSR currently performs no roughness filtering for glossy reflections, so
67/// only very smooth surfaces will reflect objects in screen space. You can
68/// adjust the `perceptual_roughness_threshold` in order to tune the threshold
69/// below which screen-space reflections will be traced.
70///
71/// As with all screen-space techniques, SSR can only reflect objects on screen.
72/// When objects leave the camera, they will disappear from reflections.
73/// An alternative that doesn't suffer from this problem is the combination of
74/// a [`LightProbe`](crate::LightProbe) and [`EnvironmentMapLight`]. The advantage of SSR is
75/// that it can reflect all objects, not just static ones.
76///
77/// SSR is an approximation technique and produces artifacts in some situations.
78/// Hand-tuning the settings in this component will likely be useful.
79///
80/// Screen-space reflections are presently unsupported on WebGL 2 because of a
81/// bug whereby Naga doesn't generate correct GLSL when sampling depth buffers,
82/// which is required for screen-space raymarching.
83#[derive(Clone, Copy, Component, Reflect)]
84#[reflect(Component, Default, Clone)]
85#[require(DepthPrepass, DeferredPrepass)]
86#[doc(alias = "Ssr")]
87pub struct ScreenSpaceReflections {
88    /// The maximum PBR roughness level that will enable screen space
89    /// reflections.
90    pub perceptual_roughness_threshold: f32,
91
92    /// When marching the depth buffer, we only have 2.5D information and don't
93    /// know how thick surfaces are. We shall assume that the depth buffer
94    /// fragments are cuboids with a constant thickness defined by this
95    /// parameter.
96    pub thickness: f32,
97
98    /// The number of steps to be taken at regular intervals to find an initial
99    /// intersection. Must not be zero.
100    ///
101    /// Higher values result in higher-quality reflections, because the
102    /// raymarching shader is less likely to miss objects. However, they take
103    /// more GPU time.
104    pub linear_steps: u32,
105
106    /// Exponent to be applied in the linear part of the march.
107    ///
108    /// A value of 1.0 will result in equidistant steps, and higher values will
109    /// compress the earlier steps, and expand the later ones. This might be
110    /// desirable in order to get more detail close to objects.
111    ///
112    /// For optimal performance, this should be a small unsigned integer, such
113    /// as 1 or 2.
114    pub linear_march_exponent: f32,
115
116    /// Number of steps in a bisection (binary search) to perform once the
117    /// linear search has found an intersection. Helps narrow down the hit,
118    /// increasing the chance of the secant method finding an accurate hit
119    /// point.
120    pub bisection_steps: u32,
121
122    /// Approximate the root position using the secant method—by solving for
123    /// line-line intersection between the ray approach rate and the surface
124    /// gradient.
125    pub use_secant: bool,
126}
127
128/// A version of [`ScreenSpaceReflections`] for upload to the GPU.
129///
130/// For more information on these fields, see the corresponding documentation in
131/// [`ScreenSpaceReflections`].
132#[derive(Clone, Copy, Component, ShaderType)]
133pub struct ScreenSpaceReflectionsUniform {
134    perceptual_roughness_threshold: f32,
135    thickness: f32,
136    linear_steps: u32,
137    linear_march_exponent: f32,
138    bisection_steps: u32,
139    /// A boolean converted to a `u32`.
140    use_secant: u32,
141}
142
143/// The node in the render graph that traces screen space reflections.
144#[derive(Default)]
145pub struct ScreenSpaceReflectionsNode;
146
147/// Identifies which screen space reflections render pipeline a view needs.
148#[derive(Component, Deref, DerefMut)]
149pub struct ScreenSpaceReflectionsPipelineId(pub CachedRenderPipelineId);
150
151/// Information relating to the render pipeline for the screen space reflections
152/// shader.
153#[derive(Resource)]
154pub struct ScreenSpaceReflectionsPipeline {
155    mesh_view_layouts: MeshPipelineViewLayouts,
156    color_sampler: Sampler,
157    depth_linear_sampler: Sampler,
158    depth_nearest_sampler: Sampler,
159    bind_group_layout: BindGroupLayout,
160    binding_arrays_are_usable: bool,
161}
162
163/// A GPU buffer that stores the screen space reflection settings for each view.
164#[derive(Resource, Default, Deref, DerefMut)]
165pub struct ScreenSpaceReflectionsBuffer(pub DynamicUniformBuffer<ScreenSpaceReflectionsUniform>);
166
167/// A component that stores the offset within the
168/// [`ScreenSpaceReflectionsBuffer`] for each view.
169#[derive(Component, Default, Deref, DerefMut)]
170pub struct ViewScreenSpaceReflectionsUniformOffset(u32);
171
172/// Identifies a specific configuration of the SSR pipeline shader.
173#[derive(Clone, Copy, PartialEq, Eq, Hash)]
174pub struct ScreenSpaceReflectionsPipelineKey {
175    mesh_pipeline_view_key: MeshPipelineViewLayoutKey,
176    is_hdr: bool,
177    has_environment_maps: bool,
178}
179
180impl Plugin for ScreenSpaceReflectionsPlugin {
181    fn build(&self, app: &mut App) {
182        load_internal_asset!(app, SSR_SHADER_HANDLE, "ssr.wgsl", Shader::from_wgsl);
183        load_internal_asset!(
184            app,
185            RAYMARCH_SHADER_HANDLE,
186            "raymarch.wgsl",
187            Shader::from_wgsl
188        );
189
190        app.register_type::<ScreenSpaceReflections>()
191            .add_plugins(ExtractComponentPlugin::<ScreenSpaceReflections>::default());
192
193        let Some(render_app) = app.get_sub_app_mut(RenderApp) else {
194            return;
195        };
196
197        render_app
198            .init_resource::<ScreenSpaceReflectionsBuffer>()
199            .add_systems(Render, prepare_ssr_pipelines.in_set(RenderSet::Prepare))
200            .add_systems(
201                Render,
202                prepare_ssr_settings.in_set(RenderSet::PrepareResources),
203            )
204            .add_render_graph_node::<ViewNodeRunner<ScreenSpaceReflectionsNode>>(
205                Core3d,
206                NodePbr::ScreenSpaceReflections,
207            );
208    }
209
210    fn finish(&self, app: &mut App) {
211        let Some(render_app) = app.get_sub_app_mut(RenderApp) else {
212            return;
213        };
214
215        render_app
216            .init_resource::<ScreenSpaceReflectionsPipeline>()
217            .init_resource::<SpecializedRenderPipelines<ScreenSpaceReflectionsPipeline>>();
218
219        // only reference the default deferred lighting pass
220        // if it has been added
221        let has_default_deferred_lighting_pass = render_app
222            .world_mut()
223            .resource_mut::<RenderGraph>()
224            .sub_graph(Core3d)
225            .get_node_state(NodePbr::DeferredLightingPass)
226            .is_ok();
227
228        if has_default_deferred_lighting_pass {
229            render_app.add_render_graph_edges(
230                Core3d,
231                (
232                    NodePbr::DeferredLightingPass,
233                    NodePbr::ScreenSpaceReflections,
234                    Node3d::MainOpaquePass,
235                ),
236            );
237        } else {
238            render_app.add_render_graph_edges(
239                Core3d,
240                (NodePbr::ScreenSpaceReflections, Node3d::MainOpaquePass),
241            );
242        }
243    }
244}
245
246impl Default for ScreenSpaceReflections {
247    // Reasonable default values.
248    //
249    // These are from
250    // <https://gist.github.com/h3r2tic/9c8356bdaefbe80b1a22ae0aaee192db?permalink_comment_id=4552149#gistcomment-4552149>.
251    fn default() -> Self {
252        Self {
253            perceptual_roughness_threshold: 0.1,
254            linear_steps: 16,
255            bisection_steps: 4,
256            use_secant: true,
257            thickness: 0.25,
258            linear_march_exponent: 1.0,
259        }
260    }
261}
262
263impl ViewNode for ScreenSpaceReflectionsNode {
264    type ViewQuery = (
265        Read<ViewTarget>,
266        Read<ViewUniformOffset>,
267        Read<ViewLightsUniformOffset>,
268        Read<ViewFogUniformOffset>,
269        Read<ViewLightProbesUniformOffset>,
270        Read<ViewScreenSpaceReflectionsUniformOffset>,
271        Read<ViewEnvironmentMapUniformOffset>,
272        Read<MeshViewBindGroup>,
273        Read<ScreenSpaceReflectionsPipelineId>,
274    );
275
276    fn run<'w>(
277        &self,
278        _: &mut RenderGraphContext,
279        render_context: &mut RenderContext<'w>,
280        (
281            view_target,
282            view_uniform_offset,
283            view_lights_offset,
284            view_fog_offset,
285            view_light_probes_offset,
286            view_ssr_offset,
287            view_environment_map_offset,
288            view_bind_group,
289            ssr_pipeline_id,
290        ): QueryItem<'w, Self::ViewQuery>,
291        world: &'w World,
292    ) -> Result<(), NodeRunError> {
293        // Grab the render pipeline.
294        let pipeline_cache = world.resource::<PipelineCache>();
295        let Some(render_pipeline) = pipeline_cache.get_render_pipeline(**ssr_pipeline_id) else {
296            return Ok(());
297        };
298
299        // Set up a standard pair of postprocessing textures.
300        let postprocess = view_target.post_process_write();
301
302        // Create the bind group for this view.
303        let ssr_pipeline = world.resource::<ScreenSpaceReflectionsPipeline>();
304        let ssr_bind_group = render_context.render_device().create_bind_group(
305            "SSR bind group",
306            &ssr_pipeline.bind_group_layout,
307            &BindGroupEntries::sequential((
308                postprocess.source,
309                &ssr_pipeline.color_sampler,
310                &ssr_pipeline.depth_linear_sampler,
311                &ssr_pipeline.depth_nearest_sampler,
312            )),
313        );
314
315        // Build the SSR render pass.
316        let mut render_pass = render_context.begin_tracked_render_pass(RenderPassDescriptor {
317            label: Some("SSR pass"),
318            color_attachments: &[Some(RenderPassColorAttachment {
319                view: postprocess.destination,
320                resolve_target: None,
321                ops: Operations::default(),
322            })],
323            depth_stencil_attachment: None,
324            timestamp_writes: None,
325            occlusion_query_set: None,
326        });
327
328        // Set bind groups.
329        render_pass.set_render_pipeline(render_pipeline);
330        render_pass.set_bind_group(
331            0,
332            &view_bind_group.value,
333            &[
334                view_uniform_offset.offset,
335                view_lights_offset.offset,
336                view_fog_offset.offset,
337                **view_light_probes_offset,
338                **view_ssr_offset,
339                **view_environment_map_offset,
340            ],
341        );
342
343        // Perform the SSR render pass.
344        render_pass.set_bind_group(1, &ssr_bind_group, &[]);
345        render_pass.draw(0..3, 0..1);
346
347        Ok(())
348    }
349}
350
351impl FromWorld for ScreenSpaceReflectionsPipeline {
352    fn from_world(world: &mut World) -> Self {
353        let mesh_view_layouts = world.resource::<MeshPipelineViewLayouts>().clone();
354        let render_device = world.resource::<RenderDevice>();
355        let render_adapter = world.resource::<RenderAdapter>();
356
357        // Create the bind group layout.
358        let bind_group_layout = render_device.create_bind_group_layout(
359            "SSR bind group layout",
360            &BindGroupLayoutEntries::sequential(
361                ShaderStages::FRAGMENT,
362                (
363                    binding_types::texture_2d(TextureSampleType::Float { filterable: true }),
364                    binding_types::sampler(SamplerBindingType::Filtering),
365                    binding_types::sampler(SamplerBindingType::Filtering),
366                    binding_types::sampler(SamplerBindingType::NonFiltering),
367                ),
368            ),
369        );
370
371        // Create the samplers we need.
372
373        let color_sampler = render_device.create_sampler(&SamplerDescriptor {
374            label: "SSR color sampler".into(),
375            address_mode_u: AddressMode::ClampToEdge,
376            address_mode_v: AddressMode::ClampToEdge,
377            mag_filter: FilterMode::Linear,
378            min_filter: FilterMode::Linear,
379            ..default()
380        });
381
382        let depth_linear_sampler = render_device.create_sampler(&SamplerDescriptor {
383            label: "SSR depth linear sampler".into(),
384            address_mode_u: AddressMode::ClampToEdge,
385            address_mode_v: AddressMode::ClampToEdge,
386            mag_filter: FilterMode::Linear,
387            min_filter: FilterMode::Linear,
388            ..default()
389        });
390
391        let depth_nearest_sampler = render_device.create_sampler(&SamplerDescriptor {
392            label: "SSR depth nearest sampler".into(),
393            address_mode_u: AddressMode::ClampToEdge,
394            address_mode_v: AddressMode::ClampToEdge,
395            mag_filter: FilterMode::Nearest,
396            min_filter: FilterMode::Nearest,
397            ..default()
398        });
399
400        Self {
401            mesh_view_layouts,
402            color_sampler,
403            depth_linear_sampler,
404            depth_nearest_sampler,
405            bind_group_layout,
406            binding_arrays_are_usable: binding_arrays_are_usable(render_device, render_adapter),
407        }
408    }
409}
410
411/// Sets up screen space reflection pipelines for each applicable view.
412pub fn prepare_ssr_pipelines(
413    mut commands: Commands,
414    pipeline_cache: Res<PipelineCache>,
415    mut pipelines: ResMut<SpecializedRenderPipelines<ScreenSpaceReflectionsPipeline>>,
416    ssr_pipeline: Res<ScreenSpaceReflectionsPipeline>,
417    views: Query<
418        (
419            Entity,
420            &ExtractedView,
421            Has<RenderViewLightProbes<EnvironmentMapLight>>,
422            Has<NormalPrepass>,
423            Has<MotionVectorPrepass>,
424        ),
425        (
426            With<ScreenSpaceReflectionsUniform>,
427            With<DepthPrepass>,
428            With<DeferredPrepass>,
429        ),
430    >,
431) {
432    for (
433        entity,
434        extracted_view,
435        has_environment_maps,
436        has_normal_prepass,
437        has_motion_vector_prepass,
438    ) in &views
439    {
440        // SSR is only supported in the deferred pipeline, which has no MSAA
441        // support. Thus we can assume MSAA is off.
442        let mut mesh_pipeline_view_key = MeshPipelineViewLayoutKey::from(Msaa::Off)
443            | MeshPipelineViewLayoutKey::DEPTH_PREPASS
444            | MeshPipelineViewLayoutKey::DEFERRED_PREPASS;
445        mesh_pipeline_view_key.set(
446            MeshPipelineViewLayoutKey::NORMAL_PREPASS,
447            has_normal_prepass,
448        );
449        mesh_pipeline_view_key.set(
450            MeshPipelineViewLayoutKey::MOTION_VECTOR_PREPASS,
451            has_motion_vector_prepass,
452        );
453
454        // Build the pipeline.
455        let pipeline_id = pipelines.specialize(
456            &pipeline_cache,
457            &ssr_pipeline,
458            ScreenSpaceReflectionsPipelineKey {
459                mesh_pipeline_view_key,
460                is_hdr: extracted_view.hdr,
461                has_environment_maps,
462            },
463        );
464
465        // Note which pipeline ID was used.
466        commands
467            .entity(entity)
468            .insert(ScreenSpaceReflectionsPipelineId(pipeline_id));
469    }
470}
471
472/// Gathers up screen space reflection settings for each applicable view and
473/// writes them into a GPU buffer.
474pub fn prepare_ssr_settings(
475    mut commands: Commands,
476    views: Query<(Entity, Option<&ScreenSpaceReflectionsUniform>), With<ExtractedView>>,
477    mut ssr_settings_buffer: ResMut<ScreenSpaceReflectionsBuffer>,
478    render_device: Res<RenderDevice>,
479    render_queue: Res<RenderQueue>,
480) {
481    let Some(mut writer) =
482        ssr_settings_buffer.get_writer(views.iter().len(), &render_device, &render_queue)
483    else {
484        return;
485    };
486
487    for (view, ssr_uniform) in views.iter() {
488        let uniform_offset = match ssr_uniform {
489            None => 0,
490            Some(ssr_uniform) => writer.write(ssr_uniform),
491        };
492        commands
493            .entity(view)
494            .insert(ViewScreenSpaceReflectionsUniformOffset(uniform_offset));
495    }
496}
497
498impl ExtractComponent for ScreenSpaceReflections {
499    type QueryData = Read<ScreenSpaceReflections>;
500
501    type QueryFilter = ();
502
503    type Out = ScreenSpaceReflectionsUniform;
504
505    fn extract_component(settings: QueryItem<'_, Self::QueryData>) -> Option<Self::Out> {
506        if !DEPTH_TEXTURE_SAMPLING_SUPPORTED {
507            once!(info!(
508                "Disabling screen-space reflections on this platform because depth textures \
509                aren't supported correctly"
510            ));
511            return None;
512        }
513
514        Some((*settings).into())
515    }
516}
517
518impl SpecializedRenderPipeline for ScreenSpaceReflectionsPipeline {
519    type Key = ScreenSpaceReflectionsPipelineKey;
520
521    fn specialize(&self, key: Self::Key) -> RenderPipelineDescriptor {
522        let mesh_view_layout = self
523            .mesh_view_layouts
524            .get_view_layout(key.mesh_pipeline_view_key);
525
526        let mut shader_defs = vec![
527            "DEPTH_PREPASS".into(),
528            "DEFERRED_PREPASS".into(),
529            "SCREEN_SPACE_REFLECTIONS".into(),
530        ];
531
532        if key.has_environment_maps {
533            shader_defs.push("ENVIRONMENT_MAP".into());
534        }
535
536        if self.binding_arrays_are_usable {
537            shader_defs.push("MULTIPLE_LIGHT_PROBES_IN_ARRAY".into());
538        }
539
540        RenderPipelineDescriptor {
541            label: Some("SSR pipeline".into()),
542            layout: vec![mesh_view_layout.clone(), self.bind_group_layout.clone()],
543            vertex: fullscreen_vertex_shader::fullscreen_shader_vertex_state(),
544            fragment: Some(FragmentState {
545                shader: SSR_SHADER_HANDLE,
546                shader_defs,
547                entry_point: "fragment".into(),
548                targets: vec![Some(ColorTargetState {
549                    format: if key.is_hdr {
550                        ViewTarget::TEXTURE_FORMAT_HDR
551                    } else {
552                        TextureFormat::bevy_default()
553                    },
554                    blend: None,
555                    write_mask: ColorWrites::ALL,
556                })],
557            }),
558            push_constant_ranges: vec![],
559            primitive: default(),
560            depth_stencil: None,
561            multisample: default(),
562            zero_initialize_workgroup_memory: false,
563        }
564    }
565}
566
567impl From<ScreenSpaceReflections> for ScreenSpaceReflectionsUniform {
568    fn from(settings: ScreenSpaceReflections) -> Self {
569        Self {
570            perceptual_roughness_threshold: settings.perceptual_roughness_threshold,
571            thickness: settings.thickness,
572            linear_steps: settings.linear_steps,
573            linear_march_exponent: settings.linear_march_exponent,
574            bisection_steps: settings.bisection_steps,
575            use_secant: settings.use_secant as u32,
576        }
577    }
578}