2021-09-09 20:42:29 -04:00

417 lines
20 KiB
HLSL

// SPTD: Spherical Pivot Transformed Distributions
// Keep in synch with the c# side (eg in Bind() and for dims)
TEXTURE2D(_PivotData);
#define PIVOT_LUT_SIZE 64
#define PIVOT_LUT_SCALE ((PIVOT_LUT_SIZE - 1) * rcp(PIVOT_LUT_SIZE))
#define PIVOT_LUT_OFFSET (0.5 * rcp(PIVOT_LUT_SIZE))
//-----------------------------------------------------------------------------
// SPTD structures
//-----------------------------------------------------------------------------
struct SphereCap
{
float3 dir; // direction of cone
float cosA; // cos(aperture angle) of cone (full opening is 2*aperture)
};
SphereCap GetSphereCap(float3 dir, float cosA)
{
SphereCap sCap;
sCap.dir = dir;
sCap.cosA = cosA;
return sCap;
}
//-----------------------------------------------------------------------------
// SPTD functions
//-----------------------------------------------------------------------------
float SphereCapSolidAngle(SphereCap c)
{
return (TWO_PI * (1.0 - c.cosA));
}
// Extract pivot parameters fitting a GGX_projected (BSDF with the cos projection factor
// folded in so we don't have to carry projected solid angle measure when integrating)
// via an SPTD, the non "pivoted" distribution being the uniform spherical distribution
// over the whole sphere (ie Dstd(w) = 1/4*PI )
//
// FGD is required to normalize the fit, otherwise integrating the SPTD over a spherical
// cap implies calculating:
//
// SolidAngle( PivotTransform(sCap) ) / 4*PI
//
// Integral of fitted GGX_projected is thus:
//
// [ SolidAngle( PivotTransform(sCap) ) / 4*PI ] * FGD
//
// orthoBasisViewNormal is assumed as follow:
//
// Basis vectors b1, b2 and b3 arranged as rows, b3 = shading normal,
// view vector lies in the b0-b2 plane.
//
float3 ExtractPivot(float clampedNdotV, float perceptualRoughness, float3x3 orthoBasisViewNormal)
{
float theta = FastACosPos(clampedNdotV);
float2 uv = PIVOT_LUT_OFFSET + PIVOT_LUT_SCALE * float2(perceptualRoughness, theta * INV_HALF_PI);
float2 pivotParams = SAMPLE_TEXTURE2D_LOD(_PivotData, s_linear_clamp_sampler, uv, 0).rg;
float pivotNorm = pivotParams.r;
float pivotElev = pivotParams.g;
float3 pivot = pivotNorm * float3(sin(pivotElev), 0, cos(pivotElev));
// express the pivot in world space
// (basis is left-mul WtoFrame rotation, so a right-mul FrameToW rotation)
pivot = mul(pivot, orthoBasisViewNormal);
return pivot;
}
// Pivot 2D Transformation (helper for the full pivot transform, CapToPCap)
float2 R2ToPR2(float2 pivotDir, float pivotMag)
{
float2 tmp1 = float2(pivotDir.x - pivotMag, pivotDir.y);
float2 tmp2 = pivotMag * pivotDir - float2(1, 0);
float x = dot(tmp1, tmp2);
float y = tmp1.y * tmp2.x - tmp1.x * tmp2.y;
float qf = dot(tmp2, tmp2);
return (float2(x, y) / qf);
}
// Pivot transform a spherical cap: pivot and cap should
// be expressed in the same basis.
SphereCap CapToPCap(SphereCap cap, float3 pivot)
{
// Avoid instability between returning huge apertures to
// none when near these extremes (eg near 1.0, ie degenerate
// cap, depending on the pivot, we can get a cap of
// cos aperture near -1.0 or 1.0 ). See area calculation
// below: we can clamp here, or test area later.
cap.cosA = clamp(cap.cosA, -0.9999, 0.9999);
// extract pivot length and direction
float pivotMag = length(pivot);
// special case: the pivot is at the origin, trivial:
if (pivotMag < 0.001)
{
return GetSphereCap(-cap.dir, cap.cosA);
}
float3 pivotDir = pivot / pivotMag;
// 2D cap dir in the capDir/pivotDir/pivotCapDir 2D plane,
// using the pivotDir as the first axis.
float cosPhi = dot(cap.dir, pivotDir);
float sinPhi = sqrt(1.0 - cosPhi * cosPhi);
// Make a 2D basis for that 2D plane:
// 2D basis = (pivotDir, PivotOrthogonalDirection)
float3 pivotOrthoDir;
if (abs(cosPhi) < 0.9999)
{
pivotOrthoDir = (cap.dir - cosPhi * pivotDir) / sinPhi;
}
else
{
pivotOrthoDir = float3(0, 0, 0);
}
// Compute the original cap 2D end points that intersect and
// lie in the previously mentionned 2D plane.
// We rotate the capDir vector (cosPhi, sinPhi) (coordinates
// expressed in the 2D pivot plane frame above) with +aperture
// and -aperture angles to find dir1 and dir2, the 2 endpoint
// vectors:
float capSinA = sqrt(1.0 - cap.cosA * cap.cosA);
float a1 = cosPhi * cap.cosA;
float a2 = sinPhi * capSinA;
float a3 = sinPhi * cap.cosA;
float a4 = cosPhi * capSinA;
float2 dir1 = float2(a1 + a2, a3 - a4); // Rot(-aperture) (clockwise)
float2 dir2 = float2(a1 - a2, a3 + a4); // Rot(+aperture) (counter clockwise)
// Pivot transform the original cap endpoints in the 2D plane
// to get the pivotCap endpoints:
float2 dir1Xf = R2ToPR2(dir1, pivotMag);
float2 dir2Xf = R2ToPR2(dir2, pivotMag);
// Compute the pivotCap 2D direction (note that the pivotCap
// direction is NOT the pivot transform of the original direction):
// It is the mean direction direction of the two pivotCap endpoints
// ie their half-vector, up to a sign.
// This sign is important, as a smaller than 90 degree aperture cap
// can, with the proper pivot, yield a cap with a much larger
// aperture (ie covering more than an hemisphere).
//
float area = dir1Xf.x * dir2Xf.y - dir1Xf.y * dir2Xf.x;
//if (abs(area) < 0.0001) area = 0.0; // see clamp above
float s = area >= 0.0 ? 1.0 : -1.0;
float2 dirXf = s * normalize(dir1Xf + dir2Xf);
// Compute the 3D pivotCap parameters:
// Transform back the pivotCap endpoints into 3D and compute
// cosine of aperture.
float3 pivotCapDir = dirXf.x * pivotDir + dirXf.y * pivotOrthoDir;
float pivotCapCosA = dot(dirXf, dir1Xf);
return GetSphereCap(pivotCapDir, pivotCapCosA);
}
// Compute specular occlusion from visibility cone:
//
// Integral[ V(w_i) bsdf(w_i, w_o) (n dot w_i) dw_i ]_{over hemisphere}
// Vs = --------------------------------------------------------------------
// Integral[ bsdf(w_i, w_o) (n dot w_i) dw_i ]_{over hemisphere}
//
// where V(w_i) is the occlusion indicator function. The denominator is thus FGD.
// With the visibility cone approximation (aka bent occlusion from bentnormal),
// V becomes the cone ray-set indicator function. We have:
//
// Vs = Integral[ bsdf(w_i, w_o) (n dot w_i) dw_i ]_{over visibility cone} / FGD
//
// We approximate the GGX bsdf() with an SPTD transformed from a uniform distribution
// on the whole unit sphere S^2, and the integral thus becomes (see ExtractPivot)
//
// Vs = Integral[ SPTD(w_i) dw_i ]_{over visibility cone} * normalization / FGD
// = Integral[ Dstd(w_ii) dw_ii ]_{over g(visibility cone)} * normalization / FGD
//
// where normalization is as explained for ExtractPivot since the fit is up to that
// normalization, and here it is FGD;
// g() is the pivot transform (ie CapToPCap), and w_ii := g(w_i) and here we use the
// uniform Dstd(w) = 1/4pi.
//
// Thus Vs becomes
//
// Vs = [SolidAngle( CapToPCap(visibility cone) ) / 4*PI] * normalization /FGD
// = [SolidAngle( CapToPCap(visibility cone) ) / 4*PI] * FGD /FGD
// = [SolidAngle( CapToPCap(visibility cone) ) / 4*PI]
//
// Finally, here we also allow intersecting the visibility cone by another one
// (one of the SPTD property is that the pivot transform is homomorphic to such
// domain composition operation).
//
// For example, IBLs would typically use a normal oriented hemisphere to prevent
// light leaking if we don't trust the construction of our visibility cone to
// not cross under that visible hemisphere horizon: the leak happens in that case
// as SPTD has support spanning the whole sphere while normally the BSDF has a support
// limited to a hemisphere and even though the fit minimizes weight away from the
// specular lobe, it can't aligned support.
//
float ComputeVs(SphereCap visibleCap,
float clampedNdotV,
float perceptualRoughness,
float3x3 orthoBasisViewNormal,
float useExtraCap = false,
SphereCap extraCap = (SphereCap)0.0)
{
float res;
float3 pivot = ExtractPivot(clampedNdotV, perceptualRoughness, orthoBasisViewNormal);
SphereCap c1 = CapToPCap(visibleCap, pivot);
if (useExtraCap)
{
// eg for IBL: extraCap = GetSphereCap(Normal, 0.0)
SphereCap c2 = CapToPCap(extraCap, pivot);
res = SphericalCapIntersectionSolidArea(c1.cosA, c2.cosA, dot(c1.dir, c2.dir));
}
else
{
res = SphereCapSolidAngle(c1);
}
res = res * INV_FOUR_PI;
return saturate(res);
}
//-----------------------------------------------------------------------------
// Specular Occlusion using SPTD functions
//-----------------------------------------------------------------------------
// From:
#include "Packages/com.unity.render-pipelines.high-definition/Runtime/Material/SphericalCapPivot/SpecularOcclusionDef.hlsl"
//#define BENT_VISIBILITY_FROM_AO_UNIFORM 0
//#define BENT_VISIBILITY_FROM_AO_COS 1
//#define BENT_VISIBILITY_FROM_AO_COS_BENT_CORRECTION 2
SphereCap GetBentVisibility(float3 bentNormalWS, float ambientOcclusion, int algorithm = BENT_VISIBILITY_FROM_AO_COS, float3 normalWS = float3(0,0,0))
{
float cosAv;
switch (algorithm)
{
case BENT_VISIBILITY_FROM_AO_UNIFORM:
// AO is uniform (ie expresses non projected solid angle measure):
cosAv = (1.0 - ambientOcclusion);
break;
case BENT_VISIBILITY_FROM_AO_COS:
// AO is cosine weighted (expresses projected solid angle):
cosAv = sqrt(1.0 - ambientOcclusion);
break;
case BENT_VISIBILITY_FROM_AO_COS_BENT_CORRECTION:
// AO is cosine weighted, but this extraction of the cosine of the aperture
// takes into account the fact that if the cone is not perflectly aligned
// with the normal (or the axis by which we define elevation angle), then
// the AO integral calculated yielded a projected solid angle measure of
// an *inclined* spherical cap, and the simple formula above is wrong.
// The projected solid angle measure of a spherical cap is given in (eg)
//
// Geometric Derivation of the Irradiance of Polygonal Lights - Heitz 2017
// https://hal.archives-ouvertes.fr/hal-01458129/document
// p5
//
// The formula below is derived from AO (aka Vd) being considered that
// projected solid angle (given the bent visibility assumption).
//
// (Note that Monte Carlo with IS would typically be used to sample the
// visibility to compute the AO, and the IS rebalancing PDF ratio (weights)
// could then have been applied to the directions or not when calculating
// the bent cone direction. We don't do anything about that, but cone of
// visibility is a gross approximation anyway and can be pretty bad if its
// shape on the hemisphere of directions is very segmented.)
cosAv = sqrt(1.0 - saturate(ambientOcclusion/dot(bentNormalWS, normalWS)) );
break;
}
return GetSphereCap(bentNormalWS, cosAv);
}
float GetSpecularOcclusionFixupLerpParam(SphereCap bentVisibility, float fixupVisibilityRatioThreshold = 0.0001, float fixupStrengthFactor = 0.0)
{
// visibilityRatio is the solid angle visible (according to the visibility cone)
// over total possible visible solid angle of the hemisphere (2*pi), and goes from 0 to 1:
float visibilityRatio = SphereCapSolidAngle(bentVisibility) / TWO_PI;
fixupVisibilityRatioThreshold = max(0.0001, fixupVisibilityRatioThreshold);
float lerpParam = max(fixupVisibilityRatioThreshold - visibilityRatio, 0.0) / fixupVisibilityRatioThreshold;
// Starting when visibilityRatio gets as low as the fixupVisibilityRatioThreshold,
// the lerpParam goes from 0 to 1 (when visibilityRatio reaches 0).
return lerpParam;
}
// Ideally, a bent normal fixup method could use the bent visibility ratio (from AO) value along with a threshold like we
// do now but as a modulation source for LOD biasing the map itself. Then the averaged bent normal value could be used along
// maybe with an additional step that use some measure of dispersion (like normal map AA) to do what we do now with AO.
// In shadergraph, that algorithm would need to be external to the master node.
// For now we just use AO directly.
void ApplyBentSpecularOcclusionFixups(inout SphereCap bentVisibility, inout float perceptualRoughness,
float ambientOcclusion, int bentconeAlgorithm, float3 normalWS,
uint bentFixup = BENT_VISIBILITY_FIXUP_FLAGS_NONE, float fixupVisibilityRatioThreshold = 0.0001, float fixupStrengthFactor = 0.0, float fixupMaxAddedRoughness = 0.0, float3 geomNormalWS = float3(0,0,1))
{
if (bentFixup != BENT_VISIBILITY_FIXUP_FLAGS_NONE)
{
SphereCap bentVisibilityForFixup = bentVisibility;
if (HasFlag(bentFixup, BENT_VISIBILITY_FIXUP_FLAGS_TILT_BENTNORMAL_TO_GEOM)
&& bentconeAlgorithm == BENT_VISIBILITY_FROM_AO_COS_BENT_CORRECTION)
{
// We can't apply (if the option is there) bentnormal fixup before inferring bent visibility
// (solid angle visible in that direction inferred from the AO value)
// since the lerp factor depends on the ratio of the visible hemisphere vs fixupVisibilityRatioThreshold.
// But when the visibility cone solid angle is inferred with BENT_VISIBILITY_FROM_AO_COS_BENT_CORRECTION,
// that solid angle is bentnormal dependent, which we would like to fixup, as the fixup is based on the
// assumption that at low visibility, the baked bentnormal becomes erratic (hence the tilting towards the
// geomNormalWS).
//
// Thus, for calculating the fixup lerpfactor (and only for it), we "cheat" (from user selected value) and
// we override (if selected) BENT_VISIBILITY_FROM_AO_COS_BENT_CORRECTION to use BENT_VISIBILITY_FROM_AO_COS
// instead.
bentVisibilityForFixup = GetBentVisibility(bentVisibility.dir, ambientOcclusion, BENT_VISIBILITY_FROM_AO_COS, normalWS);
}
float lerpParam = GetSpecularOcclusionFixupLerpParam(bentVisibilityForFixup, fixupVisibilityRatioThreshold, fixupStrengthFactor);
// We also allow applying a custom slope (strength), but we still limit the param to a max of 1:
lerpParam = min(lerpParam * fixupStrengthFactor, 1.0);
if (HasFlag(bentFixup, BENT_VISIBILITY_FIXUP_FLAGS_TILT_BENTNORMAL_TO_GEOM))
{
// TODO: fast slerp in unsafe cases ie when angle is large
bentVisibility.dir = normalize(lerp(bentVisibility.dir, geomNormalWS, lerpParam));
// See comment about bentVisibilityForFixup: also we permit ourselves to lerp cos as the solid angle is unknown in
// that case as either AO was computed with the integratal of cos() foreshortening or not, and if it was properly
// computed with it, the bent correction formula is the correct one, but we can't trust fully the bentnormal anyway:
if (bentconeAlgorithm == BENT_VISIBILITY_FROM_AO_COS_BENT_CORRECTION)
{
bentVisibility.cosA = lerp(bentVisibility.cosA, bentVisibilityForFixup.cosA, lerpParam);
}
}
if (HasFlag(bentFixup, BENT_VISIBILITY_FIXUP_FLAGS_BOOST_BSDF_ROUGHNESS))
{
float roughness = PerceptualRoughnessToRoughness(perceptualRoughness);
perceptualRoughness = RoughnessToPerceptualRoughness(roughness + lerpParam * fixupMaxAddedRoughness);
}
}
}
float GetSpecularOcclusionFromBentAOPivot(float3 V, float3 bentNormalWS, float3 normalWS, float ambientOcclusion, float perceptualRoughness, int bentconeAlgorithm = BENT_VISIBILITY_FROM_AO_COS,
bool useGivenBasis = false, float3x3 orthoBasisViewNormal = (float3x3)(0), bool useExtraCap = false, SphereCap extraCap = (SphereCap)(0),
uint bentFixup = BENT_VISIBILITY_FIXUP_FLAGS_NONE, float fixupVisibilityRatioThreshold = 0.0001, float fixupStrengthFactor = 0.0, float fixupMaxAddedRoughness = 0.0, float3 geomNormalWS = float3(0,0,1))
{
//bentNormalWS = lerp(bentNormalWS, normalWS, pow((1.0-ambientOcclusion),5)); // TEST TODO, the bent direction becomes meaningless with AO = 0.
//bentVisibility.dir = normalize(bentNormalWS);
SphereCap bentVisibility = GetBentVisibility(bentNormalWS, ambientOcclusion, bentconeAlgorithm, normalWS);
ApplyBentSpecularOcclusionFixups(bentVisibility, perceptualRoughness, ambientOcclusion, bentconeAlgorithm, normalWS,
bentFixup, fixupVisibilityRatioThreshold, fixupStrengthFactor, fixupMaxAddedRoughness, geomNormalWS);
if (useGivenBasis == false)
{
//orthoBasisViewNormal = GetOrthoBasisViewNormal(V, normalWS, dot(normalWS, V), true); // true => avoid singularity when V == N by returning arbitrary tangent/bitangents
orthoBasisViewNormal = GetOrthoBasisViewNormal(V, normalWS, dot(normalWS, V));
}
float Vs = ComputeVs(bentVisibility,
ClampNdotV(dot(normalWS, V)),
perceptualRoughness,
orthoBasisViewNormal,
useExtraCap, //false, // true => clip with a second spherical cap, eg here the visible hemisphere:
extraCap); //GetSphereCap(normalWS, 0.0));
return Vs;
}
// Different tweaks to the cone-cone method:
float GetSpecularOcclusionFromBentAOConeCone(float3 V, float3 bentNormalWS, float3 normalWS, float ambientOcclusion, float perceptualRoughness, int bentconeAlgorithm = BENT_VISIBILITY_FROM_AO_COS,
uint bentFixup = BENT_VISIBILITY_FIXUP_FLAGS_NONE, float fixupVisibilityRatioThreshold = 0.0001, float fixupStrengthFactor = 0.0, float fixupMaxAddedRoughness = 0.0, float3 geomNormalWS = float3(0,0,1))
{
// Retrieve cone angle
// Ambient occlusion is cosine weighted, thus use following equation. See slide 129
//SphereCap bentVisibility = GetBentVisibility(bentNormalWS, ambientOcclusion, BENT_VISIBILITY_FROM_AO_COS);
SphereCap bentVisibility = GetBentVisibility(bentNormalWS, ambientOcclusion, bentconeAlgorithm, normalWS);
float cosAv = bentVisibility.cosA;
float roughness = max(PerceptualRoughnessToRoughness(perceptualRoughness), 0.01); // Clamp to 0.01 to avoid edge cases
ApplyBentSpecularOcclusionFixups(bentVisibility, perceptualRoughness, ambientOcclusion, bentconeAlgorithm, normalWS,
bentFixup, fixupVisibilityRatioThreshold, fixupStrengthFactor, fixupMaxAddedRoughness, geomNormalWS);
float cosAs = exp2((-log(10.0)/log(2.0)) * Sq(roughness));
float ReflectionLobeSolidAngle = (TWO_PI * (1.0 - cosAs));
float3 R = reflect(-V, normalWS);
float3 modifiedR = GetSpecularDominantDir(normalWS, R, perceptualRoughness, ClampNdotV(dot(normalWS, V)) );
modifiedR = normalize(modifiedR);
float cosB;
#if 1
cosB = dot(bentVisibility.dir, R);
#else
// Test: offspecular modification
cosB = dot(bentVisibility.dir, modifiedR);
#endif
float HemiClippedReflectionLobeSolidAngle = SphericalCapIntersectionSolidArea(0.0, cosAs, cosB);
#if 1
// Original, less expensive, but allow the cone approximation to go under horizon of full hemisphere
// and unecessarily dampens SO (ie more occlusion).
return SphericalCapIntersectionSolidArea(cosAv, cosAs, cosB) / ReflectionLobeSolidAngle;
#else
// More correct, but more expensive:
return saturate(SphericalCapIntersectionSolidArea(cosAv, cosAs, cosB) / HemiClippedReflectionLobeSolidAngle);
#endif
}