From ba7907cae7d59f8df20bd3e991e8d5cab0734c26 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Drago=C8=99=20Tiselice?= Date: Wed, 2 Oct 2024 16:43:35 +0300 Subject: [PATCH] Added visibility bitmask as an alternative SSAO method (#13454) Early implementation. I still have to fix the documentation and consider writing a small migration guide. Questions left to answer: * [x] should thickness be an overridable constant? * [x] is there a better way to implement `Eq`/`Hash` for `SSAOMethod`? * [x] do we want to keep the linear sampler for the depth texture? * [x] is there a better way to separate the logic than preprocessor macros? ![vbao](https://github.com/bevyengine/bevy/assets/4136413/2a8a0389-2add-4c2e-be37-e208e52dcd25) ## Migration guide SSAO algorithm was changed from GTAO to VBAO (visibility bitmasks). A new field, `constant_object_thickness`, was added to `ScreenSpaceAmbientOcclusion`. `ScreenSpaceAmbientOcclusion` also lost its `Eq` and `Hash` implementations. --------- Co-authored-by: JMS55 <47158642+JMS55@users.noreply.github.com> --- .../src/deferred/deferred_lighting.wgsl | 4 +- .../bevy_pbr/src/render/mesh_view_bindings.rs | 8 +- crates/bevy_pbr/src/render/pbr_fragment.wgsl | 4 +- crates/bevy_pbr/src/ssao/mod.rs | 132 ++++++++++++------ .../bevy_pbr/src/ssao/preprocess_depth.wgsl | 3 +- crates/bevy_pbr/src/ssao/spatial_denoise.wgsl | 3 +- .../src/ssao/{gtao.wgsl => ssao.wgsl} | 86 ++++++++---- .../ssao/{gtao_utils.wgsl => ssao_utils.wgsl} | 4 +- examples/3d/ssao.rs | 27 ++++ 9 files changed, 184 insertions(+), 87 deletions(-) rename crates/bevy_pbr/src/ssao/{gtao.wgsl => ssao.wgsl} (75%) rename crates/bevy_pbr/src/ssao/{gtao_utils.wgsl => ssao_utils.wgsl} (87%) diff --git a/crates/bevy_pbr/src/deferred/deferred_lighting.wgsl b/crates/bevy_pbr/src/deferred/deferred_lighting.wgsl index de191ce295..843ed2bbf6 100644 --- a/crates/bevy_pbr/src/deferred/deferred_lighting.wgsl +++ b/crates/bevy_pbr/src/deferred/deferred_lighting.wgsl @@ -10,7 +10,7 @@ #ifdef SCREEN_SPACE_AMBIENT_OCCLUSION #import bevy_pbr::mesh_view_bindings::screen_space_ambient_occlusion_texture -#import bevy_pbr::gtao_utils::gtao_multibounce +#import bevy_pbr::ssao_utils::ssao_multibounce #endif struct FullscreenVertexOutput { @@ -64,7 +64,7 @@ fn fragment(in: FullscreenVertexOutput) -> @location(0) vec4 { #ifdef SCREEN_SPACE_AMBIENT_OCCLUSION let ssao = textureLoad(screen_space_ambient_occlusion_texture, vec2(in.position.xy), 0i).r; - let ssao_multibounce = gtao_multibounce(ssao, pbr_input.material.base_color.rgb); + let ssao_multibounce = ssao_multibounce(ssao, pbr_input.material.base_color.rgb); pbr_input.diffuse_occlusion = min(pbr_input.diffuse_occlusion, ssao_multibounce); // Neubelt and Pettineo 2013, "Crafting a Next-gen Material Pipeline for The Order: 1886" diff --git a/crates/bevy_pbr/src/render/mesh_view_bindings.rs b/crates/bevy_pbr/src/render/mesh_view_bindings.rs index f44999b8b6..0973b65136 100644 --- a/crates/bevy_pbr/src/render/mesh_view_bindings.rs +++ b/crates/bevy_pbr/src/render/mesh_view_bindings.rs @@ -44,7 +44,7 @@ use crate::{ }, prepass, EnvironmentMapUniformBuffer, FogMeta, GlobalClusterableObjectMeta, GpuClusterableObjects, GpuFog, GpuLights, LightMeta, LightProbesBuffer, LightProbesUniform, - MeshPipeline, MeshPipelineKey, RenderViewLightProbes, ScreenSpaceAmbientOcclusionTextures, + MeshPipeline, MeshPipelineKey, RenderViewLightProbes, ScreenSpaceAmbientOcclusionResources, ScreenSpaceReflectionsBuffer, ScreenSpaceReflectionsUniform, ShadowSamplers, ViewClusterBindings, ViewShadowBindings, CLUSTERED_FORWARD_STORAGE_BUFFER_COUNT, }; @@ -462,7 +462,7 @@ pub fn prepare_mesh_view_bind_groups( &ViewShadowBindings, &ViewClusterBindings, &Msaa, - Option<&ScreenSpaceAmbientOcclusionTextures>, + Option<&ScreenSpaceAmbientOcclusionResources>, Option<&ViewPrepassTextures>, Option<&ViewTransmissionTexture>, &Tonemapping, @@ -507,7 +507,7 @@ pub fn prepare_mesh_view_bind_groups( shadow_bindings, cluster_bindings, msaa, - ssao_textures, + ssao_resources, prepass_textures, transmission_texture, tonemapping, @@ -519,7 +519,7 @@ pub fn prepare_mesh_view_bind_groups( .image_for_samplecount(1, TextureFormat::bevy_default()) .texture_view .clone(); - let ssao_view = ssao_textures + let ssao_view = ssao_resources .map(|t| &t.screen_space_ambient_occlusion_texture.default_view) .unwrap_or(&fallback_ssao); diff --git a/crates/bevy_pbr/src/render/pbr_fragment.wgsl b/crates/bevy_pbr/src/render/pbr_fragment.wgsl index 2dca49ecbc..91e104ede5 100644 --- a/crates/bevy_pbr/src/render/pbr_fragment.wgsl +++ b/crates/bevy_pbr/src/render/pbr_fragment.wgsl @@ -15,7 +15,7 @@ #ifdef SCREEN_SPACE_AMBIENT_OCCLUSION #import bevy_pbr::mesh_view_bindings::screen_space_ambient_occlusion_texture -#import bevy_pbr::gtao_utils::gtao_multibounce +#import bevy_pbr::ssao_utils::ssao_multibounce #endif #ifdef MESHLET_MESH_MATERIAL_PASS @@ -344,7 +344,7 @@ fn pbr_input_from_standard_material( #endif #ifdef SCREEN_SPACE_AMBIENT_OCCLUSION let ssao = textureLoad(screen_space_ambient_occlusion_texture, vec2(in.position.xy), 0i).r; - let ssao_multibounce = gtao_multibounce(ssao, pbr_input.material.base_color.rgb); + let ssao_multibounce = ssao_multibounce(ssao, pbr_input.material.base_color.rgb); diffuse_occlusion = min(diffuse_occlusion, ssao_multibounce); // Use SSAO to estimate the specular occlusion. // Lagarde and Rousiers 2014, "Moving Frostbite to Physically Based Rendering" diff --git a/crates/bevy_pbr/src/ssao/mod.rs b/crates/bevy_pbr/src/ssao/mod.rs index dc10f09c70..a37fb9c799 100644 --- a/crates/bevy_pbr/src/ssao/mod.rs +++ b/crates/bevy_pbr/src/ssao/mod.rs @@ -42,9 +42,9 @@ use bevy_utils::{ use core::mem; const PREPROCESS_DEPTH_SHADER_HANDLE: Handle = Handle::weak_from_u128(102258915420479); -const GTAO_SHADER_HANDLE: Handle = Handle::weak_from_u128(253938746510568); +const SSAO_SHADER_HANDLE: Handle = Handle::weak_from_u128(253938746510568); const SPATIAL_DENOISE_SHADER_HANDLE: Handle = Handle::weak_from_u128(466162052558226); -const GTAO_UTILS_SHADER_HANDLE: Handle = Handle::weak_from_u128(366465052568786); +const SSAO_UTILS_SHADER_HANDLE: Handle = Handle::weak_from_u128(366465052568786); /// Plugin for screen space ambient occlusion. pub struct ScreenSpaceAmbientOcclusionPlugin; @@ -57,7 +57,7 @@ impl Plugin for ScreenSpaceAmbientOcclusionPlugin { "preprocess_depth.wgsl", Shader::from_wgsl ); - load_internal_asset!(app, GTAO_SHADER_HANDLE, "gtao.wgsl", Shader::from_wgsl); + load_internal_asset!(app, SSAO_SHADER_HANDLE, "ssao.wgsl", Shader::from_wgsl); load_internal_asset!( app, SPATIAL_DENOISE_SHADER_HANDLE, @@ -66,8 +66,8 @@ impl Plugin for ScreenSpaceAmbientOcclusionPlugin { ); load_internal_asset!( app, - GTAO_UTILS_SHADER_HANDLE, - "gtao_utils.wgsl", + SSAO_UTILS_SHADER_HANDLE, + "ssao_utils.wgsl", Shader::from_wgsl ); @@ -158,13 +158,28 @@ pub struct ScreenSpaceAmbientOcclusionBundle { /// TAA ([`bevy_core_pipeline::experimental::taa::TemporalAntiAliasing`]). /// Doing so greatly reduces SSAO noise. /// -/// SSAO is not supported on `WebGL2`, and is not currently supported on `WebGPU` or `DirectX12`. -#[derive(Component, ExtractComponent, Reflect, PartialEq, Eq, Hash, Clone, Default, Debug)] -#[reflect(Component, Debug, Default, Hash, PartialEq)] +/// SSAO is not supported on `WebGL2`, and is not currently supported on `WebGPU`. +#[derive(Component, ExtractComponent, Reflect, PartialEq, Clone, Debug)] +#[reflect(Component, Debug, Default, PartialEq)] #[require(DepthPrepass, NormalPrepass)] #[doc(alias = "Ssao")] pub struct ScreenSpaceAmbientOcclusion { + /// Quality of the SSAO effect. pub quality_level: ScreenSpaceAmbientOcclusionQualityLevel, + /// A constant estimated thickness of objects. + /// + /// This value is used to decide how far behind an object a ray of light needs to be in order + /// to pass behind it. Any ray closer than that will be occluded. + pub constant_object_thickness: f32, +} + +impl Default for ScreenSpaceAmbientOcclusion { + fn default() -> Self { + Self { + quality_level: ScreenSpaceAmbientOcclusionQualityLevel::default(), + constant_object_thickness: 0.25, + } + } } #[deprecated(since = "0.15.0", note = "Renamed to `ScreenSpaceAmbientOcclusion`")] @@ -224,7 +239,7 @@ impl ViewNode for SsaoNode { Some(camera_size), Some(preprocess_depth_pipeline), Some(spatial_denoise_pipeline), - Some(gtao_pipeline), + Some(ssao_pipeline), ) = ( camera.physical_viewport_size, pipeline_cache.get_compute_pipeline(pipelines.preprocess_depth_pipeline), @@ -260,21 +275,21 @@ impl ViewNode for SsaoNode { } { - let mut gtao_pass = + let mut ssao_pass = render_context .command_encoder() .begin_compute_pass(&ComputePassDescriptor { - label: Some("ssao_gtao_pass"), + label: Some("ssao_ssao_pass"), timestamp_writes: None, }); - gtao_pass.set_pipeline(gtao_pipeline); - gtao_pass.set_bind_group(0, &bind_groups.gtao_bind_group, &[]); - gtao_pass.set_bind_group( + ssao_pass.set_pipeline(ssao_pipeline); + ssao_pass.set_bind_group(0, &bind_groups.ssao_bind_group, &[]); + ssao_pass.set_bind_group( 1, &bind_groups.common_bind_group, &[view_uniform_offset.offset], ); - gtao_pass.dispatch_workgroups( + ssao_pass.dispatch_workgroups( div_ceil(camera_size.x, 8), div_ceil(camera_size.y, 8), 1, @@ -315,11 +330,12 @@ struct SsaoPipelines { common_bind_group_layout: BindGroupLayout, preprocess_depth_bind_group_layout: BindGroupLayout, - gtao_bind_group_layout: BindGroupLayout, + ssao_bind_group_layout: BindGroupLayout, spatial_denoise_bind_group_layout: BindGroupLayout, hilbert_index_lut: TextureView, point_clamp_sampler: Sampler, + linear_clamp_sampler: Sampler, } impl FromWorld for SsaoPipelines { @@ -358,6 +374,14 @@ impl FromWorld for SsaoPipelines { address_mode_v: AddressMode::ClampToEdge, ..Default::default() }); + let linear_clamp_sampler = render_device.create_sampler(&SamplerDescriptor { + min_filter: FilterMode::Linear, + mag_filter: FilterMode::Linear, + mipmap_filter: FilterMode::Nearest, + address_mode_u: AddressMode::ClampToEdge, + address_mode_v: AddressMode::ClampToEdge, + ..Default::default() + }); let common_bind_group_layout = render_device.create_bind_group_layout( "ssao_common_bind_group_layout", @@ -365,6 +389,7 @@ impl FromWorld for SsaoPipelines { ShaderStages::COMPUTE, ( sampler(SamplerBindingType::NonFiltering), + sampler(SamplerBindingType::Filtering), uniform_buffer::(true), ), ), @@ -385,17 +410,18 @@ impl FromWorld for SsaoPipelines { ), ); - let gtao_bind_group_layout = render_device.create_bind_group_layout( - "ssao_gtao_bind_group_layout", + let ssao_bind_group_layout = render_device.create_bind_group_layout( + "ssao_ssao_bind_group_layout", &BindGroupLayoutEntries::sequential( ShaderStages::COMPUTE, ( - texture_2d(TextureSampleType::Float { filterable: false }), + texture_2d(TextureSampleType::Float { filterable: true }), texture_2d(TextureSampleType::Float { filterable: false }), texture_2d(TextureSampleType::Uint), texture_storage_2d(TextureFormat::R16Float, StorageTextureAccess::WriteOnly), texture_storage_2d(TextureFormat::R32Uint, StorageTextureAccess::WriteOnly), uniform_buffer::(false), + uniform_buffer::(false), ), ), ); @@ -444,18 +470,19 @@ impl FromWorld for SsaoPipelines { common_bind_group_layout, preprocess_depth_bind_group_layout, - gtao_bind_group_layout, + ssao_bind_group_layout, spatial_denoise_bind_group_layout, hilbert_index_lut, point_clamp_sampler, + linear_clamp_sampler, } } } #[derive(PartialEq, Eq, Hash, Clone)] struct SsaoPipelineKey { - ssao_settings: ScreenSpaceAmbientOcclusion, + quality_level: ScreenSpaceAmbientOcclusionQualityLevel, temporal_jitter: bool, } @@ -463,7 +490,7 @@ impl SpecializedComputePipeline for SsaoPipelines { type Key = SsaoPipelineKey; fn specialize(&self, key: Self::Key) -> ComputePipelineDescriptor { - let (slice_count, samples_per_slice_side) = key.ssao_settings.quality_level.sample_counts(); + let (slice_count, samples_per_slice_side) = key.quality_level.sample_counts(); let mut shader_defs = vec![ ShaderDefVal::Int("SLICE_COUNT".to_string(), slice_count as i32), @@ -478,15 +505,15 @@ impl SpecializedComputePipeline for SsaoPipelines { } ComputePipelineDescriptor { - label: Some("ssao_gtao_pipeline".into()), + label: Some("ssao_ssao_pipeline".into()), layout: vec![ - self.gtao_bind_group_layout.clone(), + self.ssao_bind_group_layout.clone(), self.common_bind_group_layout.clone(), ], push_constant_ranges: vec![], - shader: GTAO_SHADER_HANDLE, + shader: SSAO_SHADER_HANDLE, shader_defs, - entry_point: "gtao".into(), + entry_point: "ssao".into(), } } } @@ -517,20 +544,21 @@ fn extract_ssao_settings( } #[derive(Component)] -pub struct ScreenSpaceAmbientOcclusionTextures { +pub struct ScreenSpaceAmbientOcclusionResources { preprocessed_depth_texture: CachedTexture, ssao_noisy_texture: CachedTexture, // Pre-spatially denoised texture pub screen_space_ambient_occlusion_texture: CachedTexture, // Spatially denoised texture depth_differences_texture: CachedTexture, + thickness_buffer: Buffer, } fn prepare_ssao_textures( mut commands: Commands, mut texture_cache: ResMut, render_device: Res, - views: Query<(Entity, &ExtractedCamera), With>, + views: Query<(Entity, &ExtractedCamera, &ScreenSpaceAmbientOcclusion)>, ) { - for (entity, camera) in &views { + for (entity, camera, ssao_settings) in &views { let Some(physical_viewport_size) = camera.physical_viewport_size else { continue; }; @@ -596,13 +624,20 @@ fn prepare_ssao_textures( }, ); + let thickness_buffer = render_device.create_buffer_with_data(&BufferInitDescriptor { + label: Some("thickness_buffer"), + contents: &ssao_settings.constant_object_thickness.to_le_bytes(), + usage: BufferUsages::UNIFORM, + }); + commands .entity(entity) - .insert(ScreenSpaceAmbientOcclusionTextures { + .insert(ScreenSpaceAmbientOcclusionResources { preprocessed_depth_texture, ssao_noisy_texture, screen_space_ambient_occlusion_texture: ssao_texture, depth_differences_texture, + thickness_buffer, }); } } @@ -622,7 +657,7 @@ fn prepare_ssao_pipelines( &pipeline_cache, &pipeline, SsaoPipelineKey { - ssao_settings: ssao_settings.clone(), + quality_level: ssao_settings.quality_level, temporal_jitter, }, ); @@ -635,7 +670,7 @@ fn prepare_ssao_pipelines( struct SsaoBindGroups { common_bind_group: BindGroup, preprocess_depth_bind_group: BindGroup, - gtao_bind_group: BindGroup, + ssao_bind_group: BindGroup, spatial_denoise_bind_group: BindGroup, } @@ -647,7 +682,7 @@ fn prepare_ssao_bind_groups( global_uniforms: Res, views: Query<( Entity, - &ScreenSpaceAmbientOcclusionTextures, + &ScreenSpaceAmbientOcclusionResources, &ViewPrepassTextures, )>, ) { @@ -658,15 +693,19 @@ fn prepare_ssao_bind_groups( return; }; - for (entity, ssao_textures, prepass_textures) in &views { + for (entity, ssao_resources, prepass_textures) in &views { let common_bind_group = render_device.create_bind_group( "ssao_common_bind_group", &pipelines.common_bind_group_layout, - &BindGroupEntries::sequential((&pipelines.point_clamp_sampler, view_uniforms.clone())), + &BindGroupEntries::sequential(( + &pipelines.point_clamp_sampler, + &pipelines.linear_clamp_sampler, + view_uniforms.clone(), + )), ); let create_depth_view = |mip_level| { - ssao_textures + ssao_resources .preprocessed_depth_texture .texture .create_view(&TextureViewDescriptor { @@ -692,16 +731,17 @@ fn prepare_ssao_bind_groups( )), ); - let gtao_bind_group = render_device.create_bind_group( - "ssao_gtao_bind_group", - &pipelines.gtao_bind_group_layout, + let ssao_bind_group = render_device.create_bind_group( + "ssao_ssao_bind_group", + &pipelines.ssao_bind_group_layout, &BindGroupEntries::sequential(( - &ssao_textures.preprocessed_depth_texture.default_view, + &ssao_resources.preprocessed_depth_texture.default_view, prepass_textures.normal_view().unwrap(), &pipelines.hilbert_index_lut, - &ssao_textures.ssao_noisy_texture.default_view, - &ssao_textures.depth_differences_texture.default_view, + &ssao_resources.ssao_noisy_texture.default_view, + &ssao_resources.depth_differences_texture.default_view, globals_uniforms.clone(), + ssao_resources.thickness_buffer.as_entire_binding(), )), ); @@ -709,9 +749,9 @@ fn prepare_ssao_bind_groups( "ssao_spatial_denoise_bind_group", &pipelines.spatial_denoise_bind_group_layout, &BindGroupEntries::sequential(( - &ssao_textures.ssao_noisy_texture.default_view, - &ssao_textures.depth_differences_texture.default_view, - &ssao_textures + &ssao_resources.ssao_noisy_texture.default_view, + &ssao_resources.depth_differences_texture.default_view, + &ssao_resources .screen_space_ambient_occlusion_texture .default_view, )), @@ -720,7 +760,7 @@ fn prepare_ssao_bind_groups( commands.entity(entity).insert(SsaoBindGroups { common_bind_group, preprocess_depth_bind_group, - gtao_bind_group, + ssao_bind_group, spatial_denoise_bind_group, }); } diff --git a/crates/bevy_pbr/src/ssao/preprocess_depth.wgsl b/crates/bevy_pbr/src/ssao/preprocess_depth.wgsl index 73dccaa02c..a386b09d9c 100644 --- a/crates/bevy_pbr/src/ssao/preprocess_depth.wgsl +++ b/crates/bevy_pbr/src/ssao/preprocess_depth.wgsl @@ -14,7 +14,8 @@ @group(0) @binding(4) var preprocessed_depth_mip3: texture_storage_2d; @group(0) @binding(5) var preprocessed_depth_mip4: texture_storage_2d; @group(1) @binding(0) var point_clamp_sampler: sampler; -@group(1) @binding(1) var view: View; +@group(1) @binding(1) var linear_clamp_sampler: sampler; +@group(1) @binding(2) var view: View; // Using 4 depths from the previous MIP, compute a weighted average for the depth of the current MIP diff --git a/crates/bevy_pbr/src/ssao/spatial_denoise.wgsl b/crates/bevy_pbr/src/ssao/spatial_denoise.wgsl index 2448db309f..1c04f9cfab 100644 --- a/crates/bevy_pbr/src/ssao/spatial_denoise.wgsl +++ b/crates/bevy_pbr/src/ssao/spatial_denoise.wgsl @@ -15,7 +15,8 @@ @group(0) @binding(1) var depth_differences: texture_2d; @group(0) @binding(2) var ambient_occlusion: texture_storage_2d; @group(1) @binding(0) var point_clamp_sampler: sampler; -@group(1) @binding(1) var view: View; +@group(1) @binding(1) var linear_clamp_sampler: sampler; +@group(1) @binding(2) var view: View; @compute @workgroup_size(8, 8, 1) diff --git a/crates/bevy_pbr/src/ssao/gtao.wgsl b/crates/bevy_pbr/src/ssao/ssao.wgsl similarity index 75% rename from crates/bevy_pbr/src/ssao/gtao.wgsl rename to crates/bevy_pbr/src/ssao/ssao.wgsl index ada9f1d123..1fbd73e8d9 100644 --- a/crates/bevy_pbr/src/ssao/gtao.wgsl +++ b/crates/bevy_pbr/src/ssao/ssao.wgsl @@ -1,11 +1,16 @@ -// Ground Truth-based Ambient Occlusion (GTAO) -// Paper: https://www.activision.com/cdn/research/Practical_Real_Time_Strategies_for_Accurate_Indirect_Occlusion_NEW%20VERSION_COLOR.pdf -// Presentation: https://blog.selfshadow.com/publications/s2016-shading-course/activision/s2016_pbs_activision_occlusion.pdf +// Visibility Bitmask Ambient Occlusion (VBAO) +// Paper: ttps://ar5iv.labs.arxiv.org/html/2301.11376 // Source code heavily based on XeGTAO v1.30 from Intel // https://github.com/GameTechDev/XeGTAO/blob/0d177ce06bfa642f64d8af4de1197ad1bcb862d4/Source/Rendering/Shaders/XeGTAO.hlsli -#import bevy_pbr::gtao_utils::fast_acos +// Source code based on the existing XeGTAO implementation and +// https://cdrinmatane.github.io/posts/ssaovb-code/ + +// Source code base on SSRT3 implementation +// https://github.com/cdrinmatane/SSRT3 + +#import bevy_pbr::ssao_utils::fast_acos #import bevy_render::{ view::View, @@ -19,8 +24,10 @@ @group(0) @binding(3) var ambient_occlusion: texture_storage_2d; @group(0) @binding(4) var depth_differences: texture_storage_2d; @group(0) @binding(5) var globals: Globals; +@group(0) @binding(6) var thickness: f32; @group(1) @binding(0) var point_clamp_sampler: sampler; -@group(1) @binding(1) var view: View; +@group(1) @binding(1) var linear_clamp_sampler: sampler; +@group(1) @binding(2) var view: View; fn load_noise(pixel_coordinates: vec2) -> vec2 { var index = textureLoad(hilbert_index_lut, pixel_coordinates % 64, 0).r; @@ -81,13 +88,46 @@ fn reconstruct_view_space_position(depth: f32, uv: vec2) -> vec3 { } fn load_and_reconstruct_view_space_position(uv: vec2, sample_mip_level: f32) -> vec3 { - let depth = textureSampleLevel(preprocessed_depth, point_clamp_sampler, uv, sample_mip_level).r; + let depth = textureSampleLevel(preprocessed_depth, linear_clamp_sampler, uv, sample_mip_level).r; return reconstruct_view_space_position(depth, uv); } +fn updateSectors( + min_horizon: f32, + max_horizon: f32, + samples_per_slice: f32, + bitmask: u32, +) -> u32 { + let start_horizon = u32(min_horizon * samples_per_slice); + let angle_horizon = u32(ceil((max_horizon - min_horizon) * samples_per_slice)); + + return insertBits(bitmask, 0xFFFFFFFFu, start_horizon, angle_horizon); +} + +fn processSample( + delta_position: vec3, + view_vec: vec3, + sampling_direction: f32, + n: vec2, + samples_per_slice: f32, + bitmask: ptr, +) { + let delta_position_back_face = delta_position - view_vec * thickness; + + var front_back_horizon = vec2( + fast_acos(dot(normalize(delta_position), view_vec)), + fast_acos(dot(normalize(delta_position_back_face), view_vec)), + ); + + front_back_horizon = saturate(fma(vec2(sampling_direction), -front_back_horizon, n)); + front_back_horizon = select(front_back_horizon.xy, front_back_horizon.yx, sampling_direction >= 0.0); + + *bitmask = updateSectors(front_back_horizon.x, front_back_horizon.y, samples_per_slice, *bitmask); +} + @compute @workgroup_size(8, 8, 1) -fn gtao(@builtin(global_invocation_id) global_id: vec3) { +fn ssao(@builtin(global_invocation_id) global_id: vec3) { let slice_count = f32(#SLICE_COUNT); let samples_per_slice_side = f32(#SAMPLES_PER_SLICE_SIDE); let effect_radius = 0.5 * 1.457; @@ -110,6 +150,7 @@ fn gtao(@builtin(global_invocation_id) global_id: vec3) { let sample_scale = (-0.5 * effect_radius * view.clip_from_view[0][0]) / pixel_position.z; var visibility = 0.0; + var occluded_sample_count = 0u; for (var slice_t = 0.0; slice_t < slice_count; slice_t += 1.0) { let slice = slice_t + noise.x; let phi = (PI / slice_count) * slice; @@ -123,12 +164,10 @@ fn gtao(@builtin(global_invocation_id) global_id: vec3) { let sign_norm = sign(dot(orthographic_direction, projected_normal)); let cos_norm = saturate(dot(projected_normal, view_vec) / projected_normal_length); - let n = sign_norm * fast_acos(cos_norm); + let n = vec2((HALF_PI - sign_norm * fast_acos(cos_norm)) * (1.0 / PI)); + + var bitmask = 0u; - let min_cos_horizon_1 = cos(n + HALF_PI); - let min_cos_horizon_2 = cos(n - HALF_PI); - var cos_horizon_1 = min_cos_horizon_1; - var cos_horizon_2 = min_cos_horizon_2; let sample_mul = vec2(omega.x, -omega.y) * sample_scale; for (var sample_t = 0.0; sample_t < samples_per_slice_side; sample_t += 1.0) { var sample_noise = (slice_t + sample_t * samples_per_slice_side) * 0.6180339887498948482; @@ -145,27 +184,16 @@ fn gtao(@builtin(global_invocation_id) global_id: vec3) { let sample_difference_1 = sample_position_1 - pixel_position; let sample_difference_2 = sample_position_2 - pixel_position; - let sample_distance_1 = length(sample_difference_1); - let sample_distance_2 = length(sample_difference_2); - var sample_cos_horizon_1 = dot(sample_difference_1 / sample_distance_1, view_vec); - var sample_cos_horizon_2 = dot(sample_difference_2 / sample_distance_2, view_vec); - let weight_1 = saturate(sample_distance_1 * falloff_mul + falloff_add); - let weight_2 = saturate(sample_distance_2 * falloff_mul + falloff_add); - sample_cos_horizon_1 = mix(min_cos_horizon_1, sample_cos_horizon_1, weight_1); - sample_cos_horizon_2 = mix(min_cos_horizon_2, sample_cos_horizon_2, weight_2); - - cos_horizon_1 = max(cos_horizon_1, sample_cos_horizon_1); - cos_horizon_2 = max(cos_horizon_2, sample_cos_horizon_2); + processSample(sample_difference_1, view_vec, -1.0, n, samples_per_slice_side * 2.0, &bitmask); + processSample(sample_difference_2, view_vec, 1.0, n, samples_per_slice_side * 2.0, &bitmask); } - let horizon_1 = fast_acos(cos_horizon_1); - let horizon_2 = -fast_acos(cos_horizon_2); - let v1 = (cos_norm + 2.0 * horizon_1 * sin(n) - cos(2.0 * horizon_1 - n)) / 4.0; - let v2 = (cos_norm + 2.0 * horizon_2 * sin(n) - cos(2.0 * horizon_2 - n)) / 4.0; - visibility += projected_normal_length * (v1 + v2); + occluded_sample_count += countOneBits(bitmask); } - visibility /= slice_count; + + visibility = 1.0 - f32(occluded_sample_count) / (slice_count * 2.0 * samples_per_slice_side); + visibility = clamp(visibility, 0.03, 1.0); textureStore(ambient_occlusion, pixel_coordinates, vec4(visibility, 0.0, 0.0, 0.0)); diff --git a/crates/bevy_pbr/src/ssao/gtao_utils.wgsl b/crates/bevy_pbr/src/ssao/ssao_utils.wgsl similarity index 87% rename from crates/bevy_pbr/src/ssao/gtao_utils.wgsl rename to crates/bevy_pbr/src/ssao/ssao_utils.wgsl index 32c46e1d1d..ecc5a4a54d 100644 --- a/crates/bevy_pbr/src/ssao/gtao_utils.wgsl +++ b/crates/bevy_pbr/src/ssao/ssao_utils.wgsl @@ -1,10 +1,10 @@ -#define_import_path bevy_pbr::gtao_utils +#define_import_path bevy_pbr::ssao_utils #import bevy_render::maths::{PI, HALF_PI} // Approximates single-bounce ambient occlusion to multi-bounce ambient occlusion // https://blog.selfshadow.com/publications/s2016-shading-course/activision/s2016_pbs_activision_occlusion.pdf#page=78 -fn gtao_multibounce(visibility: f32, base_color: vec3) -> vec3 { +fn ssao_multibounce(visibility: f32, base_color: vec3) -> vec3 { let a = 2.0404 * base_color - 0.3324; let b = -4.7951 * base_color + 0.6417; let c = 2.7552 * base_color + 0.6903; diff --git a/examples/3d/ssao.rs b/examples/3d/ssao.rs index e0b87b36d3..e8b1397848 100644 --- a/examples/3d/ssao.rs +++ b/examples/3d/ssao.rs @@ -109,32 +109,52 @@ fn update( sphere.translation.y = ops::sin(time.elapsed_seconds() / 1.7) * 0.7; let (camera_entity, ssao, temporal_jitter) = camera.single(); + let current_ssao = ssao.cloned().unwrap_or_default(); let mut commands = commands.entity(camera_entity); commands .insert_if( ScreenSpaceAmbientOcclusion { quality_level: ScreenSpaceAmbientOcclusionQualityLevel::Low, + ..current_ssao }, || keycode.just_pressed(KeyCode::Digit2), ) .insert_if( ScreenSpaceAmbientOcclusion { quality_level: ScreenSpaceAmbientOcclusionQualityLevel::Medium, + ..current_ssao }, || keycode.just_pressed(KeyCode::Digit3), ) .insert_if( ScreenSpaceAmbientOcclusion { quality_level: ScreenSpaceAmbientOcclusionQualityLevel::High, + ..current_ssao }, || keycode.just_pressed(KeyCode::Digit4), ) .insert_if( ScreenSpaceAmbientOcclusion { quality_level: ScreenSpaceAmbientOcclusionQualityLevel::Ultra, + ..current_ssao }, || keycode.just_pressed(KeyCode::Digit5), + ) + .insert_if( + ScreenSpaceAmbientOcclusion { + constant_object_thickness: (current_ssao.constant_object_thickness * 2.0).min(4.0), + ..current_ssao + }, + || keycode.just_pressed(KeyCode::ArrowUp), + ) + .insert_if( + ScreenSpaceAmbientOcclusion { + constant_object_thickness: (current_ssao.constant_object_thickness * 0.5) + .max(0.0625), + ..current_ssao + }, + || keycode.just_pressed(KeyCode::ArrowDown), ); if keycode.just_pressed(KeyCode::Digit1) { commands.remove::(); @@ -160,6 +180,13 @@ fn update( _ => unreachable!(), }; + if let Some(thickness) = ssao.map(|s| s.constant_object_thickness) { + text.push_str(&format!( + "Constant object thickness: {} (Up/Down)\n\n", + thickness + )); + } + text.push_str("SSAO Quality:\n"); text.push_str(&format!("(1) {o}Off{o}\n")); text.push_str(&format!("(2) {l}Low{l}\n"));