openglglslshadow-mapping

Finding the light ray that goes from light world position through the shadow map texel


I want to move from basic shadow mapping on to adaptive biased shadow mapping. I found a paper which describes how to do it, but I am not sure how to achieve a certain step in the process: Adaptive shadow mapping idea The idea is to have a plane P (which is basically just the normal of the current fragment's surface in the fragment shader stage) and the world space position of the fragment (F1 in the picture above).

In order to calculate the correct bias (to fight shadow acne) I need to find the world space position of F2 which I can get if I shoot a ray from the light source through the center of the shadow map's texel center. This ray then eventually hits the plane P which results in the needed point F2.

With F1 and F2 now known, I then can calculate the distance between F1 and F2 along the light ray (I guess) and thus get the ideal bias to fight shadow acne.

Right now my basic shader code looks like this:

Vertex shader:

in  vec3 aLocalObjectPos;
out vec4 vShadowCoord;
out vec3 vF1;

// to shift the coordinates from [-1;1] to [0;1]
const mat4 biasMatrix = mat4(
    0.5, 0.0, 0.0, 0.0,
    0.0, 0.5, 0.0, 0.0,
    0.0, 0.0, 0.5, 0.0,
    0.5, 0.5, 0.5, 1.0
);

int main()
{
    // get the vertex position in the light's view space:
    vShadowCoord =  (biasMatrix * viewProjShadowMap * modelMatrix) * vec4(aLocalObjectPos, 1.0);
    vF1 = (modelMatrix * vec4(aLocalObjectPos, 1.0)).xyz;
}

Helper method in fragment shader:

uniform sampler2DShadow uTextureShadowMap;
float calculateShadow(float bias)
{
    vShadowCoord.z -= bias;
    return textureProjOffset(uTextureShadowMap, vShadowCoord, ivec2(0, 0));
}

My problem now is:

I already found this topic: Adaptive Depth Bias for Shadow Maps Ray Casting

Unfortunately there is no answer and I don't quite get all the things the author is talking about :-/


Solution

  • So, I think I have figured it out myself. I followed the directions in this paper: http://cwyman.org/papers/i3d14_adaptiveBias.pdf

    Vertex Shader (not much going on there):

    const mat4 biasMatrix = mat4(
        0.5, 0.0, 0.0, 0.0,
        0.0, 0.5, 0.0, 0.0,
        0.0, 0.0, 0.5, 0.0,
        0.5, 0.5, 0.5, 1.0
    );
    
    in vec4 aPosition;          // vertex in model's local space (not modified in any way)
    uniform mat4 uVPShadowMap;  // light's view-projection matrix
    out vec4 vShadowCoord;
    
    void main()
    {
        // ...
    
        vShadowCoord = (biasMatrix * uVPShadowMap * uModelMatrix) * aPosition;
        
        // ...
    }
    

    Fragment Shader:

    #version 450
    
    in      vec3 vFragmentWorldSpace; // fragment position in World space
    in      vec4 vShadowCoord;        // texture coordinates for shadow map lookup (see vertex shader)
    
    uniform sampler2DShadow uTextureShadowMap;
    
    uniform vec4 uLightPosition; // Light's position in world space
    uniform vec2 uLightNearFar;  // Light's zNear and zFar values
    uniform float uK;            // variable offset faktor to tweak the computed bias a little bit
    
    uniform mat4 uVPShadowMap;   // light's view-projection matrix
    
    const vec4 corners[2] = vec4[](    // frustum diagonal points in light's view space normalized [-1;+1]
        vec4(-1.0, -1.0, -1.0, 1.0),   // left  bottom near
        vec4( 1.0,  1.0,  1.0, 1.0)    // right top    far
    );
    
    float calculateShadowIntensity(vec3 fragmentNormal)
    {
        // get fragment's position in light space:
        vec4 fragmentLightSpace = uVPShadowMap * vec4(vFragmentWorldSpace, 1.0);
        vec3 fragmentLightSpaceNormalized = fragmentLightSpace.xyz / fragmentLightSpace.w;              // range [-1;+1]
        vec3 fragmentLightSpaceNormalizedUV = fragmentLightSpaceNormalized * 0.5 + vec3(0.5, 0.5, 0.5); // range [ 0; 1]
    
        // get shadow map's texture size:
        ivec2 textureDimensions = textureSize(uTextureShadowMap, 0);
        vec2 delta = vec2(textureDimensions.x, textureDimensions.y);
        
        // get width of every texel:
        vec2 textureStep = vec2(1.0 / textureDimensions.x, 1.0 / textureDimensions.y);
        
        // get the UV coordinates of the texel center:
        vec2 fragmentLightSpaceUVScaled = fragmentLightSpaceNormalizedUV.xy * delta;
        vec2 texelCenterUV = floor(fragmentLightSpaceUVScaled) * textureStep + textureStep / 2;
        
        // convert range for texel center in light space in range [-1;+1]:
        vec2 texelCenterLightSpaceNormalized = 2.0 * texelCenterUV - vec2(1.0, 1.0);
        
        // recreate light ray in world space:
        vec4 recreatedVec4 = vec4(texelCenterLightSpaceNormalized.x, texelCenterLightSpaceNormalized.y, -uLightsNearFar.x, 1.0);
        mat4 vpShadowMapInversed = inverse(uVPShadowMap);
        vec4 texelCenterWorldSpace = vpShadowMapInversed * recreatedVec4;
        vec3 lightRayNormalized = normalize(texelCenterWorldSpace.xyz - uLightsPositions.xyz);
    
        // compute scene scale for epsilon computation:
        vec4 frustum1 = vpShadowMapInversed * corners[0];
        frustum1 = frustum1 / frustum1.w;
        vec4 frustum2 = vpShadowMapInversed * corners[1];
        frustum2 = frustum2 / frustum2.w;
    
        float ln = uLightNearFar.x;
        float lf = uLightNearFar.y;
    
        // compute light ray intersection with fragment plane:
        float dotLightRayfragmentNormal = dot(fragmentNormal, lightRayNormalized);
        float d = dot(fragmentNormal, vFragmentWorldSpace);
        float x = (d - dot(fragmentNormal, uLightsPositions.xyz)) / dot(fragmentNormal, lightRayNormalized);
        vec4 intersectionWorldSpace = vec4(uLightsPositions.xyz + lightRayNormalized * x, 1.0);
    
        // compute bias:
        vec4 texelInLightSpace = uVPShadowMap * intersectionWorldSpace;
        float intersectionDepthTexelCenterUV = (texelInLightSpace.z / texelInLightSpace.w) / 2.0 + 0.5;
        float fragmentDepthLightSpaceUV = fragmentLightSpaceNormalizedUV.z;
        float bias = intersectionDepthTexelCenterUV - fragmentDepthLightSpaceUV;
    
        float depthCompressionResult = pow(lf - fragmentLightSpaceNormalizedUV.z * (lf - ln), 2.0) / (lf * ln * (lf - ln));
        float epsilon = depthCompressionResult * length(frustum1.xyz - frustum2.xyz) * uK;
        bias += epsilon;
    
        vec4 shadowCoord = vShadowCoord;
        shadowCoord.z -= bias;
    
        float shadowValue = textureProj(uTextureShadowMap, shadowCoord);
        return max(shadowValue, 0.0);
    }
    

    Please note that this is a very verbose method (you could optimise several steps, I know) to better explain what I did to make it work. All my shadow casting lights use perspective projection. I tested the results on the CPU side in a separate project (only c# with the math structs from the OpenTK package) and they seem reasonable. I used several light positions, texture sizes, etc. The bias values looked ok in all my tests. Of course, this is no proof, but I have a good feeling about this.

    In the end:

    The benefits were very small. The visual results are good (especially for shadow maps with >= 2048 samples per dimension) but I still had to tweak the offset value (uniform float uK in the fragment shader) for each of my scenes. I found values from 0.01 to 0.03 to deliver useable results.

    I lost about 10% performance (fps-wise) compared to my previous approach (slope-scaled bias) and gained maybe 1% of visual fidelity when it comes to shadows (acne, peter panning). The 1% is not measured - only felt by me :-)

    I wanted this approach to be the "one-solution-to-all-problems". But I guess, there is no "fire-and-forget" solution when it comes to shadow mapping ;-/