[hlsl] Cook-Torrance lighting

Started by
6 comments, last by Lifepower 17 years ago
I hope someone can shed some light on what am I doing wrong when trying to implement Cook-Torrance lighting. I've taken the code from "Advanced lighting and materials book", but unfortunately, it gives the following result (I've marked artifact by red arrow): Free Image Hosting at www.ImageShack.us You can see how the half of the sphere is "cut" instead of having smooth shade. My HLSL code is the following:

float4 ApplyCookTorrance(float3 InNormal, float3 InWorld)
{
  float3 PixelToEye = normalize(EyePos - InWorld);
  float3 HalfVector = normalize(LightVector + PixelToEye);
  float  NdotH      = max(0.0f, dot(InNormal, HalfVector));
	
  float3 RoughnessParams = {0.5f, 0.5f, 0.5f};
	
  //Start the "D" term, use Blinn Gaussian
  float Alpha = acos(NdotH);
  float C     = RoughnessParams.x;
  float m     = RoughnessParams.y;
  float D    = C * exp(-(pow(Alpha / m, 2.0f)));
	
  //Start the "G" term
  float NdotV = dot(InNormal, PixelToEye);
  float VdotH = dot(HalfVector, PixelToEye);
  float NdotL = dot(LightVector, InNormal);
  float G1 = 2 * NdotH * NdotV /  NdotH;
  float G2 = 2 * NdotH * NdotL /  NdotH;
  float G = min(1.0f, max(0.0f, min(G1, G2)));
	
  //Start the fresnel term. Use the approximation from 
  //http://developer.nvidia.com/docs/IO/3035/ATT/FresnelReflection.pdf
  float R0 = RoughnessParams.z;
  float F = R0 + (1.0f - R0) * pow(1.0f - NdotL, 5.0);
	
  float4 DiffuseColor  = {1.0f, 1.0f, 1.0f, 1.0f};

  return DiffuseColor * F * D * G / (NdotL * NdotV);
}


I would really appreciate if someone would check my code and tell what is going wrong. Thanks in advance! P.S. Eye position, light vector and world matrices are specified correctly as other lighting models I've implemented work just fine. My guess is that the problem is in the math here.
Advertisement
I'm at work right now and don't have my main resources to hand - I've implemented Cook-Torrance for D3D9 and D3D10 in recent months [smile]

Nothing specific to Cook-Torrance, but a general trick I found necessary when implementing some models - put limits in.

Roughness is typically 0.0 to 1.0 and, importantly, 0.0 is a perfectly valid input value. What happens here:

float D = C * exp(-(pow(Alpha / m, 2.0f)));

An intermediary calculation, Alpha / m, generates a divide by zero - from which all sorts of nasty things happen [oh]

Similar with your final statement:

return DiffuseColor * F * D * G / (NdotL * NdotV);

Less likely but still possible - you can view the fragment with NdotL = 0.0 at which point you've just introduced another divide-by-zero.

You can usually get around these by having a small bias: (Alpha / (m+1e-5f)) for example.

I bring these up because, if you debug it far enough, you might find that sharp cut off you highlight as the point where one of your computations is hitting an error condition and you're getting +/- INF or NaN (etc...) which then overpowers/invalidates all other calculations and the GPU outputs a (0,0,0,1) colour...

Using PIX might be VERY helpful here [wink]

Most of the original research papers omit small implementation details like this and they're not always the best/true answer but they can still be a handy trick...

hth
Jack

<hr align="left" width="25%" />
Jack Hoxley <small>[</small><small> Forum FAQ | Revised FAQ | MVP Profile | Developer Journal ]</small>

