[译]改进阴影深度贴图效果的常用技术

发表于2015-08-20
评论0 5.8k浏览

[]改进阴影深度贴图效果的常用技术

By David Tuft, Software Development Engineer

XNA Developer Connection (XDC)

 

阴影贴图,在1978年首次推出,是一种游戏常用的阴影生成技术。 30年后,尽管在硬件和软件的进步,阴影算法的缺陷,即边缘闪烁,透视走样,以及其他有关精度的问题,仍然存在。

本技术文章提供了一些常见的阴影深度图的算法和常见问题的概述,并介绍了几种方法,包括初级到中级的用来提高标准地图阴影效果方法的难点。讨论普通的阴影投影很简单,但要理解的阴影缺陷的细微差别是具有挑战性的。本技术文章是为那些虽然实现了阴影贴图,但却不能充分理解出现的阴影缺陷,且不知道如何解决它们的中级程序员准备的。

选择特定的技术来减轻特定的缺陷是特殊的。当阴影图的缺陷得到解决,在品质上的差别,可令人印象深刻(图1)。正确地实施这些技术大大提高了标准的阴影。

 1.  左边有缺陷的阴影, 右边是使用本章的技术解决了缺陷的阴影

阴影深度贴图回顾

阴影深度图算法是一个2 pass算法。第一遍生成一张光照空间的深度图。在第二遍,这张图被用来比较每个像素在光照空间中的深度和在光照深度图中的深度。

2. 场景阴影生成的关键

Pass1

场景如图2。在第一阶段(图3),将场景从光源的方向渲染到一个深度缓冲区。更具体地说,顶点着色器将几何体变换到光照视图空间。

第一阶段的最终结果是一个光照方向的场景深度缓存信息。现在可用于Pass 2,以确定从光源开始,哪些像素是被遮挡的

3第一阶段的基本阴影映射

Pass2

在第二阶段(图4),顶点着色器对每个顶点变换2次。每个顶点变换到像机视空间(camera view)并作为位置(position)传入像素着色器。每个顶点还由光源的视点-投影-纹理(view-projection-texture)矩阵变换并且作为纹理坐标(texture coordinate)传入的像素着色器。view-projection-texture矩阵和Pass1中用来渲染场景的矩阵(view-projection)是相同的只是多了一个额外的变换(到texture空间的变化)。这个变换将点从视图空间(XY-11)变换到纹理空间(X01Y1 0)。

像素着色器获得了插值后的位置(position)和纹理坐标(texture coordinate)。现在在纹理坐标中的一切都需要进行深度测试。通过纹理坐标的XYpass1中的深度缓冲中进行索引,然后与位置坐标(position)的Z值比较来进行深度测试。

4第二遍基本阴影映射

阴影贴图存在的问题

阴影深度图的算法是使用最广泛的实时投影算法,但仍有一些问题需要解决。接下来总结一下相关问题。

透视走样

透视走样是一个常见问题,如图5所示。问题发生在将像素从视空间中映射到阴影贴图上但不是11比例时。因为靠近近裁剪面的像素聚集的更密集,需要更高精度的阴影贴图来进行采样。

6显示了阴影贴图和视锥。靠近眼睛部分,像素更加密集,很多像素映射到同一个阴影贴图的像素上。远平面上的像素发散了,因此降低了透视走样。

5. 高透视走样(左)对比低透视走样(右)

对左边的图片来说,透视走样更严重,很多眼空间的像素映射到了相同的阴影贴图上的像素上了。在右边的图片中,透视走样就不明显,因为右边是11映射的眼空间像素和阴影贴图像素。

 6. 视锥体和阴影贴图

远裁剪面的浅色的像素表现出低透视走样,在近裁剪面的深色像素表现出高透视走样。

阴影贴图虽然可以使用很高的纹理精度来降低走样问题,但无论如何在处理很小物件的时候,比如电话线,仍然可能导致看不到影子。同时使用太高的纹理精度可能会导致纹理存取上的一些性能问题。

