2

I've got a problem with rendering hard shadows in a PBR pipeline. I believe there is something wrong with PBR calculations because with a Blinn-Phong lighting model everything looks fine.

These are lightning calculations - basic PBR

struct DirectionalLight
{
    vec3 direction;
};

layout(std140, binding = 2) uniform Scene
{
    DirectionalLight directionalLight;
    vec3 viewPosition;
} u_scene;

layout(std140, binding = 4) uniform Material
{
    vec4 baseColor;
    float roughness;
    float metalness;
} u_material;

const float PI = 3.14159265359;
const float epsilon = 0.00001;

int lightCount = 1;

vec3 CalculateDirectionalLight(vec3 N, vec3 V, float NdotV, vec3 F0)
{
    vec3 result;
    for(int i = 0; i < lightCount; ++i) {
        vec3 L = normalize(-u_scene.directionalLight.direction);

        float NdotL = max(0.0f, dot(N, L));

        vec3 H = normalize(V + L);
        float NdotH = max(0.0f, dot(N, H));

        vec3 F  = FresnelSchlickRoughness(max(0.0f, dot(H, V)), F0, u_material.roughness);
        float D = NDFGGX(NdotH, u_material.roughness);
        float G = GeometrySmith(NdotL, NdotV, u_material.roughness);

        vec3 kd = (1.0f - F) * (1.0f - u_material.metalness);
        vec3 diffuse = kd * u_material.baseColor.rgb;

        vec3 nominator = F * G * D;
        float denominator = max(epsilon, 4.0f * NdotV * NdotL);
        vec3 specular = nominator / denominator;
        specular = clamp(specular, vec3(0.0f), vec3(10.0f));

        result += (diffuse + specular) /* u_material.radiance */ * NdotL;
    }

    return result;
}

float NDFGGX(float NdotH, float roughness)
{
    float alpha = roughness * roughness;
    float alphaSq = alpha * alpha;

    float denom = (NdotH * NdotH) * (alphaSq - 1.0) + 1.0;
    return alphaSq / (PI * denom * denom);
}

float GeometrySchlickGGX(float Ndot, float k)
{
    float nom   = Ndot;
    float denom = Ndot * (1.0 - k) + k;

    return nom / denom;
}

float GeometrySmith(float NdotL, float NdotV, float roughness)
{    
    float r = (roughness + 1.0f);
    float k = (r * r) / 8.0f;

    float ggx2 = GeometrySchlickGGX(NdotV, k);
    float ggx1 = GeometrySchlickGGX(NdotL, k);

    return ggx1 * ggx2;
}

vec3 FresnelSchlick(float cosTheta, vec3 F0)
{
    return F0 + (1.0 - F0) * pow(1.0 - cosTheta, 5.0);
}

vec3 FresnelSchlickRoughness(float cosTheta, vec3 F0, float roughness)
{
    return F0 + (max(vec3(1.0 - roughness), F0) - F0) * pow(1.0 - cosTheta, 5.0);
}

shadow functions

layout(binding = 2) uniform sampler2D u_shadowMap;

float ShadowFade = 1.0;

float GetShadowBias()
{
    const float MINIMUM_SHADOW_BIAS = 0.002;
    float bias = max(MINIMUM_SHADOW_BIAS * (1.0 - dot(normalize(v_normal), -normalize(u_scene.directionalLight.direction))), MINIMUM_SHADOW_BIAS);
    return bias;
}

float HardShadows_DirectionalLight(vec4 fragPosLightSpace)
{
    vec3 shadowCoords = fragPosLightSpace.xyz / fragPosLightSpace.w;
    float bias = GetShadowBias();
    float shadowMapDepth = texture(u_shadowMap, vec2(shadowCoords.xy * 0.5 + 0.5)).r;
    return step(shadowCoords.z, shadowMapDepth + bias) * ShadowFade;
}

and the main function

void main()
{   
    vec3 F0 = vec3(0.04f);
    F0 = mix(F0, u_material.baseColor.rgb, u_material.metalness);
    
    vec3 N = normalize(v_normal);
    vec3 V = normalize(u_scene.viewPosition - v_position);
    float NdotV = max(0.0f, dot(N, V));

    //v_positionFromLight is calculated in a vertex shader like this:
    //v_positionFromLight = u_lightViewProjection * vec4(v_position, 1.0f);
    //where v_position is modelMatrix * a_position;
    //where a_position is a input position of a vertex

    float shadow = HardShadows_DirectionalLight(v_positionFromLight);
    vec3 ambient = u_material.baseColor.rgb * 0.3f;
    vec3 lightContribution = ambient + CalculateDirectionalLight(N, V, NdotV, F0) * shadow;

    f_color = vec4(lightContribution, 1.0);
}

and this is how the scene looks like - there should be visible shadows, but there aren't: enter image description here

I've tested 2 things. First - Blinn-Phong lighting model - shadows render just fine. Second - output shadow calculations without PBR lightning like this:

void main()
{   
    float shadow = HardShadows_DirectionalLight(v_positionFromLight);
    vec3 ambient = u_material.baseColor.rgb * 0.3f;
    f_color = vec4(ambient * shadow, 1.0f);
}

and it also works (besides that they're not placed in a good spot, but that is another topic): enter image description here

Why this PBR model does not work with shadows? How can I fix it?

BrodaJarek3
  • 311
  • 1
  • 9
  • From the images in this question, it doesn't look like the shadows are _that_ broken with your PBR path. The gray environment looks quite identical in both of your paths, which suggests that your PBRL lighting doesn't really work there and all you see is the ambient part (for which you don't apply shadows in the PBR path), so technically, it as already completely in the dark as far as your light model is concerned, and you won't see shadows there. – derhass Sep 04 '21 at 16:32
  • `your PBRL lighting doesn't really work there and all you see is the ambient part` - That is not correct, because in the first image you can see a white dot on the sphere and the cone, so the light has to work. On the second image, the camera has a different view position, so I could've caught the shadow, but the lightning does not work there (because there is no light, just to test if `shadow` float has a good value). – BrodaJarek3 Sep 04 '21 at 18:05
  • I didn't say that that your PBR code path didn't have any effect, I just suggested that it is broken, or maybe the normals of your enviroment geometry are. Be it as it may, my _hypothesis_ can easily be disproved by simply omitting the addition of the `ambient` part to the final output of the shader. If I'm right, you will see a black for those gray polygons. – derhass Sep 04 '21 at 23:04

0 Answers0