Terrain lod shadows switching issue

I have terrain with high mountains and use full dynamic lightning/shadows.
when i close to mountain bottom(always look at bottom/mid, not at top) i can clearly see often switching between lods(based on shadows changes). Shadows look really bad cause of that.

Decreasing lods count doesnt help.
Disabling lods for terrain causes noticeable performance drop.
Keeping “shadows only” version of terrain on top. i am sure it will cause self shadowing issues

Maybe there are way to control shadows lod switching with code, i want to change “Y” criteria?
Do u know some other ways for dynamic lightning?

Unity stated that it will not be fixed
They are suggesting to Decrease lods count

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 &params)
{
	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 &params)
{
	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 &params)
{
	// 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 &params)
{
	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?

hey Aras, your analysis is spot on, that’s exactly what’s happening in the code. we have an opt-out coming, enableHeightmapLODFrustumCulling, true by default so existing projects are unaffected. i’ll try to get it backported to 6000.0 as well. We are prioritizing this.

For a proper frustum-aware shadow solution, we agree that’s the right long term fix but can’t commit to a timeline on that yet (hence the opt out solution for now).

Thank you, Aras and Mat!

Excellent, thank you for fixing this! Looking forward to getting a fix.

On this particular game (the terrain has a lot of very steep canyons/cliffs) the shadows issue is so bad that I was considering actually developing a complete custom Terrain replacement that would work on the same source data. Unity is flexible enough that it should be possible to do this (yay!), but that’s also like, I dunno, a month of work or something. And allocating that much work from the regular game development schedule just to work around a bug is something I’d rather not do :slight_smile: