Fix specular cutoff on lights with radius overlapping with mesh (#19157)

# Objective

- Fixes #13318

## Solution

- Clamp a dot product to be positive to avoid choosing a `centerToRay`
which is not on the ray but behind it.

## Testing

- Repro in #13318

Main:
<img width="963" alt="{DA2A2B99-27C7-4A76-83B6-CCB70FB57CAD}"
src="https://github.com/user-attachments/assets/afae8001-48ee-4762-9522-e247bbe3577a"
/>

This PR:
<img width="963" alt="{2C4BC3E7-C6A6-4736-A916-0366FBB618DA}"
src="https://github.com/user-attachments/assets/5bea4162-0b58-4df0-bf22-09fcb27dc167"
/>

Eevee reference:

![329697008-ff28a5f3-27f3-4e98-9cee-d836a6c76aee](https://github.com/user-attachments/assets/a1b566ab-16ee-40d3-a0b6-ad179ca0fe3a)
This commit is contained in:
atlv 2025-05-12 15:14:13 -04:00 committed by François Mockers
parent efde13a827
commit 82f193284a

View File

@ -278,7 +278,23 @@ fn compute_specular_layer_values_for_point_light(
// Representative Point Area Lights. // Representative Point Area Lights.
// see http://blog.selfshadow.com/publications/s2013-shading-course/karis/s2013_pbs_epic_notes_v2.pdf p14-16 // see http://blog.selfshadow.com/publications/s2013-shading-course/karis/s2013_pbs_epic_notes_v2.pdf p14-16
let centerToRay = dot(light_to_frag, R) * R - light_to_frag; var LtFdotR = dot(light_to_frag, R);
// HACK: the following line is an amendment to fix a discontinuity when a surface
// intersects the light sphere. See https://github.com/bevyengine/bevy/issues/13318
//
// This sentence in the reference is crux of the problem: "We approximate finding the point with the
// smallest angle to the reflection ray by finding the point with the smallest distance to the ray."
// This approximation turns out to be completely wrong for points inside or near the sphere.
// Clamping this dot product to be positive ensures `centerToRay` lies on ray and not behind it.
// Any non-zero epsilon works here, it just has to be positive to avoid a singularity at zero.
// However, this is still far from physically accurate. Deriving an exact solution would help,
// but really we should adopt a superior solution to area lighting, such as:
// Physically Based Area Lights by Michal Drobot, or
// Polygonal-Light Shading with Linearly Transformed Cosines by Eric Heitz et al.
LtFdotR = max(0.0001, LtFdotR);
let centerToRay = LtFdotR * R - light_to_frag;
let closestPoint = light_to_frag + centerToRay * saturate( let closestPoint = light_to_frag + centerToRay * saturate(
light_position_radius * inverseSqrt(dot(centerToRay, centerToRay))); light_position_radius * inverseSqrt(dot(centerToRay, centerToRay)));
let LspecLengthInverse = inverseSqrt(dot(closestPoint, closestPoint)); let LspecLengthInverse = inverseSqrt(dot(closestPoint, closestPoint));