We just ran into the same issue, and it is extremely annoying. This was not a problem in 2022.3.7, but became a problem starting in 2022.3.62 (i.e. introduced sometime between patches OF THE SAME LTS!). Nothing that feels relevant is mentioned in any release notes in between. This keeps on being a problem in 6000.0 (which my current project actually uses), and persists to being a problem in 6000.5 alpha 8 (latest I have tested).
We were trying to make a new trailer for the game just last week, and the shadows glitch was happening all over the place. Here’s a short excerpt showing the issue:
I have made a tiny repro and submitted it as a bug (IN-140382):
But it was closed as a duplicate of the above mentioned UUM-131681.
When a bug is closed there’s seemingly no further avenues in how to try to convince Unity that it is a problem, so I’m trying to resurrect this thread. If that does not work, I will have to try to make a viral thread on some social media. If that does not work, I dunno, will I have to apply for a job at Unity, fix that bug, and then leave?
Here’s my attempt at an analysis of what might be going on:
My impression is that terrain heightmap is LOD’ed/rendered using some sort of quad-tree scheme, and so the LOD process would be along the lines of
void LODRecursive(Node *node, const CullingParameters ¶ms)
{
bool isGood = IsNodeGoodEnough(node, params);
if (isGood)
{
// this node is detailed enough to get rendered with
// current culling parameters, render it
AddNodeForRendering(node);
}
else
{
// this node is not detailed enough, recurse into child nodes
for (int ch = 0; ch < 4; ++ch)
{
LODResursive(node->children[ch], params);
}
}
}
And my guess is that DetermineIfNodeIsGoodEnough was just looking at node’s bounding box, camera parameters (position, field of view) and the specified allowed pixel error – i.e. the sufficient LOD level was purely based on distance to the camera, and did not change at all depending on view direction. Something like this:
bool IsNodeGoodEnough(const Node *node, const CullingParameters ¶ms)
{
float distanceToNode = CalculateDistance(params.cameraPosition, node->bounds);
// some sort of math based on distance & params
}
This works, and has no visibility bugs, but is not 100% optimal. You might want to not use high detail terrain surface if it is not visible. So someone must have added code along the lines of this, presumably:
bool IsNodeGoodEnough(const Node *node, const CullingParameters ¶ms)
{
// NEW ADDITION!
// if node outside of camera frustum: just use it, do not go into more detailed
// nodes
if (!IsBoundsVisibleInFrustum(params.cameraFrustum, node->bounds))
return true;
float distanceToNode = CalculateDistance(params.cameraPosition, node->bounds);
// some sort of math based on distance & params
}
This works IF AND ONLY IF there are no things that could cause a node being outside the view to affect things inside the view. In other words, shadows – if shadows are cast from a chunk of terrain outside the view, the shadow itself can very much be inside the view!
I think people doing this optimization realized that, but instead of fixing it properly (more on that below), they added additional settings on the terrain, “Minimum Detail Limit” and “Maximum Complexity Limit”. The settings are not documented in what exactly they do, they just say what are the valid ranges of the settings, and mention that when one of them is changed, the other potentially changes too. Great job (not).
So presumably they added code like this:
bool IsNodeGoodEnough(const Node *node, const CullingParameters ¶ms)
{
if (node->level == params.maxComplexityLimit)
return true; // already reached max allowed detail terrain setting: use it
// NEW ADDITION!
if (node->level < params.minDetailLevel)
return false; // node not detailed enough
// if node outside of camera frustum: just use it, do not go into more detailed
// nodes
if (!IsBoundsVisibleInFrustum(params.cameraFrustum, node->bounds))
return true;
float distanceToNode = CalculateDistance(params.cameraPosition, node->bounds);
// some sort of math based on distance & params
}
Now, this feels like trying to put the problem if incorrect shadows under a rug. It creates more work for the user (they need to tweak minimum allowed terrain detail levels), it has downside of globally increasing terrain detail everywhere, and it is not robust; no matter how much you tweak the levels the potential for “buggy shadows” is still there.
An actual proper solution would be to take shadow casting lights into account instead of only the (hypothetical) IsBoundsVisibleInFrustum. Or heck, even just main directional light if it has shadows. Code to do all that must already exist within Unity; that’s how it would determine which shadow casters are visible, afterall! So hypothetically, something like:
if (!IsBoundsVisibleInExtrudedFrustum(params.cameraPosition, params.mainLightDirection, node->bounds))
Now, myself I’d also gladly take the previous behavior of just not doing this optimization. Or an option to not do it, e.g. expose a checkbox in terrain settings, something like bool UseLowDetailOutsideOfView, where “true” would be this new (buggy but “optimized”) behavior, and “false” would be behavior in 2022.3 and all earlier versions. And then in the (again, hypothetical) code
it would be just:
if (params.useLowDetailOutsideOfView && !IsBoundsVisibleInFrustum(params.cameraFrustum, node->bounds))
return true;
Unity, can you PLEASE at least add a way to opt-out of this “optimization” that results in buggy shadows?
It is not great at all that this started breaking in an LTS version. Not great that it was not mentioned in the release notes. Not great that the settings that got added to presumably work around the buggyness of this “optimization” are not documented except for “when you change one of these settings, the other changes too!”. All of these are bad. But whatever; all I want is for my terrain shadows to not be bugged. Please? Can haz?