I've checked for division by zero and apparently it is not the source of the problem. However, I've found that changing the line "float G = min(1.0f, max(0.0f, min(G1, G2)));" to "float G = min(1.0f, min(G1, G2));" removes the "cut" problem, although it generates new artifact - reflection appears on the back of the sphere. :(

Jack, could you share your Cook-Torrance code for D3D9? [smile]
Quote:Original post by Lifepower
Jack, could you share your Cook-Torrance code for D3D9? [smile]
hmm, well maybe... just maybe...

float4 psCookTorrance( in VS_LIGHTING_OUTPUT v ) : COLOR{    // Sample the textures  float3  Normal        = normalize( ( 2.0f * tex2D( sampNormMap, v.TexCoord ).xyz ) - 1.0f );  float3  Specular      = tex2D( sampSpecular, v.TexCoord ).rgb;  float3  Diffuse       = tex2D( sampDiffuse, v.TexCoord ).rgb;  float2  Roughness     = tex2D( sampRoughness, v.TexCoord ).rg;    Roughness.r           *= 3.0f;    // Correct the input and compute aliases  float3  ViewDir         = normalize( v.ViewDir );  float3  LightDir        = normalize( v.LightDir );  float3  vHalf           = normalize( LightDir + ViewDir );  float  NormalDotHalf    = dot( Normal, vHalf );  float  ViewDotHalf      = dot( vHalf,  ViewDir );  float  NormalDotView    = dot( Normal, ViewDir );  float  NormalDotLight   = dot( Normal, LightDir );    // Compute the geometric term  float  G1          = ( 2.0f * NormalDotHalf * NormalDotView ) / ViewDotHalf;  float  G2          = ( 2.0f * NormalDotHalf * NormalDotLight ) / ViewDotHalf;  float  G           = min( 1.0f, max( 0.0f, min( G1, G2 ) ) );    // Compute the fresnel term  float  F          = Roughness.g + ( 1.0f - Roughness.g ) * pow( 1.0f - NormalDotView, 5.0f );    // Compute the roughness term  float  R_2        = Roughness.r * Roughness.r;  float  NDotH_2    = NormalDotHalf * NormalDotHalf;  float  A          = 1.0f / ( 4.0f * R_2 * NDotH_2 * NDotH_2 );  float  B          = exp( -( 1.0f - NDotH_2 ) / ( R_2 * NDotH_2 ) );  float  R          = A * B;    // Compute the final term  float3  S          = Specular * ( ( G * F * R ) / ( NormalDotLight * NormalDotView ) );  float3  Final      = cLightColour.rgb * max( 0.0f, NormalDotLight ) * ( Diffuse + S );    return float4( Final, 1.0f );}


The above is a straight copy-n-paste from my final year's disseration at the University of Nottingham. From empirical testing it appears to be correct, but I can't say I exhaustively tested all scenarios.

Jack

<hr align="left" width="25%" />
Jack Hoxley <small>[</small><small> Forum FAQ | Revised FAQ | MVP Profile | Developer Journal ]</small>

I've adapted your code to my pixel shader and it works like charm! Many thanks!!

Btw, is your dissertation publicly available? Sounds very interesting.
I'm not that familar with Cook-Torrance, but I do believe the problem is that for your G1 and G2 terms the product should be divided by the dot product of the View and Half vectors (VdotH in your code). The giveaway is that you calculate VdotH, and never use it. :P
Quote:Original post by Lifepower
I've adapted your code to my pixel shader and it works like charm! Many thanks!!
Glad to hear it worked out [grin]

Quote:Original post by Lifepower
is your dissertation publicly available? Sounds very interesting.
Unfortunately it's not publicly available - the university holds all publication rights and I don't think they bother publishing undergraduate dissertations.

Most of my work was in applying GPU-based rendering to new areas of computer graphics rather than specifically inventing/researching new techniques. Books like Advanced Lighting and Materials With Shaders are well worth reading if you want more info [smile]

hth
Jack

<hr align="left" width="25%" />
Jack Hoxley <small>[</small><small> Forum FAQ | Revised FAQ | MVP Profile | Developer Journal ]</small>

Quote:Original post by jollyjeffers
Books like Advanced Lighting and Materials With Shaders are well worth reading if you want more info [smile]

Actually, I own this book and that's where I tried to apply Cook-Torrance model from without success. [rolleyes]

By the way, I have adapted your code to work with normal mapping to make the beauty on the screenshot below. [smile]
Free Image Hosting at www.ImageShack.us

This topic is closed to new replies.

Advertisement