February 14th, 2009

ShadowMapping with GLSL

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.

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 management is performed via GLUT. I did not use GLEW to ensure Microsoft Windows portability, again this is for ease of compilation.

EDIT June, 2013: Some of the material in this article were published in Computer Graphics: Principles and Practice (3rd Edition) :) !

Source

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.

Note 2: Kris de Greve ported the code to C# mono. Source code here


Code explanation

This is the rawest form of shadow mapping:

FBO creation

The shadowmap is rendered offscreen via an openGL Framebuffer Object (FBO). Only depth value are saved during rendition, there is no color texture bound to the FBO and color writes are disabled via glColorMask(GL_FALSE, GL_FALSE, GL_FALSE, GL_FALSE).


				
				
	// Hold id of the framebuffer for light POV rendering
	GLuint fboId;

	// Z values will be rendered to this texture when using fboId framebuffer
	GLuint depthTextureId;
					
	void generateShadowFBO()
	{
	  int shadowMapWidth = RENDER_WIDTH * SHADOW_MAP_RATIO;
	  int shadowMapHeight = RENDER_HEIGHT * SHADOW_MAP_RATIO;
	
	  GLenum FBOstatus;

	  // Try to use a texture depth component
	  glGenTextures(1, &depthTextureId);
	  glBindTexture(GL_TEXTURE_2D, depthTextureId);

	  // GL_LINEAR does not make sense for depth texture. However, next tutorial shows usage of GL_LINEAR and PCF
	  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
	  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);

	  // Remove artifact on the edges of the shadowmap
	  glTexParameterf( GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP );
	  glTexParameterf( GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP );

	  // No need to force GL_DEPTH_COMPONENT24, drivers usually give you the max precision if available
	  glTexImage2D( GL_TEXTURE_2D, 0, GL_DEPTH_COMPONENT, shadowMapWidth, shadowMapHeight, 0, GL_DEPTH_COMPONENT, GL_UNSIGNED_BYTE, 0);
	  glBindTexture(GL_TEXTURE_2D, 0);

	  // create a framebuffer object
	  glGenFramebuffersEXT(1, &fboId);
	  glBindFramebufferEXT(GL_FRAMEBUFFER_EXT, fboId);

	  // Instruct openGL that we won't bind a color texture with the currently bound FBO
	  glDrawBuffer(GL_NONE);
	  glReadBuffer(GL_NONE);
 
	  // attach the texture to FBO depth attachment point
	  glFramebufferTexture2DEXT(GL_FRAMEBUFFER_EXT, GL_DEPTH_ATTACHMENT_EXT,GL_TEXTURE_2D, depthTextureId, 0);

	  // check FBO status
	  FBOstatus = glCheckFramebufferStatusEXT(GL_FRAMEBUFFER_EXT);
	  if(FBOstatus != GL_FRAMEBUFFER_COMPLETE_EXT)
		  printf("GL_FRAMEBUFFER_COMPLETE_EXT failed, CANNOT use FBO\n");

	  // switch back to window-system-provided framebuffer
	  glBindFramebufferEXT(GL_FRAMEBUFFER_EXT, 0);
	}				
	
	
				

Light POV transformation

I've read a lot of tutorial where people concatenate the "reverse camera transform". I don't see the utility of this and I prefer to load directly the bias*projection*modelview matrix in the GL_TEXTURE7 matrix.

				
				
	void setTextureMatrix(void)
	{
		static double modelView[16];
		static double projection[16];
		
		// Moving from unit cube [-1,1] to [0,1]  
		const GLdouble bias[16] = {	
			0.5, 0.0, 0.0, 0.0, 
			0.0, 0.5, 0.0, 0.0,
			0.0, 0.0, 0.5, 0.0,
		0.5, 0.5, 0.5, 1.0};
		
		// Grab modelview and transformation matrices
		glGetDoublev(GL_MODELVIEW_MATRIX, modelView);
		glGetDoublev(GL_PROJECTION_MATRIX, projection);
		
		
		glMatrixMode(GL_TEXTURE);
		glActiveTextureARB(GL_TEXTURE7);
		
		glLoadIdentity();	
		glLoadMatrixd(bias);
		
		// concatating all matrices into one.
		glMultMatrixd (projection);
		glMultMatrixd (modelView);
		
		// Go back to normal matrix mode
		glMatrixMode(GL_MODELVIEW);
	}			
	
				

Vertex Shader

No rocket science here, we transform the vertex with the camera matrices, the same vertex with the light POV matrix and we get the fragment color




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



Fragment Shader