透视阴影贴图(PSMs Perspective shadow maps)和光空间的透视阴影贴图(LSPSMs light space perspective shadow maps)试图通过偏移光的投影矩阵来让更多的像素靠近眼睛来解决走样问题。但实际上还没有一种技术能真正解决透视走样。将眼空间像素映射到阴影贴图上的变换不一定非要由一个线性偏移来完成。比如一个对数化的。PSMs将太多细节放到了眼睛附近,导致远处阴影质量太低甚至消失。LSPSMs在增加眼睛附近细节和保留足够远处细节方面做的更好一些。2种方法在某些场景情况下都退化为了正交投影。为视锥每一个面都渲染一个独立的阴影贴图就可以抵消这样的退化,虽然这样代价很大。

对数透视阴影贴图(LogPSMs)也将视锥各个面渲染到独立的贴图中。该技术使用非线性光栅化来将更多的像素放置在靠近眼睛的地方。但目前D3D10~D3D11都还没有在硬件上支持非线性光栅化。

层次投影(CSMs)是当前处理透视走样最流行的方法了。虽然CSMs可以和PSMsLSPSMs协同工作,但没有必要。

投影走样

投影走样相对透视走样来说还不太好展示。图7中标记的地方就是投影走样的错误。投影走样发生在当映射摄像机空间的像素到光空间时候不是11比例的时候。原因是几何体和摄像机的朝向关系。当几何体的切平面和光线方向变得平行时发生投影走样。

 7.  高投影走样vs低投影走样

阴影斑块和不正常的自阴影

