EEVEE-Next: Shadow resolution scale and adaptive filtering

Allow the user to scale shadow-map resolution per-light.
Adapt the PCF scale based on shadow-map to pixel footprint ratio,
since we can no longer assume that higher LODs don't need filtering.
This allows using much lower shadow resolutions, which can yield
quite significant performance improvements, with relatively little
perceptual quality loss (at the cost of softening shadow edges).
The per-light resolution scale is a literal scale, so for example 0.5
means half the resolution. The Scene Simplify Shadows setting has
been updated to match this behavior.

Pull Request: https://projects.blender.org/blender/blender/pulls/119436
This commit is contained in:
Miguel Pozo 2024-03-20 15:54:41 +01:00
parent dc34e96dc4
commit 0c8b96d1e0
14 changed files with 121 additions and 48 deletions

View file

@ -119,6 +119,7 @@ class DATA_PT_EEVEE_light(DataButtonsPanel, Panel):
col.prop(light, "use_shadow", text="Cast Shadow")
col.prop(light, "shadow_softness_factor", text="Shadow Softness")
col.prop(light, "shadow_filter_radius", text="Filtering Radius")
col.prop(light, "shadow_resolution_scale", text="Resolution Scale")
if light.type == 'SUN':
col.prop(light, "shadow_trace_distance", text="Trace Distance")

View file

@ -3064,6 +3064,12 @@ void blo_do_versions_400(FileData *fd, Library * /*lib*/, Main *bmain)
}
}
if (!MAIN_VERSION_FILE_ATLEAST(bmain, 402, 11)) {
LISTBASE_FOREACH (Light *, light, &bmain->lights) {
light->shadow_resolution_scale = 1.0f;
}
}
/**
* Always bump subversion in BKE_blender_version.h when adding versioning
* code here, and wrap it inside a MAIN_VERSION_FILE_ATLEAST check.

View file

@ -80,6 +80,15 @@ void Light::sync(ShadowModule &shadows, const Object *ob, float threshold)
this->pcf_radius = la->shadow_filter_radius;
float resolution_scale = std::clamp(la->shadow_resolution_scale, 0.0f, 2.0f);
if (resolution_scale < 1.0) {
this->lod_bias = -log2(resolution_scale);
}
else {
this->lod_bias = -(resolution_scale - 1.0f);
}
this->lod_bias += shadows.get_global_lod_bias();
eLightType new_type = to_light_type(la->type, la->area_shape, la->mode & LA_USE_SOFT_FALLOFF);
if (assign_if_different(this->type, new_type)) {
shadow_discard_safe(shadows);

View file

@ -797,8 +797,6 @@ struct LightData {
float radius_squared;
/** Spot angle tangent. */
float spot_tan;
/** Reuse for directional LOD bias. */
#define _clipmap_lod_bias spot_tan
/** --- Shadow Data --- */
/** Near clip distances. Float stored as int for atomic operations. */
@ -821,7 +819,8 @@ struct LightData {
float shadow_trace_distance;
/* Radius in pixels for shadow filtering. */
float pcf_radius;
int _pad0;
/* Shadow Map resolution bias. */
float lod_bias;
};
BLI_STATIC_ASSERT_ALIGN(LightData, 16)
@ -1028,7 +1027,8 @@ struct ShadowSceneData {
int step_count;
/* Bias the shading point by using the normal to avoid self intersection. */
float normal_bias;
int _pad0;
/* Ratio between tile-map pixel world "radius" and film pixel world "radius". */
float tilemap_projection_ratio;
};
BLI_STATIC_ASSERT_ALIGN(ShadowSceneData, 16)

View file

