bevy_pbr/ssao/
mod.rs

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