阴影斑块(Shadow acne ( 8), 错误自阴影的代名词, 发生在阴影贴图对整块像素深度做量化的时候。当着色器用实际深度做对比时,看上去像是有自阴影,但实际上并没有被投影。阴影斑块的另一种情况是光空间的像素深度和深度图中的深度太接近由于精度错误导致深度测试失败。一种原因是深度图是由硬件的固定光栅化硬件计算的,但采样对比的深度又是由shader计算的。投影走样同样会造成阴影斑块。

 8. 阴影斑块问题

上图左边的图片,一部分像素深度测试失败,表现出了斑点和摩尔纹。为了降低自阴影错误,在光空间视锥的远近裁剪面的范围上应该尽可能的小(不要有太长的景深,保证深度测试正确)。基于斜度比率深度偏差(slope scale-based depth bias)和其他类型的偏差(bias)可以减轻阴影斑块。

彼得平移

彼得平移的术语来自一个儿童读物中描述的某个娃娃的影子被分离了,从而可以飞翔。这个问题导致物件的阴影从物件身上脱离了,感觉物件是悬浮在地表之上的。(图9

 9.  彼得平移问题

左边的图片,影子脱离了物件,给人一种浮空的效果。

一种解决表面斑块的方法就是给光照空间的像素一些额外的偏移,即增加一个深度偏移。彼得平移就是当深度偏移太大后造成的。上面的情况中深度偏移导致深度测试非正常的通过了。对于出现阴影斑块问题来说,如果深度缓存精度不够,通过增加深度偏移的方式,反而会加剧彼得平移的情况。计算紧凑的远近裁剪面有助于避免彼得平移问题。

改进阴影贴图的技术

添加阴影效果是逐步进行的。第一步是先得到基本的正常的阴影贴图效果。第二步是确保所有基本计算都是优化的:视锥大小合适,远近裁剪面满足最小距离,使用了斜度比例偏移等等。一旦基本的阴影搞定,效果看上去差不多了,那么开发者可能需要一些来让阴影更逼真的算法。本节已经给出了一些基本提示来让阴影更好看。

斜度比率深度偏差

上面提到的,自阴影可能导致阴影斑块。但使用过多的偏移又会导致彼得平移。而且,多边形的斜面(相对光的方向)倾斜越大可能会更多的受到投影走样的影响。因此,每个深度映射的值都需要针对多边形向光面的斜率来进行不同的偏移。

Direct3D 10的硬件可以支持多边形基于视口方向的斜率偏移。这个可以对多边形在光线方向上竖直偏移许多,但却不能对多边形在面向光的方向直接偏移。

10表示了2个相邻像素在做有相同的斜率测试时,在有阴影和没有阴影的情况下如何变化的。

 10.  斜度比率深度偏差和非偏差深度对比

计算紧贴的投影

将光投影紧紧的嵌入视锥中可以增加阴影贴图的有效范围。图11显示了任意投影下将整个投影嵌入场景边界中,得到了更高的透视走样(效果更差)。

 11.  任意投影视锥和适合场景的阴影视锥

上图是光源的视野。梯形表示的是相机视野的截锥体。图片上的网格表示阴影贴图。右边的图片展示了相同精度下的阴影贴图如果更紧凑的覆盖场景将有更多的有效像素。

12展示了合适的视锥。为计算投影,视锥的8个顶点变换到光照空间。接下来找出XY的最值。用这些值构建正交投影的边界。

 12.  匹配视锥的阴影投影

也可以用场景的AABB(轴对称绑定盒)来获得更紧凑的边界。但是不建议任何情况下都这样做,因为这样会导致逐帧修改光源相机的投影范围。有许多方法,比如在后面提到的逐像素光源移动(Moving the Light Texel-Sized Increments),在保持光的投影大小在每帧都不变的情况下仍然提供较好的效果。

计算远平面和近平面

远平面和近平面是计算投影矩阵的最后一步。2个平面越近,深度缓存中的精度就越高。

深度缓存可以是16bit24bit32bit,取值范围0~1。通常,深度缓存是定点数,并且在近平面附近精度更高。深度缓存的允许的精确度由近平面到远平面的比例决定。如果有最紧凑的远近平面就可以使用16bit的深度缓存。16bit的深度缓存可以节约内存并且增加处理速度。

一个简单直接计算远近面的方法是将场景绑定盒变换到光空间,最小的Z就是近平面,最大的Z就是远平面。对于多数场景和光源的情况,这种方法够用了。最坏的情况下,可能导致深度缓存精度的明显丢失。图13说明了这种情况。近平面到远平面的距离比实际需要的4倍还多。

13中的视锥应该更小一些。一个很小的视锥在一个沿摄像机方向有许多柱子延伸出去的大场景中。使用场景的AABB来做远近面选择就不可取了。CSM算法就需要要为很小的视锥计算远近面

 13.  基于场景AABB的远近平面

另外一种计算远近平面是将视锥变换到光照空间,并使用Z的最小值和最大值分别作为近平面和远平面。图14展示了这种方式的2个缺陷。第一,计算太保守了,如下所示视锥范围超过场景几何体。第二,近平面太近,可能导致阴影投影物件被裁剪。

 14. 仅依靠视锥计算的远近平面

正确计算远近平面的方法如图15所示。使用视锥在光照空间的XY的最小最大值来构建一个正交的光照视锥的4个面。这个正交视锥的最后2个面就是近平面和远平面。为了正确找到这2个面,用4个已知的光照视锥的面来裁剪场景边界。裁剪得到的最大和最小Z值分别就是远平面和近平面。

下面的代码来自CascadedShadowMaps11示例。组成世界AABB8个顶点被变换到光照空间。将顶点变换到光照空间来简化裁剪测试。4个已知的光照视锥的平面当作直线来考虑。场景包围盒在光照空间就是6个四边形。这6个四边形可以转为为12个三角形来进行基于三角形的裁剪。这些三角形和视锥已知面进行裁剪(在光照空间XY方向水平和垂直的直线)。当在XY上的交点找到后,三角形在这点被裁剪。所有被裁剪的三角形的最大最小Z值就是远平面和近平面了。CascadedShadowMaps11示例说明了ComputeNearAndFar函数中裁剪是如何进行的。

还有2种计算可能的最小远近平面的算法。这些算法没有在CascadedShadowMaps示例中提及。

1.      即使可以通过场景层次划分或者相对光照视锥独立出物件来计算更小的远近平面,但计算还是显得很复杂。虽然CascadedShadowMaps11示例中没有明确指出,实际上这种方法针对栅格(地图)还是很有效的。

2.      远平面可以由以下最小值来计算:

a)        视锥在光照空间中的最大深度

b)        视锥和场景AABB相交的最大深度

