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>
This commit is contained in:
Dragoș Tiselice 2024-10-02 16:43:35 +03:00 committed by GitHub
parent c841dd92a1
commit ba7907cae7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 184 additions and 87 deletions

View file

@ -10,7 +10,7 @@
#ifdef SCREEN_SPACE_AMBIENT_OCCLUSION #ifdef SCREEN_SPACE_AMBIENT_OCCLUSION
#import bevy_pbr::mesh_view_bindings::screen_space_ambient_occlusion_texture #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 #endif
struct FullscreenVertexOutput { struct FullscreenVertexOutput {
@ -64,7 +64,7 @@ fn fragment(in: FullscreenVertexOutput) -> @location(0) vec4<f32> {
#ifdef SCREEN_SPACE_AMBIENT_OCCLUSION #ifdef SCREEN_SPACE_AMBIENT_OCCLUSION
let ssao = textureLoad(screen_space_ambient_occlusion_texture, vec2<i32>(in.position.xy), 0i).r; let ssao = textureLoad(screen_space_ambient_occlusion_texture, vec2<i32>(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); 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" // Neubelt and Pettineo 2013, "Crafting a Next-gen Material Pipeline for The Order: 1886"

View file

@ -44,7 +44,7 @@ use crate::{
}, },
prepass, EnvironmentMapUniformBuffer, FogMeta, GlobalClusterableObjectMeta, prepass, EnvironmentMapUniformBuffer, FogMeta, GlobalClusterableObjectMeta,
GpuClusterableObjects, GpuFog, GpuLights, LightMeta, LightProbesBuffer, LightProbesUniform, GpuClusterableObjects, GpuFog, GpuLights, LightMeta, LightProbesBuffer, LightProbesUniform,
MeshPipeline, MeshPipelineKey, RenderViewLightProbes, ScreenSpaceAmbientOcclusionTextures, MeshPipeline, MeshPipelineKey, RenderViewLightProbes, ScreenSpaceAmbientOcclusionResources,
ScreenSpaceReflectionsBuffer, ScreenSpaceReflectionsUniform, ShadowSamplers, ScreenSpaceReflectionsBuffer, ScreenSpaceReflectionsUniform, ShadowSamplers,
ViewClusterBindings, ViewShadowBindings, CLUSTERED_FORWARD_STORAGE_BUFFER_COUNT, ViewClusterBindings, ViewShadowBindings, CLUSTERED_FORWARD_STORAGE_BUFFER_COUNT,
}; };
@ -462,7 +462,7 @@ pub fn prepare_mesh_view_bind_groups(
&ViewShadowBindings, &ViewShadowBindings,
&ViewClusterBindings, &ViewClusterBindings,
&Msaa, &Msaa,
Option<&ScreenSpaceAmbientOcclusionTextures>, Option<&ScreenSpaceAmbientOcclusionResources>,
Option<&ViewPrepassTextures>, Option<&ViewPrepassTextures>,
Option<&ViewTransmissionTexture>, Option<&ViewTransmissionTexture>,
&Tonemapping, &Tonemapping,
@ -507,7 +507,7 @@ pub fn prepare_mesh_view_bind_groups(
shadow_bindings, shadow_bindings,
cluster_bindings, cluster_bindings,
msaa, msaa,
ssao_textures, ssao_resources,
prepass_textures, prepass_textures,
transmission_texture, transmission_texture,
tonemapping, tonemapping,
@ -519,7 +519,7 @@ pub fn prepare_mesh_view_bind_groups(
.image_for_samplecount(1, TextureFormat::bevy_default()) .image_for_samplecount(1, TextureFormat::bevy_default())
.texture_view .texture_view
.clone(); .clone();
let ssao_view = ssao_textures let ssao_view = ssao_resources
.map(|t| &t.screen_space_ambient_occlusion_texture.default_view) .map(|t| &t.screen_space_ambient_occlusion_texture.default_view)
.unwrap_or(&fallback_ssao); .unwrap_or(&fallback_ssao);

View file

@ -15,7 +15,7 @@
#ifdef SCREEN_SPACE_AMBIENT_OCCLUSION #ifdef SCREEN_SPACE_AMBIENT_OCCLUSION
#import bevy_pbr::mesh_view_bindings::screen_space_ambient_occlusion_texture #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 #endif
#ifdef MESHLET_MESH_MATERIAL_PASS #ifdef MESHLET_MESH_MATERIAL_PASS
@ -344,7 +344,7 @@ fn pbr_input_from_standard_material(
#endif #endif
#ifdef SCREEN_SPACE_AMBIENT_OCCLUSION #ifdef SCREEN_SPACE_AMBIENT_OCCLUSION
let ssao = textureLoad(screen_space_ambient_occlusion_texture, vec2<i32>(in.position.xy), 0i).r; let ssao = textureLoad(screen_space_ambient_occlusion_texture, vec2<i32>(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); diffuse_occlusion = min(diffuse_occlusion, ssao_multibounce);
// Use SSAO to estimate the specular occlusion. // Use SSAO to estimate the specular occlusion.
// Lagarde and Rousiers 2014, "Moving Frostbite to Physically Based Rendering" // Lagarde and Rousiers 2014, "Moving Frostbite to Physically Based Rendering"

View file

@ -42,9 +42,9 @@ use bevy_utils::{
use core::mem; use core::mem;
const PREPROCESS_DEPTH_SHADER_HANDLE: Handle<Shader> = Handle::weak_from_u128(102258915420479); const PREPROCESS_DEPTH_SHADER_HANDLE: Handle<Shader> = Handle::weak_from_u128(102258915420479);
const GTAO_SHADER_HANDLE: Handle<Shader> = Handle::weak_from_u128(253938746510568); const SSAO_SHADER_HANDLE: Handle<Shader> = Handle::weak_from_u128(253938746510568);
const SPATIAL_DENOISE_SHADER_HANDLE: Handle<Shader> = Handle::weak_from_u128(466162052558226); const SPATIAL_DENOISE_SHADER_HANDLE: Handle<Shader> = Handle::weak_from_u128(466162052558226);
const GTAO_UTILS_SHADER_HANDLE: Handle<Shader> = Handle::weak_from_u128(366465052568786); const SSAO_UTILS_SHADER_HANDLE: Handle<Shader> = Handle::weak_from_u128(366465052568786);
/// Plugin for screen space ambient occlusion. /// Plugin for screen space ambient occlusion.
pub struct ScreenSpaceAmbientOcclusionPlugin; pub struct ScreenSpaceAmbientOcclusionPlugin;
@ -57,7 +57,7 @@ impl Plugin for ScreenSpaceAmbientOcclusionPlugin {
"preprocess_depth.wgsl", "preprocess_depth.wgsl",
Shader::from_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!( load_internal_asset!(
app, app,
SPATIAL_DENOISE_SHADER_HANDLE, SPATIAL_DENOISE_SHADER_HANDLE,
@ -66,8 +66,8 @@ impl Plugin for ScreenSpaceAmbientOcclusionPlugin {
); );
load_internal_asset!( load_internal_asset!(
app, app,
GTAO_UTILS_SHADER_HANDLE, SSAO_UTILS_SHADER_HANDLE,
"gtao_utils.wgsl", "ssao_utils.wgsl",
Shader::from_wgsl Shader::from_wgsl
); );
@ -158,13 +158,28 @@ pub struct ScreenSpaceAmbientOcclusionBundle {
/// TAA ([`bevy_core_pipeline::experimental::taa::TemporalAntiAliasing`]). /// TAA ([`bevy_core_pipeline::experimental::taa::TemporalAntiAliasing`]).
/// Doing so greatly reduces SSAO noise. /// Doing so greatly reduces SSAO noise.
/// ///
/// SSAO is not supported on `WebGL2`, and is not currently supported on `WebGPU` or `DirectX12`. /// SSAO is not supported on `WebGL2`, and is not currently supported on `WebGPU`.
#[derive(Component, ExtractComponent, Reflect, PartialEq, Eq, Hash, Clone, Default, Debug)] #[derive(Component, ExtractComponent, Reflect, PartialEq, Clone, Debug)]
#[reflect(Component, Debug, Default, Hash, PartialEq)] #[reflect(Component, Debug, Default, PartialEq)]
#[require(DepthPrepass, NormalPrepass)] #[require(DepthPrepass, NormalPrepass)]
#[doc(alias = "Ssao")] #[doc(alias = "Ssao")]
pub struct ScreenSpaceAmbientOcclusion { pub struct ScreenSpaceAmbientOcclusion {
/// Quality of the SSAO effect.
pub quality_level: ScreenSpaceAmbientOcclusionQualityLevel, 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`")] #[deprecated(since = "0.15.0", note = "Renamed to `ScreenSpaceAmbientOcclusion`")]
@ -224,7 +239,7 @@ impl ViewNode for SsaoNode {
Some(camera_size), Some(camera_size),
Some(preprocess_depth_pipeline), Some(preprocess_depth_pipeline),
Some(spatial_denoise_pipeline), Some(spatial_denoise_pipeline),
Some(gtao_pipeline), Some(ssao_pipeline),
) = ( ) = (
camera.physical_viewport_size, camera.physical_viewport_size,
pipeline_cache.get_compute_pipeline(pipelines.preprocess_depth_pipeline), 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 render_context
.command_encoder() .command_encoder()
.begin_compute_pass(&ComputePassDescriptor { .begin_compute_pass(&ComputePassDescriptor {
label: Some("ssao_gtao_pass"), label: Some("ssao_ssao_pass"),
timestamp_writes: None, timestamp_writes: None,
}); });
gtao_pass.set_pipeline(gtao_pipeline); ssao_pass.set_pipeline(ssao_pipeline);
gtao_pass.set_bind_group(0, &bind_groups.gtao_bind_group, &[]); ssao_pass.set_bind_group(0, &bind_groups.ssao_bind_group, &[]);
gtao_pass.set_bind_group( ssao_pass.set_bind_group(
1, 1,
&bind_groups.common_bind_group, &bind_groups.common_bind_group,
&[view_uniform_offset.offset], &[view_uniform_offset.offset],
); );
gtao_pass.dispatch_workgroups( ssao_pass.dispatch_workgroups(
div_ceil(camera_size.x, 8), div_ceil(camera_size.x, 8),
div_ceil(camera_size.y, 8), div_ceil(camera_size.y, 8),
1, 1,
@ -315,11 +330,12 @@ struct SsaoPipelines {
common_bind_group_layout: BindGroupLayout, common_bind_group_layout: BindGroupLayout,
preprocess_depth_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, spatial_denoise_bind_group_layout: BindGroupLayout,
hilbert_index_lut: TextureView, hilbert_index_lut: TextureView,
point_clamp_sampler: Sampler, point_clamp_sampler: Sampler,
linear_clamp_sampler: Sampler,
} }
impl FromWorld for SsaoPipelines { impl FromWorld for SsaoPipelines {
@ -358,6 +374,14 @@ impl FromWorld for SsaoPipelines {
address_mode_v: AddressMode::ClampToEdge, address_mode_v: AddressMode::ClampToEdge,
..Default::default() ..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( let common_bind_group_layout = render_device.create_bind_group_layout(
"ssao_common_bind_group_layout", "ssao_common_bind_group_layout",
@ -365,6 +389,7 @@ impl FromWorld for SsaoPipelines {
ShaderStages::COMPUTE, ShaderStages::COMPUTE,
( (
sampler(SamplerBindingType::NonFiltering), sampler(SamplerBindingType::NonFiltering),
sampler(SamplerBindingType::Filtering),
uniform_buffer::<ViewUniform>(true), uniform_buffer::<ViewUniform>(true),
), ),
), ),
@ -385,17 +410,18 @@ impl FromWorld for SsaoPipelines {
), ),
); );
let gtao_bind_group_layout = render_device.create_bind_group_layout( let ssao_bind_group_layout = render_device.create_bind_group_layout(
"ssao_gtao_bind_group_layout", "ssao_ssao_bind_group_layout",
&BindGroupLayoutEntries::sequential( &BindGroupLayoutEntries::sequential(
ShaderStages::COMPUTE, ShaderStages::COMPUTE,
( (
texture_2d(TextureSampleType::Float { filterable: false }), texture_2d(TextureSampleType::Float { filterable: true }),
texture_2d(TextureSampleType::Float { filterable: false }), texture_2d(TextureSampleType::Float { filterable: false }),
texture_2d(TextureSampleType::Uint), texture_2d(TextureSampleType::Uint),
texture_storage_2d(TextureFormat::R16Float, StorageTextureAccess::WriteOnly), texture_storage_2d(TextureFormat::R16Float, StorageTextureAccess::WriteOnly),
texture_storage_2d(TextureFormat::R32Uint, StorageTextureAccess::WriteOnly), texture_storage_2d(TextureFormat::R32Uint, StorageTextureAccess::WriteOnly),
uniform_buffer::<GlobalsUniform>(false), uniform_buffer::<GlobalsUniform>(false),
uniform_buffer::<f32>(false),
), ),
), ),
); );
@ -444,18 +470,19 @@ impl FromWorld for SsaoPipelines {
common_bind_group_layout, common_bind_group_layout,
preprocess_depth_bind_group_layout, preprocess_depth_bind_group_layout,
gtao_bind_group_layout, ssao_bind_group_layout,
spatial_denoise_bind_group_layout, spatial_denoise_bind_group_layout,
hilbert_index_lut, hilbert_index_lut,
point_clamp_sampler, point_clamp_sampler,
linear_clamp_sampler,
} }
} }
} }
#[derive(PartialEq, Eq, Hash, Clone)] #[derive(PartialEq, Eq, Hash, Clone)]
struct SsaoPipelineKey { struct SsaoPipelineKey {
ssao_settings: ScreenSpaceAmbientOcclusion, quality_level: ScreenSpaceAmbientOcclusionQualityLevel,
temporal_jitter: bool, temporal_jitter: bool,
} }
@ -463,7 +490,7 @@ impl SpecializedComputePipeline for SsaoPipelines {
type Key = SsaoPipelineKey; type Key = SsaoPipelineKey;
fn specialize(&self, key: Self::Key) -> ComputePipelineDescriptor { 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![ let mut shader_defs = vec![
ShaderDefVal::Int("SLICE_COUNT".to_string(), slice_count as i32), ShaderDefVal::Int("SLICE_COUNT".to_string(), slice_count as i32),
@ -478,15 +505,15 @@ impl SpecializedComputePipeline for SsaoPipelines {
} }
ComputePipelineDescriptor { ComputePipelineDescriptor {
label: Some("ssao_gtao_pipeline".into()), label: Some("ssao_ssao_pipeline".into()),
layout: vec![ layout: vec![
self.gtao_bind_group_layout.clone(), self.ssao_bind_group_layout.clone(),
self.common_bind_group_layout.clone(), self.common_bind_group_layout.clone(),
], ],
push_constant_ranges: vec![], push_constant_ranges: vec![],
shader: GTAO_SHADER_HANDLE, shader: SSAO_SHADER_HANDLE,
shader_defs, shader_defs,
entry_point: "gtao".into(), entry_point: "ssao".into(),
} }
} }
} }
@ -517,20 +544,21 @@ fn extract_ssao_settings(
} }
#[derive(Component)] #[derive(Component)]
pub struct ScreenSpaceAmbientOcclusionTextures { pub struct ScreenSpaceAmbientOcclusionResources {
preprocessed_depth_texture: CachedTexture, preprocessed_depth_texture: CachedTexture,
ssao_noisy_texture: CachedTexture, // Pre-spatially denoised texture ssao_noisy_texture: CachedTexture, // Pre-spatially denoised texture
pub screen_space_ambient_occlusion_texture: CachedTexture, // Spatially denoised texture pub screen_space_ambient_occlusion_texture: CachedTexture, // Spatially denoised texture
depth_differences_texture: CachedTexture, depth_differences_texture: CachedTexture,
thickness_buffer: Buffer,
} }
fn prepare_ssao_textures( fn prepare_ssao_textures(
mut commands: Commands, mut commands: Commands,
mut texture_cache: ResMut<TextureCache>, mut texture_cache: ResMut<TextureCache>,
render_device: Res<RenderDevice>, render_device: Res<RenderDevice>,
views: Query<(Entity, &ExtractedCamera), With<ScreenSpaceAmbientOcclusion>>, 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 { let Some(physical_viewport_size) = camera.physical_viewport_size else {
continue; 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 commands
.entity(entity) .entity(entity)
.insert(ScreenSpaceAmbientOcclusionTextures { .insert(ScreenSpaceAmbientOcclusionResources {
preprocessed_depth_texture, preprocessed_depth_texture,
ssao_noisy_texture, ssao_noisy_texture,
screen_space_ambient_occlusion_texture: ssao_texture, screen_space_ambient_occlusion_texture: ssao_texture,
depth_differences_texture, depth_differences_texture,
thickness_buffer,
}); });
} }
} }
@ -622,7 +657,7 @@ fn prepare_ssao_pipelines(
&pipeline_cache, &pipeline_cache,
&pipeline, &pipeline,
SsaoPipelineKey { SsaoPipelineKey {
ssao_settings: ssao_settings.clone(), quality_level: ssao_settings.quality_level,
temporal_jitter, temporal_jitter,
}, },
); );
@ -635,7 +670,7 @@ fn prepare_ssao_pipelines(
struct SsaoBindGroups { struct SsaoBindGroups {
common_bind_group: BindGroup, common_bind_group: BindGroup,
preprocess_depth_bind_group: BindGroup, preprocess_depth_bind_group: BindGroup,
gtao_bind_group: BindGroup, ssao_bind_group: BindGroup,
spatial_denoise_bind_group: BindGroup, spatial_denoise_bind_group: BindGroup,
} }
@ -647,7 +682,7 @@ fn prepare_ssao_bind_groups(
global_uniforms: Res<GlobalsBuffer>, global_uniforms: Res<GlobalsBuffer>,
views: Query<( views: Query<(
Entity, Entity,
&ScreenSpaceAmbientOcclusionTextures, &ScreenSpaceAmbientOcclusionResources,
&ViewPrepassTextures, &ViewPrepassTextures,
)>, )>,
) { ) {
@ -658,15 +693,19 @@ fn prepare_ssao_bind_groups(
return; 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( let common_bind_group = render_device.create_bind_group(
"ssao_common_bind_group", "ssao_common_bind_group",
&pipelines.common_bind_group_layout, &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| { let create_depth_view = |mip_level| {
ssao_textures ssao_resources
.preprocessed_depth_texture .preprocessed_depth_texture
.texture .texture
.create_view(&TextureViewDescriptor { .create_view(&TextureViewDescriptor {
@ -692,16 +731,17 @@ fn prepare_ssao_bind_groups(
)), )),
); );
let gtao_bind_group = render_device.create_bind_group( let ssao_bind_group = render_device.create_bind_group(
"ssao_gtao_bind_group", "ssao_ssao_bind_group",
&pipelines.gtao_bind_group_layout, &pipelines.ssao_bind_group_layout,
&BindGroupEntries::sequential(( &BindGroupEntries::sequential((
&ssao_textures.preprocessed_depth_texture.default_view, &ssao_resources.preprocessed_depth_texture.default_view,
prepass_textures.normal_view().unwrap(), prepass_textures.normal_view().unwrap(),
&pipelines.hilbert_index_lut, &pipelines.hilbert_index_lut,
&ssao_textures.ssao_noisy_texture.default_view, &ssao_resources.ssao_noisy_texture.default_view,
&ssao_textures.depth_differences_texture.default_view, &ssao_resources.depth_differences_texture.default_view,
globals_uniforms.clone(), globals_uniforms.clone(),
ssao_resources.thickness_buffer.as_entire_binding(),
)), )),
); );
@ -709,9 +749,9 @@ fn prepare_ssao_bind_groups(
"ssao_spatial_denoise_bind_group", "ssao_spatial_denoise_bind_group",
&pipelines.spatial_denoise_bind_group_layout, &pipelines.spatial_denoise_bind_group_layout,
&BindGroupEntries::sequential(( &BindGroupEntries::sequential((
&ssao_textures.ssao_noisy_texture.default_view, &ssao_resources.ssao_noisy_texture.default_view,
&ssao_textures.depth_differences_texture.default_view, &ssao_resources.depth_differences_texture.default_view,
&ssao_textures &ssao_resources
.screen_space_ambient_occlusion_texture .screen_space_ambient_occlusion_texture
.default_view, .default_view,
)), )),
@ -720,7 +760,7 @@ fn prepare_ssao_bind_groups(
commands.entity(entity).insert(SsaoBindGroups { commands.entity(entity).insert(SsaoBindGroups {
common_bind_group, common_bind_group,
preprocess_depth_bind_group, preprocess_depth_bind_group,
gtao_bind_group, ssao_bind_group,
spatial_denoise_bind_group, spatial_denoise_bind_group,
}); });
} }

View file

@ -14,7 +14,8 @@
@group(0) @binding(4) var preprocessed_depth_mip3: texture_storage_2d<r16float, write>; @group(0) @binding(4) var preprocessed_depth_mip3: texture_storage_2d<r16float, write>;
@group(0) @binding(5) var preprocessed_depth_mip4: texture_storage_2d<r16float, write>; @group(0) @binding(5) var preprocessed_depth_mip4: texture_storage_2d<r16float, write>;
@group(1) @binding(0) var point_clamp_sampler: sampler; @group(1) @binding(0) var point_clamp_sampler: sampler;
@group(1) @binding(1) var<uniform> view: View; @group(1) @binding(1) var linear_clamp_sampler: sampler;
@group(1) @binding(2) var<uniform> view: View;
// Using 4 depths from the previous MIP, compute a weighted average for the depth of the current MIP // Using 4 depths from the previous MIP, compute a weighted average for the depth of the current MIP

View file

@ -15,7 +15,8 @@
@group(0) @binding(1) var depth_differences: texture_2d<u32>; @group(0) @binding(1) var depth_differences: texture_2d<u32>;
@group(0) @binding(2) var ambient_occlusion: texture_storage_2d<r16float, write>; @group(0) @binding(2) var ambient_occlusion: texture_storage_2d<r16float, write>;
@group(1) @binding(0) var point_clamp_sampler: sampler; @group(1) @binding(0) var point_clamp_sampler: sampler;
@group(1) @binding(1) var<uniform> view: View; @group(1) @binding(1) var linear_clamp_sampler: sampler;
@group(1) @binding(2) var<uniform> view: View;
@compute @compute
@workgroup_size(8, 8, 1) @workgroup_size(8, 8, 1)

View file

@ -1,11 +1,16 @@
// Ground Truth-based Ambient Occlusion (GTAO) // Visibility Bitmask Ambient Occlusion (VBAO)
// Paper: https://www.activision.com/cdn/research/Practical_Real_Time_Strategies_for_Accurate_Indirect_Occlusion_NEW%20VERSION_COLOR.pdf // Paper: ttps://ar5iv.labs.arxiv.org/html/2301.11376
// Presentation: https://blog.selfshadow.com/publications/s2016-shading-course/activision/s2016_pbs_activision_occlusion.pdf
// Source code heavily based on XeGTAO v1.30 from Intel // Source code heavily based on XeGTAO v1.30 from Intel
// https://github.com/GameTechDev/XeGTAO/blob/0d177ce06bfa642f64d8af4de1197ad1bcb862d4/Source/Rendering/Shaders/XeGTAO.hlsli // 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::{ #import bevy_render::{
view::View, view::View,
@ -19,8 +24,10 @@
@group(0) @binding(3) var ambient_occlusion: texture_storage_2d<r16float, write>; @group(0) @binding(3) var ambient_occlusion: texture_storage_2d<r16float, write>;
@group(0) @binding(4) var depth_differences: texture_storage_2d<r32uint, write>; @group(0) @binding(4) var depth_differences: texture_storage_2d<r32uint, write>;
@group(0) @binding(5) var<uniform> globals: Globals; @group(0) @binding(5) var<uniform> globals: Globals;
@group(0) @binding(6) var<uniform> thickness: f32;
@group(1) @binding(0) var point_clamp_sampler: sampler; @group(1) @binding(0) var point_clamp_sampler: sampler;
@group(1) @binding(1) var<uniform> view: View; @group(1) @binding(1) var linear_clamp_sampler: sampler;
@group(1) @binding(2) var<uniform> view: View;
fn load_noise(pixel_coordinates: vec2<i32>) -> vec2<f32> { fn load_noise(pixel_coordinates: vec2<i32>) -> vec2<f32> {
var index = textureLoad(hilbert_index_lut, pixel_coordinates % 64, 0).r; var index = textureLoad(hilbert_index_lut, pixel_coordinates % 64, 0).r;
@ -81,13 +88,46 @@ fn reconstruct_view_space_position(depth: f32, uv: vec2<f32>) -> vec3<f32> {
} }
fn load_and_reconstruct_view_space_position(uv: vec2<f32>, sample_mip_level: f32) -> vec3<f32> { fn load_and_reconstruct_view_space_position(uv: vec2<f32>, sample_mip_level: f32) -> vec3<f32> {
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); 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<f32>,
view_vec: vec3<f32>,
sampling_direction: f32,
n: vec2<f32>,
samples_per_slice: f32,
bitmask: ptr<function, u32>,
) {
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 @compute
@workgroup_size(8, 8, 1) @workgroup_size(8, 8, 1)
fn gtao(@builtin(global_invocation_id) global_id: vec3<u32>) { fn ssao(@builtin(global_invocation_id) global_id: vec3<u32>) {
let slice_count = f32(#SLICE_COUNT); let slice_count = f32(#SLICE_COUNT);
let samples_per_slice_side = f32(#SAMPLES_PER_SLICE_SIDE); let samples_per_slice_side = f32(#SAMPLES_PER_SLICE_SIDE);
let effect_radius = 0.5 * 1.457; let effect_radius = 0.5 * 1.457;
@ -110,6 +150,7 @@ fn gtao(@builtin(global_invocation_id) global_id: vec3<u32>) {
let sample_scale = (-0.5 * effect_radius * view.clip_from_view[0][0]) / pixel_position.z; let sample_scale = (-0.5 * effect_radius * view.clip_from_view[0][0]) / pixel_position.z;
var visibility = 0.0; var visibility = 0.0;
var occluded_sample_count = 0u;
for (var slice_t = 0.0; slice_t < slice_count; slice_t += 1.0) { for (var slice_t = 0.0; slice_t < slice_count; slice_t += 1.0) {
let slice = slice_t + noise.x; let slice = slice_t + noise.x;
let phi = (PI / slice_count) * slice; let phi = (PI / slice_count) * slice;
@ -123,12 +164,10 @@ fn gtao(@builtin(global_invocation_id) global_id: vec3<u32>) {
let sign_norm = sign(dot(orthographic_direction, projected_normal)); let sign_norm = sign(dot(orthographic_direction, projected_normal));
let cos_norm = saturate(dot(projected_normal, view_vec) / projected_normal_length); 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<f32>(omega.x, -omega.y) * sample_scale; let sample_mul = vec2<f32>(omega.x, -omega.y) * sample_scale;
for (var sample_t = 0.0; sample_t < samples_per_slice_side; sample_t += 1.0) { 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; 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<u32>) {
let sample_difference_1 = sample_position_1 - pixel_position; let sample_difference_1 = sample_position_1 - pixel_position;
let sample_difference_2 = sample_position_2 - 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); processSample(sample_difference_1, view_vec, -1.0, n, samples_per_slice_side * 2.0, &bitmask);
let weight_2 = saturate(sample_distance_2 * falloff_mul + falloff_add); processSample(sample_difference_2, view_vec, 1.0, n, samples_per_slice_side * 2.0, &bitmask);
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);
} }
let horizon_1 = fast_acos(cos_horizon_1); occluded_sample_count += countOneBits(bitmask);
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);
} }
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); visibility = clamp(visibility, 0.03, 1.0);
textureStore(ambient_occlusion, pixel_coordinates, vec4<f32>(visibility, 0.0, 0.0, 0.0)); textureStore(ambient_occlusion, pixel_coordinates, vec4<f32>(visibility, 0.0, 0.0, 0.0));

View file

@ -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} #import bevy_render::maths::{PI, HALF_PI}
// Approximates single-bounce ambient occlusion to multi-bounce ambient occlusion // 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 // https://blog.selfshadow.com/publications/s2016-shading-course/activision/s2016_pbs_activision_occlusion.pdf#page=78
fn gtao_multibounce(visibility: f32, base_color: vec3<f32>) -> vec3<f32> { fn ssao_multibounce(visibility: f32, base_color: vec3<f32>) -> vec3<f32> {
let a = 2.0404 * base_color - 0.3324; let a = 2.0404 * base_color - 0.3324;
let b = -4.7951 * base_color + 0.6417; let b = -4.7951 * base_color + 0.6417;
let c = 2.7552 * base_color + 0.6903; let c = 2.7552 * base_color + 0.6903;

View file

@ -109,32 +109,52 @@ fn update(
sphere.translation.y = ops::sin(time.elapsed_seconds() / 1.7) * 0.7; sphere.translation.y = ops::sin(time.elapsed_seconds() / 1.7) * 0.7;
let (camera_entity, ssao, temporal_jitter) = camera.single(); let (camera_entity, ssao, temporal_jitter) = camera.single();
let current_ssao = ssao.cloned().unwrap_or_default();
let mut commands = commands.entity(camera_entity); let mut commands = commands.entity(camera_entity);
commands commands
.insert_if( .insert_if(
ScreenSpaceAmbientOcclusion { ScreenSpaceAmbientOcclusion {
quality_level: ScreenSpaceAmbientOcclusionQualityLevel::Low, quality_level: ScreenSpaceAmbientOcclusionQualityLevel::Low,
..current_ssao
}, },
|| keycode.just_pressed(KeyCode::Digit2), || keycode.just_pressed(KeyCode::Digit2),
) )
.insert_if( .insert_if(
ScreenSpaceAmbientOcclusion { ScreenSpaceAmbientOcclusion {
quality_level: ScreenSpaceAmbientOcclusionQualityLevel::Medium, quality_level: ScreenSpaceAmbientOcclusionQualityLevel::Medium,
..current_ssao
}, },
|| keycode.just_pressed(KeyCode::Digit3), || keycode.just_pressed(KeyCode::Digit3),
) )
.insert_if( .insert_if(
ScreenSpaceAmbientOcclusion { ScreenSpaceAmbientOcclusion {
quality_level: ScreenSpaceAmbientOcclusionQualityLevel::High, quality_level: ScreenSpaceAmbientOcclusionQualityLevel::High,
..current_ssao
}, },
|| keycode.just_pressed(KeyCode::Digit4), || keycode.just_pressed(KeyCode::Digit4),
) )
.insert_if( .insert_if(
ScreenSpaceAmbientOcclusion { ScreenSpaceAmbientOcclusion {
quality_level: ScreenSpaceAmbientOcclusionQualityLevel::Ultra, quality_level: ScreenSpaceAmbientOcclusionQualityLevel::Ultra,
..current_ssao
}, },
|| keycode.just_pressed(KeyCode::Digit5), || 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) { if keycode.just_pressed(KeyCode::Digit1) {
commands.remove::<ScreenSpaceAmbientOcclusion>(); commands.remove::<ScreenSpaceAmbientOcclusion>();
@ -160,6 +180,13 @@ fn update(
_ => unreachable!(), _ => 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("SSAO Quality:\n");
text.push_str(&format!("(1) {o}Off{o}\n")); text.push_str(&format!("(1) {o}Off{o}\n"));
text.push_str(&format!("(2) {l}Low{l}\n")); text.push_str(&format!("(2) {l}Low{l}\n"));