Cascaded shadow map splits

Started by
11 comments, last by KaiserJohan 10 years ago

Hello,

I'm tinkering with cascaded shadow maps and I have an issue when it comes to shadows between splits.

The below picture shows it:

http://s23.postimg.org/ngwk5gnff/image.png

From the camera position to the edge of the shadow is the first split, and then from the end of the shadow starts the other split... the problem is the aliens shadow is in both splits. The alien is not inside the bounding box of the second split and thus the shadow abruptly stops. That is my theory anyway... and ideas?

Heres my lighting pass shader FYI (directional light source, deferred renderering):


#version 430  

 layout(std140) uniform;  

 const float DEPTH_BIAS = 0.00005;  

 uniform UnifDirLight  
{  
     mat4 mVPMatrix[4];     // the bias * crop * projection* view matrices for the directional light 
     float mSplitDistance[4];   // far distance for each split in camera space 
     vec4 mLightColor;  
     vec4 mLightDir;  
     vec4 mGamma;  
     vec2 mScreenSize;  
 } UnifDirLightPass;  

 layout (binding = 2) uniform sampler2D unifPositionTexture;  
 layout (binding = 3) uniform sampler2D unifNormalTexture;  
 layout (binding = 4) uniform sampler2D unifDiffuseTexture;  
 layout (binding = 6) uniform sampler2DArrayShadow unifShadowTexture;  

 out vec4 fragColor;  

 void main()  
{  
     vec2 texcoord = gl_FragCoord.xy / UnifDirLightPass.mScreenSize; 

     vec3 worldPos = texture(unifPositionTexture, texcoord).xyz;  // stored world position; in world space
     vec3 normal   = normalize(texture(unifNormalTexture, texcoord).xyz);  
     vec3 diffuse  = texture(unifDiffuseTexture, texcoord).xyz;  

     int index = 3;  
     if (worldPos.z > UnifDirLightPass.mSplitDistance[0])     // this is problematic when looking anywhere except straight the negative Z axis... any ideas on better comparison?
         index = 0;  
     else if (worldPos.z > UnifDirLightPass.mSplitDistance[1])  
         index = 1;  
     else if (worldPos.z > UnifDirLightPass.mSplitDistance[2])  
         index = 2;  

     vec4 projCoords = UnifDirLightPass.mVPMatrix[index] * vec4(worldPos, 1.0);                                                   
     projCoords.w    = projCoords.z - DEPTH_BIAS;  
     projCoords.z    = float(index);  
     float visibilty = texture(unifShadowTexture, projCoords);  

     float angleNormal = clamp(dot(normal, UnifDirLightPass.mLightDir.xyz), 0, 1);                                                

     fragColor = vec4(diffuse, 1.0) * visibilty * angleNormal * UnifDirLightPass.mLightColor;                                     
}
Advertisement

You seem to be specifying your positions in world coordinates, usually its easier to do this in view space. When you do the comparison against the splits far value, they are relative to the cameras position (along the cameras direction, -Z in GL view space), comparing against the fragments world z wont make sense, the world z would need to be the fragments view z (which why im guessing its only working when you dont move the camera, world = view then). Your comparison would then be down the negative z and you would probably use the < operator. Try getting the positions into view space, youre on the right track.

Cascaded shadow maps biggest problem is in the computation of the frustums of the cameras used to render the shadows. There are multiple kinds of policies;

The most common is surely the one that cuts the main cmera view frusutms into subparts according to distance and use a bouding volume of those slice to create an encompassing orthogonal frustum for the shadow camera.

There are lost efficiency in this scheme because of bounding volume of bounding volume so lots of shadow pixels end up off screen and never used. In other words you loose resolution in the visible zone.

Therefore some recent solutions using compute shaders to be able to determine the actual pixel perfect min depth and max depth percieved in a given image, then you can optimize the slices of the camera frustum to perfection making crazy high resolution shadows, especially in scenes a bit enclosed in walls.

There is another very simple policy for shadow frustums, just center the shadow camera on the view camera's position and zoom out in the direction of the light, each cascade zooming out a bit more thus logically encoding more distance in view space. But this has the problem of calculating shadows behind the view where they could be unnecessary.

I say could; because actually you never know when a long oblique shadows must drop from a high rise bulding located far behind you. this is why this simple scheme is also popular.

In my opinion; this is your scheme that fails. you should visualize the shadow zones by sampling red; blue and green to obtain this:

http://i.msdn.microsoft.com/dynimg/IC340432.jpg

