bevy/crates/bevy_pbr/src/ssr/ssr.wgsl
Patrick Walton b7bcd313ca
Cluster light probes using conservative spherical bounds. (#13746)
This commit allows the Bevy renderer to use the clustering
infrastructure for light probes (reflection probes and irradiance
volumes) on platforms where at least 3 storage buffers are available. On
such platforms (the vast majority), we stop performing brute-force
searches of light probes for each fragment and instead only search the
light probes with bounding spheres that intersect the current cluster.
This should dramatically improve scalability of irradiance volumes and
reflection probes.

The primary platform that doesn't support 3 storage buffers is WebGL 2,
and we continue using a brute-force search of light probes on that
platform, as the UBO that stores per-cluster indices is too small to fit
the light probe counts. Note, however, that that platform also doesn't
support bindless textures (indeed, it would be very odd for a platform
to support bindless textures but not SSBOs), so we only support one of
each type of light probe per drawcall there in the first place.
Consequently, this isn't a performance problem, as the search will only
have one light probe to consider. (In fact, clustering would probably
end up being a performance loss.)

Known potential improvements include:

1. We currently cull based on a conservative bounding sphere test and
not based on the oriented bounding box (OBB) of the light probe. This is
improvable, but in the interests of simplicity, I opted to keep the
bounding sphere test for now. The OBB improvement can be a follow-up.

2. This patch doesn't change the fact that each fragment only takes a
single light probe into account. Typical light probe implementations
detect the case in which multiple light probes cover the current
fragment and perform some sort of weighted blend between them. As the
light probe fetch function presently returns only a single light probe,
implementing that feature would require more code restructuring, so I
left it out for now. It can be added as a follow-up.

3. Light probe implementations typically have a falloff range. Although
this is a wanted feature in Bevy, this particular commit also doesn't
implement that feature, as it's out of scope.

4. This commit doesn't raise the maximum number of light probes past its
current value of 8 for each type. This should be addressed later, but
would possibly require more bindings on platforms with storage buffers,
which would increase this patch's complexity. Even without raising the
limit, this patch should constitute a significant performance
improvement for scenes that get anywhere close to this limit. In the
interest of keeping this patch small, I opted to leave raising the limit
to a follow-up.

## Changelog

### Changed

* Light probes (reflection probes and irradiance volumes) are now
clustered on most platforms, improving performance when many light
probes are present.

---------

Co-authored-by: Benjamin Brienen <Benjamin.Brienen@outlook.com>
Co-authored-by: Alice Cecile <alice.i.cecile@gmail.com>
2024-12-05 13:07:10 +00:00

195 lines
7.5 KiB
WebGPU Shading Language

// A postprocessing pass that performs screen-space reflections.
#define_import_path bevy_pbr::ssr
#import bevy_core_pipeline::fullscreen_vertex_shader::FullscreenVertexOutput
#import bevy_pbr::{
clustered_forward,
lighting,
lighting::{LAYER_BASE, LAYER_CLEARCOAT},
mesh_view_bindings::{view, depth_prepass_texture, deferred_prepass_texture, ssr_settings},
pbr_deferred_functions::pbr_input_from_deferred_gbuffer,
pbr_deferred_types,
pbr_functions,
prepass_utils,
raymarch::{
depth_ray_march_from_cs,
depth_ray_march_march,
depth_ray_march_new_from_depth,
depth_ray_march_to_ws_dir,
},
utils,
view_transformations::{
depth_ndc_to_view_z,
frag_coord_to_ndc,
ndc_to_frag_coord,
ndc_to_uv,
position_view_to_ndc,
position_world_to_ndc,
position_world_to_view,
},
}
#import bevy_render::view::View
#ifdef ENVIRONMENT_MAP
#import bevy_pbr::environment_map
#endif
// The texture representing the color framebuffer.
@group(1) @binding(0) var color_texture: texture_2d<f32>;
// The sampler that lets us sample from the color framebuffer.
@group(1) @binding(1) var color_sampler: sampler;
// Group 1, bindings 2 and 3 are in `raymarch.wgsl`.
// Returns the reflected color in the RGB channel and the specular occlusion in
// the alpha channel.
//
// The general approach here is similar to [1]. We first project the reflection
// ray into screen space. Then we perform uniform steps along that screen-space
// reflected ray, converting each step to view space.
//
// The arguments are:
//
// * `R_world`: The reflection vector in world space.
//
// * `P_world`: The current position in world space.
//
// [1]: https://lettier.github.io/3d-game-shaders-for-beginners/screen-space-reflection.html
fn evaluate_ssr(R_world: vec3<f32>, P_world: vec3<f32>) -> vec4<f32> {
let depth_size = vec2<f32>(textureDimensions(depth_prepass_texture));
var raymarch = depth_ray_march_new_from_depth(depth_size);
depth_ray_march_from_cs(&raymarch, position_world_to_ndc(P_world));
depth_ray_march_to_ws_dir(&raymarch, normalize(R_world));
raymarch.linear_steps = ssr_settings.linear_steps;
raymarch.bisection_steps = ssr_settings.bisection_steps;
raymarch.use_secant = ssr_settings.use_secant != 0u;
raymarch.depth_thickness_linear_z = ssr_settings.thickness;
raymarch.jitter = 1.0; // Disable jitter for now.
raymarch.march_behind_surfaces = false;
let raymarch_result = depth_ray_march_march(&raymarch);
if (raymarch_result.hit) {
return vec4(
textureSampleLevel(color_texture, color_sampler, raymarch_result.hit_uv, 0.0).rgb,
0.0
);
}
return vec4(0.0, 0.0, 0.0, 1.0);
}
@fragment
fn fragment(in: FullscreenVertexOutput) -> @location(0) vec4<f32> {
// Sample the depth.
var frag_coord = in.position;
frag_coord.z = prepass_utils::prepass_depth(in.position, 0u);
// Load the G-buffer data.
let fragment = textureLoad(color_texture, vec2<i32>(frag_coord.xy), 0);
let gbuffer = textureLoad(deferred_prepass_texture, vec2<i32>(frag_coord.xy), 0);
let pbr_input = pbr_input_from_deferred_gbuffer(frag_coord, gbuffer);
// Don't do anything if the surface is too rough, since we can't blur or do
// temporal accumulation yet.
let perceptual_roughness = pbr_input.material.perceptual_roughness;
if (perceptual_roughness > ssr_settings.perceptual_roughness_threshold) {
return fragment;
}
// Unpack the PBR input.
var specular_occlusion = pbr_input.specular_occlusion;
let world_position = pbr_input.world_position.xyz;
let N = pbr_input.N;
let V = pbr_input.V;
// Calculate the reflection vector.
let R = reflect(-V, N);
// Do the raymarching.
let ssr_specular = evaluate_ssr(R, world_position);
var indirect_light = ssr_specular.rgb;
specular_occlusion *= ssr_specular.a;
// Sample the environment map if necessary.
//
// This will take the specular part of the environment map into account if
// the ray missed. Otherwise, it only takes the diffuse part.
//
// TODO: Merge this with the duplicated code in `apply_pbr_lighting`.
#ifdef ENVIRONMENT_MAP
// Unpack values required for environment mapping.
let base_color = pbr_input.material.base_color.rgb;
let metallic = pbr_input.material.metallic;
let reflectance = pbr_input.material.reflectance;
let specular_transmission = pbr_input.material.specular_transmission;
let diffuse_transmission = pbr_input.material.diffuse_transmission;
let diffuse_occlusion = pbr_input.diffuse_occlusion;
#ifdef STANDARD_MATERIAL_CLEARCOAT
// Do the above calculations again for the clearcoat layer. Remember that
// the clearcoat can have its own roughness and its own normal.
let clearcoat = pbr_input.material.clearcoat;
let clearcoat_perceptual_roughness = pbr_input.material.clearcoat_perceptual_roughness;
let clearcoat_roughness = lighting::perceptualRoughnessToRoughness(clearcoat_perceptual_roughness);
let clearcoat_N = pbr_input.clearcoat_N;
let clearcoat_NdotV = max(dot(clearcoat_N, pbr_input.V), 0.0001);
let clearcoat_R = reflect(-pbr_input.V, clearcoat_N);
#endif // STANDARD_MATERIAL_CLEARCOAT
// Calculate various other values needed for environment mapping.
let roughness = lighting::perceptualRoughnessToRoughness(perceptual_roughness);
let diffuse_color = pbr_functions::calculate_diffuse_color(
base_color,
metallic,
specular_transmission,
diffuse_transmission
);
let NdotV = max(dot(N, V), 0.0001);
let F_ab = lighting::F_AB(perceptual_roughness, NdotV);
let F0 = pbr_functions::calculate_F0(base_color, metallic, reflectance);
// Pack all the values into a structure.
var lighting_input: lighting::LightingInput;
lighting_input.layers[LAYER_BASE].NdotV = NdotV;
lighting_input.layers[LAYER_BASE].N = N;
lighting_input.layers[LAYER_BASE].R = R;
lighting_input.layers[LAYER_BASE].perceptual_roughness = perceptual_roughness;
lighting_input.layers[LAYER_BASE].roughness = roughness;
lighting_input.P = world_position.xyz;
lighting_input.V = V;
lighting_input.diffuse_color = diffuse_color;
lighting_input.F0_ = F0;
lighting_input.F_ab = F_ab;
#ifdef STANDARD_MATERIAL_CLEARCOAT
lighting_input.layers[LAYER_CLEARCOAT].NdotV = clearcoat_NdotV;
lighting_input.layers[LAYER_CLEARCOAT].N = clearcoat_N;
lighting_input.layers[LAYER_CLEARCOAT].R = clearcoat_R;
lighting_input.layers[LAYER_CLEARCOAT].perceptual_roughness = clearcoat_perceptual_roughness;
lighting_input.layers[LAYER_CLEARCOAT].roughness = clearcoat_roughness;
lighting_input.clearcoat_strength = clearcoat;
#endif // STANDARD_MATERIAL_CLEARCOAT
// Determine which cluster we're in. We'll need this to find the right
// reflection probe.
let cluster_index = clustered_forward::fragment_cluster_index(
frag_coord.xy, frag_coord.z, false);
var clusterable_object_index_ranges =
clustered_forward::unpack_clusterable_object_index_ranges(cluster_index);
// Sample the environment map.
let environment_light = environment_map::environment_map_light(
&lighting_input, &clusterable_object_index_ranges, false);
// Accumulate the environment map light.
indirect_light += view.exposure *
(environment_light.diffuse * diffuse_occlusion +
environment_light.specular * specular_occlusion);
#endif
// Write the results.
return vec4(fragment.rgb + indirect_light, 1.0);
}