The shadow variable hold the shadowed test result. As you see, the goal is to compare the z value (in light POV) of the vertex rendered, with what was rendered to the shadowmap.

				
				
	uniform sampler2D ShadowMap;

	varying vec4 ShadowCoord;

	void main()
	{	
		vec4 shadowCoordinateWdivide = ShadowCoord / ShadowCoord.w ;
		
		// Used to lower moiré pattern and self-shadowing
		shadowCoordinateWdivide.z += 0.0005;
		
		
		float distanceFromLight = texture2D(ShadowMap,shadowCoordinateWdivide.st).z;
		
		
	 	float shadow = 1.0;
	 	if (ShadowCoord.w > 0.0)
	 		shadow = distanceFromLight < shadowCoordinateWdivide.z ? 0.5 : 1.0 ;
	  	
		
		gl_FragColor =	 shadow * gl_Color;
	  
	}
				
				

Avoid self-shadowing and Moiré pattern

Self-shadowing occurs because of the depth buffer limited precision. This is also know as Z-fighting. This only affect polygons facing the light because it's what was rendered to the shadowmap.




Even if you use the maximum precision available (GL_DEPTH_COMPONENT24) this is not an issue that can be solved efficiently with raw power: No level of precision can totally get ride of self-shadowing.

A good technique to reduce it is to cull front facing polygons during the shadowmap rendition step, using glCullFace(GL_FRONT)
and switch back to glCullFace(GL_BACK) during the second step. Here is the result:




As you can see this technique only move the self-shadowing issue to the polygon not facing the light, but it's much less noticeable
Adding a small bias during sampling shadowCoordinateWdivide.z += 0.0005 remove the selfshadowing from the backfaces as well.






The importance of shadowmap resolution


Whatever resolution you use, depending on the position of the light, you will experiment aliasing issue with your shadow.
For this issue, raw processing power can help a little : You can crank up the resolution to which you render the shadowmap.

160x120 shadowmap:



640x480 shadowmap:



1280x960 shadowmap:




Set the texture filtering to GL_LINEAR won't help much. The best way, it to use Percentage Closer Filtering (PCF). This algorithm will provides us with a tiny penumbra, one step toward shadows, this shadow method is covered in my next article.



Avoid artifacts behind and on the sides of the light

When using shadowmapping, you get some projection artifacts when you try to retrieve shadow information out of the light frustrum. On the sides of the frustrum and behind it.


On the side:

As you can see on the upper left, the cube's shadows are projected again, because we fetched shadowmap information beyond the [0,1] limit of a texture.



The way to solve this one is to specify what openGL should sample in such case. This can be done with the following lines:
glTexParameterf( GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP ) and glTexParameterf( GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP ).


Behind the camera:

But there is still one artefact, visible in the upper right:



This is when we try to sample value behind the light's view frustrum. This is solved via the shader line:
if (ShadowCoord.w > 0.0) .





The Windows XP/Vista special

Unfortunatly, Microsoft decided not to support openGL extension beyond v1.1 . As a result, we need to retrieve the location of the functions needed for FBO and GLSL when the program start up. This is definitely an ugly piece of code and I recommend to use GLEW instead, it is done manually here in order to provide a single compilable file.



    #ifdef _WIN32
		
	PFNGLACTIVETEXTUREARBPROC glActiveTextureARB;

	// FrameBuffer (FBO) gen, bin and texturebind
	PFNGLGENFRAMEBUFFERSEXTPROC glGenFramebuffersEXT ;
	PFNGLBINDFRAMEBUFFEREXTPROC glBindFramebufferEXT ;
	PFNGLFRAMEBUFFERTEXTURE2DEXTPROC glFramebufferTexture2DEXT ;
	PFNGLCHECKFRAMEBUFFERSTATUSEXTPROC glCheckFramebufferStatusEXT ;
	

	void getOpenGLFunctionPointers(void)
	{
	  glActiveTextureARB = (PFNGLACTIVETEXTUREARBPROC)wglGetProcAddress("glActiveTextureARB");
	  glGenFramebuffersEXT		= (PFNGLGENFRAMEBUFFERSEXTPROC)		wglGetProcAddress("glGenFramebuffersEXT");
	  glBindFramebufferEXT		= (PFNGLBINDFRAMEBUFFEREXTPROC)		wglGetProcAddress("glBindFramebufferEXT");
	  glFramebufferTexture2DEXT	= (PFNGLFRAMEBUFFERTEXTURE2DEXTPROC)wglGetProcAddress("glFramebufferTexture2DEXT");
	  glCheckFramebufferStatusEXT	= (PFNGLCHECKFRAMEBUFFERSTATUSEXTPROC)wglGetProcAddress("glCheckFramebufferStatusEXT");
	}

    #endif

				

Recommended books

Here are two books I recommend if you want to learn more about shadowMapping:

 

@