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:
- Rendering is done offscreen via FBO. There is no fallback using
glCopyTexSubImage2D
as I assumed that any GPU with support for GLSL would also support FBOs. - There is no "hidden" mechanisms in the shader. Function used for lookup is
texture2D
and the W divide is done manually. I will write an other sample usingshadow2DProj
, built-in GLSL functions. - The light matrix is passed via the TEXTURE7 matrix. It's not best practice to re-use variables but it made the code shorter and IMHO less scary for a beginner to read.
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: