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