837 lines
28 KiB
HLSL
837 lines
28 KiB
HLSL
// Copyright Epic Games, Inc. All Rights Reserved.
|
|
//
|
|
|
|
#pragma once
|
|
|
|
#include "CapsuleLight.ush"
|
|
#include "MonteCarlo.ush"
|
|
#include "LightData.ush"
|
|
|
|
#ifndef USE_SOURCE_TEXTURE
|
|
#define USE_SOURCE_TEXTURE (FEATURE_LEVEL > FEATURE_LEVEL_ES3_1)
|
|
#endif
|
|
|
|
#if SUPPORTS_INDEPENDENT_SAMPLERS
|
|
#define GGXLTCMatSampler View.SharedBilinearClampedSampler
|
|
#define GGXLTCAmpSampler View.SharedBilinearClampedSampler
|
|
#else
|
|
#define GGXLTCMatSampler View.GGXLTCMatSampler
|
|
#define GGXLTCAmpSampler View.GGXLTCAmpSampler
|
|
#endif
|
|
|
|
struct FRect
|
|
{
|
|
float3 Origin;
|
|
float3x3 Axis;
|
|
float2 Extent;
|
|
float2 FullExtent;
|
|
float2 Offset;
|
|
};
|
|
|
|
float3 SampleRectTexture(FRectTexture RectTexture, float2 RectUV, float Level, bool bIsReference = false)
|
|
{
|
|
#if USE_SOURCE_TEXTURE
|
|
const bool bIsValid = RectTexture.AtlasMaxLevel < MAX_RECT_ATLAS_MIP;
|
|
const float2 RectTextureSize = RectTexture.AtlasUVScale * View.RectLightAtlasSizeAndInvSize.xy;
|
|
Level += log2(min(RectTextureSize.x, RectTextureSize.y)) - 2.f;
|
|
Level = min(Level, RectTexture.AtlasMaxLevel);
|
|
|
|
RectUV = saturate(RectUV) * RectTexture.AtlasUVScale + RectTexture.AtlasUVOffset;
|
|
|
|
// Compute rect border to prevent leaking during tri-linear filtering.
|
|
// Border are aligned on the coarsest MIP value
|
|
const uint2 MippedResoluton = uint2(View.RectLightAtlasSizeAndInvSize.xy) >> uint(ceil(Level));
|
|
const float2 UVBorder = 0.5f / float2(MippedResoluton);
|
|
const float2 MinRectUV = UVBorder + RectTexture.AtlasUVOffset;
|
|
const float2 MaxRectUV = -UVBorder + RectTexture.AtlasUVOffset + RectTexture.AtlasUVScale;
|
|
RectUV = clamp(RectUV, MinRectUV, MaxRectUV);
|
|
|
|
return bIsValid ? View.RectLightAtlasTexture.SampleLevel(View.SharedTrilinearClampedSampler, RectUV, bIsReference ? 0 : Level).rgb : 1.f;
|
|
#else
|
|
return 1;
|
|
#endif
|
|
}
|
|
|
|
// Optimized Lambert
|
|
float3 RectIrradianceLambert( float3 N, FRect Rect, out float BaseIrradiance, out float NoL )
|
|
{
|
|
#if 0
|
|
float3 L0 = normalize( Rect.Origin - Rect.Axis[0] * Rect.Extent.x - Rect.Axis[1] * Rect.Extent.y ); // 8 mad, 4 mul, 1 rsqrt
|
|
float3 L1 = normalize( Rect.Origin + Rect.Axis[0] * Rect.Extent.x - Rect.Axis[1] * Rect.Extent.y ); // 8 mad, 4 mul, 1 rsqrt
|
|
float3 L2 = normalize( Rect.Origin + Rect.Axis[0] * Rect.Extent.x + Rect.Axis[1] * Rect.Extent.y ); // 8 mad, 4 mul, 1 rsqrt
|
|
float3 L3 = normalize( Rect.Origin - Rect.Axis[0] * Rect.Extent.x + Rect.Axis[1] * Rect.Extent.y ); // 8 mad, 4 mul, 1 rsqrt
|
|
// 48 alu, 4 rsqrt
|
|
#else
|
|
float3 LocalPosition;
|
|
LocalPosition.x = dot( Rect.Axis[0], Rect.Origin ); // 1 mul, 2 mad
|
|
LocalPosition.y = dot( Rect.Axis[1], Rect.Origin ); // 1 mul, 2 mad
|
|
LocalPosition.z = dot( Rect.Axis[2], Rect.Origin ); // 1 mul, 2 mad
|
|
// 9 alu
|
|
|
|
float x0 = LocalPosition.x - Rect.Extent.x;
|
|
float x1 = LocalPosition.x + Rect.Extent.x;
|
|
float y0 = LocalPosition.y - Rect.Extent.y;
|
|
float y1 = LocalPosition.y + Rect.Extent.y;
|
|
float z0 = LocalPosition.z;
|
|
float z0Sqr = z0 * z0;
|
|
// 5 alu
|
|
|
|
float3 v0 = float3( x0, y0, z0 );
|
|
float3 v1 = float3( x1, y0, z0 );
|
|
float3 v2 = float3( x1, y1, z0 );
|
|
float3 v3 = float3( x0, y1, z0 );
|
|
|
|
float3 L0 = v0 * rsqrt( dot( v0.xy, v0.xy ) + z0Sqr ); // 2 mad, 3 mul, 1 rsqrt
|
|
float3 L1 = v1 * rsqrt( dot( v1.xy, v1.xy ) + z0Sqr ); // 2 mad, 3 mul, 1 rsqrt
|
|
float3 L2 = v2 * rsqrt( dot( v2.xy, v2.xy ) + z0Sqr ); // 2 mad, 3 mul, 1 rsqrt
|
|
float3 L3 = v3 * rsqrt( dot( v3.xy, v3.xy ) + z0Sqr ); // 2 mad, 3 mul, 1 rsqrt
|
|
// 20 alu, 4 rsqrt
|
|
|
|
// total 34 alu, 4 rsqrt
|
|
#endif
|
|
|
|
#if 0
|
|
float3 L;
|
|
L = acos( dot( L0, L1 ) ) * normalize( cross( L0, L1 ) );
|
|
L += acos( dot( L1, L2 ) ) * normalize( cross( L1, L2 ) );
|
|
L += acos( dot( L2, L3 ) ) * normalize( cross( L2, L3 ) );
|
|
L += acos( dot( L3, L0 ) ) * normalize( cross( L3, L0 ) );
|
|
#else
|
|
float c01 = dot( L0, L1 );
|
|
float c12 = dot( L1, L2 );
|
|
float c23 = dot( L2, L3 );
|
|
float c30 = dot( L3, L0 );
|
|
// 9 alu
|
|
|
|
#if 0
|
|
float w01 = 1.5708 + (-0.879406 + 0.308609 * abs(c01) ) * abs(c01);
|
|
float w12 = 1.5708 + (-0.879406 + 0.308609 * abs(c12) ) * abs(c12);
|
|
float w23 = 1.5708 + (-0.879406 + 0.308609 * abs(c23) ) * abs(c23);
|
|
float w30 = 1.5708 + (-0.879406 + 0.308609 * abs(c30) ) * abs(c30);
|
|
|
|
w01 = c01 > 0 ? w01 : PI * rsqrt( 1 - c01 * c01 ) - w01;
|
|
w12 = c12 > 0 ? w12 : PI * rsqrt( 1 - c12 * c12 ) - w12;
|
|
w23 = c23 > 0 ? w23 : PI * rsqrt( 1 - c23 * c23 ) - w23;
|
|
w30 = c30 > 0 ? w30 : PI * rsqrt( 1 - c30 * c30 ) - w30;
|
|
#else
|
|
// normalize( cross( L0, L1 ) ) = cross( L0, L1 ) * rsqrt( 1 - dot( L0, L1 )^2 )
|
|
// acos( x ) ~= sqrt(1 - x) * (1.5708 - 0.175 * x)
|
|
|
|
float w01 = ( 1.5708 - 0.175 * c01 ) * rsqrt( max(c01 + 1, 1.0e-4f) ); // 1 mad, 1 add, 1 rsqrt
|
|
float w12 = ( 1.5708 - 0.175 * c12 ) * rsqrt( max(c12 + 1, 1.0e-4f) ); // 1 mad, 1 add, 1 rsqrt
|
|
float w23 = ( 1.5708 - 0.175 * c23 ) * rsqrt( max(c23 + 1, 1.0e-4f) ); // 1 mad, 1 add, 1 rsqrt
|
|
float w30 = ( 1.5708 - 0.175 * c30 ) * rsqrt( max(c30 + 1, 1.0e-4f) ); // 1 mad, 1 add, 1 rsqrt
|
|
// 8 alu, 4 rsqrt
|
|
#endif
|
|
|
|
#if 0
|
|
float3 L;
|
|
L = w01 * cross( L0, L1 ); // 6 mul, 3 mad
|
|
L += w12 * cross( L1, L2 ); // 3 mul, 6 mad
|
|
L += w23 * cross( L2, L3 ); // 3 mul, 6 mad
|
|
L += w30 * cross( L3, L0 ); // 3 mul, 6 mad
|
|
#else
|
|
float3 L;
|
|
L = cross( L1, -w01 * L0 + w12 * L2 ); // 6 mul, 6 mad
|
|
L += cross( L3, w30 * L0 + -w23 * L2 ); // 3 mul, 9 mad
|
|
#endif
|
|
#endif
|
|
|
|
// Vector irradiance
|
|
L = L.x * Rect.Axis[0] + L.y * Rect.Axis[1] + L.z * Rect.Axis[2]; // 3 mul, 6 mad
|
|
|
|
float LengthSqr = dot( L, L );
|
|
float InvLength = rsqrt( LengthSqr );
|
|
float Length = LengthSqr * InvLength;
|
|
|
|
// Mean light direction
|
|
L *= InvLength;
|
|
|
|
BaseIrradiance = 0.5 * Length;
|
|
|
|
// Solid angle of sphere = 2*PI * ( 1 - sqrt(1 - r^2 / d^2 ) )
|
|
// Cosine weighted integration = PI * r^2 / d^2
|
|
// SinAlphaSqr = r^2 / d^2;
|
|
float SinAlphaSqr = BaseIrradiance * (1.0 / PI);
|
|
|
|
NoL = SphereHorizonCosWrap( dot( N, L ), SinAlphaSqr );
|
|
|
|
return L;
|
|
}
|
|
|
|
float3 RectIrradianceApproxKaris( float3 N, FRect Rect, out float BaseIrradiance, out float NoL )
|
|
{
|
|
float2 RectLocal;
|
|
RectLocal.x = SmoothClamp( dot( Rect.Axis[0], -Rect.Origin ), -Rect.Extent.x, Rect.Extent.x, 16 );
|
|
RectLocal.y = SmoothClamp( dot( Rect.Axis[1], -Rect.Origin ), -Rect.Extent.y, Rect.Extent.y, 16 );
|
|
|
|
float3 ClosestPoint = Rect.Origin;
|
|
ClosestPoint += Rect.Axis[0] * RectLocal.x;
|
|
ClosestPoint += Rect.Axis[1] * RectLocal.y;
|
|
|
|
float3 OppositePoint = 2 * Rect.Origin - ClosestPoint;
|
|
|
|
float3 L0 = normalize( ClosestPoint );
|
|
float3 L1 = normalize( OppositePoint );
|
|
float3 L = normalize( L0 + L1 );
|
|
|
|
// Intersect ray with plane
|
|
float Distance = dot( Rect.Axis[2], Rect.Origin ) / dot( Rect.Axis[2], L );
|
|
float DistanceSqr = Distance * Distance;
|
|
|
|
// right pyramid solid angle cosine weighted approx
|
|
//float Irradiance = 4 * RectExtent.x * RectExtent.y * rsqrt( ( Square( RectExtent.x ) + DistanceSqr ) * ( Square( RectExtent.y ) + DistanceSqr ) );
|
|
BaseIrradiance = 4 * Rect.Extent.x * Rect.Extent.y * rsqrt( ( (4 / PI) * Square( Rect.Extent.x ) + DistanceSqr ) * ( (4 / PI) * Square( Rect.Extent.y ) + DistanceSqr ) );
|
|
BaseIrradiance *= saturate( dot( Rect.Axis[2], L ) );
|
|
|
|
// Solid angle of sphere = 2*PI * ( 1 - sqrt(1 - r^2 / d^2 ) )
|
|
// Cosine weighted integration = PI * r^2 / d^2
|
|
// SinAlphaSqr = r^2 / d^2;
|
|
float SinAlphaSqr = BaseIrradiance * (1.0 / PI);
|
|
|
|
NoL = SphereHorizonCosWrap( dot( N, L ), SinAlphaSqr );
|
|
|
|
return L;
|
|
}
|
|
|
|
float3 RectIrradianceApproxLagarde( float3 N, FRect Rect, out float BaseIrradiance, out float NoL )
|
|
{
|
|
float3 L = normalize( Rect.Origin );
|
|
|
|
float3 v0 = Rect.Origin - Rect.Axis[0] * Rect.Extent.x - Rect.Axis[1] * Rect.Extent.y;
|
|
float3 v1 = Rect.Origin + Rect.Axis[0] * Rect.Extent.x - Rect.Axis[1] * Rect.Extent.y;
|
|
float3 v2 = Rect.Origin + Rect.Axis[0] * Rect.Extent.x + Rect.Axis[1] * Rect.Extent.y;
|
|
float3 v3 = Rect.Origin - Rect.Axis[0] * Rect.Extent.x + Rect.Axis[1] * Rect.Extent.y;
|
|
|
|
float3 n0 = normalize( cross( v0, v1 ) );
|
|
float3 n1 = normalize( cross( v1, v2 ) );
|
|
float3 n2 = normalize( cross( v2, v3 ) );
|
|
float3 n3 = normalize( cross( v3, v0 ) );
|
|
|
|
float g0 = acos( dot( n0, n1 ) );
|
|
float g1 = acos( dot( n1, n2 ) );
|
|
float g2 = acos( dot( n2, n3 ) );
|
|
float g3 = acos( dot( n3, n0 ) );
|
|
|
|
// Solid angle
|
|
BaseIrradiance = g0 + g1 + g2 + g3 - 2*PI;
|
|
|
|
NoL = 0.2 * ( saturate( dot( N, L ) ) +
|
|
saturate( dot( N, normalize(v0) ) ) +
|
|
saturate( dot( N, normalize(v1) ) ) +
|
|
saturate( dot( N, normalize(v2) ) ) +
|
|
saturate( dot( N, normalize(v3) ) ) );
|
|
|
|
return L;
|
|
}
|
|
|
|
float3 RectIrradianceApproxDrobot( float3 N, FRect Rect, out float BaseIrradiance, out float NoL )
|
|
{
|
|
#if 0
|
|
// Drobot complex
|
|
float3 d0 = Rect.Origin;
|
|
d0 += RectX * clamp( dot( Rect.Axis[0], -Rect.Origin ), -Rect.Extent.x, Rect.Extent.x );
|
|
d0 += RectY * clamp( dot( Rect.Axis[1], -Rect.Origin ), -Rect.Extent.y, Rect.Extent.y );
|
|
|
|
float3 d1 = N - Rect.Axis[2] * saturate( 0.001 + dot( Rect.Axis[2], N ) );
|
|
|
|
d0 = normalize( d0 );
|
|
d1 = normalize( d1 );
|
|
float3 dh = normalize( d0 + d1 );
|
|
#else
|
|
// Drobot simple
|
|
float clampCosAngle = 0.001 + saturate( dot( N, Rect.Axis[2] ) );
|
|
// clamp d0 to the positive hemisphere of surface normal
|
|
float3 d0 = normalize( -Rect.Axis[2] + N * clampCosAngle );
|
|
// clamp d1 to the negative hemisphere of light plane normal
|
|
float3 d1 = normalize( N - Rect.Axis[2] * clampCosAngle );
|
|
float3 dh = normalize( d0 + d1 );
|
|
#endif
|
|
|
|
// Intersect ray with plane
|
|
float3 PointOnPlane = dh * ( dot( Rect.Axis[2], Rect.Origin ) / dot( Rect.Axis[2], dh ) );
|
|
|
|
float3 ClosestPoint = Rect.Origin;
|
|
ClosestPoint += Rect.Axis[0] * clamp( dot( Rect.Axis[0], PointOnPlane - Rect.Origin ), -Rect.Extent.x, Rect.Extent.x );
|
|
ClosestPoint += Rect.Axis[1] * clamp( dot( Rect.Axis[1], PointOnPlane - Rect.Origin ), -Rect.Extent.y, Rect.Extent.y );
|
|
|
|
float3 v0 = Rect.Origin - Rect.Axis[0] * Rect.Extent.x - Rect.Axis[1] * Rect.Extent.y;
|
|
float3 v1 = Rect.Origin + Rect.Axis[0] * Rect.Extent.x - Rect.Axis[1] * Rect.Extent.y;
|
|
float3 v2 = Rect.Origin + Rect.Axis[0] * Rect.Extent.x + Rect.Axis[1] * Rect.Extent.y;
|
|
float3 v3 = Rect.Origin - Rect.Axis[0] * Rect.Extent.x + Rect.Axis[1] * Rect.Extent.y;
|
|
#if 1
|
|
float3 n0 = normalize( cross( v0, v1 ) );
|
|
float3 n1 = normalize( cross( v1, v2 ) );
|
|
float3 n2 = normalize( cross( v2, v3 ) );
|
|
float3 n3 = normalize( cross( v3, v0 ) );
|
|
|
|
float g0 = acos( dot( n0, n1 ) );
|
|
float g1 = acos( dot( n1, n2 ) );
|
|
float g2 = acos( dot( n2, n3 ) );
|
|
float g3 = acos( dot( n3, n0 ) );
|
|
|
|
float SolidAngle = g0 + g1 + g2 + g3 - 2*PI;
|
|
|
|
float3 L = normalize( ClosestPoint );
|
|
#else
|
|
float DistanceSqr = dot( RectOrigin, RectOrigin );
|
|
float3 L = RectOrigin * rsqrt( DistanceSqr );
|
|
|
|
DistanceSqr = dot( ClosestPoint, ClosestPoint );
|
|
L = ClosestPoint * rsqrt( DistanceSqr );
|
|
|
|
float SolidAngle = PI / ( DistanceSqr / ( (4 / PI) * Rect.Extent.x * Rect.Extent.y ) + 1 );
|
|
//float SolidAngle = 4 * asin( RectExtent.x * RectExtent.y / sqrt( ( Square( RectExtent.x ) + DistanceSqr ) * ( Square(RectExtent.y) + DistanceSqr ) ) );
|
|
SolidAngle *= saturate( dot( -Rect.Axis[2], L ) );
|
|
#endif
|
|
|
|
BaseIrradiance = SolidAngle;
|
|
NoL = saturate( dot( N, L ) );
|
|
|
|
return L;
|
|
}
|
|
|
|
|
|
float3 SampleSourceTexture( float3 L, FRect Rect, FRectTexture RectTexture)
|
|
{
|
|
#if USE_SOURCE_TEXTURE
|
|
// Force to point at plane
|
|
L += Rect.Axis[2] * saturate( 0.001 - dot( Rect.Axis[2], L ) );
|
|
|
|
// Intersect ray with plane
|
|
float DistToPlane = dot( Rect.Axis[2], Rect.Origin ) / dot( Rect.Axis[2], L );
|
|
float3 PointOnPlane = L * DistToPlane;
|
|
|
|
float2 PointInRect;
|
|
PointInRect.x = dot( Rect.Axis[0], PointOnPlane - Rect.Origin );
|
|
PointInRect.y = dot( Rect.Axis[1], PointOnPlane - Rect.Origin );
|
|
|
|
// Compute UV on the original rect (i.e. unoccluded rect)
|
|
float2 RectUV = (PointInRect + Rect.Offset) / max(0.0001f, Rect.FullExtent) * float2(0.5, -0.5) + 0.5;
|
|
|
|
float Level = log2( DistToPlane * rsqrt( max(0.0001f, Rect.FullExtent.x * Rect.FullExtent.y) ) );
|
|
|
|
return SampleRectTexture(RectTexture, RectUV, Level);
|
|
#else
|
|
return 1;
|
|
#endif
|
|
}
|
|
|
|
float IntegrateEdge( float3 L0, float3 L1 )
|
|
{
|
|
float c01 = dot( L0, L1 );
|
|
//float w01 = ( 1.5708 - 0.175 * c01 ) * rsqrt( c01 + 1 ); // 1 mad, 1 mul, 1 add, 1 rsqrt
|
|
//float w01 = 1.5708 + (-0.879406 + 0.308609 * abs(c01) ) * abs(c01);
|
|
|
|
//return acos( c01 ) * rsqrt( 1 - c01 * c01 );
|
|
|
|
#if 0
|
|
// [ Hill et al. 2016, "Real-Time Area Lighting: a Journey from Research to Production" ]
|
|
float w01 = ( 5.42031 + (3.12829 + 0.0902326 * abs(c01)) * abs(c01) ) /
|
|
( 3.45068 + (4.18814 + abs(c01)) * abs(c01) );
|
|
|
|
w01 = c01 > 0 ? w01 : PI * rsqrt( 1 - c01 * c01 ) - w01;
|
|
#else
|
|
float w01 = ( 0.8543985 + (0.4965155 + 0.0145206 * abs(c01)) * abs(c01) ) /
|
|
( 3.4175940 + (4.1616724 + abs(c01)) * abs(c01) );
|
|
|
|
w01 = c01 > 0 ? w01 : 0.5 * rsqrt( max(1 - c01 * c01, 1.0e-4f) ) - w01;
|
|
#endif
|
|
|
|
return w01;
|
|
}
|
|
|
|
// Optimized Lambert poly irradiance
|
|
float3 PolygonIrradiance( float3 Poly[4] )
|
|
{
|
|
float3 L0 = normalize( Poly[0] ); // 2 mad, 4 mul, 1 rsqrt
|
|
float3 L1 = normalize( Poly[1] ); // 2 mad, 4 mul, 1 rsqrt
|
|
float3 L2 = normalize( Poly[2] ); // 2 mad, 4 mul, 1 rsqrt
|
|
float3 L3 = normalize( Poly[3] ); // 2 mad, 4 mul, 1 rsqrt
|
|
// 24 alu, 4 rsqrt
|
|
|
|
#if 0
|
|
float3 L;
|
|
L = acos( dot( L0, L1 ) ) * normalize( cross( L0, L1 ) );
|
|
L += acos( dot( L1, L2 ) ) * normalize( cross( L1, L2 ) );
|
|
L += acos( dot( L2, L3 ) ) * normalize( cross( L2, L3 ) );
|
|
L += acos( dot( L3, L0 ) ) * normalize( cross( L3, L0 ) );
|
|
#else
|
|
float w01 = IntegrateEdge( L0, L1 );
|
|
float w12 = IntegrateEdge( L1, L2 );
|
|
float w23 = IntegrateEdge( L2, L3 );
|
|
float w30 = IntegrateEdge( L3, L0 );
|
|
|
|
#if 0
|
|
float3 L;
|
|
L = w01 * cross( L0, L1 ); // 6 mul, 3 mad
|
|
L += w12 * cross( L1, L2 ); // 3 mul, 6 mad
|
|
L += w23 * cross( L2, L3 ); // 3 mul, 6 mad
|
|
L += w30 * cross( L3, L0 ); // 3 mul, 6 mad
|
|
#else
|
|
float3 L;
|
|
L = cross( L1, -w01 * L0 + w12 * L2 ); // 6 mul, 6 mad
|
|
L += cross( L3, w30 * L0 + -w23 * L2 ); // 3 mul, 9 mad
|
|
#endif
|
|
#endif
|
|
|
|
// Vector irradiance
|
|
return L;
|
|
}
|
|
|
|
struct FRectLTC
|
|
{
|
|
float3x3 LTC;
|
|
float3x3 InvLTC;
|
|
float3 IrradianceScale;
|
|
};
|
|
|
|
// Return LTC coefficients for GGX-based BSDF
|
|
FRectLTC GetRectLTC_GGX(float Roughness, float3 F0, float3 F90, half NoV)
|
|
{
|
|
float2 UV = float2( Roughness, sqrt( 1 - NoV ) );
|
|
UV = UV * (63.0 / 64.0) + (0.5 / 64.0);
|
|
|
|
float4 LTCMat = View.GGXLTCMatTexture.SampleLevel( GGXLTCMatSampler, UV, 0 );
|
|
float4 LTCAmp = View.GGXLTCAmpTexture.SampleLevel( GGXLTCAmpSampler, UV, 0 );
|
|
|
|
float3x3 LTC = {
|
|
float3( LTCMat.x, 0, LTCMat.z ),
|
|
float3( 0, 1, 0 ),
|
|
float3( LTCMat.y, 0, LTCMat.w )
|
|
};
|
|
|
|
float LTCDet = LTCMat.x * LTCMat.w - LTCMat.y * LTCMat.z;
|
|
|
|
float4 InvLTCMat = LTCMat / LTCDet;
|
|
float3x3 InvLTC = {
|
|
float3( InvLTCMat.w, 0,-InvLTCMat.z ),
|
|
float3( 0, 1, 0 ),
|
|
float3(-InvLTCMat.y, 0, InvLTCMat.x )
|
|
};
|
|
|
|
FRectLTC Out = (FRectLTC)0;
|
|
Out.LTC = LTC;
|
|
Out.InvLTC = InvLTC;
|
|
Out.IrradianceScale = F90 * LTCAmp.y + ( LTCAmp.x - LTCAmp.y ) * F0;
|
|
return Out;
|
|
}
|
|
|
|
FRectLTC GetRectLTC_GGX( float Roughness, float3 SpecularColor, half NoV)
|
|
{
|
|
// F90 is derived as, anything less than 2% is physically impossible and is instead considered to be shadowing
|
|
const float3 F0 = SpecularColor;
|
|
const float3 F90 = saturate(50.0 * SpecularColor);
|
|
|
|
return GetRectLTC_GGX(Roughness, F0, F90, NoV);
|
|
}
|
|
|
|
#if SUPPORTS_INDEPENDENT_SAMPLERS
|
|
#define SharedSheenLTCSampler View.SharedBilinearClampedSampler
|
|
#else
|
|
#define SharedSheenLTCSampler View.SheenLTCSampler
|
|
#endif
|
|
|
|
// Return LTC coefficients for Sheen BSDF
|
|
FRectLTC GetRectLTC_Sheen( float Roughness, half NoV)
|
|
{
|
|
const float Alpha = sqrt(Roughness);
|
|
const float SatNoV = saturate(abs(NoV) + 1e-5);
|
|
float2 UV = float2(Alpha, SatNoV);
|
|
UV = UV * (31.0 / 32.0) + (0.5 / 32.0);
|
|
const float3 SheenLTC = View.SheenLTCTexture.SampleLevel(SharedSheenLTCSampler, UV, 0).xyz;
|
|
|
|
const float aInv = SheenLTC.x;
|
|
const float bInv = SheenLTC.y;
|
|
|
|
float3x3 LTC = {
|
|
float3(aInv, 0, bInv),
|
|
float3(0, aInv, 0),
|
|
float3(0, 0, 1)
|
|
};
|
|
|
|
float3x3 InvLTC = {
|
|
float3(1/aInv, 0, -bInv/aInv),
|
|
float3(0, 1/aInv, 0),
|
|
float3(0, 0, 1)
|
|
};
|
|
|
|
FRectLTC Out = (FRectLTC)0;
|
|
Out.LTC = LTC;
|
|
Out.InvLTC = InvLTC;
|
|
Out.IrradianceScale = SheenLTC.z;
|
|
return Out;
|
|
}
|
|
|
|
// Integrate a LTC BSDF with a rect light
|
|
// [ Heitz et al. 2016, "Real-Time Polygonal-Light Shading with Linearly Transformed Cosines" ]
|
|
float3 RectApproxLTC(FRectLTC In, half3 N, float3 V, FRect Rect, FRectTexture RectTexture, inout float3 OutMeanLightWorldDirection)
|
|
{
|
|
// No visibile rect light due to barn door occlusion
|
|
if (Rect.Extent.x == 0 || Rect.Extent.y == 0) return 0;
|
|
|
|
// Rotate to tangent space
|
|
float3 T1 = normalize( V - N * dot( N, V ) );
|
|
float3 T2 = cross( N, T1 );
|
|
float3x3 TangentBasis = float3x3( T1, T2, N );
|
|
|
|
In.LTC = mul( In.LTC, TangentBasis );
|
|
In.InvLTC = mul( transpose( TangentBasis ), In.InvLTC );
|
|
|
|
float3 Poly[4];
|
|
Poly[0] = mul( In.LTC, Rect.Origin - Rect.Axis[0] * Rect.Extent.x - Rect.Axis[1] * Rect.Extent.y );
|
|
Poly[1] = mul( In.LTC, Rect.Origin + Rect.Axis[0] * Rect.Extent.x - Rect.Axis[1] * Rect.Extent.y );
|
|
Poly[2] = mul( In.LTC, Rect.Origin + Rect.Axis[0] * Rect.Extent.x + Rect.Axis[1] * Rect.Extent.y );
|
|
Poly[3] = mul( In.LTC, Rect.Origin - Rect.Axis[0] * Rect.Extent.x + Rect.Axis[1] * Rect.Extent.y );
|
|
|
|
// Vector irradiance
|
|
float3 L = PolygonIrradiance( Poly );
|
|
|
|
#if 0
|
|
// Early out negative irradiance vector, causing 'ghost' rect light silhouette at low roughness
|
|
OutMeanLightWorldDirection = N;
|
|
if (L.z <= 0)
|
|
{
|
|
return 0;
|
|
}
|
|
#endif
|
|
|
|
float LengthSqr = dot( L, L );
|
|
float InvLength = rsqrt( LengthSqr );
|
|
float Length = LengthSqr * InvLength;
|
|
|
|
// Mean light direction
|
|
L *= InvLength;
|
|
|
|
// Solid angle of sphere = 2*PI * ( 1 - sqrt(1 - r^2 / d^2 ) )
|
|
// Cosine weighted integration = PI * r^2 / d^2
|
|
// SinAlphaSqr = r^2 / d^2;
|
|
float SinAlphaSqr = Length;
|
|
|
|
float NoL = SphereHorizonCosWrap( L.z, SinAlphaSqr );
|
|
float Irradiance = SinAlphaSqr * NoL;
|
|
|
|
// Kill negative and NaN
|
|
Irradiance = -min(-Irradiance, 0.0);
|
|
|
|
#if 0
|
|
float NoL;
|
|
float Falloff;
|
|
RectIrradianceLambert( N, Rect, Falloff, NoL );
|
|
|
|
float a2 = Pow4( Roughness );
|
|
Irradiance = Irradiance * ( 1 - a2*a2 ) + a2 * Falloff * NoL;
|
|
#endif
|
|
|
|
// Transform to world space
|
|
L = mul( In.InvLTC, L );
|
|
OutMeanLightWorldDirection = L;
|
|
|
|
float3 LightColor = SampleSourceTexture( L, Rect, RectTexture );
|
|
|
|
return LightColor * Irradiance * In.IrradianceScale;
|
|
}
|
|
|
|
// Integrated a GGX-based BSDF with a rect light using LTC
|
|
float3 RectGGXApproxLTC( float Roughness, float3 SpecularColor, half3 N, float3 V, FRect Rect, FRectTexture RectTexture, inout float3 OutMeanLightWorldDirection)
|
|
{
|
|
// No visibile rect light due to barn door occlusion
|
|
if (Rect.Extent.x == 0 || Rect.Extent.y == 0) return 0;
|
|
|
|
const float NoV = saturate( abs( dot(N, V) ) + 1e-5 );
|
|
|
|
const FRectLTC LTC = GetRectLTC_GGX(Roughness, SpecularColor, NoV);
|
|
return RectApproxLTC(LTC, N, V, Rect, RectTexture, OutMeanLightWorldDirection);
|
|
}
|
|
|
|
float3 RectGGXApproxLTC(float Roughness, float3 SpecularColor, half3 N, float3 V, FRect Rect, FRectTexture RectTexture)
|
|
{
|
|
float3 MeanLightWorldDirection = 0.0f;
|
|
return RectGGXApproxLTC(Roughness, SpecularColor, N, V, Rect, RectTexture, MeanLightWorldDirection);
|
|
}
|
|
|
|
float3 RectGGXApproxLTC(float Roughness, float3 F0, float3 F90, half3 N, float3 V, FRect Rect, FRectTexture RectTexture, inout float3 OutMeanLightWorldDirection)
|
|
{
|
|
// No visibile rect light due to barn door occlusion
|
|
if (Rect.Extent.x == 0 || Rect.Extent.y == 0) return 0;
|
|
|
|
const float NoV = saturate(abs(dot(N, V)) + 1e-5);
|
|
|
|
const FRectLTC LTC = GetRectLTC_GGX(Roughness, F0, F90, NoV);
|
|
return RectApproxLTC(LTC, N, V, Rect, RectTexture, OutMeanLightWorldDirection);
|
|
}
|
|
|
|
float3 RectGGXApproxLTC(float Roughness, float3 F0, float3 F90, half3 N, float3 V, FRect Rect, FRectTexture RectTexture)
|
|
{
|
|
float3 MeanLightWorldDirection = 0.0f;
|
|
return RectGGXApproxLTC(Roughness, F0, F90, N, V, Rect, RectTexture, MeanLightWorldDirection);
|
|
}
|
|
|
|
// Integrated a Sheen-based BSDF with a rect light using LTC
|
|
float3 RectSheenApproxLTC( float Roughness, half3 N, float3 V, FRect Rect, FRectTexture RectTexture, inout float DirectionalAlbedo)
|
|
{
|
|
// No visibile rect light due to barn door occlusion
|
|
if (Rect.Extent.x == 0 || Rect.Extent.y == 0) return 0;
|
|
|
|
const float NoV = saturate( abs( dot(N, V) ) + 1e-5 );
|
|
|
|
const FRectLTC LTC = GetRectLTC_Sheen(Roughness, NoV);
|
|
DirectionalAlbedo = LTC.IrradianceScale.x;
|
|
|
|
const float Scale = 2 * PI; // Factor to match the path-tracer
|
|
float3 MeanLightWorldDirection = 0.0f;
|
|
return RectApproxLTC(LTC, N, V, Rect, RectTexture, MeanLightWorldDirection) * Scale;
|
|
}
|
|
|
|
// Rectangle projected to a sphere
|
|
// [Urena et al. 2013, "An Area-Preserving Parametrization for Spherical Rectangles"]
|
|
struct FSphericalRect
|
|
{
|
|
float3x3 Axis;
|
|
|
|
float x0;
|
|
float x1;
|
|
float y0;
|
|
float y1;
|
|
float z0;
|
|
|
|
float b0;
|
|
float b1;
|
|
float k;
|
|
float SolidAngle;
|
|
};
|
|
|
|
// The routine below needs more accuracy near 1.0 to accurately capture distant rectlights
|
|
// The typical approach (for example see: asinFast) looses too much accuracy to cancelation near the endpoints
|
|
float SphericalRectAsin(float x)
|
|
{
|
|
const float HalfPI = PI / 2;
|
|
// Argument reduction to [0,0.5]
|
|
// using: asin(x) == pi/2-2*asin(sqrt(0.5-0.5*x))
|
|
float a = saturate(abs(x));
|
|
bool inner = a < 0.5f;
|
|
float a2 = inner ? a * a : 0.5 - 0.5 * a;
|
|
a = inner ? a : sqrt(a2);
|
|
|
|
// Fit using Mathematica -- max error for asin is 10^-4.87
|
|
float r = 0.100323f;
|
|
r = mad(r, a2, 0.163288f);
|
|
r = mad(r, a2, 1.00011f) * a;
|
|
r = inner ? r : HalfPI - 2 * r;
|
|
|
|
return asfloat(asuint(r) ^ (asuint(x) & 0x80000000u)); // propagate sign bit
|
|
}
|
|
|
|
// RectZ must face origin
|
|
FSphericalRect BuildSphericalRect( FRect Rect )
|
|
{
|
|
FSphericalRect SphericalRect;
|
|
|
|
SphericalRect.Axis = Rect.Axis;
|
|
|
|
float3 LocalPosition = mul(Rect.Axis, Rect.Origin);
|
|
|
|
SphericalRect.x0 = LocalPosition.x - Rect.Extent.x;
|
|
SphericalRect.x1 = LocalPosition.x + Rect.Extent.x;
|
|
SphericalRect.y0 = LocalPosition.y - Rect.Extent.y;
|
|
SphericalRect.y1 = LocalPosition.y + Rect.Extent.y;
|
|
SphericalRect.z0 = -abs( LocalPosition.z );
|
|
|
|
SphericalRect.Axis[2] *= LocalPosition.z > 0 ? -1 : 1;
|
|
|
|
// Original paper creates all vertices, then all normals via cross products and finally computes angles via dot products.
|
|
// However since all the points lay on an axis-aligned plane, the math can be simplified by recognizing that most terms become
|
|
// zero, leaving only the product of the z components of the normals.
|
|
float z0sq = LocalPosition.z * LocalPosition.z;
|
|
float n0z = -SphericalRect.y0 * rsqrt(z0sq + SphericalRect.y0 * SphericalRect.y0);
|
|
float n1z = SphericalRect.x1 * rsqrt(z0sq + SphericalRect.x1 * SphericalRect.x1);
|
|
float n2z = SphericalRect.y1 * rsqrt(z0sq + SphericalRect.y1 * SphericalRect.y1);
|
|
float n3z = -SphericalRect.x0 * rsqrt(z0sq + SphericalRect.x0 * SphericalRect.x0);
|
|
|
|
// Original paper uses acos(-dot(...)). Here we use the identify acos(-x) == pi/2+asin(x) to simplify
|
|
// the math.
|
|
float G0G1 = SphericalRectAsin(n0z * n1z) + SphericalRectAsin(n1z * n2z);
|
|
float G2G3 = SphericalRectAsin(n2z * n3z) + SphericalRectAsin(n3z * n0z);
|
|
|
|
SphericalRect.b0 = n0z;
|
|
SphericalRect.b1 = n2z;
|
|
SphericalRect.k = G2G3;
|
|
SphericalRect.SolidAngle = G0G1 + SphericalRect.k;
|
|
|
|
return SphericalRect;
|
|
}
|
|
|
|
struct FSphericalRectSample {
|
|
float3 Direction;
|
|
float Distance;
|
|
float2 UV;
|
|
float InvPdf;
|
|
};
|
|
|
|
#define SPHERICAL_RECT_MIN_SOLIDANGLE 1e-3
|
|
|
|
float GetSphericalRectInversePdf(float3 Direction, float DistanceSquared, FSphericalRect Rect)
|
|
{
|
|
if (Rect.SolidAngle > SPHERICAL_RECT_MIN_SOLIDANGLE)
|
|
{
|
|
// Light is big enough to use solid angle sampling
|
|
return Rect.SolidAngle;
|
|
}
|
|
else
|
|
{
|
|
// Light is very small, area sampling is sufficient
|
|
float Area = (Rect.y1 - Rect.y0) * (Rect.x1 - Rect.x0);
|
|
float NoL = abs(dot(Direction, Rect.Axis[2]));
|
|
return Area * NoL / DistanceSquared;
|
|
}
|
|
}
|
|
|
|
FSphericalRectSample UniformSampleSphericalRect(float2 E, FSphericalRect Rect)
|
|
{
|
|
float xu, yv;
|
|
if (Rect.SolidAngle > SPHERICAL_RECT_MIN_SOLIDANGLE)
|
|
{
|
|
// Some signs are flipped from the original paper on account of the different calculation of SolidAngle and k
|
|
float au = E.x * Rect.SolidAngle - Rect.k;
|
|
float fu = (cos(au) * Rect.b0 + Rect.b1) / sin(au);
|
|
float cu = rsqrt(fu * fu + Rect.b0 * Rect.b0) * (fu > 0 ? 1 : -1);
|
|
cu = clamp(cu, -1, 1); // avoid NaNs
|
|
|
|
xu = -(cu * Rect.z0) * rsqrt(1 - cu * cu);
|
|
xu = clamp(xu, Rect.x0, Rect.x1); // avoid Infs
|
|
|
|
float d2 = xu * xu + Rect.z0 * Rect.z0;
|
|
float h0 = Rect.y0 * rsqrt(d2 + Rect.y0 * Rect.y0);
|
|
float h1 = Rect.y1 * rsqrt(d2 + Rect.y1 * Rect.y1);
|
|
float hv = h0 + E.y * (h1 - h0);
|
|
float rv = 1.0 - hv * hv;
|
|
yv = (rv > 0) ? (hv * d2 * rsqrt(rv * d2)) : Rect.y1;
|
|
}
|
|
else
|
|
{
|
|
// fallback to area sampling when solid angle is tiny to avoid numerical issues
|
|
xu = lerp(Rect.x0, Rect.x1, E.x);
|
|
yv = lerp(Rect.y0, Rect.y1, E.y);
|
|
}
|
|
FSphericalRectSample Result;
|
|
Result.Direction = mul(float3(xu, yv, Rect.z0), Rect.Axis);
|
|
Result.UV = float2(xu - Rect.x0, yv - Rect.y0) / float2(Rect.x1 - Rect.x0, Rect.y1 - Rect.y0);
|
|
|
|
float DistanceSquared = xu * xu + yv * yv + Rect.z0 * Rect.z0;
|
|
float InvDistance = rsqrt(DistanceSquared);
|
|
|
|
Result.Distance = DistanceSquared * InvDistance;
|
|
Result.Direction *= InvDistance;
|
|
|
|
Result.InvPdf = GetSphericalRectInversePdf(Result.Direction, DistanceSquared, Rect);
|
|
return Result;
|
|
}
|
|
|
|
FRect GetRect(
|
|
float3 ToLight,
|
|
float3 LightDataDirection,
|
|
float3 LightDataTangent,
|
|
float LightDataSourceRadius,
|
|
float LightDataSourceLength,
|
|
float LightDataRectLightBarnCosAngle,
|
|
float LightDataRectLightBarnLength,
|
|
bool bComputeVisibleRect)
|
|
{
|
|
// Is blocked by barn doors
|
|
FRect Rect;
|
|
Rect.Origin = ToLight;
|
|
Rect.Axis[1] = LightDataTangent;
|
|
Rect.Axis[2] = LightDataDirection;
|
|
Rect.Axis[0] = cross( Rect.Axis[1], Rect.Axis[2] );
|
|
Rect.Extent = float2(LightDataSourceRadius, LightDataSourceLength);
|
|
Rect.FullExtent = Rect.Extent;
|
|
Rect.Offset = 0;
|
|
|
|
// Compute the visible rectangle from the current shading point.
|
|
// The new rectangle will have reduced width/height, and a shifted origin
|
|
//
|
|
// Common setup for occlusion computation
|
|
// Notes: Barn angle & length are identical for all sides
|
|
// D_B
|
|
// <-->
|
|
// D_S
|
|
// <---------------> O +X
|
|
// -------------.--------------->------ ^
|
|
// / . | \ |
|
|
// / . | \ | BarnDepth
|
|
// ./ | \ v
|
|
// . C v +Z
|
|
// .
|
|
// .
|
|
// S
|
|
//
|
|
// Only compute the occluded rect if the barn door has an angle less
|
|
// than 88 degrees
|
|
if (bComputeVisibleRect && LightDataRectLightBarnCosAngle > 0.035f)
|
|
{
|
|
const float3 LightdPdv = -Rect.Axis[1];
|
|
const float3 LightdPdu = -Rect.Axis[0];
|
|
const float2 LightExtent = float2(LightDataSourceRadius, LightDataSourceLength);
|
|
const float BarnLength = LightDataRectLightBarnLength;
|
|
|
|
// Project shading point S into light space
|
|
float3 S_Light = mul(Rect.Axis, ToLight);
|
|
|
|
// Compute barn door projection (D_B). Clamp the projection to the shading point if it is closer
|
|
// to the light than the actual barn door
|
|
// Theta is the angle between the Z axis and the barn door
|
|
const float CosTheta = LightDataRectLightBarnCosAngle;
|
|
const float SinTheta = sqrt(1 - CosTheta * CosTheta);
|
|
const float BarnDepth = min(S_Light.z, CosTheta * BarnLength);
|
|
const float S_ratio = BarnDepth / max(0.0001f, CosTheta * BarnLength);
|
|
const float D_B = SinTheta * BarnLength * S_ratio;
|
|
|
|
// Clamp shading point onto the closest edge, if it is inside the rect light
|
|
const float2 SignS = sign(S_Light.xy);
|
|
S_Light.xy = SignS * max(abs(S_Light.xy), LightExtent + D_B.xx);
|
|
|
|
// Compute the closest rect lignt corner, offset by the barn door size
|
|
const float3 C = float3(SignS * (LightExtent + D_B.xx), BarnDepth);
|
|
|
|
// Compute projected distance (D_S) of barn door onto the rect light
|
|
// Eta is the angle between the Z axis and the direction vector (S-C)
|
|
const float3 SProj = S_Light - C;
|
|
const float CosEta = max(SProj.z, 0.001f);
|
|
const float2 SinEta = abs(SProj.xy);
|
|
const float2 TanEta = abs(SProj.xy) / CosEta;
|
|
const float2 D_S = BarnDepth * TanEta;
|
|
|
|
// Equivalent to (e.g., X axis):
|
|
// if (SignS.x < 0) MinMaxX.x += D_S - D_B;
|
|
// if (SignS.x > 0) MinMaxX.y -= D_S - D_B;
|
|
const float2 MinXY = clamp(-LightExtent + (D_S - D_B.xx) * max(0, -SignS), -LightExtent, LightExtent);
|
|
const float2 MaxXY = clamp( LightExtent - (D_S - D_B.xx) * max(0, SignS), -LightExtent, LightExtent);
|
|
const float2 RectOffset = 0.5f * (MinXY + MaxXY);
|
|
|
|
Rect.Extent = 0.5f * (MaxXY - MinXY);
|
|
Rect.Origin = Rect.Origin + LightdPdu * RectOffset.x + LightdPdv * RectOffset.y;
|
|
Rect.Offset = -RectOffset;
|
|
Rect.FullExtent = LightExtent;
|
|
}
|
|
|
|
return Rect;
|
|
}
|
|
|
|
// Get the Rect of the light with respect to the translated world position.
|
|
FRect GetRect(FLightShaderParameters In, float3 TranslatedWorldPosition)
|
|
{
|
|
return GetRect(In.TranslatedWorldPosition - TranslatedWorldPosition,
|
|
In.Direction,
|
|
In.Tangent,
|
|
In.SourceRadius,
|
|
In.SourceLength,
|
|
In.RectLightBarnCosAngle,
|
|
In.RectLightBarnLength,
|
|
true);
|
|
}
|
|
|
|
bool IsRectVisible(FRect Rect)
|
|
{
|
|
// No-visible rect light due to barn door occlusion
|
|
return Rect.Extent.x != 0 && Rect.Extent.y != 0;
|
|
} |