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