Rendering Terrain Part 18 – Stable CSM, Finally!

I’m a little late getting this post up. I’ve been really unmotivated lately and I am really sick of shadows. This should be the second to last post on the subject. I’m pretty happy with how the shadows look now. Now all I have to do is optimize them by adding frustum culling to minimize how much geometry gets rendered on each shadow pass. Currently the whole terrain gets rendered four times every frame, just for the shadows.

At the end of last post, I showed this video. It demonstrates the still visible flickering. It is incredibly bad when the camera moves around. There is actually some flickering when the light moves as well, but it is very much less than before.

The first thing I’ll deal with is the depth bias. This was super easy to clean up. Here’s my new values:

descPSO.RasterizerState.DepthBias = 10000;
descPSO.RasterizerState.DepthBiasClamp = 0.0f;
descPSO.RasterizerState.SlopeScaledDepthBias = 1.0f;

That helped smooth out the shadows quite a bit. Are they perfect? No, but we don’t have an infinite amount of time, memory, or patience. Let’s move on.
To deal with the shimmering as the camera moves around, I needed to add back in some code I had implemented in Part 15. It hadn’t been any use to me then, but I figured I needed it now. This basically just comes down to rounding fractions so that shadows always move in exact integer increments, relative to the shadow map. Check the ‘Camera Moves’ section of that post for links to a good explanation of how to add this stuff.
Once again, that didn’t seem to change anything. At least, I didn’t think it did at first. As I moved the camera around, everything still flickered like crazy. But then I realized that the shadows weren’t flickering because they were moving in the shadow map. In fact, if I moved the camera I could tell that the shadows were fine. It was just that as I moved the camera, the shadow map was actually getting trashed.
I had been suspecting a major flaw in my code ever since I started working on shadow maps. I had seen some very odd behaviour showing up. I’d occasionally see the screen flash, and on the smaller maps, where performance is better due to less geometry, I was able to actually see the shadow map for multiple frames instead of the 3D view of the terrain. I had no idea what was going on at the time. I added a sleep() call on update for a couple of milliseconds and the flashing went away.
I tried increasing that sleep() time and this flickering in the shadows also went away. Suddenly, my shadows were fine.
The suspicion had been growing as I studied the Microsoft Code Samples that I was doing something wrong. As it turns out, I was.
Each frame, I had a set of shader constants, as well as the shadow map, that needed to be updated and sent to the GPU. The problem was that I was using the same buffers for each frame. I didn’t think about the fact that the GPU would still be working with that memory for the previous frame when I copied in the new data. This lead to what can best be described as a race condition.
Luckily, the fix is pretty straight forward. We are doing triple buffering, so we need three of every buffer that needs to be updated each frame. Then our application has to handle selecting the correct buffer. I’m not going to go over the code. I’m sure you get the idea.
That eliminated almost any trace of flickering due to the camera moving. The only remaining flickering was also addressed in Part 15. That is caused by changes in the level of detail of the geometry. We’ve already decided on a reasonable level in terms of results and performance, so we’re not going to try to improve that part. It’s barely noticeable anyway.
The only remaining source of flickering that we want to try and deal with is that still caused by the light source moving. It is better up close, but farther away looks terrible. This video demonstrates the shimmering. You can see it as the shadows retreat into the side of the hill.

I mentioned this problem back in Part 15 as well. It is not a solved problem. The best solution offered can be found here.
His solution is to render two shadow maps each frame and blend them together. I’m pretty sure he isn’t using Cascaded Shadow Maps, though. This would involve eight passes for me. That would be pretty slow. I thought I could use the previous frame’s shadow map, but then I’m back to trying to use something that is being changed while we’re trying to use it. I also thought that I could create another buffer to contain a copy of one of the frames, but the CopyResource() method is an asynchronous task, which means it returns before the GPU is actually done copying. We’re basically back to the same problem as before.
In the end, I gave up on this solution. There is a much simpler solution that gave results I am willing to accept. Just increase the resolution.
Up until this point, I had been working with a 2048×2048 shadow map. That looked really good as a single shadow map. I didn’t notice a huge amount of flickering due to the light moving, at least in the distance. But now that same size map is an atlas containing four maps, each a quarter the size, 1024×1024. The cascades used up close are covering a much smaller area and so are effectively a much higher resolution, but the distant cascades are even lower resolution than before. By increasing the size of our shadow atlas to 4096×4096, we quadruple the memory used for the shadows, but we also quadruple the resolution of our shadow maps. The flickering of the shadow maps is pretty hard to see at this point. In some cases, this solution would cause a performance hit, but our current bottle neck is the shear amount of geometry. The larger resolution maps don’t change the frame rate noticeably, if at all.

There is one other issue I needed to fix before I was happy with my shadows. There were seams along the boundaries where we switch cascades. It was difficult to get a picture or a video that really shows the problem. I had to cheat a bit to get it to show up clearly enough that you’d spot it in a video, but even as faint as it was, I didn’t want to leave it in.

The seam doesn’t exist if you don’t use the 3×3 multi-sampling that we’re doing to blur the edges of our shadows, but they do look better with that in place.
I thought the problem was caused by the filtering going over the edge of a cascade and sampling from the neighbouring cascade, but adding a bigger buffer to the radius of our frustum should have helped with that and it did not.
The fix wound up being really simple, though. When we choose what cascade to use, we have this code:

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.25) {
		return calcShadowFactor(shadowpos[0]);
	}

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

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

If we simply switch to the next cascade slightly early, the seam disappears. Just switch the < 0.25 to < 0.24 and you’re done.

Here’s a final video running around checking out the shadows. There’s still a bit of flickering due to the light moving, but I’m satisfied.

With that, the only thing I have left to do is try to get the frame rate down. We’re up to 9.3ms on a 2048×2048 height map with a 4096×4096 shadow atlas. That’s about 107fps. A little low. But once we get frustum culling in, we should be in good shape again.

The latest code is available on GitHub.

Traagen