Skip to main content
GameDev.net gamedev.net
🔒 Locked

Verify my GGX + Lambert Directional light

Started by REF_Cracker Jul 30, 2015 at 11:17 PM 21 replies 8.2k views
Original Post
REF_Cracker
REF_Cracker

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
Radikalizm
Radikalizm

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!
REF_Cracker
REF_Cracker

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
Radikalizm
Radikalizm

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!
REF_Cracker
REF_Cracker

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
L. Spiro
L. Spiro

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
Hodgman
Hodgman

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?

L. Spiro
L. Spiro

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
REF_Cracker
REF_Cracker

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
L. Spiro
L. Spiro

- 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
REF_Cracker
REF_Cracker

Hi Hodgman,

So other than those tweaks to roughness.... the code for that directional light looks alright to you?

The only thing I modified from John Hable's GGX code was to 3 component the F0 term and feed in my specular color there.

Then I added the


Diffuse * PI_INV * NdotL

For a lambert diffuse contribution. I guess I can try the fresnel on diffuse as per L. Spiro's suggestion.

Right now I'm just struggling with the fact that the specular seems way too overpowering in sun light. Even for materials with 0.04 for F0 and a high roughness.

Thanks,

Check out my project @ www.exitearth.com
REF_Cracker
REF_Cracker

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.

L. Spiro

I guess you're right as I already want to scale the specular... in fact when I hardcode a constant of (0.25 * specular) I get quite a good looking result.
Would this material param be a 3 component color or a simple single scaler?

Thanks,


Check out my project @ www.exitearth.com
Hodgman
Hodgman

Right now I'm just struggling with the fact that the specular seems way too overpowering in sun light. Even for materials with 0.04 for F0 and a high roughness.

How bright is your sun? smile.png (and how rough is your roughness?)
I get this as well in-game with my super-bright sun, but it doesn't show up as an issue in the artist's tools because they don't have such bright lights in there...

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.

His lighting function already takes separate in_diffuse and in_specular material colour parameters.
For skin, in_diffuse would be skin coloured, and in_specular would be (float3)0.04, etc...

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.

He can already plug in textures to his variables though...?

His calculateDiffuseAndSpecular function is based on the becoming-popular "metalness" workflow, where you author a "base colour" texture, which is either used as albedo for non-metals, or as F0 for metals, and then you author a "metalness mask" instead of a specular map. Metals are then given black albedo, and non-metals are given ~0.04 F0.

Multiplying by specular colour at the end (instead of using it as an input to the F function) is an approximate optimization, not one you should do by default.
e.g. gold has a yelowish (or, gold...) specular material colour, but at glancing angles (when L·H/V·H is low), the specular colour is modified by the Fresnel equation to become closer to white. Seeing as H is light-dependent, different lights will then have different specular material colours.
If you perform your optimization, your glancing-angle specular highlights on metal surfaces will appear too saturated with colour, instead of becoming saturated at glancing angles.

REF_Cracker
REF_Cracker

How bright is your sun? smile.png (and how rough is your roughness?)

I get this as well in-game with my super-bright sun, but it doesn't show up as an issue in the artist's tools because they don't have such bright lights in there...

My sun is currently a yellowish white multiplied with a slider ..... I've got that slider pumped up to exp2(3.5) ..... so it's at ~11.31
That's where it starts to feel hot enough but not blow everything out to white. The specular is WAY overpowering though.

Thank you!

Edit: The roughness is about 0.75 on the ground!

L. Spiro
L. Spiro

His calculateDiffuseAndSpecular function is based on the becoming-popular "metalness" workflow, where you author a "base colour" texture, which is either used as albedo for non-metals, or as F0 for metals, and then you author a "metalness mask" instead of a specular map.
Metals are given black albedo, and non-metals are given 0.04 F0.

Oh, I thought that was a reference function he was posting, not his own.

the specular colour is modified by the Fresnel equation to become closer to white

I see that now. This is the first time I am hearing about Fresnel modifying color directly rather than modifying its intensity.


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
Hodgman
Hodgman

Fresnel modifying color directly rather than modifying its intensity

It's not very intuitive at first, but if you don't think of it as colour then it make sense smile.png Instead, picture that we're making 3 greyscale pictures, which are illuminated by 3 different wavelengths of light.
If you measure the IOR of copper with red light, you get an F0 value of ~96%, but if you measure it using green light, you get an F0 value of of ~64%.
Fresnel is just increasing the intensity of the specular from the base F0 value up to 100%. The catch is that F0 is a wavelength-dependent value, which results in a "coloured" curve.

L. Spiro
L. Spiro
I’ve been thinking back on papers and graphs I have seen for metals and suddenly it all falls into place.
Yes, it makes sense that it is a color curve towards white at glancing angles, with separate curves for the separate wavelengths.
And dielectrics always have a white specular color, so you just use F0 as a single value and don’t multiply the specular by a “specular material color” at the end (because it is white).

Suddenly 10,000 things just clicked. What a phull I have been all my life!!


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
Eternal
Eternal




#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].

Physically this also isn't correct, I think, as you have to account for refraction through the specular layer twice, once using specular fresnel and once using diffuse fresnel.

Burley shows this here https://disney-animation.s3.amazonaws.com/library/s2012_pbs_disney_brdf_notes_v2.pdf (Section 5.3), points out that doing the "correct" thing doesn't look great and then describes Disney's diffuse BRDF, which tries to maintain physical plausibility, while matching measured BRDFs more closely.

REF_Cracker
REF_Cracker

Thanks for the input so far everyone.
I'm going to dive back into it here this morning and see if I can get some better results out of my specular.
I still feel a scalar value to control the specular contribution would be helpful with my high sun light environments.


Check out my project @ www.exitearth.com

Topic Locked

This topic has been locked by a moderator. New replies are not allowed.

Sign in to reply to this topic.