@ -531,7 +531,7 @@ void ShadowDirectional::cascade_tilemaps_distribution(Light &light, const Camera
/* The bias is applied in cascade_level_range().
* Using clipmap_lod_min here simplify code in shadow_directional_level().
* Minus 1 because of the ceil(). */
light._clipmap_lod_bias = light.clipmap_lod_min - 1;
light.lod_bias = light.clipmap_lod_min - 1;
}
/************************************************************************
@ -621,7 +621,7 @@ void ShadowDirectional::clipmap_tilemaps_distribution(Light &light,
light.clipmap_lod_min = levels_range.first();
light.clipmap_lod_max = levels_range.last();
light._clipmap_lod_bias = lod_bias;
light.lod_bias = lod_bias;
}
void ShadowDirectional::sync(const float4x4 &object_mat,
@ -757,7 +757,7 @@ void ShadowModule::init()
simplify_shadows = inst_.is_viewport() ? scene.r.simplify_shadows :
scene.r.simplify_shadows_render;
}
lod_bias_ = math::interpolate(float(SHADOW_TILEMAP_LOD), 0.0f, simplify_shadows);
lod_bias_ = -log2(simplify_shadows);
const int2 atlas_extent = shadow_page_size_ * int2(SHADOW_PAGE_PER_ROW);
const int atlas_layers = divide_ceil_u(shadow_page_len_, SHADOW_PAGE_PER_LAYER);
@ -864,7 +864,7 @@ void ShadowModule::begin_sync()
sub.bind_ssbo("tilemaps_buf", &tilemap_pool.tilemaps_data);
sub.bind_ssbo("tiles_buf", &tilemap_pool.tiles_data);
sub.bind_texture("depth_tx", &src_depth_tx_);
sub.push_constant("tilemap_projection_ratio", &tilemap_projection_ratio_);
sub.push_constant("tilemap_projection_ratio", &data_.tilemap_projection_ratio);
sub.bind_resources(inst_.lights);
sub.dispatch(&dispatch_depth_scan_size_);
}
@ -880,7 +880,7 @@ void ShadowModule::begin_sync()
sub.bind_ssbo("tilemaps_buf", &tilemap_pool.tilemaps_data);
sub.bind_ssbo("tiles_buf", &tilemap_pool.tiles_data);
sub.bind_ssbo("bounds_buf", &manager.bounds_buf.current());
sub.push_constant("tilemap_projection_ratio", &tilemap_projection_ratio_);
sub.push_constant("tilemap_projection_ratio", &data_.tilemap_projection_ratio);
sub.push_constant("pixel_world_radius", &pixel_world_radius_);
sub.push_constant("fb_resolution", &usage_tag_fb_resolution_);
sub.push_constant("fb_lod", &usage_tag_fb_lod_);
@ -932,7 +932,7 @@ void ShadowModule::end_sync()
light.shadow_discard_safe(*this);
}
else if (light.directional != nullptr) {
light.directional->release_excess_tilemaps(inst_.camera, lod_bias_);
light.directional->release_excess_tilemaps(inst_.camera, light.lod_bias);
}
else if (light.punctual != nullptr) {
light.punctual->release_excess_tilemaps();
@ -946,10 +946,10 @@ void ShadowModule::end_sync()
light.tilemap_index = LIGHT_NO_SHADOW;
}
else if (light.directional != nullptr) {
light.directional->end_sync(light, inst_.camera, lod_bias_);
light.directional->end_sync(light, inst_.camera, light.lod_bias);
}
else if (light.punctual != nullptr) {
light.punctual->end_sync(light, lod_bias_);
light.punctual->end_sync(light, light.lod_bias);
}
else {
light.tilemap_index = LIGHT_NO_SHADOW;
@ -1088,7 +1088,7 @@ void ShadowModule::end_sync()
sub.shader_set(inst_.shaders.static_shader_get(SHADOW_TILEMAP_TAG_USAGE_VOLUME));
sub.bind_ssbo("tilemaps_buf", &tilemap_pool.tilemaps_data);
sub.bind_ssbo("tiles_buf", &tilemap_pool.tiles_data);
sub.push_constant("tilemap_projection_ratio", &tilemap_projection_ratio_);
sub.push_constant("tilemap_projection_ratio", &data_.tilemap_projection_ratio);
sub.bind_resources(inst_.uniform_data);
sub.bind_resources(inst_.hiz_buffer.front);
sub.bind_resources(inst_.sampling);
@ -1310,7 +1310,8 @@ void ShadowModule::set_view(View &view, GPUTexture *depth_tx)
dispatch_depth_scan_size_ = math::divide_ceil(target_size, int3(SHADOW_DEPTH_SCAN_GROUP_SIZE));
pixel_world_radius_ = screen_pixel_radius(view, int2(target_size));
tilemap_projection_ratio_ = tilemap_pixel_radius() / pixel_world_radius_;
data_.tilemap_projection_ratio = tilemap_pixel_radius() / pixel_world_radius_;
inst_.uniform_data.push_update();
usage_tag_fb_resolution_ = math::divide_ceil(int2(target_size),
int2(std::exp2(usage_tag_fb_lod_)));

View file

@ -249,8 +249,6 @@ class ShadowModule {
StorageArrayBuffer<uint, SHADOW_VIEW_MAX, true> viewport_index_buf_ = {"viewport_index_buf"};
int3 dispatch_depth_scan_size_;
/* Ratio between tile-map pixel world "radius" and film pixel world "radius". */
float tilemap_projection_ratio_;
float pixel_world_radius_;
int2 usage_tag_fb_resolution_;
int usage_tag_fb_lod_ = 5;
@ -359,6 +357,11 @@ class ShadowModule {
return data_;
}
float get_global_lod_bias()
{
return lod_bias_;
}
private:
void remove_unused();
void debug_page_map_call(DRWPass *pass);

