Verify my GGX + Lambert Directional light

Started by
21 comments, last by Rannion 8 years, 8 months ago

I'm quite confused as to the relationship between GGX specular and Lambert diffuse... mostly concerning the placement of PI.

I've taken the GGX code from http://www.filmicworlds.com/images/ggx-opt/optimized-ggx.hlsl
(modified the F0 term to be 3 dimensional)

Basically I am adding a vanilla Lambert term divide by PI to that.

Could someone verify this to be correct \ incorrect?


float G1V(float dotNV, float k)
{
return 1.0f/(dotNV*(1.0f-k)+k);
}

float3 LightingFuncGGX_REF(float3 N, float3 V, float3 L, float roughness, float3 F0)
{
float alpha = roughness*roughness;

float3 H = normalize(V+L);

float dotNL = saturate(dot(N,L));
float dotNV = saturate(dot(N,V));
float dotNH = saturate(dot(N,H));
float dotLH = saturate(dot(L,H));

float3 F;
    float D, vis;

// D
float alphaSqr = alpha*alpha;
float pi = 3.14159f;
float denom = dotNH * dotNH *(alphaSqr-1.0) + 1.0f;
D = alphaSqr/(pi * denom * denom);

// F
float dotLH5 = pow(1.0f-dotLH,5);
F = F0 + (1.0-F0)*(dotLH5);

// V
float k = alpha/2.0f;
vis = G1V(dotNL,k)*G1V(dotNV,k);

float3 specular = dotNL * D * F * vis;
return specular;
}


float3 directionalLight(float3 N,float3 V,float3 L,float in_rough,float3 in_diffuse,float3 in_specular,float3 in_light)
{
    float NdotL = saturate(dot(N,L));

    float3 l_specular = LightingFuncGGX_REF(N,V,L,epsilonRough(in_rough),in_specular);
    float3 l_diffuse = in_diffuse * PI_INV * NdotL;

    return (l_diffuse + l_specular) * in_light;
}

Check out my project @ www.exitearth.com

Advertisement

This seems about right at first glance. A lot of implementations do compensate for the Fresnel factor in the diffuse term as well in an attempt to maintain conservation of energy. This is not completely accurate, but it's close enough.

I gets all your texture budgets!

Radikalizm... Thanks for the reply!


From what I can gather this is the correct lambert term.

Simply...


(N dot L) / PI

In low lighting conditions this looks quite good.

But in high lighting conditions such as the sun light the GGX specular really dominates the scene.

I'm wondering if it's common for games to add a specular multiplier tweakable to help better balance the scene.

If I multiply the specular contribution of the lighting with ~0.25 in a high lighting condition the scene looks a lot better. (Although metals lose a lot of their contrast)

I wonder if there is a better hack or better way to expose the scene (Currently I have a hard coded exposure feeding into the uncharted 2 tonemap)


Check out my project @ www.exitearth.com

The intensity of your specular will be generally be controlled by your roughness and F0 values. What kind of values are you generally using for F0? Are these based off of the actual (complex) index of refraction of the material you're trying to render, or are you choosing these arbitrarily? For dielectrics F0 is generally very low, and engines tend to approximate it with a fixed hardcoded value of 0.03 or 0.04. Metals get a bit more complex, but you can calculate their F0 value based off of their complex IOR. You can look these up on sites like http://refractiveindex.info/

