Files
UnrealEngine/Engine/Shaders/Private/Substrate/Glint/GlintThirdParty.ush
2025-05-18 13:04:45 +08:00

954 lines
32 KiB
HLSL

// Copyright Epic Games, Inc. All Rights Reserved.
#pragma once
////////////////////////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////////////////////////
// The MIT License
// Copyright © 2021 Xavier Chermain (ICUBE), Simon Lucas(ICUBE), Basile Sauvage (ICUBE), Jean-Michel Dishler (ICUBE) and Carsten Dachsbacher (KIT)
// Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
// Implementation of
// Real-Time Geometric Glint Anti-Aliasing with Normal Map Filtering
// 2021 Xavier Chermain (ICUBE), Simon Lucas(ICUBE), Basile Sauvage (ICUBE), Jean-Michel Dishler (ICUBE) and Carsten Dachsbacher (KIT)
// Updated to match Epic's Unreal Engine needs and standards.
#define InvSqrt2 0.707106f
#define MaxAnisotropy 4.0f
#define GlintSafeThreshold 0.000001f
// FGlintLutParams contains variables linked to the GlintShadingLUT
struct FGlintLutParams
{
float Alpha;
int N;
int NLevels;
float LevelBias;
float LevelMin;
};
FGlintLutParams GetGlintParams()
{
FGlintLutParams GlintLutParams;
const float4 Params0 = View.GlintLUTParameters0;
const float4 Params1 = View.GlintLUTParameters1;
GlintLutParams.Alpha = Params0.x;
GlintLutParams.N = asint(Params0.y);
GlintLutParams.NLevels = asint(Params0.z);
GlintLutParams.LevelBias = Params0.w;
GlintLutParams.LevelMin = Params1.x;
return GlintLutParams;
}
#ifndef FORCE_DISABLE_GLINTS_AA
#define FORCE_DISABLE_GLINTS_AA 0
#endif
// GLINT AA must be skipped for some shaders who do not support ddx/ddy, such as ray tracing shaders.
// It is also skiped for Compute shader (MegaLights). (available on SM6.6 but not on the Metal API yet).
// The good news is that the visual difference is minimal.
#define USE_GLINT_AA (!RAYCALLABLESHADER && !RAYHITGROUPSHADER && !RAYGENSHADER && !RAYMISSSHADER && !FORCE_DISABLE_GLINTS_AA)
// Geometric Glint Anti-Aliasing parameters
#define GGAAKernelSize 0.5
#define GGAAFilter 1
#define UseHemisDerivatives true
float3 SlopeToNormal(float2 Slope)
{
float Norm = sqrt(1 + Slope.x * Slope.x + Slope.y * Slope.y);
return float3(-Slope.x, -Slope.y, 1.) / Norm;
}
//=============================================================================
//============== Non axis aligned anisotropic Beckmann ========================
//=============================================================================
float non_axis_aligned_anisotropic_beckmann(float x, float y, float sigma_x, float sigma_y, float rho)
{
float x_sqr = x * x;
float y_sqr = y * y;
float sigma_x_sqr = sigma_x * sigma_x;
float sigma_y_sqr = sigma_y * sigma_y;
float z = ((x_sqr / sigma_x_sqr) - ((2. * rho * x * y)
/ (sigma_x * sigma_y)) + (y_sqr / sigma_y_sqr));
return exp(-z / (2. * (1. - rho * rho)))
/ (2. * PI * sigma_x * sigma_y * sqrt(1. - rho * rho));
}
//=============================================================================
//===================== Inverse error function ================================
//=============================================================================
float erfinv(float x)
{
float w, p;
w = -log((1.0 - x) * (1.0 + x));
if (w < 5.000000) {
w = w - 2.500000;
p = 2.81022636e-08;
p = 3.43273939e-07 + p * w;
p = -3.5233877e-06 + p * w;
p = -4.39150654e-06 + p * w;
p = 0.00021858087 + p * w;
p = -0.00125372503 + p * w;
p = -0.00417768164 + p * w;
p = 0.246640727 + p * w;
p = 1.50140941 + p * w;
}
else
{
w = sqrt(w) - 3.000000;
p = -0.000200214257;
p = 0.000100950558 + p * w;
p = 0.00134934322 + p * w;
p = -0.00367342844 + p * w;
p = 0.00573950773 + p * w;
p = -0.0076224613 + p * w;
p = 0.00943887047 + p * w;
p = 1.00167406 + p * w;
p = 2.83297682 + p * w;
}
return p * x;
}
//=============================================================================
//======================== Hash function ======================================
//======================== Inigo Quilez =======================================
//================ https://www.shadertoy.com/view/llGSzw ======================
//=============================================================================
float hashIQ(uint n)
{
// integer hash copied from Hugo Elias
n = (n << 13U) ^ n;
n = n * (n * n * 15731U + 789221U) + 1376312589U;
return float(n & 0x7fffffffU) / float(0x7fffffff);
}
//=============================================================================
//================== Pyramid size at LOD level ================================
//=============================================================================
int pyramidSize(FGlintLutParams GlintLutParams, int level)
{
return int(pow(2., float(GlintLutParams.NLevels - 1 - level)));
}
//=============================================================================
//=============== Sampling from a normal distribution =========================
//=============================================================================
float sampleNormalDistribution(float U, float mu, float sigma)
{
float x = sigma * 1.414213f * erfinv(2.0f * U - 1.0f) + mu;
return x;
}
float2 sampleBivariateNormalDistribution(float2 u, float sigmaX, float sigmaY, float rho)
{
// Sample an original 2D centered unit-variance Gaussian
float sampleXO = sampleNormalDistribution(u.x, 0., 1.);
float sampleYO = sampleNormalDistribution(u.y, 0., 1.);
float2 sampleO = float2(sampleXO, sampleYO);
// Matrix M transformed the original unit-variance Gaussian
// into a non-axis aligned Gaussian with standard deviations sigmaX and
// sigmaY and correlation factor rho
// See Chermain et al. 2021 for more information
float M00 = sigmaX * sqrt(1. - rho * rho);
float M01 = rho * sigmaX;
float M10 = 0.;
float M11 = sigmaY;
// Transform the sample using the linear transformation M
return float2(
sampleO.x * M00 + sampleO.y * M01,
sampleO.x * M10 + sampleO.y * M11);
}
//=============================================================================
//===================== Spatially-varying, multiscale, ========================
//=============== and transformed slope distribution function ================
//=============================== Equation 4 ==================================
//=============================================================================
float P22_M(
FGlintLutParams GlintLutParams,
float2 slope_h, int l, int s0, int t0,
float2 slope_dx, float2 slope_dy,
float3 sigma_x_y_rho, float l_dist,
float MicrofacetRelativeArea)
{
// Coherent index
int twoToTheL = int(pow(2., float(l)));
s0 *= twoToTheL;
t0 *= twoToTheL;
// Seed pseudo random generator
uint rngSeed = uint(s0 + 1549 * t0);
float uDensityRandomisation = hashIQ(rngSeed * 2171U);
// Discard cells by using microfacet relative area
float uMicrofacetRelativeArea = hashIQ(rngSeed * 13U);
if (uMicrofacetRelativeArea > MicrofacetRelativeArea)
{
return 0.f;
}
// Fix density randomisation to 2 to have better appearance
float densityRandomisation = 2.;
// Sample a Gaussian to randomise the distribution LOD around the
// distribution level l_dist
l_dist = sampleNormalDistribution(uDensityRandomisation, l_dist,
densityRandomisation);
l_dist = clamp(int(round(l_dist)), 0, GlintLutParams.NLevels);
// Recover roughness and slope correlation factor
float sigma_x = sigma_x_y_rho.x;
float sigma_y = sigma_x_y_rho.y;
float rho = sigma_x_y_rho.z;
// If we are too far from the surface, the SDF is a gaussian.
if (l_dist == GlintLutParams.NLevels) {
return non_axis_aligned_anisotropic_beckmann(slope_h.x, slope_h.y,
sigma_x, sigma_y, rho);
}
// Random rotations to remove glint alignment
float uTheta = hashIQ(rngSeed);
float theta = 2.0 * PI * uTheta;
float cosTheta = cos(theta);
float sinTheta = sin(theta);
//=========================================================================
//========= Linearly transformed isotropic Beckmann distribution ==========
//=========================================================================
float SIGMA_DICT = GlintLutParams.Alpha * InvSqrt2;
float tmp1 = SIGMA_DICT / (sigma_x * sqrt(1. - rho * rho));
float tmp2 =-SIGMA_DICT * rho / (sigma_y * sqrt(1. - rho * rho));
float tmp3 = SIGMA_DICT / sigma_y;
//=========================================================================
//============================= Contribution 2 ============================
//======================= Slope correlation factor ========================
//============================== Equation 18 ==============================
// Former inverse transformation matrix (Chermain et al. 2020) was:
// SIGMA_DICT * float2x2( 1. / sigma_x, 0. ,
// 0. , 1. / sigma_y)
//=========================================================================
float2x2 invM = float2x2(tmp1, 0., // first column
tmp2, tmp3); // second column // This order should be correct with the following matrix multiply
//=========================================================================
//========================== END Contribution 2 ===========================
//=========================================================================
// Apply random rotation
float2x2 invR = float2x2(cosTheta, -sinTheta, // first column
sinTheta, cosTheta); // second column
invM = mul(invM, invR);
// Get back to original space
// Equation 5
float2 slope_h_o = mul(slope_h, invM);
// The SDF is an even function
float2 abs_slope_h_o = float2(abs(slope_h_o.x), abs(slope_h_o.y));
int distPerChannel = GlintLutParams.N / 3;
float alpha_dist_isqrt2_4 = GlintLutParams.Alpha * InvSqrt2 * 4.f;
// After 4 standard deviations, the SDF equals zero
if (abs_slope_h_o.x > alpha_dist_isqrt2_4
|| abs_slope_h_o.y > alpha_dist_isqrt2_4)
return 0.f;
float u1 = hashIQ(rngSeed * 16807U);
float u2 = hashIQ(rngSeed * 48271U);
int i = int(u1 * float(GlintLutParams.N));
int j = int(u2 * float(GlintLutParams.N));
// 3 distributions values in one texel
int distIdxXOver3 = i / 3;
int distIdxYOver3 = j / 3;
float texCoordX = abs_slope_h_o.x / alpha_dist_isqrt2_4;
float texCoordY = abs_slope_h_o.y / alpha_dist_isqrt2_4;
// We also need to scale the derivatives,
// as slope_h, to maintained coherence
slope_dx = slope_dx / alpha_dist_isqrt2_4;
slope_dy = slope_dy / alpha_dist_isqrt2_4;
float3 P_20_o, P_02_o;
//=========================================================================
//=========================== Contribution 1 ==============================
//================================ GGAA ===================================
//=========================================================================
#if USE_GLINT_AA
if (GGAAFilter)
{
// As for the distribution, we transform the filtering kernel
float2 transformed_slope_dx = mul(slope_dx, invM);
float2 transformed_slope_dy = mul(slope_dy, invM);
// Scale the kernel by user parameter
transformed_slope_dx *= GGAAKernelSize;
transformed_slope_dy *= GGAAKernelSize;
P_20_o = View.GlintTexture.SampleGrad(
View.GlintSampler,
float3(texCoordX, 0.5, l_dist * distPerChannel + distIdxXOver3),
transformed_slope_dx.x,
transformed_slope_dy.x).rgb;
P_02_o = View.GlintTexture.SampleGrad(
View.GlintSampler,
float3(texCoordY, 0.5, l_dist * distPerChannel + distIdxYOver3),
transformed_slope_dx.y,
transformed_slope_dy.y).rgb;
}
//=========================================================================
//========================= END Contribution 1 ============================
//=========================================================================
else // Without geometric glint anti-aliasing
#endif
{
P_20_o = View.GlintTexture.SampleLevel(
View.GlintSampler,
float3(texCoordX, 0.5, l_dist * distPerChannel + distIdxXOver3),
0.0f).rgb;
P_02_o = View.GlintTexture.SampleLevel(
View.GlintSampler,
float3(texCoordY, 0.5, l_dist * distPerChannel + distIdxYOver3),
0.0f).rgb;
}
// Equation 15
const int Idx20 = int(fmod(i, 3)); // i % 3; // int(mod(i, 3));
const int Idx02 = int(fmod(j, 3)); // j % 3; // int(mod(j, 3));
return P_20_o[Idx20] * P_02_o[Idx02] * determinant(invM);
}
//=============================================================================
//===================== P-SDF for a discrete LOD ==============================
//=============================================================================
// Most of this function is similar to pbrt-v3 EWA function,
// which itself is similar to Heckbert 1889 algorithm,
// http://www.cs.cmu.edu/~ph/texfund/texfund.pdf, Section 3.5.9.
// Go through cells within the pixel footprint for a givin LOD
float P22__glint_discrete_LOD(
FGlintLutParams GlintLutParams,
int l, float2 slope_h, float2 st, float2 dst0,
float2 dst1, float2 slope_dx, float2 slope_dy,
float3 sigma_x_y_rho, float l_dist, float MicrofacetRelativeArea)
{
// Convert surface coordinates to appropriate scale for level
float pyrSize = pyramidSize(GlintLutParams, l);
st[0] = st[0] * pyrSize - 0.5f;
st[1] = st[1] * pyrSize - 0.5f;
dst0[0] *= pyrSize;
dst0[1] *= pyrSize;
dst1[0] *= pyrSize;
dst1[1] *= pyrSize;
// Compute ellipse coefficients to bound filter region
float A = dst0[1] * dst0[1] + dst1[1] * dst1[1] + 1.;
float B = -2. * (dst0[0] * dst0[1] + dst1[0] * dst1[1]);
float C = dst0[0] * dst0[0] + dst1[0] * dst1[0] + 1.;
float invF = 1. / (A * C - B * B * 0.25f);
A *= invF;
B *= invF;
C *= invF;
// Compute the ellipse's bounding box in texture space
float det = -B * B + 4 * A * C;
float invDet = 1 / det;
float uSqrt = sqrt(det * C), vSqrt = sqrt(A * det);
int s0 = int(ceil(st[0] - 2. * invDet * uSqrt));
int s1 = int(floor(st[0] + 2. * invDet * uSqrt));
int t0 = int(ceil(st[1] - 2. * invDet * vSqrt));
int t1 = int(floor(st[1] + 2. * invDet * vSqrt));
// Scan over ellipse bound and compute quadratic equation
float sum = 0.f;
float sumWts = 0;
int nbrOfIter = 0;
const int SafeMaxIterationCount = 100;
LOOP
for (int it = t0; it <= t1; ++it)
{
float tt = it - st[1];
LOOP
for (int is = s0; is <= s1; ++is)
{
float ss = is - st[0];
// Compute squared radius
// and filter SDF if inside ellipse
float r2 = A * ss * ss + B * ss * tt + C * tt * tt;
if (r2 < 1)
{
// Weighting function used in pbrt-v3 EWA function
float alpha = 2;
float W_P = exp(-alpha * r2) - exp(-alpha);
sum += P22_M(
GlintLutParams,
slope_h, l, is, it,
slope_dx, slope_dy,
sigma_x_y_rho, l_dist, MicrofacetRelativeArea) * W_P;
sumWts += W_P;
}
nbrOfIter++;
// Guardrail (Extremely rare case.)
if (nbrOfIter > SafeMaxIterationCount)
break;
}
// Guardrail (Extremely rare case.)
if (nbrOfIter > SafeMaxIterationCount)
break;
}
return sum / max(GlintSafeThreshold, sumWts);
}
//=============================================================================
//======== Evaluation of the procedural physically based glinty BRDF ==========
//=============================================================================
float f_P(float3 wo, float3 wi, float3 sigmas_rho, float GlintDensity, float2 TexCoord, float2 TexCoordDDX, float2 TexCoordDDY, bool bIncludeGeometryTerm=false)
{
if (wo.z <= 0.)
return 0;
if (wi.z <= 0.)
return 0;
float3 wh = normalize(wo + wi);
if (wh.z <= 0.)
return 0;
// Local masking shadowing
if (dot(wo, wh) <= 0. || dot(wi, wh) <= 0.)
return 0;
FGlintLutParams GlintLutParams = GetGlintParams();
// Clamp tex.coord to [-1..1] in order to avoid incorrect LUT look up. A value > 1, means a pixel footprint larger than 1 pixel,
// which causes the glints dot to be stretched, causing implausible pattern.
TexCoordDDX = clamp(TexCoordDDX, -1.f, 1.f);
TexCoordDDY = clamp(TexCoordDDY, -1.f, 1.f);
// Compute texture derivatives
float2 dst0 = TexCoordDDX;
float2 dst1 = TexCoordDDY;
// Normal to slope
float2 slope_h = float2(-wh.x / wh.z, -wh.y / wh.z);
float2 slope_dx = 0;
float2 slope_dy = 0;
#if USE_GLINT_AA
if (UseHemisDerivatives)
{
// Derivatives in the projected hemispherical domain
float2 projected_half_vector = float2(wh.x, wh.y);
slope_dx = ddx(projected_half_vector);
slope_dy = ddy(projected_half_vector);
}
else
{
slope_dx = ddx(slope_h);
slope_dy = ddy(slope_h);
}
#endif
float D_P = 0;
float P22_P = 0.;
//=========================================================================
// Similar to pbrt-v3 MIPMap::Lookup function,
// http://www.pbr-book.org/3ed-2018/Texture/Image_Texture.html#EllipticallyWeightedAverage
// Compute ellipse minor and major axes
float dst0LengthSquared = length(dst0);
dst0LengthSquared *= dst0LengthSquared;
float dst1LengthSquared = length(dst1);
dst1LengthSquared *= dst1LengthSquared;
if (dst0LengthSquared < dst1LengthSquared)
{
// Swap dst0 and dst1
float2 tmp = dst0;
dst0 = dst1;
dst1 = tmp;
}
float majorLength = length(dst0);
float minorLength = length(dst1);
// Clamp ellipse eccentricity if too large
if (minorLength * MaxAnisotropy < majorLength
&& minorLength > 0.)
{
float scale = majorLength / (minorLength * MaxAnisotropy);
dst1 *= scale;
minorLength *= scale;
}
//=========================================================================
// Without footprint -> no reflection
if (minorLength == 0)
{
D_P = 0;
}
else
{
// Reparameterize the glint density [0..1] into a LogMicrofacetDensity and MicrofacetRelativeArea value.
// LogMicrofacetDensity has a visible range of [~10..21]. With this we do the following remapping:
// * Range in [10..21] remaps to the expressable density
// * Range in [0..10] uses MicrofacetRelativeArea to fade progressively the glints.
const float LogMicrofacetDensity = lerp(0.f, 21.f, saturate(GlintDensity));
const float MicrofacetRelativeArea = saturate(LogMicrofacetDensity / 10.f);
// Choose LOD
float l = max(0.0, GlintLutParams.NLevels - 1. + log2(minorLength));
int il = int(floor(l));
float w = l - float(il);
// Number of microfacets in a cell at level il
float n_il = pow(2., float(2 * il - (2 * (GlintLutParams.NLevels - 1))));
n_il *= exp(LogMicrofacetDensity);
float LOD_dist_il = log(n_il) / 1.38629; // 2. * log(2) = 1.38629
// Number of microfacets in a cell at level il + 1
float n_ilp1 = pow(2., float(2 * (il + 1) - (2 * (GlintLutParams.NLevels - 1))));
n_ilp1 *= exp(LogMicrofacetDensity);
float LOD_dist_ilp1 = log(n_ilp1) / 1.38629; // 2. * log(2) = 1.38629
float P22_P_il = non_axis_aligned_anisotropic_beckmann(slope_h.x, slope_h.y, sigmas_rho.x, sigmas_rho.y, sigmas_rho.z);
float P22_P_ilp1 = P22_P_il;
// Apply here level space bias and clamp to a minimum. This before we evaluate each glint level.
LOD_dist_il = clamp(LOD_dist_il + GlintLutParams.LevelBias, 0, max(0, GlintLutParams.NLevels - GlintLutParams.LevelMin));
LOD_dist_ilp1 = clamp(LOD_dist_ilp1 + GlintLutParams.LevelBias, 0, max(0, GlintLutParams.NLevels - GlintLutParams.LevelMin));
bool opti = MicrofacetRelativeArea > 0.99;
if (int(round(LOD_dist_il)) < GlintLutParams.NLevels || !opti)
{
P22_P_il = P22__glint_discrete_LOD(GlintLutParams, il, slope_h, TexCoord, dst0, dst1, slope_dx, slope_dy, sigmas_rho, LOD_dist_il, MicrofacetRelativeArea);
}
if (int(round(LOD_dist_ilp1)) < GlintLutParams.NLevels || !opti)
{
P22_P_ilp1 = P22__glint_discrete_LOD(GlintLutParams, il + 1, slope_h, TexCoord, dst0, dst1, slope_dx, slope_dy, sigmas_rho, LOD_dist_ilp1, MicrofacetRelativeArea);
}
P22_P = lerp(P22_P_il, P22_P_ilp1, w);
D_P = P22_P / (wh.z * wh.z * wh.z * wh.z);
}
// // V-cavity masking shadowing
// float G1wowh = min(1., 2. * wh.z * wo.z / dot(wo, wh));
// float G1wiwh = min(1., 2. * wh.z * wi.z / dot(wi, wh));
// float G = G1wowh * G1wiwh; // TODO check
//
// // Fresnel is set to one for simplicity
// // but feel free to use "real" Fresnel term
// float3 F = float3(1.0f, 1.0f, 1.0f); // TODO check
//
// // (wi dot wg) is cancelled by
// // the cosine weight in the rendering equation
// return (F * G * D_P) / (4. * wo.z);
float G = 1.f;
if (bIncludeGeometryTerm)
{
float G1wowh = min(1., 2. * wh.z * wo.z / dot(wo, wh));
float G1wiwh = min(1., 2. * wh.z * wi.z / dot(wi, wh));
G = G1wowh * G1wiwh; // TODO check
}
return G * D_P / (4. * wo.z);
}
// Glint Sampling
// Returns the ith 1D distribution at level l. It uses two random
// numbers: one for sampling the positive 1D distribution, the other to
// inverse the sign of the sampled value.
float Sample_P_1D(FGlintLutParams GlintLutParams, float u, int ij, int l)
{
// The distribution equals 0 after 4 standard deviations.
// Slope standard deviation = alpha / sqrt(2)
// 3 distributions values in one texel
int distIdxOver3 = ij / 3;
int distPerChannel = GlintLutParams.N / 3;
float alpha_dist_isqrt2_4 = GlintLutParams.Alpha * InvSqrt2 * 4.f;
// The 1D distribution is an even function
// [-1, 1]
float SignScale = 1;
if (u < 0.5)
{
SignScale *= -1.;
u = RescaleRandomNumber(u, 0.0, 0.5);
}
else
{
u = RescaleRandomNumber(u, 0.5, 1.f);
}
// Equation 15
const int Idx = int(fmod(ij, 3)); // ij % 3; // int(mod(ij, 3))
// Slow version - the CDF is computed inline within the shader (TODO precompute CDF textures)
uint it;
const uint EntryCount = 64;
float Cdfs[EntryCount+1];
float Acc = 0;
LOOP
for (it=0;it<EntryCount;++it)
{
const float texCoordX = saturate(float(it) / float(EntryCount-1));
float V = View.GlintTexture.SampleLevel(View.GlintSampler, float3(texCoordX, 0.5, l * distPerChannel + distIdxOver3), 0.0f)[Idx];
Cdfs[it] = Acc;
Acc += V;
}
Acc = max(0.0001f, Acc);
LOOP
for (it = 0; it < EntryCount; ++it)
{
Cdfs[it] /= Acc;
}
Cdfs[EntryCount] = 1.f;
// Samples the positive part of the 1D distribution
// Sampled value is in [0, 1)
float Sample = 0;
LOOP
for (it = 0; it < EntryCount; ++it)
{
if (Cdfs[it] <= u && u <= Cdfs[it + 1])
{
const float s = (u - Cdfs[it]) / max(0.0001f, (Cdfs[it+1]-Cdfs[it]));
Sample = saturate((it + s) / float(EntryCount));
break;
}
}
// Output is in [-alpha_dist_isqrt2_4, alpha_dist_isqrt2_4]
return SignScale * Sample * alpha_dist_isqrt2_4;
}
//=============================================================================
//===================== Spatially-varying, multiscale, ========================
//=============== and transformed slope distribution function ================
//=============================== Equation 4 ==================================
//=============================================================================
float2 Sample_P22_M(FGlintLutParams GlintLutParams, float2 u01, int l, int s0, int t0, float3 sigma_x_y_rho, float l_dist)
{
// Coherent index
int twoToTheL = int(pow(2., float(l)));
s0 *= twoToTheL;
t0 *= twoToTheL;
// Seed pseudo random generator
uint rngSeed = uint(s0 + 1549 * t0);
float uDensityRandomisation = hashIQ(rngSeed * 2171U);
// Fix density randomisation to 2 to have better appearance
float densityRandomisation = 2.;
// Sample a Gaussian to randomise the distribution LOD around the
// distribution level l_dist
l_dist = sampleNormalDistribution(uDensityRandomisation, l_dist, densityRandomisation);
l_dist = clamp(int(round(l_dist)), 0, GlintLutParams.NLevels);
// Recover roughness and slope correlation factor
float sigma_x = sigma_x_y_rho.x;
float sigma_y = sigma_x_y_rho.y;
float rho = sigma_x_y_rho.z;
// If we are too far from the surface, the SDF is a gaussian.
if (l_dist == GlintLutParams.NLevels)
{
return sampleBivariateNormalDistribution(u01, sigma_x, sigma_y, rho);
}
// Random rotations to remove glint alignment
float uTheta = hashIQ(rngSeed);
float theta = 2.0 * PI * uTheta;
float cosTheta = cos(theta);
float sinTheta = sin(theta);
// Linear transformation represented by matrix M
float SIGMA_DICT = GlintLutParams.Alpha * InvSqrt2;
float M00 = 1. / SIGMA_DICT * (sigma_x * sqrt(1. - rho * rho));
float M01 = 1. / SIGMA_DICT * rho * sigma_y;
float M10 = 0.;
float M11 = 1. / SIGMA_DICT * sigma_y;
float MR00 = cosTheta * M00 + sinTheta * M01;
float MR01 = -sinTheta * M00 + cosTheta * M01;
float MR10 = cosTheta * M10 + sinTheta * M11;
float MR11 = -sinTheta * M10 + cosTheta * M11;
float u1 = hashIQ(rngSeed * 16807U);
float u2 = hashIQ(rngSeed * 48271U);
int i = int(u1 * float(GlintLutParams.N));
int j = int(u2 * float(GlintLutParams.N));
int il_dist = l_dist;
// Sample original (dictionary) slope space
const float slopeHXO = Sample_P_1D(GlintLutParams, u01.x, i, il_dist);
const float slopeHYO = Sample_P_1D(GlintLutParams, u01.y, j, il_dist);
float2 slope_h = float2(
slopeHXO * MR00 + slopeHYO * MR01,
slopeHXO * MR10 + slopeHYO * MR11);
return slope_h;
}
// Most of this function is similar to pbrt-v3 EWA function,
// which itself is similar to Heckbert 1889 algorithm,
// http://www.cs.cmu.edu/~ph/texfund/texfund.pdf, Section 3.5.9.
// Go through cells within the pixel footprint for a givin LOD
float2 Sample_P22_LOD(
FGlintLutParams GlintLutParams,
float u0, float u1, float u2,
int l, float2 st,
float2 dst0, float2 dst1,
float3 sigma_x_y_rho,
float l_dist)
{
// Convert surface coordinates to appropriate scale for level
float pyrSize = pyramidSize(GlintLutParams, l);
st[0] = st[0] * pyrSize - 0.5f;
st[1] = st[1] * pyrSize - 0.5f;
dst0[0] *= pyrSize;
dst0[1] *= pyrSize;
dst1[0] *= pyrSize;
dst1[1] *= pyrSize;
// Compute ellipse coefficients to bound filter region
float A = dst0[1] * dst0[1] + dst1[1] * dst1[1] + 1.;
float B = -2. * (dst0[0] * dst0[1] + dst1[0] * dst1[1]);
float C = dst0[0] * dst0[0] + dst1[0] * dst1[0] + 1.;
float invF = 1. / (A * C - B * B * 0.25f);
A *= invF;
B *= invF;
C *= invF;
// Compute the ellipse's bounding box in texture space
float det = -B * B + 4 * A * C;
float invDet = 1 / det;
float uSqrt = sqrt(det * C), vSqrt = sqrt(A * det);
int s0 = int(ceil(st[0] - 2. * invDet * uSqrt));
int s1 = int(floor(st[0] + 2. * invDet * uSqrt));
int t0 = int(ceil(st[1] - 2. * invDet * vSqrt));
int t1 = int(floor(st[1] + 2. * invDet * vSqrt));
// The size is in [1-4]x[1-4]
int SizeT = t1-t0;
int SizeS = s1-s0;
// Scan over ellipse bound and compute quadratic equation
// 1. Count valid cells
// Cell picking is done over valid cell rather actual EWA weight
// The foot print is at max 4x4
uint2 selected_st = 0;
{
#define MAX_CELL_COUNT 64
float Cdfs[MAX_CELL_COUNT + 1];
uint2 Indices[MAX_CELL_COUNT];
float Acc = 0;
uint ValidCellCount = 0;
const int SafeMaxIterationCount = 100;
// 1.1 Build CDF
int nbrOfIter = 0;
uint ValidCount = 0;
LOOP
for (int it = t0; it <= t1; ++it)
{
float tt = it - st[1];
LOOP
for (int is = s0; is <= s1; ++is)
{
float ss = is - st[0];
// Compute squared radius
// and filter SDF if inside ellipse
float r2 = A * ss * ss + B * ss * tt + C * tt * tt;
if (r2 < 1)
{
const uint Index = min(ValidCellCount, MAX_CELL_COUNT);
// Weighting function used in pbrt-v3 EWA function
float alpha = 2;
float W_P = exp(-alpha * r2) - exp(-alpha);
Cdfs[Index] = Acc;
Indices[Index] = uint2(is, it);
Acc += W_P;
++ValidCellCount;
}
nbrOfIter++;
// Guardrail (Extremely rare case.)
if (nbrOfIter > SafeMaxIterationCount)
break;
}
// Guardrail (Extremely rare case.)
if (nbrOfIter > SafeMaxIterationCount)
break;
}
ValidCellCount = min(ValidCellCount, MAX_CELL_COUNT);
// 1.2 Normalized CDF
uint CellIt;
LOOP
for (CellIt = 0; CellIt < ValidCellCount; ++CellIt)
{
Cdfs[CellIt] /= max(Acc, 0.0001f);
}
Cdfs[ValidCellCount] = 1;
// 1.3 Sample CDF
LOOP
for (CellIt = 0; CellIt < ValidCellCount; ++CellIt)
{
if (Cdfs[CellIt] <= u0 && u0 <= Cdfs[CellIt + 1])
{
selected_st = Indices[CellIt];
break;
}
}
}
// 2. Sample valid cell and return slope_h
return Sample_P22_M(GlintLutParams, float2(u0, u1), l, selected_st.x, selected_st.y, sigma_x_y_rho, l_dist);
}
float3 Sample_P(float3 u, float3 wo, float3 sigmas_rho, float LogMicrofacetDensity, float2 TexCoord, float2 TexCoordDDX, float2 TexCoordDDY)
{
FGlintLutParams GlintLutParams = GetGlintParams();
float U0 = u.x;
float U1 = u.y;
float U2 = u.z;
// Compute texture derivatives
float2 texCoord = TexCoord;
float2 dst0 = TexCoordDDX;
float2 dst1 = TexCoordDDY;
float3 OutWm = float3(0,0,1); // z+
if (wo.z <= 0.)
{
return OutWm;
}
float3 D_P = float3(0.0f, 0.0f, 0.0f);
float P22_P = 0.;
//=========================================================================
// Similar to pbrt-v3 MIPMap::Lookup function,
// http://www.pbr-book.org/3ed-2018/Texture/Image_Texture.html#EllipticallyWeightedAverage
// Compute ellipse minor and major axes
float dst0LengthSquared = dot(dst0, dst0);
float dst1LengthSquared = dot(dst1, dst1);
if (dst0LengthSquared < dst1LengthSquared)
{
Swap(dst0, dst1);
}
float majorLength = length(dst0);
float minorLength = length(dst1);
// Clamp ellipse eccentricity if too large
if (minorLength * MaxAnisotropy < majorLength && minorLength > 0.)
{
float scale = majorLength / (minorLength * MaxAnisotropy);
dst1 *= scale;
minorLength *= scale;
}
//=========================================================================
// Compute LOD
if (minorLength > 0)
{
const float l = max(0.0, GlintLutParams.NLevels - 1. + log2(minorLength));
const int il = int(floor(l));
const float wLOD = l - float(il);
// Number of microfacets in a cell at level il
float n_il = pow(2., float(2 * il - (2 * (GlintLutParams.NLevels - 1))));
n_il *= exp(LogMicrofacetDensity);
float LOD_dist_il = log(n_il) / 1.38629; // 2. * log(2) = 1.38629
// Number of microfacets in a cell at level il + 1
float n_ilp1 = pow(2., float(2 * (il + 1) - (2 * (GlintLutParams.NLevels - 1))));
n_ilp1 *= exp(LogMicrofacetDensity);
float LOD_dist_ilp1 = log(n_ilp1) / 1.38629; // 2. * log(2) = 1.38629
// Apply here level space bias and clamp to a minimum. This before we evaluate each glint level.
LOD_dist_il = clamp(LOD_dist_il + GlintLutParams.LevelBias, 0, max(0, GlintLutParams.NLevels - GlintLutParams.LevelMin));
LOD_dist_ilp1 = clamp(LOD_dist_ilp1 + GlintLutParams.LevelBias, 0, max(0, GlintLutParams.NLevels - GlintLutParams.LevelMin));
// 1. Select LOD (N or N+1)
uint LOD = il;
float LODDist = LOD_dist_il;
if (U2 < wLOD)
{
U2 = RescaleRandomNumber(U2, 0.0, wLOD);
LOD = il;
LODDist = LOD_dist_il;
}
else
{
U2 = RescaleRandomNumber(U2, wLOD, 1.0);
LOD = il + 1;
LODDist = LOD_dist_il + 1;
}
// 2. Select cell and sample distribution
if (int(round(LODDist)) < GlintLutParams.NLevels)
{
const float2 slope_h = Sample_P22_LOD(GlintLutParams, U0, U1, U2, LOD, texCoord, dst0, dst1, sigmas_rho, LODDist);
// Build micro normal and its V-cavity dual
const float3 Wm = SlopeToNormal(slope_h);
const float3 WmP = float3(-Wm.x, -Wm.y, Wm.z);
// Projected area of the other facet of the V-cavity
const float dotWOWMP = max(dot(wo, WmP), 0);
const float dotWOWM = max(dot(wo, Wm), 0);
const float projectedAreaWMP = dotWOWMP / max(0.0001f, dotWOWM + dotWOWMP);
// Samples a facet of the V-cavity
if (U2 < projectedAreaWMP)
OutWm = WmP;
else
OutWm = Wm;
}
}
return OutWm;
}