I am implementing cascade shadow mapping algorithm and currently stuck with matrix transformations - my AABBs, when projected in light space are pointing in the direction opposite to the light:
I was following the logic described in Oreon engine video on YouTube and NVidia docs.
The algorithm in my understanding looks like this:
- "cut" camera frustum into several slices
- calculate the coordinates of each frustum slice' corners in world space
- calculate the axis-aligned bounding box of each slice in world space (using the vertices from step 2)
- create an orthographic projection from the calculated AABBs
- using the orthographic projections from step 4 and light view matrix, calculate the shadow maps (as in: render the scene to the depth buffer for each of the projections)
- use the shadow maps to calculate the shadow component of each fragment' color; using
fragmentPosition.z
and comparing it to each of the camera frustum' slices to figure out which shadow map to use
I am able to correctly figure out camera frustum' vertices in world space:
The frustum extends further, but camera clipping distance... well, clips the further slices.
For this, I use inverse matrix multiplication of camera projection and camera view matrices and cube in normalized device coordinates:
std::array<glm::vec3, 8> _cameraFrustumSliceCornerVertices{
{
{ -1.0f, -1.0f, -1.0f }, { 1.0f, -1.0f, -1.0f }, { 1.0f, 1.0f, -1.0f }, { -1.0f, 1.0f, -1.0f },
{ -1.0f, -1.0f, 1.0f }, { 1.0f, -1.0f, 1.0f }, { 1.0f, 1.0f, 1.0f }, { -1.0f, 1.0f, 1.0f },
}
};
I then multiply each vertex \$p\$ by the inverse of the product \$P_{camera} \times V_{camera}\$
This gives me the vertices of the camera frustum in world space.
To generate slices, I tried applying the same logic, but using perspective projection with different near
and far
distances with little luck.
I then used vector math to calculate each camera frustum slice by taking the entire camera frustum vertices in world space and calculating the vectors for each edge of the frustum: \$v_i = v_i^{far} - v_i^{near}\$.
Then I simply multiply these vectors by the lengths of an entire camera frustum and multiply them by the corresponding slice fraction: \$v_i^{near} + v_i \cdot \|v_i^{far} - v_i^{near}\| \cdot d_i\$. Then I simply add these vectors to the near plane of the entire camera frustum to get the far plane of each slice.
std::vector<float> splits{ { 0.0f, 0.05f, 0.2f, 0.5f, 1.0f } };
const float _depth = 2.0f; // 1.0f - (-1.0f); normalized device coordinates of a view projection cube; zFar - zNear
auto proj = glm::inverse(initialCameraProjection * initialCameraView);
std::array<glm::vec3, 8> _cameraFrustumSliceCornerVertices{
{
{ -1.0f, -1.0f, -1.0f }, { 1.0f, -1.0f, -1.0f }, { 1.0f, 1.0f, -1.0f }, { -1.0f, 1.0f, -1.0f },
{ -1.0f, -1.0f, 1.0f }, { 1.0f, -1.0f, 1.0f }, { 1.0f, 1.0f, 1.0f }, { -1.0f, 1.0f, 1.0f },
}
};
std::array<glm::vec3, 8> _totalFrustumVertices;
std::transform(
_cameraFrustumSliceCornerVertices.begin(),
_cameraFrustumSliceCornerVertices.end(),
_totalFrustumVertices.begin(),
[&](glm::vec3 p) {
auto v = proj * glm::vec4(p, 1.0f);
return glm::vec3(v) / v.w;
}
);
std::array<glm::vec3, 4> _frustumVectors{
{
_totalFrustumVertices[4] - _totalFrustumVertices[0],
_totalFrustumVertices[5] - _totalFrustumVertices[1],
_totalFrustumVertices[6] - _totalFrustumVertices[2],
_totalFrustumVertices[7] - _totalFrustumVertices[3],
}
};
for (auto i = 1; i < splits.size(); ++i)
{
std::array<glm::vec3, 8> _frustumSliceVertices{
{
_totalFrustumVertices[0] + (_frustumVectors[0] * _depth * splits[i - 1]),
_totalFrustumVertices[1] + (_frustumVectors[1] * _depth * splits[i - 1]),
_totalFrustumVertices[2] + (_frustumVectors[2] * _depth * splits[i - 1]),
_totalFrustumVertices[3] + (_frustumVectors[3] * _depth * splits[i - 1]),
_totalFrustumVertices[0] + (_frustumVectors[0] * _depth * splits[i]),
_totalFrustumVertices[1] + (_frustumVectors[1] * _depth * splits[i]),
_totalFrustumVertices[2] + (_frustumVectors[2] * _depth * splits[i]),
_totalFrustumVertices[3] + (_frustumVectors[3] * _depth * splits[i]),
}
};
// render the thing
}
According to the algorithm, the next part is finding the axis-aligned bounding box (AABB) of each camera frustum slice and projecting it in the light view space.
I am able to correctly calculate the AABB of each camera frustum slice in world space:
This is a rather trivial algorithm that iterates over all the vertices from the previous step and finds minimal x
, y
and z
coordinate of each vertex of a camera frustum slice in world space.
float minX = 0.0f, maxX = 0.0f;
float minY = 0.0f, maxY = 0.0f;
float minZ = 0.0f, maxZ = 0.0f;
for (auto i = 0; i < _frustumSliceVertices.size(); ++i)
{
auto p = _frustumSliceVertices[i];
if (i == 0)
{
minX = maxX = p.x;
minY = maxY = p.y;
minZ = maxZ = p.z;
}
else
{
minX = std::fmin(minX, p.x);
minY = std::fmin(minY, p.y);
minZ = std::fmin(minZ, p.z);
maxX = std::fmax(maxX, p.x);
maxY = std::fmax(maxY, p.y);
maxZ = std::fmax(maxZ, p.z);
}
}
auto _ortho = glm::ortho(minX, maxX, minY, maxY, minZ, maxZ);
std::array<glm::vec3, 8> _aabbVertices{
{
{ minX, minY, minZ }, { maxX, minY, minZ }, { maxX, maxY, minZ }, { minX, maxY, minZ },
{ minX, minY, maxZ }, { maxX, minY, maxZ }, { maxX, maxY, maxZ }, { minX, maxY, maxZ },
}
};
std::array<glm::vec3, 8> _frustumSliceAlignedAABBVertices;
std::transform(
_aabbVertices.begin(),
_aabbVertices.end(),
_frustumSliceAlignedAABBVertices.begin(),
[&](glm::vec3 p) {
auto v = lightProjection * lightView * glm::vec4(p, 1.0f);
return glm::vec3(v) / v.w;
}
);
I then construct an orthographic projection from that data - as per algorithm, these projections, one per camera frustum slice, will be later used to calculate shadow maps, aka render to depth textures.
auto _ortho = glm::ortho(minX, maxX, minY, maxY, minZ, maxZ);
To render these AABBs, I tried rendering the view cube, like with the camera frustum, but got some dubious results:
Both the position and the size of the AABBs were wrong.
I tried making the AABBs "uniform", e.g. left = ((maxX - minX) / 2) * -1
and rihgt = ((maxX - minX) / 2) * +1
, which resulted in only centering the AABBs around the same origin point (0, 0, 0)
:
const auto _width = (maxX - minX) / 2.0f;
const auto _height = (maxY - minY) / 2.0f;
const auto _depth = (maxZ - minZ) / 2.0f;
auto _ortho = glm::ortho(-_width, _width, -_height, _height, -_depth, _depth);
I then used min / max
values of each corresponding coordinate instead of +/- 1
in the view cube to get the correct results:
std::array<glm::vec3, 8> _aabbVertices{
{
{ minX, minY, minZ }, { maxX, minY, minZ }, { maxX, maxY, minZ }, { minX, maxY, minZ },
{ minX, minY, maxZ }, { maxX, minY, maxZ }, { maxX, maxY, maxZ }, { minX, maxY, maxZ },
}
};
Last step of an algorithm, though is not willing to cooperate: I thought that by multiplying each of the orthogonal projections by the light' view matrix I will align the AABB with the light direction, but all I got was misaligned AABBs:
std::array<glm::vec3, 8> _frustumSliceAlignedAABBVertices;
std::transform(
_aabbVertices.begin(),
_aabbVertices.end(),
_frustumSliceAlignedAABBVertices.begin(),
[&](glm::vec3 p) {
auto v = lightView * glm::vec4(p, 1.0f);
return glm::vec3(v) / v.w;
}
);
Only when I multiply it by both light projection matrix and light view matrix I get something similar to alignment:
std::array<glm::vec3, 8> _frustumSliceAlignedAABBVertices;
std::transform(
_aabbVertices.begin(),
_aabbVertices.end(),
_frustumSliceAlignedAABBVertices.begin(),
[&](glm::vec3 p) {
auto v = lightProjection * lightView * glm::vec4(p, 1.0f);
return glm::vec3(v) / v.w;
}
);
Ironically, seems the direction is opposite to the light' direction.
Despite my light being pointed to origin (0, 0, 0)
, the AABBs seem to be projected in reverse order.
Question: why is this happening? Why is the direction & order of the projections reversed? How to put it in a correct order?