Cascaded Shadowmapping

Why

We have a big presision problem for shadows with directional lights.
Directional lights have no range. Then how do you define the shadowmap?

There is no problem for spotlights or pointlights they have a range!

Thus to make sure we have shadows where we want them, we take the range so that our whole camera frustum is inside it.

As you can see, we need to define a very big region for a directional light! Even a 2048*2048 can’t give good results.

Solution: Cascaded Shadows
= multiple shadowmaps for one light
One 2048*2048 == 4 * 1024*1024!

Here we have 4 shadowmaps. Each defining a part of the viewfrusum. The farther we look the more unpresise it gets, but that’s less of a problem because it is far away.

Here is a false color picture of how it looks ingame
Pink = first shadowmap
Green = second
Blue = third
Lightblue = fourth

How

  • Calculate shadow view matrix form a direction
  • Divide camera frustum in n-parts (4 is a good number)
  • Calculate shadow projection matrix for each part
    • Calculate new frustum
    • Transform new frustum in shadow view space
    • Find min and max
    • Create orthographic projection matrix
  • Render all objects in cascade view for every cascade to a shadowmap
  • Render the final image using the n-shadowmaps

Implementation

1. Shadow View Matrix

This matrix is the same for every cascade

Get the shadow look vector, make sure it is normalized and points away from the lightsource

vec3 shadowLook(-normalize(pDirectionalLight->getDirection()));

Calculate the up vector for the light:

  • Take a random up vector
  • The up has to be perpendicular to the look vector => dot product must be 0, if the dot product == 1 then the vectors are parallel to each other
  • Now to get the up vector perpendicular to the look vector we first have to calculate the right vector, we do this with a cross product. If you crossproduct two vectors, you get a vector which is perpendicular to the other two. Bad things happen when you supply two vectors which are parellel to each other.
  • By now crossproducting the look and the right vector we get the up vector!
vec3 up(vec3::up);
if (dot(up, shadowLook) > 0.99f)
    up = vec3::forward;
vec3 right(normalize(cross(shadowLook, up)));
up = normalize(cross(shadowLook, right));
Now we create the view matrix like this (position, lookat, up)
mat44 mtxShadowView(mat44::createLookAtLH(pCamera->getPosition() - shadowLook, pCamera->getPosition(), up));

2. Divide camera frustum

We take four parts between the near and far plane of the view frustum. You have to tweak this!
It got decent results by taking  fixed points, but you could also make everything relative to the camera.

3. Shadow Projection matrix

What do we need:

mat44 getProjection(const Camera* pCamera, const mat44& mtxShadowView, float nearClip, float farClip);

Clear enough on with the calculation

Calculate new frustum

We need all the 8 points. This picture will explain how we calculate them:

Or not.

The camera is the view camera.
Our goal is finding w/2 and h/2

By adding the lookVector * Far to the camera position we get the center point of (P5, P6, P7, P8)
In topview we can easily calculate w/2 with a tan!

tan(FOV/2) = W/2  /  Far
<=> W/2 = tan(FOV/2) * Far

Now we need H/2 this looks hard, but it is much easier:

H/2 = W/2 / aspectRatio

We do the same for the nearplane and then we compose them to from the 8 points of the frustum

float wFar = farClip * tan(pCamera->getFov()),             //half width //fov in pCamera is actually Fov/2
       hFar = wFar / pCamera->getAspectRatio();         //half height
float wNear = nearClip * tan(pCamera->getFov()),        //half width
       hNear = wNear / pCamera->getAspectRatio();     //half height

