Files
UnrealEngineUWP/Engine/Shaders/Private/RectLightIntegrate.ush
chris kulla 232a0ef37f Path Tracer: Fix artifacts from solid angle sampling of small rectangles
Improve the math for solid angle rectangles by simplifying cross products (fewer terms are needed), using asin() instead of acos() and replacing the calls to the builtin asin() with a faster custom approximation that still has good accuracy near the end-points (unlike asinFast).

To further improve results, fallback to area sampling when the solid angle of the rectangle drops below a small threshold.

This also improves the capsule sampling case, which samples from a bounding rectangle that can be thin when the radius is small.

#rb Aleksander.Netzel

[CL 32038181 by chris kulla in ue5-main branch]
2024-03-05 17:25:53 -05:00

393 lines
10 KiB
Plaintext

// Copyright Epic Games, Inc. All Rights Reserved.
#define CHEAP_RECT 0
#include "DeferredShadingCommon.ush"
#include "MonteCarlo.ush"
#include "AreaLightCommon.ush"
#include "ShadingModels.ush"
#include "RectLight.ush"
float3 ClampToRect( float3 L, FRect Rect )
{
// Bias toward plane
//L -= Rect.Axis[2] * saturate( 0.001 + dot( Rect.Axis[2], L ) );
//L = normalize( L );
// Intersect ray with plane
float3 PointOnPlane = L * ( dot( Rect.Axis[2], Rect.Origin ) / dot( Rect.Axis[2], L ) );
//float3 PointOnPlane = L * ( dot( Rect.Axis[2], Rect.Origin ) / -saturate( 0.001 - dot( Rect.Axis[2], L ) ) );
float2 PointInRect;
PointInRect.x = dot( Rect.Axis[0], PointOnPlane - Rect.Origin );
PointInRect.y = dot( Rect.Axis[1], PointOnPlane - Rect.Origin );
// Clamp point to rect
PointInRect = clamp( PointInRect, -Rect.Extent, Rect.Extent );
float3 ToRect = Rect.Origin;
ToRect += PointInRect.x * Rect.Axis[0];
ToRect += PointInRect.y * Rect.Axis[1];
return normalize( ToRect );
}
bool RayHitRect( float3 L, FRect Rect )
{
// Intersect ray with plane
float t = dot( Rect.Axis[2], Rect.Origin ) / dot( Rect.Axis[2], L );
float3 PointOnPlane = L * t;
bool InExtentX = abs( dot( Rect.Axis[0], PointOnPlane - Rect.Origin ) ) <= Rect.Extent.x;
bool InExtentY = abs( dot( Rect.Axis[1], PointOnPlane - Rect.Origin ) ) <= Rect.Extent.y;
return t >= 0 && InExtentX && InExtentY;
}
float IntegrateLight( FRect Rect )
{
// No visibile rect light due to barn door occlusion
if (Rect.Extent.x == 0 || Rect.Extent.y == 0) return 0;
float NoL;
float Falloff;
#if !CHEAP_RECT // Optimized Lambert
float3 L = RectIrradianceLambert( 0, Rect, Falloff, NoL );
#elif 1 // Karis
float3 L = RectIrradianceApproxKaris( 0, Rect, Falloff, NoL );
#elif 1 // Lagarde
float3 L = RectIrradianceApproxLagarde( 0, Rect, Falloff, NoL );
#else // Drobot
float3 L = RectIrradianceApproxDrobot( 0, Rect, Falloff, NoL );
#endif
return Falloff;
}
FAreaLightIntegrateContext CreateRectIntegrateContext( float Roughness, half3 N, half3 V, FRect Rect, FRectTexture SourceTexture )
{
float NoL = 0;
float Falloff = 0;
FAreaLightIntegrateContext Out = InitAreaLightIntegrateContext();
#if !CHEAP_RECT // Optimized Lambert
float3 L = RectIrradianceLambert( N, Rect, Falloff, NoL );
#elif 1 // Karis
float3 L = RectIrradianceApproxKaris( N, Rect, Falloff, NoL );
#elif 1 // Lagarde
float3 L = RectIrradianceApproxLagarde( N, Rect, Falloff, NoL );
#else // Drobot
float3 L = RectIrradianceApproxDrobot( N, Rect, Falloff, NoL );
#endif
#if CHEAP_RECT
float3 R = reflect( -V, N );
#if 0
float NoV = saturate( dot( N, V ) );
L = lerp( L, R, saturate( -2*dot( Rect.Axis[2], R ) ) );
L = normalize( L );
UNROLL
for( int k = 0; k < 2; k++ )
{
#if 1
float NoL = dot( N, L );
float NoV = dot( N, V );
float VoL = dot( V, L );
float NoHInvLenH = ( NoL + NoV ) / ( 2 + 2*VoL );
float3 Gradient = ( ( N - L*NoL ) - ( V - L*VoL ) * NoHInvLenH ) * (2*NoHInvLenH);
#else
float RoL = dot( R, L );
float3 Gradient = 2 * RoL * ( R - L * RoL );
#endif
Gradient -= Rect.Axis[2] * dot( Rect.Axis[2], Gradient );
Gradient = lerp( Gradient, 0, (1 - NoV) * saturate( 2*dot( Rect.Axis[2], L ) ) );
L = ClampToRect( L + Gradient * ( 2.0 / ( 2.0 + k ) ), Rect );
}
#elif 1
float3 Ls = L;
float3 v[4];
v[0] = Rect.Origin - Rect.Axis[0] * Rect.Extent.x - Rect.Axis[1] * Rect.Extent.y;
v[1] = Rect.Origin + Rect.Axis[0] * Rect.Extent.x - Rect.Axis[1] * Rect.Extent.y;
v[2] = Rect.Origin + Rect.Axis[0] * Rect.Extent.x + Rect.Axis[1] * Rect.Extent.y;
v[3] = Rect.Origin - Rect.Axis[0] * Rect.Extent.x + Rect.Axis[1] * Rect.Extent.y;
float3 e0 = v[0];
float3 e1 = v[1];
float3 MinEdgeN = 0;
float MinEdgeCos = 1;
UNROLL
for( uint i = 0; i < 4; i++ )
{
float3 v0 = v[i];
float3 v1 = v[ (i+1) % 4 ];
float3 EdgeN = normalize( cross( v0, v1 ) );
float EdgeCos = dot( R, EdgeN );
if( EdgeCos < MinEdgeCos )
{
MinEdgeN = EdgeN;
MinEdgeCos = EdgeCos;
e0 = v0;
e1 = v1;
}
}
if( MinEdgeCos > 0 )
{
Ls = R;
}
else
{
#if 0
Ls = SmallestAnglePointOnLineToRay( e0, e1, length( e0 - e1 ), R );
#else
float3 Rp = R - MinEdgeCos * MinEdgeN;
if( dot( cross( Rp, e0 ), R ) < 0 ) Ls = e0;
else if(dot( cross( e1, Rp ), R ) < 0 ) Ls = e1;
else Ls = Rp;
#endif
Ls = normalize( Ls );
}
float a = Pow2( GBuffer.Roughness );
//L = lerp( Ls, L, a );
L = normalize( Ls );
#else
L = R;
if( !RayHitRect( R, Rect ) )
{
float3 MaxL = R;
float MaxNoH = -1;
uint NumSteps = 128;
for( uint i = 0; i < NumSteps; i++ )
{
float Theta = (2*PI) * i / (float)NumSteps;
float2 p;
p.x = cos( Theta );
p.y = sin( Theta );
p.xy /= dot( 1, abs(p) );
float2 PointInRect = float2( p.x + p.y, p.x - p.y ) * Rect.Extent;
//0.5 * sqrt( 2 + (2*sqrt(2.0)) *
float3 ToRect = Rect.Origin;
ToRect += PointInRect.x * Rect.Axis[0];
ToRect += PointInRect.y * Rect.Axis[1];
L = normalize( ToRect );
float RoL = dot( R, L );
BxDFContext Context;
Context.Init( N, V, L );
if( Context.NoH > MaxNoH )
{
MaxNoH = Context.NoH;
MaxL = L;
}
}
L = MaxL;
}
for( int k = 0; k < 0; k++ )
{
float NoL = dot( N, L );
float NoV = dot( N, V );
float VoL = dot( V, L );
float NoHInvLenH = ( NoL + NoV ) / ( 2 + 2*VoL );
float3 Gradient = ( ( N - L*NoL ) - ( V - L*VoL ) * NoHInvLenH ) * (2*NoHInvLenH);
L = ClampToRect( L + Gradient * ( 2.0 / ( 2.0 + k ) ), Rect );
}
#endif
Out.AreaLight.SphereSinAlpha = sqrt( Falloff * (1.0 / PI) );
Out.AreaLight.SphereSinAlphaSoft = 0;
Out.AreaLight.LineCosSubtended = 1;
Out.AreaLight.FalloffColor = 1;
Out.AreaLight.Rect = Rect;
Out.AreaLight.Texture = SourceTexture;
Out.AreaLight.IsRectAndDiffuseMicroReflWeight = 0;
SetIsRectLight(Out.AreaLight, false);
SetAreaLightDiffuseMicroReflWeight(Out.AreaLight, 0.0);
Out.L = L;
Out.NoL = NoL;
Out.Falloff = Falloff;
#else
float3 FalloffColor = SampleSourceTexture( L, Rect, SourceTexture );
Out.AreaLight.SphereSinAlpha = 0;
Out.AreaLight.SphereSinAlphaSoft = 0;
Out.AreaLight.LineCosSubtended = 1;
Out.AreaLight.FalloffColor = FalloffColor;
Out.AreaLight.Rect = Rect;
Out.AreaLight.Texture = SourceTexture;
Out.AreaLight.IsRectAndDiffuseMicroReflWeight = 0;
SetIsRectLight(Out.AreaLight, true);
SetAreaLightDiffuseMicroReflWeight(Out.AreaLight, 0.0);
Out.L = L;
Out.NoL = NoL;
Out.Falloff = Falloff;
#endif
return Out;
}
FDirectLighting IntegrateBxDF(FGBufferData GBuffer, half3 N, half3 V, FRect Rect, FShadowTerms Shadow, FRectTexture SourceTexture)
{
// No-visible rect light due to barn door occlusion
FDirectLighting Out = (FDirectLighting)0;
if (IsRectVisible(Rect))
{
FAreaLightIntegrateContext Context = CreateRectIntegrateContext(GBuffer.Roughness, N, V, Rect, SourceTexture);
GBuffer.Roughness = max(GBuffer.Roughness, 0.02);
Out = IntegrateBxDF(GBuffer, N, V, Context.L, Context.Falloff, Context.NoL, Context.AreaLight, Shadow);
}
return Out;
}
FDirectLighting IntegrateBxDF( FGBufferData GBuffer, half3 N, half3 V, FRect Rect, FShadowTerms Shadow, FRectTexture SourceTexture, uint2 SVPos )
{
FDirectLighting Lighting = (FDirectLighting)0;
const float SurfaceArea = 4 * Rect.Extent.x * Rect.Extent.y;
const float SurfaceColor = 2.0 / SurfaceArea;
// Rect normal points away from point
if( dot( Rect.Axis[2], Rect.Origin ) < 0 )
return Lighting;
// No-visible rect light due to barn door occlusion
if (!IsRectVisible(Rect))
return Lighting;
FSphericalRect SphericalRect = BuildSphericalRect( Rect );
const uint NumSets = 4;
const uint NumSamples[ NumSets ] =
{
0, // Cosine hemisphere
16, // GGX
0, // Light area
16, // Spherical rect
};
uint2 SobolBase = SobolPixel( SVPos );
uint2 SobolFrame = SobolIndex( SobolBase, View.StateFrameIndexMod8, 3 );
UNROLL
for( uint Set = 0; Set < NumSets; Set++ )
{
LOOP
for( uint i = 0; i < NumSamples[ Set ]; i++ )
{
uint2 Random = Rand3DPCG16( uint3( SVPos.xy, View.Random ^ Set ) ).xy;
float2 E = float2( SobolIndex( SobolFrame, i << 3 ) ) / 0x10000;
//float2 E = Hammersley( i, NumSamples[ Set ], Random );
//float2 E = CorrelatedMultiJitter2D( i, NumSamples[ Set ], Random.x );
float3 L, H;
switch( Set )
{
case 0:
{
L = TangentToWorld( CosineSampleHemisphere( E ).xyz, N );
H = normalize( V + L );
break;
}
case 1:
{
H = TangentToWorld( ImportanceSampleGGX( E, Pow4(GBuffer.Roughness) ).xyz, N );
L = 2 * dot( V, H ) * H - V;
break;
}
case 2:
{
float3 ToArea = Rect.Origin;
ToArea += (E.x * 2 - 1) * Rect.Axis[0] * Rect.Extent.x;
ToArea += (E.y * 2 - 1) * Rect.Axis[1] * Rect.Extent.y;
L = normalize( ToArea );
H = normalize( V + L );
break;
}
case 3:
{
L = UniformSampleSphericalRect( E, SphericalRect ).Direction;
H = normalize( V + L );
break;
}
}
float NoL = saturate( dot(N, L) );
float NoH = saturate( dot(N, H) );
float VoH = saturate( dot(V, H) );
if( NoL > 0 && VoH > 0 )
{
// Intersect ray with plane
float t = dot( Rect.Axis[2], Rect.Origin ) / dot( Rect.Axis[2], L );
float3 PointOnPlane = L * t;
float2 PointInRect;
PointInRect.x = dot( Rect.Axis[0], PointOnPlane - Rect.Origin );
PointInRect.y = dot( Rect.Axis[1], PointOnPlane - Rect.Origin );
float2 RectUV = PointInRect / Rect.Extent * float2( 0.5, -0.5 ) + 0.5;
float3 LightColor = SampleRectTexture(SourceTexture, RectUV, 0, true);
if( Set == 0 || Set == 1 )
{
bool InExtentX = abs( PointInRect.x ) <= Rect.Extent.x;
bool InExtentY = abs( PointInRect.y ) <= Rect.Extent.y;
BRANCH
if( t < 0 || !InExtentX || !InExtentY )
{
// Missed rect
continue;
}
}
float PDF[] =
{
NoL * (1 / PI),
D_GGX( Pow4(GBuffer.Roughness), NoH ) * NoH / (4 * VoH),
dot( PointOnPlane, PointOnPlane ) / ( SurfaceArea * abs( dot( L, Rect.Axis[2] ) ) ),
1.0 / SphericalRect.SolidAngle,
};
// MIS power heuristic
float InvWeight = 0;
UNROLL for( uint j = 0; j < NumSets; j++ )
{
InvWeight += Square( PDF[j] * NumSamples[j] );
}
float Weight = rcp( InvWeight ) * PDF[Set] * NumSamples[Set];
FDirectLighting LightingSample = EvaluateBxDF( GBuffer, N, V, L, NoL, Shadow );
Lighting.Diffuse += ( LightColor * Weight ) * LightingSample.Diffuse;
Lighting.Specular += ( LightColor * Weight ) * LightingSample.Specular;
Lighting.Transmission += ( LightColor * Weight ) * LightingSample.Transmission;
}
}
}
return Lighting;
}