View file

@ -110,22 +110,8 @@ void shadow_tag_usage_tilemap_punctual(
/* TODO(fclem): 3D shift for jittered soft shadows. */
lP += vec3(0.0, 0.0, -light.shadow_projection_shift);
/* How much a shadow map pixel covers a final image pixel.
* We project a shadow map pixel (as a sphere for simplicity) to the receiver plane.
* We then reproject this sphere onto the camera screen and compare it to the film pixel size.
* This gives a good approximation of what LOD to select to get a somewhat uniform shadow map
* resolution in screen space. */
float footprint_ratio = dist_to_light;
/* Project the radius to the screen. 1 unit away from the camera the same way
* pixel_world_radius_inv was computed. Not needed in orthographic mode. */
bool is_persp = (ProjectionMatrix[3][3] == 0.0);
if (is_persp) {
footprint_ratio /= dist_to_cam;
}
/* Apply resolution ratio. */
footprint_ratio *= tilemap_projection_ratio;
/* Take the frustum padding into account. */
footprint_ratio *= light.clip_side / orderedIntBitsToFloat(light.clip_near);
float footprint_ratio = shadow_punctual_footprint_ratio(
light, P, drw_view_is_perspective(), dist_to_cam, tilemap_projection_ratio);
if (radius == 0) {
int face_id = shadow_punctual_face_index_get(lP);

View file

@ -23,7 +23,7 @@ void main()
light.type = LIGHT_SUN;
light.clipmap_lod_min = -5;
light.clipmap_lod_max = 8;
light._clipmap_lod_bias = 0.0;
light.lod_bias = 0.0;
float fac = float(SHADOW_TILEMAP_RES - 1) / float(SHADOW_TILEMAP_RES);
EXPECT_EQ(shadow_directional_level(light, vec3(fac * 0.0)), light.clipmap_lod_min);
EXPECT_EQ(shadow_directional_level(light, vec3(fac * 0.49)), 1);
@ -51,7 +51,7 @@ void main()
light._clipmap_origin_x = 0.0;
light._clipmap_origin_y = 0.0;
float half_size = exp2(float(light.clipmap_lod_min - 1));
light._clipmap_lod_bias = light.clipmap_lod_min - 1;
light.lod_bias = light.clipmap_lod_min - 1;
float fac = float(SHADOW_TILEMAP_RES - 1) / float(SHADOW_TILEMAP_RES);
EXPECT_EQ(shadow_directional_level(light, vec3(fac * half_size * 0.0, 0.0, 0.0)), 2);
EXPECT_EQ(shadow_directional_level(light, vec3(fac * half_size * 0.5, 0.0, 0.0)), 2);
@ -76,7 +76,7 @@ void main()
light._position = vec3(0.0);
light._clipmap_origin_x = 0.0;
light._clipmap_origin_y = 0.0;
light._clipmap_lod_bias = 0;
light.lod_bias = 0;
float lod_min_tile_size = exp2(float(light.clipmap_lod_min)) / float(SHADOW_TILEMAP_RES);
float lod_max_half_size = exp2(float(light.clipmap_lod_max)) / 2.0;
@ -189,7 +189,7 @@ void main()
light.clipmap_lod_max = 2; /* 3 tile-maps. */
light.tilemap_index = 1;
light._position = vec3(0.0);
light._clipmap_lod_bias = light.clipmap_lod_min - 1;
light.lod_bias = light.clipmap_lod_min - 1;
light._clipmap_origin_x = 0.0;
light._clipmap_origin_y = 0.0;
float lod_tile_size = exp2(float(light.clipmap_lod_min)) / float(SHADOW_TILEMAP_RES);

View file

@ -3,6 +3,7 @@
* SPDX-License-Identifier: GPL-2.0-or-later */
#pragma BLENDER_REQUIRE(common_shape_lib.glsl)
#pragma BLENDER_REQUIRE(gpu_shader_utildefines_lib.glsl)
/* ---------------------------------------------------------------------- */
/** \name Tile-map data
@ -106,7 +107,8 @@ ShadowTileData shadow_tile_load(usampler2D tilemaps_tx, ivec2 tile_co, int tilem
* \a lP shading point position in light space, relative to the to camera position snapped to
* the smallest clip-map level (`shadow_world_to_local(light, P) - light._position`).
*/
int shadow_directional_level(LightData light, vec3 lP)
float shadow_directional_level_fractional(LightData light, vec3 lP)
{
float lod;
if (light.type == LIGHT_SUN) {
@ -124,10 +126,41 @@ int shadow_directional_level(LightData light, vec3 lP)
float lod_min_half_size = exp2(float(light.clipmap_lod_min - 1));
lod = length(lP.xy) * narrowing / lod_min_half_size;
}
int clipmap_lod = int(ceil(lod + light._clipmap_lod_bias));
float clipmap_lod = lod + light.lod_bias;
return clamp(clipmap_lod, light.clipmap_lod_min, light.clipmap_lod_max);
}
int shadow_directional_level(LightData light, vec3 lP)
{
return int(ceil(shadow_directional_level_fractional(light, lP)));
}
/* How much a tilemap pixel covers a final image pixel. */
float shadow_punctual_footprint_ratio(LightData light,
vec3 P,
bool is_perspective,
float dist_to_cam,
float tilemap_projection_ratio)
{
/* We project a shadow map pixel (as a sphere for simplicity) to the receiver plane.
* We then reproject this sphere onto the camera screen and compare it to the film pixel size.
* This gives a good approximation of what LOD to select to get a somewhat uniform shadow map
* resolution in screen space. */
float dist_to_light = distance(P, light._position);
float footprint_ratio = dist_to_light;
/* Project the radius to the screen. 1 unit away from the camera the same way
* pixel_world_radius_inv was computed. Not needed in orthographic mode. */
if (is_perspective) {
footprint_ratio /= dist_to_cam;
}
/* Apply resolution ratio. */
footprint_ratio *= tilemap_projection_ratio;
/* Take the frustum padding into account. */
footprint_ratio *= light.clip_side / orderedIntBitsToFloat(light.clip_near);
return footprint_ratio;
}
struct ShadowCoordinates {
/* Index of the tile-map to containing the tile. */
int tilemap_index;

View file

@ -412,7 +412,7 @@ SHADOW_MAP_TRACE_FN(ShadowRayPunctual)
/* Compute the world space offset of the shading position required for
* stochastic percentage closer filtering of shadow-maps. */
vec3 shadow_pcf_offset(LightData light, const bool is_directional, vec3 P, vec3 Ng)
vec3 shadow_pcf_offset(LightData light, const bool is_directional, vec3 P, vec3 Ng, vec2 random)
{
if (light.pcf_radius <= 0.001) {
/* Early return. */
@ -461,14 +461,27 @@ vec3 shadow_pcf_offset(LightData light, const bool is_directional, vec3 P, vec3
/* Compute the actual offset. */
vec2 rand = vec2(0.0);
#ifdef EEVEE_SAMPLING_DATA
rand = sampling_rng_2D_get(SAMPLING_SHADOW_V);
#endif
vec2 pcf_offset = interlieved_gradient_noise(UTIL_TEXEL, vec2(0.0), rand);
pcf_offset = pcf_offset * 2.0 - 1.0;
vec2 pcf_offset = random * 2.0 - 1.0;
pcf_offset *= light.pcf_radius;
/* Scale the offset based on shadow LOD. */
if (is_directional) {
vec3 lP = light_world_to_local(light, P);
float level = shadow_directional_level_fractional(light, lP - light._position);
float pcf_scale = mix(0.5, 1.0, fract(level));
pcf_offset *= pcf_scale;
}
else {
bool is_perspective = drw_view_is_perspective();
float dist_to_cam = distance(P, drw_view_position());
float footprint_ratio = shadow_punctual_footprint_ratio(
light, P, is_perspective, dist_to_cam, uniform_buf.shadow.tilemap_projection_ratio);
float lod = -log2(footprint_ratio) + light.lod_bias;
lod = clamp(lod, 0.0, float(SHADOW_TILEMAP_LOD));
float pcf_scale = exp2(lod);
pcf_offset *= pcf_scale;
}
vec3 ws_offset = TBN * vec3(pcf_offset, 0.0);
vec3 offset_P = P + ws_offset;
@ -515,17 +528,19 @@ ShadowEvalResult shadow_eval(LightData light,
# elif defined(GPU_COMPUTE_SHADER)
vec2 pixel = vec2(gl_GlobalInvocationID.xy);
# endif
vec3 random_shadow_3d = utility_tx_fetch(utility_tx, pixel, UTIL_BLUE_NOISE_LAYER).rgb;
random_shadow_3d += sampling_rng_3D_get(SAMPLING_SHADOW_U);
vec3 blue_noise_3d = utility_tx_fetch(utility_tx, pixel, UTIL_BLUE_NOISE_LAYER).rgb;
vec3 random_shadow_3d = blue_noise_3d + sampling_rng_3D_get(SAMPLING_SHADOW_U);
vec2 random_pcf_2d = fract(blue_noise_3d.xy + sampling_rng_2D_get(SAMPLING_SHADOW_X));
float normal_offset = uniform_buf.shadow.normal_bias;
#else
/* Case of surfel light eval. */
vec3 random_shadow_3d = vec3(0.5);
vec2 random_pcf_2d = vec2(0.0);
/* TODO(fclem): Parameter on irradiance volumes? */
float normal_offset = 0.02;
#endif
P += shadow_pcf_offset(light, is_directional, P, Ng);
P += shadow_pcf_offset(light, is_directional, P, Ng, random_pcf_2d);
/* Avoid self intersection. */
P = offset_ray(P, Ng);

View file

@ -44,6 +44,7 @@
.shadow_softness_factor = 1.0f, \
.shadow_trace_distance = 10.0f, \
.shadow_filter_radius = 3.0f, \
.shadow_resolution_scale = 1.0f, \
.att_dist = 40.0f, \
.sun_angle = DEG2RADF(0.526f), \
.area_spread = DEG2RADF(180.0f), \

View file

@ -78,6 +78,9 @@ typedef struct Light {
float shadow_softness_factor;
float shadow_trace_distance;
float shadow_filter_radius;
float shadow_resolution_scale;
float _pad0;
/* Preview */
struct PreviewImage *preview;

View file

@ -304,6 +304,17 @@ static void rna_def_light_shadow(StructRNA *srna, bool sun)
RNA_def_property_override_flag(prop, PROPOVERRIDE_OVERRIDABLE_LIBRARY);
RNA_def_property_update(prop, 0, "rna_Light_update");
prop = RNA_def_property(srna, "shadow_resolution_scale", PROP_FLOAT, PROP_FACTOR);
RNA_def_property_range(prop, 0.0f, 2.0f);
RNA_def_property_ui_range(prop, 0.0f, 2.0f, 0.25f, 2);
RNA_def_property_ui_text(
prop,
"Shadow Resolution Scale",
"Scale the Shadow Map target resolution, where 1.0 tries to match shadow map and screen "
"pixel density. (The scale is applied on top of the scene Simplify Shadow Resolution)");
RNA_def_property_override_flag(prop, PROPOVERRIDE_OVERRIDABLE_LIBRARY);
RNA_def_property_update(prop, 0, "rna_Light_update");
if (sun) {
prop = RNA_def_property(srna, "shadow_cascade_max_distance", PROP_FLOAT, PROP_DISTANCE);
RNA_def_property_float_sdna(prop, nullptr, "cascade_max_dist");

View file

@ -2071,6 +2071,10 @@ static void object_simplify_update(Scene *scene,
DEG_id_tag_update(&ob->id, ID_RECALC_GEOMETRY);
}
}
if (ob->type == OB_LAMP) {
DEG_id_tag_update(&ob->id, ID_RECALC_SHADING);
}
}
static void rna_Scene_simplify_update_impl(Main *bmain,