阴影映射(Shadow Mapping)
:我们以光的位置为视角进行渲染,能看到的东西都将被点亮,看不见的一定是在阴影之中了。
在深度缓冲里的一个值是摄像机视角下,对应于一个片段的一个0到1之间的深度值。如果我们从光源的透视图来渲染场景,并把深度值的结果储存到纹理中,就能对光源的透视图所见的最近的深度值进行采样。最终,深度值就会显示从光源的透视图下见到的第一个片段了。我们管储存在纹理中的所有这些深度值,叫做深度贴图(depth map)
或阴影贴图
。
投影和视图矩阵结合在一起成为一个T
变换,它可以将任何三维位置转变到光源的可见坐标空间。
我们渲染一个点P
处的片段,需要决定它是否在阴影中。
T
把P
变换到光源的坐标空间里。既然点P
是从光的透视图中看到的,它的z
坐标就对应于它的深度,例子中这个值是0.9P
在光源的坐标空间的坐标,我们可以索引深度贴图,来获得从光的视角中最近的可见深度,结果是点C
,最近的深度是0.4。P
的深度,我们可以断定P
被挡住了,它在阴影中了。可以看到地板四边形渲染出很大一块交替黑线,这种阴影贴图的不真实感叫做阴影失真(Shadow Acne)
,产生原因如下:
因为阴影贴图受限于分辨率,在距离光源比较远的情况下,多个片段可能从深度贴图的同一个值中去采样,意味着片段得到的最近深度是一样的。但是片段到光源的距离又是不一样的,就有可能导致相邻的片段z
值在最近深度上下波动,计算时有的在阴影中,有的不在。
可以用一个叫做阴影偏移(shadow bias)
的技巧来解决这个问题,我们简单的对表面的深度(或深度贴图)应用一个偏移量,使得相邻片段的深度在同一个水平,这样片段就不会被错误地认为在表面之下了。
使用阴影偏移的一个缺点是你对物体的实际深度应用了平移。偏移有可能足够大,以至于可以看出阴影相对实际物体位置的偏移,你可以从下图看到这个现象(这是一个夸张的偏移值):
这个阴影失真叫做悬浮(Peter Panning)
,因为物体看起来轻轻悬浮在表面之上。当渲染深度贴图时候使用正面剔除(front face culling)
,这样一来正面不会产生深度信息,本质上是在不引入偏移的情况下解决了阴影失真。
光的视锥不可见的区域一律被认为是处于阴影中,不管它真的处于阴影之中。出现这个状况是因为超出光的视锥的投影坐标比1.0大,这样采样的深度纹理就会超出他默认的0到1的范围。根据纹理环绕方式,我们将会得到不正确的深度结果,它不是基于真实的来自光源的深度值。
光照有一个区域,超出该区域就成为了阴影;这个区域实际上代表着深度贴图的大小,这个贴图投影到了地板上。发生这种情况的原因是我们之前将深度贴图的环绕方式设置成了GL_REPEAT
。让所有超出深度贴图的坐标的深度范围是1.0,纹理函数总会返回一个1.0的深度值,阴影值为0.0。结果看起来会更真实。
仍有一部分是黑暗区域,那里的坐标超出了光的正交视锥的远平面。你可以看到这片黑色区域总是出现在光源视锥的极远处。当一个点比光的远平面还要远时,它的投影坐标的z坐标大于1.0。这种情况下,GL_CLAMP_TO_BORDER
环绕方式不起作用,因为我们把坐标的z
元素和深度贴图的值进行了对比;它总是为大于1.0的z
返回true。
放大看阴影,阴影映射对分辨率的依赖很快变得很明显。因为深度贴图有一个固定的分辨率,多个片段对应于一个纹理像素。结果就是多个片段会从深度贴图的同一个深度值进行采样,这几个片段便得到的是同一个阴影,这就会产生锯齿边。
PCF(percentage-closer filtering)
,这是一种多个不同过滤方式的组合,它产生柔和阴影,使它们出现更少的锯齿块和硬边。核心思想是从深度贴图中多次采样,每一次采样的纹理坐标都稍有不同。每个独立的样本可能在也可能不再阴影中。所有的次生结果接着结合在一起,进行平均化,我们就得到了柔和阴影。
float shadow = 0.0;
// textureSize返回一个给定采样器纹理的0级mipmap的vec2类型的宽和高。用1除以它返回一个单独纹理像素的大小,我们用以对纹理坐标进行偏移,确保每个新样本,来自不同的深度值。
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;
在各种方向生成动态阴影可以适用于点光源,生成所有方向上的阴影。这个技术叫做点光阴影
,过去的名字是万向阴影贴图(omnidirectional shadow maps)
技术。
对于深度贴图,我们需要从一个点光源的所有渲染场景,普通2D深度贴图不能工作;如果我们使用立方体贴图会怎样?因为立方体贴图可以储存6个面的环境数据,它可以将整个场景渲染到立方体贴图的每个面上,把它们当作点光源四周的深度值来采样。
生成深度立方体贴图有两种方式:
渲染场景6次:每次一个面。显然渲染场景6次需要6个不同的视图矩阵,每次把一个不同的立方体贴图面附加到帧缓冲对象上。这看起来是这样的:
for(int i = 0; i < 6; i++)
{
GLuint face = GL_TEXTURE_CUBE_MAP_POSITIVE_X + i;
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, face, depthCubemap, 0);
BindViewMatrix(lightViewMatrices[i]);
RenderScene();
}
几何着色器允许我们使用一次渲染过程来建立深度立方体贴图。有一个内建变量叫做gl_Layer
,它指定发散出基本图形送到立方体贴图的哪个面。当不管它时,几何着色器就会像往常一样把它的基本图形发送到输送管道的下一阶段,但当我们更新这个变量就能控制每个基本图形将渲染到立方体贴图的哪一个面。当然这只有当我们有了一个附加到激活的帧缓冲的立方体贴图纹理才有效:
#version 330 core
layout (triangles) in;
layout (triangle_strip, max_vertices=18) out;
uniform mat4 shadowMatrices[6];
out vec4 FragPos; // FragPos from GS (output per emitvertex)
void main()
{
for(int face = 0; face < 6; ++face)
{
gl_Layer = face; // built-in variable that specifies to which face we render.
for(int i = 0; i < 3; ++i) // for each triangle's vertices
{
FragPos = gl_in[i].gl_Position;
gl_Position = shadowMatrices[face] * FragPos;
EmitVertex();
}
EndPrimitive();
}
}
至于阴影的判断逻辑,跟定向光区别不大。