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
42pub 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#[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 pub quality_level: ScreenSpaceAmbientOcclusionQualityLevel,
120 pub radius: f32,
128 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 slice_count: u32,
164 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), Self::Medium => (2, 2), Self::High => (3, 3), Self::Ultra => (9, 3), 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 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, pub screen_space_ambient_occlusion_texture: CachedTexture, 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#[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#[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
766const 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}