Skip to content

Commit af9c945

Browse files
JMS55IceSentrydanchiaElabajabasuperdump
authored
Screen Space Ambient Occlusion (SSAO) MVP (#7402)
![image](https://github.com/bevyengine/bevy/assets/47158642/dbb62645-f639-4f2b-b84b-26fd915c186d) # Objective - Add Screen space ambient occlusion (SSAO). SSAO approximates small-scale, local occlusion of _indirect_ diffuse light between objects. SSAO does not apply to direct lighting, such as point or directional lights. - This darkens creases, e.g. on staircases, and gives nice contact shadows where objects meet, giving entities a more "grounded" feel. - Closes #3632. ## Solution - Implement the GTAO algorithm. - https://www.activision.com/cdn/research/Practical_Real_Time_Strategies_for_Accurate_Indirect_Occlusion_NEW%20VERSION_COLOR.pdf - https://blog.selfshadow.com/publications/s2016-shading-course/activision/s2016_pbs_activision_occlusion.pdf - Source code heavily based on [Intel's XeGTAO](https://github.com/GameTechDev/XeGTAO/blob/0d177ce06bfa642f64d8af4de1197ad1bcb862d4/Source/Rendering/Shaders/XeGTAO.hlsli). - Add an SSAO bevy example. ## Algorithm Overview * Run a depth and normal prepass * Create downscaled mips of the depth texture (preprocess_depths pass) * GTAO pass - for each pixel, take several random samples from the depth+normal buffers, reconstruct world position, raytrace in screen space to estimate occlusion. Rather then doing completely random samples on a hemisphere, you choose random _slices_ of the hemisphere, and then can analytically compute the full occlusion of that slice. Also compute edges based on depth differences here. * Spatial denoise pass - bilateral blur, using edge detection to not blur over edges. This is the final SSAO result. * Main pass - if SSAO exists, sample the SSAO texture, and set occlusion to be the minimum of ssao/material occlusion. This then feeds into the rest of the PBR shader as normal. --- ## Future Improvements - Maybe remove the low quality preset for now (too noisy) - WebGPU fallback (see below) - Faster depth->world position (see reverted code) - Bent normals - Try interleaved gradient noise or spatiotemporal blue noise - Replace the spatial denoiser with a combined spatial+temporal denoiser - Render at half resolution and use a bilateral upsample - Better multibounce approximation (https://drive.google.com/file/d/1SyagcEVplIm2KkRD3WQYSO9O0Iyi1hfy/view) ## Far-Future Performance Improvements - F16 math (missing naga-wgsl support https://github.com/gfx-rs/naga/issues/1884) - Faster coordinate space conversion for normals - Faster depth mipchain creation (https://github.com/GPUOpen-Effects/FidelityFX-SPD) (wgpu/naga does not currently support subgroup ops) - Deinterleaved SSAO for better cache efficiency (https://developer.nvidia.com/sites/default/files/akamai/gameworks/samples/DeinterleavedTexturing.pdf) ## Other Interesting Papers - Visibility bitmask (https://link.springer.com/article/10.1007/s00371-022-02703-y, https://cdrinmatane.github.io/posts/cgspotlight-slides/) - Screen space diffuse lighting (https://github.com/Patapom/GodComplex/blob/master/Tests/TestHBIL/2018%20Mayaux%20-%20Horizon-Based%20Indirect%20Lighting%20(HBIL).pdf) ## Platform Support * SSAO currently does not work on DirectX12 due to issues with wgpu and naga: * gfx-rs/wgpu#3798 * gfx-rs/naga#2353 * SSAO currently does not work on WebGPU because r16float is not a valid storage texture format https://gpuweb.github.io/gpuweb/wgsl/#storage-texel-formats. We can fix this with a fallback to r32float. --- ## Changelog - Added ScreenSpaceAmbientOcclusionSettings, ScreenSpaceAmbientOcclusionQualityLevel, and ScreenSpaceAmbientOcclusionBundle --------- Co-authored-by: IceSentry <[email protected]> Co-authored-by: IceSentry <[email protected]> Co-authored-by: Daniel Chia <[email protected]> Co-authored-by: Elabajaba <[email protected]> Co-authored-by: Robert Swain <[email protected]> Co-authored-by: robtfm <[email protected]> Co-authored-by: Brandon Dyer <[email protected]> Co-authored-by: Edgar Geier <[email protected]> Co-authored-by: Nicola Papale <[email protected]> Co-authored-by: Carter Anderson <[email protected]>
1 parent 6c86545 commit af9c945

File tree

18 files changed

+1678
-30
lines changed

18 files changed

+1678
-30
lines changed

Cargo.toml

+10
Original file line numberDiff line numberDiff line change
@@ -517,6 +517,16 @@ description = "Create a custom material to draw 3d lines"
517517
category = "3D Rendering"
518518
wasm = true
519519

520+
[[example]]
521+
name = "ssao"
522+
path = "examples/3d/ssao.rs"
523+
524+
[package.metadata.example.ssao]
525+
name = "Screen Space Ambient Occlusion"
526+
description = "A scene showcasing screen space ambient occlusion"
527+
category = "3D Rendering"
528+
wasm = false
529+
520530
[[example]]
521531
name = "spotlight"
522532
path = "examples/3d/spotlight.rs"

crates/bevy_core_pipeline/src/prepass/node.rs

-1
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,6 @@ impl ViewNode for PrepassNode {
9494
stencil_ops: None,
9595
}),
9696
});
97-
9897
if let Some(viewport) = camera.viewport.as_ref() {
9998
render_pass.set_camera_viewport(viewport);
10099
}

