Fabien Sanglard's non-blog

  




ShadowMapping with GLSL



And hence, I had to learn matrices.

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 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.

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.

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:


Add a comment



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



Comments (29)


#1 - Rick Pingry - 03/03/2011 - 21:53
Thank you so much for the great article. It was direct, concise, and easy to follow, even for a noob like me. :)
#2 - Guo Chow - 03/11/2011 - 21:56
Awesome article, but I notice that here the scale and bias matrix has been included in your light pov transformation BEFORE perspective divide. The Projection matrix transforms from eye coords to clip coords (CC). Divide CC xyz by CC w (perspective divide) to get normalized device coords (NDC). Scale and bias NDC xy by the viewport and NDC z by the depth range to get window coords. Shouldn't scale and bias matrix can be multiplied only AFTER perspective divide?
Thanks in advance.
Guo
#3 - opengeler - 04/04/2011 - 00:50
@Guo Cho

Whether you do it BEFORE or AFTER, it is the same:

Just to clarify:

1. Applying bias transform before perspective divide (i.e. in CLIP SPACE):

(x,y,z,w) * bias = (0.5*x + 0.5*w, ..., ..., w)
Then perspective divide = ( 0.5*x/w + 0.5, ..., ... )

2. Applying bias transform "after" perspective divide (i.e. in NDC SPACE):

(x,y,z,w) -> Perspective divide = (x/w, y/w, z/w)
Then apply bias transform = (0.5*x/w + 0.5, ..., ... )
#4 - Guo Chow - 04/07/2011 - 14:00
Got it. With your generous help I've successfully implemented ShadowMapping using OpenSceneGraph. Thanks again.
#5 - sage - 05/03/2011 - 21:33
In your draw objects function, if you draw the following:
startTranslate(0,8,-16);
glutSolidCube(4);
endTranslate();
startTranslate(0,4,-16);
glutSolidCube(4);
endTranslate();
startTranslate(4,4,-16);
glutSolidCube(4);
endTranslate();
startTranslate(4,8,-16);
glutSolidCube(4);
endTranslate();

Which is a 4x4 cube, you get massive shadowing artifacts while the light is spinning. The shadowing does not work for some reason when you have objects that close together. If there is a reason for this I would like to know, as I'm running into the same problem no matter what I do to try and correct it. Thank you for the concise tutorial though.
#6 - JMan - 05/10/2011 - 10:51
Hey,
thank you for the tutorial very cool one, I am wondering in case of using shadow map with shader and you are drawing gemetric which already have shader let's say bump would you have to draw the scene 3 times one to capture the depth another to draw the shadow map and a third to draw the geometric with their bump shader too or the is another whay of doing it?

thank you.
#7 - Fabien Sanglard - 05/10/2011 - 11:04
@JMan If you want shadowmapping and bumpmapping in your scene you DO NOT draw the scene twice with two shaders, you combine both effects in the same shaders.
#8 - Lers - 05/16/2011 - 12:05
Nice tutorials. I downloaded shadowing binaries, and there's no shadow at all in this and with PCF (using ati 4870, w7).
Fortunately shadows with VSM and in bumpmapping demo works.
#9 - Lers - 05/16/2011 - 13:17
Ah, got these to work.
Added .xy to texture2D(ShadowMap,shadowCoordinateWdivide).z -> texture2D(ShadowMap,shadowCoordinateWdivide.xy).z

And shadows with PCF, I changed shadow2DProj(...).w; to shadow2DProj(...).x; (and y & z works too) to make shadows visible.
#10 - Brad Hubbard - 09/26/2011 - 22:22
Well I decided to delve into the shadowy world (pun intended) of openGL after stumbling upon this excellent non-blog. I decided a good way to get started, and to learn something, was to download the samples available and have a play. I'm currently running Fedora 15 and unfortunately nothing seems to work "out of the box". I decided this was an even better learning opportunity so I am going to have a crack at getting these working on my system (and hopefully help other Linux openGL noobs in the process). I've decided to document this on my new blog here http://www.weirdcomputerscience.com so feel free to take a peak. I'm sure it will pale in comparison to Fabien's efforts here but it will have some Linux specific information and we'll see where it leads. That's the beauty of technology and the internet, you never know where it might lead!

