Before I start to explain how I implemented shadow mapping using C++ and OpenGL, if you don't know what shadow mapping is take a read of my earlier blog post where I explain the process of shadow mapping.
I'm not going to go into detail on how you can set up a project from scratch to create a window using a library like Freeglut as that would be a whole tutorial by its self and there are many good tutorials out there which will explain how to do that. However, for a reference what I've used to create this project is:
I'm not going to go into detail on how you can set up a project from scratch to create a window using a library like Freeglut as that would be a whole tutorial by its self and there are many good tutorials out there which will explain how to do that. However, for a reference what I've used to create this project is:
- Freeglut
- Glew
- GLM
- Tiny_obj_loader
- SOIL
We are going to use two passes for each frame, the first pass being the shadow pass and the second our lighting pass. In order to do this we need to create a Framebuffer and Texture to store the results of the first pass to be used in the second.
GLsizei shadowFrameBuffer;
glGenFramebuffers(1, &shadowFrameBuffer);
glBindFramebuffer(GL_FRAMEBUFFER, shadowFrameBuffer);
// generate a texture to store the depth buffer from the shader pass, come to this in a bit
if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE)
printf("Framebuffer Error\n");
For those how haven't used framebuffers before, don't worry they are not that complicated to understand although I do recommend you read up on them. In our case we will use the framebuffer in the shadow pass, we bind this newly created framebuffer which then allows us to write to it. Once we are done we unbind that framebuffer, which returns us to the original framebuffer you always have when you create a OpenGL context (this one is sent to the monitor). Within the code snippet above I left a comment about creating a texture, so lets do this now:
GLsizei id;
glGenTextures(1, &id);
glBindTexture(GL_TEXTURE_2D, id);
glTexImage2D(GL_TEXTURE_2D, 0, GL_DEPTH_COMPONENT, 512, 512, 0, GL_DEPTH_COMPONENT, GL_FLOAT, NULL);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_COMPARE_MODE, GL_COMPARE_REF_TO_TEXTURE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_COMPARE_FUNC, GL_LEQUAL);
glBindTexture(GL_TEXTURE_2D, 0);
There a quite a lot there, but compared to creating any other normal texture there are only a few differences. GL_DEPTH_COMPONENT is used, which should be self explanatory, and I'm using GL_TEXTURE_COMPARE_MODE and GL_TEXTURE_COMPARE_FUNC. These two are used as I'm going to use sampler2DShadow in the fragment shader, which allows us to use textureProj function which means the texture look up between fragments is done by the texture hardware rather then us in the fragment shader.
Okay a quick check list, we have our framebuffer and texture set up to perform the shadow pass. So lets write the shadow pass:
// MVP from light poisition
lightPos = glm::vec3(-3.0f, 20.0f, 20.0f);
glm::mat4 depthProjection = glm::ortho(-10.0f, 10.0f, -10.0f, 10.0f, 0.1f, 100.0f);
glm::mat4 depthView = glm::lookAt(lightPos, glm::vec3(0.0f, 0.0f, 0.0f), glm::vec3(0.0f, 1.0f, 0.0f));
glm::mat4 depthModel = glm::mat4(1.0);
glm::mat4 depthMVP = depthProjection * depthView * depthModel;
// set the viewport to the same size as the texture, bind the texture and framebuffer.
glViewport(0, 0, 512, 512);
glUseProgram(program); // the handle of the shader program you complied, see it's source below
glBindFramebuffer(GL_FRAMEBUFFER, shadowFrameBuffer);
glClear(GL_DEPTH_BUFFER_BIT);
glActiveTexture(GL_TEXTURE0 + 1);
glBindTexture(GL_TEXTURE_2D, id);
// set the uniform
glUniformMatrix4fv(glGetUniformLocation(shaderProgram, "depthMVP"), 1, false, &depthMVP[0][0]);
// draw a plane which other objects will be above
objDrawCall(planeObject);
glm::mat4 depthM = sphereObject->getMatrix(); // get objects model matrix, default glm::mat4(1)
depthMVP = depthProjection * depthView * depthM; // need to update MVP for each object
shadowProgram->uniformMatrix4("depthMVP", 1, depthMVP);
objDrawCall(sphereObject);
// unbind everything
glBindTexture(GL_TEXTURE_2D, 0);
glBindFramebuffer(GL_FRAMEBUFFER, 0);
glUseProgram(0);
The majority of the above is made up of binding and unbinding objects and creating the model, view and projection matrix and setting shader uniforms. This is pretty straight forward code as it is the same as you would normally do for rendering any objects apart from we are using our new framebuffer. The objDrawCall function does pretty much what is says it does, it draws the object it's passed. This might vary depending on how your loading and storing your model, but I'll include the code anyway to the sake of completeness.
void objDrawCall(ObjObject* objPtr)
{
const float* offset = 0;
glBindBuffer(GL_ARRAY_BUFFER, objPtr->getVertexBuffer()->getVBO());
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, objPtr->getVertexBuffer()->getIBO());
glVertexAttribPointer(0, 3, GL_FLOAT, false, sizeof(vertexNormalUV), offset); // vertex position
glEnableVertexAttribArray(0);
glVertexAttribPointer(1, 3, GL_FLOAT, true, sizeof(vertexNormalUV), offset + 3); // normal
glEnableVertexAttribArray(1);
glVertexAttribPointer(2, 2, GL_FLOAT, true, sizeof(vertexNormalUV), offset + 6); // uv
glEnableVertexAttribArray(2);
glDrawElements(GL_TRIANGLES, (GLsizei)objPtr->getIndicesCount(), GL_UNSIGNED_INT, 0);
glDisableVertexAttribArray(2);
glDisableVertexAttribArray(1);
glDisableVertexAttribArray(0);
glBindBuffer(GL_ARRAY_BUFFER, 0);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);
}
We can't run the program yet as we have not written any shaders. For the shadow pass we need to write a vertex and fragment shader, I'm not doing to explain the steps for compiling and linking shaders as there is information already out there and this post is starting to get long.
Shadow vertex shader:
#version 330
layout(location = 0) in vec4 position; ///< the vertex co-odinate from VBO
layout(location = 1) in vec3 normal; ///< the normal from VBO
layout(location = 2) in vec2 uv; ///< the UV co-odinate from VBO
uniform mat4 depthMVP;
void main()
{
gl_Position = depthMVP * position;
}
Shadow Fragment shader:
#version 330
layout(location = 0) out float fragmentdepth;
void main()
{
// OpenGL will output gl_FragCoord.z;
}
That is it, very simple shaders. The vertex shader is very standard and not doing anything interesting, the fragment shader outputs a float which makes our depth texture. As the fragment shader is empty OpenGL will output the gl_FragCoord by default and as we are using a GL_DEPTH_COMPONENT it will output it Z value. I'm not sure if this will work on all OpenGL vendor's card but is happy enough on Nvidia, just bare that in mind if it's not outputting anything for you. If you run the code you should get a result similar to the image below.
GLsizei shadowFrameBuffer;
glGenFramebuffers(1, &shadowFrameBuffer);
glBindFramebuffer(GL_FRAMEBUFFER, shadowFrameBuffer);
// generate a texture to store the depth buffer from the shader pass, come to this in a bit
if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE)
printf("Framebuffer Error\n");
For those how haven't used framebuffers before, don't worry they are not that complicated to understand although I do recommend you read up on them. In our case we will use the framebuffer in the shadow pass, we bind this newly created framebuffer which then allows us to write to it. Once we are done we unbind that framebuffer, which returns us to the original framebuffer you always have when you create a OpenGL context (this one is sent to the monitor). Within the code snippet above I left a comment about creating a texture, so lets do this now:
GLsizei id;
glGenTextures(1, &id);
glBindTexture(GL_TEXTURE_2D, id);
glTexImage2D(GL_TEXTURE_2D, 0, GL_DEPTH_COMPONENT, 512, 512, 0, GL_DEPTH_COMPONENT, GL_FLOAT, NULL);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_COMPARE_MODE, GL_COMPARE_REF_TO_TEXTURE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_COMPARE_FUNC, GL_LEQUAL);
glBindTexture(GL_TEXTURE_2D, 0);
There a quite a lot there, but compared to creating any other normal texture there are only a few differences. GL_DEPTH_COMPONENT is used, which should be self explanatory, and I'm using GL_TEXTURE_COMPARE_MODE and GL_TEXTURE_COMPARE_FUNC. These two are used as I'm going to use sampler2DShadow in the fragment shader, which allows us to use textureProj function which means the texture look up between fragments is done by the texture hardware rather then us in the fragment shader.
Okay a quick check list, we have our framebuffer and texture set up to perform the shadow pass. So lets write the shadow pass:
// MVP from light poisition
lightPos = glm::vec3(-3.0f, 20.0f, 20.0f);
glm::mat4 depthProjection = glm::ortho(-10.0f, 10.0f, -10.0f, 10.0f, 0.1f, 100.0f);
glm::mat4 depthView = glm::lookAt(lightPos, glm::vec3(0.0f, 0.0f, 0.0f), glm::vec3(0.0f, 1.0f, 0.0f));
glm::mat4 depthModel = glm::mat4(1.0);
glm::mat4 depthMVP = depthProjection * depthView * depthModel;
// set the viewport to the same size as the texture, bind the texture and framebuffer.
glViewport(0, 0, 512, 512);
glUseProgram(program); // the handle of the shader program you complied, see it's source below
glBindFramebuffer(GL_FRAMEBUFFER, shadowFrameBuffer);
glClear(GL_DEPTH_BUFFER_BIT);
glActiveTexture(GL_TEXTURE0 + 1);
glBindTexture(GL_TEXTURE_2D, id);
// set the uniform
glUniformMatrix4fv(glGetUniformLocation(shaderProgram, "depthMVP"), 1, false, &depthMVP[0][0]);
// draw a plane which other objects will be above
objDrawCall(planeObject);
glm::mat4 depthM = sphereObject->getMatrix(); // get objects model matrix, default glm::mat4(1)
depthMVP = depthProjection * depthView * depthM; // need to update MVP for each object
shadowProgram->uniformMatrix4("depthMVP", 1, depthMVP);
objDrawCall(sphereObject);
// unbind everything
glBindTexture(GL_TEXTURE_2D, 0);
glBindFramebuffer(GL_FRAMEBUFFER, 0);
glUseProgram(0);
The majority of the above is made up of binding and unbinding objects and creating the model, view and projection matrix and setting shader uniforms. This is pretty straight forward code as it is the same as you would normally do for rendering any objects apart from we are using our new framebuffer. The objDrawCall function does pretty much what is says it does, it draws the object it's passed. This might vary depending on how your loading and storing your model, but I'll include the code anyway to the sake of completeness.
void objDrawCall(ObjObject* objPtr)
{
const float* offset = 0;
glBindBuffer(GL_ARRAY_BUFFER, objPtr->getVertexBuffer()->getVBO());
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, objPtr->getVertexBuffer()->getIBO());
glVertexAttribPointer(0, 3, GL_FLOAT, false, sizeof(vertexNormalUV), offset); // vertex position
glEnableVertexAttribArray(0);
glVertexAttribPointer(1, 3, GL_FLOAT, true, sizeof(vertexNormalUV), offset + 3); // normal
glEnableVertexAttribArray(1);
glVertexAttribPointer(2, 2, GL_FLOAT, true, sizeof(vertexNormalUV), offset + 6); // uv
glEnableVertexAttribArray(2);
glDrawElements(GL_TRIANGLES, (GLsizei)objPtr->getIndicesCount(), GL_UNSIGNED_INT, 0);
glDisableVertexAttribArray(2);
glDisableVertexAttribArray(1);
glDisableVertexAttribArray(0);
glBindBuffer(GL_ARRAY_BUFFER, 0);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);
}
We can't run the program yet as we have not written any shaders. For the shadow pass we need to write a vertex and fragment shader, I'm not doing to explain the steps for compiling and linking shaders as there is information already out there and this post is starting to get long.
Shadow vertex shader:
#version 330
layout(location = 0) in vec4 position; ///< the vertex co-odinate from VBO
layout(location = 1) in vec3 normal; ///< the normal from VBO
layout(location = 2) in vec2 uv; ///< the UV co-odinate from VBO
uniform mat4 depthMVP;
void main()
{
gl_Position = depthMVP * position;
}
Shadow Fragment shader:
#version 330
layout(location = 0) out float fragmentdepth;
void main()
{
// OpenGL will output gl_FragCoord.z;
}
That is it, very simple shaders. The vertex shader is very standard and not doing anything interesting, the fragment shader outputs a float which makes our depth texture. As the fragment shader is empty OpenGL will output the gl_FragCoord by default and as we are using a GL_DEPTH_COMPONENT it will output it Z value. I'm not sure if this will work on all OpenGL vendor's card but is happy enough on Nvidia, just bare that in mind if it's not outputting anything for you. If you run the code you should get a result similar to the image below.
I'm going to stop this post at this point, I will write a second post for how we can use this shadow map to actually create shadows for our 3D objects.
Click here for the second part of the tutorial. Source code can be found on my GitHub. |
© 2016 Lewis Ward. All rights reserved.