crates/bevy_pbr/src/lib.rs

+4
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ mod parallax;
1212
mod pbr_material;
1313
mod prepass;
1414
mod render;
15+
mod ssao;
1516

1617
pub use alpha::*;
1718
pub use bundle::*;
@@ -23,6 +24,7 @@ pub use parallax::*;
2324
pub use pbr_material::*;
2425
pub use prepass::*;
2526
pub use render::*;
27+
pub use ssao::*;
2628

2729
pub mod prelude {
2830
#[doc(hidden)]
@@ -38,6 +40,7 @@ pub mod prelude {
3840
material::{Material, MaterialPlugin},
3941
parallax::ParallaxMappingMethod,
4042
pbr_material::StandardMaterial,
43+
ssao::ScreenSpaceAmbientOcclusionPlugin,
4144
};
4245
}
4346

@@ -184,6 +187,7 @@ impl Plugin for PbrPlugin {
184187
prepass_enabled: self.prepass_enabled,
185188
..Default::default()
186189
})
190+
.add_plugin(ScreenSpaceAmbientOcclusionPlugin)
187191
.add_plugin(EnvironmentMapPlugin)
188192
.init_resource::<AmbientLight>()
189193
.init_resource::<GlobalVisiblePointLights>()

crates/bevy_pbr/src/material.rs