once you get this debugging will be easy.

I've done some more digging using debugging colors. Here's the changes done to the shader when debugging:


vec4 worldPos2 =  UnifDirLightPass.mCamViewMatrix * vec4(worldPos, 1.0);    // mCamViewMatrix is the cameras view matrix                   
fragColor = vec4(1.0, 1.0, 1.0, 1.0);                                                                   
if (worldPos2.z > UnifDirLightPass.mSplitDistance[0])                                                                           
	fragColor = vec4(1.0, 0.0, 0.0, 1.0);                                                                                                           
else if (worldPos2.z > UnifDirLightPass.mSplitDistance[1])                                                                      
	fragColor = vec4(0.0, 1.0, 0.0, 1.0);                                                                                                     
else if (worldPos2.z > UnifDirLightPass.mSplitDistance[2])                                                                      
	fragColor = vec4(0.0, 0.0, 1.0, 1.0);                                                                                                         

Here's a screenshot without the debug colors:

image.png

Same image bug debug colors:

1_color.png

The splits looks okay... but as you see, the shadows gets clipped, and the cubes shadow isnt visible at all.

Another angle:

image.png

2_color.png

The splits are wrong right, when looking at this angle? It looks fine when looking straight down +Z or -Z, but looking 90 degrees to the side yields strange split colors. As you can see, theres also some shadow artifacts...

The values of "mSplitDistance" is {-6, -12, -18, -100}.

EDIT:

Heres how I create the lights VP matrix used for generating the shadowmap; it is multiplied by the bias matrix before being passed on to the shader.


Mat4 CreateDirLightVPMatrix(const CameraFrustrum& cameraFrustrum, const Vec3& lightDir)
    {
        const Vec3 lightDirx = glm::normalize(lightDir);
        const Vec3 perpVec1  = glm::normalize(glm::cross(lightDirx, Vec3(0.0f, 0.0f, 1.0f)));
        const Vec3 perpVec2  = glm::normalize(glm::cross(lightDirx, perpVec1));
        Mat4 lightViewMatrix(Vec4(perpVec1, 0.0f), Vec4(perpVec2, 0.0f), Vec4(lightDirx, 0.0f), Vec4(0.0f, 0.0f, 0.0f, 1.0f));

        Vec4 transf = lightViewMatrix * cameraFrustrum[0];    // cameraFrustrum is a std::array<Vec4, 8> and 0-3 is near-points and 4-7 are far points of the frustrum
        float maxZ = cameraFrustrum[0].z, minZ = cameraFrustrum[0].z;
        for (uint32_t i = 1; i < 8; i++)
        {
            transf = lightViewMatrix * cameraFrustrum[i];
            if (cameraFrustrum[i].z > maxZ)
                maxZ = cameraFrustrum[i].z;
            if (cameraFrustrum[i].z < minZ)
                minZ = cameraFrustrum[i].z;
        }

        const Mat4 mvp = glm::ortho(-1.0f, 1.0f, -1.0f, 1.0f, maxZ, minZ) * lightViewMatrix;

        float maxX = -1000.0f, minX = 1000.0f;
        float maxY = -1000.0f, minY = 1000.0f;
        for (uint32_t i = 0; i < 8; i++)
        {
            transf = mvp * cameraFrustrum[i];

            if (cameraFrustrum[i].x > maxX)
                maxX = cameraFrustrum[i].x;
            if (cameraFrustrum[i].x < minX)
                minX = cameraFrustrum[i].x;
            if (cameraFrustrum[i].y > maxY)
                maxY = cameraFrustrum[i].y;
            if (cameraFrustrum[i].y < minY)
                minY = cameraFrustrum[i].y;
        }

        float scaleX = 2.0f / (maxX - minX);
        float scaleY = 2.0f / (maxY - minY);
        float offsetX = -0.5f * (maxX + minX) * scaleX;
        float offsetY = -0.5f * (maxY + minY) * scaleY;

        Mat4 cropMatrix(1.0f);
        cropMatrix[0][0] = scaleX;
        cropMatrix[1][1] = scaleY;
        cropMatrix[3][0] = offsetX;
        cropMatrix[3][1] = offsetY;

        return cropMatrix * glm::ortho(-1.0f, 1.0f, -1.0f, 1.0f, maxZ, minZ) * lightViewMatrix;
    }
Heres how I create the lights VP matrix used for generating the shadowmap; it is multiplied by the bias matrix before being passed on to the shader.