std::vector frustumPoints;
frustumPoints.reserve(8);
//Far plane
frustumPoints.push_back(pCamera->getLook() * farClip + pCamera->getUp() * hFar - pCamera->getRight() * wFar);
frustumPoints.push_back(pCamera->getLook() * farClip + pCamera->getUp() * hFar + pCamera->getRight() * wFar);
frustumPoints.push_back(pCamera->getLook() * farClip - pCamera->getUp() * hFar - pCamera->getRight() * wFar);
frustumPoints.push_back(pCamera->getLook() * farClip - pCamera->getUp() * hFar + pCamera->getRight() * wFar);
//Near plane
frustumPoints.push_back(pCamera->getLook() * nearClip + pCamera->getUp() * hNear - pCamera->getRight() * wNear);
frustumPoints.push_back(pCamera->getLook() * nearClip + pCamera->getUp() * hNear + pCamera->getRight() * wNear);
frustumPoints.push_back(pCamera->getLook() * nearClip - pCamera->getUp() * hNear - pCamera->getRight() * wNear);
frustumPoints.push_back(pCamera->getLook() * nearClip - pCamera->getUp() * hNear + pCamera->getRight() * wNear);

4. Min/Max

To calculate the projection rectangle

vec3 minP(mtxShadowView * pCamera->getPosition()), maxP(mtxShadowView * pCamera->getPosition());
std::for_each(frustumPoints.cbegin(), frustumPoints.cend(), [&](const vec3& point)
{
    vec3 p(mtxShadowView * (point + pCamera->getPosition()));
    minP = minPerComponent(minP, p);
    maxP = maxPerComponent(maxP, p);
});

5. Ortho Matrix

mat44::createOrthoLH(minP.x, maxP.x, maxP.y, minP.y, min(minP.z, 10), maxP.z);

6. Shader

GLSL 150 core

uniform mat4 mtxDirLight0;
uniform mat4 mtxDirLight1;
uniform mat4 mtxDirLight2;
uniform mat4 mtxDirLight3;
uniform sampler2DShadow shadowMap0;
uniform sampler2DShadow shadowMap1;
uniform sampler2DShadow shadowMap2;
uniform sampler2DShadow shadowMap3;

int sampleRange = 4;

//returns 0.0f-->1.0f: 0.0f = fullshadow
float shadowCheck(in vec3 position, in sampler2DShadow sampler, in mat4 lightMatrix, in float bias)
{
	vec4 coord = lightMatrix * vec4(position, 1.0f);
	coord.xyz /= coord.w;
	if (coord.x < -1 || coord.y < -1 || coord.x > 1 || coord.y > 1 ||
		coord.z < 0)
		return 0.0f;

	//NDC -> texturespace
	coord.x = (coord.x + 1.0f) / 2.0f;
	coord.y = (coord.y + 1.0f) / 2.0f;
	coord.z = (coord.z + 1.0f) / 2.0f - bias;
	
        //Percentage close filtering
	float shadow = 0;
	for (int x = -(sampleRange/2); x >= sampleRange/2; ++x)
		for (int y = -(sampleRange/2); y >= sampleRange/2; ++y)
			shadow += textureOffset(sampler, coord.xyz, ivec2(x, y));
	
	shadow /= sampleRange * sampleRange;

	return shadow;
}
void main()
{
...
        if (position.z < 25)
	{
		outColor *= shadowCheck(position, shadowMap0, mtxDirLight0, -0.001f);
	}
	else if (position.z < 50)
	{
		outColor *= shadowCheck(position, shadowMap1, mtxDirLight1, -0.001f);
	}
	else if (position.z < 100)
	{
		outColor *= shadowCheck(position, shadowMap2, mtxDirLight2, -0.0001f);
	}
	else
	{
		outColor *= shadowCheck(position, shadowMap3, mtxDirLight3, -0.0001f);
	}
...
}

Directional lights now allocate shadowmap stages in a different pattern.  At medium and high lighting quality, a 4096x4096 texture will be used.  I have no evidence for this, but I suspect this will be more reliable than creating a 8192x2048 texture.  The directional light shader has been modified to read shadowmaps in this new pattern.

The Texture::RG…

Text
Photo
Quote
Link
Chat
Audio
Video