+8-2
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
use crate::{
22
render, AlphaMode, DrawMesh, DrawPrepass, EnvironmentMapLight, MeshPipeline, MeshPipelineKey,
3-
MeshUniform, PrepassPipelinePlugin, PrepassPlugin, RenderLightSystems, SetMeshBindGroup,
4-
SetMeshViewBindGroup, Shadow,
3+
MeshUniform, PrepassPipelinePlugin, PrepassPlugin, RenderLightSystems,
4+
ScreenSpaceAmbientOcclusionSettings, SetMeshBindGroup, SetMeshViewBindGroup, Shadow,
55
};
66
use bevy_app::{App, Plugin};
77
use bevy_asset::{AddAsset, AssetEvent, AssetServer, Assets, Handle};
@@ -387,6 +387,7 @@ pub fn queue_material_meshes<M: Material>(
387387
Option<&Tonemapping>,
388388
Option<&DebandDither>,
389389
Option<&EnvironmentMapLight>,
390+
Option<&ScreenSpaceAmbientOcclusionSettings>,
390391
Option<&NormalPrepass>,
391392
Option<&TemporalAntiAliasSettings>,
392393
&mut RenderPhase<Opaque3d>,
@@ -402,6 +403,7 @@ pub fn queue_material_meshes<M: Material>(
402403
tonemapping,
403404
dither,
404405
environment_map,
406+
ssao,
405407
normal_prepass,
406408
taa_settings,
407409
mut opaque_phase,
@@ -455,6 +457,10 @@ pub fn queue_material_meshes<M: Material>(
455457
}
456458
}
457459

460+
if ssao.is_some() {
461+
view_key |= MeshPipelineKey::SCREEN_SPACE_AMBIENT_OCCLUSION;
462+
}
463+
458464
let rangefinder = view.rangefinder3d();
459465
for visible_entity in &visible_entities.entities {
460466
if let Ok((material_handle, mesh_handle, mesh_uniform)) =

crates/bevy_pbr/src/render/mesh.rs

+42-11
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
use crate::{
22
environment_map, prepass, EnvironmentMapLight, FogMeta, GlobalLightMeta, GpuFog, GpuLights,
33
GpuPointLights, LightMeta, NotShadowCaster, NotShadowReceiver, PreviousGlobalTransform,
4-
ShadowSamplers, ViewClusterBindings, ViewFogUniformOffset, ViewLightsUniformOffset,
5-
ViewShadowBindings, CLUSTERED_FORWARD_STORAGE_BUFFER_COUNT, MAX_CASCADES_PER_LIGHT,
6-
MAX_DIRECTIONAL_LIGHTS,
4+
ScreenSpaceAmbientOcclusionTextures, ShadowSamplers, ViewClusterBindings, ViewFogUniformOffset,
5+
ViewLightsUniformOffset, ViewShadowBindings, CLUSTERED_FORWARD_STORAGE_BUFFER_COUNT,
6+
MAX_CASCADES_PER_LIGHT, MAX_DIRECTIONAL_LIGHTS,
77
};
88
use bevy_app::Plugin;
99
use bevy_asset::{load_internal_asset, Assets, Handle, HandleUntyped};
@@ -433,22 +433,33 @@ impl FromWorld for MeshPipeline {
433433
},
434434
count: None,
435435
},
436+
// Screen space ambient occlusion texture
437+
BindGroupLayoutEntry {
438+
binding: 11,
439+
visibility: ShaderStages::FRAGMENT,
440+
ty: BindingType::Texture {
441+
multisampled: false,
442+
sample_type: TextureSampleType::Float { filterable: false },
443+
view_dimension: TextureViewDimension::D2,
444+
},
445+
count: None,
446+
},
436447
];
437448

438449
// EnvironmentMapLight
439450
let environment_map_entries =
440-
environment_map::get_bind_group_layout_entries([11, 12, 13]);
451+
environment_map::get_bind_group_layout_entries([12, 13, 14]);
441452
entries.extend_from_slice(&environment_map_entries);
442453

443454
// Tonemapping
444-
let tonemapping_lut_entries = get_lut_bind_group_layout_entries([14, 15]);
455+
let tonemapping_lut_entries = get_lut_bind_group_layout_entries([15, 16]);
445456
entries.extend_from_slice(&tonemapping_lut_entries);
446457

