February 14th, 2009

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:


 

@