• Announcements

    • khawk

      Download the Game Design and Indie Game Marketing Freebook   07/19/17

      GameDev.net and CRC Press have teamed up to bring a free ebook of content curated from top titles published by CRC Press. The freebook, Practices of Game Design & Indie Game Marketing, includes chapters from The Art of Game Design: A Book of Lenses, A Practical Guide to Indie Game Marketing, and An Architectural Approach to Level Design. The GameDev.net FreeBook is relevant to game designers, developers, and those interested in learning more about the challenges in game development. We know game development can be a tough discipline and business, so we picked several chapters from CRC Press titles that we thought would be of interest to you, the GameDev.net audience, in your journey to design, develop, and market your next game. The free ebook is available through CRC Press by clicking here. The Curated Books The Art of Game Design: A Book of Lenses, Second Edition, by Jesse Schell Presents 100+ sets of questions, or different lenses, for viewing a game’s design, encompassing diverse fields such as psychology, architecture, music, film, software engineering, theme park design, mathematics, anthropology, and more. Written by one of the world's top game designers, this book describes the deepest and most fundamental principles of game design, demonstrating how tactics used in board, card, and athletic games also work in video games. It provides practical instruction on creating world-class games that will be played again and again. View it here. A Practical Guide to Indie Game Marketing, by Joel Dreskin Marketing is an essential but too frequently overlooked or minimized component of the release plan for indie games. A Practical Guide to Indie Game Marketing provides you with the tools needed to build visibility and sell your indie games. With special focus on those developers with small budgets and limited staff and resources, this book is packed with tangible recommendations and techniques that you can put to use immediately. As a seasoned professional of the indie game arena, author Joel Dreskin gives you insight into practical, real-world experiences of marketing numerous successful games and also provides stories of the failures. View it here. An Architectural Approach to Level Design This is one of the first books to integrate architectural and spatial design theory with the field of level design. The book presents architectural techniques and theories for level designers to use in their own work. It connects architecture and level design in different ways that address the practical elements of how designers construct space and the experiential elements of how and why humans interact with this space. Throughout the text, readers learn skills for spatial layout, evoking emotion through gamespaces, and creating better levels through architectural theory. View it here. Learn more and download the ebook by clicking here. Did you know? GameDev.net and CRC Press also recently teamed up to bring GDNet+ Members up to a 20% discount on all CRC Press books. Learn more about this and other benefits here.
Sign in to follow this  
Followers 0
KaiserJohan

Cascaded shadow map splits

12 posts in this topic

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;                                     
}
Edited by KaiserJohan
0

Share this post


Link to post
Share on other sites

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.

Edited by NumberXaero
1

Share this post


Link to post
Share on other sites

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.

1

Share this post


Link to post
Share on other sites

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;
    }
Edited by KaiserJohan
0

Share this post


Link to post
Share on other sites
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

Edited by NumberXaero
1

Share this post


Link to post
Share on other sites

 

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?

Edited by KaiserJohan
0

Share this post


Link to post
Share on other sites

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.

Edited by NumberXaero
1

Share this post


Link to post
Share on other sites

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.

Edited by KaiserJohan
0

Share this post


Link to post
Share on other sites

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?

Edited by KaiserJohan
0

Share this post


Link to post
Share on other sites

What if you were to use the view space pos in this line instead of the world

 

vec4 projCoords = UnifDirLightPass.mVPMatrix[index] * vec4(worldPos, 1.0);

 

and incorporate the invMainView into the lights matrix?

0

Share this post


Link to post
Share on other sites

I don't know GLSL 4xx quite well, what is this line here trying to do?


layout(std140) uniform;

 

You declare the layout of an uniform... but without the uniform?

0

Share this post


Link to post
Share on other sites

What if you were to use the view space pos in this line instead of the world

 

vec4 projCoords = UnifDirLightPass.mVPMatrix[index] * vec4(worldPos, 1.0);

 

and incorporate the invMainView into the lights matrix?

 

I tried, I guess they cancel each other out; the result is the same as using the world position directly.

 

 

I don't know GLSL 4xx quite well, what is this line here trying to do?

 


layout(std140) uniform;

 

You declare the layout of an uniform... but without the uniform?

 

Yeah that was a bug; wierd that it compiled, fixed it but it didn't have any effect on the rest of the code :)

0

Share this post


Link to post
Share on other sites

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!


Register a new account

Sign in

Already have an account? Sign in here.


Sign In Now
Sign in to follow this  
Followers 0