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
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
Transform new frustum in shadow view space
Create orthographic projection matrix
Render all objects in cascade view for every cascade to a shadowmap
Render the final image using the n-shadowmaps
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:
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));
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.
Cascade near far 1. camera near 25 2. 25 50 3. 50 100 4. 100 camera far
3. Shadow Projection matrix
mat44 getProjection(const Camera* pCamera, const mat44& mtxShadowView, float nearClip, float farClip);
Clear enough on with the calculation
We need all the 8 points. This picture will explain how we calculate them:
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:
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);
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); });
mat44::createOrthoLH(minP.x, maxP.x, maxP.y, minP.y, min(minP.z, 10), maxP.z);
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); } ... }