Progress Report: Shadow Mapping

Chen  —  2 months, 1 week ago [Edited 4 days, 20 hours later]
Hi everyone. It’s an honor to be featured by handmade network. I will be pushing out blogs where I talk about the challenges I face during Monter’s development. I’ve been thinking long and hard about what to write about on a first blog. The best choice seems to be writing a review of what I’ve done for the game engine so far, but I’ve just finished shadow mapping, so I decided to go with it this time because it’s still fresh. A review of the entire engine will probably be the topic of my second blog, so stay tuned.

Shadow in Real Time Rendering

The cleanest way to do shadow, which is done in most ray tracers, is raycasting against light casters. When computing radiance from a surface point to the screen, a ray is cast from the surface point to the light caster, and if it’s obstructed by any shadow occluders, the surface point is shadowed. However, testing rays against meshes made of polygons is expensive. That process can be accelerated by kd trees and such, but it doesn’t integrate easily to my rendering pipeline that mainly runs on the GPU. It also raises complexity and bugs creep out of that. The more time I have to spend on engine before actually prototyping the game, the more likely Monter will be stalled in development. So this method is a no no.

The low-complexity way of doing shadow is shadow texture mapping (or more accurately, depth mapping). It’s standard practice for games because it’s relatively cheap and simple to implement. But it’s pretty nasty; and we are going to talk about the shadow mapping implementation in Monter and why I think it’s an unpleasant experience.

The General Idea

The general idea of shadow mapping is pretty simple. You take the camera to where the light caster is at and you point the camera in the same direction as the light caster. You render a depth map of the scene and store it away as a texture. After that, when you are lighting any surface point in the scene, you can transform that point into the view space of that light caster and compare the depth of that point against the depth value inside that depth texture that corresponds to that point’s direction. If the depth of the shaded point is deeper than the one queried from texture, then we know that this point is occluded by something else from the light caster, since there’s surface that has a smaller depth value than that point. It might seem like a clever idea at first, but it’s hard to execute well in practice. While implementing shadow mapping for Monter’s renderer, a lot of gross artifact appeared. I am going to list them, explain why they occur and how I eliminated them (mostly) in the following text.

Shadow Quality Problem

Ok, so we are going to render the scene into a depth texture. And we know one thing about texture mapping: if too many pixels map to the same texel, the rendered image could look blocky. And if we are not careful with the way we render the depth texture, this could happen to us too. First thing to realize is that, we are mapping a scene onto a finite texture. If we want the shadow mapping pass to run fast, we better not optimize the shadow quality by increasing the texture resolution, since the cost just goes up both in memory and performance. Our only option is decreasing the amount of scene that gets mapped to the depth textures, which results in fewer pixels mapped to the same texel.

The only part of the scene that needs to be covered by the shadow map is whatever the camera is viewing. So we can use the view frustum to deduce how to place our light caster camera to optimize for space. For now, let’s assume the light caster is the sun, with all light rays being parallel, so I used a orthogonal projection here. We can easily find the view frustum corners in world space, then fit it tightly with a bounding box in light view space. The near plane needs special care though, because we will need to include all shadow occluders present in the scene, even the ones outside of the view frustum. The bounding box we computed in light view place is going to be the light view frustum used for rendering the scene into depth texture. That way, I made sure all the pixels that gets rendered to the screen is covered by the shadow depth texture, and it’s the highest quality possible (not really). The shadow quality turned out to be terrible even with a 2048x2048 resolution depth texture on a normal scene. So apparently, our “most optimized” method is not enough.

Cascaded Shadow Maps

One important realization is that the player pays almost all their attention to the geometries that are close up to the camera, such as player and enemies, and geometries that are farther away, like mountains, are ignored. Objects that are closer need high shadow quality because that’s what the player will always be looking at, but not so much for distant objects. So it’s reasonable to distribute more texels to cover the closer geometries, and fewer texels to cover the farther geometries, despite that the farther geometries is much bigger in size and volume.

With that in mind, the solution to that problem is cascaded shadow map. It’s a rather simple idea; The view frustum is chopped up into small sub frustums, and each one of them are rendered into a separate depth texture of equal size (which is not really necessary and I will explain why in a minute). Therefore, the closest ¼ of the view frustum is have the same shadow map resolution as the farthest ¼ of the view frustum, which is what we want here.

