Why does my shadow mapping implementation gives wrong results?
22:08 03 Jun 2025

I am adding shadow mapping to my renderer.
Algorithm overview:

  1. Render shadow map(one directional light for now) --> DXGI_FORMAT_D32_FLOAT depth buffer
  2. Render gbuffer
  3. Bind (1) as an DXGI_FORMAT_R32_FLOAT SRV and perform deferred lighting using (2)

Doing so, I get this:

I get this

When I expect more something like this (rendered by raytracing):

what I expect

The depth/shadow map as rendered from the light POV. Looks fine to me:

light POV

Depth seems to look good, but there is something wrong with the orientation of the final render.

For testing I decided to set the light transform to the camera view projection matrix.

XMStoreFloat4x4(&dx12Light.transform, scene.getCamera()->getDirectXTransposedVP());

Result: https://youtu.be/cgt6F5CEh80

There is definitely something wrong.

When I check the depth inside PIX, I get this if the format is let at R32_FLOAT:

R32_FLOAT

And if I set it to D32_FLOAT, the expected result:

D32_FLOAT

This is strange to me, but still I don't think that's the issue.

So, the code:

Render shadow map VS

#include "MeshGroup.hlsli"
#include "Light.hlsli"

struct VS_INPUT
{
    float3 position : POSITION;
    float3 normal : NORMAL;
    float3 tangent : TANGENT;
    float3 bitangent : BITANGENT;
    float2 texCoord : TEXCOORD;
};

struct VS_OUTPUT
{
    float4 position : SV_POSITION;
};

// LightMainCB is b1
cbuffer LightStructCB : register(b2)
{
    LightStruct light;
};

VS_OUTPUT main(VS_INPUT input, uint instanceID : SV_InstanceID)
{
    VS_OUTPUT output;

    const float4x4 modelMat = meshGroupDatas[instanceID].transform;
    const float4 worldPosition = mul(float4(input.position, 1.0f), modelMat);
    output.position = mul(worldPosition, light.transform);

    return output;
}

Render shadow map PS

struct VS_OUTPUT
{
    float4 position: SV_POSITION;
};

float4 main(VS_OUTPUT input) : SV_TARGET
{
    return float4(0.0f, 0.0f, 0.0f, 0.0f);
}

Shadow mapping calculation during deferred shading

// @See : https://learnopengl.com/Advanced-Lighting/Shadows/Shadow-Mapping
float ShadowMapping(LightStruct light, float3 P)
{
    const float4 homogenous = mul(float4(P, 1.0f), light.transform);
    const float3 NDC = homogenous.xyz/ homogenous.w;
    const float3 projCoords = NDC * 0.5f + 0.5f;

    const float closestDepth = shadowMap.Sample(tSampler, saturate(projCoords.xy)).x;
    const float currentDepth = projCoords.z;

    const float shadow = currentDepth > closestDepth ? 0.0 : 1.0f;

    return shadow;
}

What am I doing wrong? My feeling is that the shadow map is properly rendered but sampling is broken.

Additional information

This is how getDirectXTransposedVP is computed:

inline DirectX::XMMATRIX getDirectXTransposedVP() const
{
    return getDirectXTransposedVP(m_nearZ, m_farZ);
}

inline DirectX::XMMATRIX getDirectXTransposedVP(float nearZ, float farZ) const
{
    const DirectX::XMMATRIX projMat = getDirectXPerspectiveMatrix(nearZ, farZ);
    const DirectX::XMMATRIX viewMat = getDirectXViewMatrix();
    const DirectX::XMMATRIX vpMat = viewMat * projMat;

    return XMMatrixTranspose(vpMat);
}

inline DirectX::XMMATRIX getDirectXPerspectiveMatrix() const
{
    return getDirectXPerspectiveMatrix(m_nearZ, m_farZ);
}

inline DirectX::XMMATRIX getDirectXPerspectiveMatrix(float nearZ, float farZ) const
{
    return  DirectX::XMMatrixPerspectiveFovRH(m_fovY.getValue(), m_aspectRatio,
        nearZ, farZ);
}

inline DirectX::XMMATRIX getDirectXViewMatrix() const
{
    const DirectX::XMVECTOR cPos = DirectX::XMVectorSet(m_position.x, m_position.y, m_position.z, 0.0f);

    const auto target = computeTarget();
    const DirectX::XMVECTOR cTarg = DirectX::XMVectorSet(target.x, target.y, target.z, 0.0f);
    const DirectX::XMVECTOR cUp = DirectX::XMVectorSet(s_UP.x, s_UP.y, s_UP.z, 0.0f);

    return DirectX::XMMatrixLookAtRH(cPos, cTarg, cUp);
}

Here the minimalist deferred shading implementation:

float4 main(VS_OUTPUT input) : SV_TARGET
{
    const float4 wsPos = positionWsOccludedTex[input.position.xy];
    if (wsPos.w == 0.0f)
        discard;

    const float3 P = wsPos.xyz;

    float3 N = normalWsTex[input.position.xy];

    float3 V = EyePosition.xyz - P;
    if (dot(N, V) < 0.0f)
    {
        N *= float3(-1.0f, -1.0f, -1.0f);
    }

    const float4 albedo = albedoTexture[input.position.xy];
    const LightStruct light = Lights[0];// We only use the first directional light for the test:)
    const float3 L = -light.direction.xyz;
    const float NoL = max(0.0f, dot(N, L));

    float4 color = light.color * albedo * InvPI;
    color *= NoL;
    color *= ShadowMapping(light, P);

    return color;
}

The world position AOV(positionWsOccludedTex)
[aov world position

The albedo AOV(albedoTexture):

albedo aov

directx shadow directx-12 shadow-mapping