EEVEE-Next: Add Shadows PCF #118220

Merged
Miguel Pozo merged 24 commits from pragma37/blender:pull-eevee-pcf-shadows into main 2024-02-29 15:47:26 +01:00
12 changed files with 287 additions and 74 deletions

View File

@ -118,6 +118,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")
if light.type == 'SUN':
col.prop(light, "shadow_trace_distance", text="Trace Distance")

View File

@ -29,7 +29,7 @@ extern "C" {
/* Blender file format version. */
#define BLENDER_FILE_VERSION BLENDER_VERSION
#define BLENDER_FILE_SUBVERSION 7
#define BLENDER_FILE_SUBVERSION 8
/* Minimum Blender version that supports reading file written with the current
* version. Older Blender versions will test this and cancel loading the file, showing a warning to

View File

@ -2975,6 +2975,12 @@ void blo_do_versions_400(FileData *fd, Library * /*lib*/, Main *bmain)
}
}
if (!MAIN_VERSION_FILE_ATLEAST(bmain, 402, 8)) {
LISTBASE_FOREACH (Light *, light, &bmain->lights) {
light->shadow_filter_radius = 3.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

@ -78,6 +78,8 @@ void Light::sync(ShadowModule &shadows, const Object *ob, float threshold)
this->power[LIGHT_SPECULAR] = la->spec_fac * shape_power;
this->power[LIGHT_VOLUME] = la->volume_fac * point_power;
this->pcf_radius = la->shadow_filter_radius;
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

@ -807,8 +807,6 @@ struct LightData {
int tilemap_index;
/** Directional : Offset of the LOD min in LOD min tile units. */
int2 clipmap_base_offset;
/** Number of step for shadow map tracing. */
int shadow_ray_step_count;
/** Punctual: Other parts of the perspective matrix. */
float clip_side;
/** Punctual: Shift to apply to the light origin to get the shadow projection origin. */
@ -817,7 +815,9 @@ struct LightData {
float shadow_shape_scale_or_angle;
/** Trace distance for directional lights. */
float shadow_trace_distance;
float _pad2;
/* Radius in pixels for shadow filtering. */
float pcf_radius;
int _pad0;
};
BLI_STATIC_ASSERT_ALIGN(LightData, 16)
@ -1024,7 +1024,7 @@ struct ShadowSceneData {
int step_count;
/* Bias the shading point by using the normal to avoid self intersection. */
pragma37 marked this conversation as resolved Outdated

Add description.

Add description.
float normal_bias;
int _pad2;
int _pad0;
};
BLI_STATIC_ASSERT_ALIGN(ShadowSceneData, 16)

View File

@ -14,20 +14,39 @@
# define SHADOW_ATLAS_TYPE usampler2DArray
#endif
float shadow_read_depth_at_tilemap_uv(SHADOW_ATLAS_TYPE atlas_tx,
usampler2D tilemaps_tx,
int tilemap_index,
vec2 tilemap_uv)
struct ShadowSampleParams {
vec3 lP;
vec3 uv;
int tilemap_index;
float z_range;
};
ShadowTileData shadow_tile_data_get(usampler2D tilemaps_tx, ShadowSampleParams params)
{
/* Prevent out of bound access. Assumes the input is already non negative. */
tilemap_uv = min(tilemap_uv, vec2(0.99999));
vec2 tilemap_uv = min(params.uv.xy, vec2(0.99999));
ivec2 texel_coord = ivec2(tilemap_uv * float(SHADOW_MAP_MAX_RES));
/* Using bitwise ops is way faster than integer ops. */
const int page_shift = SHADOW_PAGE_LOD;
ivec2 tile_coord = texel_coord >> page_shift;
ShadowTileData tile = shadow_tile_load(tilemaps_tx, tile_coord, tilemap_index);
return shadow_tile_load(tilemaps_tx, tile_coord, params.tilemap_index);
}
float shadow_read_depth(SHADOW_ATLAS_TYPE atlas_tx,
usampler2D tilemaps_tx,
ShadowSampleParams params)
{
/* Prevent out of bound access. Assumes the input is already non negative. */
vec2 tilemap_uv = min(params.uv.xy, vec2(0.99999));
ivec2 texel_coord = ivec2(tilemap_uv * float(SHADOW_MAP_MAX_RES));
/* Using bitwise ops is way faster than integer ops. */
const int page_shift = SHADOW_PAGE_LOD;
ivec2 tile_coord = texel_coord >> page_shift;
ShadowTileData tile = shadow_tile_load(tilemaps_tx, tile_coord, params.tilemap_index);
if (!tile.is_allocated) {
return -1.0;
@ -51,25 +70,36 @@ struct ShadowEvalResult {
/** \name Shadow Sampling Functions
* \{ */
/* TODO(fclem): Remove. Only here to avoid include order hell with common_math_lib. */
mat4x4 shadow_projection_perspective(
float left, float right, float bottom, float top, float near_clip, float far_clip)
mat4x4 shadow_projection_perspective(float side, float near_clip, float far_clip)
{
pragma37 marked this conversation as resolved Outdated

You can remove the TODO on top as this is now a valid optimization

You can remove the `TODO` on top as this is now a valid optimization
float x_delta = right - left;
float y_delta = top - bottom;
float z_delta = far_clip - near_clip;
mat4x4 mat = mat4x4(1.0);
if (x_delta != 0.0 && y_delta != 0.0 && z_delta != 0.0) {
mat[0][0] = near_clip * 2.0 / x_delta;
mat[1][1] = near_clip * 2.0 / y_delta;
mat[2][0] = (right + left) / x_delta; /* NOTE: negate Z. */
mat[2][1] = (top + bottom) / y_delta;
mat[2][2] = -(far_clip + near_clip) / z_delta;
mat[2][3] = -1.0;
mat[3][2] = (-2.0 * near_clip * far_clip) / z_delta;
mat[3][3] = 0.0;
}
mat[0][0] = near_clip / side;
mat[1][1] = near_clip / side;
mat[2][0] = 0.0;
mat[2][1] = 0.0;
mat[2][2] = -(far_clip + near_clip) / z_delta;
mat[2][3] = -1.0;
mat[3][2] = (-2.0 * near_clip * far_clip) / z_delta;
mat[3][3] = 0.0;
return mat;
}
mat4x4 shadow_projection_perspective_inverse(float side, float near_clip, float far_clip)
{
float z_delta = far_clip - near_clip;
float d = 2.0 * near_clip * far_clip;
mat4x4 mat = mat4x4(1.0);
mat[0][0] = side / near_clip;
mat[1][1] = side / near_clip;
mat[2][0] = 0.0;
mat[2][1] = 0.0;
pragma37 marked this conversation as resolved Outdated

I'm wondering if we could remove this check as we ensure both are valid on CPU side.

I'm wondering if we could remove this check as we ensure both are valid on CPU side.
mat[2][2] = 0.0;
mat[2][3] = (near_clip - far_clip) / d;
mat[3][2] = -1.0;
mat[3][3] = (near_clip + far_clip) / d;
return mat;
}
@ -92,33 +122,142 @@ float shadow_linear_occluder_distance(LightData light,
return receiver_z - occluder_z;
}
ShadowEvalResult shadow_punctual_sample_get(SHADOW_ATLAS_TYPE atlas_tx,
usampler2D tilemaps_tx,
LightData light,
vec3 P)
mat4 shadow_punctual_projection_perspective(LightData light)
{
/* Face Local (View) Space > Clip Space. */
float clip_far = intBitsToFloat(light.clip_far);
pragma37 marked this conversation as resolved Outdated

Given this function is called twice, I would put the winmat or rather wininv (or both) in the ShadowSampleParams to avoid setting it up twice. The compiler should be smart enough to eliminate it when not needed.

Given this function is called twice, I would put the `winmat` or rather `wininv` (or both) in the `ShadowSampleParams` to avoid setting it up twice. The compiler should be smart enough to eliminate it when not needed.
float clip_near = intBitsToFloat(light.clip_near);
pragma37 marked this conversation as resolved Outdated

This inverse is very nasty. Please create a shadow_projection_perspective_inverse and populate it manually. Might as well do the TODO and simplify it for the symmetrical case.

This is the simplified function:

mat4x4 shadow_projection_perspective(float side, float near_clip, float far_clip)
{
  float z_delta = far_clip - near_clip;

  mat4x4 mat = mat4x4(1.0);
  if (side != 0.0 && z_delta != 0.0) {
    mat[0][0] = near_clip / side;
    mat[1][1] = near_clip / side;
    mat[2][0] = 0.0;
    mat[2][1] = 0.0;
    mat[2][2] = -(far_clip + near_clip) / z_delta;
    mat[2][3] = -1.0;
    mat[3][2] = (-2.0 * near_clip * far_clip) / z_delta;
    mat[3][3] = 0.0;
  }
  return mat;
}

This link is the inversion for the symmetric matrix.

This inverse is very nasty. Please create a `shadow_projection_perspective_inverse` and populate it manually. Might as well do the TODO and simplify it for the symmetrical case. This is the simplified function: ``` mat4x4 shadow_projection_perspective(float side, float near_clip, float far_clip) { float z_delta = far_clip - near_clip; mat4x4 mat = mat4x4(1.0); if (side != 0.0 && z_delta != 0.0) { mat[0][0] = near_clip / side; mat[1][1] = near_clip / side; mat[2][0] = 0.0; mat[2][1] = 0.0; mat[2][2] = -(far_clip + near_clip) / z_delta; mat[2][3] = -1.0; mat[3][2] = (-2.0 * near_clip * far_clip) / z_delta; mat[3][3] = 0.0; } return mat; } ``` [This link]( https://www.wolframalpha.com/input?i2d=true&i=+invert%7B%7BDivide%5Bc%2Ca%5D%2C0%2C0%2C0%7D%2C%7B0%2CDivide%5Bc%2Cb%5D%2C0%2C0%7D%2C%7B0%2C0%2C-Divide%5B%5C%2840%29c%2Bd%5C%2841%29%2C%5C%2840%29c-d%5C%2841%29%5D%2C-Divide%5B2%5C%2840%29c*d%5C%2841%29%2C%5C%2840%29c-d%5C%2841%29%5D%7D%2C%7B0%2C0%2C0%2C1%7D%7D) is the inversion for the symmetric matrix.
float clip_side = light.clip_side;
return shadow_projection_perspective(clip_side, clip_near, clip_far);
}
mat4 shadow_punctual_projection_perspective_inverse(LightData light)
{
/* Face Local (View) Space > Clip Space. */
float clip_far = intBitsToFloat(light.clip_far);
float clip_near = intBitsToFloat(light.clip_near);
float clip_side = light.clip_side;
return shadow_projection_perspective_inverse(clip_side, clip_near, clip_far);
}
vec3 shadow_punctual_reconstruct_position(ShadowSampleParams params,
mat4 wininv,
LightData light,
vec3 uvw)
{
vec3 clip_P = uvw * 2.0 - 1.0;
vec3 lP = project_point(wininv, clip_P);
int face_id = params.tilemap_index - light.tilemap_index;
lP = shadow_punctual_face_local_to_local_position(face_id, lP);
return mat3(light.object_mat) * lP + light._position;
}
ShadowSampleParams shadow_punctual_sample_params_get(usampler2D tilemaps_tx,
LightData light,
vec3 P)
{
vec3 lP = (P - light._position) * mat3(light.object_mat);
int face_id = shadow_punctual_face_index_get(lP);
/* Local Light Space > Face Local (View) Space. */
lP = shadow_punctual_local_position_to_face_local(face_id, lP);
/* Face Local (View) Space > Clip Space. */
float clip_far = intBitsToFloat(light.clip_far);
float clip_near = intBitsToFloat(light.clip_near);
float clip_side = light.clip_side;
/* TODO: Could be simplified since frustum is completely symmetrical. */
mat4 winmat = shadow_projection_perspective(
-clip_side, clip_side, -clip_side, clip_side, clip_near, clip_far);
mat4 winmat = shadow_punctual_projection_perspective(light);
vec3 clip_P = project_point(winmat, lP);
/* Clip Space > UV Space. */
vec3 uv_P = saturate(clip_P * 0.5 + 0.5);
float depth = shadow_read_depth_at_tilemap_uv(
atlas_tx, tilemaps_tx, light.tilemap_index + face_id, uv_P.xy);
ShadowSampleParams result;
result.lP = lP;
result.uv = uv_P;
result.tilemap_index = light.tilemap_index + face_id;
result.z_range = 1.0;
return result;
}
ShadowEvalResult shadow_punctual_sample_get(SHADOW_ATLAS_TYPE atlas_tx,
usampler2D tilemaps_tx,
LightData light,
vec3 P)
{
ShadowSampleParams params = shadow_punctual_sample_params_get(tilemaps_tx, light, P);
float depth = shadow_read_depth(atlas_tx, tilemaps_tx, params);
ShadowEvalResult result;
result.light_visibilty = float(uv_P.z < depth);
result.occluder_distance = shadow_linear_occluder_distance(light, false, lP, depth);
result.light_visibilty = float(params.uv.z < depth);
result.occluder_distance = shadow_linear_occluder_distance(light, false, params.lP, depth);
return result;
}
struct ShadowDirectionalSampleInfo {
float clip_near;
float clip_far;
int level_relative;
int lod_relative;
ivec2 clipmap_offset;
vec2 clipmap_origin;
};
ShadowDirectionalSampleInfo shadow_directional_sample_info_get(LightData light, vec3 lP)
{
ShadowDirectionalSampleInfo info;
info.clip_near = orderedIntBitsToFloat(light.clip_near);
info.clip_far = orderedIntBitsToFloat(light.clip_far);
int level = shadow_directional_level(light, lP - light._position);
/* This difference needs to be less than 32 for the later shift to be valid.
* This is ensured by ShadowDirectional::clipmap_level_range(). */
info.level_relative = level - light.clipmap_lod_min;
info.lod_relative = (light.type == LIGHT_SUN_ORTHO) ? light.clipmap_lod_min : level;
info.clipmap_offset = shadow_decompress_grid_offset(
light.type, light.clipmap_base_offset, info.level_relative);
info.clipmap_origin = vec2(light._clipmap_origin_x, light._clipmap_origin_y);
return info;
}
vec3 shadow_directional_reconstruct_position(ShadowSampleParams params, LightData light, vec3 uvw)
{
ShadowDirectionalSampleInfo info = shadow_directional_sample_info_get(light, params.lP);
vec2 tilemap_uv = uvw.xy;
tilemap_uv += vec2(info.clipmap_offset) / float(SHADOW_TILEMAP_RES);
vec2 clipmap_pos = (tilemap_uv - 0.5) / exp2(-float(info.lod_relative));
vec3 lP;
lP.xy = clipmap_pos + info.clipmap_origin;
lP.z = (params.uv.z + info.clip_near) * -1.0;
return mat3(light.object_mat) * lP;
}
ShadowSampleParams shadow_directional_sample_params_get(usampler2D tilemaps_tx,
LightData light,
vec3 P)
{
vec3 lP = P * mat3(light.object_mat);
ShadowDirectionalSampleInfo info = shadow_directional_sample_info_get(light, lP);
ShadowCoordinates coord = shadow_directional_coordinates(light, lP);
/* Assumed to be non-null. */
float z_range = info.clip_far - info.clip_near;
float dist_to_near_plane = -lP.z - info.clip_near;
vec2 clipmap_pos = lP.xy - info.clipmap_origin;
vec2 tilemap_uv = clipmap_pos * exp2(-float(info.lod_relative)) + 0.5;
/* Translate tilemap UVs to its origin. */
tilemap_uv -= vec2(info.clipmap_offset) / float(SHADOW_TILEMAP_RES);
/* Clamp to avoid out of tilemap access. */
tilemap_uv = saturate(tilemap_uv);
ShadowSampleParams result;
result.lP = lP;
result.uv = vec3(tilemap_uv, dist_to_near_plane);
result.tilemap_index = light.tilemap_index + info.level_relative;
result.z_range = z_range;
return result;
}
@ -127,40 +266,13 @@ ShadowEvalResult shadow_directional_sample_get(SHADOW_ATLAS_TYPE atlas_tx,
LightData light,
vec3 P)
{
vec3 lP = P * mat3(light.object_mat);
ShadowCoordinates coord = shadow_directional_coordinates(light, lP);
ShadowSampleParams params = shadow_directional_sample_params_get(tilemaps_tx, light, P);
float clip_near = orderedIntBitsToFloat(light.clip_near);
float clip_far = orderedIntBitsToFloat(light.clip_far);
/* Assumed to be non-null. */
float z_range = clip_far - clip_near;
float dist_to_near_plane = -lP.z - clip_near;
int level = shadow_directional_level(light, lP - light._position);
/* This difference needs to be less than 32 for the later shift to be valid.
* This is ensured by ShadowDirectional::clipmap_level_range(). */
int level_relative = level - light.clipmap_lod_min;
int lod_relative = (light.type == LIGHT_SUN_ORTHO) ? light.clipmap_lod_min : level;
vec2 clipmap_origin = vec2(light._clipmap_origin_x, light._clipmap_origin_y);
vec2 clipmap_pos = lP.xy - clipmap_origin;
vec2 tilemap_uv = clipmap_pos * exp2(-float(lod_relative)) + 0.5;
/* Compute offset in tile. */
ivec2 clipmap_offset = shadow_decompress_grid_offset(
light.type, light.clipmap_base_offset, level_relative);
/* Translate tilemap UVs to its origin. */
tilemap_uv -= vec2(clipmap_offset) / float(SHADOW_TILEMAP_RES);
/* Clamp to avoid out of tilemap access. */
tilemap_uv = saturate(tilemap_uv);
float depth = shadow_read_depth_at_tilemap_uv(
atlas_tx, tilemaps_tx, light.tilemap_index + level_relative, tilemap_uv);
float depth = shadow_read_depth(atlas_tx, tilemaps_tx, params);
ShadowEvalResult result;
result.light_visibilty = float(dist_to_near_plane < depth * z_range);
result.occluder_distance = shadow_linear_occluder_distance(light, true, lP, depth);
result.light_visibilty = float(params.uv.z < depth * params.z_range);
result.occluder_distance = shadow_linear_occluder_distance(light, true, params.lP, depth);
return result;
}

View File

@ -205,6 +205,24 @@ vec3 shadow_punctual_local_position_to_face_local(int face_id, vec3 lL)
}
}
vec3 shadow_punctual_face_local_to_local_position(int face_id, vec3 fL)
{
switch (face_id) {
case 1:
return vec3(-fL.z, -fL.x, fL.y);
case 2:
return vec3(fL.z, fL.x, fL.y);
case 3:
return vec3(fL.x, -fL.z, fL.y);
case 4:
return vec3(-fL.x, fL.z, fL.y);
case 5:
return vec3(fL.x, -fL.y, -fL.z);
default:
return fL;
}
}
/* Turns local light coordinate into shadow region index. Matches eCubeFace order.
* \note lL does not need to be normalized. */
int shadow_punctual_face_index_get(vec3 lL)

View File

@ -12,6 +12,8 @@
#pragma BLENDER_REQUIRE(eevee_shadow_lib.glsl)
#pragma BLENDER_REQUIRE(eevee_sampling_lib.glsl)
#pragma BLENDER_REQUIRE(eevee_bxdf_sampling_lib.glsl)
#pragma BLENDER_REQUIRE(draw_view_lib.glsl)
#pragma BLENDER_REQUIRE(draw_math_geom_lib.glsl)
float shadow_read_depth_at_tilemap_uv(int tilemap_index, vec2 tilemap_uv)
{
@ -406,6 +408,67 @@ SHADOW_MAP_TRACE_FN(ShadowRayPunctual)
/** \name Shadow Evaluation
* \{ */
/* Compute the world space offset of the shading position required for
pragma37 marked this conversation as resolved Outdated

Add overall description of what this function does.

Add overall description of what this function does.
* stochastic percentage closer filtering of shadow-maps. */
vec3 shadow_pcf_offset(LightData light, const bool is_directional, vec3 P, vec3 Ng)
{
if (light.pcf_radius <= 0.001) {
/* Early return. */
return vec3(0.0);
}
ShadowSampleParams params;
if (is_directional) {
params = shadow_directional_sample_params_get(shadow_tilemaps_tx, light, P);
}
else {
params = shadow_punctual_sample_params_get(shadow_tilemaps_tx, light, P);
}
ShadowTileData tile = shadow_tile_data_get(shadow_tilemaps_tx, params);
if (!tile.is_allocated) {
return vec3(0.0);
}
pragma37 marked this conversation as resolved Outdated

shadow-map

shadow-map
/* Compute the shadow-map tangent-bitangent matrix. */
float uv_offset = 1.0 / float(SHADOW_MAP_MAX_RES);
vec3 TP, BP;
if (is_directional) {
TP = shadow_directional_reconstruct_position(
params, light, params.uv + vec3(uv_offset, 0.0, 0.0));
BP = shadow_directional_reconstruct_position(
params, light, params.uv + vec3(0.0, uv_offset, 0.0));
vec3 L = light._back;
/* Project the offset positions into the surface plane. */
TP = line_plane_intersect(TP, dot(L, TP) > 0.0 ? L : -L, P, Ng);
BP = line_plane_intersect(BP, dot(L, BP) > 0.0 ? L : -L, P, Ng);
}
else {
mat4 wininv = shadow_punctual_projection_perspective_inverse(light);
TP = shadow_punctual_reconstruct_position(
params, wininv, light, params.uv + vec3(uv_offset, 0.0, 0.0));
BP = shadow_punctual_reconstruct_position(
params, wininv, light, params.uv + vec3(0.0, uv_offset, 0.0));
/* Project the offset positions into the surface plane. */
TP = line_plane_intersect(light._position, normalize(TP - light._position), P, Ng);
BP = line_plane_intersect(light._position, normalize(BP - light._position), P, Ng);
pragma37 marked this conversation as resolved Outdated

You could do it with a mat2x3.

You could do it with a `mat2x3`.
}
mat2x3 TB = mat2x3(TP - P, BP - P);
/* Compute the actual offset. */
vec2 rand = vec2(0.0);
#ifdef EEVEE_SAMPLING_DATA
rand = sampling_rng_2D_get(SAMPLING_SHADOW_V);
pragma37 marked this conversation as resolved

Nitpick: pcf_offset * 2.0 - 1.0;

Nitpick: `pcf_offset * 2.0 - 1.0;`
#endif
vec2 pcf_offset = interlieved_gradient_noise(UTIL_TEXEL, vec2(0.0), rand);
pcf_offset = pcf_offset * 2.0 - 1.0;
pcf_offset *= light.pcf_radius;
return TB * pcf_offset;
}
/**
* Evaluate shadowing by casting rays toward the light direction.
*/
@ -433,6 +496,8 @@ ShadowEvalResult shadow_eval(LightData light,
float normal_offset = 0.02;
#endif
P += shadow_pcf_offset(light, is_directional, P, Ng);
/* Avoid self intersection. */
P = offset_ray(P, Ng);
/* The above offset isn't enough in most situation. Still add a bigger bias. */

View File

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

View File

@ -77,7 +77,7 @@ typedef struct Light {
float spec_fac, att_dist;
float shadow_softness_factor;
float shadow_trace_distance;
float _pad3;
float shadow_filter_radius;
/* Preview */
struct PreviewImage *preview;

View File

@ -1917,8 +1917,8 @@ typedef struct SceneEEVEE {
int shadow_ray_count;
int shadow_step_count;
float shadow_normal_bias;
float _pad0;
pragma37 marked this conversation as resolved Outdated

Rename to shadow_filter_radius. No need to mention PCF anywhere but on EEVEE's code (implementation detail).

Rename to `shadow_filter_radius`. No need to mention PCF anywhere but on EEVEE's code (implementation detail).
char _pad[4];
int ray_tracing_method;
struct RaytraceEEVEE ray_tracing_options;

View File

@ -296,6 +296,14 @@ static void rna_def_light_shadow(StructRNA *srna, bool sun)
prop, "Shadow Softness Factor", "Scale light shape for smaller penumbra");
RNA_def_property_update(prop, 0, "rna_Light_update");
prop = RNA_def_property(srna, "shadow_filter_radius", PROP_FLOAT, PROP_FACTOR);
RNA_def_property_range(prop, 0.0f, FLT_MAX);
RNA_def_property_ui_range(prop, 0.0f, 5.0f, 1.0f, 2);
RNA_def_property_ui_text(
prop, "Shadow Filter Radius", "Blur shadow aliasing using Percentage Closer Filtering");
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");