When I implemented cascaded shadow map, I didn’t make separate depth textures for each sub frustum, instead I simply stored all four depth textures into a texture atlas. It saves the hassle to create new framebuffers and switch textures when rendering different slices of the scene.

The result image from this technique is much nicer and almost acceptable. There’s probably smarter ways to chop the view frustum up, such as giving the closer scene more texels and farther scene less texels. But I stopped digging any further and stayed with the 4 equal sub frusta with the same z length. Since the result is good enough for a first pass.

Cascaded Shadow Map Artifacts

Artifacts are introduced when we are refitting the frustum every frame. When the view camera rotates or translate, the edge of the shadow shimmers. It’s due to the fact the same surface points do not map to the same shadow map texels across frames because the shadow map “wiggles” too much. I eliminated this artifact by just snapping the orthogonal light caster view frustum to be a multiple of texel-size-in-world-unit amount.

Another artifact is “seam”s between each sub frustum. It’s not really seams, but the noticeable quality difference right at the boundary line between two sub frustums can sometimes be an unpleasant artifact. It occurs in Monter, but it’s not really noticeable, so I am letting it pass for now. Again, in order to get to the game prototyping phase as fast as possible, polishing work must be deferred.

Shadow Acne

Another noticeable artifact is shadow acne, a phenomenon where the renderer determines that the surface intersects with itself. The cause for this artifact is too many pixels mapping to the same texel in the depth texture. During the shadow mapping pass, we are going to get some pixels that map to the same texel, since we have a finite amount of texture to work with. So, these pixels sometimes have different depth values, but they are comparing against the same shadow map depth value due to a not 1:1 pixel/texel ratio. It’s clear that some pixels will falsely be shadowed, even though they shouldn’t be. Here's what it looks like:

The first thing I did was turning on bilinear filtering, so that four depth texels are fetched at once and blended, then compare with the pixel. This mitigates the artifact, but doesn’t remove it completely. I then tried adding depth bias to the depth value being tested against; it gives some room between the previously falsely shadowed pixels and the depth value sampled from the texture. It eliminates the self-intersection issue on most surfaces, but not on the surfaces with their normal orthogonal to the light caster direction. A depth bias offset along the normal of the surface fixes the issue. Here's what the same image looks like after the fix:

Other small details

If a user’s computer can’t run the game fast enough, shadow map texture will have to use lower resolution, and the shadow is going to look blocky. To mitigate the jaggy looking edge, I just sample the neighbor depth values and blend them, and the shadow edges are blurred as the result.

Recall that I said that I should fit the near plane of the light caster view frustum to the highest shadow occluder, that could be simplified here in Monter. Since we are doing a top-down view game, there’s some limit to how tall an object can be. So I just set a magic near plane value for the sun light caster, since all the objects are in front of that plane, but it’s subject to change.

Final result

The shadow generated by this technique adds a great deal of realism to the final scene. Here’s a comparison of what it looks like with shadow and without shadow:

#13491 Jeroen van Rijn  —  2 months, 1 week ago
That's a damn fine first blog post there, Chen. Like your project application, I consider it exemplary.

Welcome again to the network.
#13492 Chen  —  2 months, 1 week ago
Thank you!
#13497 Birkal  —  2 months, 1 week ago
Nice blog post on why shadow texture mapping is nasty to implement and raycasting against light casters is a no no for your use case.
#13593 pragmatic_hero  —  1 month, 4 weeks ago [Edited 1 minute later]
If the camera angle is restricted - e.g. its some variation of top-down view (something like an isometric view with a perspective projection) - like in the screenshots above. Isn't single fixed resolution shadow-map "good enough"?
#13604 Chen  —  1 month, 3 weeks ago
If the camera angle is restricted - e.g. its some variation of top-down view (something like an isometric view with a perspective projection) - like in the screenshots above. Isn't single fixed resolution shadow-map "good enough"?

Good point.

I'm implementing CSM because it still gives a better quality than a single fixed shadow map. Despite being at a fixed angle, it still covers a large scene because my camera's depression angle is small. It's also necessary for cinematic shots where the camera is not top-down anymore.

Last but not least, Monter is probably(???) not going to be top-down anymore. I was playing around with it last night and made that decision. I was shooting for a much simpler game in the beginning than what it has become (technology wise). Since the game has come this far, if I still restrict where the camera is looking at, which is a design decision I made when I still thought the game is going to be simple, wouldn't that be limiting the game's potential?

I'm still not 100% sure if I want to make this change or not , but it's probably going to happen :).
Log in to comment