I think youre missing the main cameras inverse view matrix in there

light split final matrix = bias * crop * proj * view * invViewCam

Heres how I create the lights VP matrix used for generating the shadowmap; it is multiplied by the bias matrix before being passed on to the shader.

I think youre missing the main cameras inverse view matrix in there

light split final matrix = bias * crop * proj * view * invViewCam

Oh, ok will try that. Is the invViewCam used in the shadow map pass aswell or is it only in the lighting pass (along with the bias matrix)?

Also, what is the reason behind having to use the inverse cam view matrix aswell?

Its used when projecting the created shadow maps onto the scene. Think of it this way

light split final matrix = ((bias * crop * proj * view) * invViewCam) * pixelPosInViewSpaceOfMainCamera

Parts:

(invViewCam) -> takes pixel in main cameras view space, transforms it backwards to world
(proj * view) -> takes pixel being rendering from main camera (now in world because of invViewCam application), puts it in lights clip space
(crop * proj) -> tightens the projection frustum around the split frustum area, increases depth use (z), tightens (x, y) around split area
(bias) -> takes cropped proj (clip) space coords, bias them for shadow mapping, takes them from clip (proj) coord [-1:+1] to texture coord space [0:+1]

The split distances were in view space, so anything compared against them had to be in view space (the pixel pos z). In view space, the Z axis is lined up straight out infront of the camera (we know this because that what view space is), allowing for the z comparison along a staight line to select the split.

Doing this in world coordinates wouldnt work because the camera might be looking down the x or y axis, or any other random axis, making comparing z's meaningless.

In short the comparison needs to be done in the main views camera space to avoid doing comparisions along a random camera world direction, but pixel pos in question needs to start in world space inorder to project->crop->bias into the lights shadow map for lookup.

I see, but given that the positions are originally stored in world space, there is no need to multiply the lights bias*crop*proj*view with the invViewCam though, as the positions already are in world space right?

I understand that the comparison to find the right split must be done in camera space though, so I did it like this:


#version 430  

 layout(std140) uniform;  

 const float DEPTH_BIAS = 0.00005;  

 uniform UnifDirLight  
{  
     mat4 mVPMatrix[4];     // the bias * crop * projection* view matrices for the directional light 
     mat4 mCamViewMatrix;     // <---  main cameras view matrix
     float mSplitDistance[4];   // far distance for each split in camera space 
     vec4 mLightColor;  
     vec4 mLightDir;  
     vec4 mGamma;  
     vec2 mScreenSize;  
 } UnifDirLightPass;  

 layout (binding = 2) uniform sampler2D unifPositionTexture;  
 layout (binding = 3) uniform sampler2D unifNormalTexture;  
 layout (binding = 4) uniform sampler2D unifDiffuseTexture;  
 layout (binding = 6) uniform sampler2DArrayShadow unifShadowTexture;  

 out vec4 fragColor;  

 void main()  
{  
     vec2 texcoord = gl_FragCoord.xy / UnifDirLightPass.mScreenSize; 

     vec3 worldPos = texture(unifPositionTexture, texcoord).xyz;  // stored world position; in world space
     vec3 normal   = normalize(texture(unifNormalTexture, texcoord).xyz);  
     vec3 diffuse  = texture(unifDiffuseTexture, texcoord).xyz;  

     vec4 camPos = UnifDirLightPass.mCamViewMatrix * vec4(worldPos, 1.0);       // <--- get camera space position for the lookup
	 
     int index = 3;  
     if (camPos.z < UnifDirLightPass.mSplitDistance[0])
         index = 0;  
     else if (camPos.z < UnifDirLightPass.mSplitDistance[1])  
         index = 1;  
     else if (camPos.z < UnifDirLightPass.mSplitDistance[2])  
         index = 2;  

     vec4 projCoords = UnifDirLightPass.mVPMatrix[index] * vec4(worldPos, 1.0);      // <--- lights VPs are bias*crop*proj*view; no need for inverse view matrix as we already have world position?                                                 
     projCoords.w    = projCoords.z - DEPTH_BIAS;  
     projCoords.z    = float(index);  
     float visibilty = texture(unifShadowTexture, projCoords);  

     float angleNormal = clamp(dot(normal, UnifDirLightPass.mLightDir.xyz), 0, 1);                                                

     fragColor = vec4(diffuse, 1.0) * visibilty * angleNormal * UnifDirLightPass.mLightColor;                                     
}

So I passed the cameras view matrix to the shader to transform the world position into camera space for the index lookup, but leave the lights matrix untouched.

