I've been working furiously and so far stable cascaded shadow maps are mocking me..... I can't seem to get this working for the life of me.....

So I will run through my algorithm and perhaps some kind soul will take pity on me and show me the error of my ways......

[1] I calculate the split frustum values for FOUR cascades.

I use this equation:

// get the currently active camera ICamera *pcCamera = getCurrentCamera (); vsAssert (pcCamera != NULL); if (m_fSplitNearZ < pcCamera->getNearZ ()) { m_fSplitNearZ = pcCamera->getNearZ (); } NUM_CASCADE_SPLITS = 4; for (vsInt32 iSplit = 1; iSplit < NUM_CASCADE_SPLITS; iSplit++) { vsFloat32 fStep = iSplit / (vsFloat32) NUM_CASCADE_SPLITS; vsFloat32 fLogSplit = m_fSplitNearZ * powf (m_fSplitFarZ / m_fSplitNearZ, fStep); vsFloat32 fLinearSplit = m_fSplitNearZ + (m_fSplitFarZ - m_fSplitNearZ) * fStep; // linearly interpolate between the linear and logarithmic split values m_fSplitPositions [iSplit] = fLogSplit * m_fLogSplitWeight + fLinearSplit * (1.0f - m_fLogSplitWeight); } // ensure the border split values are accurate m_fSplitPositions [0] = m_fSplitNearZ; m_fSplitPositions [NUM_CASCADE_SPLITS] = m_fSplitFarZ;

So now I have my frustum splits.....

I use these splits to calculate the corners of the cascade frustum that is being considered. So if I am going to update the first cascade shadow map I use the first and second frustum split positions. ie. m_fSplitPositions [0] and m_fSplitPositions [1].

So now I have 8 view space corners for the split frustum in view space. I generate the bounding box of these points and get the center. Again this is in View space. I take the max AABB point, subtract from the centroid I calculated and get it's length. This is what I use as the radius of the sphere that encloses this split frustum.

I then multiply the View Space frustum center by the inverse view to get the center in world space. I do the above for all FOUR cascade frustums when I am done I have FOUR spheres in TOTAL. One for each split sub frustum. Since I have 4 cascades I have 4 spheres.

