Welcome, Guest. Please login or register.

Login with username, password and session length

 
Advanced search

1411507 Posts in 69374 Topics- by 58429 Members - Latest Member: Alternalo

April 26, 2024, 12:15:35 AM

Need hosting? Check out Digital Ocean
(more details in this thread)
TIGSource ForumsDeveloperTechnical (Moderator: ThemsAllTook)Calculate cascaded shadow map matrix?
Pages: [1]
Print
Author Topic: Calculate cascaded shadow map matrix?  (Read 1764 times)
oahda
Level 10
*****



View Profile
« on: July 31, 2021, 08:29:43 AM »

Hey! Implemented shadow maps a while ago, been working fine, and tried to turn them into cascaded ones based mainly on this tutorial:

https://ogldev.org/www/tutorial49/tutorial49.html

I haven't just followed it, I'm sure I fully understand it too, but I can't for the life of me seem to get it exactly right. My maths should be the same as here, just adapted to fit the camera setup and so on already in place in my engine, but depending on the camera angle the shadow maps cut off:



For comparison, here's an angle that's not as bad:



There are two cascades; red is the nearer one and green is the farther one. I've kept the shadow acne in to make it easier to see the bugged cutoff points clearly.

Since I'm quite sure that my maths is the same as in the tutorial (and GLM deals with the specifics of calculating the ortho matrices based on left, right, top, bottom, near and far inputs) I'm not sure it's necessary to share my exact code…

I guess I'm wondering if the tutorial is correct in the first place, and if so if this kind of behaviour is expected using its method? Seems to me the resultant matrices should fully cover each subfrustum and not leave cutoffs in frame, right? I've enabled GL_DEPTH_CLAMP for the shadow map cameras.

To be clear I'm not talking about shadows getting cut off because their casters are not in the shadow map camera's frustum, but given that the acne also gets cut off the orthographic matrices (based on the light space bounding boxes of the main camera's frusta) for the cameras seem to be wrong in the first place…

If not, is it obvious what I might've done wrong? Will share code if it does end up being necessary. Thanks!

EDIT:

Might be clearer to see where I'm at with a GIF. Only one cascade here, I tied it to a separate camera object in the scene instead of the camera used to render the scene, so you can see what's happening from the outside:



The box around the frustum aligned with the light seems correct, always containing it? Here I've multiplied the points of the box with the inverse light view matrix to get it back to world space. The values I'm using for the shadow map matrix are the values before that, as in the tutorial.
« Last Edit: July 31, 2021, 10:52:37 AM by Prinsessa » Logged

Schrompf
Level 9
****

C++ professional, game dev sparetime


View Profile WWW
« Reply #1 on: July 31, 2021, 11:43:18 AM »

These things are hard to debug, and even harder to show to someone else. I have no idea what's going on here, just a few shots into the dark:

- 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?
- 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.
- Maybe your frustum in the shadow pass is off. When rendering, XY is -1..+1, while when accessing the SM it's 0..1. If your SM projection somehow also clamps XY to 0..1, it might look just like this.
Logged

Snake World, multiplayer worm eats stuff and grows DevLog
oahda
Level 10
*****



View Profile
« Reply #2 on: July 31, 2021, 01:06:21 PM »

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):

Code:
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):

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:

Code:
vec4 uvShadowDir = _projShadows[indexShadowmap] * _viewShadows[indexShadowmap] * vec4(position.xyz, 1.0);

For a regular sampler2D I did this:

Code:
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:

Code:
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:

Code:
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 Tongue )

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…
« Last Edit: August 09, 2021, 11:01:04 AM by Prinsessa » Logged

Pages: [1]
Print
Jump to:  

Theme orange-lt created by panic