Rendering Terrain Part 25 – Fixing More Bugs with Shadows

So I wound up throwing my lower back out earlier this week and didn’t feel like doing much work on this project. When I did eventually sit down and focus, I decided to look at fixing the bugs with my shadows. At first, there were two bugs that had been bugging me for the last couple of weeks. As I was looking at those bugs, I wound up discovering a third. So let’s look at what those bugs are.

Bug #1: Light Before The Dawn

The first bug I noticed only seemed to be visible at certain points on the height map, just before sunrise. There’s a moment before sunrise where the terrain lights up as though it isn’t still in shadow, then it goes back into shadow, then the sun comes up as normal.

It’s hard to catch, but in the last 2 or 3 seconds of the video, you can see a flash of colour in the black area to the right. It then goes back to black and then the shadows begin to shrink as the sun rises.
I was pretty sure I knew what the cause of this was, I just wasn’t sure why it was happening. Here’s an image of what one of the shadow maps looks like when the bug is visible:
shadowmapskirtclipped
Notice how the terrain sort of looks see through. I added a skirt back in Part 15 to ensure the terrain is always solid, but for some reason it still isn’t. The skirt seems to be getting clipped out. The result is that at certain points the sun can shine through the terrain, because we are using back face culling to reduce the geometry rendered, and the terrain winds up not being in shadow at night when it should be.
This really only seems to be an issue at about sunrise or sunset, depending on where you are on the map. Only the far side of the terrain from you seems to get clipped. Let’s look at the code that determines the shadow frustum and see why.

// orthographic frustum
float l = spherecenterls.x - radius;
float b = spherecenterls.y - radius;
float n = spherecenterls.z - radiusScene;
float r = spherecenterls.x + radius;
float t = spherecenterls.y + radius;
float f = spherecenterls.z + radiusScene;

This chunk of code is taken from our CalculateShadowMatrices() method, which we talked about in Part 17. It defines the six planes of the orthographic function used to define each shadow map.
Spherecenterls is the center of the current cascade’s frustum. Radius is the radius of the bounding sphere for that frustum. RadiusScene is the bounding sphere for the terrain itself. I chose to use radiusScene for the near and far planes of the shadow frustum in order to ensure we don’t clip the edges of the terrain in the direction of the light source. That isn’t working correctly at the moment, and if you haven’t already determined why, I’ll try and show you.
badclippingshadows
That picture isn’t the greatest, but I hope it gets the point across with some basic context. The green square is our terrain. The red square is the center of the terrain. The yellow circle is the center of the shadow frustum. The black circle has a radius of the bounding sphere of the terrain. The red arrow shows you the direction of the light.
So our current value of n is the far left point of the black circle. And f is the far right point. The idea was that those values would contain the entire terrain, but that is clearly incorrect. Luckily, the fix for this is pretty simple. We’ll simply recenter n and f on the center of the terrain. I was actually already passing that value to this method, so it’s just a matter of adding or subtracting the correct value from spherecenterls.

XMVECTOR sc = XMLoadFloat3(&centerBS);
XMFLOAT4 cbs;
XMStoreFloat4(&cbs, XMVector3TransformCoord(sc, V));

// orthographic frustum
float l = spherecenterls.x - radius;
float b = spherecenterls.y - radius;
float n = spherecenterls.z - cbs.z - radiusScene;
float r = spherecenterls.x + radius;
float t = spherecenterls.y + radius;
float f = spherecenterls.z + cbs.z + radiusScene;

It’s hard to get a screen shot of the same moment in time that the bug was occurring now that it isn’t, but here’s a shot of all four shadow maps approximately just before sunrise. You can see that all of them properly display the skirt and no longer have hollow spots for the sun to shine through.
shadowclippingfixed

Bug #2: Ghosts in The Hills

This bug was a hard one to track down. At first I thought it was one thing, then it turned out to be something else, which will lead to the third bug.
Here’s a video that shows the bug in action:

