

# 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 https://github.com/bevyengine/bevy/issues/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](0d177ce06b/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:
* https://github.com/gfx-rs/wgpu/pull/3798
* https://github.com/gfx-rs/naga/pull/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 <c.giguere42@gmail.com>
Co-authored-by: IceSentry <IceSentry@users.noreply.github.com>
Co-authored-by: Daniel Chia <danstryder@gmail.com>
Co-authored-by: Elabajaba <Elabajaba@users.noreply.github.com>
Co-authored-by: Robert Swain <robert.swain@gmail.com>
Co-authored-by: robtfm <50659922+robtfm@users.noreply.github.com>
Co-authored-by: Brandon Dyer <brandondyer64@gmail.com>
Co-authored-by: Edgar Geier <geieredgar@gmail.com>
Co-authored-by: Nicola Papale <nicopap@users.noreply.github.com>
Co-authored-by: Carter Anderson <mcanders1@gmail.com>
168 lines
5.9 KiB
WebGPU Shading Language
168 lines
5.9 KiB
WebGPU Shading Language
#import bevy_pbr::mesh_view_bindings
|
|
#import bevy_pbr::pbr_bindings
|
|
#import bevy_pbr::mesh_bindings
|
|
|
|
#import bevy_pbr::utils
|
|
#import bevy_pbr::clustered_forward
|
|
#import bevy_pbr::lighting
|
|
#import bevy_pbr::pbr_ambient
|
|
#import bevy_pbr::shadows
|
|
#import bevy_pbr::fog
|
|
#import bevy_pbr::pbr_functions
|
|
#import bevy_pbr::parallax_mapping
|
|
|
|
#import bevy_pbr::prepass_utils
|
|
|
|
#ifdef SCREEN_SPACE_AMBIENT_OCCLUSION
|
|
#import bevy_pbr::gtao_utils
|
|
#endif
|
|
|
|
struct FragmentInput {
|
|
@builtin(front_facing) is_front: bool,
|
|
@builtin(position) frag_coord: vec4<f32>,
|
|
#import bevy_pbr::mesh_vertex_output
|
|
};
|
|
|
|
@fragment
|
|
fn fragment(in: FragmentInput) -> @location(0) vec4<f32> {
|
|
let is_orthographic = view.projection[3].w == 1.0;
|
|
let V = calculate_view(in.world_position, is_orthographic);
|
|
#ifdef VERTEX_UVS
|
|
var uv = in.uv;
|
|
#ifdef VERTEX_TANGENTS
|
|
if ((material.flags & STANDARD_MATERIAL_FLAGS_DEPTH_MAP_BIT) != 0u) {
|
|
let N = in.world_normal;
|
|
let T = in.world_tangent.xyz;
|
|
let B = in.world_tangent.w * cross(N, T);
|
|
// Transform V from fragment to camera in world space to tangent space.
|
|
let Vt = vec3(dot(V, T), dot(V, B), dot(V, N));
|
|
uv = parallaxed_uv(
|
|
material.parallax_depth_scale,
|
|
material.max_parallax_layer_count,
|
|
material.max_relief_mapping_search_steps,
|
|
uv,
|
|
// Flip the direction of Vt to go toward the surface to make the
|
|
// parallax mapping algorithm easier to understand and reason
|
|
// about.
|
|
-Vt,
|
|
);
|
|
}
|
|
#endif
|
|
#endif
|
|
var output_color: vec4<f32> = material.base_color;
|
|
#ifdef VERTEX_COLORS
|
|
output_color = output_color * in.color;
|
|
#endif
|
|
#ifdef VERTEX_UVS
|
|
if ((material.flags & STANDARD_MATERIAL_FLAGS_BASE_COLOR_TEXTURE_BIT) != 0u) {
|
|
output_color = output_color * textureSample(base_color_texture, base_color_sampler, uv);
|
|
}
|
|
#endif
|
|
|
|
// NOTE: Unlit bit not set means == 0 is true, so the true case is if lit
|
|
if ((material.flags & STANDARD_MATERIAL_FLAGS_UNLIT_BIT) == 0u) {
|
|
// Prepare a 'processed' StandardMaterial by sampling all textures to resolve
|
|
// the material members
|
|
var pbr_input: PbrInput;
|
|
|
|
pbr_input.material.base_color = output_color;
|
|
pbr_input.material.reflectance = material.reflectance;
|
|
pbr_input.material.flags = material.flags;
|
|
pbr_input.material.alpha_cutoff = material.alpha_cutoff;
|
|
|
|
// TODO use .a for exposure compensation in HDR
|
|
var emissive: vec4<f32> = material.emissive;
|
|
#ifdef VERTEX_UVS
|
|
if ((material.flags & STANDARD_MATERIAL_FLAGS_EMISSIVE_TEXTURE_BIT) != 0u) {
|
|
emissive = vec4<f32>(emissive.rgb * textureSample(emissive_texture, emissive_sampler, uv).rgb, 1.0);
|
|
}
|
|
#endif
|
|
pbr_input.material.emissive = emissive;
|
|
|
|
var metallic: f32 = material.metallic;
|
|
var perceptual_roughness: f32 = material.perceptual_roughness;
|
|
#ifdef VERTEX_UVS
|
|
if ((material.flags & STANDARD_MATERIAL_FLAGS_METALLIC_ROUGHNESS_TEXTURE_BIT) != 0u) {
|
|
let metallic_roughness = textureSample(metallic_roughness_texture, metallic_roughness_sampler, uv);
|
|
// Sampling from GLTF standard channels for now
|
|
metallic = metallic * metallic_roughness.b;
|
|
perceptual_roughness = perceptual_roughness * metallic_roughness.g;
|
|
}
|
|
#endif
|
|
pbr_input.material.metallic = metallic;
|
|
pbr_input.material.perceptual_roughness = perceptual_roughness;
|
|
|
|
// TODO: Split into diffuse/specular occlusion?
|
|
var occlusion: vec3<f32> = vec3(1.0);
|
|
#ifdef VERTEX_UVS
|
|
if ((material.flags & STANDARD_MATERIAL_FLAGS_OCCLUSION_TEXTURE_BIT) != 0u) {
|
|
occlusion = vec3(textureSample(occlusion_texture, occlusion_sampler, in.uv).r);
|
|
}
|
|
#endif
|
|
#ifdef SCREEN_SPACE_AMBIENT_OCCLUSION
|
|
let ssao = textureLoad(screen_space_ambient_occlusion_texture, vec2<i32>(in.frag_coord.xy), 0i).r;
|
|
let ssao_multibounce = gtao_multibounce(ssao, pbr_input.material.base_color.rgb);
|
|
occlusion = min(occlusion, ssao_multibounce);
|
|
#endif
|
|
pbr_input.occlusion = occlusion;
|
|
|
|
pbr_input.frag_coord = in.frag_coord;
|
|
pbr_input.world_position = in.world_position;
|
|
|
|
#ifdef LOAD_PREPASS_NORMALS
|
|
pbr_input.world_normal = prepass_normal(in.frag_coord, 0u);
|
|
#else // LOAD_PREPASS_NORMALS
|
|
pbr_input.world_normal = prepare_world_normal(
|
|
in.world_normal,
|
|
(material.flags & STANDARD_MATERIAL_FLAGS_DOUBLE_SIDED_BIT) != 0u,
|
|
in.is_front,
|
|
);
|
|
#endif // LOAD_PREPASS_NORMALS
|
|
|
|
pbr_input.is_orthographic = is_orthographic;
|
|
|
|
pbr_input.N = apply_normal_mapping(
|
|
material.flags,
|
|
pbr_input.world_normal,
|
|
#ifdef VERTEX_TANGENTS
|
|
#ifdef STANDARDMATERIAL_NORMAL_MAP
|
|
in.world_tangent,
|
|
#endif
|
|
#endif
|
|
#ifdef VERTEX_UVS
|
|
uv,
|
|
#endif
|
|
);
|
|
pbr_input.V = V;
|
|
pbr_input.occlusion = occlusion;
|
|
|
|
pbr_input.flags = mesh.flags;
|
|
|
|
output_color = pbr(pbr_input);
|
|
} else {
|
|
output_color = alpha_discard(material, output_color);
|
|
}
|
|
|
|
// fog
|
|
if (fog.mode != FOG_MODE_OFF && (material.flags & STANDARD_MATERIAL_FLAGS_FOG_ENABLED_BIT) != 0u) {
|
|
output_color = apply_fog(fog, output_color, in.world_position.xyz, view.world_position.xyz);
|
|
}
|
|
|
|
#ifdef TONEMAP_IN_SHADER
|
|
output_color = tone_mapping(output_color);
|
|
#ifdef DEBAND_DITHER
|
|
var output_rgb = output_color.rgb;
|
|
output_rgb = powsafe(output_rgb, 1.0 / 2.2);
|
|
output_rgb = output_rgb + screen_space_dither(in.frag_coord.xy);
|
|
// This conversion back to linear space is required because our output texture format is
|
|
// SRGB; the GPU will assume our output is linear and will apply an SRGB conversion.
|
|
output_rgb = powsafe(output_rgb, 2.2);
|
|
output_color = vec4(output_rgb, output_color.a);
|
|
#endif
|
|
#endif
|
|
#ifdef PREMULTIPLY_ALPHA
|
|
output_color = premultiply_alpha(material.flags, output_color);
|
|
#endif
|
|
return output_color;
|
|
}
|