bevy_pbr/ssao/
mod.rs

1#![expect(deprecated)]
2
3use crate::NodePbr;
4use bevy_app::{App, Plugin};
5use bevy_asset::{load_internal_asset, Handle};
6use bevy_core_pipeline::{
7    core_3d::graph::{Core3d, Node3d},
8    prelude::Camera3d,
9    prepass::{DepthPrepass, NormalPrepass, ViewPrepassTextures},
10};
11use bevy_ecs::{
12    prelude::{Bundle, Component, Entity},
13    query::{Has, QueryItem, With},
14    reflect::ReflectComponent,
15    schedule::IntoSystemConfigs,
16    system::{Commands, Query, Res, ResMut, Resource},
17    world::{FromWorld, World},
18};
19use bevy_reflect::{std_traits::ReflectDefault, Reflect};
20use bevy_render::{
21    camera::{ExtractedCamera, TemporalJitter},
22    extract_component::ExtractComponent,
23    globals::{GlobalsBuffer, GlobalsUniform},
24    prelude::Camera,
25    render_graph::{NodeRunError, RenderGraphApp, RenderGraphContext, ViewNode, ViewNodeRunner},
26    render_resource::{
27        binding_types::{
28            sampler, texture_2d, texture_depth_2d, texture_storage_2d, uniform_buffer,
29        },
30        *,
31    },
32    renderer::{RenderAdapter, RenderContext, RenderDevice, RenderQueue},
33    sync_component::SyncComponentPlugin,
34    sync_world::RenderEntity,
35    texture::{CachedTexture, TextureCache},
36    view::{Msaa, ViewUniform, ViewUniformOffset, ViewUniforms},
37    Extract, ExtractSchedule, Render, RenderApp, RenderSet,
38};
39use bevy_utils::{
40    prelude::default,
41    tracing::{error, warn},
42};
43use core::mem;
44
45const PREPROCESS_DEPTH_SHADER_HANDLE: Handle<Shader> = Handle::weak_from_u128(102258915420479);
46const SSAO_SHADER_HANDLE: Handle<Shader> = Handle::weak_from_u128(253938746510568);
47const SPATIAL_DENOISE_SHADER_HANDLE: Handle<Shader> = Handle::weak_from_u128(466162052558226);
48const SSAO_UTILS_SHADER_HANDLE: Handle<Shader> = Handle::weak_from_u128(366465052568786);
49
50/// Plugin for screen space ambient occlusion.
51pub struct ScreenSpaceAmbientOcclusionPlugin;
52
53impl Plugin for ScreenSpaceAmbientOcclusionPlugin {
54    fn build(&self, app: &mut App) {
55        load_internal_asset!(
56            app,
57            PREPROCESS_DEPTH_SHADER_HANDLE,
58            "preprocess_depth.wgsl",
59            Shader::from_wgsl
60        );
61        load_internal_asset!(app, SSAO_SHADER_HANDLE, "ssao.wgsl", Shader::from_wgsl);
62        load_internal_asset!(
63            app,
64            SPATIAL_DENOISE_SHADER_HANDLE,
65            "spatial_denoise.wgsl",
66            Shader::from_wgsl
67        );
68        load_internal_asset!(
69            app,
70            SSAO_UTILS_SHADER_HANDLE,
71            "ssao_utils.wgsl",
72            Shader::from_wgsl
73        );
74
75        app.register_type::<ScreenSpaceAmbientOcclusion>();
76
77        app.add_plugins(SyncComponentPlugin::<ScreenSpaceAmbientOcclusion>::default());
78    }
79
80    fn finish(&self, app: &mut App) {
81        let Some(render_app) = app.get_sub_app_mut(RenderApp) else {
82            return;
83        };
84
85        if !render_app
86            .world()
87            .resource::<RenderAdapter>()
88            .get_texture_format_features(TextureFormat::R16Float)
89            .allowed_usages
90            .contains(TextureUsages::STORAGE_BINDING)
91        {
92            warn!("ScreenSpaceAmbientOcclusionPlugin not loaded. GPU lacks support: TextureFormat::R16Float does not support TextureUsages::STORAGE_BINDING.");
93            return;
94        }
95
96        if render_app
97            .world()
98            .resource::<RenderDevice>()
99            .limits()
100            .max_storage_textures_per_shader_stage
101            < 5
102        {
103            warn!("ScreenSpaceAmbientOcclusionPlugin not loaded. GPU lacks support: Limits::max_storage_textures_per_shader_stage is less than 5.");
104            return;
105        }
106
107        render_app
108            .init_resource::<SsaoPipelines>()
109            .init_resource::<SpecializedComputePipelines<SsaoPipelines>>()
110            .add_systems(ExtractSchedule, extract_ssao_settings)
111            .add_systems(
112                Render,
113                (
114                    prepare_ssao_pipelines.in_set(RenderSet::Prepare),
115                    prepare_ssao_textures.in_set(RenderSet::PrepareResources),
116                    prepare_ssao_bind_groups.in_set(RenderSet::PrepareBindGroups),
117                ),
118            )
119            .add_render_graph_node::<ViewNodeRunner<SsaoNode>>(
120                Core3d,
121                NodePbr::ScreenSpaceAmbientOcclusion,
122            )
123            .add_render_graph_edges(
124                Core3d,
125                (
126                    // END_PRE_PASSES -> SCREEN_SPACE_AMBIENT_OCCLUSION -> MAIN_PASS
127                    Node3d::EndPrepasses,
128                    NodePbr::ScreenSpaceAmbientOcclusion,
129                    Node3d::StartMainPass,
130                ),
131            );
132    }
133}
134
135/// Bundle to apply screen space ambient occlusion.
136#[derive(Bundle, Default, Clone)]
137#[deprecated(
138    since = "0.15.0",
139    note = "Use the `ScreenSpaceAmbientOcclusion` component instead. Inserting it will now also insert the other components required by it automatically."
140)]
141pub struct ScreenSpaceAmbientOcclusionBundle {
142    pub settings: ScreenSpaceAmbientOcclusion,
143    pub depth_prepass: DepthPrepass,
144    pub normal_prepass: NormalPrepass,
145}
146
147/// Component to apply screen space ambient occlusion to a 3d camera.
148///
149/// Screen space ambient occlusion (SSAO) approximates small-scale,
150/// local occlusion of _indirect_ diffuse light between objects, based on what's visible on-screen.
151/// SSAO does not apply to direct lighting, such as point or directional lights.
152///
153/// This darkens creases, e.g. on staircases, and gives nice contact shadows
154/// where objects meet, giving entities a more "grounded" feel.
155///
156/// # Usage Notes
157///
158/// Requires that you add [`ScreenSpaceAmbientOcclusionPlugin`] to your app.
159///
160/// It strongly recommended that you use SSAO in conjunction with
161/// TAA ([`bevy_core_pipeline::experimental::taa::TemporalAntiAliasing`]).
162/// Doing so greatly reduces SSAO noise.
163///
164/// SSAO is not supported on `WebGL2`, and is not currently supported on `WebGPU`.
165#[derive(Component, ExtractComponent, Reflect, PartialEq, Clone, Debug)]
166#[reflect(Component, Debug, Default, PartialEq)]
167#[require(DepthPrepass, NormalPrepass)]
168#[doc(alias = "Ssao")]
169pub struct ScreenSpaceAmbientOcclusion {
170    /// Quality of the SSAO effect.
171    pub quality_level: ScreenSpaceAmbientOcclusionQualityLevel,
172    /// A constant estimated thickness of objects.
173    ///
174    /// This value is used to decide how far behind an object a ray of light needs to be in order
175    /// to pass behind it. Any ray closer than that will be occluded.
176    pub constant_object_thickness: f32,
177}
178
179impl Default for ScreenSpaceAmbientOcclusion {
180    fn default() -> Self {
181        Self {
182            quality_level: ScreenSpaceAmbientOcclusionQualityLevel::default(),
183            constant_object_thickness: 0.25,
184        }
185    }
186}
187
188#[deprecated(since = "0.15.0", note = "Renamed to `ScreenSpaceAmbientOcclusion`")]
189pub type ScreenSpaceAmbientOcclusionSettings = ScreenSpaceAmbientOcclusion;
190
191#[derive(Reflect, PartialEq, Eq, Hash, Clone, Copy, Default, Debug)]
192pub enum ScreenSpaceAmbientOcclusionQualityLevel {
193    Low,
194    Medium,
195    #[default]
196    High,
197    Ultra,
198    Custom {
199        /// Higher slice count means less noise, but worse performance.
200        slice_count: u32,
201        /// Samples per slice side is also tweakable, but recommended to be left at 2 or 3.
202        samples_per_slice_side: u32,
203    },
204}
205
206impl ScreenSpaceAmbientOcclusionQualityLevel {
207    fn sample_counts(&self) -> (u32, u32) {
208        match self {
209            Self::Low => (1, 2),    // 4 spp (1 * (2 * 2)), plus optional temporal samples
210            Self::Medium => (2, 2), // 8 spp (2 * (2 * 2)), plus optional temporal samples
211            Self::High => (3, 3),   // 18 spp (3 * (3 * 2)), plus optional temporal samples
212            Self::Ultra => (9, 3),  // 54 spp (9 * (3 * 2)), plus optional temporal samples
213            Self::Custom {
214                slice_count: slices,
215                samples_per_slice_side,
216            } => (*slices, *samples_per_slice_side),
217        }
218    }
219}
220
221#[derive(Default)]
222struct SsaoNode {}
223
224impl ViewNode for SsaoNode {
225    type ViewQuery = (
226        &'static ExtractedCamera,
227        &'static SsaoPipelineId,
228        &'static SsaoBindGroups,
229        &'static ViewUniformOffset,
230    );
231
232    fn run(
233        &self,
234        _graph: &mut RenderGraphContext,
235        render_context: &mut RenderContext,
236        (camera, pipeline_id, bind_groups, view_uniform_offset): QueryItem<Self::ViewQuery>,
237        world: &World,
238    ) -> Result<(), NodeRunError> {
239        let pipelines = world.resource::<SsaoPipelines>();
240        let pipeline_cache = world.resource::<PipelineCache>();
241        let (
242            Some(camera_size),
243            Some(preprocess_depth_pipeline),
244            Some(spatial_denoise_pipeline),
245            Some(ssao_pipeline),
246        ) = (
247            camera.physical_viewport_size,
248            pipeline_cache.get_compute_pipeline(pipelines.preprocess_depth_pipeline),
249            pipeline_cache.get_compute_pipeline(pipelines.spatial_denoise_pipeline),
250            pipeline_cache.get_compute_pipeline(pipeline_id.0),
251        )
252        else {
253            return Ok(());
254        };
255
256        render_context.command_encoder().push_debug_group("ssao");
257
258        {
259            let mut preprocess_depth_pass =
260                render_context
261                    .command_encoder()
262                    .begin_compute_pass(&ComputePassDescriptor {
263                        label: Some("ssao_preprocess_depth_pass"),
264                        timestamp_writes: None,
265                    });
266            preprocess_depth_pass.set_pipeline(preprocess_depth_pipeline);
267            preprocess_depth_pass.set_bind_group(0, &bind_groups.preprocess_depth_bind_group, &[]);
268            preprocess_depth_pass.set_bind_group(
269                1,
270                &bind_groups.common_bind_group,
271                &[view_uniform_offset.offset],
272            );
273            preprocess_depth_pass.dispatch_workgroups(
274                camera_size.x.div_ceil(16),
275                camera_size.y.div_ceil(16),
276                1,
277            );
278        }
279
280        {
281            let mut ssao_pass =
282                render_context
283                    .command_encoder()
284                    .begin_compute_pass(&ComputePassDescriptor {
285                        label: Some("ssao_ssao_pass"),
286                        timestamp_writes: None,
287                    });
288            ssao_pass.set_pipeline(ssao_pipeline);
289            ssao_pass.set_bind_group(0, &bind_groups.ssao_bind_group, &[]);
290            ssao_pass.set_bind_group(
291                1,
292                &bind_groups.common_bind_group,
293                &[view_uniform_offset.offset],
294            );
295            ssao_pass.dispatch_workgroups(camera_size.x.div_ceil(8), camera_size.y.div_ceil(8), 1);
296        }
297
298        {
299            let mut spatial_denoise_pass =
300                render_context
301                    .command_encoder()
302                    .begin_compute_pass(&ComputePassDescriptor {
303                        label: Some("ssao_spatial_denoise_pass"),
304                        timestamp_writes: None,
305                    });
306            spatial_denoise_pass.set_pipeline(spatial_denoise_pipeline);
307            spatial_denoise_pass.set_bind_group(0, &bind_groups.spatial_denoise_bind_group, &[]);
308            spatial_denoise_pass.set_bind_group(
309                1,
310                &bind_groups.common_bind_group,
311                &[view_uniform_offset.offset],
312            );
313            spatial_denoise_pass.dispatch_workgroups(
314                camera_size.x.div_ceil(8),
315                camera_size.y.div_ceil(8),
316                1,
317            );
318        }
319
320        render_context.command_encoder().pop_debug_group();
321        Ok(())
322    }
323}
324
325#[derive(Resource)]
326struct SsaoPipelines {
327    preprocess_depth_pipeline: CachedComputePipelineId,
328    spatial_denoise_pipeline: CachedComputePipelineId,
329
330    common_bind_group_layout: BindGroupLayout,
331    preprocess_depth_bind_group_layout: BindGroupLayout,
332    ssao_bind_group_layout: BindGroupLayout,
333    spatial_denoise_bind_group_layout: BindGroupLayout,
334
335    hilbert_index_lut: TextureView,
336    point_clamp_sampler: Sampler,
337    linear_clamp_sampler: Sampler,
338}
339
340impl FromWorld for SsaoPipelines {
341    fn from_world(world: &mut World) -> Self {
342        let render_device = world.resource::<RenderDevice>();
343        let render_queue = world.resource::<RenderQueue>();
344        let pipeline_cache = world.resource::<PipelineCache>();
345
346        let hilbert_index_lut = render_device
347            .create_texture_with_data(
348                render_queue,
349                &(TextureDescriptor {
350                    label: Some("ssao_hilbert_index_lut"),
351                    size: Extent3d {
352                        width: HILBERT_WIDTH as u32,
353                        height: HILBERT_WIDTH as u32,
354                        depth_or_array_layers: 1,
355                    },
356                    mip_level_count: 1,
357                    sample_count: 1,
358                    dimension: TextureDimension::D2,
359                    format: TextureFormat::R16Uint,
360                    usage: TextureUsages::TEXTURE_BINDING,
361                    view_formats: &[],
362                }),
363                TextureDataOrder::default(),
364                bytemuck::cast_slice(&generate_hilbert_index_lut()),
365            )
366            .create_view(&TextureViewDescriptor::default());
367
368        let point_clamp_sampler = render_device.create_sampler(&SamplerDescriptor {
369            min_filter: FilterMode::Nearest,
370            mag_filter: FilterMode::Nearest,
371            mipmap_filter: FilterMode::Nearest,
372            address_mode_u: AddressMode::ClampToEdge,
373            address_mode_v: AddressMode::ClampToEdge,
374            ..Default::default()
375        });
376        let linear_clamp_sampler = render_device.create_sampler(&SamplerDescriptor {
377            min_filter: FilterMode::Linear,
378            mag_filter: FilterMode::Linear,
379            mipmap_filter: FilterMode::Nearest,
380            address_mode_u: AddressMode::ClampToEdge,
381            address_mode_v: AddressMode::ClampToEdge,
382            ..Default::default()
383        });
384
385        let common_bind_group_layout = render_device.create_bind_group_layout(
386            "ssao_common_bind_group_layout",
387            &BindGroupLayoutEntries::sequential(
388                ShaderStages::COMPUTE,
389                (
390                    sampler(SamplerBindingType::NonFiltering),
391                    sampler(SamplerBindingType::Filtering),
392                    uniform_buffer::<ViewUniform>(true),
393                ),
394            ),
395        );
396
397        let preprocess_depth_bind_group_layout = render_device.create_bind_group_layout(
398            "ssao_preprocess_depth_bind_group_layout",
399            &BindGroupLayoutEntries::sequential(
400                ShaderStages::COMPUTE,
401                (
402                    texture_depth_2d(),
403                    texture_storage_2d(TextureFormat::R16Float, StorageTextureAccess::WriteOnly),
404                    texture_storage_2d(TextureFormat::R16Float, StorageTextureAccess::WriteOnly),
405                    texture_storage_2d(TextureFormat::R16Float, StorageTextureAccess::WriteOnly),
406                    texture_storage_2d(TextureFormat::R16Float, StorageTextureAccess::WriteOnly),
407                    texture_storage_2d(TextureFormat::R16Float, StorageTextureAccess::WriteOnly),
408                ),
409            ),
410        );
411
412        let ssao_bind_group_layout = render_device.create_bind_group_layout(
413            "ssao_ssao_bind_group_layout",
414            &BindGroupLayoutEntries::sequential(
415                ShaderStages::COMPUTE,
416                (
417                    texture_2d(TextureSampleType::Float { filterable: true }),
418                    texture_2d(TextureSampleType::Float { filterable: false }),
419                    texture_2d(TextureSampleType::Uint),
420                    texture_storage_2d(TextureFormat::R16Float, StorageTextureAccess::WriteOnly),
421                    texture_storage_2d(TextureFormat::R32Uint, StorageTextureAccess::WriteOnly),
422                    uniform_buffer::<GlobalsUniform>(false),
423                    uniform_buffer::<f32>(false),
424                ),
425            ),
426        );
427
428        let spatial_denoise_bind_group_layout = render_device.create_bind_group_layout(
429            "ssao_spatial_denoise_bind_group_layout",
430            &BindGroupLayoutEntries::sequential(
431                ShaderStages::COMPUTE,
432                (
433                    texture_2d(TextureSampleType::Float { filterable: false }),
434                    texture_2d(TextureSampleType::Uint),
435                    texture_storage_2d(TextureFormat::R16Float, StorageTextureAccess::WriteOnly),
436                ),
437            ),
438        );
439
440        let preprocess_depth_pipeline =
441            pipeline_cache.queue_compute_pipeline(ComputePipelineDescriptor {
442                label: Some("ssao_preprocess_depth_pipeline".into()),
443                layout: vec![
444                    preprocess_depth_bind_group_layout.clone(),
445                    common_bind_group_layout.clone(),
446                ],
447                push_constant_ranges: vec![],
448                shader: PREPROCESS_DEPTH_SHADER_HANDLE,
449                shader_defs: Vec::new(),
450                entry_point: "preprocess_depth".into(),
451                zero_initialize_workgroup_memory: false,
452            });
453
454        let spatial_denoise_pipeline =
455            pipeline_cache.queue_compute_pipeline(ComputePipelineDescriptor {
456                label: Some("ssao_spatial_denoise_pipeline".into()),
457                layout: vec![
458                    spatial_denoise_bind_group_layout.clone(),
459                    common_bind_group_layout.clone(),
460                ],
461                push_constant_ranges: vec![],
462                shader: SPATIAL_DENOISE_SHADER_HANDLE,
463                shader_defs: Vec::new(),
464                entry_point: "spatial_denoise".into(),
465                zero_initialize_workgroup_memory: false,
466            });
467
468        Self {
469            preprocess_depth_pipeline,
470            spatial_denoise_pipeline,
471
472            common_bind_group_layout,
473            preprocess_depth_bind_group_layout,
474            ssao_bind_group_layout,
475            spatial_denoise_bind_group_layout,
476
477            hilbert_index_lut,
478            point_clamp_sampler,
479            linear_clamp_sampler,
480        }
481    }
482}
483
484#[derive(PartialEq, Eq, Hash, Clone)]
485struct SsaoPipelineKey {
486    quality_level: ScreenSpaceAmbientOcclusionQualityLevel,
487    temporal_jitter: bool,
488}
489
490impl SpecializedComputePipeline for SsaoPipelines {
491    type Key = SsaoPipelineKey;
492
493    fn specialize(&self, key: Self::Key) -> ComputePipelineDescriptor {
494        let (slice_count, samples_per_slice_side) = key.quality_level.sample_counts();
495
496        let mut shader_defs = vec![
497            ShaderDefVal::Int("SLICE_COUNT".to_string(), slice_count as i32),
498            ShaderDefVal::Int(
499                "SAMPLES_PER_SLICE_SIDE".to_string(),
500                samples_per_slice_side as i32,
501            ),
502        ];
503
504        if key.temporal_jitter {
505            shader_defs.push("TEMPORAL_JITTER".into());
506        }
507
508        ComputePipelineDescriptor {
509            label: Some("ssao_ssao_pipeline".into()),
510            layout: vec![
511                self.ssao_bind_group_layout.clone(),
512                self.common_bind_group_layout.clone(),
513            ],
514            push_constant_ranges: vec![],
515            shader: SSAO_SHADER_HANDLE,
516            shader_defs,
517            entry_point: "ssao".into(),
518            zero_initialize_workgroup_memory: false,
519        }
520    }
521}
522
523fn extract_ssao_settings(
524    mut commands: Commands,
525    cameras: Extract<
526        Query<
527            (RenderEntity, &Camera, &ScreenSpaceAmbientOcclusion, &Msaa),
528            (With<Camera3d>, With<DepthPrepass>, With<NormalPrepass>),
529        >,
530    >,
531) {
532    for (entity, camera, ssao_settings, msaa) in &cameras {
533        if *msaa != Msaa::Off {
534            error!(
535                "SSAO is being used which requires Msaa::Off, but Msaa is currently set to Msaa::{:?}",
536                *msaa
537            );
538            return;
539        }
540        let mut entity_commands = commands
541            .get_entity(entity)
542            .expect("SSAO entity wasn't synced.");
543        if camera.is_active {
544            entity_commands.insert(ssao_settings.clone());
545        } else {
546            entity_commands.remove::<ScreenSpaceAmbientOcclusion>();
547        }
548    }
549}
550
551#[derive(Component)]
552pub struct ScreenSpaceAmbientOcclusionResources {
553    preprocessed_depth_texture: CachedTexture,
554    ssao_noisy_texture: CachedTexture, // Pre-spatially denoised texture
555    pub screen_space_ambient_occlusion_texture: CachedTexture, // Spatially denoised texture
556    depth_differences_texture: CachedTexture,
557    thickness_buffer: Buffer,
558}
559
560fn prepare_ssao_textures(
561    mut commands: Commands,
562    mut texture_cache: ResMut<TextureCache>,
563    render_device: Res<RenderDevice>,
564    views: Query<(Entity, &ExtractedCamera, &ScreenSpaceAmbientOcclusion)>,
565) {
566    for (entity, camera, ssao_settings) in &views {
567        let Some(physical_viewport_size) = camera.physical_viewport_size else {
568            continue;
569        };
570        let size = Extent3d {
571            width: physical_viewport_size.x,
572            height: physical_viewport_size.y,
573            depth_or_array_layers: 1,
574        };
575
576        let preprocessed_depth_texture = texture_cache.get(
577            &render_device,
578            TextureDescriptor {
579                label: Some("ssao_preprocessed_depth_texture"),
580                size,
581                mip_level_count: 5,
582                sample_count: 1,
583                dimension: TextureDimension::D2,
584                format: TextureFormat::R16Float,
585                usage: TextureUsages::STORAGE_BINDING | TextureUsages::TEXTURE_BINDING,
586                view_formats: &[],
587            },
588        );
589
590        let ssao_noisy_texture = texture_cache.get(
591            &render_device,
592            TextureDescriptor {
593                label: Some("ssao_noisy_texture"),
594                size,
595                mip_level_count: 1,
596                sample_count: 1,
597                dimension: TextureDimension::D2,
598                format: TextureFormat::R16Float,
599                usage: TextureUsages::STORAGE_BINDING | TextureUsages::TEXTURE_BINDING,
600                view_formats: &[],
601            },
602        );
603
604        let ssao_texture = texture_cache.get(
605            &render_device,
606            TextureDescriptor {
607                label: Some("ssao_texture"),
608                size,
609                mip_level_count: 1,
610                sample_count: 1,
611                dimension: TextureDimension::D2,
612                format: TextureFormat::R16Float,
613                usage: TextureUsages::STORAGE_BINDING | TextureUsages::TEXTURE_BINDING,
614                view_formats: &[],
615            },
616        );
617
618        let depth_differences_texture = texture_cache.get(
619            &render_device,
620            TextureDescriptor {
621                label: Some("ssao_depth_differences_texture"),
622                size,
623                mip_level_count: 1,
624                sample_count: 1,
625                dimension: TextureDimension::D2,
626                format: TextureFormat::R32Uint,
627                usage: TextureUsages::STORAGE_BINDING | TextureUsages::TEXTURE_BINDING,
628                view_formats: &[],
629            },
630        );
631
632        let thickness_buffer = render_device.create_buffer_with_data(&BufferInitDescriptor {
633            label: Some("thickness_buffer"),
634            contents: &ssao_settings.constant_object_thickness.to_le_bytes(),
635            usage: BufferUsages::UNIFORM,
636        });
637
638        commands
639            .entity(entity)
640            .insert(ScreenSpaceAmbientOcclusionResources {
641                preprocessed_depth_texture,
642                ssao_noisy_texture,
643                screen_space_ambient_occlusion_texture: ssao_texture,
644                depth_differences_texture,
645                thickness_buffer,
646            });
647    }
648}
649
650#[derive(Component)]
651struct SsaoPipelineId(CachedComputePipelineId);
652
653fn prepare_ssao_pipelines(
654    mut commands: Commands,
655    pipeline_cache: Res<PipelineCache>,
656    mut pipelines: ResMut<SpecializedComputePipelines<SsaoPipelines>>,
657    pipeline: Res<SsaoPipelines>,
658    views: Query<(Entity, &ScreenSpaceAmbientOcclusion, Has<TemporalJitter>)>,
659) {
660    for (entity, ssao_settings, temporal_jitter) in &views {
661        let pipeline_id = pipelines.specialize(
662            &pipeline_cache,
663            &pipeline,
664            SsaoPipelineKey {
665                quality_level: ssao_settings.quality_level,
666                temporal_jitter,
667            },
668        );
669
670        commands.entity(entity).insert(SsaoPipelineId(pipeline_id));
671    }
672}
673
674#[derive(Component)]
675struct SsaoBindGroups {
676    common_bind_group: BindGroup,
677    preprocess_depth_bind_group: BindGroup,
678    ssao_bind_group: BindGroup,
679    spatial_denoise_bind_group: BindGroup,
680}
681
682fn prepare_ssao_bind_groups(
683    mut commands: Commands,
684    render_device: Res<RenderDevice>,
685    pipelines: Res<SsaoPipelines>,
686    view_uniforms: Res<ViewUniforms>,
687    global_uniforms: Res<GlobalsBuffer>,
688    views: Query<(
689        Entity,
690        &ScreenSpaceAmbientOcclusionResources,
691        &ViewPrepassTextures,
692    )>,
693) {
694    let (Some(view_uniforms), Some(globals_uniforms)) = (
695        view_uniforms.uniforms.binding(),
696        global_uniforms.buffer.binding(),
697    ) else {
698        return;
699    };
700
701    for (entity, ssao_resources, prepass_textures) in &views {
702        let common_bind_group = render_device.create_bind_group(
703            "ssao_common_bind_group",
704            &pipelines.common_bind_group_layout,
705            &BindGroupEntries::sequential((
706                &pipelines.point_clamp_sampler,
707                &pipelines.linear_clamp_sampler,
708                view_uniforms.clone(),
709            )),
710        );
711
712        let create_depth_view = |mip_level| {
713            ssao_resources
714                .preprocessed_depth_texture
715                .texture
716                .create_view(&TextureViewDescriptor {
717                    label: Some("ssao_preprocessed_depth_texture_mip_view"),
718                    base_mip_level: mip_level,
719                    format: Some(TextureFormat::R16Float),
720                    dimension: Some(TextureViewDimension::D2),
721                    mip_level_count: Some(1),
722                    ..default()
723                })
724        };
725
726        let preprocess_depth_bind_group = render_device.create_bind_group(
727            "ssao_preprocess_depth_bind_group",
728            &pipelines.preprocess_depth_bind_group_layout,
729            &BindGroupEntries::sequential((
730                prepass_textures.depth_view().unwrap(),
731                &create_depth_view(0),
732                &create_depth_view(1),
733                &create_depth_view(2),
734                &create_depth_view(3),
735                &create_depth_view(4),
736            )),
737        );
738
739        let ssao_bind_group = render_device.create_bind_group(
740            "ssao_ssao_bind_group",
741            &pipelines.ssao_bind_group_layout,
742            &BindGroupEntries::sequential((
743                &ssao_resources.preprocessed_depth_texture.default_view,
744                prepass_textures.normal_view().unwrap(),
745                &pipelines.hilbert_index_lut,
746                &ssao_resources.ssao_noisy_texture.default_view,
747                &ssao_resources.depth_differences_texture.default_view,
748                globals_uniforms.clone(),
749                ssao_resources.thickness_buffer.as_entire_binding(),
750            )),
751        );
752
753        let spatial_denoise_bind_group = render_device.create_bind_group(
754            "ssao_spatial_denoise_bind_group",
755            &pipelines.spatial_denoise_bind_group_layout,
756            &BindGroupEntries::sequential((
757                &ssao_resources.ssao_noisy_texture.default_view,
758                &ssao_resources.depth_differences_texture.default_view,
759                &ssao_resources
760                    .screen_space_ambient_occlusion_texture
761                    .default_view,
762            )),
763        );
764
765        commands.entity(entity).insert(SsaoBindGroups {
766            common_bind_group,
767            preprocess_depth_bind_group,
768            ssao_bind_group,
769            spatial_denoise_bind_group,
770        });
771    }
772}
773
774#[allow(clippy::needless_range_loop)]
775fn generate_hilbert_index_lut() -> [[u16; 64]; 64] {
776    let mut t = [[0; 64]; 64];
777
778    for x in 0..64 {
779        for y in 0..64 {
780            t[x][y] = hilbert_index(x as u16, y as u16);
781        }
782    }
783
784    t
785}
786
787// https://www.shadertoy.com/view/3tB3z3
788const HILBERT_WIDTH: u16 = 64;
789fn hilbert_index(mut x: u16, mut y: u16) -> u16 {
790    let mut index = 0;
791
792    let mut level: u16 = HILBERT_WIDTH / 2;
793    while level > 0 {
794        let region_x = (x & level > 0) as u16;
795        let region_y = (y & level > 0) as u16;
796        index += level * level * ((3 * region_x) ^ region_y);
797
798        if region_y == 0 {
799            if region_x == 1 {
800                x = HILBERT_WIDTH - 1 - x;
801                y = HILBERT_WIDTH - 1 - y;
802            }
803
804            mem::swap(&mut x, &mut y);
805        }
806
807        level /= 2;
808    }
809
810    index
811}