Minimizing shadow mapping shimmer

Started by
2 comments, last by helikopterodaktyl 2 years, 6 months ago

I am trying to implement cascaded shadow mapping. Before I work with the cascades part, I want to implement n=1 cascades scenario. I am trying to calculate the shadow frustum to encompass my camera view frustum. My initial approach was with making an AABB, but it doesn't work well when rotating (the shadows still shimmer). I am trying to get the bounding sphere approach going. This was my attempt:

Matrix4f calculateOrtho(Matrix4f camProj, Matrix4f camView, ref Matrix4f shadowView /* = lookAtMatrix(Vector3f(0.0f, 1.0f, 0.0f), Vector3f(0.0f, 0.0f, 0.0f),
                Vector3f(1.0f, 0.0f, 0.0f)) */
)
{
    Vector4f[8] corners;

    // frustum corners defined in Vulkan NDC space
    corners[0] = Vector4f(-1.0f, -1.0f, 0.0f, 1.0f);
    corners[1] = Vector4f(1.0f, -1.0f, 0.0f, 1.0f);
    corners[2] = Vector4f(1.0f, 1.0f, 0.0f, 1.0f);
    corners[3] = Vector4f(-1.0f, 1.0f, 0.0f, 1.0f);

    corners[4] = Vector4f(-1.0f, -1.0f, 1.0f, 1.0f);
    corners[5] = Vector4f(1.0f, -1.0f, 1.0f, 1.0f);
    corners[6] = Vector4f(1.0f, 1.0f, 1.0f, 1.0f);
    corners[7] = Vector4f(-1.0f, 1.0f, 1.0f, 1.0f);

    for (int a = 0; a < 8; a++)
    {
        // transform frustum corners into world space
        corners[a] = corners[a] * (camProj * camView).inverse();
        corners[a] /= corners[a].w;
        // transform world space frustum corners into shadow light space
        corners[a] = corners[a] * shadowView;
    }

    // calculate light space frustum center
    Vector3f frustumCenter = Vector3f(0, 0, 0);
    for (int i = 0; i < 8; i++)
        frustumCenter += corners[i].xyz;
    frustumCenter *= 1.0f / 8.0f;

    // calculate lightspace radius of bounding sphere
    float radius = 0.0f;
    for (int i = 0; i < 8; i++)
        radius = max(radius, (frustumCenter - corners[i].xyz).length());

    float shadowmapSize = 4096.0f;
    float unitsPerShadowmapTexel = (2.0f * radius) / shadowmapSize;

    // align frustum center to multiply of texel size
    frustumCenter.x = floor(frustumCenter.x / unitsPerShadowmapTexel) * unitsPerShadowmapTexel;
    frustumCenter.y = floor(frustumCenter.y / unitsPerShadowmapTexel) * unitsPerShadowmapTexel;

    return orthoMatrixVulkan(frustumCenter.x - radius, frustumCenter.x + radius,
            frustumCenter.y - radius, frustumCenter.y + radius,
            frustumCenter.z - radius * 3.0f, frustumCenter.z + radius * 3.0f);
}

It almost works… but only when I am near the start of the coordinate system. The shimmer is minimal or nonexistent there. But when I go further away from the (0,0,0) point, the shimmer gets more and more noticeable.

Am I implementing the bounding sphere approach incorrectly? Or is this just floating point inaccuracy, is there some way to prevent it? I noticed my radius value is not always constant, which could be the cause of shimmering, but I don't know why would the radius be different frame-to-frame.

Advertisement

I might have to think this through a little more, but typically you want your “rounding offset” (which snaps the texels of your shadow map to a fixed grid in world space) to be a translation applied to your projection matrix rather than being baked into the X/Y scale factors. Here's how I did it: https://github.com/TheRealMJP/Shadows/blob/master/Shadows/MeshRenderer.cpp#L1492​

if(AppSettings::StabilizeCascades)
{
    // Create the rounding matrix, by projecting the world-space origin and determining
    // the fractional offset in texel space
    XMMATRIX shadowMatrix = shadowCamera.ViewProjectionMatrix().ToSIMD();
    XMVECTOR shadowOrigin = XMVectorSet(0.0f, 0.0f, 0.0f, 1.0f);
    shadowOrigin = XMVector4Transform(shadowOrigin, shadowMatrix);
    shadowOrigin = XMVectorScale(shadowOrigin, sMapSize / 2.0f);

    XMVECTOR roundedOrigin = XMVectorRound(shadowOrigin);
    XMVECTOR roundOffset = XMVectorSubtract(roundedOrigin, shadowOrigin);
    roundOffset = XMVectorScale(roundOffset, 2.0f / sMapSize);
    roundOffset = XMVectorSetZ(roundOffset, 0.0f);
    roundOffset = XMVectorSetW(roundOffset, 0.0f);

    XMMATRIX shadowProj = shadowCamera.ProjectionMatrix().ToSIMD();
    shadowProj.r[3] = XMVectorAdd(shadowProj.r[3], roundOffset);
    shadowCamera.SetProjection(shadowProj);
}

MJP said:

I might have to think this through a little more, but typically you want your “rounding offset” (which snaps the texels of your shadow map to a fixed grid in world space) to be a translation applied to your projection matrix rather than being baked into the X/Y scale factors. Here's how I did it: https://github.com/TheRealMJP/Shadows/blob/master/Shadows/MeshRenderer.cpp#L1492​

if(AppSettings::StabilizeCascades)
{
    // Create the rounding matrix, by projecting the world-space origin and determining
    // the fractional offset in texel space
    XMMATRIX shadowMatrix = shadowCamera.ViewProjectionMatrix().ToSIMD();
    XMVECTOR shadowOrigin = XMVectorSet(0.0f, 0.0f, 0.0f, 1.0f);
    shadowOrigin = XMVector4Transform(shadowOrigin, shadowMatrix);
    shadowOrigin = XMVectorScale(shadowOrigin, sMapSize / 2.0f);

    XMVECTOR roundedOrigin = XMVectorRound(shadowOrigin);
    XMVECTOR roundOffset = XMVectorSubtract(roundedOrigin, shadowOrigin);
    roundOffset = XMVectorScale(roundOffset, 2.0f / sMapSize);
    roundOffset = XMVectorSetZ(roundOffset, 0.0f);
    roundOffset = XMVectorSetW(roundOffset, 0.0f);

    XMMATRIX shadowProj = shadowCamera.ProjectionMatrix().ToSIMD();
    shadowProj.r[3] = XMVectorAdd(shadowProj.r[3], roundOffset);
    shadowCamera.SetProjection(shadowProj);
}

Thanks! I merged my code with yours and got some very good results. What I was missing, was rounding the actual radius value to a certain multiplier (I see you're rounding it to 16).

This topic is closed to new replies.

Advertisement