At first, I thought this bug might be caused by the fact my final cascade is the entire terrain. I know most implementations don’t do that. Hell, it wouldn’t make sense for most implementations because their worlds likely wouldn’t be limited to a single height map like mine is. My scene is my entire world. That isn’t usually the case. But for me, it’s a slight optimization. But I thought that was the problem. When I switched that last shadow map to be a cascade calculated just like the others, this bug disappeared. Or so I thought.
In reality, it was still there. I didn’t take a video of it, but the bug reared its ugly head again after I thought I had fixed it. I could stand in a relatively flat region, look towards the ground, and spin the view around in a circle. It wouldn’t take long to find a viewpoint where the ghost shadows appeared underneath us.
In Part 18, I had talked about adjusting the cut off value for determining which cascade to look at, with the last cascade being the fall back.

float decideOnCascade(float4 shadowpos[4]) {
	// if shadowpos[0].xy is in the range [0, 0.5], then this point is in the first cascade
	if (max(abs(shadowpos[0].x - 0.25), abs(shadowpos[0].y - 0.25)) < 0.24) {
		return calcShadowFactor(shadowpos[0]);
	}

	if (max(abs(shadowpos[1].x - 0.25), abs(shadowpos[1].y - 0.75)) < 0.24) {
		return calcShadowFactor(shadowpos[1]);
	}

	if (max(abs(shadowpos[2].x - 0.75), abs(shadowpos[2].y - 0.25)) < 0.24) {
		return calcShadowFactor(shadowpos[2]);
	}
	
	return calcShadowFactor(shadowpos[3]);
}

The reason for having the value 0.24 instead of 0.25 was to eliminate lines that were appearing at the transition between cascades. Making this change to eliminate those lines turned out to be the cause of these ghost shadows. The reason being that we’d wind up skipping the first shadow map, and since our value wasn’t in the range for the second or third maps, we’d wind up falling back to the final map, which was very low resolution in comparison so we’d introduce these glitches.
So I switched back to using 0.25 in order to fix this bug, but that reintroduced the old bug again. I can’t find the reference now, but I’m certain I had read that adjusting this cut off value for the edge of the shadow map is how you fix the transition bug. Nothing else I’ve tried, short of disabling the 3×3 PCF filtering that makes the shadows a bit smoother looking, has had any effect on this.
I decided to try and split the difference. Rather than using 0.24 that caused one bug or 0.25 that caused the other, I tried a few different values and eventually settled on 0.247. As far as I can tell, both bugs are no longer happening. I’ll have to keep an eye out but I looked pretty hard for either one to rear its head and neither did.

Bug #3: Bad Field of View Calculations

This is a relatively minor mistake that I made when coming up with my cascade frustums in Part 17. If you look back at that code, I needed both the horizontal and vertical fields of view. When we initially defined our camera, however, we only needed the vertical field of view for the perspective projection. I made the mistake of calculating the horizontal FOV as just the vertical FOV multiplied by the aspect ratio.

mHFOV = mVFOV * w / h;

This is incorrect. The correct calculation can be found here.
So I fixed this bug in the process of trying to fix the other bugs. Now, I have no idea what issues to attribute to this. When I change back to the incorrect code after having fixed the other bugs, everything looks perfectly fine. There were some bugs that I didn’t get screen shots or videos of before I fixed them, but I can’t reproduce them now in order to show them to you.
This bug would cause the cascade frustums to be calculated incorrectly, so I thought it would be part of the reason for either the ghost shadows or the transition lines, but neither bug reappears when I use the broken hFOV calculation.
I also had an issue while working on those where shadows around the edges of the screen would disappear as I move the mouse around. Again, that feels like an issue with the size of the frustum, but it doesn’t reappear. I did briefly try using an actual cascade frustum for the final cascade, as opposed to the scene bound frustum I’m currently using. It’s possible that the issue with shadows disappearing due to the incorrect hFOV calculation was only an issue while I was using that frustum.

My guess is that I had a few different mistakes that were compounding issues and some of the things I was seeing as I attempted to fix the above bugs were caused by the intermediate code I was trying. The important thing for me is that I’ve corrected the mistakes I know of and now I can’t find any remaining bugs in my shadow code.

With that out of the way, my next question to try and address is can I improve performance at all? Is there a way to do the height and slope based colour and normal mapping with less dynamic branching? Is there a way to reduce the number of samples we need to take without introducing the weird lines I showed you last post?
Also, I’m currently storing my normal maps as four separate textures. What would happen if I put those into a texture array? Some things I read indicate that would be more efficient. Other sources sounded like it probably won’t help me. I’ll try adding that and see if it makes any difference.

The latest code is available on GitHub.

Traagen