参考于:https://learnopengl.com/#!Advanced-Lighting/Shadows/Shadow-Mapping
一、游戏中的阴影
阴影是光线被阻挡的结果,当一个光源的光线由于其他物体的阻挡不能够达到一个物体的表面的时候,那么这个物体就在阴影中了。理论上阴影无处不在,但是想要渲染出很好的阴影效果,并不是一件特别容易的事,目前实时渲染领域还没找到一种完美的阴影算法,尽管有不少近似阴影技术,但它们都有自己的弱点和不足
minecraft游戏截图目前较多使用的一种技术是阴影贴图(shadow mapping),这是一个容易扩展的的阴影算法,想要通过阴影贴图实现一个还不错的效果还是比较困难的,但可以暂时先了解下阴影贴图的概念以及尝试渲染出最简单的阴影效果
二、阴影映射
阴影映射的原理非常简单:以光的位置为视角进行渲染,能看到的片段都将被点亮,看不见的一定是在阴影之中了,当然也需要考虑投影方式,一般对于平行光是正交投影,对于点光源等其它光源是透视投影
不过对于点光源,它的投影没有特定的视角一说,可以理解为是360°全方位视角,因此在计算阴影时需要渲染深度值到立方体贴图,为了先入门可以暂时只考虑平行光(正交投影),比较简单一些
还记不记得OpenGL的深度测试,或许可以利用深度值去判断片段对于当前光源是否被遮挡呢?
正如下左图,黄色的片段就是直接被照射的片段,黑色的片段就是被遮挡的,而对于所有的黑色片段,它与光源的连线上一定存在着其它可视片段,这样我们要做的就是下右图:对于每一个片段 ,判断是否存在一个满足条件的片段 其深度值 < ,也就是判断 点是否被光源直接照射
具体步骤:
- 进入以光源为观察点的坐标空间中,按照深度测试的方法渲染出一张深度贴图(depth map):对于深度贴图中的每一个深度值,一定是光源透视图下见到的第一个片元的深度值
- 得到深度贴图后,在正常渲染流程中检查所有的渲染片段 :通过 变换到光源坐标空间得到 ,然后索引深度贴图来获得从光的视角中最近的可见深度,根据其和 点深度的关系来确定 点是否位被遮挡,如果 被遮挡,则无视当前片段的光照计算结果
三、深度贴图
为了渲染得到深度贴图,需要再定义一个帧缓冲,因为只需要关心深度值,所以可以把纹理格式指定为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);
绘制的时候要考虑的东西:
- 如果有自定义深度贴图的解析度,需要glViewport()设置绘图区域
- 要以光源为观察点,计算光空间的投影和观察矩阵,它们相乘得到的正是光源的空间矩阵
代码中为了简单暂时把点光源当成平行光来算,因此投影矩阵为正交矩阵
- glm::ortho(float left, float right, float bottom, float top, float zNear, float zFar):前两个参数为平截头体的左右坐标,中间两个参数为平截头体的底部和顶部,这四个参数决定了近平面和远平面的大小,最后两个参数为近平面和远平面的距离
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方法中的逻辑:
- 顶点着色器输出顶点位置到gl_Position时,OpenGL会自动进行透视除法,而我们自己计算的 当然就需要手动映射到NDC空间了,这个很好办,前3个分量除以w就好
- 然后就是经常操作的:将坐标范围由[-1, 1]转化为[0, 1]
- 第三步获取深度贴图的深度值,进行深度比较
- 最后如果 的深度值大于深度贴图对应深度值,则可以确定被遮挡
#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吧