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, BindGroupLayout, BindGroupLayoutEntries,
35        CachedRenderPipelineId, ColorTargetState, ColorWrites, DynamicUniformBuffer, FilterMode,
36        FragmentState, Operations, PipelineCache, RenderPassColorAttachment, RenderPassDescriptor,
37        RenderPipelineDescriptor, Sampler, SamplerBindingType, SamplerDescriptor, ShaderStages,
38        ShaderType, SpecializedRenderPipeline, SpecializedRenderPipelines, TextureFormat,
39        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, MeshPipelineViewLayoutKey, MeshPipelineViewLayouts,
51    MeshViewBindGroup, RenderViewLightProbes, ViewEnvironmentMapUniformOffset,
52    ViewFogUniformOffset, ViewLightProbesUniformOffset, ViewLightsUniformOffset,
53};
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`](bevy_light::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    fullscreen_shader: FullscreenShader,
162    fragment_shader: Handle<Shader>,
163}
164
165/// A GPU buffer that stores the screen space reflection settings for each view.
166#[derive(Resource, Default, Deref, DerefMut)]
167pub struct ScreenSpaceReflectionsBuffer(pub DynamicUniformBuffer<ScreenSpaceReflectionsUniform>);
168
169/// A component that stores the offset within the
170/// [`ScreenSpaceReflectionsBuffer`] for each view.
171#[derive(Component, Default, Deref, DerefMut)]
172pub struct ViewScreenSpaceReflectionsUniformOffset(u32);
173
174/// Identifies a specific configuration of the SSR pipeline shader.
175#[derive(Clone, Copy, PartialEq, Eq, Hash)]
176pub struct ScreenSpaceReflectionsPipelineKey {
177    mesh_pipeline_view_key: MeshPipelineViewLayoutKey,
178    is_hdr: bool,
179    has_environment_maps: bool,
180}
181
182impl Plugin for ScreenSpaceReflectionsPlugin {
183    fn build(&self, app: &mut App) {
184        load_shader_library!(app, "ssr.wgsl");
185        load_shader_library!(app, "raymarch.wgsl");
186
187        app.add_plugins(ExtractComponentPlugin::<ScreenSpaceReflections>::default());
188
189        let Some(render_app) = app.get_sub_app_mut(RenderApp) else {
190            return;
191        };
192
193        render_app
194            .init_resource::<ScreenSpaceReflectionsBuffer>()
195            .init_resource::<SpecializedRenderPipelines<ScreenSpaceReflectionsPipeline>>()
196            .add_systems(
197                RenderStartup,
198                (
199                    init_screen_space_reflections_pipeline,
200                    add_screen_space_reflections_render_graph_edges,
201                ),
202            )
203            .add_systems(Render, prepare_ssr_pipelines.in_set(RenderSystems::Prepare))
204            .add_systems(
205                Render,
206                prepare_ssr_settings.in_set(RenderSystems::PrepareResources),
207            )
208            // Note: we add this node here but then we add edges in
209            // `add_screen_space_reflections_render_graph_edges`.
210            .add_render_graph_node::<ViewNodeRunner<ScreenSpaceReflectionsNode>>(
211                Core3d,
212                NodePbr::ScreenSpaceReflections,
213            );
214    }
215}
216
217fn add_screen_space_reflections_render_graph_edges(mut render_graph: ResMut<RenderGraph>) {
218    let subgraph = render_graph.sub_graph_mut(Core3d);
219
220    subgraph.add_node_edge(NodePbr::ScreenSpaceReflections, Node3d::MainOpaquePass);
221
222    if subgraph
223        .get_node_state(NodePbr::DeferredLightingPass)
224        .is_ok()
225    {
226        subgraph.add_node_edge(
227            NodePbr::DeferredLightingPass,
228            NodePbr::ScreenSpaceReflections,
229        );
230    }
231}
232
233impl Default for ScreenSpaceReflections {
234    // Reasonable default values.
235    //
236    // These are from
237    // <https://gist.github.com/h3r2tic/9c8356bdaefbe80b1a22ae0aaee192db?permalink_comment_id=4552149#gistcomment-4552149>.
238    fn default() -> Self {
239        Self {
240            perceptual_roughness_threshold: 0.1,
241            linear_steps: 16,
242            bisection_steps: 4,
243            use_secant: true,
244            thickness: 0.25,
245            linear_march_exponent: 1.0,
246        }
247    }
248}
249
250impl ViewNode for ScreenSpaceReflectionsNode {
251    type ViewQuery = (
252        Read<ViewTarget>,
253        Read<ViewUniformOffset>,
254        Read<ViewLightsUniformOffset>,
255        Read<ViewFogUniformOffset>,
256        Read<ViewLightProbesUniformOffset>,
257        Read<ViewScreenSpaceReflectionsUniformOffset>,
258        Read<ViewEnvironmentMapUniformOffset>,
259        Read<MeshViewBindGroup>,
260        Read<ScreenSpaceReflectionsPipelineId>,
261    );
262
263    fn run<'w>(
264        &self,
265        _: &mut RenderGraphContext,
266        render_context: &mut RenderContext<'w>,
267        (
268            view_target,
269            view_uniform_offset,
270            view_lights_offset,
271            view_fog_offset,
272            view_light_probes_offset,
273            view_ssr_offset,
274            view_environment_map_offset,
275            view_bind_group,
276            ssr_pipeline_id,
277        ): QueryItem<'w, '_, Self::ViewQuery>,
278        world: &'w World,
279    ) -> Result<(), NodeRunError> {
280        // Grab the render pipeline.
281        let pipeline_cache = world.resource::<PipelineCache>();
282        let Some(render_pipeline) = pipeline_cache.get_render_pipeline(**ssr_pipeline_id) else {
283            return Ok(());
284        };
285
286        let diagnostics = render_context.diagnostic_recorder();
287
288        // Set up a standard pair of postprocessing textures.
289        let postprocess = view_target.post_process_write();
290
291        // Create the bind group for this view.
292        let ssr_pipeline = world.resource::<ScreenSpaceReflectionsPipeline>();
293        let ssr_bind_group = render_context.render_device().create_bind_group(
294            "SSR bind group",
295            &ssr_pipeline.bind_group_layout,
296            &BindGroupEntries::sequential((
297                postprocess.source,
298                &ssr_pipeline.color_sampler,
299                &ssr_pipeline.depth_linear_sampler,
300                &ssr_pipeline.depth_nearest_sampler,
301            )),
302        );
303
304        // Build the SSR render pass.
305        let mut render_pass = render_context.begin_tracked_render_pass(RenderPassDescriptor {
306            label: Some("ssr"),
307            color_attachments: &[Some(RenderPassColorAttachment {
308                view: postprocess.destination,
309                depth_slice: None,
310                resolve_target: None,
311                ops: Operations::default(),
312            })],
313            depth_stencil_attachment: None,
314            timestamp_writes: None,
315            occlusion_query_set: None,
316        });
317        let pass_span = diagnostics.pass_span(&mut render_pass, "ssr");
318
319        // Set bind groups.
320        render_pass.set_render_pipeline(render_pipeline);
321        render_pass.set_bind_group(
322            0,
323            &view_bind_group.main,
324            &[
325                view_uniform_offset.offset,
326                view_lights_offset.offset,
327                view_fog_offset.offset,
328                **view_light_probes_offset,
329                **view_ssr_offset,
330                **view_environment_map_offset,
331            ],
332        );
333        render_pass.set_bind_group(1, &view_bind_group.binding_array, &[]);
334
335        // Perform the SSR render pass.
336        render_pass.set_bind_group(2, &ssr_bind_group, &[]);
337        render_pass.draw(0..3, 0..1);
338
339        pass_span.end(&mut render_pass);
340
341        Ok(())
342    }
343}
344
345pub fn init_screen_space_reflections_pipeline(
346    mut commands: Commands,
347    render_device: Res<RenderDevice>,
348    render_adapter: Res<RenderAdapter>,
349    mesh_view_layouts: Res<MeshPipelineViewLayouts>,
350    fullscreen_shader: Res<FullscreenShader>,
351    asset_server: Res<AssetServer>,
352) {
353    // Create the bind group layout.
354    let bind_group_layout = render_device.create_bind_group_layout(
355        "SSR bind group layout",
356        &BindGroupLayoutEntries::sequential(
357            ShaderStages::FRAGMENT,
358            (
359                binding_types::texture_2d(TextureSampleType::Float { filterable: true }),
360                binding_types::sampler(SamplerBindingType::Filtering),
361                binding_types::sampler(SamplerBindingType::Filtering),
362                binding_types::sampler(SamplerBindingType::NonFiltering),
363            ),
364        ),
365    );
366
367    // Create the samplers we need.
368
369    let color_sampler = render_device.create_sampler(&SamplerDescriptor {
370        label: "SSR color sampler".into(),
371        address_mode_u: AddressMode::ClampToEdge,
372        address_mode_v: AddressMode::ClampToEdge,
373        mag_filter: FilterMode::Linear,
374        min_filter: FilterMode::Linear,
375        ..default()
376    });
377
378    let depth_linear_sampler = render_device.create_sampler(&SamplerDescriptor {
379        label: "SSR depth linear sampler".into(),
380        address_mode_u: AddressMode::ClampToEdge,
381        address_mode_v: AddressMode::ClampToEdge,
382        mag_filter: FilterMode::Linear,
383        min_filter: FilterMode::Linear,
384        ..default()
385    });
386
387    let depth_nearest_sampler = render_device.create_sampler(&SamplerDescriptor {
388        label: "SSR depth nearest sampler".into(),
389        address_mode_u: AddressMode::ClampToEdge,
390        address_mode_v: AddressMode::ClampToEdge,
391        mag_filter: FilterMode::Nearest,
392        min_filter: FilterMode::Nearest,
393        ..default()
394    });
395
396    commands.insert_resource(ScreenSpaceReflectionsPipeline {
397        mesh_view_layouts: mesh_view_layouts.clone(),
398        color_sampler,
399        depth_linear_sampler,
400        depth_nearest_sampler,
401        bind_group_layout,
402        binding_arrays_are_usable: binding_arrays_are_usable(&render_device, &render_adapter),
403        fullscreen_shader: fullscreen_shader.clone(),
404        // Even though ssr was loaded using load_shader_library, we can still access it like a
405        // normal embedded asset (so we can use it as both a library or a kernel).
406        fragment_shader: load_embedded_asset!(asset_server.as_ref(), "ssr.wgsl"),
407    });
408}
409
410/// Sets up screen space reflection pipelines for each applicable view.
411pub fn prepare_ssr_pipelines(
412    mut commands: Commands,
413    pipeline_cache: Res<PipelineCache>,
414    mut pipelines: ResMut<SpecializedRenderPipelines<ScreenSpaceReflectionsPipeline>>,
415    ssr_pipeline: Res<ScreenSpaceReflectionsPipeline>,
416    views: Query<
417        (
418            Entity,
419            &ExtractedView,
420            Has<RenderViewLightProbes<EnvironmentMapLight>>,
421            Has<NormalPrepass>,
422            Has<MotionVectorPrepass>,
423        ),
424        (
425            With<ScreenSpaceReflectionsUniform>,
426            With<DepthPrepass>,
427            With<DeferredPrepass>,
428        ),
429    >,
430) {
431    for (
432        entity,
433        extracted_view,
434        has_environment_maps,
435        has_normal_prepass,
436        has_motion_vector_prepass,
437    ) in &views
438    {
439        // SSR is only supported in the deferred pipeline, which has no MSAA
440        // support. Thus we can assume MSAA is off.
441        let mut mesh_pipeline_view_key = MeshPipelineViewLayoutKey::from(Msaa::Off)
442            | MeshPipelineViewLayoutKey::DEPTH_PREPASS
443            | MeshPipelineViewLayoutKey::DEFERRED_PREPASS;
444        mesh_pipeline_view_key.set(
445            MeshPipelineViewLayoutKey::NORMAL_PREPASS,
446            has_normal_prepass,
447        );
448        mesh_pipeline_view_key.set(
449            MeshPipelineViewLayoutKey::MOTION_VECTOR_PREPASS,
450            has_motion_vector_prepass,
451        );
452
453        // Build the pipeline.
454        let pipeline_id = pipelines.specialize(
455            &pipeline_cache,
456            &ssr_pipeline,
457            ScreenSpaceReflectionsPipelineKey {
458                mesh_pipeline_view_key,
459                is_hdr: extracted_view.hdr,
460                has_environment_maps,
461            },
462        );
463
464        // Note which pipeline ID was used.
465        commands
466            .entity(entity)
467            .insert(ScreenSpaceReflectionsPipelineId(pipeline_id));
468    }
469}
470
471/// Gathers up screen space reflection settings for each applicable view and
472/// writes them into a GPU buffer.
473pub fn prepare_ssr_settings(
474    mut commands: Commands,
475    views: Query<(Entity, Option<&ScreenSpaceReflectionsUniform>), With<ExtractedView>>,
476    mut ssr_settings_buffer: ResMut<ScreenSpaceReflectionsBuffer>,
477    render_device: Res<RenderDevice>,
478    render_queue: Res<RenderQueue>,
479) {
480    let Some(mut writer) =
481        ssr_settings_buffer.get_writer(views.iter().len(), &render_device, &render_queue)
482    else {
483        return;
484    };
485
486    for (view, ssr_uniform) in views.iter() {
487        let uniform_offset = match ssr_uniform {
488            None => 0,
489            Some(ssr_uniform) => writer.write(ssr_uniform),
490        };
491        commands
492            .entity(view)
493            .insert(ViewScreenSpaceReflectionsUniformOffset(uniform_offset));
494    }
495}
496
497impl ExtractComponent for ScreenSpaceReflections {
498    type QueryData = Read<ScreenSpaceReflections>;
499
500    type QueryFilter = ();
501
502    type Out = ScreenSpaceReflectionsUniform;
503
504    fn extract_component(settings: QueryItem<'_, '_, Self::QueryData>) -> Option<Self::Out> {
505        if !DEPTH_TEXTURE_SAMPLING_SUPPORTED {
506            once!(info!(
507                "Disabling screen-space reflections on this platform because depth textures \
508                aren't supported correctly"
509            ));
510            return None;
511        }
512
513        Some((*settings).into())
514    }
515}
516
517impl SpecializedRenderPipeline for ScreenSpaceReflectionsPipeline {
518    type Key = ScreenSpaceReflectionsPipelineKey;
519
520    fn specialize(&self, key: Self::Key) -> RenderPipelineDescriptor {
521        let layout = self
522            .mesh_view_layouts
523            .get_view_layout(key.mesh_pipeline_view_key);
524        let layout = vec![
525            layout.main_layout.clone(),
526            layout.binding_array_layout.clone(),
527            self.bind_group_layout.clone(),
528        ];
529
530        let mut shader_defs = vec![
531            "DEPTH_PREPASS".into(),
532            "DEFERRED_PREPASS".into(),
533            "SCREEN_SPACE_REFLECTIONS".into(),
534        ];
535
536        if key.has_environment_maps {
537            shader_defs.push("ENVIRONMENT_MAP".into());
538        }
539
540        if self.binding_arrays_are_usable {
541            shader_defs.push("MULTIPLE_LIGHT_PROBES_IN_ARRAY".into());
542        }
543
544        RenderPipelineDescriptor {
545            label: Some("SSR pipeline".into()),
546            layout,
547            vertex: self.fullscreen_shader.to_vertex_state(),
548            fragment: Some(FragmentState {
549                shader: self.fragment_shader.clone(),
550                shader_defs,
551                targets: vec![Some(ColorTargetState {
552                    format: if key.is_hdr {
553                        ViewTarget::TEXTURE_FORMAT_HDR
554                    } else {
555                        TextureFormat::bevy_default()
556                    },
557                    blend: None,
558                    write_mask: ColorWrites::ALL,
559                })],
560                ..default()
561            }),
562            ..default()
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}