Fabien Sanglard's non-blog

  




Soft shadows with VSM



February 14th, 2009

The actual stuff


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 Variance shadow mapping.
The explanation are very sparse because the method is wonderfully explained in the original paper by William Donnelly and Andrew Lauritzen and also in the books referenced at the bottom of this page. My goal was to provide something easy to compile, on Windows, MacOS and Linux. That's why the entire source is one .c file (+6 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
StoreDepthVertexShader.c
StoreDepthFragmentShader.c
BlurVertexShader.c
BlurFragmentShader.c
VertexShader.c
FragmentShader.c

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.

Variance Shadow Mapping


The main issue with PCF Shadow Mapping is that values generated in the depth buffer cannot be filtered with hardware (except for GL_LINEAR on nVidia). The workaround was to use PCF and do software filtering in the shader ; Performance hit is massive.
Variance Shadow Mapping address this issue relying on a chebyshev probabilist prediction. Chebyshev probability prediction rely on mean and "variance", that's why this method is called VSM.
The approach is changed: In the way depth informations are stored and in the way shadow/penumbra information is retrieved. The benefict of these changes is that we can then use all the power of hardware GPU filtering: From bilinear to mippaping, anisotropic and even gaussian glur filtering.

So what is changing ?

First we now store two informations: depth and depth*depth into a color FBO, preferably with 32 bits floating precision.

Second, in order to determine if a fragment is in shadow or not, we use chebyshev's formula in order to determine the probability of a fragment to be shadowed. This probability is very handy because it also generate free penumbra.


Code explanation


First the easy part: Storing depth and depth*depth. This is done is a first pass with a specific shader.

Vertex shader

					
	varying vec4 v_position;

	void main()
	{
			gl_Position = ftransform();
			v_position = gl_Position;
	}						
	
	


The fragment shader is not very complicated either. The only fancy part is the way we bias the depth information usig partial derivative. Don't worry if you don't get this part, it's mostly to avoid surface acne (self-shadowing). This shader would be much more complicated if we had no 32 bits precision FBO. With a 16 bits floating point texture, values would have to be packed in the four components.

	varying vec4 v_position;
	
	void main()
	{
		float depth = v_position.z / v_position.w ;
		depth = depth * 0.5 + 0.5;			//Don't forget to move away from unit cube ([-1,1]) to [0,1] coordinate system
	
		float moment1 = depth;
		float moment2 = depth * depth;
	
		// Adjusting moments (this is sort of bias per pixel) using partial derivative
		float dx = dFdx(depth);
		float dy = dFdy(depth);
		moment2 += 0.25*(dx*dx+dy*dy) ;
		
	
		gl_FragColor = vec4( moment1,moment2, 0.0, 0.0 );
	}				
				

I won't talk about the Gaussian Blur, it's just a standard filtering, with a ping-pong FBO.The goal is to blur the shadow.
Now the real beef of VSM is to determine if a fragment is in shadow.

Vertex shader

					
					
	// Used for shadow lookup
	varying vec4 ShadowCoord;
	
	void main()
	{
	     	ShadowCoord= gl_TextureMatrix[7] * gl_Vertex;
			gl_Position = ftransform();
			gl_FrontColor = gl_Color;
	}
					
					


Fragment shader

				
	uniform sampler2D ShadowMap;
	
	varying vec4 ShadowCoord;
	
	vec4 ShadowCoordPostW;
	
	float chebyshevUpperBound( float distance)
	{
		// We retrive the two moments previously stored (depth and depth*depth)
		vec2 moments = texture2D(ShadowMap,ShadowCoordPostW.xy).rg;
		
		// Surface is fully lit. as the current fragment is before the light occluder
		if (distance <= moments.x)
			return 1.0 ;
	
		// The fragment is either in shadow or penumbra. We now use chebyshev's upperBound to check
		// How likely this pixel is to be lit (p_max)
		float variance = moments.y - (moments.x*moments.x);
		variance = max(variance,0.00002);
	
		float d = distance - moments.x;
		float p_max = variance / (variance + d*d);
	
		return p_max;
	}
	
	
	void main()
	{	
		ShadowCoordPostW = ShadowCoord / ShadowCoord.w;
		//ShadowCoordPostW = ShadowCoordPostW * 0.5 + 0.5; This is done via a bias matrix in main.c
	
		float shadow = chebyshevUpperBound(ShadowCoordPostW.z);
	
		gl_FragColor = vec4(shadow ) *gl_Color;
	  
	}


Results


Even with partial derivative adjustment, selfshadowing still occurs if we render frontface during the shadowmap generation pass:

 

Cull GL_FRONT as usual solve this issue:



Tuning


Depending on plenty of factors, you may not get a good looking result:


In this case, try to go with a different max value for sampling:


	variance = max(variance,0.002);


Recommended books


Here are two books I recommend if you want to learn more about Variance Shadow Mapping:


Add a comment



Name Homepage
E-mail
(Will not appear online)
Comment



Comments (5)


#1 - Dan - 04/04/2011 - 11:43
I found you post where you added a VSM shader to OpenGL.

I am using Visual Studio 2008 on a PC to compile - when I compile the code, all is well, but when it runs, all I see is a blank, black screen (no errors in the console). However, when I download and run your compiled executable, I can see the gorgeous shadows just fine on that. Did the exe you compiled have some kind of updates made to the code that weren't released on your site?
#2 - Erwin Coumans - 06/07/2011 - 05:04
@Dan:

try adding #include "math.h" at the top, the cos/sin function return an invalid value, causing the black screen.
#3 - Lukasz Iwanski - 08/11/2011 - 04:45
Very nice tutorial,
well done!
#4 - Jouni - 08/23/2011 - 14:39
Very useful tutorial. Though I'm having some hard time with self-shadows in my implementation. Culling front faces
did not entirely solve the problem. By shifting the depth value when rendering from camera's POV I can get rid
of erroneous self-shadows in front of objects, but then I don't get any self-shadows at all. Also tuning the maximum
variance does not seem to help.
#5 - fruel - 10/24/2012 - 06:58
Hi!
Love your tutorials. I tried to port this tutorial to Java LWJGL but I cant get it to work. The only thing I see is black for a few seconds and then a grey quad rotating through the screen. http://imageshack.us/a/img849/2370/53483910.png

Porting your basic shadow mapping tutorial to Java worked fine.

I am using the current LWJGL 2.8.4 x64 with Java 1.7. GPU: Nvidia GTX 680.

Do you have an idea what I can do to fix this?

PS: The line 263 in your main.c causes an exception in LWJGL because you cannot set the GL_TEXTURE_MAG_FILTER to GL_LINEAR_MIPMAP_LINEAR.

GL11.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_MAG_FILTER, GL11.GL_LINEAR_MIPMAP_LINEAR);

 

@2008