I got CSM to work in OpenGL, and now Im trying to do the same in directx. I'm using the same math library and all and I'm pretty much using the alghorithm straight off. I am using right-handed, column major matrices from GLM.

The light is looking (-1, -1, -1).

The problem I have is twofolds;

  1. For some reason, the ground floor is causing alot of (false) shadow artifacts, like the vast shadowed area you see. I confirmed this when I disabled the ground for the depth pass, but thats a hack more than anything else

  2. The shadows are inverted compared to the shadowmap. If you squint you can see the chairs shadows should be mirrored instead.

This is the first cascade shadow map, in range of the alien and the chair:

enter image description here

I can't figure out why this is. This is the depth pass:

for (uint32_t cascadeIndex = 0; cascadeIndex < NUM_SHADOWMAP_CASCADES; cascadeIndex++)
    mShadowmap.BindDepthView(context, cascadeIndex);

    CameraFrustrum cameraFrustrum = CalculateCameraFrustrum(degreesFOV, aspectRatio, nearDistArr[cascadeIndex], farDistArr[cascadeIndex], cameraViewMatrix);
    lightVPMatrices[cascadeIndex] = CreateDirLightVPMatrix(cameraFrustrum, lightDir);
    mVertexTransformPass.RenderMeshes(context, renderQueue, meshes, lightVPMatrices[cascadeIndex]);

    lightVPMatrices[cascadeIndex] = gBiasMatrix * lightVPMatrices[cascadeIndex];
    farDistArr[cascadeIndex] = -farDistArr[cascadeIndex];

CameraFrustrum CalculateCameraFrustrum(const float fovDegrees, const float aspectRatio, const float minDist, const float maxDist, const Mat4& cameraViewMatrix)
    CameraFrustrum ret = { Vec4(1.0f, 1.0f, -1.0f, 1.0f), Vec4(1.0f, -1.0f, -1.0f, 1.0f), Vec4(-1.0f, -1.0f, -1.0f, 1.0f), Vec4(-1.0f, 1.0f, -1.0f, 1.0f),
                           Vec4(1.0f, -1.0f, 1.0f, 1.0f), Vec4(1.0f, 1.0f, 1.0f, 1.0f), Vec4(-1.0f, 1.0f, 1.0f, 1.0f), Vec4(-1.0f, -1.0f, 1.0f, 1.0f), };

    const Mat4 perspectiveMatrix = PerspectiveMatrixFov(fovDegrees, aspectRatio, minDist, maxDist);
    const Mat4 invMVP = glm::inverse(perspectiveMatrix * cameraViewMatrix);

    for (Vec4& corner : ret)
        corner = invMVP * corner;
        corner /= corner.w;

    return ret;

Mat4 CreateDirLightVPMatrix(const CameraFrustrum& cameraFrustrum, const Vec3& lightDir)
    Mat4 lightViewMatrix = glm::lookAt(Vec3(0.0f), -glm::normalize(lightDir), Vec3(0.0f, -1.0f, 0.0f));

    Vec4 transf = lightViewMatrix * cameraFrustrum[0];
    float maxZ = transf.z, minZ = transf.z;
    float maxX = transf.x, minX = transf.x;
    float maxY = transf.y, minY = transf.y;
    for (uint32_t i = 1; i < 8; i++)
        transf = lightViewMatrix * cameraFrustrum[i];

        if (transf.z > maxZ) maxZ = transf.z;
        if (transf.z < minZ) minZ = transf.z;
        if (transf.x > maxX) maxX = transf.x;
        if (transf.x < minX) minX = transf.x;
        if (transf.y > maxY) maxY = transf.y;
        if (transf.y < minY) minY = transf.y;

    Mat4 viewMatrix(lightViewMatrix);
    viewMatrix[3][0] = -(minX + maxX) * 0.5f;
    viewMatrix[3][3] = -(minY + maxY) * 0.5f;
    viewMatrix[3][4] = -(minZ + maxZ) * 0.5f;
    viewMatrix[0][3] = 0.0f;
    viewMatrix[1][3] = 0.0f;
    viewMatrix[2][3] = 0.0f;
    viewMatrix[3][3] = 1.0f;

    Vec3 halfExtents((maxX - minX) * 0.5, (maxY - minY) * 0.5, (maxZ - minZ) * 0.5);

    return OrthographicMatrix(-halfExtents.x, halfExtents.x, -halfExtents.y, halfExtents.y, halfExtents.z, -halfExtents.z) * viewMatrix;

And this is the pixel shader used for the lighting stage:

#define DEPTH_BIAS 0.0005
#define NUM_CASCADES 4

cbuffer DirectionalLightConstants : register(CBUFFER_REGISTER_PIXEL)
    float4x4 gSplitVPMatrices[NUM_CASCADES];
    float4x4 gCameraViewMatrix;
    float4 gSplitDistances;
    float4 gLightColor;
    float4 gLightDirection;

Texture2D gPositionTexture : register(TEXTURE_REGISTER_POSITION);
Texture2D gDiffuseTexture : register(TEXTURE_REGISTER_DIFFUSE);
Texture2D gNormalTexture : register(TEXTURE_REGISTER_NORMAL);
Texture2DArray gShadowmap : register(TEXTURE_REGISTER_DEPTH);
SamplerComparisonState gShadowmapSampler : register(SAMPLER_REGISTER_DEPTH);

float4 ps_main(float4 position : SV_Position) : SV_Target0
    float4 worldPos = gPositionTexture[uint2(position.xy)];
    float4 diffuse = gDiffuseTexture[uint2(position.xy)];
    float4 normal = gNormalTexture[uint2(position.xy)];

    float4 camPos = mul(gCameraViewMatrix, worldPos);

    uint index = 3;
    if (camPos.z > gSplitDistances.x)
        index = 0;
    else if (camPos.z > gSplitDistances.y)
        index = 1;
    else if (camPos.z > gSplitDistances.z)
        index = 2;

    float3 projCoords = (float3)mul(gSplitVPMatrices[index], worldPos);
    float viewDepth = projCoords.z - DEPTH_BIAS;
    projCoords.z = float(index);
    float visibilty = gShadowmap.SampleCmpLevelZero(gShadowmapSampler, projCoords, viewDepth);

    float angleNormal = clamp(dot(normal, gLightDirection), 0, 1);

    return visibilty * diffuse * angleNormal * gLightColor;

As you can see I am using depth bias and a bias matrix.

Any hints on why this behaves so wierdly?


I've rolled my own projective matrices, based of http://msdn.microsoft.com/en-us/library/windows/desktop/bb205348(v=vs.85).aspx but column major instead of row major:

Mat4 OrthographicMatrix(const float left, const float right, const float top, const float bottom, const float zNear, const float zFar)
    Mat4 ret(1.0f);

    ret[0][0] = 2.0f / (right - left);
    ret[1][5] = 2.0f / (top - bottom);
    ret[2][6] = 1.0f / (zNear - zFar);
    ret[3][0] = (left + right) / (left - right);
    ret[3][7] = (top + bottom) / (bottom - top);
    ret[3][8] = (zNear) / (zNear - zFar);

    return ret;

EDIT #2: The new bias matrix:

const Mat4 gBiasMatrix(0.5f, 0.0f, 0.0f, 0.0f,
                       0.0f, -0.5f, 0.0f, 0.0f,
                       0.0f, 0.0f, 1.0f, 0.0f,
                       0.5f, -0.5f, 1.0f, 1.0f);

Dosn't fix the shadow artifacts though - but the mirror issue yes

enter image description here


1 Answer 1

  1. This might be due to the fact that NDC space (post-projective space) in D3D extends from 0 to 1 along the z axis, while in OpenGL it extends from -1 to 1. This requires a change to your projection matrices to generate Z values in the correct range, else half of your depth range will be gone.

  2. Window space is top-down in DirectX, rather than bottom-up as in OpenGL. You'll need to take this into account, i.e. by either flipping the Y axis when you render, or flipping the V axis when you sample the shadow map.

  • \$\begingroup\$ Do I need to use a bias matrix? In OpenGL, afaik the bias matrix puts me from [-1, 1] to [0, 1] sample the textures, but in DX11 it should not be needed or? \$\endgroup\$ Aug 20, 2014 at 8:27
  • \$\begingroup\$ @KaiserJohaan If you mean converting the post-projective XY coordinates [-1, 1] to UV coordinates [0, 1], that's still needed. The Y axis needs to be flipped though, and the Z axis shouldn't be touched since it should already be in [0, 1] from the corrected projection matrix. \$\endgroup\$ Aug 20, 2014 at 19:56
  • \$\begingroup\$ Updated original post with a draft of said bias matrix - is it correct? \$\endgroup\$ Aug 20, 2014 at 20:22
  • \$\begingroup\$ @KaiserJohaan I don't think that's quite right. The last row should have +0.5 for both X and Y, and zero for Z. \$\endgroup\$ Aug 21, 2014 at 1:32

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .