bevy_pbr/ssr/
mod.rs

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