├── README.md └── brdf.h /README.md: -------------------------------------------------------------------------------- 1 | # Crash Course in BRDF Implementation Code Sample 2 | 3 | This is a code sample accompanying the article at https://boksajak.github.io/blog/BRDF. The file can be compiled as a HLSL shader or C++ code. 4 | 5 | # Example 6 | 7 | A working path tracer which uses this BRDF implementation can be found in the [ReferencePT project](https://github.com/boksajak/referencePT) on GitHub. See file [PathTracer.hlsl](https://github.com/boksajak/referencePT/blob/master/shaders/PathTracer.hlsl) which includes **brdf.h** and calls to functions **evalCombinedBRDF** and **evalIndirectCombinedBRDF** for details. 8 | 9 | # How to use 10 | 11 | 1. Include **brdf.h** in your project 12 | 2. If compiling as C++, make sure that supporting [GLM library](https://github.com/g-truc/glm) is available and its includes at the beginning of the **brdf.h** file are valid. This library makes it possible to use HLSL-style structures and functions and compile it as C++ with minimal effort. 13 | 3. Call functions **evalCombinedBRDF** and **evalIndirectCombinedBRDF** to evaluate BRDFs as described below and in the example. When compiling for C++, all functions and structures are placed in the **brdf** namespace. 14 | 15 | ## Direct light evaluation 16 | 17 | To evaluate BRDF for light coming from a light source, use the function **evalCombinedBRDF**, as follows: 18 | 19 | ```cpp 20 | const float3 V = -ray.Direction; //< V is the direction towards the viewer 21 | const float3 L = normalize(lightVector); //< L is the direction towards the light source 22 | const float3 N = shadingNormal; //< N is the shading normal 23 | 24 | MaterialProperties material = ... //< Initialize material properties to that of evaluated surface 25 | material.reflectance = 0.5f; //< If reflectance of the surface is used (see below) but not specified, 26 | //< it should be initialized to 0.5 27 | 28 | float3 light = evalCombinedBRDF(N, L, V, material); 29 | ``` 30 | 31 | ## Indirect light evaluation 32 | 33 | To generate a reflected ray according to the BRDF of the surface (and essentially evaluate indirect light contribution to the surface), use the function **evalIndirectCombinedBRDF** as follows: 34 | 35 | ```cpp 36 | const float2 random = ... //< 2 random numbers in the interval <0,1). Note that interval is open on the right 37 | //< side - number 1 can never occur here as it would generate NaNs when sampling the BRDF 38 | const int brdfType = ... //< Choose between SPECULAR_TYPE and DIFFUSE_TYPE. An example of how to choose in a path tracer 39 | //< is in [ReferencePT project](https://github.com/boksajak/referencePT) 40 | 41 | float3 rayDirection; //< Sampled ray direction will be here 42 | float3 brdfWeight; //< The weight of the BRDF sample will be here 43 | 44 | if (!evalIndirectCombinedBRDF(random, shadingNormal, geometryNormal, V, material, brdfType, rayDirection, brdfWeight)) 45 | { 46 | break; // Failed to generate valid ray direction 47 | } 48 | 49 | ``` 50 | 51 | # Options 52 | 53 | The **brdf.h** file is configurable by macros at its beginning. 54 | 55 | 56 | ## Minimal reflectance specification 57 | 58 | By default, the specular reflectance of dielectrics (materials with metalness set to zero) is specified by definition **MIN_DIELECTRICS_F0** to 4%. 59 | 60 | If you want to let users to specify dielectrics reflectance per material, you can enable macro **USE_REFLECTANCE_PARAMETER** and set the __reflectance__ parameter of the **MaterialProperties** structure. Note that actual reflectance is calculated as __0.16 * reflectance * reflectance__. This makes changes to the parameter to appear more linear. If reflectance parameter is used, but some material doesn't specify it, it should be initialized to 0.5 - this will result in the reflectance of 4% as per formula above. 61 | 62 | ## VNDF Sampling 63 | 64 | By default, the implementation uses VNDF sampling from __"Sampling the GGX Distribution of Visible Normals"__ by Heitz. If you want to use newer sampling from __"Sampling Visible GGX Normals with Spherical Caps"__ by Dupuy & Benyoub, enable macro **USE_VNDF_WITH_SPHERICAL_CAPS**. This new method can bring performance benefits. 65 | 66 | There is also an option to use older Walter's sampling from __"Microfacet Models for Refraction through Rough Surfaces"__ by enablinf macro **USE_WALTER_GGX_SAMPLING**. 67 | 68 | ## FUNCTION macro 69 | 70 | All functions are prefixed with the macro called **FUNCTION**, which is set to **static** keyword when compiling as C++, and is empty in HLSL. 71 | 72 | ## Release notes 73 | 74 | ### brdf.h 1.2 - August 2023 75 | #### Features: 76 | - Added VNDF sampling from __"Sampling Visible GGX Normals with Spherical Caps"__ by Dupuy & Benyoub 77 | - Added Walter's sampling of GG-X distribution 78 | - Added optional reflectance parameter to specify minimal reflectance of dielectrics per material 79 | - Added function **specularGGXReflectanceApprox** from "Accurate Real-Time Specular Reflections with Radiance Caching" in Ray Tracing Gems by Hirvonen et al. 80 | - All functions and structures put into **brdf** namespace when compiling as C++ 81 | - All functions specified as static when compiling as C++ 82 | -------------------------------------------------------------------------------- /brdf.h: -------------------------------------------------------------------------------- 1 | /* 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. */ 2 | 3 | // This is a code sample accompanying the "Crash Course in BRDF Implementation" article 4 | // v1.2, August 2023 5 | 6 | // ------------------------------------------------------------------------- 7 | // C++ compatibility 8 | // ------------------------------------------------------------------------- 9 | 10 | #if __cplusplus 11 | 12 | #pragma once 13 | 14 | // Include additional things when compiling HLSL as C++ 15 | // Here we use the GLM library to support HLSL types and functions 16 | #include "glm/glm/glm.hpp" 17 | #include "glm/glm/gtc/constants.hpp" 18 | #include "glm/glm/gtx/compatibility.hpp" 19 | 20 | #define OUT_PARAMETER(X) X& 21 | #define FUNCTION static 22 | 23 | using namespace glm; 24 | 25 | namespace brdf { 26 | 27 | inline float rsqrt(float x) { return inversesqrt(x); } 28 | inline float saturate(float x) { return clamp(x, 0.0f, 1.0f); } 29 | inline float2 mul(float2x2 a, float2 b) { return b * a; } 30 | inline float3 mul(float3x3 a, float3 b) { return b * a; } 31 | 32 | #else 33 | #define OUT_PARAMETER(X) out X 34 | #define FUNCTION 35 | #endif 36 | 37 | // ------------------------------------------------------------------------- 38 | // Constant Definitions 39 | // ------------------------------------------------------------------------- 40 | 41 | #define NONE 0 42 | 43 | // NDF definitions 44 | #define GGX 1 45 | #define BECKMANN 2 46 | 47 | // Specular BRDFs 48 | #define MICROFACET 1 49 | #define PHONG 2 50 | 51 | // Diffuse BRDFs 52 | #define LAMBERTIAN 1 53 | #define OREN_NAYAR 2 54 | #define DISNEY 3 55 | #define FROSTBITE 4 56 | 57 | // BRDF types 58 | #define DIFFUSE_TYPE 1 59 | #define SPECULAR_TYPE 2 60 | 61 | // PIs 62 | #ifndef PI 63 | #define PI 3.141592653589f 64 | #endif 65 | 66 | #ifndef TWO_PI 67 | #define TWO_PI (2.0f * PI) 68 | #endif 69 | 70 | #ifndef ONE_OVER_PI 71 | #define ONE_OVER_PI (1.0f / PI) 72 | #endif 73 | 74 | #ifndef ONE_OVER_TWO_PI 75 | #define ONE_OVER_TWO_PI (1.0f / TWO_PI) 76 | #endif 77 | 78 | // ------------------------------------------------------------------------- 79 | // Configuration macros (user editable - set your preferences here) 80 | // ------------------------------------------------------------------------- 81 | 82 | // Specify what NDF (GGX or BECKMANN you want to use) 83 | #ifndef MICROFACET_DISTRIBUTION 84 | #define MICROFACET_DISTRIBUTION GGX 85 | //#define MICROFACET_DISTRIBUTION BECKMANN 86 | #endif 87 | 88 | // Specify default specular and diffuse BRDFs 89 | #ifndef SPECULAR_BRDF 90 | #define SPECULAR_BRDF MICROFACET 91 | //#define SPECULAR_BRDF PHONG 92 | //#define SPECULAR_BRDF NONE 93 | #endif 94 | 95 | #ifndef DIFFUSE_BRDF 96 | #define DIFFUSE_BRDF LAMBERTIAN 97 | //#define DIFFUSE_BRDF OREN_NAYAR 98 | //#define DIFFUSE_BRDF DISNEY 99 | //#define DIFFUSE_BRDF FROSTBITE 100 | //#define DIFFUSE_BRDF NONE 101 | #endif 102 | 103 | // Specifies minimal reflectance for dielectrics (when metalness is zero) 104 | // Nothing has lower reflectance than 2%, but we use 4% to have consistent results with UE4, Frostbite, et al. 105 | // Note: only takes effect when USE_REFLECTANCE_PARAMETER is not defined 106 | #define MIN_DIELECTRICS_F0 0.04f 107 | 108 | // Define this to use minimal reflectance (F0) specified per material, instead of global MIN_DIELECTRICS_F0 value 109 | //#define USE_REFLECTANCE_PARAMETER 1 110 | 111 | // Enable this to weigh diffuse by Fresnel too, otherwise specular and diffuse will be simply added together 112 | // (this is disabled by default for Frostbite diffuse which is normalized to combine well with GGX Specular BRDF) 113 | #if DIFFUSE_BRDF != FROSTBITE 114 | #define COMBINE_BRDFS_WITH_FRESNEL 1 115 | #endif 116 | 117 | // 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) 118 | //#define Smith_G1 Smith_G1_General 119 | 120 | // 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) 121 | #define USE_OPTIMIZED_G2 1 122 | 123 | // Enable height correlated version of G2 term. Separable version will be used otherwise 124 | #define USE_HEIGHT_CORRELATED_G2 1 125 | 126 | // Enable this to use Walter's sampling for GG-X distribution instead of more recent VNDF 127 | //#define USE_WALTER_GGX_SAMPLING 1 128 | 129 | // Enable this to VNDF sampling using spherical caps instead of original Heitz's method 130 | //#define USE_VNDF_WITH_SPHERICAL_CAPS 1 131 | 132 | // ------------------------------------------------------------------------- 133 | // Automatically resolved macros based on preferences (don't edit these) 134 | // ------------------------------------------------------------------------- 135 | 136 | // Select distribution function 137 | #if MICROFACET_DISTRIBUTION == GGX 138 | #define Microfacet_D GGX_D 139 | #elif MICROFACET_DISTRIBUTION == BECKMANN 140 | #define Microfacet_D Beckmann_D 141 | #endif 142 | 143 | // Select G functions (masking/shadowing) depending on selected distribution 144 | #if MICROFACET_DISTRIBUTION == GGX 145 | #define Smith_G_Lambda Smith_G_Lambda_GGX 146 | #elif MICROFACET_DISTRIBUTION == BECKMANN 147 | #define Smith_G_Lambda Smith_G_Lambda_Beckmann_Walter 148 | #endif 149 | 150 | #ifndef Smith_G1 151 | // Define version of G1 optimized specifically for selected NDF 152 | #if MICROFACET_DISTRIBUTION == GGX 153 | #define Smith_G1 Smith_G1_GGX 154 | #elif MICROFACET_DISTRIBUTION == BECKMANN 155 | #define Smith_G1 Smith_G1_Beckmann_Walter 156 | #endif 157 | #endif 158 | 159 | // Select default specular and diffuse BRDF functions 160 | #if SPECULAR_BRDF == MICROFACET 161 | #define evalSpecular evalMicrofacet 162 | #define sampleSpecular sampleSpecularMicrofacet 163 | #if MICROFACET_DISTRIBUTION == GGX 164 | #if USE_WALTER_GGX_SAMPLING 165 | #define sampleSpecularHalfVector sampleGGXWalter 166 | #else 167 | #define sampleSpecularHalfVector sampleGGXVNDF 168 | #endif 169 | #else 170 | #define sampleSpecularHalfVector sampleBeckmannWalter 171 | #endif 172 | #elif SPECULAR_BRDF == PHONG 173 | #define evalSpecular evalPhong 174 | #define sampleSpecular sampleSpecularPhong 175 | #define sampleSpecularHalfVector samplePhong 176 | #else 177 | #define evalSpecular evalVoid 178 | #define sampleSpecular sampleSpecularVoid 179 | #define sampleSpecularHalfVector sampleSpecularHalfVectorVoid 180 | #endif 181 | 182 | #if MICROFACET_DISTRIBUTION == GGX 183 | #if USE_WALTER_GGX_SAMPLING 184 | #define specularSampleWeight specularSampleWeightGGXWalter 185 | #else 186 | #define specularSampleWeight specularSampleWeightGGXVNDF 187 | #endif 188 | #if USE_WALTER_GGX_SAMPLING 189 | #define specularPdf sampleWalterReflectionPdf 190 | #else 191 | #define specularPdf sampleGGXVNDFReflectionPdf 192 | #endif 193 | #else 194 | #define specularSampleWeight specularSampleWeightBeckmannWalter 195 | #define specularPdf sampleWalterReflectionPdf 196 | #endif 197 | 198 | #if DIFFUSE_BRDF == LAMBERTIAN 199 | #define evalDiffuse evalLambertian 200 | #define diffuseTerm lambertian 201 | #elif DIFFUSE_BRDF == OREN_NAYAR 202 | #define evalDiffuse evalOrenNayar 203 | #define diffuseTerm orenNayar 204 | #elif DIFFUSE_BRDF == DISNEY 205 | #define evalDiffuse evalDisneyDiffuse 206 | #define diffuseTerm disneyDiffuse 207 | #elif DIFFUSE_BRDF == FROSTBITE 208 | #define evalDiffuse evalFrostbiteDisneyDiffuse 209 | #define diffuseTerm frostbiteDisneyDiffuse 210 | #else 211 | #define evalDiffuse evalVoid 212 | #define evalIndirectDiffuse evalIndirectVoid 213 | #define diffuseTerm none 214 | #endif 215 | 216 | // ------------------------------------------------------------------------- 217 | // Structures 218 | // ------------------------------------------------------------------------- 219 | 220 | struct MaterialProperties 221 | { 222 | float3 baseColor; 223 | float metalness; 224 | 225 | float3 emissive; 226 | float roughness; 227 | 228 | float transmissivness; 229 | float reflectance; //< This should default to 0.5 to set minimal reflectance at 4% 230 | float opacity; 231 | }; 232 | 233 | // Data needed to evaluate BRDF (surface and material properties at given point + configuration of light and normal vectors) 234 | struct BrdfData 235 | { 236 | // Material properties 237 | float3 specularF0; 238 | float3 diffuseReflectance; 239 | 240 | // Roughnesses 241 | float roughness; //< perceptively linear roughness (artist's input) 242 | float alpha; //< linear roughness - often 'alpha' in specular BRDF equations 243 | float alphaSquared; //< alpha squared - pre-calculated value commonly used in BRDF equations 244 | 245 | // Commonly used terms for BRDF evaluation 246 | float3 F; //< Fresnel term 247 | 248 | // Vectors 249 | float3 V; //< Direction to viewer (or opposite direction of incident ray) 250 | float3 N; //< Shading normal 251 | float3 H; //< Half vector (microfacet normal) 252 | float3 L; //< Direction to light (or direction of reflecting ray) 253 | 254 | float NdotL; 255 | float NdotV; 256 | 257 | float LdotH; 258 | float NdotH; 259 | float VdotH; 260 | 261 | // True when V/L is backfacing wrt. shading normal N 262 | bool Vbackfacing; 263 | bool Lbackfacing; 264 | }; 265 | 266 | // ------------------------------------------------------------------------- 267 | // Utilities 268 | // ------------------------------------------------------------------------- 269 | 270 | // Converts Phong's exponent (shininess) to Beckmann roughness (alpha) 271 | // Source: "Microfacet Models for Refraction through Rough Surfaces" by Walter et al. 272 | FUNCTION float shininessToBeckmannAlpha(float shininess) { 273 | return sqrt(2.0f / (shininess + 2.0f)); 274 | } 275 | 276 | // Converts Beckmann roughness (alpha) to Phong's exponent (shininess) 277 | // Source: "Microfacet Models for Refraction through Rough Surfaces" by Walter et al. 278 | FUNCTION float beckmannAlphaToShininess(float alpha) { 279 | return 2.0f / min(0.9999f, max(0.0002f, (alpha * alpha))) - 2.0f; 280 | } 281 | 282 | // Converts Beckmann roughness (alpha) to Oren-Nayar roughness (sigma) 283 | // Source: "Moving Frostbite to Physically Based Rendering" by Lagarde & de Rousiers 284 | FUNCTION float beckmannAlphaToOrenNayarRoughness(float alpha) { 285 | return 0.7071067f * atan(alpha); 286 | } 287 | 288 | FUNCTION float luminance(float3 rgb) 289 | { 290 | return dot(rgb, float3(0.2126f, 0.7152f, 0.0722f)); 291 | } 292 | 293 | FUNCTION float3 baseColorToSpecularF0(const float3 baseColor, const float metalness, const float reflectance = 0.5f) { 294 | #if USE_REFLECTANCE_PARAMETER 295 | const float minDielectricsF0 = 0.16f * reflectance * reflectance; 296 | #else 297 | const float minDielectricsF0 = MIN_DIELECTRICS_F0; 298 | #endif 299 | return lerp(float3(minDielectricsF0, minDielectricsF0, minDielectricsF0), baseColor, metalness); 300 | } 301 | 302 | FUNCTION float3 baseColorToDiffuseReflectance(float3 baseColor, float metalness) 303 | { 304 | return baseColor * (1.0f - metalness); 305 | } 306 | 307 | FUNCTION float none(const BrdfData data) { 308 | return 0.0f; 309 | } 310 | 311 | FUNCTION float3 evalVoid(const BrdfData data) { 312 | return float3(0.0f, 0.0f, 0.0f); 313 | } 314 | 315 | FUNCTION void evalIndirectVoid(const BrdfData data, float2 u, OUT_PARAMETER(float3) rayDirection, OUT_PARAMETER(float3) weight) { 316 | rayDirection = float3(0.0f, 0.0f, 1.0f); 317 | weight = float3(0.0f, 0.0f, 0.0f); 318 | } 319 | 320 | FUNCTION float3 sampleSpecularVoid(float3 Vlocal, float alpha, float alphaSquared, float3 specularF0, float2 u, OUT_PARAMETER(float3) weight) { 321 | weight = float3(0.0f, 0.0f, 0.0f); 322 | return float3(0.0f, 0.0f, 0.0f); 323 | } 324 | 325 | FUNCTION float3 sampleSpecularHalfVectorVoid(float3 Vlocal, float2 alpha2D, float2 u) { 326 | return float3(0.0f, 0.0f, 0.0f); 327 | } 328 | 329 | // ------------------------------------------------------------------------- 330 | // Quaternion rotations 331 | // ------------------------------------------------------------------------- 332 | 333 | // Calculates rotation quaternion from input vector to the vector (0, 0, 1) 334 | // Input vector must be normalized! 335 | FUNCTION float4 getRotationToZAxis(float3 input) { 336 | 337 | // Handle special case when input is exact or near opposite of (0, 0, 1) 338 | if (input.z < -0.99999f) return float4(1.0f, 0.0f, 0.0f, 0.0f); 339 | 340 | return normalize(float4(input.y, -input.x, 0.0f, 1.0f + input.z)); 341 | } 342 | 343 | // Calculates rotation quaternion from vector (0, 0, 1) to the input vector 344 | // Input vector must be normalized! 345 | FUNCTION float4 getRotationFromZAxis(float3 input) { 346 | 347 | // Handle special case when input is exact or near opposite of (0, 0, 1) 348 | if (input.z < -0.99999f) return float4(1.0f, 0.0f, 0.0f, 0.0f); 349 | 350 | return normalize(float4(-input.y, input.x, 0.0f, 1.0f + input.z)); 351 | } 352 | 353 | // Returns the quaternion with inverted rotation 354 | FUNCTION float4 invertRotation(float4 q) 355 | { 356 | return float4(-q.x, -q.y, -q.z, q.w); 357 | } 358 | 359 | // Optimized point rotation using quaternion 360 | // Source: https://gamedev.stackexchange.com/questions/28395/rotating-vector3-by-a-quaternion 361 | FUNCTION float3 rotatePoint(float4 q, float3 v) { 362 | const float3 qAxis = float3(q.x, q.y, q.z); 363 | return 2.0f * dot(qAxis, v) * qAxis + (q.w * q.w - dot(qAxis, qAxis)) * v + 2.0f * q.w * cross(qAxis, v); 364 | } 365 | 366 | // ------------------------------------------------------------------------- 367 | // Sampling 368 | // ------------------------------------------------------------------------- 369 | 370 | // Samples a direction within a hemisphere oriented along +Z axis with a cosine-weighted distribution 371 | // Source: "Sampling Transformations Zoo" in Ray Tracing Gems by Shirley et al. 372 | FUNCTION float3 sampleHemisphere(float2 u, OUT_PARAMETER(float) pdf) { 373 | 374 | float a = sqrt(u.x); 375 | float b = TWO_PI * u.y; 376 | 377 | float3 result = float3( 378 | a * cos(b), 379 | a * sin(b), 380 | sqrt(1.0f - u.x)); 381 | 382 | pdf = result.z * ONE_OVER_PI; 383 | 384 | return result; 385 | } 386 | 387 | FUNCTION float3 sampleHemisphere(float2 u) { 388 | float pdf; 389 | return sampleHemisphere(u, pdf); 390 | } 391 | 392 | // For sampling of all our diffuse BRDFs we use cosine-weighted hemisphere sampling, with PDF equal to (NdotL/PI) 393 | FUNCTION float diffusePdf(float NdotL) { 394 | return NdotL * ONE_OVER_PI; 395 | } 396 | 397 | // ------------------------------------------------------------------------- 398 | // Fresnel 399 | // ------------------------------------------------------------------------- 400 | 401 | // Schlick's approximation to Fresnel term 402 | // f90 should be 1.0, except for the trick used by Schuler (see 'shadowedF90' function) 403 | FUNCTION float3 evalFresnelSchlick(float3 f0, float f90, float NdotS) 404 | { 405 | return f0 + (f90 - f0) * pow(1.0f - NdotS, 5.0f); 406 | } 407 | 408 | // Schlick's approximation to Fresnel term calculated using spherical gaussian approximation 409 | // Source: https://seblagarde.wordpress.com/2012/06/03/spherical-gaussien-approximation-for-blinn-phong-phong-and-fresnel/ by Lagarde 410 | FUNCTION float3 evalFresnelSchlickSphericalGaussian(float3 f0, float f90, float NdotV) 411 | { 412 | return f0 + (f90 - f0) * exp2((-5.55473f * NdotV - 6.983146f) * NdotV); 413 | } 414 | 415 | // Schlick's approximation to Fresnel term with Hoffman's improvement using the Lazanyi's error term 416 | // Source: "Fresnel Equations Considered Harmful" by Hoffman 417 | // Also see slides http://renderwonk.com/publications/mam2019/naty_mam2019.pdf for examples and explanation of f82 term 418 | FUNCTION float3 evalFresnelHoffman(float3 f0, float f82, float f90, float NdotS) 419 | { 420 | const float alpha = 6.0f; //< Fixed to 6 in order to put peak angle for Lazanyi's error term at 82 degrees (f82) 421 | float3 a = 17.6513846f * (f0 - f82) + 8.166666f * (float3(1.0f, 1.0f, 1.0f) - f0); 422 | return saturate(f0 + (f90 - f0) * pow(1.0f - NdotS, 5.0f) - a * NdotS * pow(1.0f - NdotS, alpha)); 423 | } 424 | 425 | FUNCTION float3 evalFresnel(float3 f0, float f90, float NdotS) 426 | { 427 | // Default is Schlick's approximation 428 | return evalFresnelSchlick(f0, f90, NdotS); 429 | } 430 | 431 | // Attenuates F90 for very low F0 values 432 | // Source: "An efficient and Physically Plausible Real-Time Shading Model" in ShaderX7 by Schuler 433 | // Also see section "Overbright highlights" in Hoffman's 2010 "Crafting Physically Motivated Shading Models for Game Development" for discussion 434 | // IMPORTANT: Note that when F0 is calculated using metalness, it's value is never less than MIN_DIELECTRICS_F0, and therefore, 435 | // this adjustment has no effect. To be effective, F0 must be authored separately, or calculated in different way. See main text for discussion. 436 | FUNCTION float shadowedF90(float3 F0) { 437 | // This scaler value is somewhat arbitrary, Schuler used 60 in his article. In here, we derive it from MIN_DIELECTRICS_F0 so 438 | // that it takes effect for any reflectance lower than least reflective dielectrics 439 | //const float t = 60.0f; 440 | const float t = (1.0f / MIN_DIELECTRICS_F0); 441 | return min(1.0f, t * luminance(F0)); 442 | } 443 | 444 | // ------------------------------------------------------------------------- 445 | // Lambert 446 | // ------------------------------------------------------------------------- 447 | 448 | FUNCTION float lambertian(const BrdfData data) { 449 | return 1.0f; 450 | } 451 | 452 | FUNCTION float3 evalLambertian(const BrdfData data) { 453 | return data.diffuseReflectance * (ONE_OVER_PI * data.NdotL); 454 | } 455 | 456 | // ------------------------------------------------------------------------- 457 | // Phong 458 | // ------------------------------------------------------------------------- 459 | 460 | // For derivation see "Phong Normalization Factor derivation" by Giesen 461 | FUNCTION float phongNormalizationTerm(float shininess) { 462 | 463 | return (1.0f + shininess) * ONE_OVER_TWO_PI; 464 | } 465 | 466 | FUNCTION float3 evalPhong(const BrdfData data) { 467 | 468 | // First convert roughness to shininess (Phong exponent) 469 | float shininess = beckmannAlphaToShininess(data.alpha); 470 | 471 | float3 R = reflect(-data.L, data.N); 472 | return data.specularF0 * (phongNormalizationTerm(shininess) * pow(max(0.0f, dot(R, data.V)), shininess) * data.NdotL); 473 | } 474 | 475 | // Samples a Phong distribution lobe oriented along +Z axis 476 | // Source: "Sampling Transformations Zoo" in Ray Tracing Gems by Shirley et al. 477 | FUNCTION float3 samplePhong(float3 Vlocal, float shininess, float2 u, OUT_PARAMETER(float) pdf) { 478 | 479 | float cosTheta = pow(1.0f - u.x, 1.0f / (1.0f + shininess)); 480 | float sinTheta = sqrt(1.0f - cosTheta * cosTheta); 481 | 482 | float phi = TWO_PI * u.y; 483 | 484 | pdf = phongNormalizationTerm(shininess) * pow(cosTheta, shininess); 485 | 486 | return float3( 487 | cos(phi) * sinTheta, 488 | sin(phi) * sinTheta, 489 | cosTheta); 490 | } 491 | 492 | FUNCTION float3 samplePhong(float3 Vlocal, float2 alpha2D, float2 u) { 493 | float shininess = beckmannAlphaToShininess(dot(alpha2D, float2(0.5f, 0.5f))); 494 | float pdf; 495 | return samplePhong(Vlocal, shininess, u, pdf); 496 | } 497 | 498 | // Sampling the specular BRDF based on Phong, includes normalization term 499 | FUNCTION float3 sampleSpecularPhong(float3 Vlocal, float alpha, float alphaSquared, float3 specularF0, float2 u, OUT_PARAMETER(float3) weight) { 500 | 501 | // First convert roughness to shininess (Phong exponent) 502 | float shininess = beckmannAlphaToShininess(alpha); 503 | 504 | float pdf; 505 | float3 LPhong = samplePhong(Vlocal, shininess, u, pdf); 506 | 507 | // Sampled LPhong is in "lobe space" - where Phong lobe is centered around +Z axis 508 | // We need to rotate it in direction of perfect reflection 509 | float3 Nlocal = float3(0.0f, 0.0f, 1.0f); 510 | float3 lobeDirection = reflect(-Vlocal, Nlocal); 511 | float3 Llocal = rotatePoint(getRotationFromZAxis(lobeDirection), LPhong); 512 | 513 | // Calculate the weight of the sample 514 | float3 Rlocal = reflect(-Llocal, Nlocal); 515 | float NdotL = max(0.00001f, dot(Nlocal, Llocal)); 516 | weight = max(float3(0.0f, 0.0f, 0.0f), specularF0 * NdotL); 517 | 518 | // Unoptimized formula was: 519 | //weight = specularF0 * (phongNormalizationTerm(shininess) * pow(max(0.0f, dot(Rlocal, Vlocal)), shininess) * NdotL) / pdf; 520 | 521 | return Llocal; 522 | } 523 | 524 | // ------------------------------------------------------------------------- 525 | // Oren-Nayar 526 | // ------------------------------------------------------------------------- 527 | 528 | // Based on Oren-Nayar's qualitative model 529 | // Source: "Generalization of Lambert's Reflectance Model" by Oren & Nayar 530 | FUNCTION float orenNayar(BrdfData data) { 531 | 532 | // Oren-Nayar roughness (sigma) is in radians - use conversion from Beckmann roughness here 533 | float sigma = beckmannAlphaToOrenNayarRoughness(data.alpha); 534 | 535 | float thetaV = acos(data.NdotV); 536 | float thetaL = acos(data.NdotL); 537 | 538 | float alpha = max(thetaV, thetaL); 539 | float beta = min(thetaV, thetaL); 540 | 541 | // Calculate cosine of azimuth angles difference - by projecting L and V onto plane defined by N. Assume L, V, N are normalized. 542 | float3 l = data.L - data.NdotL * data.N; 543 | float3 v = data.V - data.NdotV * data.N; 544 | float cosPhiDifference = dot(normalize(v), normalize(l)); 545 | 546 | float sigma2 = sigma * sigma; 547 | float A = 1.0f - 0.5f * (sigma2 / (sigma2 + 0.33f)); 548 | float B = 0.45f * (sigma2 / (sigma2 + 0.09f)); 549 | 550 | return (A + B * max(0.0f, cosPhiDifference) * sin(alpha) * tan(beta)); 551 | } 552 | 553 | FUNCTION float3 evalOrenNayar(const BrdfData data) { 554 | return data.diffuseReflectance * (orenNayar(data) * ONE_OVER_PI * data.NdotL); 555 | } 556 | 557 | // ------------------------------------------------------------------------- 558 | // Disney 559 | // ------------------------------------------------------------------------- 560 | 561 | // Disney's diffuse term 562 | // Source "Physically-Based Shading at Disney" by Burley 563 | FUNCTION float disneyDiffuse(const BrdfData data) { 564 | 565 | float FD90MinusOne = 2.0f * data.roughness * data.LdotH * data.LdotH - 0.5f; 566 | 567 | float FDL = 1.0f + (FD90MinusOne * pow(1.0f - data.NdotL, 5.0f)); 568 | float FDV = 1.0F + (FD90MinusOne * pow(1.0f - data.NdotV, 5.0f)); 569 | 570 | return FDL * FDV; 571 | } 572 | 573 | FUNCTION float3 evalDisneyDiffuse(const BrdfData data) { 574 | return data.diffuseReflectance * (disneyDiffuse(data) * ONE_OVER_PI * data.NdotL); 575 | } 576 | 577 | // Frostbite's version of Disney diffuse with energy normalization. 578 | // Source: "Moving Frostbite to Physically Based Rendering" by Lagarde & de Rousiers 579 | FUNCTION float frostbiteDisneyDiffuse(const BrdfData data) { 580 | float energyBias = 0.5f * data.roughness; 581 | float energyFactor = lerp(1.0f, 1.0f / 1.51f, data.roughness); 582 | 583 | float FD90MinusOne = energyBias + 2.0 * data.LdotH * data.LdotH * data.roughness - 1.0f; 584 | 585 | float FDL = 1.0f + (FD90MinusOne * pow(1.0f - data.NdotL, 5.0f)); 586 | float FDV = 1.0f + (FD90MinusOne * pow(1.0f - data.NdotV, 5.0f)); 587 | 588 | return FDL * FDV * energyFactor; 589 | } 590 | 591 | FUNCTION float3 evalFrostbiteDisneyDiffuse(const BrdfData data) { 592 | return data.diffuseReflectance * (frostbiteDisneyDiffuse(data) * ONE_OVER_PI * data.NdotL); 593 | } 594 | 595 | // ------------------------------------------------------------------------- 596 | // Smith G term 597 | // ------------------------------------------------------------------------- 598 | 599 | // Function to calculate 'a' parameter for lambda functions needed in Smith G term 600 | // This is a version for shape invariant (isotropic) NDFs 601 | // Note: makse sure NdotS is not negative 602 | FUNCTION float Smith_G_a(float alpha, float NdotS) { 603 | return NdotS / (max(0.00001f, alpha) * sqrt(1.0f - min(0.99999f, NdotS * NdotS))); 604 | } 605 | 606 | // Lambda function for Smith G term derived for GGX distribution 607 | FUNCTION float Smith_G_Lambda_GGX(float a) { 608 | return (-1.0f + sqrt(1.0f + (1.0f / (a * a)))) * 0.5f; 609 | } 610 | 611 | // Lambda function for Smith G term derived for Beckmann distribution 612 | // This is Walter's rational approximation (avoids evaluating of error function) 613 | // Source: "Real-time Rendering", 4th edition, p.339 by Akenine-Moller et al. 614 | // Note that this formulation is slightly optimized and different from Walter's 615 | FUNCTION float Smith_G_Lambda_Beckmann_Walter(float a) { 616 | if (a < 1.6f) { 617 | return (1.0f - (1.259f - 0.396f * a) * a) / ((3.535f + 2.181f * a) * a); 618 | //return ((1.0f + (2.276f + 2.577f * a) * a) / ((3.535f + 2.181f * a) * a)) - 1.0f; //< Walter's original 619 | } else { 620 | return 0.0f; 621 | } 622 | } 623 | 624 | // Smith G1 term (masking function) 625 | // This non-optimized version uses NDF specific lambda function (G_Lambda) resolved bia macro based on selected NDF 626 | FUNCTION float Smith_G1_General(float a) { 627 | return 1.0f / (1.0f + Smith_G_Lambda(a)); 628 | } 629 | 630 | // Smith G1 term (masking function) optimized for GGX distribution (by substituting G_Lambda_GGX into G1) 631 | FUNCTION float Smith_G1_GGX(float a) { 632 | float a2 = a * a; 633 | return 2.0f / (sqrt((a2 + 1.0f) / a2) + 1.0f); 634 | } 635 | 636 | // Smith G1 term (masking function) further optimized for GGX distribution (by substituting G_a into G1_GGX) 637 | FUNCTION float Smith_G1_GGX(float alpha, float NdotS, float alphaSquared, float NdotSSquared) { 638 | return 2.0f / (sqrt(((alphaSquared * (1.0f - NdotSSquared)) + NdotSSquared) / NdotSSquared) + 1.0f); 639 | } 640 | 641 | // Smith G1 term (masking function) optimized for Beckmann distribution (by substituting G_Lambda_Beckmann_Walter into G1) 642 | // Source: "Microfacet Models for Refraction through Rough Surfaces" by Walter et al. 643 | FUNCTION float Smith_G1_Beckmann_Walter(float a) { 644 | if (a < 1.6f) { 645 | return ((3.535f + 2.181f * a) * a) / (1.0f + (2.276f + 2.577f * a) * a); 646 | } else { 647 | return 1.0f; 648 | } 649 | } 650 | 651 | FUNCTION float Smith_G1_Beckmann_Walter(float alpha, float NdotS, float alphaSquared, float NdotSSquared) { 652 | return Smith_G1_Beckmann_Walter(Smith_G_a(alpha, NdotS)); 653 | } 654 | 655 | // Smith G2 term (masking-shadowing function) 656 | // Separable version assuming independent (uncorrelated) masking and shadowing, uses G1 functions for selected NDF 657 | FUNCTION float Smith_G2_Separable(float alpha, float NdotL, float NdotV) { 658 | float aL = Smith_G_a(alpha, NdotL); 659 | float aV = Smith_G_a(alpha, NdotV); 660 | return Smith_G1(aL) * Smith_G1(aV); 661 | } 662 | 663 | // Smith G2 term (masking-shadowing function) 664 | // Height correlated version - non-optimized, uses G_Lambda functions for selected NDF 665 | FUNCTION float Smith_G2_Height_Correlated(float alpha, float NdotL, float NdotV) { 666 | float aL = Smith_G_a(alpha, NdotL); 667 | float aV = Smith_G_a(alpha, NdotV); 668 | return 1.0f / (1.0f + Smith_G_Lambda(aL) + Smith_G_Lambda(aV)); 669 | } 670 | 671 | // Smith G2 term (masking-shadowing function) for GGX distribution 672 | // Separable version assuming independent (uncorrelated) masking and shadowing - optimized by substituing G_Lambda for G_Lambda_GGX and 673 | // dividing by (4 * NdotL * NdotV) to cancel out these terms in specular BRDF denominator 674 | // Source: "Moving Frostbite to Physically Based Rendering" by Lagarde & de Rousiers 675 | // Note that returned value is G2 / (4 * NdotL * NdotV) and therefore includes division by specular BRDF denominator 676 | FUNCTION float Smith_G2_Separable_GGX_Lagarde(float alphaSquared, float NdotL, float NdotV) { 677 | float a = NdotV + sqrt(alphaSquared + NdotV * (NdotV - alphaSquared * NdotV)); 678 | float b = NdotL + sqrt(alphaSquared + NdotL * (NdotL - alphaSquared * NdotL)); 679 | return 1.0f / (a * b); 680 | } 681 | 682 | // Smith G2 term (masking-shadowing function) for GGX distribution 683 | // Height correlated version - optimized by substituing G_Lambda for G_Lambda_GGX and dividing by (4 * NdotL * NdotV) to cancel out 684 | // the terms in specular BRDF denominator 685 | // Source: "Moving Frostbite to Physically Based Rendering" by Lagarde & de Rousiers 686 | // Note that returned value is G2 / (4 * NdotL * NdotV) and therefore includes division by specular BRDF denominator 687 | FUNCTION float Smith_G2_Height_Correlated_GGX_Lagarde(float alphaSquared, float NdotL, float NdotV) { 688 | float a = NdotV * sqrt(alphaSquared + NdotL * (NdotL - alphaSquared * NdotL)); 689 | float b = NdotL * sqrt(alphaSquared + NdotV * (NdotV - alphaSquared * NdotV)); 690 | return 0.5f / (a + b); 691 | } 692 | 693 | // Smith G2 term (masking-shadowing function) for GGX distribution 694 | // Height correlated version - approximation by Hammon 695 | // Source: "PBR Diffuse Lighting for GGX + Smith Microsurfaces", slide 84 by Hammon 696 | // Note that returned value is G2 / (4 * NdotL * NdotV) and therefore includes division by specular BRDF denominator 697 | FUNCTION float Smith_G2_Height_Correlated_GGX_Hammon(float alpha, float NdotL, float NdotV) { 698 | return 0.5f / (lerp(2.0f * NdotL * NdotV, NdotL + NdotV, alpha)); 699 | } 700 | 701 | // A fraction G2/G1 where G2 is height correlated can be expressed using only G1 terms 702 | // Source: "Implementing a Simple Anisotropic Rough Diffuse Material with Stochastic Evaluation", Appendix A by Heitz & Dupuy 703 | FUNCTION float Smith_G2_Over_G1_Height_Correlated(float alpha, float alphaSquared, float NdotL, float NdotV) { 704 | float G1V = Smith_G1(alpha, NdotV, alphaSquared, NdotV * NdotV); 705 | float G1L = Smith_G1(alpha, NdotL, alphaSquared, NdotL * NdotL); 706 | return G1L / (G1V + G1L - G1V * G1L); 707 | } 708 | 709 | // Evaluates G2 for selected configuration (GGX/Beckmann, optimized/non-optimized, separable/height-correlated) 710 | // Note that some paths aren't optimized too much... 711 | // Also note that when USE_OPTIMIZED_G2 is specified, returned value will be: G2 / (4 * NdotL * NdotV) if GG-X is selected 712 | FUNCTION float Smith_G2(float alpha, float alphaSquared, float NdotL, float NdotV) { 713 | 714 | #if USE_OPTIMIZED_G2 && (MICROFACET_DISTRIBUTION == GGX) 715 | #if USE_HEIGHT_CORRELATED_G2 716 | #define G2_DIVIDED_BY_DENOMINATOR 1 717 | return Smith_G2_Height_Correlated_GGX_Lagarde(alphaSquared, NdotL, NdotV); 718 | #else 719 | #define G2_DIVIDED_BY_DENOMINATOR 1 720 | return Smith_G2_Separable_GGX_Lagarde(alphaSquared, NdotL, NdotV); 721 | #endif 722 | #else 723 | #if USE_HEIGHT_CORRELATED_G2 724 | return Smith_G2_Height_Correlated(alpha, NdotL, NdotV); 725 | #else 726 | return Smith_G2_Separable(alpha, NdotL, NdotV); 727 | #endif 728 | #endif 729 | 730 | } 731 | 732 | // ------------------------------------------------------------------------- 733 | // Normal distribution functions 734 | // ------------------------------------------------------------------------- 735 | 736 | FUNCTION float Beckmann_D(float alphaSquared, float NdotH) 737 | { 738 | float cos2Theta = NdotH * NdotH; 739 | float numerator = exp((cos2Theta - 1.0f) / (alphaSquared * cos2Theta)); 740 | float denominator = PI * alphaSquared * cos2Theta * cos2Theta; 741 | return numerator / denominator; 742 | } 743 | 744 | FUNCTION float GGX_D(float alphaSquared, float NdotH) { 745 | float b = ((alphaSquared - 1.0f) * NdotH * NdotH + 1.0f); 746 | return alphaSquared / (PI * b * b); 747 | } 748 | 749 | // ------------------------------------------------------------------------- 750 | // Microfacet model 751 | // ------------------------------------------------------------------------- 752 | 753 | // Samples a microfacet normal for the GGX distribution using VNDF method. 754 | // Source: "Sampling the GGX Distribution of Visible Normals" by Heitz 755 | // Source: "Sampling Visible GGX Normals with Spherical Caps" by Dupuy & Benyoub 756 | // Random variables 'u' must be in <0;1) interval 757 | // PDF is 'G1(NdotV) * D' 758 | FUNCTION float3 sampleGGXVNDF(float3 Ve, float2 alpha2D, float2 u) { 759 | 760 | // Section 3.2: transforming the view direction to the hemisphere configuration 761 | float3 Vh = normalize(float3(alpha2D.x * Ve.x, alpha2D.y * Ve.y, Ve.z)); 762 | 763 | #if USE_VNDF_WITH_SPHERICAL_CAPS 764 | 765 | // Source: "Sampling Visible GGX Normals with Spherical Caps" by Dupuy & Benyoub 766 | 767 | // Sample a spherical cap in (-Vh.z, 1] 768 | float phi = 2.0f * PI * u.x; 769 | float z = ((1.0f - u.y) * (1.0f + Vh.z)) - Vh.z; 770 | float sinTheta = sqrt(clamp(1.0f - z * z, 0.0f, 1.0f)); 771 | float x = sinTheta * cos(phi); 772 | float y = sinTheta * sin(phi); 773 | 774 | // compute halfway direction; 775 | float3 Nh = float3(x, y, z) + Vh; 776 | 777 | #else 778 | 779 | // Source: "Sampling the GGX Distribution of Visible Normals" by Heitz 780 | // See also https://hal.inria.fr/hal-00996995v1/document and http://jcgt.org/published/0007/04/01/ 781 | 782 | // Section 4.1: orthonormal basis (with special case if cross product is zero) 783 | float lensq = Vh.x * Vh.x + Vh.y * Vh.y; 784 | float3 T1 = lensq > 0.0f ? float3(-Vh.y, Vh.x, 0.0f) * rsqrt(lensq) : float3(1.0f, 0.0f, 0.0f); 785 | float3 T2 = cross(Vh, T1); 786 | 787 | // Section 4.2: parameterization of the projected area 788 | float r = sqrt(u.x); 789 | float phi = TWO_PI * u.y; 790 | float t1 = r * cos(phi); 791 | float t2 = r * sin(phi); 792 | float s = 0.5f * (1.0f + Vh.z); 793 | t2 = lerp(sqrt(1.0f - t1 * t1), t2, s); 794 | 795 | // Section 4.3: reprojection onto hemisphere 796 | float3 Nh = t1 * T1 + t2 * T2 + sqrt(max(0.0f, 1.0f - t1 * t1 - t2 * t2)) * Vh; 797 | 798 | #endif 799 | 800 | // Section 3.4: transforming the normal back to the ellipsoid configuration 801 | return normalize(float3(alpha2D.x * Nh.x, alpha2D.y * Nh.y, max(0.0f, Nh.z))); 802 | } 803 | 804 | // PDF of sampling a reflection vector L using 'sampleGGXVNDF'. 805 | // Note that PDF of sampling given microfacet normal is (G1 * D) when vectors are in local space (in the hemisphere around shading normal). 806 | // Remaining terms (1.0f / (4.0f * NdotV)) are specific for reflection case, and come from multiplying PDF by jacobian of reflection operator 807 | FUNCTION float sampleGGXVNDFReflectionPdf(float alpha, float alphaSquared, float NdotH, float NdotV, float LdotH) { 808 | NdotH = max(0.00001f, NdotH); 809 | NdotV = max(0.00001f, NdotV); 810 | return (GGX_D(max(0.00001f, alphaSquared), NdotH) * Smith_G1_GGX(alpha, NdotV, alphaSquared, NdotV * NdotV)) / (4.0f * NdotV); 811 | } 812 | 813 | // "Walter's trick" is an adjustment of alpha value for Walter's sampling to reduce maximal weight of sample to about 4 814 | // Source: "Microfacet Models for Refraction through Rough Surfaces" by Walter et al., page 8 815 | FUNCTION float waltersTrick(float alpha, float NdotV) { 816 | return (1.2f - 0.2f * sqrt(abs(NdotV))) * alpha; 817 | } 818 | 819 | // PDF of sampling a reflection vector L using 'sampleBeckmannWalter' or 'sampleGGXWalter'. 820 | // Note that PDF of sampling given microfacet normal is (D * NdotH). Remaining terms (1.0f / (4.0f * LdotH)) are specific for 821 | // reflection case, and come from multiplying PDF by jacobian of reflection operator 822 | FUNCTION float sampleWalterReflectionPdf(float alpha, float alphaSquared, float NdotH, float NdotV, float LdotH) { 823 | NdotH = max(0.00001f, NdotH); 824 | LdotH = max(0.00001f, LdotH); 825 | return Microfacet_D(max(0.00001f, alphaSquared), NdotH) * NdotH / (4.0f * LdotH); 826 | } 827 | 828 | // Samples a microfacet normal for the Beckmann distribution using walter's method. 829 | // Source: "Microfacet Models for Refraction through Rough Surfaces" by Walter et al. 830 | // PDF is 'D * NdotH' 831 | FUNCTION float3 sampleBeckmannWalter(float3 Vlocal, float2 alpha2D, float2 u) { 832 | float alpha = dot(alpha2D, float2(0.5f, 0.5f)); 833 | 834 | // Equations (28) and (29) from Walter's paper for Beckmann distribution 835 | float tanThetaSquared = -(alpha * alpha) * log(1.0f - u.x); 836 | float phi = TWO_PI * u.y; 837 | 838 | // Calculate cosTheta and sinTheta needed for conversion to H vector 839 | float cosTheta = rsqrt(1.0f + tanThetaSquared); 840 | float sinTheta = sqrt(1.0f - cosTheta * cosTheta); 841 | 842 | // Convert sampled spherical coordinates to H vector 843 | return normalize(float3(sinTheta * cos(phi), sinTheta * sin(phi), cosTheta)); 844 | } 845 | 846 | // Samples a microfacet normal for the GG-X distribution using walter's method. 847 | // Source: "Microfacet Models for Refraction through Rough Surfaces" by Walter et al. 848 | // PDF is 'D * NdotH' 849 | FUNCTION float3 sampleGGXWalter(float3 Vlocal, float2 alpha2D, float2 u) { 850 | float alpha = dot(alpha2D, float2(0.5f, 0.5f)); 851 | float alphaSquared = alpha * alpha; 852 | 853 | // Calculate cosTheta and sinTheta needed for conversion to H vector 854 | float cosThetaSquared = (1.0f - u.x) / ((alphaSquared - 1.0f) * u.x + 1.0f); 855 | float cosTheta = sqrt(cosThetaSquared); 856 | float sinTheta = sqrt(1.0f - cosThetaSquared); 857 | float phi = TWO_PI * u.y; 858 | 859 | // Convert sampled spherical coordinates to H vector 860 | return normalize(float3(sinTheta * cos(phi), sinTheta * sin(phi), cosTheta)); 861 | } 862 | 863 | // Weight for the reflection ray sampled from GGX distribution using VNDF method 864 | FUNCTION float specularSampleWeightGGXVNDF(float alpha, float alphaSquared, float NdotL, float NdotV, float HdotL, float NdotH) { 865 | #if USE_HEIGHT_CORRELATED_G2 866 | return Smith_G2_Over_G1_Height_Correlated(alpha, alphaSquared, NdotL, NdotV); 867 | #else 868 | return Smith_G1_GGX(alpha, NdotL, alphaSquared, NdotL * NdotL); 869 | #endif 870 | } 871 | 872 | // Weight for the reflection ray sampled from Beckmann distribution using Walter's method 873 | FUNCTION float specularSampleWeightBeckmannWalter(float alpha, float alphaSquared, float NdotL, float NdotV, float HdotL, float NdotH) { 874 | return (HdotL * Smith_G2(alpha, alphaSquared, NdotL, NdotV)) / (NdotV * NdotH); 875 | } 876 | 877 | // Weight for the reflection ray sampled from GGX distribution using Walter's method 878 | FUNCTION float specularSampleWeightGGXWalter(float alpha, float alphaSquared, float NdotL, float NdotV, float HdotL, float NdotH) { 879 | #if USE_OPTIMIZED_G2 880 | return (NdotL * HdotL * Smith_G2(alpha, alphaSquared, NdotL, NdotV) * 4.0f) / NdotH; 881 | #else 882 | return (HdotL * Smith_G2(alpha, alphaSquared, NdotL, NdotV)) / (NdotV * NdotH); 883 | #endif 884 | } 885 | 886 | // Samples a reflection ray from the rough surface using selected microfacet distribution and sampling method 887 | // Resulting weight includes multiplication by cosine (NdotL) term 888 | FUNCTION float3 sampleSpecularMicrofacet(float3 Vlocal, float alpha, float alphaSquared, float3 specularF0, float2 u, OUT_PARAMETER(float3) weight) { 889 | 890 | // Sample a microfacet normal (H) in local space 891 | float3 Hlocal; 892 | if (alpha == 0.0f) { 893 | // Fast path for zero roughness (perfect reflection), also prevents NaNs appearing due to divisions by zeroes 894 | Hlocal = float3(0.0f, 0.0f, 1.0f); 895 | } else { 896 | // For non-zero roughness, this calls VNDF sampling for GG-X distribution or Walter's sampling for Beckmann distribution 897 | Hlocal = sampleSpecularHalfVector(Vlocal, float2(alpha, alpha), u); 898 | } 899 | 900 | // Reflect view direction to obtain light vector 901 | float3 Llocal = reflect(-Vlocal, Hlocal); 902 | 903 | // Note: HdotL is same as HdotV here 904 | // Clamp dot products here to small value to prevent numerical instability. Assume that rays incident from below the hemisphere have been filtered 905 | float HdotL = max(0.00001f, min(1.0f, dot(Hlocal, Llocal))); 906 | const float3 Nlocal = float3(0.0f, 0.0f, 1.0f); 907 | float NdotL = max(0.00001f, min(1.0f, dot(Nlocal, Llocal))); 908 | float NdotV = max(0.00001f, min(1.0f, dot(Nlocal, Vlocal))); 909 | float NdotH = max(0.00001f, min(1.0f, dot(Nlocal, Hlocal))); 910 | float3 F = evalFresnel(specularF0, shadowedF90(specularF0), HdotL); 911 | 912 | // Calculate weight of the sample specific for selected sampling method 913 | // (this is microfacet BRDF divided by PDF of sampling method - notice how most terms cancel out) 914 | weight = F * specularSampleWeight(alpha, alphaSquared, NdotL, NdotV, HdotL, NdotH); 915 | 916 | return Llocal; 917 | } 918 | 919 | // Evaluates microfacet specular BRDF 920 | FUNCTION float3 evalMicrofacet(const BrdfData data) { 921 | 922 | float D = Microfacet_D(max(0.00001f, data.alphaSquared), data.NdotH); 923 | float G2 = Smith_G2(data.alpha, data.alphaSquared, data.NdotL, data.NdotV); 924 | //float3 F = evalFresnel(data.specularF0, shadowedF90(data.specularF0), data.VdotH); //< Unused, F is precomputed already 925 | 926 | #if G2_DIVIDED_BY_DENOMINATOR 927 | return data.F * (G2 * D * data.NdotL); 928 | #else 929 | return ((data.F * G2 * D) / (4.0f * data.NdotL * data.NdotV)) * data.NdotL; 930 | #endif 931 | } 932 | 933 | // ------------------------------------------------------------------------- 934 | // Combined BRDF 935 | // ------------------------------------------------------------------------- 936 | 937 | // Precalculates commonly used terms in BRDF evaluation 938 | // Clamps around dot products prevent NaNs and ensure numerical stability, but make sure to 939 | // correctly ignore rays outside of the sampling hemisphere, by using 'Vbackfacing' and 'Lbackfacing' flags 940 | FUNCTION BrdfData prepareBRDFData(float3 N, float3 L, float3 V, MaterialProperties material) { 941 | BrdfData data; 942 | 943 | // Evaluate VNHL vectors 944 | data.V = V; 945 | data.N = N; 946 | data.H = normalize(L + V); 947 | data.L = L; 948 | 949 | float NdotL = dot(N, L); 950 | float NdotV = dot(N, V); 951 | data.Vbackfacing = (NdotV <= 0.0f); 952 | data.Lbackfacing = (NdotL <= 0.0f); 953 | 954 | // Clamp NdotS to prevent numerical instability. Assume vectors below the hemisphere will be filtered using 'Vbackfacing' and 'Lbackfacing' flags 955 | data.NdotL = min(max(0.00001f, NdotL), 1.0f); 956 | data.NdotV = min(max(0.00001f, NdotV), 1.0f); 957 | 958 | data.LdotH = saturate(dot(L, data.H)); 959 | data.NdotH = saturate(dot(N, data.H)); 960 | data.VdotH = saturate(dot(V, data.H)); 961 | 962 | // Unpack material properties 963 | data.specularF0 = baseColorToSpecularF0(material.baseColor, material.metalness, material.reflectance); 964 | data.diffuseReflectance = baseColorToDiffuseReflectance(material.baseColor, material.metalness); 965 | 966 | // Unpack 'perceptively linear' -> 'linear' -> 'squared' roughness 967 | data.roughness = material.roughness; 968 | data.alpha = material.roughness * material.roughness; 969 | data.alphaSquared = data.alpha * data.alpha; 970 | 971 | // Pre-calculate some more BRDF terms 972 | data.F = evalFresnel(data.specularF0, shadowedF90(data.specularF0), data.LdotH); 973 | 974 | return data; 975 | } 976 | 977 | // This is an entry point for evaluation of all other BRDFs based on selected configuration (for direct light) 978 | FUNCTION float3 evalCombinedBRDF(float3 N, float3 L, float3 V, MaterialProperties material) { 979 | 980 | // Prepare data needed for BRDF evaluation - unpack material properties and evaluate commonly used terms (e.g. Fresnel, NdotL, ...) 981 | const BrdfData data = prepareBRDFData(N, L, V, material); 982 | 983 | // Ignore V and L rays "below" the hemisphere 984 | if (data.Vbackfacing || data.Lbackfacing) return float3(0.0f, 0.0f, 0.0f); 985 | 986 | // Eval specular and diffuse BRDFs 987 | float3 specular = evalSpecular(data); 988 | float3 diffuse = evalDiffuse(data); 989 | 990 | // Combine specular and diffuse layers 991 | #if COMBINE_BRDFS_WITH_FRESNEL 992 | // Specular is already multiplied by F, just attenuate diffuse 993 | return (float3(1.0f, 1.0f, 1.0f) - data.F) * diffuse + specular; 994 | #else 995 | return diffuse + specular; 996 | #endif 997 | } 998 | 999 | // This is an entry point for evaluation of all other BRDFs based on selected configuration (for indirect light) 1000 | FUNCTION bool evalIndirectCombinedBRDF(float2 u, float3 shadingNormal, float3 geometryNormal, float3 V, MaterialProperties material, const int brdfType, OUT_PARAMETER(float3) rayDirection, OUT_PARAMETER(float3) sampleWeight) { 1001 | 1002 | // Ignore incident ray coming from "below" the hemisphere 1003 | if (dot(shadingNormal, V) <= 0.0f) return false; 1004 | 1005 | // Transform view direction into local space of our sampling routines 1006 | // (local space is oriented so that its positive Z axis points along the shading normal) 1007 | float4 qRotationToZ = getRotationToZAxis(shadingNormal); 1008 | float3 Vlocal = rotatePoint(qRotationToZ, V); 1009 | const float3 Nlocal = float3(0.0f, 0.0f, 1.0f); 1010 | 1011 | float3 rayDirectionLocal = float3(0.0f, 0.0f, 0.0f); 1012 | 1013 | if (brdfType == DIFFUSE_TYPE) { 1014 | 1015 | // Sample diffuse ray using cosine-weighted hemisphere sampling 1016 | rayDirectionLocal = sampleHemisphere(u); 1017 | const BrdfData data = prepareBRDFData(Nlocal, rayDirectionLocal, Vlocal, material); 1018 | 1019 | // Function 'diffuseTerm' is predivided by PDF of sampling the cosine weighted hemisphere 1020 | sampleWeight = data.diffuseReflectance * diffuseTerm(data); 1021 | 1022 | #if COMBINE_BRDFS_WITH_FRESNEL 1023 | // 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 1024 | float3 Hspecular = sampleSpecularHalfVector(Vlocal, float2(data.alpha, data.alpha), u); 1025 | 1026 | #if USE_WALTER_GGX_SAMPLING 1027 | // Check if specular sample is valid (does not reflect under the hemisphere) 1028 | float VdotH = dot(Vlocal, Hspecular); 1029 | if (VdotH > 0.00001f) 1030 | { 1031 | sampleWeight *= (float3(1.0f, 1.0f, 1.0f) - evalFresnel(data.specularF0, shadowedF90(data.specularF0), min(1.0f, VdotH))); 1032 | } else { 1033 | return false; 1034 | } 1035 | #else 1036 | // Clamp HdotL to small value to prevent numerical instability. Assume that rays incident from below the hemisphere have been filtered 1037 | // Note: VdotH is always positive for VNDF sampling so we don't need to test if it's positive like we do for sampling with Walter's method 1038 | float VdotH = max(0.00001f, min(1.0f, dot(Vlocal, Hspecular))); 1039 | sampleWeight *= (float3(1.0f, 1.0f, 1.0f) - evalFresnel(data.specularF0, shadowedF90(data.specularF0), VdotH)); 1040 | #endif 1041 | #endif 1042 | 1043 | } else if (brdfType == SPECULAR_TYPE) { 1044 | const BrdfData data = prepareBRDFData(Nlocal, float3(0.0f, 0.0f, 1.0f) /* unused L vector */, Vlocal, material); 1045 | rayDirectionLocal = sampleSpecular(Vlocal, data.alpha, data.alphaSquared, data.specularF0, u, sampleWeight); 1046 | } 1047 | 1048 | // Prevent tracing direction with no contribution 1049 | if (luminance(sampleWeight) == 0.0f) return false; 1050 | 1051 | // Transform sampled direction Llocal back to V vector space 1052 | rayDirection = normalize(rotatePoint(invertRotation(qRotationToZ), rayDirectionLocal)); 1053 | 1054 | // Prevent tracing direction "under" the hemisphere (behind the triangle) 1055 | if (dot(geometryNormal, rayDirection) <= 0.0f) return false; 1056 | 1057 | return true; 1058 | } 1059 | 1060 | // Approximates the directional-hemispherical reflectance of the micriofacet specular BRDF with GG-X distribution 1061 | // Source: "Accurate Real-Time Specular Reflections with Radiance Caching" in Ray Tracing Gems by Hirvonen et al. 1062 | FUNCTION float3 specularGGXReflectanceApprox(float3 specularF0, float alpha, float NdotV) 1063 | { 1064 | #if USE_HEIGHT_CORRELATED_G2 1065 | const float2x2 A = float2x2( 1066 | 0.995367f, -1.38839f, 1067 | -0.24751f, 1.97442f 1068 | ); 1069 | 1070 | const float3x3 B = float3x3( 1071 | 1.0f, 2.68132f, 52.366f, 1072 | 16.0932f, -3.98452f, 59.3013f, 1073 | -5.18731f, 255.259f, 2544.07f 1074 | ); 1075 | 1076 | const float2x2 C = float2x2( 1077 | -0.0564526f, 3.82901f, 1078 | 16.91f, -11.0303f 1079 | ); 1080 | 1081 | const float3x3 D = float3x3( 1082 | 1.0f, 4.11118f, -1.37886f, 1083 | 19.3254f, -28.9947f, 16.9514f, 1084 | 0.545386f, 96.0994f, -79.4492f 1085 | ); 1086 | #else 1087 | const float2x2 A = float2x2( 1088 | 0.99044f, -1.28514f, 1089 | 1.29678f, -0.755907f 1090 | ); 1091 | 1092 | const float3x3 B = float3x3( 1093 | 1.0f, 2.92338f, 59.4188f, 1094 | 20.3225f, -27.0302f, 222.592f, 1095 | 121.563f, 626.13f, 316.627f 1096 | ); 1097 | 1098 | const float2x2 C = float2x2( 1099 | 0.0365463f, 3.32707f, 1100 | 9.0632f, -9.04756f 1101 | ); 1102 | 1103 | const float3x3 D = float3x3( 1104 | 1.0f, 3.59685f, -1.36772f, 1105 | 9.04401f, -16.3174f, 9.22949f, 1106 | 5.56589f, 19.7886f, -20.2123f 1107 | ); 1108 | #endif 1109 | 1110 | const float alpha2 = alpha * alpha; 1111 | const float alpha3 = alpha * alpha2; 1112 | const float NdotV2 = NdotV * NdotV; 1113 | const float NdotV3 = NdotV * NdotV2; 1114 | 1115 | const float E = dot(mul(A, float2(1.0f, NdotV)), float2(1.0f, alpha)); 1116 | const float F = dot(mul(B, float3(1.0f, NdotV, NdotV3)), float3(1.0f, alpha, alpha3)); 1117 | 1118 | const float G = dot(mul(C, float2(1.0f, NdotV)), float2(1.0f, alpha)); 1119 | const float H = dot(mul(D, float3(1.0f, NdotV2, NdotV3)), float3(1.0f, alpha, alpha3)); 1120 | 1121 | // Turn the bias off for near-zero specular 1122 | const float biasModifier = saturate(dot(specularF0, float3(0.333333f, 0.333333f, 0.333333f)) * 50.0f); 1123 | 1124 | const float bias = max(0.0f, (E / F)) * biasModifier; 1125 | const float scale = max(0.0f, (G / H)); 1126 | 1127 | return float3(bias, bias, bias) + float3(scale, scale, scale) * specularF0; 1128 | } 1129 | 1130 | #if __cplusplus 1131 | 1132 | }; //< namespace brdf 1133 | 1134 | #endif --------------------------------------------------------------------------------