Files
2026-07-13 13:13:17 +08:00

112 lines
4.0 KiB
HLSL

// Physically Based Rendering
// Copyright (c) 2017-2018 Michał Siejak
// Pre-integrates Cook-Torrance specular BRDF for varying roughness and viewing directions.
// Results are saved into 2D LUT texture in the form of DFG1 and DFG2 split-sum approximation terms,
// which act as a scale and bias to F0 (Fresnel reflectance at normal incidence) during rendering.
static const float PI = 3.141592;
static const float TwoPI = 2 * PI;
static const float Epsilon = 0.001; // This program needs larger eps.
static const uint NumSamples = 1024;
static const float InvNumSamples = 1.0 / float(NumSamples);
RWTexture2D<float4> LUT : register(u0);
// Compute Van der Corput radical inverse
// See: http://holger.dammertz.org/stuff/notes_HammersleyOnHemisphere.html
float radicalInverse_VdC(uint bits)
{
bits = (bits << 16u) | (bits >> 16u);
bits = ((bits & 0x55555555u) << 1u) | ((bits & 0xAAAAAAAAu) >> 1u);
bits = ((bits & 0x33333333u) << 2u) | ((bits & 0xCCCCCCCCu) >> 2u);
bits = ((bits & 0x0F0F0F0Fu) << 4u) | ((bits & 0xF0F0F0F0u) >> 4u);
bits = ((bits & 0x00FF00FFu) << 8u) | ((bits & 0xFF00FF00u) >> 8u);
return float(bits) * 2.3283064365386963e-10; // / 0x100000000
}
// Sample i-th point from Hammersley point set of NumSamples points total.
float2 sampleHammersley(uint i)
{
return float2(i * InvNumSamples, radicalInverse_VdC(i));
}
// Importance sample GGX normal distribution function for a fixed roughness value.
// This returns normalized half-vector between Li & Lo.
// For derivation see: http://blog.tobias-franke.eu/2014/03/30/notes_on_importance_sampling.html
float3 sampleGGX(float u1, float u2, float roughness)
{
float alpha = roughness * roughness;
float cosTheta = sqrt((1.0 - u2) / (1.0 + (alpha*alpha - 1.0) * u2));
float sinTheta = sqrt(1.0 - cosTheta*cosTheta); // Trig. identity
float phi = TwoPI * u1;
// Convert to Cartesian upon return.
return float3(sinTheta * cos(phi), sinTheta * sin(phi), cosTheta);
}
// Single term for separable Schlick-GGX below.
float gaSchlickG1(float cosTheta, float k)
{
return cosTheta / (cosTheta * (1.0 - k) + k);
}
// Schlick-GGX approximation of geometric attenuation function using Smith's method (IBL version).
float gaSchlickGGX_IBL(float cosLi, float cosLo, float roughness)
{
float r = roughness;
float k = (r * r) / 2.0; // Epic suggests using this roughness remapping for IBL lighting.
return gaSchlickG1(cosLi, k) * gaSchlickG1(cosLo, k);
}
[numthreads(32, 32, 1)]
void main(uint2 ThreadID : SV_DispatchThreadID)
{
// Get output LUT dimensions.
float outputWidth, outputHeight;
LUT.GetDimensions(outputWidth, outputHeight);
// Get integration parameters.
float cosLo = ThreadID.x / outputWidth;
float roughness = ThreadID.y / outputHeight;
// Make sure viewing angle is non-zero to avoid divisions by zero (and subsequently NaNs).
cosLo = max(cosLo, Epsilon);
// Derive tangent-space viewing vector from angle to normal (pointing towards +Z in this reference frame).
float3 Lo = float3(sqrt(1.0 - cosLo*cosLo), 0.0, cosLo);
// We will now pre-integrate Cook-Torrance BRDF for a solid white environment and save results into a 2D LUT.
// DFG1 & DFG2 are terms of split-sum approximation of the reflectance integral.
// For derivation see: "Moving Frostbite to Physically Based Rendering 3.0", SIGGRAPH 2014, section 4.9.2.
float DFG1 = 0;
float DFG2 = 0;
for(uint i=0; i<NumSamples; ++i) {
float2 u = sampleHammersley(i);
// Sample directly in tangent/shading space since we don't care about reference frame as long as it's consistent.
float3 Lh = sampleGGX(u.x, u.y, roughness);
// Compute incident direction (Li) by reflecting viewing direction (Lo) around half-vector (Lh).
float3 Li = 2.0 * dot(Lo, Lh) * Lh - Lo;
float cosLi = Li.z;
float cosLh = Lh.z;
float cosLoLh = max(dot(Lo, Lh), 0.0);
if(cosLi > 0.0) {
float G = gaSchlickGGX_IBL(cosLi, cosLo, roughness);
float Gv = G * cosLoLh / (cosLh * cosLo);
float Fc = pow(1.0 - cosLoLh, 5);
DFG1 += (1 - Fc) * Gv;
DFG2 += Fc * Gv;
}
}
LUT[ThreadID] = float4(float2(DFG1, DFG2) * InvNumSamples, 0, 1) ;
}