Shadow mapping 的原理:
一个物体之所以会处在阴影当中,是由于在它和光源之间存在着遮蔽物,或者说遮蔽物离光源的距离比物体要近,这就是 shadow mapping 算法的基本原理。
Pass1: 以光源为视点,或者说在光源坐标系下面对整个场景进行渲染,目的是要得到一副所有物体相对于光源的 depth map (也就是我们所说的shadow map),也就是这副图像中每个象素的值代表着场景里面离光源最近的 fragment 的深度值。由于这个pass中我们感兴趣的只是象素的深度值,所以可以把所有的光照计算关掉,打开 z-test 和 z-write 的 render state 。
Pass2: 将视点恢复到原来的正常位置,渲染整个场景,对每个象素计算它和光源的距离,然后将这个值和 depth map中相应的值比较,以确定这个象素点是否处在阴影当中。然后根据比较的结果,对 shadowed fragment 和 lighted fragment 分别进行不同的光照计算,这样就可以得到阴影的效果了。
从上面的分析可以看出来,depth map的渲染只和光源的位置以及场景中物体的位置有关,无论视点怎么运动,只要光源和物体的相互位置关系不变,shadow map就可以被重复使用,因此对于没有动态光源的场景,shadow mapping 是很明智的一种选择。
除了上面提到的不能很好应付动态光源场景的限制之外,shadow mapping 还存在着所有使用 texture的场景面临的共同问题-锯齿。根据采样定理,只有纹理分辨率小于或者等于物体的实际分辨率时才不会失真,而当一副很大的纹理被贴到尺寸比它小的物体上时,会出现一个 fragment 覆盖多个 texel 的情况,这时要准确的再现这个 fragment 的颜色信息,就要综合考虑所有被它覆盖的texel 产生的影响,这就是各种纹理滤波方法最基本的原理。但是由于 depth map 是在不断的变化当中,所以不能像一般的纹理那样把各个mip-map 事先计算好放到显存里面。有一种利用 pixel shader 的方法对 depth map 做 bilinear filtering,但是开销很大,在现阶段不具备实用意义。同样的问题在纹理分辨率小于屏幕分辨率的时候仍然存在,这时多个 fragment会被投射到同一个 texel 上面,虽然从再现纹理的角度来说并不存在失真,但是由于多个 fragment 共用同一个纹理值,锯齿问题还是存在。更糟糕的是,没有一种滤波技术可以从根本上解决这样的锯齿,因为从数学上讲,人们不可能通过运算来创造出比原始量更多的信息。近年来,为了解决 shadow mapping 的锯齿问题,人们做了很多努力,比较有前景的是 adaptive shadowmap(ASM) 和 perspective shadow map(PSM) 。两者的基本原理都是在可能产生锯齿的地方人为增加采样率,使得一个fragment 至少对应一个 texel , 区别是 ASM 增加采样率的地方是在 shadow 边缘,而 PSM是在靠近视点的地方。修补一个本身存在缺陷的方法从数学上来说是缺乏美感的,正像 John Carmack 在 2002年8月的一封 email中所说:
“ Shadow buffers makegood looking demos with controlled circumstances, but when you startusing them for a “real” application, you find that you need absolutely massive resolution to get acceptable results for omni - directional lights, and a lot of the artifacts need to be tweaked on a per-light basis. While it is possible to do shadow buffers on GF1/radeon class hardware, without percentage closer filtering they look wretched. If wewere targeting on
看起来似乎 John Carmack 找到了实现阴影更好的方法?让我们来看看它究竟是什么。
Shadow volume 的原理:
Shadow volume 这种算法第一次被提出是在Franklin C. Crow 在 1977 年写的一篇论文 “SHADOW ALGORITHMS FOR COMPUTERGRAPHICS ”里。其基本原理是根据光源和遮蔽物的位置关系计算出场景中会产生阴影的区域( shadow volume),然后对所有物体进行检测,以确定其会不会受阴影的影响。
图中的绿色物体就是所谓的遮蔽物,而灰色的区域就是 shadow volume。
只有处于 shadow volume 里面的物体才会受阴影的影响。
shadow volume的算法
现在清楚了 shadow volume 的基本原理,那么如何确定一个物体或者一个物体的某一部分处于 shadow volume 中呢?这就要用到 stencil buffer 的帮助了。
z-pass 算法:
z-pass 是 shadow volume 一开始的标准算法,用来确定某一个象素是否处于阴影当中。其原理是:
Pass1:enable z-buffer write ,渲染整个场景,得到关于所有物体的 depth map 。注意这里的 depth map 和 shadowmapping 里面的区别是 shadow volume 里面的 depth map 是以真实视点作为视点得到的,而 shadowmapping 里面的 depth map 是以光源为视点得到的。
Pass2:disable z-buffer write ,enable stencil buffer write,然后渲染所有的 shadow volume 。对于 shadow volume 的 front face( 既面对视点的这一面 ) ,如果 depth test 的结果是 pass,那么和这个象素对应的 stencil 值加1,否则stencil 值不变。而对于 shadow volume 的 back face(远离视点的一侧 ) ,如果 depth test 的结果是 pass,stencil 值减1,否则保持不变。
用一句简单的话来概括 z-pass的算法就是从视点向物体引一条视线,当这条射线进入 shadow volume 的时候, stencil 值加1,而当这条射线离开 shadowvolume 的时候,stencil 值减1。如果 stencil 值为零,则表示实现进入和离开 shadow volume的次数相等,自然就表示物体不在 shadow volume 内了。
Pass3:第二步完成以后,根据每个象素的 stencil 值判断其是否处于阴影当中(如果 stencil 的值大于零,则这个象素在 shadow volume 内,否则在 shadow volume 的外面),然后据此绘制阴影效果。
在这副图里面,视线三进三出 shadow volume, 最后的 stencil 值为零,表示物体在 shadow volume 外,不受阴影的影响。
这副图里面视线三进一出, stencil 值为 2 ,表示物体在 shadow volume 内,有阴影产生。
这副图里面从视点到物体的视线中止于 shadow volume 前,也就是说所有的 z-test 都是 fail,相应的 stencil 值为零,表示物体在阴影外面。
z-pass 算法缺点及补救办法
以上的讨论都是基于视点在 shadow volume 外面的情况。在这个条件可以得到满足的情况下,z-pass 算法工作的很好,不过一旦视点进入到了 shadow volume 里面,z-pass 算法就会立即失效。
这副图里面的视线二进二出,按照 z-pass的算法,最后的 stencil 值为 0,表示物体在阴影外,可实际上物体是处于阴影内的。错误的原因就在于视点进入到阴影内,使得视线失去了一次进入 shadow volume的机会,让原本应该是 1 的 stencil 值变成了 0 。
Z-Pass 这种错误的行为可以从下图中看出 :
注意地下的影子
Z-Fail 算法:
Z-Fail 算法是 John Carmack,Bill Bilodeau 和 Mike Songy 各自独立发明的,其目的就是解决视点进入 shadow volume 后 z-pass 算法失效的问题。
Pass1:enable z-write/z-test, 渲染整个场景,得到 depth map 。 ( 这一步和 z-pass 的完全一样 )
Pass2:disable z-write, enable z-test/stencil-write 。渲染 shadow volume, 对于它的 back face ,如果 z-test 的结果是fail, stencil 值加1,如果 z-test 的结果是 pass,stencil 值不变。对于 front face,如果z-test 的结果是 fail,stencil 值减1 ,如果结果是 pass,stencil 值不变。
图中所有的 shadow volume 都处在 z-pass 的位置,因此 stencil 值不会改变。
图中所有的 shadow volume 都处在 z-pass 的位置,因此 stencil 值不会改变。
视点在 shadow volume 内也没有问题,最后 stencil 的值是 2, 表示物体在阴影内。
上面那个 Z-Pass 无法处理的场景,用 Z-Fail 计算则可以得到正确的结果: