I am not entirely sure how to resolve this issue or evenQuestion: why this is this happening...? Why is the direction & order of the projections reversed? How to put it in a correct order?
I am not entirely sure how to resolve this issue or even why this is happening...
Question: why is this happening? Why is the direction & order of the projections reversed? How to put it in a correct order?
Matrix math in cascade shadow mapping
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.zand 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.
I am not entirely sure how to resolve this issue or even why this is happening...