CalculateCascadeBoundingSphere (void) { // find the minimum enclosing sphere for the split frustum, this is done in // local space to avoid precision variation between frames ICamera *pcCurrentCamera = getCurrentCamera (); vsAssert (pcCurrentCamera != NULL); vsFloat32 fTanHalfFOVY = pcCurrentCamera->getTanHalfFOVY (); vsFloat32 fTanHalfFOVX = fTanHalfFOVY * pcCurrentCamera->getAspectRatio (); vsFloat32 fNearX = fTanHalfFOVX * m_fCurrentNearSplit; vsFloat32 fNearY = fTanHalfFOVY * m_fCurrentNearSplit; vsFloat32 fNearZ = m_fCurrentNearSplit; vsFloat32 fFarX = fTanHalfFOVX * m_fCurrentFarSplit; vsFloat32 fFarY = fTanHalfFOVY * m_fCurrentFarSplit; vsFloat32 fFarZ = m_fCurrentFarSplit; // calculate the frustum AABB in view space CAABB cFrustumAABB; cFrustumAABB.m_cMin.setVector (FLT_MAX, FLT_MAX, FLT_MAX); cFrustumAABB.m_cMax.setVector (-FLT_MAX, -FLT_MAX, -FLT_MAX); cFrustumAABB.Add (CVector3 (fNearX, fNearY, fNearZ)); cFrustumAABB.Add (CVector3 (fNearX, -fNearY, fNearZ)); cFrustumAABB.Add (CVector3 (-fNearX, -fNearY, fNearZ)); cFrustumAABB.Add (CVector3 (-fNearX, fNearY, fNearZ)); cFrustumAABB.Add (CVector3 (fFarX, fFarY, fFarZ)); cFrustumAABB.Add (CVector3 (fFarX, -fFarY, fFarZ)); cFrustumAABB.Add (CVector3 (-fFarX, -fFarY, fFarZ)); cFrustumAABB.Add (CVector3 (-fFarX, fFarY, fFarZ)); CVector3 cCenter, cRadius; cCenter.Add (cFrustumAABB.m_cMin, cFrustumAABB.m_cMax); cCenter.MulScalar (0.5f); cRadius.Sub (cFrustumAABB.m_cMax, cCenter); vsFloat32 fRadius = cRadius.getLength (); }

These spheres I use to generate FOUR light view and FOUR light view projection matrices. I do that as follows:

Remember I do this FOUR times, so at the end I have FOUR light view matrices and FOUR light projection matrices:

CalculateCascadeViewMatrix (vsUInt32 iCascade) { vsAssert (m_pcDirectionalLightEditorObject != NULL); // calculate the cascade view matrix const CVector3 &rcLightDirection = m_pcDirectionalLightEditorObject->getDirection (); CVector3 cEye, cTarget; // target is center of view split frustum in world space cTarget.setVector (m_cCurrentCascadeBoundingSphere [iCascade].m_cCenter); // scale the light direction based on the shadow range CVector3 cScaledLightDirection (rcLightDirection); cScaledLightDirection.MulScalar (m_fShadowRange); // eye = target - (light_direction * shadow_range) cEye.Sub (cTarget, cScaledLightDirection); // generate the light view look at matrix LookAt (m_cViewMatrix [m_iCascade], cEye, cTarget); } LookAt (CMatrix4 &rcViewMatrix, const CVector3 &rcEye, const CVector3 &rcTarget) { CVector3 cZAxis; // we look down -z axis this is why vector is reversed cZAxis.Sub (rcEye, rcTarget); cZAxis.Normalize (); CVector3 cYAxis (CVector3::Y_AXIS); // check it's not coincident with direction if (CMath::FAbs (cYAxis.DotProduct (cZAxis)) >= 1.0f) { // use camera up cYAxis.setVector (CVector3::Z_AXIS); } // cross twice to rederive, only direction is unaltered CVector3 cXAxis; cXAxis.CrossProduct (cYAxis, cZAxis); cXAxis.Normalize (); cYAxis.CrossProduct (cZAxis, cXAxis); cYAxis.Normalize (); // setup the light view matrix (this places objects in light view space) rcViewMatrix.m [m00] = cXAxis.x; rcViewMatrix.m [m01] = cXAxis.y; rcViewMatrix.m [m02] = cXAxis.z; rcViewMatrix.m [m03] = -cXAxis.DotProduct (rcEye); rcViewMatrix.m [m10] = cYAxis.x; rcViewMatrix.m [m11] = cYAxis.y; rcViewMatrix.m [m12] = cYAxis.z; rcViewMatrix.m [m13] = -cYAxis.DotProduct (rcEye); rcViewMatrix.m [m20] = cZAxis.x; rcViewMatrix.m [m21] = cZAxis.y; rcViewMatrix.m [m22] = cZAxis.z; rcViewMatrix.m [m23] = -cZAxis.DotProduct (rcEye); rcViewMatrix.m [m30] = 0.0f; rcViewMatrix.m [m31] = 0.0f; rcViewMatrix.m [m32] = 0.0f; rcViewMatrix.m [m33] = 1.0f; }

Ok now I generate FOUR orthographic projection matrices, using the radius values of the four frustum split bounding spheres I generated up above:

CalculateCascadeProjectionMatrix (vsUInt32 iCascade) { vsFloat32 fWidth = m_cCascadeBoundingSphere [iCascade].m_fRadius * 2.0f; vsFloat32 fHeight = m_cCascadeBoundingSphere [iCascade].m_fRadius * 2.0f; // calculate the cascade orthographic projection matrix m_cProjectionMatrix [iCascade].m [m00] = 2.0f / fWidth; m_cProjectionMatrix [iCascade].m [m11] = 2.0f / fHeight; m_cProjectionMatrix [iCascade].m [m22] = -2.0f / (m_fProjectionFarZ - m_fProjectionNearZ); m_cProjectionMatrix [iCascade].m [m23] = -(m_fProjectionFarZ + m_fProjectionNearZ) / (m_fProjectionFarZ - m_fProjectionNearZ); m_cProjectionMatrix [iCascade].m [m33] = 1.0f; // engine specific projection matrix with render system depth range m_pcRenderSystem->ConvertProjectionMatrix (m_cProjectionMatrix [iCascade], m_cProjectionRSDepthMatrix [iCascade], true); }

I also generate a rounding matrix to prevent the shadow map cascades from shimmering as the camera translates:

CalculateCascadeRoundMatrix (vsUInt32 iCascade) { CVector3 cOriginShadow; CVector3 cOrigin (0.0f, 0.0f, 0.0f); // transform origin to light view projection space m_cLightViewProjectionMatrix [iCascade].Transform (cOriginShadow, cOrigin); vsFloat32 fShadowMapSize = getShadowMapSize (); // convert clip space to texture coordinates vsFloat32 fTexCoordX = cOriginShadow.x * fShadowMapSize * 0.5f; vsFloat32 fTexCoordY = cOriginShadow.y * fShadowMapSize * 0.5f; // round to the nearest whole texel vsFloat32 fTexCoordRoundedX = CMath::Round (fTexCoordX); vsFloat32 fTexCoordRoundedY = CMath::Round (fTexCoordY); /* the difference between the rounded and actual tex coordinate is the amount by which we need to translate the shadow matrix in order to cancel sub-texel movement */ vsFloat32 fDX = fTexCoordRoundedX - fTexCoordX; vsFloat32 fDY = fTexCoordRoundedY - fTexCoordY; // transform fDX, fDY back to homogenous light space fDX /= fShadowMapSize * 0.5f; fDY /= fShadowMapSize * 0.5f; // set the rounding matrix m_cRoundMatrix [iCascade].m [m03] = fDX; m_cRoundMatrix [iCascade].m [m13] = fDY; m_cRoundMatrix [iCascade].m [m23] = 0.0f; m_cRoundMatrix [iCascade].m [m33] = 1.0f; }

So now I have CMatrix4x4 m_cLightViewMatrix [4], CMatrix4x4 m_cLightProjectionMatrix [4] and m_cRoundMatrix [4];

I loop like so and generate the shadow matrix:

for (iCascade = 0; iCascade < 4; iCascade++)

{

m_cShadowMatrix [iCascade] = m_cRoundMatrix [iCascade] * m_cLightProjectionMatrix [iCascade] * m_cLightViewMatrix [iCascade];

}

I use m_cShadowMatrix [4] to generate the shadow maps in my shadow atlas..... So In the shader I generate the vertices of the shadow caster into word space and then multiply like so:

m_cShadowMatrix * m_cWorldMatrix * vsInput.vPosition;

In the pixel shader I save out the z value.

Is my algortihm sound? Or perhaps I missed something in my understanding of the algorithm. I would appreciate any help anyone can give me as to where I could be going wrong. Also if anyone knows of any demos with source that I could run to test this algorithm I would be ever so greatful.

Many thanks to anyone who can help me!