Additionally, since surfaces are never just completely smooth or faceted an additional value can be introduced to tone down specular based on how light gets scattered away in surface cavities. John Hable (who I see you're familiar with) gives a good overview of this here: http://www.filmicworlds.com/2014/03/17/fresnel-f0-is-not-direct-reflectance/

Just a general correction, the NdotL cosine term is not actually part of your Lambertian diffuse term. Lambertian diffuse lighting comes down to 1/PI * c_diffuse

As an optimization to your code above you can actually factor out your NdotL factor from both your specular and diffuse terms and only apply it once when calculating total light contribution.

I gets all your texture budgets!

For roughness I'm using a SRGB smoothness value. (not linearly correcting it in shader or sampler state)


roughness = (1.0 - smoothness);

Most of the assets currently have fixed smoothness values until I figure out how this all works.....

dielectrics are getting fed smoothness 0.5 (roughness 0.5)
metals are getting fed around smoothness 0.75 (roughness 0.25)

Specular and diffuse colors are being modeled after UE4 style values based on metal cavity and base color. (Cavity is being fed 1.0 until I get the hang of that too)


void calculateDiffuseAndSpecular(float3 in_baseColor,float in_metal,float in_cavity,out float3 in_diffuse,out float3 in_specular)
{
    float3 l_baseColor;
    float3 l_specular;

    l_baseColor = in_cavity * in_baseColor;
    l_specular = in_cavity * 0.5;
    //0.5 is the default specularity for non metals see https://docs.unrealengine.com/latest/INT/Engine/Rendering/Materials/PhysicallyBased/index.htmlnn
    in_diffuse = lerp(l_baseColor,0.0,in_metal);
    in_specular = lerp(0.08 * l_specular,l_baseColor,in_metal);
}

So I guess I need to better author these values.
It's nice to make sure the equations look right before tweaking them.

Thanks for your help so far!

Check out my project @ www.exitearth.com

A few points to mention.
#1: Keep specular and diffuse separate. You need to multiply the accumulated specular light by the specular material color. You are combining them way too early.

#2: Your sun is too over-powering because your materials are not properly tweaked. Since you are hard-coding a lot of values you can’t expect to get good results under all lighting conditions.

#3: You are likely using or have available some authoring tool that uses common materials such as Phong and include a “specular power” term. The specular power term from Blinn, Blinn-Phong, and Phong (exponent ?, as in pow( saturate( NdotH ), ? )) can be converted to roughness (?) via:
? = sqrt( 2.0 / ? )
or:
? = sqrt( 2.0 / (? + 2.0) )
Every standard model format has access to a specular power value, and it can be converted thusly for your needs.
Note that since you are only using ?2, you can skip the sqrt() in this conversion and pass roughness as a squared value to the shaders (avoiding squaring it in the shaders).

#4: If you do link the diffuse and specular components for energy conservation, beware of an incorrect trend in the graphics industry right now.
I wrote about this in detail here.
The correct way to modify specular by Fresnel is Fspec( f0 ) = f0 + (1 - f0)(1 - V?H). This is basically correct in every resource you will find, and matches your current implementation (there is no confusion here).
The correct way to modify diffuse by Fresnel is Fdiff( f0 ) = f0 + (1 - f0)(1 - N?L), (1 - Fdiff( f0 )). Notice that the Fresnel function is different, as it uses NdotL, whereas specular uses VdotH. IE, in code:
float fSpecF = fresnel( Reflectance, VdotH );
float fDiffF = 1 - fresnel( Reflectance, NdotL );
It is a common mistake to use this instead:
float fSpecF = fresnel( Reflectance, VdotH );
//float fDiffF = 1 - fresnel( Reflectance, VdotH );
float fDiffF = 1 - fSpecF;

The amount of light that goes into the diffuse term based on Fresnel’s equation is not view-dependent. It is based purely on the angle at which the light hits the surface (NdotL).
Refer to section 2, equations 5 and 6: [Gotanda 2010].

L. Spiro

I restore Nintendo 64 video-game OST’s into HD! https://www.youtube.com/channel/UCCtX_wedtZ5BoyQBXEhnVZw/playlists?view=1&sort=lad&flow=grid

For roughness I'm using a SRGB smoothness value. (not linearly correcting it in shader or sampler state)


roughness = (1.0 - smoothness);

Using sRGB smoothness will dedicate more bits to storing dark smoothness ('rough') values, and less bits to storing bright smoothness ('smooth') values.
Most engines seem to use linear, not sRGB textures for roughness/smoothness.


//So what you've got is:
smoothness = pow(<smoothness texture>, 2.2); //approx... replace pow(x,2.2) with sRGBDecode(x)...
roughness = 1-smoothness;
alpha = roughness*roughness;

//or, alternatively:
roughness = 1 - pow(1 - <roughness texture>,2.2); 
alpha = roughness*roughness;

//when what you really want is:
roughness = <roughness texture>; 
roughness = 1 - <smoothness texture>; //or this
alpha = roughness*roughness;

Most of the assets currently have fixed smoothness values until I figure out how this all works.....
dielectrics are getting fed smoothness 0.5 (roughness 0.5)
metals are getting fed around smoothness 0.75 (roughness 0.25)

Roughness is not dependent on the type of materials smile.png plastic packaging will be a smooth dielectrics, and steel frames will be rough metal. Set up some test materials that cover the full 2D range of rough/smooth and metal/non-metal.

#1: Keep specular and diffuse separate. You need to multiply the accumulated specular light by the specular material color. You are combining them way too early.

His specular material color is F0, which is correctly used to calculate F.
Or are you talking about a second specular material colour (Ks?) as well as the F0 colour? If do you have an extra specular constant, shouldn't it be scalar instead of RGB?

His specular material color is F0, which is correctly used to calculate F.
Or are you talking about a second specular material colour (Ks?) as well as the F0 colour?

It’s correct now while he is using just that for both, but I am expecting he will soon want to have separate control over them, plus specular textures, etc. There really aren’t many real materials where specular and diffuse are the same color—even one of the most common materials in all of gaming, skin, has X diffuse and white specular.


L. Spiro

I restore Nintendo 64 video-game OST’s into HD! https://www.youtube.com/channel/UCCtX_wedtZ5BoyQBXEhnVZw/playlists?view=1&sort=lad&flow=grid

L. Spiro.. thanks for the reply,

A few things ...

- Your example of specular fresnel is using VdotH .... all resources I'm referencing including the posted one are using LdotH

- I've seen a few places mentioning this fresnel on the diffuse contribution... I will try it

- I am confused by you first point

#1: Keep specular and diffuse separate. You need to multiply the accumulated specular light by the specular material color. You are combining them way too early.

As far as I understand the specular material color is F0 this gets fed into the fresnel equation either as some small constant for dielectrics float3(0.04,0.04,0.04) or the actual base color for metals like gold float3(1.0,0.765557,0.336057)

So as far as I can tell that's the end of that color. in the equation. Am I missing something here? Does it need to be multiplied through again?


Thank you!

Check out my project @ www.exitearth.com

- Your example of specular fresnel is using VdotH .... all resources I'm referencing including the posted one are using LdotH

Mathematically they are the same, since the half vector is their average (thus at the same angle to either of them).
So that is really just convention. Most use VdotH (or EdotH) so that it is obvious that this is based off the view or eye position, but anything using H is also relative to the view/eye position.

- I've seen a few places mentioning this fresnel on the diffuse contribution... I will try it

Just be careful of the common mistakes.


- I am confused by you first point

As far as I understand the specular material color is F0 this gets fed into the fresnel equation either as some small constant for dielectrics float3(0.04,0.04,0.04) or the actual base color for metals like gold float3(1.0,0.765557,0.336057)

So as far as I can tell that's the end of that color. in the equation. Am I missing something here? Does it need to be multiplied through again?

What you have is fine for what it is, but you will likely want more control over specular, either as a material property or through specular texture maps.
When this happens you will want to accumulate these terms separately and combine them at the end.

Technically, you should be doing this anyway. From a modular design standpoint, your lighting equations should only calculate light. From a performance standpoint, when you have multiple lights you are saving one multiply per light above 1, as this multiply can be deferred until all lights are accumulated and done once instead of once per light.


L. Spiro

I restore Nintendo 64 video-game OST’s into HD! https://www.youtube.com/channel/UCCtX_wedtZ5BoyQBXEhnVZw/playlists?view=1&sort=lad&flow=grid

This topic is closed to new replies.

Advertisement