What is the best OpenGL self-shadowing technique?

Viewed 43

I chose the quite old, but sufficient method of shadow mapping, which is OK overall, but I quickly discovered some self-shadowing problems:

self-shadowing problem on the backpack

It seems, this problem appears because of the bias offset, which is necessary to eliminate shadow acne artifacts.

After some googling, it seems that there is no easy solution to this, so I tried some shader tricks which worked, but not very well.

My first idea was to perform a calculation of a dot multiplication between a light direction vector and a normal vector. If the result is lower than 0, the angle between vectors is >90 degrees, so this surface is pointing outward at the light source, hence it is not illuminated. This works good, except shadows may appear too sharp and hard:

hard self-shadows problem

After I was not satisfied with the results, I tried another trick, by multiplying the shadow value by the abs value of the dot product of light direction and normal vector (based on the normal map), and it did work (hard shadows from the previous image got smooth transition from shadow to regular diffuse color), except it created another artifact in situations, when the normal map normal vector is pointing somewhat at the sun, but the face normal vector does not. It also made self-shadows much brighter (but it is fixable):

Another problematic shadow

Can I do something about it, or should I just choose the lesser evil?

Shader shadows code for example 1:

        vec4 fragPosViewSpace = view * vec4(FragPos, 1.0);
        float depthValue = abs(fragPosViewSpace.z);
        vec4 fragPosLightSpace = lightSpaceMatrix * vec4(FragPos, 1.0);

        vec3 projCoords = fragPosLightSpace.xyz / fragPosLightSpace.w;
        // transform to [0,1] range
        projCoords = projCoords * 0.5 + 0.5;

        // get depth of current fragment from light's perspective
        float currentDepth = projCoords.z;

        // keep the shadow at 0.0 when outside the far_plane region of the light's frustum.
        if (currentDepth > 1.0)
        {
            return 0.0;
        }
        // calculate bias (based on depth map resolution and slope)
        float bias = max(0.005 * (1.0 - dot(normal, lightDir)), 0.0005);
        vec2 texelSize = 1.0 / vec2(textureSize(material.texture_shadow, 0));

        const int sampleRadius = 2;
        const float sampleRadiusCount = pow(sampleRadius * 2 + 1, 2);
        for(int x = -sampleRadius; x <= sampleRadius; ++x)
        {
            for(int y = -sampleRadius; y <= sampleRadius; ++y)
            {
                float pcfDepth = texture(material.texture_shadow, vec3(projCoords.xy + vec2(x, y) * texelSize, layer)).r;
                shadow += (currentDepth - bias) > pcfDepth ? ambientShadow : 0.0;
            }
        }
        shadow /= sampleRadiusCount;

Hard self shadows trick code:

    float shadow = 0.0f;
    float ambientShadow = 0.9f;
    // "Normal" is a face normal vector, "normal" is calculated based on normal map. I know there is a naming problem with that))
    
    float faceNormalDot   = dot(Normal, lightDir);
    float vectorNormalDot = dot(normal, lightDir);

    if (faceNormalDot <= 0 || vectorNormalDot <= 0)
    {
        shadow = max(abs(vectorNormalDot), ambientShadow);
    }
    else
    {
        vec4 fragPosViewSpace = view * vec4(FragPos, 1.0);
        float depthValue = abs(fragPosViewSpace.z);
        ...
    }

Dot product multiplication trick code:

    float shadow = 0.0f;
    float ambientShadow = 0.9f;

    float faceNormalDot   = dot(Normal, lightDir);
    float vectorNormalDot = dot(normal, lightDir);

    if (faceNormalDot <= 0 || vectorNormalDot <= 0)
    {
        shadow = ambientShadow * abs(vectorNormalDot);
    }
    else
    {
        vec4 fragPosViewSpace = view * vec4(FragPos, 1.0);
        float depthValue = abs(fragPosViewSpace.z);
        ...

0 Answers
Related