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