阴影是光线被阻挡的结果;当一个光源的光线由于其他物体的阻挡不能够达到一个物体的表面的时候,那么这个物体就在阴影中了。阴影能够使场景看起来真实得多,并且可以让观察者获得物体之间的空间位置关系。
阴影映射
阴影映射(Shadow Mapping)背后的思路非常简单:我们以光的位置为视角进行渲染,我们能看到的东西都将被点亮,看不见的一定是在阴影之中了。假设有一个地板,在光源和它之间有一个大盒子。由于光源处向光线方向看去,可以看到这个盒子,但看不到地板的一部分,这部分就应该在阴影中了。
这里的所有蓝线代表光源可以看到的fragment。黑线代表被遮挡的fragment。我们绘制一条从光源出发,到达最右边盒子上的一个片段上的线段或射线,那么射线将先击中悬浮的盒子,随后才会到达最右侧的盒子。结果就是悬浮的盒子被照亮,而最右侧的盒子将处于阴影之中。
从光源发出一条射线,将射线击中的物体上的点进行对比。找出最近之外的点,那么这个点就在阴影中。但是从光源发出的射线有成千上万条,对于引擎来说是极耗性能的。我们可以采取相似的举措:深度缓冲。
左侧的图片展示了一个定向光源(所有光线都是平行的)在立方体下的表面投射的阴影。通过储存到深度贴图中的深度值,我们就能找到最近点,用以决定片段是否在阴影中。我们使用一个来自光源的视图和投影矩阵来渲染场景就能创建一个深度贴图。
在右边的图中我们显示出同样的平行光和观察者。我们渲染一个点P处的片段,需要决定它是否在阴影中。我们先得使用T变换矩阵把P变换到光源的坐标空间里。既然点P是从光的透视图中看到的,它的z坐标就对应于它的深度,例子中这个值是0.9。使用点P在光源的坐标空间的坐标,我们可以索引深度贴图,来获得从光的视角中最近的可见深度,结果是点C,最近的深度是0.4。因为索引深度贴图的结果是一个小于点P的深度,我们可以断定P被挡住了,它在阴影中了。
阴影映射由两个步骤组成:1.我们渲染深度贴图 ;2.我们像往常一样渲染场景,使用生成的深度贴图来计算片段是否在阴影之中。
深度贴图
第一步我们需要生成一张深度贴图(Depth Map)。
首先,我们要为渲染的深度贴图创建一个帧缓冲对象:
把我们把生成的深度纹理作为帧缓冲的深度缓冲:
渲染阴影
正确地生成深度贴图以后我们就可以开始生成阴影了。这段代码在片段着色器中执行,用来检验一个片段是否在阴影之中,不过我们在顶点着色器中进行光空间的变换:
这里light_space_pos把世界空间坐标转化为光空间。顶点着色器传递一个普通的经变换的世界空间顶点位置v_pos和一个光空间的light_space_pos给片段着色器。
改进阴影贴图
阴影失真
前面的图片中明显有不对的地方。放大看会发现明显的线条样式:
我们可以看到地板四边形渲染出很大一块交替黑线。这种阴影贴图的不真实感叫做阴影失真(Shadow Acne),官方解释如下:
因为阴影贴图受限于分辨率,在距离光源比较远的情况下,多个片段可能从深度贴图的同一个值中去采样。图片每个斜坡代表深度贴图一个单独的纹理像素。你可以看到,多个片段从同一个深度值进行采样。有些片段被认为是在阴影之中,有些不在,由此产生了图片中的条纹样式。
我们可以用一个叫做阴影偏移(shadow bias)的技巧来解决这个问题,我们简单的对表面的深度(或深度贴图)应用一个偏移量,这样片段就不会被错误地认为在表面之下了。
悬浮
使用阴影偏移的一个缺点是你对物体的实际深度应用了平移。偏移如果足够大,可以很明显看出阴影相对实际物体位置的偏移。
我们可以使用一个叫技巧解决大部分的Peter panning问题:当渲染深度贴图时候使用正面剔除(front face culling)
PCF
目标是得到柔和阴影
深度贴图有一个固定的分辨率,多个片段对应于一个纹理像素。结果就是多个片段会从深度贴图的同一个深度值进行采样,这几个片段便得到的是同一个阴影,这就会产生锯齿边。
有两种方案可以解决:可以通过增加深度贴图的分辨率的方式来降低锯齿块,也可以尝试尽可能的让光的视锥接近场景;另一个解决方案叫做PCF(percentage-closer filtering),核心思想是从深度贴图中多次采样,每一次采样的纹理坐标都稍有不同。每个独立的样本可能在也可能不再阴影中。所有的次生结果接着结合在一起,进行平均化,我们就得到了柔和阴影。