In the last tutorial, we had set up the texture and framebuffer to do a shadow pass, in this tutorial, we are going to use the depth texture from the shadow pass and use it in the second pass which will draw the scene with lighting. I'm going to call the second pass the lighting pass as we perform the lighting calculations in this pass. I have used two shader programs, the first program has the shadow vertex/fragment shaders and the second program has the lighting vertex/fragment shaders.
In the shadow pass, we created a Model-View-Projection matrix from the lights' position, in the lighting pass we are going to create a Model-View-Projection matrix from the camera's position.
// MVP from camera position
glm::mat4 model(1.0f);
glm::mat4 view = glm::lookAt(m_camera, glm::vec3(0.0f, 0.0f, 0.0f), glm::vec3(0.0f, 1.0f, 0.0f));
glm::mat4 projection = glm::perspective(45.0f, scrennSize.x / scrennSize.y, 0.1f, 100.0f);
glm::mat3 normalMatrix = glm::transpose(glm::inverse(model));
The normalMatrix will be used as a uniform in the fragment shader for normal calculations, we will come to this in a bit. The rest of the lighting pass is very simple on the C++ side as we just have to set up the uniforms, bind shader programs and textures:
// set the viewport and bind resources
glViewport(0, 0, (size_t)scrennSize.x, (size_t)scrennSize.y);
glUseProgram(programLighting);
glActiveTexture(GL_TEXTURE0 + 1);
glBindTexture(GL_TEXTURE_2D, texture_id); ///< just a texture we will use in the scene
glUniform1i(glGetUniformLocation(programLighting, "texture"), 1);
glActiveTexture(GL_TEXTURE0 + 2);
glBindTexture(GL_TEXTURE_2D, shadow_id); ///< the shadow texture
glUniform1i(glGetUniformLocation(programLighting, "shadowMap"), 2);
// set the uniforms for matrices etc.
glUniformMatrix4fv(glGetUniformLocation(programLighting, "depthBiasMVP"), count, false, &depthBiasMVP[0][0]);
glUniformMatrix4fv(glGetUniformLocation(programLighting, "modelMatrix"), count, false, &model[0][0]);
glUniformMatrix4fv(glGetUniformLocation(programLighting, "viewMatrix"), count, false, &view[0][0]);
glUniformMatrix4fv(glGetUniformLocation(programLighting, "projectionlMatrix"), count, false, &projection[0][0]);
glUniformMatrix3fv(glGetUniformLocation(programLighting, "normalMatrix"), count, false, &normalMatrix[0][0]);
glUniform3f(glGetUniformLocation(programLighting, "gammaCorrectness"), 1.0f / 2.2f, 1.0f / 2.2f, 1.0f / 2.2f);
glUniform3f(glGetUniformLocation(programLighting,"eyePosition"), m_camera.x, m_camera.y, m_camera.z);
glUniform3f(glGetUniformLocation(programLighting, "ambientColour"), 5.0f / 255.0f, 5.0f / 255.0f, 5.0f / 255.0f);
glUniform3f(glGetUniformLocation(programLighting, "light.colour"), 75.0f / 255.0f, 75.0f / 255.0f, 75.0f / 255.0f);
glUniform3f(glGetUniformLocation(programLighting, "light.position"), lightPos.x, lightPos.y, lightPos.z);
glUniform1f(glGetUniformLocation(programLighting, "light.shininess"), 90.0f);
// draw calls
objDrawCall(planeObject);
// need to recompute the MVP for each object in the scene and set the uniforms for it.
glm::mat4 depthM = m_sphereObject[i]->getMatrix();
depthMVP = depthProjection * depthView * depthM;
depthBiasMVP = biasMatrix * depthMVP;
glUniformMatrix4fv(glGetUniformLocation(programLighting, "depthBiasMVP"), count, false, &depthBiasMVP[0][0]);
glUniformMatrix4fv(glGetUniformLocation(programLighting, "modelMatrix"), count, false, &m_sphereObject[i]->getMatrix()[0][0]);
glUniformMatrix4fv(glGetUniformLocation(programLighting, "normalMatrix"), count, false, &glm::transpose(glm::inverse(m_sphereObject[i]->getMatrix()))[0][0]);
objDrawCall(sphereObject);
glBindTexture(GL_TEXTURE_2D, 0);
glUseProgram(0);
Key points about the about is the depthBiasMVP that is used to convert the results from the shadow pass from [-1, 1] to [0, 1] so we can use it like a texture using UV's, this is computed by:
glm::mat4 biasMatrix(
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
);
glm::mat4 depthBiasMVP = biasMatrix * depthMVP;
Gamma correctness is set as a uniform and this will be used at the very end of the fragment shader to apply gamma correctness to the final fragment colour before writing it to the framebuffer. Some of the uniforms are 'light.xxxxx' we will use a light structure within the shader, which works that same way as a struct in C++ for reading/writing to. We have set up the uniforms for our shaders, now for the shader code:
vertex shader:
#version 330
layout(location = 0) in vec4 position; ///< the vertex co-ordinate from VBO
layout(location = 1) in vec3 normal; ///< the normal from VBO
layout(location = 2) in vec2 uv; ///< the UV co-ordinate from VBO
// uniforms
uniform vec3 eyePosition;
uniform mat4 modelMatrix;
uniform mat4 viewMatrix;
uniform mat4 projectionlMatrix;
uniform mat4 depthBiasMVP;
// output to fragment shader
out vec2 vs_uv;
out vec3 vs_fragpos;
out vec3 vs_normal;
out vec3 vs_position;
out vec3 vs_lightDir;
out vec3 vs_halfVector;
out vec4 vs_shadowCoord;
// light
uniform struct Light
{
vec3 position;
vec3 colour;
float shininess;
} light;
void main()
{
// compute the world, view and clip spaces
vec4 worldPos = modelMatrix * position;
vec4 eyePos = viewMatrix * worldPos;
vec4 clipPos = projectionlMatrix * eyePos;
gl_Position = clipPos;
// set the outputs to fragment shader
vs_uv = uv;
vs_normal = normal;
vs_fragpos = worldPos.xyz;
vs_shadowCoord = depthBiasMVP * position;
// compute the light direction and length
vec3 lightDir = vec3(light.position - worldPos.xyz);
float lightDis = length(lightDir);
// basically the same as doing normalize
vs_lightDir = lightDir / lightDis;
// compute the half vector
vs_halfVector = normalize(lightDir + eyePosition);
}
Fragment shader:
#version 330
uniform sampler2D texture;
uniform sampler2DShadow shadowMap;
uniform vec3 gammaCorrectness;
uniform vec3 ambientColour;
uniform mat3 normalMatrix;
uniform mat4 modelMatrix;
in vec2 vs_uv;
in vec3 vs_fragpos;
in vec3 vs_normal;
in vec3 vs_position;
in vec3 vs_lightDir;
in vec3 vs_halfVector;
in vec4 vs_shadowCoord;
out vec4 output_colour;
uniform struct Light
{
vec3 position;
vec3 colour;
float shininess;
} light;
void main()
{
// what pixel colour is the texture for this Uv position
vec4 diffuse = texture2D(texture, vs_uv);
vec3 normal = normalize(normalMatrix * vs_normal); // the normal
// compute the amount of diffuse and specular
float diff = max(0.0, dot(normal, vs_lightDir));
float spec = max(0.0, dot(normal, vs_halfVector));
// normals that face away from the light cannot be lit
if(diff == 0)
spec = 0;
else
spec = pow(spec, light.shininess);
vec3 surfaceToLight = light.position - vs_fragpos;
float brightness = dot(normal, surfaceToLight) / (length(surfaceToLight) * length(normal));
brightness = clamp(brightness, 0, 1);
// in shadow or not
float visibility = 1.0;
// calulate shadow using smapler2DShadow
visibility = textureProj(shadowMap, vs_shadowCoord);
// compute if the light is directly or indirectly hitting the camera and clamp the results
vec3 scatteredLight = ambientColour + light.colour * diff;
vec3 reflectedLight = light.colour * spec * brightness;
scatteredLight = clamp(scatteredLight, 0, 1);
reflectedLight = clamp(reflectedLight, 0, 1);
vec3 finalColour = vec3((scatteredLight * diffuse.rgb + reflectedLight) * visibility);
// apply gamma correctness
vec3 gamma = vec3(gammaCorrectness);
finalColour = pow(finalColour, gamma);
// output colour
output_colour = vec4(finalColour, diffuse.a);
}
If you compile and run the program you should have an output result something like:
In the shadow pass, we created a Model-View-Projection matrix from the lights' position, in the lighting pass we are going to create a Model-View-Projection matrix from the camera's position.
// MVP from camera position
glm::mat4 model(1.0f);
glm::mat4 view = glm::lookAt(m_camera, glm::vec3(0.0f, 0.0f, 0.0f), glm::vec3(0.0f, 1.0f, 0.0f));
glm::mat4 projection = glm::perspective(45.0f, scrennSize.x / scrennSize.y, 0.1f, 100.0f);
glm::mat3 normalMatrix = glm::transpose(glm::inverse(model));
The normalMatrix will be used as a uniform in the fragment shader for normal calculations, we will come to this in a bit. The rest of the lighting pass is very simple on the C++ side as we just have to set up the uniforms, bind shader programs and textures:
// set the viewport and bind resources
glViewport(0, 0, (size_t)scrennSize.x, (size_t)scrennSize.y);
glUseProgram(programLighting);
glActiveTexture(GL_TEXTURE0 + 1);
glBindTexture(GL_TEXTURE_2D, texture_id); ///< just a texture we will use in the scene
glUniform1i(glGetUniformLocation(programLighting, "texture"), 1);
glActiveTexture(GL_TEXTURE0 + 2);
glBindTexture(GL_TEXTURE_2D, shadow_id); ///< the shadow texture
glUniform1i(glGetUniformLocation(programLighting, "shadowMap"), 2);
// set the uniforms for matrices etc.
glUniformMatrix4fv(glGetUniformLocation(programLighting, "depthBiasMVP"), count, false, &depthBiasMVP[0][0]);
glUniformMatrix4fv(glGetUniformLocation(programLighting, "modelMatrix"), count, false, &model[0][0]);
glUniformMatrix4fv(glGetUniformLocation(programLighting, "viewMatrix"), count, false, &view[0][0]);
glUniformMatrix4fv(glGetUniformLocation(programLighting, "projectionlMatrix"), count, false, &projection[0][0]);
glUniformMatrix3fv(glGetUniformLocation(programLighting, "normalMatrix"), count, false, &normalMatrix[0][0]);
glUniform3f(glGetUniformLocation(programLighting, "gammaCorrectness"), 1.0f / 2.2f, 1.0f / 2.2f, 1.0f / 2.2f);
glUniform3f(glGetUniformLocation(programLighting,"eyePosition"), m_camera.x, m_camera.y, m_camera.z);
glUniform3f(glGetUniformLocation(programLighting, "ambientColour"), 5.0f / 255.0f, 5.0f / 255.0f, 5.0f / 255.0f);
glUniform3f(glGetUniformLocation(programLighting, "light.colour"), 75.0f / 255.0f, 75.0f / 255.0f, 75.0f / 255.0f);
glUniform3f(glGetUniformLocation(programLighting, "light.position"), lightPos.x, lightPos.y, lightPos.z);
glUniform1f(glGetUniformLocation(programLighting, "light.shininess"), 90.0f);
// draw calls
objDrawCall(planeObject);
// need to recompute the MVP for each object in the scene and set the uniforms for it.
glm::mat4 depthM = m_sphereObject[i]->getMatrix();
depthMVP = depthProjection * depthView * depthM;
depthBiasMVP = biasMatrix * depthMVP;
glUniformMatrix4fv(glGetUniformLocation(programLighting, "depthBiasMVP"), count, false, &depthBiasMVP[0][0]);
glUniformMatrix4fv(glGetUniformLocation(programLighting, "modelMatrix"), count, false, &m_sphereObject[i]->getMatrix()[0][0]);
glUniformMatrix4fv(glGetUniformLocation(programLighting, "normalMatrix"), count, false, &glm::transpose(glm::inverse(m_sphereObject[i]->getMatrix()))[0][0]);
objDrawCall(sphereObject);
glBindTexture(GL_TEXTURE_2D, 0);
glUseProgram(0);
Key points about the about is the depthBiasMVP that is used to convert the results from the shadow pass from [-1, 1] to [0, 1] so we can use it like a texture using UV's, this is computed by:
glm::mat4 biasMatrix(
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
);
glm::mat4 depthBiasMVP = biasMatrix * depthMVP;
Gamma correctness is set as a uniform and this will be used at the very end of the fragment shader to apply gamma correctness to the final fragment colour before writing it to the framebuffer. Some of the uniforms are 'light.xxxxx' we will use a light structure within the shader, which works that same way as a struct in C++ for reading/writing to. We have set up the uniforms for our shaders, now for the shader code:
vertex shader:
#version 330
layout(location = 0) in vec4 position; ///< the vertex co-ordinate from VBO
layout(location = 1) in vec3 normal; ///< the normal from VBO
layout(location = 2) in vec2 uv; ///< the UV co-ordinate from VBO
// uniforms
uniform vec3 eyePosition;
uniform mat4 modelMatrix;
uniform mat4 viewMatrix;
uniform mat4 projectionlMatrix;
uniform mat4 depthBiasMVP;
// output to fragment shader
out vec2 vs_uv;
out vec3 vs_fragpos;
out vec3 vs_normal;
out vec3 vs_position;
out vec3 vs_lightDir;
out vec3 vs_halfVector;
out vec4 vs_shadowCoord;
// light
uniform struct Light
{
vec3 position;
vec3 colour;
float shininess;
} light;
void main()
{
// compute the world, view and clip spaces
vec4 worldPos = modelMatrix * position;
vec4 eyePos = viewMatrix * worldPos;
vec4 clipPos = projectionlMatrix * eyePos;
gl_Position = clipPos;
// set the outputs to fragment shader
vs_uv = uv;
vs_normal = normal;
vs_fragpos = worldPos.xyz;
vs_shadowCoord = depthBiasMVP * position;
// compute the light direction and length
vec3 lightDir = vec3(light.position - worldPos.xyz);
float lightDis = length(lightDir);
// basically the same as doing normalize
vs_lightDir = lightDir / lightDis;
// compute the half vector
vs_halfVector = normalize(lightDir + eyePosition);
}
Fragment shader:
#version 330
uniform sampler2D texture;
uniform sampler2DShadow shadowMap;
uniform vec3 gammaCorrectness;
uniform vec3 ambientColour;
uniform mat3 normalMatrix;
uniform mat4 modelMatrix;
in vec2 vs_uv;
in vec3 vs_fragpos;
in vec3 vs_normal;
in vec3 vs_position;
in vec3 vs_lightDir;
in vec3 vs_halfVector;
in vec4 vs_shadowCoord;
out vec4 output_colour;
uniform struct Light
{
vec3 position;
vec3 colour;
float shininess;
} light;
void main()
{
// what pixel colour is the texture for this Uv position
vec4 diffuse = texture2D(texture, vs_uv);
vec3 normal = normalize(normalMatrix * vs_normal); // the normal
// compute the amount of diffuse and specular
float diff = max(0.0, dot(normal, vs_lightDir));
float spec = max(0.0, dot(normal, vs_halfVector));
// normals that face away from the light cannot be lit
if(diff == 0)
spec = 0;
else
spec = pow(spec, light.shininess);
vec3 surfaceToLight = light.position - vs_fragpos;
float brightness = dot(normal, surfaceToLight) / (length(surfaceToLight) * length(normal));
brightness = clamp(brightness, 0, 1);
// in shadow or not
float visibility = 1.0;
// calulate shadow using smapler2DShadow
visibility = textureProj(shadowMap, vs_shadowCoord);
// compute if the light is directly or indirectly hitting the camera and clamp the results
vec3 scatteredLight = ambientColour + light.colour * diff;
vec3 reflectedLight = light.colour * spec * brightness;
scatteredLight = clamp(scatteredLight, 0, 1);
reflectedLight = clamp(reflectedLight, 0, 1);
vec3 finalColour = vec3((scatteredLight * diffuse.rgb + reflectedLight) * visibility);
// apply gamma correctness
vec3 gamma = vec3(gammaCorrectness);
finalColour = pow(finalColour, gamma);
// output colour
output_colour = vec4(finalColour, diffuse.a);
}
If you compile and run the program you should have an output result something like:
I have not gone over how to load models or textures but if you are interested in how I have done it, or see any of the other source code, you can find it here: https://github.com/LewisWard/Shadow-Mapping