Fix: EEVEE-Next: Shadow: Casters behind area lights cast shadow

This introduces fragment clipping for the shadow pipeline.

The fragments are culled using a sphere distance test
for sphere lights, which is then re-purposed as a simple
distance test for area lights.

However, in the case of area lights, this introduces some
light bleeding. To mitigate this, we bring the shadow origin
back towards the light center so that this artifact only
appears if using larger shadow tracing.

Fixes #119334

Pull Request: https://projects.blender.org/blender/blender/pulls/120992
This commit is contained in:
Clément Foucault
2024-04-24 17:08:12 +02:00
committed by Clément Foucault
parent b8e17cf531
commit e725cc3494
15 changed files with 177 additions and 54 deletions

View File

@@ -245,7 +245,7 @@
/* Only during shadow rendering. */
#define SHADOW_RENDER_MAP_BUF_SLOT 3
#define SHADOW_PAGE_INFO_SLOT 4
#define SHADOW_VIEWPORT_INDEX_BUF_SLOT 5
#define SHADOW_RENDER_VIEW_BUF_SLOT 5
/* Only during pre-pass. */
#define VELOCITY_OBJ_PREV_BUF_SLOT 0

View File

@@ -204,7 +204,7 @@ void ShadowPipeline::sync()
draw::PassMain::Sub &pass = render_ps_.sub("Shadow.Surface");
pass.state_set(state);
pass.bind_texture(RBUFS_UTILITY_TEX_SLOT, inst_.pipelines.utility_tx);
pass.bind_ssbo(SHADOW_VIEWPORT_INDEX_BUF_SLOT, &inst_.shadows.viewport_index_buf_);
pass.bind_ssbo(SHADOW_RENDER_VIEW_BUF_SLOT, &inst_.shadows.render_view_buf_);
if (!shadow_update_tbdr) {
/* We do not need all of the shadow information when using the TBDR-optimized approach. */
pass.bind_image(SHADOW_ATLAS_IMG_SLOT, inst_.shadows.atlas_tx_);

View File

@@ -1146,9 +1146,12 @@ struct ShadowTileMapData {
int clip_data_index;
/** Bias LOD to tag for usage to lower the amount of tile used. */
float lod_bias;
int _pad0;
int _pad1;
int _pad2;
/** Light type this tilemap is from. */
eLightType light_type;
/** True if the tilemap is part of area light shadow and is one of the side projections. */
bool32_t is_area_side;
/** Distance behind the area light a shadow is shifted. */
float area_shift;
/** Near and far clip distances for punctual. */
float clip_near;
float clip_far;
@@ -1159,8 +1162,29 @@ struct ShadowTileMapData {
};
BLI_STATIC_ASSERT_ALIGN(ShadowTileMapData, 16)
/**
* Lightweight version of ShadowTileMapData that only contains data used for rendering the shadow.
*/
struct ShadowRenderView {
/**
* Is either:
* - positive radial distance for point lights.
* - negative distance to light plane (divided by sqrt3) for area lights side projections.
* - zero if disabled.
* Use sign to determine with case we are in.
*/
float clip_distance_inv;
/* Viewport to submit the geometry of this tilemap view to. */
uint viewport_index;
uint _pad0;
uint _pad1;
};
BLI_STATIC_ASSERT_ALIGN(ShadowRenderView, 16)
/**
* Per tilemap data persistent on GPU.
* Kept separately for easier clearing on GPU.
*/
struct ShadowTileMapClip {
/** Clip distances that were used to render the pages. */
@@ -1976,6 +2000,7 @@ using ShadowPageCacheBuf = draw::StorageArrayBuffer<uint2, SHADOW_MAX_PAGE, true
using ShadowTileMapDataBuf = draw::StorageVectorBuffer<ShadowTileMapData, SHADOW_MAX_TILEMAP>;
using ShadowTileMapClipBuf = draw::StorageArrayBuffer<ShadowTileMapClip, SHADOW_MAX_TILEMAP, true>;
using ShadowTileDataBuf = draw::StorageArrayBuffer<ShadowTileDataPacked, SHADOW_MAX_TILE, true>;
using ShadowRenderViewBuf = draw::StorageArrayBuffer<ShadowRenderView, SHADOW_VIEW_MAX, true>;
using SurfelBuf = draw::StorageArrayBuffer<Surfel, 64>;
using SurfelRadianceBuf = draw::StorageArrayBuffer<SurfelRadiance, 64>;
using CaptureInfoBuf = draw::StorageBuffer<CaptureInfoData>;

View File

@@ -37,6 +37,8 @@ void ShadowTileMap::sync_orthographic(const float4x4 &object_mat_,
}
projection_type = projection_type_;
level = clipmap_level;
light_type = eLightType::LIGHT_SUN;
is_area_side = false;
if (grid_shift == int2(0)) {
/* Only replace shift if it is not already dirty. */
@@ -69,7 +71,8 @@ void ShadowTileMap::sync_orthographic(const float4x4 &object_mat_,
1.0);
}
void ShadowTileMap::sync_cubeface(const float4x4 &object_mat_,
void ShadowTileMap::sync_cubeface(eLightType light_type_,
const float4x4 &object_mat_,
float near_,
float far_,
float side_,
@@ -84,6 +87,8 @@ void ShadowTileMap::sync_cubeface(const float4x4 &object_mat_,
cubeface = face;
grid_offset = int2(0);
lod_bias = lod_bias_;
light_type = light_type_;
is_area_side = is_area_light(light_type) && (face != eCubeFace::Z_NEG);
if ((clip_near != near_) || (clip_far != far_) || (half_size != side_)) {
set_dirty();
@@ -91,6 +96,7 @@ void ShadowTileMap::sync_cubeface(const float4x4 &object_mat_,
clip_near = near_;
clip_far = far_;
area_shift = shift;
half_size = side_;
center_offset = float2(0.0f);
@@ -259,25 +265,27 @@ void ShadowPunctual::release_excess_tilemaps()
tilemaps_ = span.take_front(tilemaps_needed_);
}
void ShadowPunctual::compute_projection_boundaries(float light_radius,
void ShadowPunctual::compute_projection_boundaries(eLightType light_type,
float light_radius,
float shadow_radius,
float max_lit_distance,
float &near,
float &far,
float &side)
float &side,
float &back_shift)
{
/**
/*
* In order to make sure we can trace any ray in its entirety using a single tile-map, we have
* to make sure that the tile-map cover all potential occluder that can intersect any ray shot
* in this particular shadow quadrant.
*
* To this end, we shift the tile-map perspective origin behind the light shape and make sure the
* To this end, we inflate the tile-map perspective sides to make sure the
* tile-map frustum starts where the rays cannot go.
*
* We are interesting in finding `I` the new origin and `n` the new near plane distances.
*
* I .... Shifted light center
* /|
* I .... Intersection between tangent and
* /| projection center axis
* / |
* / |
* / |
@@ -292,22 +300,22 @@ void ShadowPunctual::compute_projection_boundaries(float light_radius,
* / ... |
* /. |
* / |
* Tangent to light shape .... T\--------------N .... Shifted near plane
* Tangent to light shape .... T\--------------N
* / --\ Beta |
* / -\ |
* / --\ |
* /. --\ |
* / . -\ |
* / . Alpha -O .... Light center
* / . --/ |
* / . --/ |
* / . -/ |
* / . --/ |
* /-------------/------------x .... Desired near plane (inscribed cube)
* / --/ .. |
* / --/ ... |
* / --/ .... |
* / -/ ....|
* / . /-/ |
* Inflated side / . /--- -/ |
* . / . /---- --/ |
* . / /---- . --/ |
* /-------------/------------X .... Desired near plane (inscribed cube)
* /---- --/ .. |
* /---- / --/ ... |
* /---- / --/ .... |
* / -/ ....| .... Shadow radius
* / --/ |
* /--/ |
* F .... Most distant shadow receiver possible.
@@ -315,18 +323,36 @@ void ShadowPunctual::compute_projection_boundaries(float light_radius,
* F: The most distant shadowed point at the edge of the 45° cube-face pyramid.
* O: The light origin.
* T: The tangent to the circle of radius `radius` centered at the origin and passing through F.
* I: The shifted light origin.
* I: Intersection between tangent and the projection center axis.
* N: The shifted near plane center.
* X: Intersection between the near plane and the projection center axis.
* Alpha: FOT angle.
* Beta: OTN angle.
*
* TODO(fclem): Explain derivation.
* Note: FTO, ONT and TNI are right angles.
*/
float cos_alpha = shadow_radius / max_lit_distance;
float sin_alpha = sqrt(1.0f - math::square(cos_alpha));
float near_shift = M_SQRT2 * shadow_radius * 0.5f * (sin_alpha - cos_alpha);
float side_shift = M_SQRT2 * shadow_radius * 0.5f * (sin_alpha + cos_alpha);
float origin_shift = M_SQRT2 * shadow_radius / (sin_alpha - cos_alpha);
/* Make near plane to be inside the inscribed cube of the sphere. */
near = max_ff(light_radius, max_lit_distance / 4000.0f) / M_SQRT3;
float min_near = (max_lit_distance / 4000.0f) / M_SQRT3;
if (is_area_light(light_type)) {
/* Make near plane be inside the inscribed cube of the shadow sphere. */
near = max_ff(shadow_radius / M_SQRT3, min_near);
/* Subtract min_near to make the shadow center match the light center if there is no shadow
* tracing required. This avoid light leaking issues near the light plane caused by the
* shadow discard clipping. */
back_shift = (near - min_near);
}
else {
/* Make near plane be inside the inscribed cube of the light sphere. */
near = max_ff(light_radius / M_SQRT3, min_near);
back_shift = 0.0f;
}
far = max_lit_distance;
if (shadow_radius > 1e-5f) {
side = ((side_shift / (origin_shift - near_shift)) * (origin_shift + near));
@@ -340,11 +366,9 @@ void ShadowPunctual::end_sync(Light &light, float lod_bias)
{
ShadowTileMapPool &tilemap_pool = shadows_.tilemap_pool;
float side, near, far;
compute_projection_boundaries(light_radius_, shadow_radius_, max_distance_, near, far, side);
/* Shift shadow map origin for area light to avoid clipping nearby geometry. */
float shift = is_area_light(light.type) ? near : 0.0f;
float side, near, far, shift;
compute_projection_boundaries(
light.type, light_radius_, shadow_radius_, max_distance_, near, far, side, shift);
float4x4 obmat_tmp = light.object_mat;
@@ -357,15 +381,20 @@ void ShadowPunctual::end_sync(Light &light, float lod_bias)
tilemaps_.append(tilemap_pool.acquire());
}
tilemaps_[Z_NEG]->sync_cubeface(obmat_tmp, near, far, side, shift, Z_NEG, lod_bias);
tilemaps_[Z_NEG]->sync_cubeface(light.type, obmat_tmp, near, far, side, shift, Z_NEG, lod_bias);
if (tilemaps_needed_ >= 5) {
tilemaps_[X_POS]->sync_cubeface(obmat_tmp, near, far, side, shift, X_POS, lod_bias);
tilemaps_[X_NEG]->sync_cubeface(obmat_tmp, near, far, side, shift, X_NEG, lod_bias);
tilemaps_[Y_POS]->sync_cubeface(obmat_tmp, near, far, side, shift, Y_POS, lod_bias);
tilemaps_[Y_NEG]->sync_cubeface(obmat_tmp, near, far, side, shift, Y_NEG, lod_bias);
tilemaps_[X_POS]->sync_cubeface(
light.type, obmat_tmp, near, far, side, shift, X_POS, lod_bias);
tilemaps_[X_NEG]->sync_cubeface(
light.type, obmat_tmp, near, far, side, shift, X_NEG, lod_bias);
tilemaps_[Y_POS]->sync_cubeface(
light.type, obmat_tmp, near, far, side, shift, Y_POS, lod_bias);
tilemaps_[Y_NEG]->sync_cubeface(
light.type, obmat_tmp, near, far, side, shift, Y_NEG, lod_bias);
}
if (tilemaps_needed_ == 6) {
tilemaps_[Z_POS]->sync_cubeface(obmat_tmp, near, far, side, shift, Z_POS, lod_bias);
tilemaps_[Z_POS]->sync_cubeface(
light.type, obmat_tmp, near, far, side, shift, Z_POS, lod_bias);
}
light.tilemap_index = tilemap_pool.tilemaps_data.size();
@@ -1195,7 +1224,7 @@ void ShadowModule::end_sync()
sub.bind_ssbo("dst_coord_buf", dst_coord_buf_);
sub.bind_ssbo("src_coord_buf", src_coord_buf_);
sub.bind_ssbo("render_map_buf", render_map_buf_);
sub.bind_ssbo("viewport_index_buf", viewport_index_buf_);
sub.bind_ssbo("render_view_buf", render_view_buf_);
sub.bind_ssbo("pages_infos_buf", pages_infos_data_);
sub.bind_image("tilemaps_img", tilemap_pool.tilemap_tx);
sub.dispatch(int3(1, 1, tilemap_pool.tilemaps_data.size()));

View File

@@ -103,7 +103,8 @@ struct ShadowTileMap : public ShadowTileMapData {
float lod_bias_,
eShadowProjectionType projection_type_);
void sync_cubeface(const float4x4 &object_mat,
void sync_cubeface(eLightType light_type_,
const float4x4 &object_mat,
float near,
float far,
float side,
@@ -252,8 +253,8 @@ class ShadowModule {
StorageArrayBuffer<uint, SHADOW_RENDER_MAP_SIZE, true> src_coord_buf_ = {"src_coord_buf"};
/** Same as dst_coord_buf_ but is not compact. More like a linear texture. */
StorageArrayBuffer<uint, SHADOW_RENDER_MAP_SIZE, true> render_map_buf_ = {"render_map_buf"};
/** View to viewport index mapping. */
StorageArrayBuffer<uint, SHADOW_VIEW_MAX, true> viewport_index_buf_ = {"viewport_index_buf"};
/** View to viewport index mapping and other render-only related data. */
ShadowRenderViewBuf render_view_buf_ = {"render_view_buf"};
int3 dispatch_depth_scan_size_;
float pixel_world_radius_;
@@ -453,12 +454,14 @@ class ShadowPunctual : public NonCopyable, NonMovable {
* Make sure that the projection encompass all possible rays that can start in the projection
* quadrant.
*/
void compute_projection_boundaries(float light_radius,
void compute_projection_boundaries(eLightType light_type,
float light_radius,
float shadow_radius,
float max_lit_distance,
float &near,
float &far,
float &side);
float &side,
float &back_shift);
};
class ShadowDirectional : public NonCopyable, NonMovable {

View File

@@ -13,7 +13,7 @@ void main()
{
DRW_VIEW_FROM_RESOURCE_ID;
#ifdef MAT_SHADOW
shadow_viewport_layer_set(int(drw_view_id), int(viewport_index_buf[drw_view_id]));
shadow_viewport_layer_set(int(drw_view_id), int(render_view_buf[drw_view_id].viewport_index));
#endif
init_interface();
@@ -57,5 +57,10 @@ void main()
clip_interp.clip_distance = dot(clip_plane.plane, vec4(interp.P, 1.0));
#endif
#ifdef MAT_SHADOW
shadow_clip.vector = shadow_clip_vector_get(drw_point_world_to_view(interp.P),
render_view_buf[drw_view_id]);
#endif
gl_Position = drw_point_world_to_homogenous(interp.P);
}

View File

@@ -13,7 +13,7 @@ void main()
{
DRW_VIEW_FROM_RESOURCE_ID;
#ifdef MAT_SHADOW
shadow_viewport_layer_set(int(drw_view_id), int(viewport_index_buf[drw_view_id]));
shadow_viewport_layer_set(int(drw_view_id), int(render_view_buf[drw_view_id].viewport_index));
#endif
init_interface();
@@ -56,4 +56,9 @@ void main()
#ifdef MAT_CLIP_PLANE
clip_interp.clip_distance = dot(clip_plane.plane, vec4(interp.P, 1.0));
#endif
#ifdef MAT_SHADOW
shadow_clip.vector = shadow_clip_vector_get(drw_point_world_to_view(interp.P),
render_view_buf[drw_view_id]);
#endif
}

View File

@@ -12,7 +12,7 @@ void main()
{
DRW_VIEW_FROM_RESOURCE_ID;
#ifdef MAT_SHADOW
shadow_viewport_layer_set(int(drw_view_id), int(viewport_index_buf[drw_view_id]));
shadow_viewport_layer_set(int(drw_view_id), int(render_view_buf[drw_view_id].viewport_index));
#endif
init_interface();
@@ -39,5 +39,10 @@ void main()
clip_interp.clip_distance = dot(clip_plane.plane, vec4(interp.P, 1.0));
#endif
#ifdef MAT_SHADOW
shadow_clip.vector = shadow_clip_vector_get(drw_point_world_to_view(interp.P),
render_view_buf[drw_view_id]);
#endif
gl_Position = drw_point_world_to_homogenous(interp.P);
}

View File

@@ -14,7 +14,7 @@ void main()
{
DRW_VIEW_FROM_RESOURCE_ID;
#ifdef MAT_SHADOW
shadow_viewport_layer_set(int(drw_view_id), int(viewport_index_buf[drw_view_id]));
shadow_viewport_layer_set(int(drw_view_id), int(render_view_buf[drw_view_id].viewport_index));
#endif
init_interface();
@@ -51,5 +51,10 @@ void main()
clip_interp.clip_distance = dot(clip_plane.plane, vec4(interp.P, 1.0));
#endif
#ifdef MAT_SHADOW
shadow_clip.vector = shadow_clip_vector_get(drw_point_world_to_view(interp.P),
render_view_buf[drw_view_id]);
#endif
gl_Position = drw_point_world_to_homogenous(interp.P);
}

View File

@@ -103,7 +103,21 @@ void main()
view_index = atomicAdd(statistics_buf.view_needed_count, 1);
if (view_index < SHADOW_VIEW_MAX) {
/* Setup the view. */
viewport_index_buf[view_index] = viewport_index;
render_view_buf[view_index].viewport_index = viewport_index;
/* Clipping setup. */
if (tilemap_data.is_area_side) {
/* Negative for tagging this case. See shadow_clip_vector_get for explanation. */
render_view_buf[view_index].clip_distance_inv = -M_SQRT1_3 / tilemap_data.area_shift;
}
else if (is_point_light(tilemap_data.light_type)) {
/* Clip as a sphere around the clip_near cube. */
render_view_buf[view_index].clip_distance_inv = M_SQRT1_3 / tilemap_data.clip_near;
}
else {
/* Disable local clipping. */
render_view_buf[view_index].clip_distance_inv = 0.0;
}
view_infos_buf[view_index].viewmat = tilemap_data.viewmat;
view_infos_buf[view_index].viewinv = inverse(tilemap_data.viewmat);

View File

@@ -161,6 +161,26 @@ void shadow_viewport_layer_set(int view_id, int lod)
# endif
gpu_ViewportIndex = lod;
}
/* In order to support physical clipping, we pass a vector to the fragment shader that then clips
* each fragment using a unit sphere test. This allows to support both point light and area light
* clipping at the same time. */
vec3 shadow_clip_vector_get(vec3 view_position, ShadowRenderView shadow_view)
{
float clip_distance_inv = shadow_view.clip_distance_inv;
if (clip_distance_inv == 0.0) {
/* No clipping. */
return vec3(2.0);
}
if (clip_distance_inv < 0.0) {
/* Area light side projections. Clip using the up axis (which maps to light -Z). */
/* Note: clip_distance_inv should already be scaled by M_SQRT3. */
return vec3(view_position.y * clip_distance_inv);
}
/* Sphere light case. */
return view_position * clip_distance_inv;
}
#endif
#if defined(GPU_FRAGMENT_SHADER) && defined(MAT_SHADOW)

View File

@@ -26,6 +26,12 @@ void main()
{
float f_depth = gl_FragCoord.z + fwidth(gl_FragCoord.z);
/* Clip to light shape. */
if (length_squared(shadow_clip.vector) < 1.0) {
discard;
return;
}
#ifdef MAT_TRANSPARENT
init_globals();

View File

@@ -231,14 +231,18 @@ GPU_SHADER_CREATE_INFO(eevee_surf_world)
GPU_SHADER_INTERFACE_INFO(eevee_surf_shadow_atomic_iface, "shadow_iface")
.flat(Type::INT, "shadow_view_id");
GPU_SHADER_INTERFACE_INFO(eevee_surf_shadow_clipping_iface, "shadow_clip")
.smooth(Type::VEC3, "vector");
GPU_SHADER_CREATE_INFO(eevee_surf_shadow)
.define("DRW_VIEW_LEN", STRINGIFY(SHADOW_VIEW_MAX))
.define("MAT_SHADOW")
.builtins(BuiltinBits::VIEWPORT_INDEX)
.storage_buf(SHADOW_VIEWPORT_INDEX_BUF_SLOT,
.vertex_out(eevee_surf_shadow_clipping_iface)
.storage_buf(SHADOW_RENDER_VIEW_BUF_SLOT,
Qualifier::READ,
"uint",
"viewport_index_buf[SHADOW_VIEW_MAX]")
"ShadowRenderView",
"render_view_buf[SHADOW_VIEW_MAX]")
.fragment_source("eevee_surf_shadow_frag.glsl")
.additional_info("eevee_global_ubo", "eevee_utility_texture", "eevee_sampling_data");

View File

@@ -189,7 +189,7 @@ GPU_SHADER_CREATE_INFO(eevee_shadow_tilemap_finalize)
.storage_buf(7, Qualifier::WRITE, SHADOW_PAGE_PACKED, "dst_coord_buf[SHADOW_RENDER_MAP_SIZE]")
.storage_buf(8, Qualifier::WRITE, SHADOW_PAGE_PACKED, "src_coord_buf[SHADOW_RENDER_MAP_SIZE]")
.storage_buf(9, Qualifier::WRITE, SHADOW_PAGE_PACKED, "render_map_buf[SHADOW_RENDER_MAP_SIZE]")
.storage_buf(10, Qualifier::WRITE, "uint", "viewport_index_buf[SHADOW_VIEW_MAX]")
.storage_buf(10, Qualifier::WRITE, "ShadowRenderView", "render_view_buf[SHADOW_VIEW_MAX]")
.storage_buf(11, Qualifier::READ, "ShadowTileMapClip", "tilemaps_clip_buf[]")
/* 12 is the minimum number of storage buf we require. Do not go above this limit. */
.image(0, GPU_R32UI, Qualifier::WRITE, ImageType::UINT_2D, "tilemaps_img")

View File

@@ -233,7 +233,8 @@ static void test_eevee_shadow_tag_update()
{
ShadowTileMap tilemap(0 * SHADOW_TILEDATA_PER_TILEMAP);
tilemap.sync_cubeface(float4x4::identity(), 0.01f, 1.0f, 0.01f, 0.0f, Z_NEG, 0.0f);
tilemap.sync_cubeface(
LIGHT_OMNI_SPHERE, float4x4::identity(), 0.01f, 1.0f, 0.01f, 0.0f, Z_NEG, 0.0f);
tilemaps_data.append(tilemap);
}
{
@@ -1541,7 +1542,8 @@ static void test_eevee_shadow_page_mask_ex(int max_view_per_tilemap)
{
ShadowTileMap tilemap(0);
tilemap.sync_cubeface(float4x4::identity(), 0.01f, 1.0f, 0.01f, 0.0f, Z_NEG, 0.0f);
tilemap.sync_cubeface(
LIGHT_OMNI_SPHERE, float4x4::identity(), 0.01f, 1.0f, 0.01f, 0.0f, Z_NEG, 0.0f);
tilemaps_data.append(tilemap);
}