Soft shadows with PCF
When I got started learning openGL Shading Language, the "Orange Book" was a great resource, but I thought it lacked code samples. Here is a modest cross platform implementation of shadow mapping with Percentage Closer Filtering..
My goal was to provide something easy to compile, on Windows, MacOS and Linux. That's why the entire source is one .c file (+2 shaders) coded in ANSI C. Crossplatform window managment is performed via GLUT. I did not use GLEW to ensure Microsoft Windows portability, again this is for ease of compilation.
Source
main.c
VertexShader.glsl
FragmentShader.glsl
Zipped pack
Win32 and macOS X Binaries:
Note: Due to a Macos X bug with GLUT (relative path is lost an set to "/" when the app starts), I cannot distribute the binary. Running the sample in XCode is fine.
Percentage closer Filtering
Percentage closer Filtering is a step forward soft shadows, but don't expect a huge increase of quality without a serious drop of performances.
The method is quite simple: Instead of taking one sample in order to determine if a fragment is in the shadow or not, we take "several" (the set of sample is called "Kernel" ) and average them.
The result is a penumbra zone and an attenuation of the aliasing. The bigger the kernel, the bigger the penumbra.
After experimentations, I found PCF to be a massive waste of GPU ressources. As the kernel sampling has to be done for every fragment, the shader end up doing a lot of sampling operations in order to achieve a gain in a small portion of the screen. Moreover, the performance hit is huge if you try to generate penumbra with an 8x8 or bigger kernel, Variance Shadow Mapping is a much better approach.
Code explanation
The vertex shader doesn't change at all:
// Used for shadow lookup varying vec4 ShadowCoord; void main() { ShadowCoord= gl_TextureMatrix[7] * gl_Vertex; gl_Position = ftransform(); gl_FrontColor = gl_Color; }
The fragment shader feature a new lookup function: lookup
. The following code sample features 16 pixels kernel, you will find a lot more kernel types in the actual code. There is no more "W divide", as we now use the "shadow2DProj" sampler.
The GLSL sampler shadow2DProj
is supposed to be better than texture2DProj
on Nvidia hardware, used with a GL_LINEAR
filter, we get a "free" 4 pixel sample. Tests showed a neat improvement (see screenshots).
uniform sampler2DShadow ShadowMap; varying vec4 ShadowCoord; // This define the value to move one pixel left or right uniform float xPixelOffset ; // This define the value to move one pixel up or down uniform float yPixelOffset ; float lookup( vec2 offSet) { // Values are multiplied by ShadowCoord.w because shadow2DProj does a W division for us. return shadow2DProj(ShadowMap, ShadowCoord + vec4(offSet.x * xPixelOffset * ShadowCoord.w, offSet.y * yPixelOffset * ShadowCoord.w, 0.05, 0.0) ).w; } void main() { float shadow ; // Avoid counter shadow if (ShadowCoord.w > 1.0) { float x,y; for (y = -1.5 ; y <=1.5 ; y+=1.0) for (x = -1.5 ; x <=1.5 ; x+=1.0) shadow += lookup(vec2(x,y)); shadow /= 16.0 ; } gl_FragColor = (shadow+0.2) * gl_Color; }
The main program get's a few modifications as well. We now use the shadow2DProj
sampler, we also need to se the GL_TEXTURE_COMPARE_MODE
.
// GL_LINEAR does not make sense for depth texture. However, next tutorial shows usage of GL_LINEAR and PCF. Using GL_NEAREST glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); // This is to allow usage of shadow2DProj function in the shader glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_COMPARE_MODE, GL_COMPARE_R_TO_TEXTURE); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_COMPARE_FUNC, GL_LEQUAL); glTexParameteri(GL_TEXTURE_2D, GL_DEPTH_TEXTURE_MODE, GL_INTENSITY);
Results:
Regular shadowmap
Percentage Closer Filtering 16 pixels kernel:
Percentage Closer Filtering 64 pixels kernel:
Percentage Closer Filtering 4 pixels kernel, dithered:
Recommended books
Here are two books I recommend if you want to learn more about Percentage Closer Filtering: