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