这种方法在使用层次投影时会有问题,很有可能会索引到视锥外面去。这样阴影贴图就可能丢失某些几何体的阴影。

 15. 基于4个光照视锥平面和场景绑定盒相交计算的远近平面

基于像素移动的光源

阴影贴图的一个常见问题是闪烁的边缘。当摄像机移动时,阴影边缘的像素就忽暗忽明。在静态下这个问题看不出来不过在实时交互的情况下,这个问题非常明显。图16标识出了这个问题。图17显示了阴影边缘应该的样子。

闪烁边缘的错误是由于每当摄像机移动,光的投影矩阵都要重新计算。在生成阴影贴图的时候会有细微的差别。以下因素都会影响场景的矩阵。

1.      视锥的大小

2.      视锥的朝向

3.      光源位置

4.      相机位置

每次矩阵改变,阴影边缘也会改变。

 16. 闪烁的阴影边缘

在相机从左向右的过程中,阴影边缘保持不变。

对于方向光,解决这个问题的办法是对XY的最小最大值(组成正交投影范围)做四舍五入到像素大小的增量。这个由一个除法、取整和乘法操作完成。

        vLightCameraOrthographicMin /= vWorldUnitsPerTexel;

        vLightCameraOrthographicMin = XMVectorFloor( vLightCameraOrthographicMin );

        vLightCameraOrthographicMin *= vWorldUnitsPerTexel;

        vLightCameraOrthographicMax /= vWorldUnitsPerTexel;

        vLightCameraOrthographicMax = XMVectorFloor( vLightCameraOrthographicMax );

        vLightCameraOrthographicMax *= vWorldUnitsPerTexel;

vWorldUnitsPerTexel 由视锥范围计算得来,再除以整个buffer的大小

        FLOAT fWorldUnitsPerTexel = fCascadeBound /

        (float)m_CopyOfCascadeConfig.m_iBufferSize;

        vWorldUnitsPerTexel = XMVectorSet( fWorldUnitsPerTexel, fWorldUnitsPerTexel,                                                0.0f, 0.0f );

限制视锥的最大尺寸来获得比较松散的正交投影。一点很重要的是当使用这个方法时,纹理在宽高上要要比实际的大1个像素。这样能保证从阴影贴图之外也能索引阴影坐标

背面与正面

阴影贴图应该是用标准的背面剔除渲染过程,就是避免光栅化那些观察者看不到的物件,并加速场景渲染。另外一个常见选择是渲染阴影贴图使用正面剔除,这样面向观察者的面就被清除了。这样有助于几何体渲染自阴影时在物件背面做轻微的偏移。这种方法有2个问题。

任何对象如果有不恰当的正面或者背面几何体,都会导致阴影贴图出现问题。但是,错误的正面和背面几何体还会导致其他问题,所以最好是先假设几何体正面和背面几何体都是正确的。比如为树叶这样的基于面片(sprite)的几何体创建背面就是不切实际的。

彼特平移和一些物体比如墙面,的底部会出现阴影间隙,原因可能是阴影深度差异太小造成。

阴影贴图-友好的几何体

创建恰当的几何体有利于解决类似彼得平移和阴影斑块。自阴影的生硬边缘也是个问题。在尖端的边缘上深度差别很小,即使一个很小的偏移也会导致物件丢失自己的影子。(图18

 18. 低精度深度偏移导致生硬边缘的填充问题

狭窄的物体比如墙也会有背面,虽然不可见。(渲染背面)这样可以增加深度差异。

同样保证几何体面朝向是正确的也很重要。即,物体的外面应该朝向背面的,物件里面应该朝向前面的。这样在渲染有背面剔除时,也能很好的处理深度偏移效果。

总结

本文的几种技术都可以增加标准阴影贴图的的质量。下一步是关注那些可以提高标准阴影贴图的技术。推荐CSMs,解决透视走样问题非常优秀。最近百分比过滤(PCF)在处理透视走样上也很有用。PCF或者方差投影都可以产生软阴影

 

如社区发表内容存在侵权行为,您可以点击这里查看侵权投诉指引