How exactly Shadow Maps work?

Hi,

I want to understand how Shadow Maps work precisely, I know they render the scene from the points of each light and output the Depth Map, right? Then, when the scene is rendered from the Main Camera, it somehow compares the pixel depth to depth from all other Depth Maps from light sources. Then if the depth is greater than this pixel is in shadow.

Could anyone clarify how to know the position of a fragment from the Main Camera perspective relative to other Depth Maps baked from other light source’s perspectives? Do I need to get the world-space position from Depth, then transform it into light source space?

Your explanation is correct.

In forward rendering you know the world-space position of each vertex. Just transform it with the view-projection-viewport matrix of each light. The viewport transformation maps the NDC-space xy coordinate in the range -1 to 1 to the texture space 0-1 range.

In your C#/C++ code:

        fMat4x4 lightViewProjectionTextureMatrix =
            fMat4x4::CreateTranslationMatrix(fVec3(0.5f, 0.5f, 0.0f)) *
            fMat4x4::CreateScaleMatrix(fVec3(0.5f, -0.5f, 1.0f)) *
            shadowMapCamera->GetProjectionMatrix() *
            shadowMapCamera->GetViewMatrix();

In the vertex shader:

VsToPs vs(IaToVs inData)
{
    VsToPs outData;

    float4 posObjectSpace = float4(inData.posObjectSpace, 1.0);
    float4 posWorldSpace = MUL(curObject.worldMatrix, posObjectSpace);
    float4 posClipSpace = MUL(curFrame.viewProjectionMatrix, posWorldSpace);
    outData.positionClipSpace = posClipSpace;

    outData.shadowTexCoord = MUL(curFrame.lightViewProjectionTextureMatrix, posWorldSpace);

    ...

    return outData;
}

In the pixel shader:

#define SAMPLE_SHADOW_MAP(name, uv, z) name.SampleCmpLevelZero(CAT(name, Sampler), uv, z)

float SampleShadowMap(float3 texCoord, float bias)
{
    return SAMPLE_SHADOW_MAP(shadowMapCmpLinear, texCoord.xy, texCoord.z - bias).r;
}

PsToOm ps(VsToPs inData)
{

    // Calculate shadow factor
    // TODO is division by w necessary? Probably only for spot lights
    float shadowFactor = SampleShadowMap(inData.shadowTexCoord.xyz / inData.shadowTexCoord.w, 0.00001);

}

Note that you need to use a special shadow map comparison sampler. If it’s set to linear sampling, you’ll get 4-tap percentage closer filtering for free.

    shadowMapSamplerCmpLinear = Sampler::Builder()
        .SetCompareMode(Sampler::CompareFunction::LessOrEqual)
        .SetAddressModeU(Sampler::AddressMode::Clamp)
        .SetAddressModeV(Sampler::AddressMode::Clamp)
        .SetFilter(Sampler::Filter::Bilinear)
        .Create();

In deferred rendering you reconstruct the world-space position of each fragment from the depth buffer. In deferred rendering the shadow map of the current light is tested when you render the light volume of each light. By the way, in deferred rendering you can render each light’s shadow map just before you render the light volume and then reuse the shadow map’s render target for the next shadow casting light.

float3 GetPixelWorldPosition(uint2 pixelCoordinate)  // pixelCoordinate = SV_Position.xy in the pixel shader
{
    // Read this pixel's depth value from the depth buffer
    const float depth = LOAD_TEXTURE_MAP_LEVEL(depthBuffer, pixelCoordinate, 0).x;

    // Reconstruct 3d position
    float3 ndcPosition;
    ndcPosition.xy = float2(pixelCoordinate) + 0.5; // center in the middle of the pixel.
    ndcPosition.xy = ndcPosition.xy * constantBuffer.renderTargetSizeInv * 2.0 - 1.0;
    ndcPosition.y = -ndcPosition.y;
    ndcPosition.z = depth;
    float4 worldPosition = MUL(constantBuffer.viewProjectionMatrixInv, float4(ndcPosition, 1));
    worldPosition.xyz /= worldPosition.w;

    return worldPosition.xyz;
}

Hope this helps