PBR specular BRDF shines through on back side of mesh with normal maps

Started by
3 comments, last by sophia9 11 months, 3 weeks ago

I recently noticed a strange issue with my implementation of the Cook-Torrance specular BRDF, where the specular part of the BRDF will shine through to the back side of a lit object if normal maps are enabled. Below is an example of what this looks like. The camera is underneath a thin box plane looking upward, with a point light source above the plane (located at the center of the bright spot). Ambient and diffuse lighting is disabled.

This problem does not occur with either the Phong or Blinn-Phong BRDFs, nor does it occur if I only include the diffuse part of the Cook-Torrance BRDF. It also does not occur if I disable normal mapping, or if shadow mapping is enabled (shadow visibility term cancels it out).

I have double checked that normals look OK by visualizing them in RGB channels. There are no normals on the bottom side of the plane that point up toward the light. I've also checked that all vectors are normalized, and that dot products are clamped to be >0.

I haven't been able to find any mention of this issue anywhere online, and I have checked my shader code against various implementations online and don't see any major differences. My shader code for the Cook-Torrance BRDF looks like this:

void evaluateBRDFCookTorrance( in vec3 lightColor, in vec3 L, in float visibility, in float attenuation, in float ambient )
{
    #if DIFFUSE_LIGHTING || SPECULAR_LIGHTING || AMBIENT_LIGHTING
        float cosL = dot( surfaceNormal, L );
        float dotNL = max( cosL, 0.000001 );
    #endif
    
    #if AMBIENT_LIGHTING
        ambientAccumulator += lightColor*(ambient * ambientShade(cosL) * attenuation);
    #endif
    
    #if DIFFUSE_LIGHTING
        // Diffuse Fresnel Term
        float invDotNL = 1.0 - dotNL;
        float invDotNL2 = invDotNL * invDotNL;
        vec3 Fdiff = F0 + (1.0 - F0) * (invDotNL2*invDotNL2*invDotNL);
        
        diffuseAccumulator += lightColor*(1.0 - Fdiff)*(dotNL*visibility*attenuation);
    #endif
    
    #if SPECULAR_LIGHTING
        // Compute half vector (L + V)
        vec3 H = normalize( L - surfaceDirection );
        
        // Compute dot products.
        float dotNV = max( -dot( surfaceNormal, surfaceDirection ), 0.000001 );
        float dotNH = max( dot( surfaceNormal, H ), 0.000001 );
        float dotHV = max( -dot( H, surfaceDirection ), 0.000001 );
        
        // Geometry term G.
        float G = min( 1.0, min( dotNV, dotNL )*(2.0*dotNH / dotHV) );
        
        // Normal distribution function D. (Beckmann distribution)
        float dotNH2 = dotNH * dotNH;
        float dotNHR2 = dotNH2 * surfaceRoughness * surfaceRoughness;
        float D = exp((dotNH2 - 1.0)/dotNHR2) / (dotNH2*dotNHR2);
        
        // Specular Fresnel Term (Schlick approximation).
        // H dot V is correct for microfacet BRDF, even though Schlick specifies N dot V.
        float invDotHV = 1.0 - dotHV;
        float invDotHV2 = invDotHV * invDotHV;
        vec3 Fspec = F0 + (1.0 - F0) * (invDotHV2*invDotHV2*invDotHV);
        
        // Cook-Torrance Specular BRDF (without Fresnel Fspec, multiplied below)
        float specularBRDF = (D * G) / (4.0 * dotNV * dotNL);
        
        specularAccumulator += (lightColor*Fspec) * (dotNL*specularBRDF*visibility*attenuation);
    #endif
}

The main source of the strange specular highlights is the “specularBRDF” term. If I set that equal to 1, then the highlights are not there.

One thing I tried which seems to work OK is to change the “dotNL” (cos(theta)) term of the final line (but not other uses) so that it uses the interpolated vertex normals instead of the “surfaceNormal” which is modified by the normal map. This cancels out the weird specular on the back side of objects, but it also seems like it's not the correct thing to do because it ignores the normal map. It also seems to make the normals look more “flat”, as you can see in the following comparison.

Does anyone have any ideas how to fix this problem in the correct way? Do I have some error in my geometry term G?

Advertisement

I tried the Smith geometry term from this example, and it seems to work a bit better than either of the previous options. The specular still shines through, but it's much less noticeable than before. I'd still be interested if anyone has any better options that would completely eliminate the light shining through to back sides. I don't want to rely on shadow mapping to fix it, since I may not have shadows on all lights.

fleabay said:

Seems like a function is returning undefined because you're sending it invalid values.

I don't think that's the case here, I tried clamping to avoid negative/divide by zero/infinity and didn't help. Usually that sort of thing would show up as black pixels. Here the pixels are bright (and scale with light source intensity). The unwanted highlights are located at the grazing angles where Fresnel reflectance is high.

Update: part of the problem was that I accidentally was rendering 2 identical point lights at the same location, only one of which had a shadow map. This resulted in some light seeming to shine through. It was just the second copy of the light. I still get some specular highlights on back sides of objects, but only where there is no shadow map, and only for places where the normal map is pointing strongly in X or Y.

This topic is closed to new replies.

Advertisement