All said and done, I still see pretty much the same results though, with the shadows being clipped and popping in and out at various distances and angles.

How are you rendering the depth to the shadow maps when you create them?

This is how I create the shadow maps and then render them:


std::array<float, gNumShadowmapCascades> nearDistArr, farDistArr;
std::array<Mat4, gNumShadowmapCascades> lightVPMatrices;

CalculateShadowmapCascades(nearDistArr, farDistArr, Z_NEAR, Z_FAR);    // gets the split ranges
std::array<float, gNumShadowmapCascades> splitDistances;       // contains the camera space split ranges that is used in the lighting shader

// fill shadowmaps
mDirectionalShadowmap.BindForDrawing();
GLCALL(glViewport(0, 0, (GLsizei)mShadowmapResolution, (GLsizei)mShadowmapResolution));
for (uint8_t cascadeIndex = 0; cascadeIndex < gNumShadowmapCascades; cascadeIndex++)
{
	Vec4 camFarDistCenter;
	CameraFrustrum cameraFrustrum = CalculateCameraFrustrum(nearDistArr[cascadeIndex], farDistArr[cascadeIndex], lighting.mCameraPosition, lighting.mCameraDirection, camFarDistCenter);

	lightVPMatrices[cascadeIndex] = CreateDirLightVPMatrix(cameraFrustrum, directionalLight.mLightDirection);
	DirLightShadowPass(renderQueue, lightVPMatrices[cascadeIndex], cascadeIndex);         // a simple depth pass, all geometry is in renderQueue and is drawn using the lights matrice, and is drawn to depth map cascadeIndex

	lightVPMatrices[cascadeIndex] = gBiasMatrix * lightVPMatrices[cascadeIndex];   // store the lights VP matrice multiplied by the bias matrice

        camFarDistCenter = lighting.mCameraViewMatrix * camFarDistCenter;   
	splitDistances[cascadeIndex] = camFarDistCenter.z;       
}

mGBuffer.BindFinalForDrawing();
GLCALL(glViewport(0, 0, (GLsizei)mWindowWidth, (GLsizei)mWindowHeight));
DirLightLightingPass(directionalLight, lightVPMatrices, lighting.mCameraViewMatrix, splitDistances, lighting.mGamma, lighting.mScreenSize);      // draws a rectangle and does the lighting

Heres how I create the camera frustrum (which is just a std::array<8, Vec4>)


CameraFrustrum CalculateCameraFrustrum(const float minDist, const float maxDist, const Vec3& cameraPosition, const Vec3& cameraDirection, Vec4& camFarZ)
{
	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 Vec3 forwardVec = glm::normalize(cameraDirection);
	const Vec3 rightVec   = glm::normalize(glm::cross(forwardVec, Vec3(0.0f, 1.0f, 0.0f)));
	const Vec3 upVec      = glm::normalize(glm::cross(rightVec, forwardVec));

	const Vec3 nearCenter = cameraPosition + forwardVec * minDist;
	const Vec3 farCenter  = cameraPosition + forwardVec * maxDist;

	camFarZ = Vec4(farCenter, 1.0);

	const float nearHeight = tan(glm::radians(70.0f) / 2.0f) * minDist;
	const float nearWidth = nearHeight * 1920.0f / 1080.0f;
	const float farHeight  = tan(glm::radians(70.0f) / 2.0f) * maxDist;
	const float farWidth = farHeight * 1920.0f / 1080.0f;

	ret[0] = Vec4(nearCenter - (upVec * nearHeight) - (rightVec * nearWidth), 1.0);
	ret[1] = Vec4(nearCenter + (upVec * nearHeight) - (rightVec * nearWidth), 1.0);
	ret[2] = Vec4(nearCenter + (upVec * nearHeight) + (rightVec * nearWidth), 1.0);
	ret[3] = Vec4(nearCenter - (upVec * nearHeight) + (rightVec * nearWidth), 1.0);

	ret[4] = Vec4(farCenter - upVec * farHeight - rightVec * farWidth, 1.0);
	ret[5] = Vec4(farCenter + upVec * farHeight - rightVec * farWidth, 1.0);
	ret[6] = Vec4(farCenter + upVec * farHeight + rightVec * farWidth, 1.0);
	ret[7] = Vec4(farCenter - upVec * farHeight + rightVec * farWidth, 1.0);

	return ret;
}

Does this shed any light?

This topic is closed to new replies.

Advertisement