Search Unity

Quick question on PBS / GGX:

Discussion in 'Shaders' started by metaleap, Jul 2, 2014.

  1. metaleap

    metaleap

    Joined:
    Oct 3, 2012
    Posts:
    589
    You know the standard energy-preserving "modular" specular formula:

    Code (csharp):
    1.  
    2. specTerm = D * F * G / (4 * dotNL * dotNV);
    3.  
    Where F is just the standard fresnelSchlick(f0, dotHL) and D and G are "pluggable" normal-distribution and geometry-visibility terms.

    Main question: does anyone know whether this above formula is supposed to always result in a specific range such as 0..1 or other? Or is it permitted, even expected, to "go up arbitrarily"?

    Reason I'm asking, I have arrived at a sort-of-GGX implementation based (after various other detours) largely on Disney. But it's creating overly bright specular highlights. They're actually pretty cool but a lot flickering in small normal-cavities and a little too much blooming (with an otherwise reasonable/non-exaggerated bloom setup as far as thresholds etc are concerned) --- this is all fixed and nicely normalized as soon as I saturate(specTerm).

    But first of all that feels messy, no one else seems to do that or ever suggest that D*F*G/4*nl*nv should be saturated.

    Secondly, when saturating the spec-highlights are still correct and clean but actually I wouldn't mind a *little* brighter. So I could clamp, seems like clamp(specTerm, 0, 12) would result in a bloomy shinyness that'd work for me without going as crazy as unclamped does.

    But this bothers me, it feels... hacky! :D I know I know, everything in realtime graphics is a huge pile of hacks, but still!

    Here's my D:

    Code (csharp):
    1.  
    2. half dotHN2 = dotHN * dotHN; // dot of half-vector and normal
    3. half alph = roughness * roughness; // my roughness is always linear but is this even correct for GGX?
    4. half alph2 = alph * alph;
    5. half denom = dotHN2 * (alph2 - 1.0) + 1.0;
    6. half ggx_D = alph2 / (PI * denom * denom);
    7.  

    And here's my G:

    Code (csharp):
    1.  
    2. #define SQRT_2_PI 0.79788456080286535587989211986876
    3. half k = roughness * SQRT_2_PI; // others such as Hable use pow(alph/2, 2) but during many tests I settled on this variant for k, no idea where I first picked it up......
    4. half k1 = 1.0 - k;
    5. half ggx_V = (dotNLc / (dotNLc * k1 + k)) * (dotNVc / (dotNVc * k1 + k));
    6.  
    I've read somewhere that using this Smith-Schlick geometric-visibility term I can get rid of the energy-conserving part (divide by 4*nl*nv) but in my extensive experiments that does not seem to be the case for me. Could be another indicator that I might have a bug somewhere.... or that just like me, most gfx writers out there also only have half-a-semi-clue :D

    Fresnel and all the rest I don't need to post, it's the proven battle-tested standard stuff.
     
  2. Deleted User

    Deleted User

    Guest

    Okay, we need to cover a few things.

    The answer to your main question is that the equation is expected to very frequently go well above the [0,1] range. Often it reaches several thousand in intensity as smoother materials concentrate reflected energy into a smaller point. This is expected behavior.

    The flickering you see is a limitation of using tangent-space normal maps where they do not correctly mipmap. As a result, they tend to get smoother for their higher mips, causing sparkling due to specular aliasing. The proper solution for this is specular antialiasing, a problem which is an active area of research as there are no easy solutions to it.

    Punctual Lighting Equation
    So since you are doing lighting from a Punctual Light (ie. infinitely small point) rather than integrating all incoming light over a hemisphere, you actually plug your BRDFs into this equation:
    Code (CSharp):
    1. illumination = NdotL * PI * (diffuseBrdf + specularBrdf);
    The net result is that the 1/PI in your Lambert diffuse and GGX specular BRDFs is cancelled.

    Visibility vs Geometric Functions
    It is increasingly common for Physical BRDFs to combine the foreshortening term (ie. 1 / (NdotL * NdotV)) with the Geometric function to create a Visibility function. This typically takes the form of the Geometric function's numerator cancelling the foreshortening term in the denominator. Note, you must still keep the 1/4 from the same term as that does not cancel.

    Simplification:
    V = (N.L / (N.L * (1 - k) + k)) * (N.V / (N.V * (1 - k) + k)) / (N.L * N.V)
    V = (N.L * N.V) / ((N.L * (1 - k) + k) * (N.V * (1 - k) + k)) / (N.L * N.V)
    V = (N.L * N.V) / ((N.L * N.V) * (N.L * (1 - k) + k) * (N.V * (1 - k) + k))
    V = 1 / ((N.L * (1 - k) + k) * (N.V * (1 - k) + k))

    Hope that helps.
     
    Noisecrime and hippocoder like this.