Tuesday 10 July 2018

Implementing Cascaded Shadow Maps in "EveryRay - Rendering Engine"


For the last couple of weeks I've been working on a small rendering engine - "EveryRay - Rendering Engine". I have decided to start building it because I thought it could be useful both for practicing graphics programming and C++ skills on a more "serious" level. The engine currently supports DirectX 11 and has more or less all essential parts. I should say that I have used the book "Real-Time 3D Rendering with DirectX and HLSL: A Practical Guide to Graphics Programming" by Paul Varcholik as a reference for all fundamental aspects of making a rendering engine/framework. However, my main idea was to be able to implement various techniques and algorithms from different publications. (In a way, I do not want to build an advanced engine, but instead to have a framework where I can test things and try to put them into practice).

Thankfully, in the book, there is also an introductory chapter about shadow mapping. After getting acquainted with the structure that was suggested by the author, I thought about cascaded shadow mapping implementation. Yes, the technique is not new, however, it is still very popular among developers. It has its own advantages and disadvantages, but in the end, I was satisfied with the results. My implementation is not perfect (I will explain why and where), as I was only focused on the key logic of the technique. Down below I am going to explain my steps in details! 

Firstly, I assume that you know how basic shadow mapping works. I've actually written a short post about shadows before, so you can check it out or google:) 
In general, we can have directional and point lights. And creating shadow maps for them requires 2 different types of projections: orthographic and perspective, respectively. So make sure to prepare your frustums for orthographic projection if you want to use cascaded shadow maps. 

Ok, but why do I need cascaded shadow maps at all and why do I need an orthographic projection for them? Well, if you worked with shadow maps before you know that the biggest problem with them is aliasing. When we project our shadow from an object, we may notice jaggies around the edges of the shadow. And it is logical - we are trying to project depth information to pixels! Even if we increase the resolution of our shadow map, we will still see perspective/orthographic aliasing. Moreover, the farther we move our light source, the more pixelated our shadow will be! And imagine if we want to create a shadow map from an "infinite" source, like directional light?... Look at the comparison below (left one is standard shadow map with 2K texture and the right one is csm also with 2K texture): 


As for orthographic projection - if you have not guessed, it is common to have it for directional lights. That's why cascaded shadow mapping uses that type of projection. 

So people came up with an idea: what if we render several shadow maps according to our player's camera position. We want the most accurate shadows near the player and we do not care about the quality of long-distance shadows. Let's create "cascades"!

3 camera frustums

We can bind our projector (I will call them projection boxes) to our camera frustum. In addition, we are going to create several connected frustums (as in the image above) and bind several projectors to them! So the first and the smallest one will be the most accurate! Of course, we can change distances between frustum planes (so-called "clipping planes") and thus render shadows for our specific needs. Sounds easy! We are just doing the same thing as with standard shadow mapping, but several times. 

And what's the catch? Well, the most difficult part of this algorithm, in my opinion, is to bind your projection boxes to frustums. They should be able to rotate with the light source, but still be tighten to the frustums' geometry (think about them as AABB volumes). Also, they should be able to move with your camera, however, they should not rotate with it. Sounds a bit complicated, but a couple of proper matrix multiplications and a bit of math will solve the problem. I will try to demonstrate the behavior from the top-down view below:



So as you can see, our projection boxes are always rotated in the direction of the directional light. Even when our camera (so its frustum) rotates, boxes do not! However, boxes are still bound to their own frustum cascades. If you look at the sides of each projection box, you will see that they always lie on the frustum corners. That is why every update we should recalculate the positions and properties, like width, length, and height, of our projection boxes. Here is the code that I have used:

I should also mention that there are actually two known ways of binding projection boxes: fitting to the scene and fitting to the cascade. You've seen the second one above. Below is the fitting to the scene method that I used in my demo. It is a bit easier to configure, but there is a drawback as well - overdraw factor is higher in comparison with another method, as we are wasting more resources. However, I was satisfied with the results anyway:)


Once you get the projection matrices, you can pass them into the shader. Also, do not forget to pass you clipping planes' distances. Without them, the shader would not know when to switch to another cascade! You can simply do it like that in the pixel shader:


Nothing special there: we are just comparing clipping distances to the pixel depth, then sampling a proper shadow map and, finally, changing the diffuse value (pixel color) for our simple lighting. As you can see, I am using PCF filtering for smoothing shadow edges, but you can use more complex algorithms. I won't explain them here, because this post is not about shadow filtering:) Finally, you can color your cascades, as it really helps when you are trying to debug your shadows.


And that is it, I guess! Now you have an understanding of basic csm algorithm. But is it perfect? No, there is always a place for improvements. CSM itself was not created yesterday, so there are many things that can be added to it.

For instance, we may store our depth maps in a texture array if we are building for modern machines (I suppose we are not interested in DirectX 9 era devices anymore). In addition, I saw some implementations where calculations were done in the shader itself (calculation of projection boundaries and etc). I believe it can improve the performance if you are worried about your CPU time. Another problem that is not fixed in my setup, but actually not very difficult to solve, is shimmering. When we move and render our depth maps every frame, we may notice some stuttering of our shadows. It makes sense because we are changing depth values all the time. That is why you should interpolate you shadow texels somehow. Also, you might want to smooth the cascade edges between shadows. Sometimes abrupt changes in shadow 'qualities' are very noticeable to the player. And, finally, you can use proper filtering techniques. PCF is quite old already, so there are several modern solutions that you should consider if you want to filter your shadows:)

Links:
1) https://docs.microsoft.com/en-us/windows/desktop/dxtecharts/cascaded-shadow-maps
2) https://developer.download.nvidia.com/SDK/10.5/opengl/src/cascaded_shadow_maps/doc/cascaded_shadow_maps.pdf
3) https://mynameismjp.wordpress.com/2013/09/10/shadow-maps/
4) https://mynameismjp.wordpress.com/2015/02/18/shadow-sample-update/
5) http://the-witness.net/news/2013/09/shadow-mapping-summary-part-1/

Other useful resources:
1) "Cascaded Shadow Maps" by Wolfgang F.Engel, from ShaderX5, Advanced Rendering Techniques

No comments:

Post a Comment