Shadows give important cues about the relative positioning of graphical objects. There are myriads of shadow generation techniques, including shadow volumes, shadow maps, cascaded shadow maps, and so on. An excellent reference on several shadow generation techniques is given in the See also section. We will now see how to carry out basic shadow mapping using FBO.
For this recipe, we will use the previous scene but instead of a grid object, we will use a plane object so that the generated shadows can be seen. The code for this recipe is contained in the Chapter4/ShadowMapping
directory.
Let us start with this recipe by following these simple steps:
GL_CLAMP_TO_BORDER
, set the border color to {1,0,0,0}
, give the texture comparison mode to GL_COMPARE_REF_TO_TEXTURE
, and set the compare function to GL_LEQUAL
. Set the texture internal format to GL_DEPTH_COMPONENT24
.glGenTextures(1, &shadowMapTexID); glActiveTexture(GL_TEXTURE0); glBindTexture(GL_TEXTURE_2D, shadowMapTexID); GLfloat border[4]={1,0,0,0}; glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MAG_FILTER,GL_NEAREST); glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MIN_FILTER,GL_NEAREST); glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_WRAP_S,GL_CLAMP_TO_BORDER); glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_WRAP_T,GL_CLAMP_TO_BORDER); glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_COMPARE_MODE,GL_COMPARE_REF_TO_TEXTURE); glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_COMPARE_FUNC,GL_LEQUAL); glTexParameterfv(GL_TEXTURE_2D,GL_TEXTURE_BORDER_COLOR,border); glTexImage2D(GL_TEXTURE_2D,0,GL_DEPTH_COMPONENT24,SHADOWMAP_WIDTH,SHADOWMAP_HEIGHT,0,GL_DEPTH_COMPONENT,GL_UNSIGNED_BYTE,NULL);
glGenFramebuffers(1,&fboID);
glBindFramebuffer(GL_FRAMEBUFFER,fboID);
glFramebufferTexture2D(GL_FRAMEBUFFER,GL_DEPTH_ATTACHMENT,GL_TEXTURE_2D,shadowMapTexID,0);
GLenum status = glCheckFramebufferStatus(GL_FRAMEBUFFER);
if(status == GL_FRAMEBUFFER_COMPLETE) {
cout<<"FBO setup successful."<<endl;
} else {
cout<<"Problem in FBO setup."<<endl;
}
glBindFramebuffer(GL_FRAMEBUFFER,0);
S
) by combining the light modelview matrix (MV_L
), projection matrix (P_L
), and bias matrix (B
). For reducing runtime calculation, we store the combined projection and bias matrix (BP
) at initialization.MV_L = glm::lookAt(lightPosOS,glm::vec3(0,0,0), glm::vec3(0,1,0)); P_L = glm::perspective(50.0f,1.0f,1.0f, 25.0f); B = glm::scale(glm::translate(glm::mat4(1), glm::vec3(0.5,0.5,0.5)),glm::vec3(0.5,0.5,0.5)); BP = B*P_L; S = BP*MV_L;
glEnable(GL_CULL_FACE)
and glCullFace(GL_FRONT)
) so that the back-face depth values are rendered. Otherwise our objects will suffer from shadow acne.Normally, a simple shader could be used for rendering of a scene in the depth texture. This may also be achieved by disabling writing to the color buffer (glDrawBuffer(GL_NONE)
) and then enabling it for normal rendering. In addition, an offset bias can also be added in the shader code to reduce shadow acne.
glBindFramebuffer(GL_FRAMEBUFFER,fboID); glClear(GL_DEPTH_BUFFER_BIT); glViewport(0,0,SHADOWMAP_WIDTH, SHADOWMAP_HEIGHT); glCullFace(GL_FRONT); DrawScene(MV_L, P_L); glCullFace(GL_BACK);
glBindFramebuffer(GL_FRAMEBUFFER,0); glViewport(0,0,WIDTH, HEIGHT); DrawScene(MV, P, 0 );
M*vec4(vVertex,1)
) with the shadow matrix (S
) to obtain the shadow coordinates. These will be used for lookup of the depth values from the shadowmap
texture in the fragment shader.#version 330 core
layout(location=0) in vec3 vVertex;
layout(location=1) in vec3 vNormal;
uniform mat4 MVP; //modelview projection matrix
uniform mat4 MV; //modelview matrix
uniform mat4 M; //model matrix
uniform mat3 N; //normal matrix
uniform mat4 S; //shadow matrix
smooth out vec3 vEyeSpaceNormal;
smooth out vec3 vEyeSpacePosition;
smooth out vec4 vShadowCoords;
void main()
{
vEyeSpacePosition = (MV*vec4(vVertex,1)).xyz;
vEyeSpaceNormal = N*vNormal;
vShadowCoords = S*(M*vec4(vVertex,1));
gl_Position = MVP*vec4(vVertex,1);
}
sampler2Dshadow
type. This sampler can be used with the textureProj
function to return a comparison outcome. We then use the comparison result to darken the diffuse component, simulating shadows.#version 330 core layout(location=0) out vec4 vFragColor; uniform sampler2DShadow shadowMap; uniform vec3 light_position; //light position in eye space uniform vec3 diffuse_color; smooth in vec3 vEyeSpaceNormal; smooth in vec3 vEyeSpacePosition; smooth in vec4 vShadowCoords; const float k0 = 1.0; //constant attenuation const float k1 = 0.0; //linear attenuation const float k2 = 0.0; //quadratic attenuation uniform bool bIsLightPass; //no shadows in light pass void main() { if(bIsLightPass) return; vec3 L = (light_position.xyz-vEyeSpacePosition); float d = length(L); L = normalize(L); float attenuationAmount = 1.0/(k0 + (k1*d) + (k2*d*d)); float diffuse = max(0, dot(vEyeSpaceNormal, L)) * attenuationAmount; if(vShadowCoords.w>1) { float shadow = textureProj(shadowMap, vShadowCoords); diffuse = mix(diffuse, diffuse*shadow, 0.5); } vFragColor = diffuse*vec4(diffuse_color, 1); }
The shadow mapping algorithm works in two passes. In the first pass, the scene is rendered from the point of view of light, and the depth buffer is stored into a texture called shadowmap
. We use a single FBO with a depth attachment for this purpose. Apart from the conventional minification/magnification texture filtering, we set the texture wrapping mode to GL_CLAMP_TO_BORDER
, which ensures that the values are clamped to the specified border color. Had we set this as GL_CLAMP
or GL_CLAMP_TO_EDGE
, the border pixels forming the shadow map would produce visible artefacts.
The shadowmap
texture has some additional parameters. The first is the GL_TEXTURE_COMPARE_MODE
parameter, which is set as the GL_COMPARE_REF_TO_TEXTURE
value. This enables the texture to be used for depth comparison in the shader. Next, we specify the GL_TEXTURE_COMPARE_FUNC
parameter, which is set as GL_LEQUAL
. This compares the currently interpolated texture coordinate value (r
) with the depth texture's sample value (D
). It returns 1 if r<=D
, otherwise it returns 0. This means that if the depth of the current sample is less than or equal to the depth from the shadowmap
texture, the sample is not in shadow; otherwise, it is in shadow. The textureProj
GLSL shader function performs this comparison for us and returns 0 or 1 based on whether the point is in shadow or not. These are the texture parameters required for the shadowmap
texture.
To ensure that we do not have any shadow acne, we enable front-face culling (glEnable(GL_CULL_FACE)
and glCullFace(GL_FRONT)
) so that the back-face depth values get written to the shadowmap
texture. In the second pass, the scene is rendered normally from the point of view of the camera and the shadow map is projected on the scene geometry using shaders.
To render the scene from the point of view of light, the modelview matrix of the light (MV_L
), the projection matrix (P_L
), and the bias matrix (B
) are calculated. After multiplying with the projection matrix, the coordinates are in clip space (that is, they range from [-1,-1,-1]). to [1,1,1]. The bias matrix rescales this range to bring the coordinates from [0,0,0] to [1,1,1] range so that the shadow lookup can be carried out.
If we have the object's vertex position in the object space given as Vobj
, the shadow coordinates (UVproj
) for the lookup in the shadow map can be given by multiplying the shadow matrix (S
) with the world space position of the object (M*Vobj
). The whole series of transformations is given as follows:
Here, B is the bias matrix, P L is the projection matrix of light, and MV L is the modelview matrix of light. For efficiency, we precompute the bias matrix of the light and the projection matrix, since they are unchanged for the lifetime of the application. Based on the user input, the light's modelview is modified and then the shadow matrix is recalculated. This is then passed to the shader.
In the vertex shader, the shadowmap
texture coordinates are obtained by multiplying the world space vertex position (M*Vobj
) with the shadow matrix (S
). In the fragment shader, the shadow map is looked up using the projected texture coordinate to find if the current fragment is in shadow. Before the texture lookup, we check the value of the w
coordinate of the projected texture coordinate. We only do our calculations if the w
coordinate is greater than 1. This ensures that we only accept the forward projection and reject the back projection. Try removing this condition to see what we mean.
The shadow map lookup computation is facilitated by the textureProj
GLSL function. The result from the shadow map lookup returns 1 or 0. This result is multiplied with the shading computation. As it happens in the real world, we never have coal black shadows. Therefore, we combine the shadow outcome with the shading computation by using the mix
GLSL function.
The demo application for this recipe shows a plane, a cube, and a sphere. A point light source, which can be rotated using the right mouse button, is placed. The distance of the light source can be altered using the mouse wheel. The output result from the demo is displayed in the following figure:
This recipe detailed the shadow mapping technique for a single light source. With each additional light source, the processing, as well as storage requirements, increase.