[font=arial]Finally after a week late, here is the post about CSMs (Cascaded Shadow Maps).[/font]
It's obvious that the further objects get from the camera (the more the depth of objects), the less is their noticeable level of the details. Same applies to shadows, meaning the enough details of shadows needed to make the shadows look realistic, differs relatively to the depth of the objects (in view space). So this means we can represent the shadows further from the camera with a less detailed (low resolution) shadow map, while the close shadows may need a much more detailed (high resolution) shadow map. This is most useful for directional shadow casting lights, since the are no limits to the casting area but the view frustum.
CSMs (Cascaded Shadow Maps)
The main idea of CSM is to divide the view frustum to several sub-frustum/cascades, and make shadow maps separately for each frustum. This will allow us to make a large shadow map for objects near the camera (allowing us to represent enough details for shadows near the camera) and reduce the resolution of shadow maps as we get further from the camera (thus saving VRAM by not saving extra details for shadows far from the camera, while increasing the shadow quality). This means several passes, depending on the number of cascades, but this will not affect the performance since we'll cull the objects in the frustum on each pass.
Figure 1-1 A single 4096*4096 shadow map for the light
Figure 1-2 3 cascades with 2046*2046, 1024*1024, and 512*512 resolutions
Figure 1-3 5 cascades with 2046*2046, 1024*1024, 512*512, 256*256, and 128*128 resolutions
Figure 1-4 8cascades with 2046*2046, 1024*1024, 512*512, 256*256, 128*128, 64*64, 32*32, and 16*16 resolutions
So CSM method is consisted of these steps:
1- Divide the view frustum (by the depth) to several cascades
2- Calculate a shadow map frustum (from light's view) for each cascade, then calculate view and projection matrix for each frustum
3- Do a Z-pass (or for VSMs save Z and squared Z) for each shadow map frustum
4- While drawing shadows determine which cascade's shadow map you should read
5- Do the shadow test like always
Dividing View Frustum
In this step we calculate 8 points representing a frustum, for each cascade. These points are calculated in view space and then using inverse of the view matrix, they are transformed to world space. The division is done along the Z axis (in view space) (Figure 2-1).
Figure 2-1 View frustum divided to 5 sub-frustum along the Z axis
So let's say we have projection matrix using the values left, right, top, bottom, near and far. This projection matrix is defining the view frustum. Now we know that this view frustum can be represented by these points:
far_left = left * far/ near
far_right = right * far/ near
far_top = top * far/ near
far_bottom = bottom * far/ near
The eight points (in view space) of the frustum will be:
(left, bottom, near) (right, bottom, near) (left, top, near) (right, top, near)
(far_left, bottom, far) (far_right, bottom, far) (far_left, top, far) (far_right, top, far)
We will divide this frustum to sub-frustums, along the Z axis, so let's say we have a new near and far value for each sub-frustum. We will have:
new_near_left = left * new_near / near
new_near_right = right * new_near / near
new_near_top = top * new_near / near
new_near_bottom = bottom * new_near / near
new_far_left = left * new_far/ near
new_far_right = right * new_far/ near
new_far_top = top * new_ far/ near
new_far_bottom = bottom * new_ far/ near
Now the eight points (in view space) of the frustum will be:
(new_near_left, new_near_bottom, new_near) (new_near_right, new_near_bottom, new_near)
(new_near_left, new_near_top, new_near) (new_near_right, new_near_top, new_near)
(new_far_left, new_far_bottom, new_ far) (new_far_right, new_far_bottom, new_ far)
(new_far_left, new_far_top, new_ far) (new_far_right, new_far_top, new_ far)
So now we can calculate a sub-frustum with given new_near and new_ far values, now, we should find these values for each sub-frustum. This is the actual division step, we are trying divide the (near, far) range to n parts for each sub-frustum. This division can be done in a linear way ensuring equal length for each range but I implemented a quadratic division which I believe is way effective. So let's say we are trying to calculate new_near and new_ far values for i[sup]th[/sup] frustum of the n division we will have:
new_near = (i - 1)* (far - near) / n + near
new_far = i * (far - near) / n + near
new_near = (i - 1)[sup]2[/sup]* (far - near) / n[sup]2[/sup]+ near
new_far = i[sup]2[/sup] * (far - near) / n[sup]2[/sup]+ near
Now we have divided the view frustum to several sub-frustums that each is represented by eight points, but there is still one problem, these sub-frustums are in view space, so to find their actual positions in world space we simply multiply their points by the inverse of view matrix. This will transform our points to world space.
Calculating the shadow map frustums (for point or spot lights) and orthogonal projection for directional light's shadow maps
Well the CSM method is mostly used for directional lights but it's still possible to use them for point or spot lights (which will not be that useful unless the light is guaranteed to be outside the view frustum, but still your choice, if you find it useful use it).
Since I believe CSMs are most useful (and easier to implement properly) for directional lights I'm going to start with them. Now let's say we have a view matrix for our light view, now what we do is to transform our sub-frustums (from the previous part) to the light view space (by multiplying their points by light's view matrix), and then we calculate AABBs (axis aligned bounding box) for these sub-frustums (finding the minimum and maximum X and Y of the eight points of each sub-frustum). So let's say for each sub-frustum we have Min_X , Max_X, Min_Y and Max_Y, now we can simply use these values as left, right, bottom and top values to calculate our orthogonal projection matrix (this is not the most accurate way to calculate the projection matrix but it's the easiest). We calculate one projection matrix per sub-frustum and render each sub-frustum to its own cascade/shadow map using its projection matrix (notice that there is no need to recalculate the view matrix).
Now dealing with directional lights was a little easy, but point and spot lights are quite different. The idea is that we have frustum for the shadow map (before doing the CSM) now what we want to do is to select several rectangles on the shadow map, and calculate a new frustum for each of those rectangles (Figure 3-1 and Figure 3-2 ).
Figure 3-1 The shadow map before doing CSM, with the view frustum divided to 5 sub-frustums
Figure 3-2 The shadow map before doing CSM, with the view frustum divided to 5 sub-frustums and the used rectangles for the sub-frustums, each of these rectangles will later be rendered separately to separate shadow maps
So we can say we have shadow map projection and view matrices (this is before doing the CSM). Now what we do is project each sub-frustum using these view and projection matrices. This will show us which part of the shadow map is actually used and then we'll only render these parts. So after the projection we'll find the axis aligned bounding rectangle for each sub-frustum, this is done easily by simply finding the maximum and minimum X and Y for the eight points of a sub-frustum and simply clamping them by the range of (-1,1), let's call these values Min_X , Max_X, Min_Y and Max_Y. Now what we do is to calculate these points for each sub-frustum:
(Min_X, Min_Y, 0) (Max_X, Min_Y, 0) (Min_X, Max_Y, 0) (Max_X, Max_Y, 0)
And then we multiply them by the inverse of the projection matrix. This will give us:
(left, bottom, near) (right, bottom, near) (left, top, near) (right, top, near)
Now using these values we can calculate a new projection matrix for each sub-frustum and using these matrices we render each sub-frustum to a separate shadow map (note that still there is no need to recalculate the view matrix).
Writing the Shadow Maps
Simply do a Z-pass using the projection matrices from the previous section. I myself rendered 8 cascades to shadow maps of 2046*2046, 1024*1024, 512*512, 256*256, 128*128, 64*64, 32*32, and 16*16 resolution. Still you can use VSMs with no changes.
Determining which shadow map to use
Now when you are trying to determine if a fragment is shadowed or not you should first find which shadow map you should use. We find this using the Z value of the fragment in view space and the far and near value of projection.
For linear division we have:
i = floor ((Z - near)/ (fa r- near) * n) + 1
For quadratic division we have:
i = floor (sqrt ((Z - near)/ (fa r- near)) * n) + 1
n is the number of cascades
i is the cascade that you should read for the fragment so if i is 1 the fragment is in the closest sub-frustum and you should read the first cascade
Final Step Z test
I guess I have nothing to say here, it's a normal shadow test, and there you have the CSM implemented.
As always I'm open to every kind of suggestions, problem, and all other stuff. Feel free to comment, and thanks for reading :D