Cascaded shadow maps: Fix prepass ortho depth clamping (#8877)

# Objective

- Fixes #8645

## Solution

Cascaded shadow maps use a technique commonly called shadow pancaking to
enhance shadow map resolution by restricting the orthographic projection
used in creating the shadow maps to the frustum slice for the cascade.
The implication of this restriction is that shadow casters can be closer
than the near plane of the projection volume.

Prior to this PR, we address clamp the depth of the prepass vertex
output to ensure that these shadow casters do not get clipped, resulting
in shadow loss. However, a flaw / bug of the prior approach is that the
depth that gets written to the shadow map isn't quite correct - the
depth was previously derived by interpolated the clamped clip position,
resulting in depths that are further than they should be. This creates
artifacts that are particularly noticeable when a very 'long' object
intersects the near plane close to perpendicularly.

The fix in this PR is to propagate the unclamped depth to the prepass
fragment shader and use that depth value directly.

A complementary solution would be to use
[DEPTH_CLIP_CONTROL](https://docs.rs/wgpu/latest/wgpu/struct.Features.html#associatedconstant.DEPTH_CLIP_CONTROL)
to request `unclipped_depth`. However due to the relatively low support
of the feature on Vulkan (I believe it's ~38%), I went with this
solution for now to get the broadest fix out first.

---

## Changelog

- Fixed: Shadows from directional lights were sometimes incorrectly
omitted when the shadow caster was partially out of view.

---------

Co-authored-by: Carter Anderson <mcanders1@gmail.com>
This commit is contained in:
Daniel Chia 2023-06-21 15:00:19 -07:00 committed by GitHub
parent f18f28874a
commit 0a881ab37f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 39 additions and 2 deletions

View File

@ -385,6 +385,13 @@ where
));
if key.mesh_key.contains(MeshPipelineKey::DEPTH_CLAMP_ORTHO) {
shader_defs.push("DEPTH_CLAMP_ORTHO".into());
// PERF: This line forces the "prepass fragment shader" to always run in
// common scenarios like "directional light calculation". Doing so resolves
// a pretty nasty depth clamping bug, but it also feels a bit excessive.
// We should try to find a way to resolve this without forcing the fragment
// shader to run.
// https://github.com/bevyengine/bevy/pull/8877
shader_defs.push("PREPASS_FRAGMENT".into());
}
if layout.contains(Mesh::ATTRIBUTE_UV_0) {
@ -457,8 +464,9 @@ where
// The fragment shader is only used when the normal prepass or motion vectors prepass
// is enabled or the material uses alpha cutoff values and doesn't rely on the standard
// prepass shader
// prepass shader or we are clamping the orthographic depth.
let fragment_required = !targets.is_empty()
|| key.mesh_key.contains(MeshPipelineKey::DEPTH_CLAMP_ORTHO)
|| (key.mesh_key.contains(MeshPipelineKey::MAY_DISCARD)
&& self.material_fragment_shader.is_some());

View File

@ -41,6 +41,10 @@ struct VertexOutput {
@location(3) world_position: vec4<f32>,
@location(4) previous_world_position: vec4<f32>,
#endif // MOTION_VECTOR_PREPASS
#ifdef DEPTH_CLAMP_ORTHO
@location(5) clip_position_unclamped: vec4<f32>,
#endif // DEPTH_CLAMP_ORTHO
}
@vertex
@ -55,7 +59,8 @@ fn vertex(vertex: Vertex) -> VertexOutput {
out.clip_position = mesh_position_local_to_clip(model, vec4(vertex.position, 1.0));
#ifdef DEPTH_CLAMP_ORTHO
out.clip_position.z = min(out.clip_position.z, 1.0);
out.clip_position_unclamped = out.clip_position;
out.clip_position.z = min(out.clip_position.z, 1.0);
#endif // DEPTH_CLAMP_ORTHO
#ifdef VERTEX_UVS
@ -96,6 +101,10 @@ struct FragmentInput {
@location(3) world_position: vec4<f32>,
@location(4) previous_world_position: vec4<f32>,
#endif // MOTION_VECTOR_PREPASS
#ifdef DEPTH_CLAMP_ORTHO
@location(5) clip_position_unclamped: vec4<f32>,
#endif // DEPTH_CLAMP_ORTHO
}
struct FragmentOutput {
@ -106,6 +115,10 @@ struct FragmentOutput {
#ifdef MOTION_VECTOR_PREPASS
@location(1) motion_vector: vec2<f32>,
#endif // MOTION_VECTOR_PREPASS
#ifdef DEPTH_CLAMP_ORTHO
@builtin(frag_depth) frag_depth: f32,
#endif // DEPTH_CLAMP_ORTHO
}
@fragment
@ -116,6 +129,10 @@ fn fragment(in: FragmentInput) -> FragmentOutput {
out.normal = vec4(in.world_normal * 0.5 + vec3(0.5), 1.0);
#endif
#ifdef DEPTH_CLAMP_ORTHO
out.frag_depth = in.clip_position_unclamped.z;
#endif // DEPTH_CLAMP_ORTHO
#ifdef MOTION_VECTOR_PREPASS
let clip_position_t = view.unjittered_view_proj * in.world_position;
let clip_position = clip_position_t.xy / clip_position_t.w;

View File

@ -22,6 +22,10 @@ struct FragmentInput {
@location(3) world_position: vec4<f32>,
@location(4) previous_world_position: vec4<f32>,
#endif // MOTION_VECTOR_PREPASS
#ifdef DEPTH_CLAMP_ORTHO
@location(5) clip_position_unclamped: vec4<f32>,
#endif // DEPTH_CLAMP_ORTHO
};
// Cutoff used for the premultiplied alpha modes BLEND and ADD.
@ -66,6 +70,10 @@ struct FragmentOutput {
#ifdef MOTION_VECTOR_PREPASS
@location(1) motion_vector: vec2<f32>,
#endif // MOTION_VECTOR_PREPASS
#ifdef DEPTH_CLAMP_ORTHO
@builtin(frag_depth) frag_depth: f32,
#endif // DEPTH_CLAMP_ORTHO
}
@fragment
@ -74,6 +82,10 @@ fn fragment(in: FragmentInput) -> FragmentOutput {
var out: FragmentOutput;
#ifdef DEPTH_CLAMP_ORTHO
out.frag_depth = in.clip_position_unclamped.z;
#endif // DEPTH_CLAMP_ORTHO
#ifdef NORMAL_PREPASS
// 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 {