974 lines
38 KiB
C
974 lines
38 KiB
C
/* Creative Commons CC0 Public Domain. To the extent possible under law,
|
|
Jakub Boksansky has waived all copyright and related or neighboring rights
|
|
to Crash Course in BRDF Implementation Code Sample.
|
|
This work is published from: Germany. */
|
|
|
|
// This is a code sample accompanying the "Crash Course in BRDF Implementation" article
|
|
// v1.1, February 2021
|
|
|
|
// Xash3D amendments:
|
|
// - remove c++ compat
|
|
// - port to glsl
|
|
|
|
float rsqrt(float x) { return inversesqrt(x); }
|
|
float saturate(float x) { return clamp(x, 0.0f, 1.0f); }
|
|
vec3 saturate(vec3 x) { return clamp(x, vec3(0.0f), vec3(1.0f)); }
|
|
|
|
#define OUT_PARAMETER(X) out X
|
|
|
|
// -------------------------------------------------------------------------
|
|
// Constant Definitions
|
|
// -------------------------------------------------------------------------
|
|
|
|
#define NONE 0
|
|
|
|
// NDF definitions
|
|
#define GGX 1
|
|
#define BECKMANN 2
|
|
|
|
// Specular BRDFs
|
|
#define MICROFACET 1
|
|
#define PHONG 2
|
|
|
|
// Diffuse BRDFs
|
|
#define LAMBERTIAN 1
|
|
#define OREN_NAYAR 2
|
|
#define DISNEY 3
|
|
#define FROSTBITE 4
|
|
|
|
// BRDF types
|
|
#define DIFFUSE_TYPE 1
|
|
#define SPECULAR_TYPE 2
|
|
|
|
// PIs
|
|
#ifndef PI
|
|
#define PI 3.141592653589f
|
|
#endif
|
|
|
|
#ifndef TWO_PI
|
|
#define TWO_PI (2.0f * PI)
|
|
#endif
|
|
|
|
#ifndef ONE_OVER_PI
|
|
#define ONE_OVER_PI (1.0f / PI)
|
|
#endif
|
|
|
|
#ifndef ONE_OVER_TWO_PI
|
|
#define ONE_OVER_TWO_PI (1.0f / TWO_PI)
|
|
#endif
|
|
|
|
// -------------------------------------------------------------------------
|
|
// Configuration macros (user editable - set your preferences here)
|
|
// -------------------------------------------------------------------------
|
|
|
|
// Specify what NDF (GGX or BECKMANN you want to use)
|
|
#ifndef MICROFACET_DISTRIBUTION
|
|
#define MICROFACET_DISTRIBUTION GGX
|
|
//#define MICROFACET_DISTRIBUTION BECKMANN
|
|
#endif
|
|
|
|
// Specify default specular and diffuse BRDFs
|
|
#ifndef SPECULAR_BRDF
|
|
#define SPECULAR_BRDF MICROFACET
|
|
//#define SPECULAR_BRDF PHONG
|
|
//#define SPECULAR_BRDF NONE
|
|
#endif
|
|
|
|
#ifndef DIFFUSE_BRDF
|
|
#define DIFFUSE_BRDF LAMBERTIAN
|
|
//#define DIFFUSE_BRDF OREN_NAYAR
|
|
//#define DIFFUSE_BRDF DISNEY
|
|
//#define DIFFUSE_BRDF FROSTBITE
|
|
//#define DIFFUSE_BRDF NONE
|
|
#endif
|
|
|
|
// Specifies minimal reflectance for dielectrics (when metalness is zero)
|
|
// Nothing has lower reflectance than 2%, but we use 4% to have consistent results with UE4, Frostbite, et al.
|
|
#define MIN_DIELECTRICS_F0 0.04f
|
|
|
|
// Enable this to weigh diffuse by Fresnel too, otherwise specular and diffuse will be simply added together
|
|
// (this is disabled by default for Frostbite diffuse which is normalized to combine well with GGX Specular BRDF)
|
|
#if DIFFUSE_BRDF != FROSTBITE
|
|
#define COMBINE_BRDFS_WITH_FRESNEL 1
|
|
#endif
|
|
|
|
// Uncomment this to use "general" version of G1 which is not optimized and uses NDF-specific G_Lambda (can be useful for experimenting and debugging)
|
|
//#define Smith_G1 Smith_G1_General
|
|
|
|
// Enable optimized G2 implementation which includes division by specular BRDF denominator (not available for all NDFs, check macro G2_DIVIDED_BY_DENOMINATOR if it was actually used)
|
|
#define USE_OPTIMIZED_G2 1
|
|
|
|
// Enable height correlated version of G2 term. Separable version will be used otherwise
|
|
#define USE_HEIGHT_CORRELATED_G2 1
|
|
|
|
// -------------------------------------------------------------------------
|
|
// Automatically resolved macros based on preferences (don't edit these)
|
|
// -------------------------------------------------------------------------
|
|
|
|
// Select distribution function
|
|
#if MICROFACET_DISTRIBUTION == GGX
|
|
#define Microfacet_D GGX_D
|
|
#elif MICROFACET_DISTRIBUTION == BECKMANN
|
|
#define Microfacet_D Beckmann_D
|
|
#endif
|
|
|
|
// Select G functions (masking/shadowing) depending on selected distribution
|
|
#if MICROFACET_DISTRIBUTION == GGX
|
|
#define Smith_G_Lambda Smith_G_Lambda_GGX
|
|
#elif MICROFACET_DISTRIBUTION == BECKMANN
|
|
#define Smith_G_Lambda Smith_G_Lambda_Beckmann_Walter
|
|
#endif
|
|
|
|
#ifndef Smith_G1
|
|
// Define version of G1 optimized specifically for selected NDF
|
|
#if MICROFACET_DISTRIBUTION == GGX
|
|
#define Smith_G1 Smith_G1_GGX
|
|
#elif MICROFACET_DISTRIBUTION == BECKMANN
|
|
#define Smith_G1 Smith_G1_Beckmann_Walter
|
|
#endif
|
|
#endif
|
|
|
|
// Select default specular and diffuse BRDF functions
|
|
#if SPECULAR_BRDF == MICROFACET
|
|
#define evalSpecular evalMicrofacet
|
|
#define sampleSpecular sampleSpecularMicrofacet
|
|
#if MICROFACET_DISTRIBUTION == GGX
|
|
#define sampleSpecularHalfVector sampleGGXVNDF
|
|
#else
|
|
#define sampleSpecularHalfVector sampleBeckmannWalter
|
|
#endif
|
|
#elif SPECULAR_BRDF == PHONG
|
|
#define evalSpecular evalPhong
|
|
#define sampleSpecular sampleSpecularPhong
|
|
#define sampleSpecularHalfVector samplePhong
|
|
#else
|
|
#define evalSpecular evalVoid
|
|
#define sampleSpecular sampleSpecularVoid
|
|
#define sampleSpecularHalfVector sampleSpecularHalfVectorVoid
|
|
#endif
|
|
|
|
#if MICROFACET_DISTRIBUTION == GGX
|
|
#define specularSampleWeight specularSampleWeightGGXVNDF
|
|
#define specularPdf sampleGGXVNDFReflectionPdf
|
|
#else
|
|
#define specularSampleWeight specularSampleWeightBeckmannWalter
|
|
#define specularPdf sampleBeckmannWalterReflectionPdf
|
|
#endif
|
|
|
|
#if DIFFUSE_BRDF == LAMBERTIAN
|
|
#define evalDiffuse evalLambertian
|
|
#define diffuseTerm lambertian
|
|
#elif DIFFUSE_BRDF == OREN_NAYAR
|
|
#define evalDiffuse evalOrenNayar
|
|
#define diffuseTerm orenNayar
|
|
#elif DIFFUSE_BRDF == DISNEY
|
|
#define evalDiffuse evalDisneyDiffuse
|
|
#define diffuseTerm disneyDiffuse
|
|
#elif DIFFUSE_BRDF == FROSTBITE
|
|
#define evalDiffuse evalFrostbiteDisneyDiffuse
|
|
#define diffuseTerm frostbiteDisneyDiffuse
|
|
#else
|
|
#define evalDiffuse evalVoid
|
|
#define evalIndirectDiffuse evalIndirectVoid
|
|
#define diffuseTerm none
|
|
#endif
|
|
|
|
// -------------------------------------------------------------------------
|
|
// Structures
|
|
// -------------------------------------------------------------------------
|
|
|
|
struct MaterialProperties
|
|
{
|
|
vec3 baseColor;
|
|
float metalness;
|
|
|
|
vec3 emissive;
|
|
float roughness;
|
|
|
|
//float transmissivness;
|
|
//float opacity;
|
|
};
|
|
|
|
// Data needed to evaluate BRDF (surface and material properties at given point + configuration of light and normal vectors)
|
|
struct BrdfData
|
|
{
|
|
// Material properties
|
|
vec3 specularF0;
|
|
vec3 diffuseReflectance;
|
|
|
|
// Roughnesses
|
|
float roughness; //< perceptively linear roughness (artist's input)
|
|
float alpha; //< linear roughness - often 'alpha' in specular BRDF equations
|
|
float alphaSquared; //< alpha squared - pre-calculated value commonly used in BRDF equations
|
|
|
|
// Commonly used terms for BRDF evaluation
|
|
vec3 F; //< Fresnel term
|
|
|
|
// Vectors
|
|
vec3 V; //< Direction to viewer (or opposite direction of incident ray)
|
|
vec3 N; //< Shading normal
|
|
vec3 H; //< Half vector (microfacet normal)
|
|
vec3 L; //< Direction to light (or direction of reflecting ray)
|
|
|
|
float NdotL;
|
|
float NdotV;
|
|
|
|
float LdotH;
|
|
float NdotH;
|
|
float VdotH;
|
|
|
|
// True when V/L is backfacing wrt. shading normal N
|
|
bool Vbackfacing;
|
|
bool Lbackfacing;
|
|
};
|
|
|
|
// -------------------------------------------------------------------------
|
|
// Utilities
|
|
// -------------------------------------------------------------------------
|
|
|
|
// Converts Phong's exponent (shininess) to Beckmann roughness (alpha)
|
|
// Source: "Microfacet Models for Refraction through Rough Surfaces" by Walter et al.
|
|
float shininessToBeckmannAlpha(float shininess) {
|
|
return sqrt(2.0f / (shininess + 2.0f));
|
|
}
|
|
|
|
// Converts Beckmann roughness (alpha) to Phong's exponent (shininess)
|
|
// Source: "Microfacet Models for Refraction through Rough Surfaces" by Walter et al.
|
|
float beckmannAlphaToShininess(float alpha) {
|
|
return 2.0f / min(0.9999f, max(0.0002f, (alpha * alpha))) - 2.0f;
|
|
}
|
|
|
|
// Converts Beckmann roughness (alpha) to Oren-Nayar roughness (sigma)
|
|
// Source: "Moving Frostbite to Physically Based Rendering" by Lagarde & de Rousiers
|
|
float beckmannAlphaToOrenNayarRoughness(float alpha) {
|
|
return 0.7071067f * atan(alpha);
|
|
}
|
|
|
|
float luminance(vec3 rgb)
|
|
{
|
|
return dot(rgb, vec3(0.2126f, 0.7152f, 0.0722f));
|
|
}
|
|
|
|
vec3 baseColorToSpecularF0(vec3 baseColor, float metalness) {
|
|
return mix(vec3(MIN_DIELECTRICS_F0, MIN_DIELECTRICS_F0, MIN_DIELECTRICS_F0), baseColor, metalness);
|
|
}
|
|
|
|
vec3 baseColorToDiffuseReflectance(vec3 baseColor, float metalness)
|
|
{
|
|
return baseColor * (1.0f - metalness);
|
|
}
|
|
|
|
float none(const BrdfData data) {
|
|
return 0.0f;
|
|
}
|
|
|
|
vec3 evalVoid(const BrdfData data) {
|
|
return vec3(0.0f, 0.0f, 0.0f);
|
|
}
|
|
|
|
void evalIndirectVoid(const BrdfData data, vec2 u, OUT_PARAMETER(vec3) rayDirection, OUT_PARAMETER(vec3) weight) {
|
|
rayDirection = vec3(0.0f, 0.0f, 1.0f);
|
|
weight = vec3(0.0f, 0.0f, 0.0f);
|
|
}
|
|
|
|
vec3 sampleSpecularVoid(vec3 Vlocal, float alpha, float alphaSquared, vec3 specularF0, vec2 u, OUT_PARAMETER(vec3) weight) {
|
|
weight = vec3(0.0f, 0.0f, 0.0f);
|
|
return vec3(0.0f, 0.0f, 0.0f);
|
|
}
|
|
|
|
vec3 sampleSpecularHalfVectorVoid(vec3 Vlocal, vec2 alpha2D, vec2 u) {
|
|
return vec3(0.0f, 0.0f, 0.0f);
|
|
}
|
|
|
|
// -------------------------------------------------------------------------
|
|
// Quaternion rotations
|
|
// -------------------------------------------------------------------------
|
|
|
|
// Calculates rotation quaternion from input vector to the vector (0, 0, 1)
|
|
// Input vector must be normalized!
|
|
vec4 getRotationToZAxis(vec3 v) {
|
|
|
|
// Handle special case when input is exact or near opposite of (0, 0, 1)
|
|
if (v.z < -0.99999f) return vec4(1.0f, 0.0f, 0.0f, 0.0f);
|
|
|
|
return normalize(vec4(v.y, -v.x, 0.0f, 1.0f + v.z));
|
|
}
|
|
|
|
// Calculates rotation quaternion from vector (0, 0, 1) to the input vector
|
|
// Input vector must be normalized!
|
|
vec4 getRotationFromZAxis(vec3 v) {
|
|
|
|
// Handle special case when input is exact or near opposite of (0, 0, 1)
|
|
if (v.z < -0.99999f) return vec4(1.0f, 0.0f, 0.0f, 0.0f);
|
|
|
|
return normalize(vec4(-v.y, v.x, 0.0f, 1.0f + v.z));
|
|
}
|
|
|
|
// Returns the quaternion with inverted rotation
|
|
vec4 invertRotation(vec4 q)
|
|
{
|
|
return vec4(-q.x, -q.y, -q.z, q.w);
|
|
}
|
|
|
|
// Optimized point rotation using quaternion
|
|
// Source: https://gamedev.stackexchange.com/questions/28395/rotating-vector3-by-a-quaternion
|
|
vec3 rotatePoint(vec4 q, vec3 v) {
|
|
const vec3 qAxis = vec3(q.x, q.y, q.z);
|
|
return 2.0f * dot(qAxis, v) * qAxis + (q.w * q.w - dot(qAxis, qAxis)) * v + 2.0f * q.w * cross(qAxis, v);
|
|
}
|
|
|
|
// -------------------------------------------------------------------------
|
|
// Sampling
|
|
// -------------------------------------------------------------------------
|
|
|
|
// Samples a direction within a hemisphere oriented along +Z axis with a cosine-weighted distribution
|
|
// Source: "Sampling Transformations Zoo" in Ray Tracing Gems by Shirley et al.
|
|
vec3 sampleHemisphere(vec2 u, OUT_PARAMETER(float) pdf) {
|
|
|
|
float a = sqrt(u.x);
|
|
float b = TWO_PI * u.y;
|
|
|
|
vec3 result = vec3(
|
|
a * cos(b),
|
|
a * sin(b),
|
|
sqrt(1.0f - u.x));
|
|
|
|
pdf = result.z * ONE_OVER_PI;
|
|
|
|
return result;
|
|
}
|
|
|
|
vec3 sampleHemisphere(vec2 u) {
|
|
float pdf;
|
|
return sampleHemisphere(u, pdf);
|
|
}
|
|
|
|
// For sampling of all our diffuse BRDFs we use cosine-weighted hemisphere sampling, with PDF equal to (NdotL/PI)
|
|
float diffusePdf(float NdotL) {
|
|
return NdotL * ONE_OVER_PI;
|
|
}
|
|
|
|
// -------------------------------------------------------------------------
|
|
// Fresnel
|
|
// -------------------------------------------------------------------------
|
|
|
|
// Schlick's approximation to Fresnel term
|
|
// f90 should be 1.0, except for the trick used by Schuler (see 'shadowedF90' function)
|
|
vec3 evalFresnelSchlick(vec3 f0, float f90, float NdotS)
|
|
{
|
|
return f0 + (f90 - f0) * pow(1.0f - NdotS, 5.0f);
|
|
}
|
|
|
|
// Schlick's approximation to Fresnel term calculated using spherical gaussian approximation
|
|
// Source: https://seblagarde.wordpress.com/2012/06/03/spherical-gaussien-approximation-for-blinn-phong-phong-and-fresnel/ by Lagarde
|
|
vec3 evalFresnelSchlickSphericalGaussian(vec3 f0, float f90, float NdotV)
|
|
{
|
|
return f0 + (f90 - f0) * exp2((-5.55473f * NdotV - 6.983146f) * NdotV);
|
|
}
|
|
|
|
// Schlick's approximation to Fresnel term with Hoffman's improvement using the Lazanyi's error term
|
|
// Source: "Fresnel Equations Considered Harmful" by Hoffman
|
|
// Also see slides http://renderwonk.com/publications/mam2019/naty_mam2019.pdf for examples and explanation of f82 term
|
|
vec3 evalFresnelHoffman(vec3 f0, float f82, float f90, float NdotS)
|
|
{
|
|
const float alpha = 6.0f; //< Fixed to 6 in order to put peak angle for Lazanyi's error term at 82 degrees (f82)
|
|
vec3 a = 17.6513846f * (f0 - f82) + 8.166666f * (vec3(1.0f, 1.0f, 1.0f) - f0);
|
|
return saturate(f0 + (f90 - f0) * pow(1.0f - NdotS, 5.0f) - a * NdotS * pow(1.0f - NdotS, alpha));
|
|
}
|
|
|
|
vec3 evalFresnel(vec3 f0, float f90, float NdotS)
|
|
{
|
|
// Default is Schlick's approximation
|
|
return evalFresnelSchlick(f0, f90, NdotS);
|
|
}
|
|
|
|
// Attenuates F90 for very low F0 values
|
|
// Source: "An efficient and Physically Plausible Real-Time Shading Model" in ShaderX7 by Schuler
|
|
// Also see section "Overbright highlights" in Hoffman's 2010 "Crafting Physically Motivated Shading Models for Game Development" for discussion
|
|
// IMPORTANT: Note that when F0 is calculated using metalness, it's value is never less than MIN_DIELECTRICS_F0, and therefore,
|
|
// this adjustment has no effect. To be effective, F0 must be authored separately, or calculated in different way. See main text for discussion.
|
|
float shadowedF90(vec3 F0) {
|
|
// This scaler value is somewhat arbitrary, Schuler used 60 in his article. In here, we derive it from MIN_DIELECTRICS_F0 so
|
|
// that it takes effect for any reflectance lower than least reflective dielectrics
|
|
//const float t = 60.0f;
|
|
const float t = (1.0f / MIN_DIELECTRICS_F0);
|
|
return min(1.0f, t * luminance(F0));
|
|
}
|
|
|
|
// -------------------------------------------------------------------------
|
|
// Lambert
|
|
// -------------------------------------------------------------------------
|
|
|
|
float lambertian(const BrdfData data) {
|
|
return 1.0f;
|
|
}
|
|
|
|
vec3 evalLambertian(const BrdfData data) {
|
|
return data.diffuseReflectance * (ONE_OVER_PI * data.NdotL);
|
|
}
|
|
|
|
// -------------------------------------------------------------------------
|
|
// Phong
|
|
// -------------------------------------------------------------------------
|
|
|
|
// For derivation see "Phong Normalization Factor derivation" by Giesen
|
|
float phongNormalizationTerm(float shininess) {
|
|
|
|
return (1.0f + shininess) * ONE_OVER_TWO_PI;
|
|
}
|
|
|
|
vec3 evalPhong(const BrdfData data) {
|
|
|
|
// First convert roughness to shininess (Phong exponent)
|
|
float shininess = beckmannAlphaToShininess(data.alpha);
|
|
|
|
vec3 R = reflect(-data.L, data.N);
|
|
return data.specularF0 * (phongNormalizationTerm(shininess) * pow(max(0.0f, dot(R, data.V)), shininess) * data.NdotL);
|
|
}
|
|
|
|
// Samples a Phong distribution lobe oriented along +Z axis
|
|
// Source: "Sampling Transformations Zoo" in Ray Tracing Gems by Shirley et al.
|
|
vec3 samplePhong(vec3 Vlocal, float shininess, vec2 u, OUT_PARAMETER(float) pdf) {
|
|
|
|
float cosTheta = pow(1.0f - u.x, 1.0f / (1.0f + shininess));
|
|
float sinTheta = sqrt(1.0f - cosTheta * cosTheta);
|
|
|
|
float phi = TWO_PI * u.y;
|
|
|
|
pdf = phongNormalizationTerm(shininess) * pow(cosTheta, shininess);
|
|
|
|
return vec3(
|
|
cos(phi) * sinTheta,
|
|
sin(phi) * sinTheta,
|
|
cosTheta);
|
|
}
|
|
|
|
vec3 samplePhong(vec3 Vlocal, vec2 alpha2D, vec2 u) {
|
|
float shininess = beckmannAlphaToShininess(dot(alpha2D, vec2(0.5f, 0.5f)));
|
|
float pdf;
|
|
return samplePhong(Vlocal, shininess, u, pdf);
|
|
}
|
|
|
|
// Sampling the specular BRDF based on Phong, includes normalization term
|
|
vec3 sampleSpecularPhong(vec3 Vlocal, float alpha, float alphaSquared, vec3 specularF0, vec2 u, OUT_PARAMETER(vec3) weight) {
|
|
|
|
// First convert roughness to shininess (Phong exponent)
|
|
float shininess = beckmannAlphaToShininess(alpha);
|
|
|
|
float pdf;
|
|
vec3 LPhong = samplePhong(Vlocal, shininess, u, pdf);
|
|
|
|
// Sampled LPhong is in "lobe space" - where Phong lobe is centered around +Z axis
|
|
// We need to rotate it in direction of perfect reflection
|
|
vec3 Nlocal = vec3(0.0f, 0.0f, 1.0f);
|
|
vec3 lobeDirection = reflect(-Vlocal, Nlocal);
|
|
vec3 Llocal = rotatePoint(getRotationFromZAxis(lobeDirection), LPhong);
|
|
|
|
// Calculate the weight of the sample
|
|
vec3 Rlocal = reflect(-Llocal, Nlocal);
|
|
float NdotL = max(0.00001f, dot(Nlocal, Llocal));
|
|
weight = max(vec3(0.0f, 0.0f, 0.0f), specularF0 * NdotL);
|
|
|
|
// Unoptimized formula was:
|
|
//weight = specularF0 * (phongNormalizationTerm(shininess) * pow(max(0.0f, dot(Rlocal, Vlocal)), shininess) * NdotL) / pdf;
|
|
|
|
return Llocal;
|
|
}
|
|
|
|
// -------------------------------------------------------------------------
|
|
// Oren-Nayar
|
|
// -------------------------------------------------------------------------
|
|
|
|
// Based on Oren-Nayar's qualitative model
|
|
// Source: "Generalization of Lambert's Reflectance Model" by Oren & Nayar
|
|
float orenNayar(BrdfData data) {
|
|
|
|
// Oren-Nayar roughness (sigma) is in radians - use conversion from Beckmann roughness here
|
|
float sigma = beckmannAlphaToOrenNayarRoughness(data.alpha);
|
|
|
|
float thetaV = acos(data.NdotV);
|
|
float thetaL = acos(data.NdotL);
|
|
|
|
float alpha = max(thetaV, thetaL);
|
|
float beta = min(thetaV, thetaL);
|
|
|
|
// Calculate cosine of azimuth angles difference - by projecting L and V onto plane defined by N. Assume L, V, N are normalized.
|
|
vec3 l = data.L - data.NdotL * data.N;
|
|
vec3 v = data.V - data.NdotV * data.N;
|
|
float cosPhiDifference = dot(normalize(v), normalize(l));
|
|
|
|
float sigma2 = sigma * sigma;
|
|
float A = 1.0f - 0.5f * (sigma2 / (sigma2 + 0.33f));
|
|
float B = 0.45f * (sigma2 / (sigma2 + 0.09f));
|
|
|
|
return (A + B * max(0.0f, cosPhiDifference) * sin(alpha) * tan(beta));
|
|
}
|
|
|
|
vec3 evalOrenNayar(const BrdfData data) {
|
|
return data.diffuseReflectance * (orenNayar(data) * ONE_OVER_PI * data.NdotL);
|
|
}
|
|
|
|
// -------------------------------------------------------------------------
|
|
// Disney
|
|
// -------------------------------------------------------------------------
|
|
|
|
// Disney's diffuse term
|
|
// Source "Physically-Based Shading at Disney" by Burley
|
|
float disneyDiffuse(const BrdfData data) {
|
|
|
|
float FD90MinusOne = 2.0f * data.roughness * data.LdotH * data.LdotH - 0.5f;
|
|
|
|
float FDL = 1.0f + (FD90MinusOne * pow(1.0f - data.NdotL, 5.0f));
|
|
float FDV = 1.0F + (FD90MinusOne * pow(1.0f - data.NdotV, 5.0f));
|
|
|
|
return FDL * FDV;
|
|
}
|
|
|
|
vec3 evalDisneyDiffuse(const BrdfData data) {
|
|
return data.diffuseReflectance * (disneyDiffuse(data) * ONE_OVER_PI * data.NdotL);
|
|
}
|
|
|
|
// Frostbite's version of Disney diffuse with energy normalization.
|
|
// Source: "Moving Frostbite to Physically Based Rendering" by Lagarde & de Rousiers
|
|
float frostbiteDisneyDiffuse(const BrdfData data) {
|
|
float energyBias = 0.5f * data.roughness;
|
|
float energyFactor = mix(1.0f, 1.0f / 1.51f, data.roughness);
|
|
|
|
float FD90MinusOne = energyBias + 2.0 * data.LdotH * data.LdotH * data.roughness - 1.0f;
|
|
|
|
float FDL = 1.0f + (FD90MinusOne * pow(1.0f - data.NdotL, 5.0f));
|
|
float FDV = 1.0f + (FD90MinusOne * pow(1.0f - data.NdotV, 5.0f));
|
|
|
|
return FDL * FDV * energyFactor;
|
|
}
|
|
|
|
vec3 evalFrostbiteDisneyDiffuse(const BrdfData data) {
|
|
return data.diffuseReflectance * (frostbiteDisneyDiffuse(data) * ONE_OVER_PI * data.NdotL);
|
|
}
|
|
|
|
// -------------------------------------------------------------------------
|
|
// Smith G term
|
|
// -------------------------------------------------------------------------
|
|
|
|
// Function to calculate 'a' parameter for lambda functions needed in Smith G term
|
|
// This is a version for shape invariant (isotropic) NDFs
|
|
// Note: makse sure NdotS is not negative
|
|
float Smith_G_a(float alpha, float NdotS) {
|
|
return NdotS / (max(0.00001f, alpha) * sqrt(1.0f - min(0.99999f, NdotS * NdotS)));
|
|
}
|
|
|
|
// Lambda function for Smith G term derived for GGX distribution
|
|
float Smith_G_Lambda_GGX(float a) {
|
|
return (-1.0f + sqrt(1.0f + (1.0f / (a * a)))) * 0.5f;
|
|
}
|
|
|
|
// Lambda function for Smith G term derived for Beckmann distribution
|
|
// This is Walter's rational approximation (avoids evaluating of error function)
|
|
// Source: "Real-time Rendering", 4th edition, p.339 by Akenine-Moller et al.
|
|
// Note that this formulation is slightly optimized and different from Walter's
|
|
float Smith_G_Lambda_Beckmann_Walter(float a) {
|
|
if (a < 1.6f) {
|
|
return (1.0f - (1.259f - 0.396f * a) * a) / ((3.535f + 2.181f * a) * a);
|
|
//return ((1.0f + (2.276f + 2.577f * a) * a) / ((3.535f + 2.181f * a) * a)) - 1.0f; //< Walter's original
|
|
} else {
|
|
return 0.0f;
|
|
}
|
|
}
|
|
|
|
// Smith G1 term (masking function)
|
|
// This non-optimized version uses NDF specific lambda function (G_Lambda) resolved bia macro based on selected NDF
|
|
float Smith_G1_General(float a) {
|
|
return 1.0f / (1.0f + Smith_G_Lambda(a));
|
|
}
|
|
|
|
// Smith G1 term (masking function) optimized for GGX distribution (by substituting G_Lambda_GGX into G1)
|
|
float Smith_G1_GGX(float a) {
|
|
float a2 = a * a;
|
|
return 2.0f / (sqrt((a2 + 1.0f) / a2) + 1.0f);
|
|
}
|
|
|
|
// Smith G1 term (masking function) further optimized for GGX distribution (by substituting G_a into G1_GGX)
|
|
float Smith_G1_GGX(float alpha, float NdotS, float alphaSquared, float NdotSSquared) {
|
|
return 2.0f / (sqrt(((alphaSquared * (1.0f - NdotSSquared)) + NdotSSquared) / NdotSSquared) + 1.0f);
|
|
}
|
|
|
|
// Smith G1 term (masking function) optimized for Beckmann distribution (by substituting G_Lambda_Beckmann_Walter into G1)
|
|
// Source: "Microfacet Models for Refraction through Rough Surfaces" by Walter et al.
|
|
float Smith_G1_Beckmann_Walter(float a) {
|
|
if (a < 1.6f) {
|
|
return ((3.535f + 2.181f * a) * a) / (1.0f + (2.276f + 2.577f * a) * a);
|
|
} else {
|
|
return 1.0f;
|
|
}
|
|
}
|
|
|
|
float Smith_G1_Beckmann_Walter(float alpha, float NdotS, float alphaSquared, float NdotSSquared) {
|
|
return Smith_G1_Beckmann_Walter(Smith_G_a(alpha, NdotS));
|
|
}
|
|
|
|
// Smith G2 term (masking-shadowing function)
|
|
// Separable version assuming independent (uncorrelated) masking and shadowing, uses G1 functions for selected NDF
|
|
float Smith_G2_Separable(float alpha, float NdotL, float NdotV) {
|
|
float aL = Smith_G_a(alpha, NdotL);
|
|
float aV = Smith_G_a(alpha, NdotV);
|
|
return Smith_G1(aL) * Smith_G1(aV);
|
|
}
|
|
|
|
// Smith G2 term (masking-shadowing function)
|
|
// Height correlated version - non-optimized, uses G_Lambda functions for selected NDF
|
|
float Smith_G2_Height_Correlated(float alpha, float NdotL, float NdotV) {
|
|
float aL = Smith_G_a(alpha, NdotL);
|
|
float aV = Smith_G_a(alpha, NdotV);
|
|
return 1.0f / (1.0f + Smith_G_Lambda(aL) + Smith_G_Lambda(aV));
|
|
}
|
|
|
|
// Smith G2 term (masking-shadowing function) for GGX distribution
|
|
// Separable version assuming independent (uncorrelated) masking and shadowing - optimized by substituing G_Lambda for G_Lambda_GGX and
|
|
// dividing by (4 * NdotL * NdotV) to cancel out these terms in specular BRDF denominator
|
|
// Source: "Moving Frostbite to Physically Based Rendering" by Lagarde & de Rousiers
|
|
// Note that returned value is G2 / (4 * NdotL * NdotV) and therefore includes division by specular BRDF denominator
|
|
float Smith_G2_Separable_GGX_Lagarde(float alphaSquared, float NdotL, float NdotV) {
|
|
float a = NdotV + sqrt(alphaSquared + NdotV * (NdotV - alphaSquared * NdotV));
|
|
float b = NdotL + sqrt(alphaSquared + NdotL * (NdotL - alphaSquared * NdotL));
|
|
return 1.0f / (a * b);
|
|
}
|
|
|
|
// Smith G2 term (masking-shadowing function) for GGX distribution
|
|
// Height correlated version - optimized by substituing G_Lambda for G_Lambda_GGX and dividing by (4 * NdotL * NdotV) to cancel out
|
|
// the terms in specular BRDF denominator
|
|
// Source: "Moving Frostbite to Physically Based Rendering" by Lagarde & de Rousiers
|
|
// Note that returned value is G2 / (4 * NdotL * NdotV) and therefore includes division by specular BRDF denominator
|
|
float Smith_G2_Height_Correlated_GGX_Lagarde(float alphaSquared, float NdotL, float NdotV) {
|
|
float a = NdotV * sqrt(alphaSquared + NdotL * (NdotL - alphaSquared * NdotL));
|
|
float b = NdotL * sqrt(alphaSquared + NdotV * (NdotV - alphaSquared * NdotV));
|
|
return 0.5f / (a + b);
|
|
}
|
|
|
|
// Smith G2 term (masking-shadowing function) for GGX distribution
|
|
// Height correlated version - approximation by Hammon
|
|
// Source: "PBR Diffuse Lighting for GGX + Smith Microsurfaces", slide 84 by Hammon
|
|
// Note that returned value is G2 / (4 * NdotL * NdotV) and therefore includes division by specular BRDF denominator
|
|
float Smith_G2_Height_Correlated_GGX_Hammon(float alpha, float NdotL, float NdotV) {
|
|
return 0.5f / (mix(2.0f * NdotL * NdotV, NdotL + NdotV, alpha));
|
|
}
|
|
|
|
// A fraction G2/G1 where G2 is height correlated can be expressed using only G1 terms
|
|
// Source: "Implementing a Simple Anisotropic Rough Diffuse Material with Stochastic Evaluation", Appendix A by Heitz & Dupuy
|
|
float Smith_G2_Over_G1_Height_Correlated(float alpha, float alphaSquared, float NdotL, float NdotV) {
|
|
float G1V = Smith_G1(alpha, NdotV, alphaSquared, NdotV * NdotV);
|
|
float G1L = Smith_G1(alpha, NdotL, alphaSquared, NdotL * NdotL);
|
|
return G1L / (G1V + G1L - G1V * G1L);
|
|
}
|
|
|
|
// Evaluates G2 for selected configuration (GGX/Beckmann, optimized/non-optimized, separable/height-correlated)
|
|
// Note that some paths aren't optimized too much...
|
|
// Also note that when USE_OPTIMIZED_G2 is specified, returned value will be: G2 / (4 * NdotL * NdotV) if GG-X is selected
|
|
float Smith_G2(float alpha, float alphaSquared, float NdotL, float NdotV) {
|
|
|
|
#if USE_OPTIMIZED_G2 && (MICROFACET_DISTRIBUTION == GGX)
|
|
#if USE_HEIGHT_CORRELATED_G2
|
|
#define G2_DIVIDED_BY_DENOMINATOR 1
|
|
return Smith_G2_Height_Correlated_GGX_Lagarde(alphaSquared, NdotL, NdotV);
|
|
#else
|
|
#define G2_DIVIDED_BY_DENOMINATOR 1
|
|
return Smith_G2_Separable_GGX_Lagarde(alphaSquared, NdotL, NdotV);
|
|
#endif
|
|
#else
|
|
#if USE_HEIGHT_CORRELATED_G2
|
|
return Smith_G2_Height_Correlated(alpha, NdotL, NdotV);
|
|
#else
|
|
return Smith_G2_Separable(alpha, NdotL, NdotV);
|
|
#endif
|
|
#endif
|
|
|
|
}
|
|
|
|
// -------------------------------------------------------------------------
|
|
// Normal distribution functions
|
|
// -------------------------------------------------------------------------
|
|
|
|
float Beckmann_D(float alphaSquared, float NdotH)
|
|
{
|
|
float cos2Theta = NdotH * NdotH;
|
|
float numerator = exp((cos2Theta - 1.0f) / (alphaSquared * cos2Theta));
|
|
float denominator = PI * alphaSquared * cos2Theta * cos2Theta;
|
|
return numerator / denominator;
|
|
}
|
|
|
|
float GGX_D(float alphaSquared, float NdotH) {
|
|
float b = ((alphaSquared - 1.0f) * NdotH * NdotH + 1.0f);
|
|
return alphaSquared / (PI * b * b);
|
|
}
|
|
|
|
// -------------------------------------------------------------------------
|
|
// Microfacet model
|
|
// -------------------------------------------------------------------------
|
|
|
|
// Samples a microfacet normal for the GGX distribution using VNDF method.
|
|
// Source: "Sampling the GGX Distribution of Visible Normals" by Heitz
|
|
// See also https://hal.inria.fr/hal-00996995v1/document and http://jcgt.org/published/0007/04/01/
|
|
// Random variables 'u' must be in <0;1) interval
|
|
// PDF is 'G1(NdotV) * D'
|
|
vec3 sampleGGXVNDF(vec3 Ve, vec2 alpha2D, vec2 u) {
|
|
|
|
// Section 3.2: transforming the view direction to the hemisphere configuration
|
|
vec3 Vh = normalize(vec3(alpha2D.x * Ve.x, alpha2D.y * Ve.y, Ve.z));
|
|
|
|
// Section 4.1: orthonormal basis (with special case if cross product is zero)
|
|
float lensq = Vh.x * Vh.x + Vh.y * Vh.y;
|
|
vec3 T1 = lensq > 0.0f ? vec3(-Vh.y, Vh.x, 0.0f) * rsqrt(lensq) : vec3(1.0f, 0.0f, 0.0f);
|
|
vec3 T2 = cross(Vh, T1);
|
|
|
|
// Section 4.2: parameterization of the projected area
|
|
float r = sqrt(u.x);
|
|
float phi = TWO_PI * u.y;
|
|
float t1 = r * cos(phi);
|
|
float t2 = r * sin(phi);
|
|
float s = 0.5f * (1.0f + Vh.z);
|
|
t2 = mix(sqrt(1.0f - t1 * t1), t2, s);
|
|
|
|
// Section 4.3: reprojection onto hemisphere
|
|
vec3 Nh = t1 * T1 + t2 * T2 + sqrt(max(0.0f, 1.0f - t1 * t1 - t2 * t2)) * Vh;
|
|
|
|
// Section 3.4: transforming the normal back to the ellipsoid configuration
|
|
return normalize(vec3(alpha2D.x * Nh.x, alpha2D.y * Nh.y, max(0.0f, Nh.z)));
|
|
}
|
|
|
|
// PDF of sampling a reflection vector L using 'sampleGGXVNDF'.
|
|
// Note that PDF of sampling given microfacet normal is (G1 * D) when vectors are in local space (in the hemisphere around shading normal).
|
|
// Remaining terms (1.0f / (4.0f * NdotV)) are specific for reflection case, and come from multiplying PDF by jacobian of reflection operator
|
|
float sampleGGXVNDFReflectionPdf(float alpha, float alphaSquared, float NdotH, float NdotV, float LdotH) {
|
|
NdotH = max(0.00001f, NdotH);
|
|
NdotV = max(0.00001f, NdotV);
|
|
return (GGX_D(max(0.00001f, alphaSquared), NdotH) * Smith_G1_GGX(alpha, NdotV, alphaSquared, NdotV * NdotV)) / (4.0f * NdotV);
|
|
}
|
|
|
|
// "Walter's trick" is an adjustment of alpha value for Walter's sampling to reduce maximal weight of sample to about 4
|
|
// Source: "Microfacet Models for Refraction through Rough Surfaces" by Walter et al., page 8
|
|
float waltersTrick(float alpha, float NdotV) {
|
|
return (1.2f - 0.2f * sqrt(abs(NdotV))) * alpha;
|
|
}
|
|
|
|
// PDF of sampling a reflection vector L using 'sampleBeckmannWalter'.
|
|
// Note that PDF of sampling given microfacet normal is (D * NdotH). Remaining terms (1.0f / (4.0f * LdotH)) are specific for
|
|
// reflection case, and come from multiplying PDF by jacobian of reflection operator
|
|
float sampleBeckmannWalterReflectionPdf(float alpha, float alphaSquared, float NdotH, float NdotV, float LdotH) {
|
|
NdotH = max(0.00001f, NdotH);
|
|
LdotH = max(0.00001f, LdotH);
|
|
return Beckmann_D(max(0.00001f, alphaSquared), NdotH) * NdotH / (4.0f * LdotH);
|
|
}
|
|
|
|
// Samples a microfacet normal for the Beckmann distribution using walter's method.
|
|
// Source: "Microfacet Models for Refraction through Rough Surfaces" by Walter et al.
|
|
// PDF is 'D * NdotH'
|
|
vec3 sampleBeckmannWalter(vec3 Vlocal, vec2 alpha2D, vec2 u) {
|
|
float alpha = dot(alpha2D, vec2(0.5f, 0.5f));
|
|
|
|
// Equations (28) and (29) from Walter's paper for Beckmann distribution
|
|
float tanThetaSquared = -(alpha * alpha) * log(1.0f - u.x);
|
|
float phi = TWO_PI * u.y;
|
|
|
|
// Calculate cosTheta and sinTheta needed for conversion to H vector
|
|
float cosTheta = rsqrt(1.0f + tanThetaSquared);
|
|
float sinTheta = sqrt(1.0f - cosTheta * cosTheta);
|
|
|
|
// Convert sampled spherical coordinates to H vector
|
|
return normalize(vec3(sinTheta * cos(phi), sinTheta * sin(phi), cosTheta));
|
|
}
|
|
|
|
// Weight for the reflection ray sampled from GGX distribution using VNDF method
|
|
float specularSampleWeightGGXVNDF(float alpha, float alphaSquared, float NdotL, float NdotV, float HdotL, float NdotH) {
|
|
#if USE_HEIGHT_CORRELATED_G2
|
|
return Smith_G2_Over_G1_Height_Correlated(alpha, alphaSquared, NdotL, NdotV);
|
|
#else
|
|
return Smith_G1_GGX(alpha, NdotL, alphaSquared, NdotL * NdotL);
|
|
#endif
|
|
}
|
|
|
|
// Weight for the reflection ray sampled from Beckmann distribution using Walter's method
|
|
float specularSampleWeightBeckmannWalter(float alpha, float alphaSquared, float NdotL, float NdotV, float HdotL, float NdotH) {
|
|
return (HdotL * Smith_G2(alpha, alphaSquared, NdotL, NdotV)) / (NdotV * NdotH);
|
|
}
|
|
|
|
// Samples a reflection ray from the rough surface using selected microfacet distribution and sampling method
|
|
// Resulting weight includes multiplication by cosine (NdotL) term
|
|
vec3 sampleSpecularMicrofacet(vec3 Vlocal, float alpha, float alphaSquared, vec3 specularF0, vec2 u, OUT_PARAMETER(vec3) weight) {
|
|
|
|
// Sample a microfacet normal (H) in local space
|
|
vec3 Hlocal;
|
|
if (alpha == 0.0f) {
|
|
// Fast path for zero roughness (perfect reflection), also prevents NaNs appearing due to divisions by zeroes
|
|
Hlocal = vec3(0.0f, 0.0f, 1.0f);
|
|
} else {
|
|
// For non-zero roughness, this calls VNDF sampling for GG-X distribution or Walter's sampling for Beckmann distribution
|
|
Hlocal = sampleSpecularHalfVector(Vlocal, vec2(alpha, alpha), u);
|
|
}
|
|
|
|
// Reflect view direction to obtain light vector
|
|
vec3 Llocal = reflect(-Vlocal, Hlocal);
|
|
|
|
// Note: HdotL is same as HdotV here
|
|
// Clamp dot products here to small value to prevent numerical instability. Assume that rays incident from below the hemisphere have been filtered
|
|
float HdotL = max(0.00001f, min(1.0f, dot(Hlocal, Llocal)));
|
|
const vec3 Nlocal = vec3(0.0f, 0.0f, 1.0f);
|
|
float NdotL = max(0.00001f, min(1.0f, dot(Nlocal, Llocal)));
|
|
float NdotV = max(0.00001f, min(1.0f, dot(Nlocal, Vlocal)));
|
|
float NdotH = max(0.00001f, min(1.0f, dot(Nlocal, Hlocal)));
|
|
vec3 F = evalFresnel(specularF0, shadowedF90(specularF0), HdotL);
|
|
|
|
// Calculate weight of the sample specific for selected sampling method
|
|
// (this is microfacet BRDF divided by PDF of sampling method - notice how most terms cancel out)
|
|
weight = F * specularSampleWeight(alpha, alphaSquared, NdotL, NdotV, HdotL, NdotH);
|
|
|
|
return Llocal;
|
|
}
|
|
|
|
// Evaluates microfacet specular BRDF
|
|
vec3 evalMicrofacet(const BrdfData data) {
|
|
|
|
float D = Microfacet_D(max(0.00001f, data.alphaSquared), data.NdotH);
|
|
float G2 = Smith_G2(data.alpha, data.alphaSquared, data.NdotL, data.NdotV);
|
|
//vec3 F = evalFresnel(data.specularF0, shadowedF90(data.specularF0), data.VdotH); //< Unused, F is precomputed already
|
|
|
|
#if G2_DIVIDED_BY_DENOMINATOR
|
|
return data.F * (G2 * D * data.NdotL);
|
|
#else
|
|
return ((data.F * G2 * D) / (4.0f * data.NdotL * data.NdotV)) * data.NdotL;
|
|
#endif
|
|
}
|
|
|
|
// -------------------------------------------------------------------------
|
|
// Combined BRDF
|
|
// -------------------------------------------------------------------------
|
|
|
|
// Precalculates commonly used terms in BRDF evaluation
|
|
// Clamps around dot products prevent NaNs and ensure numerical stability, but make sure to
|
|
// correctly ignore rays outside of the sampling hemisphere, by using 'Vbackfacing' and 'Lbackfacing' flags
|
|
BrdfData prepareBRDFData(vec3 N, vec3 L, vec3 V, MaterialProperties material) {
|
|
BrdfData data;
|
|
|
|
// Evaluate VNHL vectors
|
|
data.V = V;
|
|
data.N = N;
|
|
data.H = normalize(L + V);
|
|
data.L = L;
|
|
|
|
float NdotL = dot(N, L);
|
|
float NdotV = dot(N, V);
|
|
data.Vbackfacing = (NdotV <= 0.0f);
|
|
data.Lbackfacing = (NdotL <= 0.0f);
|
|
|
|
// Clamp NdotS to prevent numerical instability. Assume vectors below the hemisphere will be filtered using 'Vbackfacing' and 'Lbackfacing' flags
|
|
data.NdotL = min(max(0.00001f, NdotL), 1.0f);
|
|
data.NdotV = min(max(0.00001f, NdotV), 1.0f);
|
|
|
|
data.LdotH = saturate(dot(L, data.H));
|
|
data.NdotH = saturate(dot(N, data.H));
|
|
data.VdotH = saturate(dot(V, data.H));
|
|
|
|
// Unpack material properties
|
|
data.specularF0 = baseColorToSpecularF0(material.baseColor, material.metalness);
|
|
data.diffuseReflectance = baseColorToDiffuseReflectance(material.baseColor, material.metalness);
|
|
|
|
// Unpack 'perceptively linear' -> 'linear' -> 'squared' roughness
|
|
data.roughness = material.roughness;
|
|
data.alpha = material.roughness * material.roughness;
|
|
data.alphaSquared = data.alpha * data.alpha;
|
|
|
|
// Pre-calculate some more BRDF terms
|
|
data.F = evalFresnel(data.specularF0, shadowedF90(data.specularF0), data.LdotH);
|
|
|
|
return data;
|
|
}
|
|
|
|
// This is an entry point for evaluation of all other BRDFs based on selected configuration (for direct light)
|
|
vec3 evalCombinedBRDF(vec3 N, vec3 L, vec3 V, MaterialProperties material) {
|
|
|
|
// Prepare data needed for BRDF evaluation - unpack material properties and evaluate commonly used terms (e.g. Fresnel, NdotL, ...)
|
|
const BrdfData data = prepareBRDFData(N, L, V, material);
|
|
|
|
// Ignore V and L rays "below" the hemisphere
|
|
//if (data.Vbackfacing || data.Lbackfacing) return vec3(0.0f, 0.0f, 0.0f);
|
|
|
|
// Eval specular and diffuse BRDFs
|
|
vec3 specular = evalSpecular(data);
|
|
vec3 diffuse = evalDiffuse(data);
|
|
|
|
// Combine specular and diffuse layers
|
|
#if COMBINE_BRDFS_WITH_FRESNEL
|
|
// Specular is already multiplied by F, just attenuate diffuse
|
|
return (vec3(1.0f, 1.0f, 1.0f) - data.F) * diffuse + specular;
|
|
#else
|
|
return diffuse + specular;
|
|
#endif
|
|
}
|
|
|
|
// This is an entry point for evaluation of all other BRDFs based on selected configuration (for indirect light)
|
|
bool evalIndirectCombinedBRDF(vec2 u, vec3 shadingNormal, vec3 geometryNormal, vec3 V, MaterialProperties material, const int brdfType, OUT_PARAMETER(vec3) rayDirection, OUT_PARAMETER(vec3) sampleWeight) {
|
|
|
|
// Ignore incident ray coming from "below" the hemisphere
|
|
if (dot(shadingNormal, V) <= 0.0f) return false;
|
|
|
|
// Transform view direction into local space of our sampling routines
|
|
// (local space is oriented so that its positive Z axis points along the shading normal)
|
|
vec4 qRotationToZ = getRotationToZAxis(shadingNormal);
|
|
vec3 Vlocal = rotatePoint(qRotationToZ, V);
|
|
const vec3 Nlocal = vec3(0.0f, 0.0f, 1.0f);
|
|
|
|
vec3 rayDirectionLocal = vec3(0.0f, 0.0f, 0.0f);
|
|
|
|
if (brdfType == DIFFUSE_TYPE) {
|
|
|
|
// Sample diffuse ray using cosine-weighted hemisphere sampling
|
|
rayDirectionLocal = sampleHemisphere(u);
|
|
const BrdfData data = prepareBRDFData(Nlocal, rayDirectionLocal, Vlocal, material);
|
|
|
|
// Function 'diffuseTerm' is predivided by PDF of sampling the cosine weighted hemisphere
|
|
sampleWeight = data.diffuseReflectance * diffuseTerm(data);
|
|
|
|
#if COMBINE_BRDFS_WITH_FRESNEL
|
|
// Sample a half-vector of specular BRDF. Note that we're reusing random variable 'u' here, but correctly it should be an new independent random number
|
|
vec3 Hspecular = sampleSpecularHalfVector(Vlocal, vec2(data.alpha, data.alpha), u);
|
|
|
|
// Clamp HdotL to small value to prevent numerical instability. Assume that rays incident from below the hemisphere have been filtered
|
|
float VdotH = max(0.00001f, min(1.0f, dot(Vlocal, Hspecular)));
|
|
sampleWeight *= (vec3(1.0f, 1.0f, 1.0f) - evalFresnel(data.specularF0, shadowedF90(data.specularF0), VdotH));
|
|
#endif
|
|
|
|
} else if (brdfType == SPECULAR_TYPE) {
|
|
const BrdfData data = prepareBRDFData(Nlocal, vec3(0.0f, 0.0f, 1.0f) /* unused L vector */, Vlocal, material);
|
|
rayDirectionLocal = sampleSpecular(Vlocal, data.alpha, data.alphaSquared, data.specularF0, u, sampleWeight);
|
|
}
|
|
|
|
// Prevent tracing direction with no contribution
|
|
if (luminance(sampleWeight) == 0.0f) return false;
|
|
|
|
// Transform sampled direction Llocal back to V vector space
|
|
rayDirection = normalize(rotatePoint(invertRotation(qRotationToZ), rayDirectionLocal));
|
|
|
|
// Prevent tracing direction "under" the hemisphere (behind the triangle)
|
|
if (dot(geometryNormal, rayDirection) <= 0.0f) return false;
|
|
|
|
return true;
|
|
}
|
|
|
|
// Calculates probability of selecting BRDF (specular or diffuse) using the approximate Fresnel term
|
|
float getBrdfProbability(MaterialProperties material, vec3 V, vec3 shadingNormal) {
|
|
|
|
// Evaluate Fresnel term using the shading normal
|
|
// Note: we use the shading normal instead of the microfacet normal (half-vector) for Fresnel term here. That's suboptimal for rough surfaces at grazing angles, but half-vector is yet unknown at this point
|
|
float specularF0 = luminance(baseColorToSpecularF0(material.baseColor, material.metalness));
|
|
float diffuseReflectance = luminance(baseColorToDiffuseReflectance(material.baseColor, material.metalness));
|
|
float Fresnel = saturate(luminance(evalFresnel(vec3(specularF0), shadowedF90(vec3(specularF0)), max(0.0f, dot(V, shadingNormal)))));
|
|
|
|
// Approximate relative contribution of BRDFs using the Fresnel term
|
|
float specular = Fresnel;
|
|
float diffuse = diffuseReflectance * (1.0f - Fresnel); //< If diffuse term is weighted by Fresnel, apply it here as well
|
|
|
|
// Return probability of selecting specular BRDF over diffuse BRDF
|
|
float p = (specular / max(0.0001f, (specular + diffuse)));
|
|
|
|
// Clamp probability to avoid undersampling of less prominent BRDF
|
|
return clamp(p, 0.1f, 0.9f);
|
|
}
|