I am in the somewhat early stages of adding shadow mapping for directional light sources to my engine and after having some issues with this I decided to follow the step-by-step implementation offered by Alex Tardif here: http://alextardif.com/ShadowMapping.html
Still so I'm getting about the same shimmering edges, as well as seeming frame-to-frame offsets in the whole depth map when moving the viewing camera.
Here's a short video to show the issues in action:
What strikes me as particularly odd is the fact that there are significantly less artifacts when moving the camera left-to-right, as opposed to forward / backwards. As is more understandable the most severe artifacts occur when changing the orientation of the camera.
Here's my relevant code if anybody can spot any obvious issues or things I've missed that may be considered universally as "obvious".
The code is intentionally as close a match to the one presented in Tardif's article as possible, even if this makes it a bit messier with the changes between XMFLOATX and XMVECTOR etc. I have also left out the second part of his article (or rather not gotten to it yet) which deals with downsampling and blurring, but I cannot see how this would have any relevance besides making the shadows appear smoother.
I am also only using a single cascade split for now, which arbitrarily spans the 1..400 depth range of the rendering camera (which roughly correponds to the size of my testing scene):
// Create a new projection matrix for the rendering camera (which is assumed to use perspective projection here) that
// only stretches over the current cascade
XMMATRIX matCascadeProjection = XMMatrixPerspectiveFovLH(pRenderingCamera->GetFOV(), pRenderingCamera->GetAspect(), 1.0f, 300.0f);
XMVECTOR frustumCorners[8] = {
XMVectorSet(-1.0f, 1.0f, 0.0f, 0.0f),
XMVectorSet(1.0f, 1.0f, 0.0f, 0.0f),
XMVectorSet(1.0f, -1.0f, 0.0f, 0.0f),
XMVectorSet(-1.0f, -1.0f, 0.0f, 0.0f),
XMVectorSet(-1.0f, 1.0f, 1.0f, 0.0f),
XMVectorSet(1.0f, 1.0f, 1.0f, 0.0f),
XMVectorSet(1.0f, -1.0f, 1.0f, 0.0f),
XMVectorSet(-1.0f, -1.0f, 1.0f, 0.0f)
};
// NOTE: The transpose part here seems rather useless; the tutorial mentions it is for being sent to the GPU, but this
// particular matrix never is. Nevertheless, I'll do it like this to achieve the highest possible correspondence to the
// article's code snippets. Furthermore, not using a transposed matrix (and obviously not using the TransformTransposed
// function) seems to give identical results. Try to remove the transpose part once everything seems to work as intended.
XMMATRIX matCamViewProj = XMMatrixTranspose(pRenderingCamera->GetViewMatrix() * matCascadeProjection);
XMMATRIX matInvCamViewProj = XMMatrixInverse(nullptr, matCamViewProj);
// Unproject frustum corners into world space
for(size_t n = 0; n < 8; n++) {
XMFLOAT3 tmp;
XMStoreFloat3(&tmp, frustumCorners[n]);
tmp = util::TransformTransposedFloat3(tmp, matInvCamViewProj);
frustumCorners[n] = XMLoadFloat3(&tmp);
}
// Find frustum center
XMFLOAT3 frustumCenter(0.0f, 0.0f, 0.0f);
{
XMVECTOR v = XMLoadFloat3(&frustumCenter);
for(size_t n = 0; n < 8; n++)
v += frustumCorners[n];
v *= (1.0f / 8.0f);
XMStoreFloat3(&frustumCenter, v);
}
// Retrieve normalized light direction
XMVECTOR lightDirection = XMVector3Normalize(light->GetTransform().GetForwardVector());
// Determine the radius of the to-be orthographic projection as the distance between the farthest frustum corner points divided by two
float radius = XMVectorGetX(XMVector3Length((frustumCorners[0] - frustumCorners[6]))) / 2.0f; // The length is copied into each element
// Figure out how many texels per world-unit will fit if we project a cube with the given "radius" (side length / 2)
float texelsPerUnit = (float)shadowMapWidth / (radius * 2); // NOTE: The shadow map *must* be square!
// Build a scaling matrix to scale evenly in all directions to the number of texels per unit
XMMATRIX matScaling = XMMatrixScaling(texelsPerUnit, texelsPerUnit, texelsPerUnit);
// Create look-at vector and matrix by accounting for scaling (and later snapping) to the number of texels per unit
const XMVECTOR UpVector = XMVectorSet(0.0f, 1.0f, 0.0f, 0.0f);
const XMVECTOR ZeroVector = XMVectorSet(0.0f, 0.0f, 0.0f, 0.0f);
XMVECTOR vecBaseLookat = XMVectorSet(-XMVectorGetX(lightDirection), - XMVectorGetY(lightDirection), -XMVectorGetZ(lightDirection), 0.0f);
XMMATRIX matLookat = XMMatrixMultiply(XMMatrixLookAtLH(ZeroVector, vecBaseLookat, UpVector), matScaling);
XMMATRIX matInvLookat = XMMatrixInverse(nullptr, matLookat); // Take note that this will also undo the scaling effect imposed on «matLookat»!
// Now the above can be used to move the frustum center in texel-sized increments (when transformed by matLookat, and the
// result can then be brought back into world-space by the inverse lookat matrix).
frustumCenter = util::TransformFloat3(frustumCenter, matLookat);
frustumCenter.x = (float)floor(frustumCenter.x); // Clamp to texel increment (by rounding down)
frustumCenter.y = (float)floor(frustumCenter.y); // Clamp to texel increment (by rounding down)
frustumCenter = util::TransformFloat3(frustumCenter, matInvLookat);
// Calculate eye position by backtracking in the opposite light direction, ie. towards the light, by the cascade radius * 2
XMVECTOR eye = XMLoadFloat3(&frustumCenter) - (lightDirection * radius * 2.0f);
// Build the final light view matrix
XMMATRIX matLightView = XMMatrixLookAtLH(eye, XMLoadFloat3(&frustumCenter), UpVector);
// Build the light's projection matrix. This is intended to keep a consistent size and should therefore minimize
// shimmering edges due to per-frame matrix recalculations.
// The near- and far value multiplications are arbitrary and meant to catch shadow casters outside of the frustum,
// whose shadows may extend into it. These should probably be better tweaked later on, but lets see if it at all works first.
const float zMod = 6.0f;
XMMATRIX matLightProj = XMMatrixOrthographicOffCenterLH(-radius, radius, -radius, radius, -radius * zMod, radius * zMod);
// Associate the current matrices with the light source (the shader side will need to know the "light matrix" to properly sample the shadow map(s))
light->SetCascadeViewProjectionMatrix(split, matLightView * matLightProj);
// Bind the corresponding shadow map for rendering by the special, global shadow mapping camera
gGlob.pShadowCamera->SetDepthStencilBuffer(shadowMap, (UINT)light->GetShadowMapIndex() + split);
// Set the shadow camera's matrices to those of the light source
gGlob.pShadowCamera->SetViewMatrix(matLightView);
gGlob.pShadowCamera->SetProjectionMatrix(matLightProj);
// Render the depth (shadow) map (note that this automatically clears the associated depth-stencil view)
gGlob.pShadowCamera->RenderDepthOnly(true);
The implementations of util::TransformFloat3 and util::TransformTransposedFloat3 are direct copies, with the change that they use the XMFLOAT3 struct instead of Vector3, of the implementations given by Tardif here: http://alextardif.com/code/transformvector3.txt
For completedness, here's my code for those as well:
inline XMFLOAT3 TransformFloat3(const XMFLOAT3& point, const XMMATRIX& matrix) {
XMFLOAT3 result;
XMFLOAT4 temp(point.x, point.y, point.z, 1); // Need a 4-part vector in order to multiply by a 4x4 matrix
XMFLOAT4 temp2;
temp2.x = temp.x * matrix._11 + temp.y * matrix._21 + temp.z * matrix._31 + temp.w * matrix._41;
temp2.y = temp.x * matrix._12 + temp.y * matrix._22 + temp.z * matrix._32 + temp.w * matrix._42;
temp2.z = temp.x * matrix._13 + temp.y * matrix._23 + temp.z * matrix._33 + temp.w * matrix._43;
temp2.w = temp.x * matrix._14 + temp.y * matrix._24 + temp.z * matrix._34 + temp.w * matrix._44;
result.x = temp2.x / temp2.w; // View projection matrices make use of the W component
result.y = temp2.y / temp2.w;
result.z = temp2.z / temp2.w;
return result;
}
inline XMFLOAT3 TransformTransposedFloat3(const XMFLOAT3& point, const XMMATRIX& matrix) {
XMFLOAT3 result;
XMFLOAT4 temp(point.x, point.y, point.z, 1); // Need a 4-part vector in order to multiply by a 4x4 matrix
XMFLOAT4 temp2;
temp2.x = temp.x * matrix._11 + temp.y * matrix._12 + temp.z * matrix._13 + temp.w * matrix._14;
temp2.y = temp.x * matrix._21 + temp.y * matrix._22 + temp.z * matrix._23 + temp.w * matrix._24;
temp2.z = temp.x * matrix._31 + temp.y * matrix._32 + temp.z * matrix._33 + temp.w * matrix._34;
temp2.w = temp.x * matrix._41 + temp.y * matrix._42 + temp.z * matrix._43 + temp.w * matrix._44;
result.x = temp2.x / temp2.w; // View projection matrices make use of the W component
result.y = temp2.y / temp2.w;
result.z = temp2.z / temp2.w;
return result;
}
Any light-shedding on what may be at fault here would be most welcome.
I can also provide my HLSL code if requested but I don't see how that can really be at fault since it doesn't do any offsetting or such and the shadow are after all projected where they should, were it not for the jumping around between frames. So I believe the fault should be with the depth (shadow) map rendering as outlined above.
The depth map is a slice in a Texture2DArray, 2048x2048 pixels in size and uses the DXGI_FORMAT_D32_FLOAT format. Naturally it has only a single mip slice.