2021-09-01 19:37:30 +02:00
/* 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
2021-09-14 20:35:09 +02:00
//if (data.Vbackfacing || data.Lbackfacing) return vec3(0.0f, 0.0f, 0.0f);
2021-09-01 19:37:30 +02:00
// 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 ;
}
2021-09-01 20:01:40 +02:00
// Calculates probability of selecting BRDF (specular or diffuse) using the approximate Fresnel term
float getBrdfProbability ( MaterialProperties material , vec3 V , vec3 shadingNormal ) {
2021-09-14 20:35:09 +02:00
2021-09-01 20:01:40 +02:00
// 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 ) ;
}