阴影贴图的原理较为简单,就是以光的方向为视角生成一副深度贴图,这幅图中能看到的部分就是光能照到的部分,所以在计算阴影时只要将片段深度与这幅深度贴图进行对比,深度比它大的片段即在阴影中。
实际实现的步骤如下:
- 创建画面元素(箱子,地板等)的VBO,VAO,光照矩阵,传入深度贴图shader
注意,因为本节考虑定向光,不存在透视,所以使用正交投影矩阵
GLfloat near_plane = 1.0f, far_plane = 7.5f;
glm::mat4 lightProjection = glm::ortho(-10.0f, 10.0f, -10.0f, 10.0f, near_plane, far_plane);
- 创建帧缓存和附加在上面的深度贴图
注意以下代码,贴图的类型是GL_DEPTH_COMPONENT,这种情况下贴图只有一个通道用于保存片段的深度值;其次SHADOW_WIDTH, SHADOW_HEIGHT为深度图的长宽分辨率,它们一般与屏幕不同。
glTexImage2D(GL_TEXTURE_2D, 0, GL_DEPTH_COMPONENT, SHADOW_WIDTH, SHADOW_HEIGHT, 0, GL_DEPTH_COMPONENT, GL_FLOAT, NULL);
由于这幅深度图是不用颜色数据来渲染的,所以我们加入以下两行。
glDrawBuffer(GL_NONE);
glReadBuffer(GL_NONE);
- 将深度贴图和对应的转换矩阵传入绘制阴影的shader中,将片段位置和片段在深度图中的位置从顶点着色器传到片元着色器。
- 计算阴影,过程如下:
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;
}
- 在计算光照时,将diffuse和specular颜色乘以这个阴影元素。
局限性
阴影贴图虽然实现简单,但是存在诸多局限性:
- 阴影失真
这是由于绘制程序中,每个片段要从深度图中读取自己在光照角度下的深度,但是读取的精度不高,往往是多个片段共用一个光照角度下的深度值
如图,阶梯状表示当前片段读取的深度值,横线表示光能照亮的深度,于是出现了平面上一段亮一段暗的情况。
为了解决它,我们通常在当前片段的深度值上减去一个偏移量bias,这样整个平面就都能被照亮了。
但是这也会带来另一个问题,由于偏移量的存在,阴影的位置也出现了偏移,我们只能将偏移量尽量设置小,保证它不是那么明显。
- 采样空间限制
因为我们照亮的区域,实际上是光照角度下平截头体内的空间,当地板非常大时,就会有一部分是平截头体无法覆盖的(可能在它的左右,也可能在远平面后面),这部分也会变成阴影。
为了解决这一问题,我们可以把深度贴图的纹理环绕选项设置为GL_CLAMP_TO_BORDER,同时把投影向量的z坐标大于1.0,我们就把shadow的值强制设为0.0。这样较远的地面也可以被照亮。
但是问题还是存在的,比如当远处的地面实际需要阴影的时候。
- 阴影走样
同样因为片段读取深度值精度不高的问题,阴影的边缘会出现明显的锯齿。
我们一般采用一种叫PCF(percentage-closer filtering)的方法柔化阴影,以此减少走样。
一个简单的PCF的实现是简单的从纹理像素四周对深度贴图采样,然后把结果平均起来:
float shadow = 0.0;
vec2 texelSize = 1.0 / textureSize(shadowMap, 0);
for(int x = -1; x <= 1; ++x)
{
for(int y = -1; y <= 1; ++y)
{
float pcfDepth = texture(shadowMap, projCoords.xy + vec2(x, y) * texelSize).r;
shadow += currentDepth - bias > pcfDepth ? 1.0 : 0.0;
}
}
shadow /= 9.0;
这种柔化并不能完全消除锯齿,