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
44pub 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 Node3d::EndPrepasses,
95 NodePbr::ScreenSpaceAmbientOcclusion,
96 Node3d::StartMainPass,
97 ),
98 );
99 }
100}
101
102#[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 pub quality_level: ScreenSpaceAmbientOcclusionQualityLevel,
127 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 slice_count: u32,
154 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), Self::Medium => (2, 2), Self::High => (3, 3), Self::Ultra => (9, 3), 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 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, pub screen_space_ambient_occlusion_texture: CachedTexture, 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
749const 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}