转自:http://blog.csdn.net/hippig/article/details/7858574ios
shadow volume 这个术语几乎是随着 DOOM3 的发布而成为FPS 玩家和图形学爱好者谈论的对象的。虽然这个游戏尚未上市,可是凭借 John Carmack 的传奇经历以及 DOOM3发布的一些让人惊讶的预览图片,咱们仍然有理由认为它将会是 2004 年最热门的 FPS 游戏之一。 id software向来都不吝惜为了达到最好的图像效果而使用最早进的渲染技术,这曾经使得玩家为了玩它开发的游戏而不得不掏光口袋里面的钱来升级电脑,不知道此次咱们能够幸免吗?程序员
自DX9 发布以来,你们的注意力彷佛都被 shader 吸引住了, BBS里面谈论的话题也老是离不开 shader based rendering ,前一段时间关于 GPU内部精度的讨论大有遮天蔽日之感,但其实和闪闪发光的金属小球以及波光鳞鳞的水面比较,几个简简单单的影子经常能带给场景更多的真实感。也许这就是为何
DOOM3 可以在多如牛毛的 FPS 游戏中脱颖而出的缘由之一。算法
阴影的实现方法有不少种,如今比较流行的主要是 shadow mapping 和shadow volume. 前者实现起来相对简单,能够发挥如今 GPU 可编程流水线的能力,可是因为先天不足,shadow mapping在处理动态光源/物体的时候开销过大,常常做为一种静态场景中的廉价替代物。而 Shadow volume 的强项偏偏是 shadow mapping 的短处,像 DOOM3 这种大量运用动态光源,而且要对时刻都在运动中的物体投射阴影,shadow volume是现阶段惟一的选择。编程
Shadow mapping 的原理: 一个物体之因此会处在阴影当中,是因为在它和光源之间存在着遮蔽物,或者说遮蔽物离光源的距离比物体要近,这就是 shadow mapping 算法的基本原理。缓存 Pass1: 以光源为视点,或者说在光源坐标系下面对整个场景进行渲染,目的是要获得一副全部物体相对于光源的 depth map (也就是咱们所说的shadow map ) , 也就是这副图像中每一个象素的值表明着场景里面离光源最近的 fragment 的深度值。因为这个 pass中咱们感兴趣的只是象素的深度值,因此能够把全部的光照计算关掉,打开 z-test 和 z-write 的 render state 。app Pass2: 将视点恢复到原来的正常位置,渲染整个场景,对每一个象素计算它和光源的距离,而后将这个值和 depth map中相应的值比较,以肯定这个象素点是否处在阴影当中。而后根据比较的结果,对 shadowed fragment 和 lightedfragment 分别进行不一样的光照计算,这样就能够获得阴影的效果了。性能 |
从上面的分析能够看出来,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中所说:spa
“ Shadow buffers make
good looking demos with controlled circumstances, but when you start
using 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 we
were targeting only the newest hardware, shadow buffers would have a
better shot, but even then, they have more drawbacks than are commonly
appreciated. ”
看起来彷佛 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 和 shadow
mapping 里面的区别是 shadow volume 里面的 depth map 是以真实视点做为视点获得的,而 shadow
mapping 里面的 depth map 是以光源为视点获得的。
Pass2:disable z-buffer write , eNABle
stencil buffer write, 而后渲染全部的 shadow volume 。对于 shadow volume 的 front
face( 既面对视点的这一面 ) ,若是 depth test 的结果是 pass, 那么和这个象素对应的 stencil 值加一。若是
depth test 的结果是 fail, stencil 值不变。而对于 shadow volume 的 back face(
远离视点的一侧 ) ,若是 depth test 的结果是 fail, stencil 值减一,不然保持不变。
用一句简单的话来归纳 z-pass的算法就是从视点向物体引一条视线,当这条射线进入 shadow volume 的时候, stencil 值加一,而当这条射线离开 shadow volume 的时候,stencil 值减一。若是 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 值加一,若是 z-test 的结果是 pass, stencil 值不变。对于 front face, 若是z-test 的结果是 fail, stencil 值减 一 ,若是结果是 pass, stencil 值不变。
图中全部的 shadow volume 都处在 z-pass 的位置,所以 stencil 值不会改变。
视点在 shadow volume 内也没有问题,最后 stencil 的值是 2, 表示物体在阴影内。
上面那个 Z-Pass 没法处理的场景,用 Z-Fail 计算则能够获得正确的结果:
使用 z-Fail 算法的条件
Capping For Z-Fail
因为 Z-Fail 算法依靠计算 shadow volume 不能经过 Z-test 的部分来肯定 stencil buffer 的值,因此要求 shadow volume是闭合的。下面的那张图里面红色的实线表示 capping, 能够想象,假如不人为的添加 capping, 那么 shadow object1/2 的 stencil 值都会是 0 ,而实际上正确的 stencil 值应该是 1 ,由于它们都在阴影内。
Z-Pass 和近剪裁面的关系:
在 Z-PASS 算法中,当 shadow volume 和视图体 (view frustum) 发生剪切关系的时候,须要附加的capping 才能保证最后的结果正确。由于通过view frustum 的剪裁做用之后,shadow volume 的一部分有可能变成敞开的,好比在图中 additional capping 的位置,假如不人为的附加一部分多边形,在渲染 shadow volume 的时候 stencil buffer 就不会发生+1 的操做 ( 由于这里没有任何多边形,天然也就不会和原来的 depth map 比较 ) ,最后的结果显然是不对的。
如何创建 shadow volume?
shadow volume的创建是整个算法里面最重要的部分,在 GPU 出现之前, shadow volume 的创建都是基于 CPU 的。随着 GPU应用的逐渐开展,人们又将 shadow volume 运算移植到了 GPU上,不事后面一种方法须要对物体的几何数据进行预处理,下面就对两种方法分别进行解释:
CPU based method(基于CPU创建方法):
想必熟悉 shadow volume 的朋友对silhouette edge 这个词会很熟悉。它表示从光源的角度看物体所获得的轮廓线。 Shadow volume 就是由silhouette edge 扩展到必定距离之外或者无穷远处获得的。silhouette edge
的肯定方法有不少种,基本思想就是找出那些被朝向相反 ( 一个面向光源,另外一个背向光源) 的两个三角形 ( 相对于光源来讲)所共享的边,由于只有这样的边会最终成为 silhouette edge ,其余的边在光源看来都在物体投影的内部而不是边缘。
这副图是一个由 4 个三角形组成的多边形,假设光源处在读者头部的位置,那么外围的一圈实线就是所谓的 silhouette edge 。咱们所要作的就是从原始数据里面将内部多余的 4 条边 ( 虚线 ) 去掉。具体实现是这样:
# 遍历模型的全部三角形
# 计算 dot3( light_direction , triangle_normal ) 。用这个结果判断三角形是面向光源 (dot3>0) 仍是背向光源 (dot<0) 。
# 对于面向光源的三角形,将全部的三条边压入一个 栈 ,和里面的边进行比较,若是发现重复的 (edge1 和 edge2) ,将这些边删除
#检测过全部三角形的 全部边 之后, 栈 里面剩下的 边就是 当前光源 / 物体位置下面的 silhouette edge.
# 根据光源方向 , 利用 CPU 或者 vertex shader 将这些 silhouette edge 投射出去造成 shadow volume.
值得一提的是,这种方法正是 DOOM3所采用的方案,可是其中有一个问题就是 silhouette edge是由光源和物体的相互位置肯定的,也就是说这两者之间有一个的位置发生了变化, silhouette edge就要从新计算,更新的数据也要传回显卡才能渲染 shadow volume ,这对 CPU 的计算能力以及 AGP的带宽不能不说是一个不小的考验。
GPU based method(基于GPU创建方法):
Vertex shader一出现人们就在思考能不能利用它来加速 shadow volume 的渲染速度。但即便是如今最早进的 vertex shader 3.0也不具有建立新的几何物体的能力。简单点说 vertex shader 只能接受一个顶点,修改这个顶点的属性 ( 位置,颜色,纹理坐标,etc), 以后输出这个顶点到光栅化部分,继而进行 pixel shader 运算。碰到须要建立新顶点的地方,就只有依靠 CPU 直接操做vertex buffer 了。
另一个方法就是事先把 shadow volume须要的空间留出来,而后再经过 vertex shader的运算使以外形达到咱们须要的样子。这就比如我要存储一串数据,但又不很肯定具体的规模是多大,只好事先分配一块很大的区域,这样难免会形成很大浪费,但也是不得以而为之。
因为物体上的每条边都有可能成为 silhouette edge ,因此咱们须要事先插入 degenerate quad( 上图的红色三角形 ), 这些 quad的面积为零,不做任何变换的话是不可见的,不会形成视觉瑕疵。可是在须要的地方,能够把这些 quad 拉伸成为 shadow volume 的侧壁。
显然,插入冗余的顶点会形成极大的浪费。由于大部分的边最终 并不会成为 silhouette edge ,也就是说插入的 degenerate quad是无用的。不过这样作的好处是几何数据只须要传输到显卡一次,以后不管光源的位置在哪里,预处理事后的几何体均可以用来生成 shadow volume ,不像刚才解释过的方法那样一旦光源和物体的相对位置发生变化,就须要从新用 CPU 计算 silhouette edge,以后再把结果 传送给显卡。
实际编程的时候,能够作一下改进,因为平坦的表面是不会产生阴影的,因此在这些表面所包含的边上就不必插入 degenerate quad。并且全部的预处理应该在软件开发过程当中完成,用户启动程序之后直接调用的就是插入过 quad 的模型,不须要 CPU 再进行计算。
创建/渲染 shadow volume 的 shader 代码: // c0 : Light position in object space // c1 : 1, 1, 1, 0 // c2- c5 : Light * View * Proj = LightClip // c6- c9 : WorldInvLight matrix // c10 : Color for exposing the shadow volume vs.2.0 mov oD0, c10 // 输出特定的颜色使 shadow volume 可见 sub r1, v0, c0 // 光源方向 m4×4 r4, v0, c[6] // 将顶点变换到光源坐标系 nrm r1, r1 // 光源向量归一化,这是为了 shadow volume 的各个边同样长 mov r10, c1 dp3 r10.w, v1, r1 //dp3 顶点法向量和光源向量,肯定顶点的朝向 slt r10, c1.w, r10 // 根据 dp3 的结果设置 r10 寄存器的第四个单元 mul r4, r4, r10 // 设定 r4 的 w 位 m4×4 r5, r4, c[2] // 输出顶点到 clip space mov oPos , r5 |
Shadow volume 的算法优化(一)
Shadow volume 的基本算法讲到这里就基本完成了,下面说一下如今比较经常使用的一些优化算法。
(一)Z-Pass .VS. Z-Fail
前面提到过,Z-Pass 比 Z-Fail 速度要快,所以咱们能够在不会产生问题的场合下适当使用 Z-Pass 来提升性能,可是如何肯定什么时候 Z-Pass 不会带来问题呢? Z-Pass 失效主要是因为两种缘由 :
缘由一:视点进入 shadow volume 内,好比下图:
只要能探测出这两种状况,就能在须要的时候切换到 Z-Fail 算法。条件 A 的断定能够参照下图,在视点和光源之间作一条连线,若是这条线和遮蔽物相交,那么能够确定视点在 shadow volume 内,将切换到 Z-Fail 算法。
缘由二:shadow volume 与近 剪裁面相交
至于状况 B 的断定能够利用光源和近 剪裁面 造成的light-pyramid( 红色阴影部分 ) 与遮蔽物的交汇关系。若是遮蔽物彻底在 light-pyramid 以外,则由它生成的shadow volume 不会和近 剪裁面 相交,可使用 Z-Pass 算法,不然将只能使用 Z-Fail 算法。
Shadow volume 的算法优化(二)
(二)tricks to save fillrate :
前面提到过,shadow volume算法里面两个最耗时的步骤就是 silhouette edge determination 和 shadow volume rendering。其中 shadow volume rendering 是彻底考验 GPU 填充率的步骤,虽然如今的显卡动辄就有几十 G fragment/s的填充率能力,可是遇到复杂的场景,流水线也难免不堪重负。此外,频繁的 stencil buffer操做也会占据一部分显存带宽,若是可以找出一些办法尽可能减少 shadow volume 的尺寸,将会是效果很明显的一种优化方法:
限定光照的范围(Attenuated Light Bounds):
若是所用的光源有衰减效应,则能够利用 scissor test 将渲染的范围限定在光源的做用范围以内,由于超出了这个范围就不会有阴影存在,天然用不着去渲染那部分的shadow volume了。所谓 scissor test 就是人为地在屏幕坐标系下面定义一个矩形,只有坐标处在这个矩形范围内的 fragment才可以经过测试,其内容才能被写入帧缓存。
NVIDIA的阴影加速技术(ultra shadow):
ultra shadow这项技术是随着NV35 的发布而浮出水面的,进而在 NV36/38 中获得了继承,咱们基本上能够在 NVIDIA 从此的产品中,这项技术会获得持续的应用。
id software 的当家程序员 John Carmack 曾经说过 NV35 是为 DOOM3 量身打造的 GPU ,咱们在这里有理由怀疑 Carmack说这番话的缘由颇有可能就是因为 NV35 中集成了 ultra shadow 阴影加速技术(近日GeForceFX系列已经成为DOOM3的推荐GPU),那么 ultra shadow 到底是什么,它如何加速阴影的渲染速度呢?
其实 ultra shadow 技术仅仅利用了一个 NVIDIA 新近提交的 OpenGL 扩展—— EXT_depth_bounds_test,咱们先来看一下 NVIDIA 官方在 GDC2003 上对这个扩展的介绍:
首先注意一下名称的问题,GDC2003在三月举行,那时这个扩展还只是 NVIDIA 独家的东西,到了 4 月这个扩展改名为 EXT_depth_bounds_test 。 EXT开头的扩展表示有多家厂商在开发这项技术,也许不久之后咱们就会看到 ultra shadow 在 ATI 的 GPU 上面实现。
Depth bounds test 的做用是比较由当前 fragment 的屏幕坐标( xw , yw )指定的 depth buffer 中的 z 值与用户经过 glDepthBoundsNV(GLclampd zmin , GLclampd zmax )所指定的 [ zmin,zmax ], 若是 z 值在次范围以外,则将当前的 fragment 从流水线中剔除掉,不进行此处的 stencil buffer 操做。注意这里比较的并非 fragment(shadow volume) 的 z 值,而是前一个 path 中已经渲染过的shadow receiver 的 z 值。具体状况请看下图:
能够看到,因为 A 点的 z 值在 [ zmin,zmax ] 范围以外,此点没有可能被阴影遮住,所以 A1/A2 点处的 fragment 就能够被丢弃。而 B 点的 z 值在 [ zmin,zmax ] 以外,因此 B1 点处的 fragment 就必须进行 stencil buffer 操做。
(详细的技术介绍请看:《NVIDIA的复仇计划 GF FX 5900 Ultra》)
阴影渲染实现技术的展望
shadow volume是近阶段实现统一光照模型比较好的一种技术,如今主要的问题是基于 CPU 的方法对处理器依赖比较重,在 AI/ 物理运算较多的场景中 CPU的运算能力可能不足,而基于 GPU 的方法效率过低,会产生大量的冗余顶点,其缘由仍是因为如今的 GPU( 包括即将发布的 NV40/R420)都不具有在芯片内部产生新顶点的能力。 Microsoft 意识到了这一点,在 DirectX Next的发展规划中将这种能力列为了要实现的目标之一:
从更长远的角度来讲,基于真实物理模型的光照模型(好比spherical harmonic lighting、ray-tracing、radiosity)才是发展的方向,那时咱们没有必要设计单独的算法来实现阴影,全部的光照/阴影效果都被包扩在了一个统一的光照模型之中,任何效果实现起来都是天然而然的,就像它们在真实世界中的状况同样。固然,全部这些设想都要基于半导体生产技术的支持才行,咱们在近期(5-10年)将不会看到它们在硬件上的实现。