I was mucking about with this in the BRDF explorer, and the fresnel factor didn't seem to be behaving right; even at front-on angles (L==V) there would always be a highlight, even when Ks was 0. I replaced your exp(-6 * LdotH) with pow(1-LdotH, 5) and it seems more correct now.
I've taken to Cook-Torrance with the GGX distribution and Smith geometry factor (thanks CryZe) for specular, and the qualitative version of Oren-Nayar for diffuse.
To help me compare it with the other BRDF's that come with BRDF explorer, I also divided everything by PI, which I'm not sure is correct, but seemed to make it behave more like the other BRDF's, and I divided the final result by NdotL, so that I could let BRDF explorer multiply by NdotL itself.
There's really not a lot of difference between the two fresnel approximations. I graphed the two side-by-side here:
The one that uses exp() essentially kicks in slightly sooner and is more gradual. For materials with an IOR value of ~1.4 (average dialectics) this seems to be slightly closer to the full fresnel equation, and I'm guessing it's not any more expensive to evaluate on modern GPUs.
As for PI and NdotL, I went ahead and rewrote the unoptimized version of the shader:
color Diffuse 1 0 0
color Specular 1 1 1
float DiffuseScale 0 1 0.5
float SpecularScale 0 0.999 .028
float Roughness 0.005 2 0.2
vec3 BRDF( vec3 L, vec3 V, vec3 N, vec3 X, vec3 Y )
float PI = 3.14159265358979323846;
vec3 Kd = Diffuse * DiffuseScale;
vec3 Ks = Specular * SpecularScale;
vec3 H = normalize(L + V);
float NdotL = clamp(dot(N, L), 0, 1);
float NdotV = dot(N, V);
float NdotH = dot(N, H);
float LdotH = dot(L, H);
float a_2 = Roughness * Roughness;
float NdotL_2 = NdotL * NdotL;
float NdotV_2 = NdotV * NdotV;
float NdotH_2 = NdotH * NdotH;
float OneMinusNdotL_2 = 1.0 - NdotL_2;
float OneMinusNdotV_2 = 1.0 - NdotV_2;
vec3 Fd = 1.0 - Ks;
float gamma = clamp(dot(V - N * NdotV, L - N * NdotL), 0, 1);
float A = 1.0 - 0.5 * (a_2 / (a_2 + 0.33));
float B = 0.45 * (a_2 / (a_2 + 0.09));
float C = sqrt(OneMinusNdotL_2 * OneMinusNdotV_2) / max(NdotL, NdotV);
vec3 Rd = Kd / PI * Fd * (A + B * gamma * C);
float D = a_2 / (PI * pow(NdotH_2 * (a_2 - 1.0) + 1.0, 2.0));
vec3 Fs = Ks + Fd * exp(-6 * LdotH);
float G1_1 = 2.0 / (1.0 + sqrt(1.0 + a_2 * (OneMinusNdotL_2 / NdotL_2)));
float G1_2 = 2.0 / (1.0 + sqrt(1.0 + a_2 * (OneMinusNdotV_2 / NdotV_2)));
float G = G1_1 * G1_2;
vec3 Rs = (D * Fs * G) / (4 * NdotL * NdotV);
return (Rd + Rs) * NdotL; //remove NdotL and let BRDF Explorer handle that
You can see there is a factor of PI located in the calculation of Rd. Kd over PI is essentially the Lambert BRDF. The factor of PI is necessary for energy conservation. A factor of PI also shows up in the calculating of D. This is part of the normalization of the GGX distribution. When calculating Rs you see the familiar Cook-Torrance equation. Finally, Rd and Rs are summed and then multiplied by NdotL. This NdotL is not a part of either the specular or diffuse BRDFs, but the lighting equation. The version I posted before is identical to this, only I have removes terms that cancel out in order to get rid of unnecessary shader instructions. I also removed PI from both diffuse and specular BRDFs, since it's not really necessary for video games. The only affect it has is that your lights appear to be PI times brighter.
At least that is my current understanding. I'm still very new to the concepts behind lighting.
Edit: So I suppose it would make sense to remove the final NdotL, since this shader represents only the BRDF and not the final pixel color. Presumably BRDF Explorer is automatically multiplying the result by NdotL.
Edited by Chris_F, 24 February 2013 - 02:10 AM.