参考于:https://learnopengl.com/#!Advanced-Lighting/Shadows/Shadow-Mapping
阴影是光线被阻挡的结果,当一个光源的光线由于其他物体的阻挡不能够达到一个物体的表面的时候,那么这个物体就在阴影中了。理论上阴影无处不在,但是想要渲染出很好的阴影效果,并不是一件特别容易的事,目前实时渲染领域还没找到一种完美的阴影算法,尽管有不少近似阴影技术,但它们都有自己的弱点和不足
目前较多使用的一种技术是阴影贴图(shadow mapping),这是一个容易扩展的的阴影算法,想要通过阴影贴图实现一个还不错的效果还是比较困难的,但可以暂时先了解下阴影贴图的概念以及尝试渲染出最简单的阴影效果
阴影映射的原理非常简单:以光的位置为视角进行渲染,能看到的片段都将被点亮,看不见的一定是在阴影之中了,当然也需要考虑投影方式,一般对于平行光是正交投影,对于点光源等其它光源是透视投影
不过对于点光源,它的投影没有特定的视角一说,可以理解为是360°全方位视角,因此在计算阴影时需要渲染深度值到立方体贴图,为了先入门可以暂时只考虑平行光(正交投影),比较简单一些
还记不记得OpenGL的深度测试,或许可以利用深度值去判断片段对于当前光源是否被遮挡呢?
正如下左图,黄色的片段就是直接被照射的片段,黑色的片段就是被遮挡的,而对于所有的黑色片段,它与光源的连线上一定存在着其它可视片段,这样我们要做的就是下右图:对于每一个片段 ,判断是否存在一个满足条件的片段 其深度值 < ,也就是判断 点是否被光源直接照射
具体步骤:
为了渲染得到深度贴图,需要再定义一个帧缓冲,因为只需要关心深度值,所以可以把纹理格式指定为GL_DEPTH_COMPONENT,并通过glDrawBuffer(GL_NONE)和glReadBuffer(GL_NONE)来通知OpenGL不对颜色缓冲进行读写,除此之外还可以自定义其分辨率,它不一定要是屏幕的大小
GLuint depthFBO;
glGenFramebuffers(1, &depthFBO);
glBindFramebuffer(GL_FRAMEBUFFER, depthFBO);
GLuint* depthColorBuffer = getAttachmentTexture(1, true);
glBindTexture(GL_TEXTURE_2D, depthColorBuffer[0]);
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_TEXTURE_2D, depthColorBuffer[0], 0);
glDrawBuffer(GL_NONE);
glReadBuffer(GL_NONE); //不需要考虑颜色
if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE)
cout << "ERROR::FRAMEBUFFER:: Framebuffer is not complete!" << endl;
glBindFramebuffer(GL_FRAMEBUFFER, 0);
绘制的时候要考虑的东西:
代码中为了简单暂时把点光源当成平行光来算,因此投影矩阵为正交矩阵
while (!glfwWindowShouldClose(window))
{
glViewport(0, 0, SHADOW_WIDTH, SHADOW_HEIGHT);
glBindFramebuffer(GL_FRAMEBUFFER, depthFBO);
glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);
glm::mat4 lightProjection, lightView;
glm::mat4 lightSpaceMatrix;
GLfloat near_plane = 0.2f, far_plane = 15.0f;
lightProjection = glm::ortho(-20.0f, 20.0f, -20.0f, 20.0f, near_plane, far_plane);
lightView = glm::lookAt(glm::vec3(-4.0f, 2.6f, -0.25f), glm::vec3(0.0f), glm::vec3(0.0, 1.0, 0.0));
lightSpaceMatrix = lightProjection * lightView;
shaderDepth.Use();
glUniformMatrix4fv(glGetUniformLocation(shaderDepth.Program, "lightSpaceMatrix"), 1, GL_FALSE, glm::value_ptr(lightSpaceMatrix));
//wall.Draw(shaderObj, 3);
wood.Draw(shaderObj, 8);
//ground.Draw(shaderObj, groundIndex + 1);
//lightObj.Draw(shaderObj, 1);
//……
}
着色器没有什么特别的,因为颜色缓冲的话片段着色器不需要进行任何处理
如果正确的话,深度贴图大概是这样的:它是一片红因为对于RGB三属性,当然只有R属性有值,其值即为深度
#version 330 core
layout (location = 0) in vec3 position;
layout (location = 5) in mat4 model;
uniform mat4 lightSpaceMatrix;
void main()
{
gl_Position = lightSpaceMatrix * model * vec4(position, 1.0f);
}
//
#version 330 core
void main()
{
}
得到深度贴图后,就开始走正常的渲染流程,不过这次需要将阴影贴图传入片段着色器,将光源空间矩阵传入顶点着色器以进行光源空间变换
对于顶点着色器,直接上完整的代码:
其中 LightSpaceFragPosIn 就是片段在光空间的坐标,对应着图中的 ,传入片段着色器进行下一步计算
#version 420 core
layout (location = 0) in vec3 position;
layout (location = 1) in vec3 normal;
layout (location = 2) in vec2 texture;
layout (location = 3) in vec3 tangent;
layout (location = 4) in vec3 bitangent;
layout (location = 5) in mat4 model;
out VS_OUT
{
vec2 texIn;
vec3 normalIn;
vec3 fragPosIn;
vec4 LightSpaceFragPosIn;
mat3 TBN;
}vs_out;
uniform mat4 lightSpaceMatrix;
layout (std140, binding = 0) uniform Matrices
{
mat4 view; //观察矩阵
mat4 projection; //投影矩阵
};
void main()
{
gl_Position = projection * view * model * vec4(position, 1.0);
vs_out.fragPosIn = vec3(model * vec4(position, 1.0f));
vs_out.texIn = texture;
mat3 normalMat = transpose(inverse(mat3(model)));
vs_out.normalIn = normalMat * normal;
vec3 T = normalize(normalMat * tangent);
vec3 N = normalize(normalMat * normal);
T = normalize(T - dot(T, N) * N);
vec3 B = cross(T, N);
vs_out.TBN = mat3(T, B, N);
vs_out.LightSpaceFragPosIn = lightSpaceMatrix * vec4(vs_out.fragPosIn, 1.0);
}
对于片段着色器修改的部分如下:
ShadowCalculation方法即是判断当前的片段是否被遮挡,如果被遮挡,在下面计算对应光照时就不考虑当前光源对片段颜色的贡献,而对于ShadowCalculation方法中的逻辑:
#version 330 core
uniform sampler2D shadowMap;
//……
void main()
{
//……
//vec3 result = //……
for (int i = 0; i <= 0; i++)
{
float shadow = ShadowCalculation(LightSpaceFragPosIn);
result = result + (1.0 - shadow) * CalcPointLight(pointLights[i], normal, fragPos, viewDir);
}
//……
lightColor = vec4(result.rgb, 1.0);
}
float ShadowCalculation(vec4 fragPosLightSpace)
{
// 执行透视除法
vec3 projCoords = fragPosLightSpace.xyz / fragPosLightSpace.w;
// 变换到[0,1]的范围
projCoords = projCoords * 0.5 + 0.5;
// 取得最近点的深度(使用[0,1]范围下的fragPosLight当坐标)
float closestDepth = texture(shadowMap, projCoords.xy).r;
// 取得当前片段在光源视角下的深度
float currentDepth = projCoords.z;
// 检查当前片段是否在阴影中
float shadow = currentDepth > closestDepth ? 1.0 : 0.0;
return shadow;
}
如果没问题的话,可以得到这样的结果:
已经有阴影的效果了,但是问题很多,包括阴影失真以及很明显的锯齿,这些都一定是需要优化的,不过这章篇幅已经不小了分一下P吧