注:翻译只求意思准确,省略了一些不太相关的内容,如有错误烦请指正,原文:http://developer.download.nvidia.com/whitepapers/2007/SDK10/VarianceShadowMapping.pdf
Variance Shadow Mapping
Author:Kevin Myers kmyers@nvidia.com
译:清风 http://www.cppblog.com/zzxhang/
Motivation:
在Dx10级别的硬件下VSM能被完美支持,如果不能对fp32纹理过滤,算法就只能在fp16精度下完成,这是不够的,会让计算在数值上变得不稳定,此外当渲染阴影图时还可以打开multisample anti-aliasing(MSAA)改善生成的阴影图质量
How Does It Work?
Shadow maps首先在光源视角上把场景的深度信息渲染到一张纹理中去。完成后这张纹理就包含了所有最靠近光源的像素片段。之后渲染场景时我们就可以查询这张纹理用于判断当前进行着色的像素片段是不是被阴影图上的像素片段遮挡了。通过Percentage-closer filtering(PCF)进行一定数量的纹理查询可以得到软阴影。
PFC的问题在于没有一个好的方法能对深度值进行预采样,如果你武断地对深度图进行一些特定的采样方法,如anisotropic filtering或者mip-mapping,那么你得到的深度值就不是一个正确的遮挡值,而是好几个可能的遮挡值的平均值,用这个值来进行判断就会造成图像失真。
Summary of the Algorithm:
你可以在由William Donnelly 和 Andrew Lauritzen完成的最原始的VSM论文http://www.punkuser.net/vsm/中看到细致的算法理论阐述。
简单地说,VSM的作者在研究中进行PCF得到的是在PCF采样区域中深度值大于我们当前渲染像素深度的像素的百分比的一个边界值,换句话说我们在拿一个单独的深度值和采样区域的整体分布进行比较,我们想知道的是在这个区域中有多少百分比的深度值是大于这个单独的深度值的,我们只关心采样区域深度分布的百分率而不是单独的采样值,通过切比雪夫不等式,我们能用平均值(或者说期望E(x))和分布的方差求出这个百分比的边界值。
![](http://www.cppblog.com/images/cppblog_com/zzxhang/clip_image002.jpg)
平均值是很容易通过深度图得到的,我们可以通过mip-mapping或者separable blur在shader中过滤得到,方差也一样很简单,通过平均值E(x)和平方的平均值E(x2)就能算出来:
就象我们之前说的那样,平均值是很容易得到的,而平方的平均值我们可以只需要把深度图的格式改为双通道纹理并把深度的平均值渲染进第二个通道。在照亮场景前我们仍然可以随意进行一些过滤,之后简单的一次纹理查询就可以计算出平均值和方差。
![](http://www.cppblog.com/images/cppblog_com/zzxhang/clip_image003.jpg)
VSM Shader:
这是在DrawScene.fx的shader代码:
首先注意最后的max语句,如果当前片段的深度值小于或者等于平均深度值,即t<=E(x),那么方程1已经不适用了,图2展示了如果我们不做t >= E(x)的限制从切比雪夫方程中得到的结果,注意在场景中模糊区域是怎样从被照亮区域到阴影区域的,如果你对此有困惑,先想想导致方差改变的因素是什么,分布区域包含值域的范围越广,方差的值就越大,深度值范围的剧烈变化(如轮廓区域)会使得方差的值变得比较大,在图3中你能更清楚地看到这一点,轮廓区域是方差迅速增加的地方,回头看我们的方程这些区域会使得我们方程的结果趋向于1,在轮廓的另一边方差会迅速减少,此时影响方程值继续增大的因素就是另一个变量了(t – E(x)),在图4中这个变量的值用绿色来形象化显示,注意跟阴影区域相反的颜色变化是怎么发生的,当靠近轮廓边缘时采样的深度值越来越接近像素片段深度值,(t – E(x))迅速减少。
![](http://www.cppblog.com/images/cppblog_com/zzxhang/clip_image004.jpg)
![](http://www.cppblog.com/images/cppblog_com/zzxhang/clip_image005.jpg)
![](http://www.cppblog.com/images/cppblog_com/zzxhang/clip_image007.jpg)
![](http://www.cppblog.com/images/cppblog_com/zzxhang/clip_image006.jpg)
Performance:
当加上separable blur后VSM的效果真的很炫,这是用1024x1024的VSM在GeForce 8800 GTX上用1024 x 768的分辨率跑例子程序的结果:
![](http://www.cppblog.com/images/cppblog_com/zzxhang/clip_image008.jpg)
Integration:
把已经在用shadow map的程序改成用VSM还是比较简单的,首先深度图必须变为R32G32F格式的纹理用来存深度值和深度值的平方,当写深度进纹理时我们不必使用投影后的深度值,直接把像素点和光源的线性距离作为深度值更好。最后在shader中必须把原先的光照算法改成使用VSM中的数学方法计算阴影。
当深度复杂性很高的时候VSM存在光照溢出(light bleeding)的错误,最初的VSM论文中证明了当遮挡体和接收体都比较平坦的时候VSM算法能计算得很正确,但如果情况不是那样,剧烈的方差变化可能会导致方程返回一个错误的结果,照亮了不该照亮的地方。
精度也是一个需要控制的问题,就算使用fp32的精度仍然可能不够因为我们要存贮深度的平方值,在例子程序你可以通过改变光源距离(滑块控制)看到这个问题,当光源距离增加时遮挡体的深度值也越来越大,然后我们深度值的平方很快就遇到了精度问题。通过改变深度值的缩放值能一定程度上减轻这个问题,实际上在大多数场景中都可以通过把光源视锥体的大小限制在一定范围内控制这个问题。
由于在VSM中处理的是Z值的分布而不是一个简单的深度测试,因此VSM的一个改进是不再需要z-offset来避免Z值过于靠近而产生的Z-fighting问题(译者:但是仍然需要一个varance-offset…)
译者: 本人把dx sample中的shadow map改成了使用VSM,1024 X 1024的shadow map,3 x 3 PCF,下面是VSM版和原版的效果图对比(为了突出阴影做了些改动):
dx sample:
![](http://www.cppblog.com/images/cppblog_com/zzxhang/vsm2.JPG)
vsm:
![](http://www.cppblog.com/images/cppblog_com/zzxhang/vsm1.JPG)
posted on 2009-10-20 22:52
清風 阅读(4873)
评论(8) 编辑 收藏 引用 所属分类:
图形