Thanks for giving it a go! Sounds like I will have to provide some code after all.
Finding the points in world space of the subfrustum for a cascade (FOV is in radians, a Scalf is just a float):
auto const
planes([this, &pass](std::size_t const i) -> mth::Scalf
{
return
(
pass.camera->projection().near +
(pass.camera->projection().far - pass.camera->projection().near) *
cutoffShadowmaps(i)
);
});
mth::Scalf const
width {static_cast<mth::Scalf>(pass.camera->viewport().x)},
height{static_cast<mth::Scalf>(pass.camera->viewport().y)},
FOV {pass.camera->projection().FOV * 0.5f},
tanHor{mth::tan(FOV)},
tanVer{mth::tan(FOV * (height / width))},
xNear {planes(indexCascade + 0) * tanHor},
xFar {planes(indexCascade + 1) * tanHor},
yNear {planes(indexCascade + 0) * tanVer},
yFar {planes(indexCascade + 1) * tanVer};
data.frustum[0] = { xNear, yNear, planes(indexCascade + 0), 1.0f};
data.frustum[1] = {-xNear, yNear, planes(indexCascade + 0), 1.0f};
data.frustum[2] = {-xNear, -yNear, planes(indexCascade + 0), 1.0f};
data.frustum[3] = { xNear, -yNear, planes(indexCascade + 0), 1.0f};
data.frustum[4] = { xFar, yFar, planes(indexCascade + 1), 1.0f};
data.frustum[5] = {-xFar, yFar, planes(indexCascade + 1), 1.0f};
data.frustum[6] = {-xFar, -yFar, planes(indexCascade + 1), 1.0f};
data.frustum[7] = { xFar, -yFar, planes(indexCascade + 1), 1.0f};
mth::Mat4f const
transformCam{pass.transformCamera->matrixGlobal()};
for (std::size_t i{0}; i < data.frustum.size(); ++ i)
data.frustum[i] = transformCam * data.frustum[i];
Using them to calculate the projection matrix for a light's camera (the view matrix is calculated same as for any camera based on its transform which is set at the world origin with the rotation of the light at the end of this code):
constexpr auto
calcBoundsFrustum([](std::array<mth::Vec4f, 8> const &frustum) -> mth::Bounds3f
{
mth::Vec3f const
xyz{frustum[0]};
mth::Bounds3f
bounds{xyz, xyz};
for (std::size_t i{1}; i < frustum.size(); ++ i)
{
bounds.min.x = mth::min(bounds.min.x, frustum[i].x);
bounds.max.x = mth::max(bounds.max.x, frustum[i].x);
bounds.min.y = mth::min(bounds.min.y, frustum[i].y);
bounds.max.y = mth::max(bounds.max.y, frustum[i].y);
bounds.min.z = mth::min(bounds.min.z, frustum[i].z);
bounds.max.z = mth::max(bounds.max.z, frustum[i].z);
}
return bounds;
});
mth::Transformf t{};
t.rotation = tr->global().rotation; // tr is the light's transform
mth::Mat4f const
transformLight{mth::inverse(t.matrix())};
std::array<mth::Vec4f, 8> const
frustum
{
transformLight * data.frustum[0],
transformLight * data.frustum[1],
transformLight * data.frustum[2],
transformLight * data.frustum[3],
transformLight * data.frustum[4],
transformLight * data.frustum[5],
transformLight * data.frustum[6],
transformLight * data.frustum[7]
};
mth::Bounds3f const
bounds{calcBoundsFrustum(frustum)};
data.camera.matrixProjection
(
mth::projection::orthographic
(
bounds.min.x,
bounds.max.x,
bounds.min.y,
bounds.max.y,
bounds.min.z,
bounds.max.z
)
);
data.transform.local(std::move(t));
Simplified version of shader (deferred, fragment position reconstructed from depth buffer) starts with this:
vec4 uvShadowDir = _projShadows[indexShadowmap] * _viewShadows[indexShadowmap] * vec4(position.xyz, 1.0);
For a regular
sampler2D I did this:
float z = uvShadowDir.z * 0.5 + 0.5;
float zShadowRaw = texture(_depthShadows[indexShadowmap], uvShadowDir.xy * 0.5 + 0.5).r;
float shadow = (zShadowDir < z) ? _darknessShadows : 1.0;
Which had the same issue. However I'm currently trying out
sampler2DShadow so now it looks like this:
float shadow = _darknessShadows * texture((indexShadowmap == 0) ? _depthShadows0 : _depthShadows1, (uvShadowDir.xyz * 0.5 + 0.5));
(those samplers won't work in an array for me so I have a hardcoded ternary for now)
- Is your shadow depth cut off? I often have the problem that even though any doc claims that Floating Point RenderTargets can carry any value, all my shader output is clamped to 0..1. That's why I normalize my shadow depth. Maybe you're in OGL and your shadow depth is -1..+1 and is clamped to 0..+1?
Yeah, I'm in OpenGL and using a depth buffer for this so not a "custom" float buffer. Should be 0-1.
- Maybe your cascade selection in the main pass takes the wrong turn? The first image shows a shadow from a build to the left, starting at the left, then a gap, then re-starting at the yellow ground. If your SM texture coords would be set to clamp, such a building would continue along the ground.
Buffer is set to clamp, but I also have this:
if (abs(uvShadowDir.x) > 1.0 || abs(uvShadowDir.y) > 1.0 || abs(uvShadowDir.z) > 1.0)
shadow = 1.0;
Because without it I get weird results sometimes (but this probably gets automatically resolved once this whole thing gets figured out?), like this intrusive shadow from nothing on the left:
If I colour it yellow if that if is true:
One thing I have noticed is that in many SM implementations the splits seem to follow the direction of the light instead of the direction of the main camera like I do (and the tutorial and most others AFAICT), is that something I should be doing? Or should this way work too?
EDIT:Used the if to filter cascades instead of the distance along the main camera frustum, which does at least ensure that it switches cascades when another doesn't cover any more area, but it seems clear that the matrices must still be wrong? Again red is near cascade, green is far cascade, and darkened areas are not covered by any cascade.
The debug lines are again the boxes around the frusta aligned with the light, but this time of the camera I'm actually rendering from.
( shadows flickering in and out is a GIF issue, as long as the area is red or green there are shadows where they should be
)
EDIT 2:So embarrassingly I got some of the matrices I was juggling mixed up and I was passing the wrong one in in some places. Turned out the bounding boxes were never fully aligned with the light. But there were still issues even after I fixed that, depending on the angle and such.
Before and after: Doesn't make sense to me based on what I thought I knew, but the projection matrix likes it a lot more if the view matrix is positioned in the middle of where I want to focus and the projection matrix bounds are neatly distributed evenly around that point. But even that was not a perfect guard against angular issues.
In the end I decided to just drop what I'd read and do something that makes sense in my own head (with inspiration from
this video), so at least I'll have something that works for now, which is this:
For each subfrustum I figure out which is greatest of the far plane width, its height, or the distance between the two planes, and I use that as the size of the bounding box in every direction to get a perfect cube around the middle of the subfrustum. I then multiply those extents by √2 to cover its maximum AABB at a 45° angle. That way I'm sure it always covers the whole subfrustum regardless of the angle of the light or the camera; it's probably a bit wasteful but the shadow maps always have the same texel size which according to the video helps reduce against "shimmering" anyway so it seems okay.
So I still don't feel like I've learnt how to do this the "proper" or "standard" way or whatever but at least I came up with one of my own that works so that's good enough for now:
Nonetheless, I'd still love help to do it "right" if anybody knows more about it all…