Skip to main content

bevy_pbr/ssao/
mod.rs

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