As I said I will go into more detail on my blog but will post a comment for each of the attempted fixes in the comments of each entry (just like this). Please feel free to make additions or corrections in either location (with Fabien's blessing of course).

Anyway, on to ShadowMapping with GLSL.

I was initially using the Mesa driver with my ATI Technologies Inc Manhattan [Mobility Radeon HD 5400 Series] and was able to get the sample compiled and working using the following patch;

diff -uwr a/src/main.c c/src/main.c
--- a/src/main.c 2009-02-08 10:40:18.000000000 +1000
+++ c/src/main.c 2011-09-27 11:39:07.168034670 +1000
@@ -2,7 +2,13 @@
#include "windows.h"
#endif

+#ifdef __linux__
+#include "GL/glew.h"
+#include "GL/glut.h"
+#include "GL/gl.h"
+#else
#include "GLUT/glut.h"
+#endif

#ifdef _WIN32
#include "glext.h"
@@ -10,6 +16,8 @@


#include
+#include
+#include


#ifdef _WIN32



Copy the above content to a file patch1 in the directory where you have unpacked shadowmapping.zip and run the following command.

$ patch -p1 < patch1

You should then be able to cd to the src subdirectory and compile with this line.

$ gcc -g -o ShadowMapping_with_GLSL main.c -lGL -lm -lGLU -lglut -lGLEW

Of course you'll need freeglut-devel and glew-devel installed as well as a libGL and libGLU (at this point provided by mesa-libGL-devel and mesa-libGLU respectively).

That got me going but due to problems with other openGL examples I decided to switch to the Catalyst (fglrx) driver so....

I ended up with this patch.

diff -uwr a/src/main.c b/src/main.c
--- a/src/main.c 2009-02-08 10:40:18.000000000 +1000
+++ b/src/main.c 2011-09-27 11:02:07.601099602 +1000
@@ -2,7 +2,13 @@
#include "windows.h"
#endif

+#ifdef __linux__
+#include "GL/glx.h"
+#include "GL/glut.h"
+#include "GL/gl.h"
+#else
#include "GLUT/glut.h"
+#endif

#ifdef _WIN32
#include "glext.h"
@@ -10,6 +16,8 @@


#include
+#include
+#include


#ifdef _WIN32


and the following compiler line.

$ gcc -g -o ShadowMapping_with_GLSL main.c -I/usr/include/fglrx/ATI -lGL -lm -lGLU -lglut

Well, this ended up being way too long. I'll provide more details in my post and will post comments as I work on the others (if it's OK with you Fabien?)

Cheers,
Brad
#11 - Anon - 10/06/2011 - 12:05
Thank you very much for the straight to the point and bare bones tutorial on the topic. It helped me greatly with understanding the core of the technique; much more than any other resource I found.
#12 - John - 10/16/2011 - 12:05
Great tutorial! I always thought this was some obscure magic I'd better stay away from, but I'm finally beginning to understand it thanks to you.

One question: how can I add textures to this setup? I've tried redrawing the scene without any shaders and using glSetActiveTextureARB(GL_TEXTURE0), before and after the shadow pass, but no matter what I do I can't get the texture to show up. I've confirmed it's properly loaded.

I just can't figure out how to combine the shadow mapping with regular texturing!
#13 - John - 10/16/2011 - 21:26
I've got it to work by passing a second texture to the shader. Similar to what said about bump mapping in that other comment. Now that I've been forced to tamper with GLSL a bit I understand it even better :)
#14 - Matti - 11/16/2011 - 16:15
Hey,

Thanks for these, very informative. However I'd be interested to see this done with modern OpenGL (ES 2.0) without any fixed pipeline / matrix stack and using that shadow2DProj(), any updates? ;)
#15 - Fabien Sangladr - 11/16/2011 - 16:18
@Matti

