Softshadow with GLUT,GLSL and VSM
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.
For further explanation about VSM, checkou the original paper by William Donnelly and Andrew Lauritzen and also 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: