When we look at texture mapping our GPU nicely interpolates the colors between pixels to not make our textures look very blocky when we come too close.
Now it does do the same when we query our shadow map however we're just interpolating our Z, when we then apply that to our rendering we still end up with a very blocky result:
Obviously in this case it is clear our shadow map simply doesn't contain the resolution we require to get nice looking shadows for our trees and we'll be looking at resolving that at least somewhat in our next post but we'll never get rid of this completely unless we're willing to waste GPU and memory on really large shadow maps.
Instead we'll take a page our of our texture mapping book and smooth our shadows and the algorithm we're going to use is commonly known as Percentage Closer Soft Shadows.
Now note that this technique isn't just for smoothing out shadows to get rid of our blockiness. Another use for it is to soften the shadows more as the distance between the surface and the shadow casting object grows as more ambient light is able to illuminate the surface. We won't be going into that today though.
The algorithm itself requires us to obtain values for surrounding pixels in our shadow map and obtain an average for our shadow. The more of those pixels are in shadow, the darker we render our surface.
To enable doing this we add a table of offsets to our shader:
// Precision ring // 9 9 9 // 9 1 2 // 9 4 4 const vec2 offsets[] = vec2[]( vec2( 0.0000, 0.0000), vec2( 0.0005, 0.0000), vec2( 0.0000, 0.0005), vec2( 0.0005, 0.0005), vec2(-0.0005, 0.0005), vec2(-0.0005, 0.0000), vec2(-0.0005, -0.0005), vec2( 0.0000, -0.0005), vec2(-0.0005, -0.0005) );Note that this table gives us the option to use 1, 2, 4 and 9 samples. We could add more rings if we wish to go further.
Now we replace our sample function with one that applies the PCF algorithm:
float samplePCF(float pZ, vec2 pCoords, int pSamples) { float bias = 0.0000005; // our bias float result = 1.0; // our result float deduct = 0.8 / float(pSamples); // deduct if we're in shadow for (int i = 0; i < pSamples; i++) { float Depth = texture(shadowMap, pCoords + offsets[i]).x; if (pZ - bias > Depth) { result -= deduct; }; }; return result; }And now we call this new function from our shadow factor function:
// check if we're in shadow.. float shadow(vec4 pVs) { float factor; vec3 Proj = pVs.xyz / pVs.w; if ((abs(Proj.x) < 0.99) && (abs(Proj.y) < 0.99) && (abs(Proj.z) < 0.99)) { // bring it into the range of 0.0 to 1.0 instead of -1.0 to 1.0 factor = samplePCF(0.5 * Proj.z + 0.5, vec2(0.5 * Proj.x + 0.5, 0.5 * Proj.y + 0.5), 4); } else { factor = 1.0; }; return factor; }Note that for now I've duplicated this code in each shader and they are all using a sample size of 4 for now.
Here is our result with this sample size:
And with a sample size of 9:
Download the source here
What's next?
Okay, that was a short one. I've left the shadow map projection matrix calculation alone for now, I may come back to that at a later time but I haven't found a really good adjustment yet.The next tutorial we'll have a look at cascaded shadow maps to get some sharper shadows up close.
No comments:
Post a Comment