1/ It is very easy to port this tutorial to OpenGL ES 2.0, just create a matrix uniform to replace the texture Matrix.
2/ As for the shadow2DProj usage, look at the PCF tutorial, it is used there.
#16 - Matti - 11/19/2011 - 08:48
Yeah I got shadow maps running using this method that packs depth values into a RGBA texture. The method though seems really slow (60fps -> 18fps on my N9) - havent really debugged yet to see why exactly, I know its not the shadow map creation but something else - so I'm looking forward to implementing the proper depth texture version. What puzzles me a bit is that according to the specs, OpenGL ES 2.0 does not support GL_DEPTH_COMPONENT textures, but such code does compile at least with the Harmattan/Qt framework :o

Great job btw with all this stuff, very informative for the rest of us.
#17 - Matti - 11/23/2011 - 15:04
Do you have a nice solution for handling things that end up outside the shadow map? Currently everything that doesnt get projected properly onto the shadow map gets a full shadow..
#18 - Fabien Sanglard - 11/23/2011 - 15:06
@Matti: The solution is Cubic Shadow Mapping.
#19 - Matti - 11/26/2011 - 04:31
As for the depth textures / shadow2DProj() with OpenGL ES 2.0 .. stuff like GL_TEXTURE_COMPARE_MODE, GL_TEXTURE_COMPARE_FUNC, GL_DEPTH_TEXTURE_MODE don't exist - how do you go around that?
#20 - Fabien Sanglard - 11/26/2011 - 13:13
@Matti

For OpenGL ES 2.0: Don't use the shadow projection function and perform the comparaison manually.
#21 - Matti - 11/27/2011 - 08:11
I'm quite surprised how much of an effect using the shadowing causes on performance. Rendering the shadow map only causes about 3FPS loss, but rendering the scene objects with the shadowing shader causes a massive 20+ FPS hit! This is on the Nokia N9 which has a PowerVR SGX530 graphics hardware, quite similar to what iPhone 4 has. Without shadows it runs about 50-60 FPS and with shadows about 25FPS. The shader I'm using handles shadows pretty much similarly than your dEngine.

Also I cant figure out what causes this annoying glitch in the shadows when the light source is almost on top of the objects (the shadow cast by the 2 blocks on the right): http://777-team.org/~matti/tmp/shadowbug.jpg

- Matti
#22 - Fabien Sanglard - 11/27/2011 - 16:42
@Matti:

The issue you are describing is called "Peter Panning", it is due to a depth offset set to high when rendering the shadow map
or two low when comparing the Z value in the shader.

As for the performance hit: It is even worse if you go with PCF !!

Fabien
#23 - Matti - 11/28/2011 - 08:16
I wasn't using a Z offset since I was culling front faces and handling lighting the backsides using the normal*light_direction so there was no z fighting issues. For some reason I dont fully understand, the shadows were generated badly. I changed this to cull backfaces and added a small Z offset to remove z-fighting, this fixed it.

As for the performance, I don't get how come rendering the shadow map is relatively cheap but using it really expensive :o This is with a 512x512 map.
#24 - Q - 12/01/2011 - 03:25
thank you

i'm doing shadow map

it help me a lot
#25 - Matti - 12/06/2011 - 07:54
Even though the platform supports GL_OES_depth_texture, I am unable to pass GL_DEPTH_COMPONENT to glTexImage2d() when trying to create a depth texture, as this fails to GL_INVALID_ENUM.
#26 - Damian - 01/26/2012 - 05:00
Nice tutorials. Thanks for them.
#27 - zhodj - 03/12/2012 - 03:00
Thanks for your good code!I am tring to use "GLint loc=glGetAttribLocationARB(shadowShaderId,"time");" to query the attribute "time" defined by me in vertex shader,but it makes mistakes! Could you please tell me why?
#28 - Alkis - 06/06/2012 - 11:15
One small sidenote though, it seems that on AMD cards you need a colorbuffer along with your depth component in the FBO.
#29 - Robert M - 10/08/2012 - 16:54
Hi,

The binaries run fine on my nVidia GeForce 650M, but the ones compiled from the sources, display no shadows. Any thought?

 

@2008