I want to add shadow mapping to my realtime renderer. I start with directional lights first.
The algorithm:
I am stuck at (1). I need to compute the light optimal view projection matrix. It is made of the product of the view and projection matrices. To do so I need to make the light frustrum fit the whole scene.
The scene AABB(min,max) is given as well as the light direction.
To summarize the matrices i need are:
a) The light View matrix.
Typically computed by DirectX::XMMatrixLookAtRH
or glm::lookAtRH.
What should be the parameters EyePosition, FocusPosition and UpDirection?
b) The light projection matrix.
Since it is a directional light this should be an orthographic projection.
Typically computed by DirectX::XMMatrixOrthographicOffCenterRH
or glm::orthoRH
What would be the parameters ViewLeft, ViewRight, ViewBottom, ViewTop, NearZ, farZ?
c) The light view projection matrix.
Product of (a) and (b)
If too far would get something like this. I lose precision
If too close i lose information
The light's view matrix transforms world space into light space.
EyePosition
Pick a position along the light direction such that it can "see" the scene's bounding box. A simple choice is to center it on the scene's AABB center, offset by some distance along the negative light direction.
glm::vec3 lightDir = glm::normalize(lightDirection);
glm::vec3 center = (sceneAABBMin + sceneAABBMax) * 0.5f;
glm::vec3 eyePos = center - lightDir * distance;
The distance
should be large enough to encompass the scene.
FocusPosition
The center of the scene's AABB
FocusPosition = center
UpDirection
You need to pick an arbitrary up vector that isn't parallel to your light direction. Typically (0,1,0) is fine unless your light is exactly vertical.
if (abs(dot(lightDir, glm::vec3(0,1,0))) > 0.99f)
up = glm::vec3(0,0,1);
else
up = glm::vec3(0,1,0);
How to use
glm::mat4 lightView = glm::lookAtRH(eyePos, center, up);
This is where you fit the projection to tightly cover the scene's AABB in light space.
Code as below.
// Get scene AABB corners in world space
std::vector<glm::vec3> corners = {
{sceneMin.x, sceneMin.y, sceneMin.z},
{sceneMax.x, sceneMin.y, sceneMin.z},
{sceneMin.x, sceneMax.y, sceneMin.z},
{sceneMax.x, sceneMax.y, sceneMin.z},
{sceneMin.x, sceneMin.y, sceneMax.z},
{sceneMax.x, sceneMin.y, sceneMax.z},
{sceneMin.x, sceneMax.y, sceneMax.z},
{sceneMax.x, sceneMax.y, sceneMax.z},
};
// Transform to light space
glm::mat4 lightView = ...;
std::vector<glm::vec3> lightSpaceCorners;
for (const auto& c : corners)
lightSpaceCorners.push_back(glm::vec3(lightView * glm::vec4(c, 1.0)));
// Compute new AABB in light space
glm::vec3 minLS = lightSpaceCorners[0];
glm::vec3 maxLS = lightSpaceCorners[0];
for (const auto& c : lightSpaceCorners)
{
minLS = glm::min(minLS, c);
maxLS = glm::max(maxLS, c);
}
// Use as ortho bounds
float left = minLS.x;
float right = maxLS.x;
float bottom = minLS.y;
float top = maxLS.y;
float nearZ = minLS.z;
float farZ = maxLS.z;
glm::mat4 lightProj = glm::orthoRH(left, right, bottom, top, nearZ, farZ);
Simply glm::mat4 lightViewProj = lightProj * lightView;
For Precision and Coverage
If you're doing cascaded shadow maps, you'd adjust the AABB per cascade frustum slice.
Sometimes bias the near/far a little (e.g., expand by 5-10%) to avoid clipping edge cases.
You can also clamp nearZ
to 0 and shift farZ
out slightly, to avoid depth fighting in light space.
To improve shadow stability (avoid shimmering as the camera moves)
Snap the orthographic projection to shadow map texel size in world units.
Align the orthographic projection bounds to texel-sized increments.