447458
if cfg!(any(not(feature = "webgl"), not(target_arch = "wasm32")))
448459
|| (cfg!(all(feature = "webgl", target_arch = "wasm32")) && !multisampled)
449460
{
450461
entries.extend_from_slice(&prepass::get_bind_group_layout_entries(
451-
[16, 17, 18],
462+
[17, 18, 19],
452463
multisampled,
453464
));
454465
}
@@ -586,8 +597,9 @@ bitflags::bitflags! {
586597
const MAY_DISCARD = (1 << 6); // Guards shader codepaths that may discard, allowing early depth tests in most cases
587598
// See: https://www.khronos.org/opengl/wiki/Early_Fragment_Test
588599
const ENVIRONMENT_MAP = (1 << 7);
589-
const DEPTH_CLAMP_ORTHO = (1 << 8);
590-
const TAA = (1 << 9);
600+
const SCREEN_SPACE_AMBIENT_OCCLUSION = (1 << 8);
601+
const DEPTH_CLAMP_ORTHO = (1 << 9);
602+
const TAA = (1 << 10);
591603
const BLEND_RESERVED_BITS = Self::BLEND_MASK_BITS << Self::BLEND_SHIFT_BITS; // ← Bitmask reserving bits for the blend state
592604
const BLEND_OPAQUE = (0 << Self::BLEND_SHIFT_BITS); // ← Values are just sequential within the mask, and can range from 0 to 3
593605
const BLEND_PREMULTIPLIED_ALPHA = (1 << Self::BLEND_SHIFT_BITS); //
@@ -727,6 +739,10 @@ impl SpecializedMeshPipeline for MeshPipeline {
727739
bind_group_layout.push(self.mesh_layout.clone());
728740
};
729741

742+
if key.contains(MeshPipelineKey::SCREEN_SPACE_AMBIENT_OCCLUSION) {
743+
shader_defs.push("SCREEN_SPACE_AMBIENT_OCCLUSION".into());
744+
}
745+
730746
let vertex_buffer_layout = layout.get_layout(&vertex_attributes)?;
731747

732748
let (label, blend, depth_write_enabled);
@@ -974,6 +990,7 @@ pub fn queue_mesh_view_bind_groups(
974990
Entity,
975991
&ViewShadowBindings,
976992
&ViewClusterBindings,
993+
Option<&ScreenSpaceAmbientOcclusionTextures>,
977994
Option<&ViewPrepassTextures>,
978995
Option<&EnvironmentMapLight>,
979996
&Tonemapping,
@@ -1003,11 +1020,17 @@ pub fn queue_mesh_view_bind_groups(
10031020
entity,
10041021
view_shadow_bindings,
10051022
view_cluster_bindings,
1023+
ssao_textures,
10061024
prepass_textures,
10071025
environment_map,
10081026
tonemapping,
10091027
) in &views
10101028
{
1029+
let fallback_ssao = fallback_images
1030+
.image_for_samplecount(1)
1031+
.texture_view
1032+
.clone();
1033+
10111034
let layout = if msaa.samples() > 1 {
10121035
&mesh_pipeline.view_layout_multisampled
10131036
} else {
@@ -1063,18 +1086,26 @@ pub fn queue_mesh_view_bind_groups(
10631086
binding: 10,
10641087
resource: fog_binding.clone(),
10651088
},
1089+
BindGroupEntry {
1090+
binding: 11,
1091+
resource: BindingResource::TextureView(
1092+
ssao_textures
1093+
.map(|t| &t.screen_space_ambient_occlusion_texture.default_view)
1094+
.unwrap_or(&fallback_ssao),
1095+
),
1096+
},
10661097
];
10671098

10681099
let env_map = environment_map::get_bindings(
10691100
environment_map,
10701101
&images,
10711102
&fallback_cubemap,
1072-
[11, 12, 13],
1103+
[12, 13, 14],
10731104
);
10741105
entries.extend_from_slice(&env_map);
10751106

10761107
let tonemapping_luts =
1077-
get_lut_bindings(&images, &tonemapping_luts, tonemapping, [14, 15]);
1108+
get_lut_bindings(&images, &tonemapping_luts, tonemapping, [15, 16]);
10781109
entries.extend_from_slice(&tonemapping_luts);
10791110

10801111
// When using WebGL, we can't have a depth texture with multisampling
@@ -1086,7 +1117,7 @@ pub fn queue_mesh_view_bind_groups(
10861117
&mut fallback_images,
10871118
&mut fallback_depths,
10881119
&msaa,
1089-
[16, 17, 18],
1120+
[17, 18, 19],
10901121
));
10911122
}
10921123

crates/bevy_pbr/src/render/mesh_view_bindings.wgsl

+13-10
Original file line numberDiff line numberDiff line change
@@ -47,29 +47,32 @@ var<uniform> globals: Globals;
4747
var<uniform> fog: Fog;
4848

4949
@group(0) @binding(11)
50-
var environment_map_diffuse: texture_cube<f32>;
50+
var screen_space_ambient_occlusion_texture: texture_2d<f32>;
51+
5152
@group(0) @binding(12)
52-
var environment_map_specular: texture_cube<f32>;
53+
var environment_map_diffuse: texture_cube<f32>;
5354
@group(0) @binding(13)
55+
var environment_map_specular: texture_cube<f32>;
56+
@group(0) @binding(14)
5457
var environment_map_sampler: sampler;
5558

56-
@group(0) @binding(14)
57-
var dt_lut_texture: texture_3d<f32>;
5859
@group(0) @binding(15)
60+
var dt_lut_texture: texture_3d<f32>;
61+
@group(0) @binding(16)
5962
var dt_lut_sampler: sampler;
6063

6164
#ifdef MULTISAMPLED
62-
@group(0) @binding(16)
63-
var depth_prepass_texture: texture_depth_multisampled_2d;
6465
@group(0) @binding(17)
65-
var normal_prepass_texture: texture_multisampled_2d<f32>;
66+
var depth_prepass_texture: texture_depth_multisampled_2d;
6667
@group(0) @binding(18)
68+
var normal_prepass_texture: texture_multisampled_2d<f32>;
69+
@group(0) @binding(19)
6770
var motion_vector_prepass_texture: texture_multisampled_2d<f32>;
6871
#else
69-
@group(0) @binding(16)
70-
var depth_prepass_texture: texture_depth_2d;
7172
@group(0) @binding(17)
72-
var normal_prepass_texture: texture_2d<f32>;
73+
var depth_prepass_texture: texture_depth_2d;
7374
@group(0) @binding(18)
75+
var normal_prepass_texture: texture_2d<f32>;
76+
@group(0) @binding(19)
7477
var motion_vector_prepass_texture: texture_2d<f32>;
7578
#endif

crates/bevy_pbr/src/render/pbr.wgsl

+14-2
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,10 @@
1313

1414
#import bevy_pbr::prepass_utils
1515

16+
#ifdef SCREEN_SPACE_AMBIENT_OCCLUSION
17+
#import bevy_pbr::gtao_utils
18+
#endif
19+
1620
struct FragmentInput {
1721
@builtin(front_facing) is_front: bool,
1822
@builtin(position) frag_coord: vec4<f32>,
@@ -88,12 +92,20 @@ fn fragment(in: FragmentInput) -> @location(0) vec4<f32> {
8892
pbr_input.material.metallic = metallic;
8993
pbr_input.material.perceptual_roughness = perceptual_roughness;
9094

91-
var occlusion: f32 = 1.0;
95+
// TODO: Split into diffuse/specular occlusion?
96+
var occlusion: vec3<f32> = vec3(1.0);
9297
#ifdef VERTEX_UVS
9398
if ((material.flags & STANDARD_MATERIAL_FLAGS_OCCLUSION_TEXTURE_BIT) != 0u) {
94-
occlusion = textureSample(occlusion_texture, occlusion_sampler, uv).r;
99+
occlusion = vec3(textureSample(occlusion_texture, occlusion_sampler, in.uv).r);
95100
}
96101
#endif
102+
#ifdef SCREEN_SPACE_AMBIENT_OCCLUSION
103+
let ssao = textureLoad(screen_space_ambient_occlusion_texture, vec2<i32>(in.frag_coord.xy), 0i).r;
104+
let ssao_multibounce = gtao_multibounce(ssao, pbr_input.material.base_color.rgb);
105+
occlusion = min(occlusion, ssao_multibounce);
106+
#endif
107+
pbr_input.occlusion = occlusion;
108+
97109
pbr_input.frag_coord = in.frag_coord;
98110
pbr_input.world_position = in.world_position;
99111

crates/bevy_pbr/src/render/pbr_ambient.wgsl

+1-1
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ fn ambient_light(
1010
diffuse_color: vec3<f32>,
1111
specular_color: vec3<f32>,
1212
perceptual_roughness: f32,
13-
occlusion: f32,
13+
occlusion: vec3<f32>,
1414
) -> vec3<f32> {
1515
let diffuse_ambient = EnvBRDFApprox(diffuse_color, F_AB(1.0, NdotV)) * occlusion;
1616
let specular_ambient = EnvBRDFApprox(specular_color, F_AB(perceptual_roughness, NdotV));

crates/bevy_pbr/src/render/pbr_functions.wgsl

+2-2
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,7 @@ fn calculate_view(
126126

127127
struct PbrInput {
128128
material: StandardMaterial,
129-
occlusion: f32,
129+
occlusion: vec3<f32>,
130130
frag_coord: vec4<f32>,
131131
world_position: vec4<f32>,
132132
// Normalized world normal used for shadow mapping as normal-mapping is not used for shadow
@@ -146,7 +146,7 @@ fn pbr_input_new() -> PbrInput {
146146
var pbr_input: PbrInput;
147147

148148
pbr_input.material = standard_material_new();
149-
pbr_input.occlusion = 1.0;
149+
pbr_input.occlusion = vec3<f32>(1.0);
150150

151151
pbr_input.frag_coord = vec4<f32>(0.0, 0.0, 0.0, 1.0);
152152
pbr_input.world_position = vec4<f32>(0.0, 0.0, 0.0, 1.0);

crates/bevy_pbr/src/render/utils.wgsl

+1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
#define_import_path bevy_pbr::utils
22

33
const PI: f32 = 3.141592653589793;
4+
const HALF_PI: f32 = 1.57079632679;
45
const E: f32 = 2.718281828459045;
56

67
fn hsv2rgb(hue: f32, saturation: f32, value: f32) -> vec3<f32> {

0 commit comments

Comments
 (0)