« FBX、DAE模型的格式、导入与骨骼动画[无题]蹲地上画圈圈中 »

shader复习与深入:Screen Space Ambient Occlusion(屏幕空间环境光遮蔽)

Screen Space Ambient Occlusion (SSAO) 是一种以后处理的方式模拟出场景接受环境光遮挡情况的图形渲染技术。直观地说,经过SSAO处理的场景,在其(接受环境光照较少的)被遮蔽处会呈现出些许明显的局部自阴影,进而提升场景整体的光影真实感。——ZwqXin.com

[shader复习与深入:HDR(高动态范围)]
[shader复习与深入:Depth of Field(景深)]

本文来源于 ZwqXin (http://www.zwqxin.com/), 转载请注明
      原文地址:http://www.zwqxin.com/archives/shaderglsl/review-screen-space-ambient-occlusion.html

传统的离线渲染光照模型(光线跟踪、辐射度)中,全局光或通过上帝视觉或通过万物视觉进行庞大的一连串运算,来产生出对场景光照情况的趋真实模拟,这在实时渲染中还是远未到能直接应用的。在传统至今的实时渲染中,首先把光照这个复杂问题分解成光与影两部分,前者通过简化的光照模型(如[Shader快速复习:Per Pixel Lighting(逐像素光照)])模拟出各屏幕像素接受光源照亮的程度,注意这里每个像素点都是独立计算的;后者常通过阴影算法(如[Shadow Volume 阴影锥技术之探Ⅰ] [Shadow Map阴影贴图技术之探Ⅰ] )给场景覆盖上一层由实质遮挡物件产生的阴影层,这里每个像素点接受了其入射光线路径上的遮挡点的阴影贡献。无论是光照模型还是阴影贡献,都是非视点独立而且场景像素点都忽略掉上述离线渲染模型中的重要着色要素:四周环境给予的贡献值(除了那略粗暴的直线遮挡点)。而这就触发了实时渲染领域中一系列以对全局光照(Global Illusion)的近似为目的、足够逼近且足够廉价的图形学技术。

而这其中,Screen Space Ambient Occlusion (SSAO)就是很具代表性的一项后处理技术。首先,它基于视点相关这一前提,直接处理眼睛所能看见的像素范围,所以可以在屏幕空间处理,处理主体从上述离线渲染中的“三维场景点”转变为屏幕范围内的“二维像素点”,复杂度大大降低;其次,像素点的“四周环境”被抽象成对极有限的范围内场景点的极有限采样(加上“逐像素”的特质让它看上去更类似辐射度算法的模式),复杂度进一步大大降低;最后,它计算的核心是AO(Ambient Occlusion),即像素的遮蔽值,并不考虑四周环境给予的颜色等其他贡献而仅仅考虑被四周环境遮蔽程度——这是SSAO算法的核。

SSAO

最早实现并让SSAO这种技术进入大众眼球的,正是CryTek在2007年推出的划世代画面级游戏Crysis(孤岛危机)。虽然听说现在CryTek使用的也是改良后的SSAO算法,但现在各种各样的SSAO算法的实质其实都是一致的,或者说都是这原始的CryTek SSAO的改良型。

AO(Ambient Occlusion)是环境遮蔽值,注意这跟Shadow中的遮挡不一样。譬如房间的一个角落点,我们站在房间内注视它时,它不受任何东西遮挡,不会被投射阴影,但是它连接着三面墙体,连接处各面墙体都会部分遮挡住该角落点能接受的环境光,使得它显得更暗一些,产生局部的自阴影,这时就可以说这三面墙在连接处的各一小部分墙体对角落点产生了环境遮蔽。不妨假设产生遮蔽的场景点的集合是以角落点为球心的一个半径为r的球体,球体内各点x对中心的遮蔽值贡献为f(x),则中心点的遮蔽值就是这个球体对f(x)的球体积分。依据上述SSAO的第二个特性,我们让这个r足够小,然后在这个球体中进行有限采样,计算出采样点的f(x)后,取其统合值得到AO值(其实就是一个离散化过程了)。当然,采样的数量越大效果是越接近真实的,但是运算量会越大,其实对实时渲染来说,只要采样数达到一定程度,就能产生比较让人信服的效果了(这当然也是SSAO得以被使用得如此广泛的原因)。

根据遮挡的定义,判断一个场景点A有没有受周围某个场景点B的遮挡,应该是先判断向量AB是否与A点的光线出射方向(法线向量)同侧,如果非同侧则不具备遮蔽光线的条件;如果同侧,则两向量夹角越小,遮挡程度越大;同时,两点距离越小,遮蔽贡献也越大。在屏幕空间下,我们能进行采样的也就目标像素点的相邻像素了,为了进行上述的运算,还需要知道每个像素的法线值和位置值。在deffered渲染下这类信息应该是很容易获得的,但是如果是forward渲染下呢?如果要获得屏幕上各点的法线和位置信息并归集到纹理上,这是很不现实的,所以就必须依靠在进行后处理前已有的ColorBuffer和DepthBuffer重构这些信息。ColorBuffer是无法还原这类信息的,所以可以依靠的只有DepthBuffer了(怎么得到场景渲染的深度缓冲?只要渲染场景的FBO同时绑定一张深度纹理就可以了,FBO相关内容见[学一学,FBO] )。

当然也可以根据屏幕坐标和深度反算视图坐标系下的坐标(通过逆投影矩阵),但是比较直接的方式是先直接取出投影运算中z坐标的运算公式反转计算视图坐标下的z值。首先你得知道正常的流水线中,DepthBuffer里的z值是怎么来的([乱弹OpenGL中的矩阵变换(上)] ):场景顶点经过一些列变换到达投影空间后,在栅格化(Rasterization)之前完成裁剪和透视相除(坐标除以w值)后得到的z,栅格化后,将作为深度写入深度缓冲(当然了,会先经历深度比较)。把深度缓存中的z称为zdepth,裁剪(投影)空间下的z称为zclip,视图空间空间下的z称为zview,则根据透视投影矩阵(mtproj)的计算,有:

zdepth= zclip / wclip =  (mtproj[10] * zview + mtproj[14]) / (- zview

由此可解得视图坐标系下的z坐标为:

zview  =  (- mtproj[14]) / ( zdepth + mtproj[10])

其中,mtproj[10] =  (ProjNear + ProjFar) / (ProjNear - ProjFar), mtproj[14] = (2 * ProjNear * ProjFar) / (ProjNear - ProjFar),Near和Far是投影中的近远裁剪面距离值了。如此,我们就能根据深度缓存重建视图坐标的z值了(x和y值根据平顶透视锥的视角度和相似三角形定理即可解)。必须要提的是,深度缓存中的z值在进行投影变换时,形象化地说是视锥从后往前压缩,会导致透视相除后z的值的精度会从前到后快速地降低(精度丢失),所以深度缓冲的值也是z值越大精度越小(提高ProjNear 的值会稍微改善但也有限),这样,经过上述重构出来的zview,在远离摄像机的点会有越来越大的误差。算了,将就着用吧。

 至于像素的Normal,只能更近似地用用面法线来代替了,可直接利用glsl中提供的dfdx和dfdy来计算(同一个面内部的各位置切向量是相同的,但在边缘处必然是不连续的):

glsl代码
  1. vNorm = normalize(cross(dFdx(vPos), dFdy(vPos)))  

这样我们得到了屏幕空间中的像素在视图空间下的坐标和粗略的法线信息了,接下来使用这些信息去计算SSAO的核。根据上述的讨论,在屏幕空间,针对每个像素点,在其半径为r的圆内随机采样一定数量的二维采样点,根据场景的深度数据重建两点的位置数据和法线数据,计算两个因子核和遮蔽因子:

glsl代码
  1. float fCoffNorm = max(0, dot(vNorm, VecAB) - fbias);  
  2. float fCoffDist = 1.0 / (1.0 + fDist * fDist);  
  3.   
  4. float fOcclusion = fCoffNorm * fCoffDist;  

 这里的fbias(可以直接取0.01,或进行外部控制)是为了避免采样点刚好对目标点形成完全遮挡时(点乘结果为1)产生的自阴影效果,因SSAO注重的是遮蔽关系。当然这里也可以省略掉法线的因子核(毕竟这里只是近似的法线数据,Crytek最初的实现好像也是没考虑法线的),我觉得结果来说也是可以接受的。或者从视觉相关这点考虑,设光线从视点出发,距离因子只考虑深度分量上的非负距离(fDist = max(0, B.z - A.z))则可以滤掉在目标点的“后面”的采样点,在一些接近静态的场景也不会有什么问题。

shader复习与深入:Screen Space Ambient Occlusion(屏幕空间环境光遮蔽)

(原场景【点击看原图】)

shader复习与深入:Screen Space Ambient Occlusion(屏幕空间环境光遮蔽)

(SSAO处理过的场景【点击看原图】)

shader复习与深入:Screen Space Ambient Occlusion(屏幕空间环境光遮蔽)

(对应的AO图【点击看原图】)

SSAO最后得出的各像素的遮蔽因子,也就是各采样点的遮蔽因子的均值。采样点的选取须满足均衡的随机,可以预先准备随机纹理进行检索,也可以利用柏林噪声等生成随机数,但这些方法其实对均衡性没有一定保障,所以往往要增加采样数量(运算量)来平衡。稍好的改进是得到一个随机采样向量(目标点到采样点的向量)后进行一定旋转和缩放来得到另一个或几个采样向量,以取得一定的均衡性。但这样的结果还是会过分依赖对像素集采取的随机方式(若是伪随机,画面感就会很微妙),而如果先均衡出采样向量,再对这些采样向量进行随机扰动,效果就会好很多。

glsl代码
  1. // vPos - 目标点位置  
  2. int nSampleCount = 5;  
  3.   
  4. int SelAng[5] = int[](1, 4, 3, 0, 2);  
  5. int SelDist[5] = int[](0, 4, 1, 2, 3);  
  6.   
  7. float fSelAng = const_2pi / nSampleCount;  
  8.   
  9. float fSelDist = 1.0 / nSampleCount;  
  10.   
  11. for (int i = 0; i < nSampleCount; ++i)  
  12. {  
  13.     int n = SelAng[i];  
  14.     int m = SelDist[i];  
  15.   
  16.     vSampleVector = vec2(cos(fSelAng * n), sin(fSelAng * n)) * (fSelDist * (m + 1)) * fScaler  / vPos.z; //  
  17.   
  18.     vSampleVector = Rotate(vSampleVector, const_pi * rand(vPos.yx));  
  19.   
  20.      //...  
  21.      fAOFractor += ...;  
  22. }  

这里取5个采样点为例(对于传统的SSAO这采样数必定大大的不够,但是对后面的改良型我都是暂取这样的定式,后面再详述),构建两个乱序的index数组(其实最好是应用端作为uniform传入)作为采样向量的角度和长度的index,构建采样向量vSampleVector(fScaler  / vPos.z是半径长度,这首先是按视图空间下的线性深度大小缩放的,因为是在屏幕空间的采样,所以采样半径也该按距离进行透视缩放,以保证目标点的采样点总是近旁的点,fScaler应作为外部uniform参数,以适应不同的场景下对均衡采样半径的控制)。得到采样向量后,以一个随机角度(这里是依赖于目标点xy位置的伪随机角度,也可以用更好的方式)进行二维旋转,形成扰动。这样的结果是相当于对一个螺旋采样集进行扰动:

shader复习与深入:Screen Space Ambient Occlusion(屏幕空间环境光遮蔽)

接下来介绍两个比较受注目的SSAO改进型。

Volume Obscurance

VO(Volume Obscurance),或者说SSVO,收录于犹他大学CS院文献<Volume Obscurance>(2010),是一种采用线积分形式代替传统SSAO的球/半球积分形式以计算遮蔽值的改进型。它在Toy Story 3中的使用也让这个改进方案广为人知,许多游戏包括Cysis也纷纷采用。相比传统SSAO,VO可以以更少的采样达到更好的效果。

其实质是积分形式的改变。如前述,目标点的四周形成球体,球/半球积分的离散化通常是在三维的球体内随机的点采样再加合起来;而球体的的线积分则是以垂直屏幕的平行线分割球体,形成互相平行的圆面,再加合圆上的遮蔽值,所以其离散化是随机采样不在同一个圆面上的点——这正是上述的螺旋扰动式均衡采样的特点。对于采样点所在圆面的遮蔽值的计算,因采样点的垂直位置对于圆面来说也是随机的,所以可以近似为该采样点对所在垂直线的覆盖率。

再次设目标点为A(x,y,z),采样点为B(x,y,z),对于半径r的目标球体,B点所在xy位置对应的球内垂直线段长为:

Zlen = 2 * Zsample = 2 * sqrt(r * r - B.x * B.x - B.y * B.y)

其中Zsample为半长,采样点B的覆盖率则可以如此计算:

Occupation = max(min(B.z - A.z, Zsample) + Zsample, 0.0)  /  Zlen

这里的max和min确保了采样点在球体范围外时,若在目标点前面则覆盖率恒为1,若在后面则恒为0,若在球体范围内,则覆盖率从后往前递增——按前述,这个覆盖率间接表达了采样点所在圆面对目标点的遮蔽值(理论上需要考虑采样点处圆面对应的球体体积,或者分配采样点非线性的半径使各采样点能对应相等的体积,但考虑复杂度这里就忽略罢了)。注意这里也是视点相关地取深度方向的z值作为覆盖率计算的依据(分割线垂直屏幕),没有另外涉及法线数据。论文也仅仅提及可以结合法线数据把圆限制成半圆(法线方向的半圆,即先选择性抛弃落到半圆后方的采样点),但我个人觉得这个对于VO来说不太必要。

 应用VO的一个比较明显的问题是上面覆盖率的计算中没有明确排除掉落到目标点的球体范围外的采样点,而只是给予0或1值,这就导致如果采样点虽在屏幕平面上离目标点很近但深度却相差很大——譬如物件的边缘上的目标像素点,其边缘外侧的采样点在相距很远的别的物体上(同样,对于边缘外侧别的物体的目标点,其部分采样点在这个物件上),同样会把采样的结果加合到最终的遮蔽率上。在画面上的结果就是物体会向距离甚远的物体产生自局部阴影——AO渗漏。这是严重违反实际物理效果的说,SSAO可只是为了产生局部自阴影!那么,把这些外侧的采样点(可根据深度距离比较知道某采样点是否属于这种情况)去掉呢?——这却是违反VO算法的基础点——统合球体各圆面的覆盖率得到球体的覆盖率即遮蔽值。实际上,对于这些采样点,把其覆盖率改为0或0.5或任何固定值会造成物体的白边/黑边效果,因为这相当于不分是非曲直地硬修改覆盖率,破坏了采样的意义。

shader复习与深入:Screen Space Ambient Occlusion(屏幕空间环境光遮蔽)

(AO渗漏【点击看原图】)

VO中解决此问题的方法是Paired-Sample。对于每个采样点B,另外再产生一个关于目标点对称的采样点C,B和C称为Paired-Sample(采样对)。这样,如果采样点B的z值与目标点相差过大,则取1减C的覆盖率的值(Ob = 1 - Oc,因为是对称的,覆盖率的计算参数等等都相同,若直接相加得出的结果为0,互相抵消),这样会抹掉B造成的过亮/过暗的局部,但也会变相取消掉C的影响(欠采样)。根据C与目标点的距离作一定调和(Ob = 1 - a * Oc)可以使得结果保留部分C的影响,最后的整体效果就会比硬修改覆盖率的方式好很多。

shader复习与深入:Screen Space Ambient Occlusion(屏幕空间环境光遮蔽)

(Paired-Sample处理后【点击看原图】)

shader glsl代码
  1. fAOFractor += 0.5;  
  2.           
  3. for (int i = 0; i < nSampleCount; ++i)  
  4. {  
  5.     //...  
  6.         //sample1 (vRes1): vTexcoord + vSampleVector  
  7.         //sample2 (vRes2): vTexcoord - vSampleVector          
  8.   
  9.     if(vDist1 < fUniformBias && vDist2 >= fUniformBias)           vRes2 = vRes1 = (fUniformBias - vDist1) * vRes1 / fUniformBias;  
  10.     else if(vDist1 >= fUniformBias && vDist2 < fUniformBias)  vRes1 = vRes2 = (fUniformBias - vDist2) * vRes2 / fUniformBias;  
  11.     else if(vDist1 >= fUniformBias && vDist2 >= fUniformBias)vRes1 = vRes2 = 0;  
  12.   
  13.     fAOFractor += (vRes1 + vRes2);  
  14. }  
  15.   
  16. fAOFractor /= (1 + nSampleCount * 2);  
  17.       
  18. fAOFractor = scaler * (1.0 - fAOFractor);  

 这里最开始先加的0.5是目标点(圆心处)本身对遮蔽率的贡献,所以全过程实际的采样数是(1+n*2),n为paired-sample的数目。最后取1减去遮蔽率(遮蔽率越大自阴影程度越大)的值作一定放大(我取的是2.0)以去掉场景中遮蔽较低的地方并突出高遮蔽的地方。这样与以前的SSAO手法相比,VO下的场景通常有更明显的遮蔽对比,只突出遮蔽处的细节其他地方不作影响,不会明显造成场景变暗。

shader复习与深入:Screen Space Ambient Occlusion(屏幕空间环境光遮蔽)

(VO处理过的场景【点击看原图】)

shader复习与深入:Screen Space Ambient Occlusion(屏幕空间环境光遮蔽)

(对应的AO图【点击看原图】)

Scalable Ambient Obscurance

在2011年的AO算法中,最引人注目的是用于Alchemy商业引擎中的AO,名为<Alchemy AO>(2011)。但是这名字明显不怎么样(Alchemy是炼金术的意思嘛,跟AO毫不相关啊),为了让自己研究的东西脱离这个引擎名,Morgan McGuire后来又发表了其改进型<Scalable AO>(2012),名为改进其实并没有实际改变算法理论基础,而是从其他方面优化算法执行的环境(譬如直接计算面法线、用Hi-Z map来提高sample的采样效率之类),当然也不必吐槽加上ScreenSpace后名字缩写变成有点汗颜的SSSAO了。

Scalable Ambient Obsurance是对传统SSAO算法从另一个方向上的改进法(虽然采样也是参考VO作类似的螺旋扰乱型采样),而且更加直接。如果你去看看AlchemyAO的论文,就会发现其实它就是直接对SSAO的球积分决定式,结合它认为合适的因子核计算式,进行一系列推导,得出一个新的计算式而已。在文中,把采样点的遮蔽率计算式称为Visibility Function(可见性函数),距离因子核即fall-off function(下降式函数,意为结果随输入增大而下降),结合法线因子核(还是那个点积)和一些防脱型参数,化简化简化简,得出最后的计算式。其实我觉得它声称的能够以比传统SSAO少得多的采样量完成更高质量的AO效果,前者(更少采样)应该归功于采样方式(螺旋扰动)和类似Lod般进行优化(譬如前文提到的Hi-Z)的效果,而后者(更高质量)也就取决于算法选用的“fall-off function”了,而且从Alchemy到Scalable包括期间作者做的一些演讲稿,这个函数就一直在变(谓之“根据场景自行选择更好的”),所以觉得这个方向到最后都变成了怎样因地制宜选取这个函数(其实就是采样点到目标点的距离的影响因子核)的问题了。

glsl代码
  1. vec3 vCalPos  = estimatePosition(vTexcoord + vSampleVector);  
  2.   
  3. vec3 vVelter = vCalPos - vPos;  
  4.   
  5. float fAbsV = dot(vVelter, vVelter);          
  6.               
  7. float fRes = 0;      
  8.   
  9. fRes = max(0.0, 1.0 - sqrt(fAbsV) / fRadius) * max(0.0, dot(vVelter / sqrt(fAbsV), vNorm) - 0.01);  
  10.       
  11. //fRes = 4.0 * max(0.0, 1.0 - fAbsV / fRadius / fRadius) * max(0.0, dot(vVelter, vNorm) - 0.01);      
  12. //fRes = 2.0 * float(fAbsV < fRadius * fRadius) * max(dot(vVelter, vNorm) - 0.01, 0.0);  
  13. //fRes = 5.0 * max(0.0, pow((fRadius * fRadius - fAbsV), 3)) * max(0.0, (dot(vVelter, vNorm) - 0.01) / (fAbsV  + 0.01)) / pow(fRadius, 6);  
  14.   
  15. fAOFractor += fUniformScaler * fRes;  

这里把几种作者提及过fall-off计算式列出来了,结合不同的控制参数(譬如fUniformScaler)使用。可看出这些因子核比起上文明显地很是依赖于采样球半径Radius,其中Scalable AO文中最后列出的就是最后一个了,不过我是觉得这个不仅计算量大而且对我测试用的场景也没法很好适用就是了。注意Scalable AO用到的确确实实的距离值而不是单纯的深度方向的距离,而且适用了面法线信息(通过dxdy像素位置值计算出来的),会让场景结果出现更多细节(同时引入不少的边缘不连续感)。

shader复习与深入:Screen Space Ambient Occlusion(屏幕空间环境光遮蔽)

(SAO处理过的场景【点击看原图】)

shader复习与深入:Screen Space Ambient Occlusion(屏幕空间环境光遮蔽)

(对应的AO图【点击看原图】)

就结果而言比起传统的SSAO的效果确实好不少,从论文的例子来看也会觉得这改进后得出的AO图(单由遮蔽率大小构成灰度信息的场景图)很是赏心悦目,也会让人觉得这个方法很棒。但就我个人意见,我们使用SSAO并不是为了得到一幅一幅赏心悦目的AO图(除非我们给场景弄个AO模式),而是通过AO计算知道场景内各像素的遮蔽程度,把结果信息结合到正常场景中,所以像VO那样虽然通常难得到很好看很细节感的AO图,但是其提供的遮蔽信息已经很足够让场景的明暗对比处很鲜明了。事实上,Volume Obscurance和Scable Ambient Obscurance是线积分和球积分两条道上对SSAO的改进,无谓非得比较出个绝对优劣,依个人口味和各自优缺点而选择就好。(如果要说缺点的话,个人觉得前者有点依赖于深度信息的精确度,精度不够时物体边缘有漏边现象,而后者则更容易暴露AO渗漏的问题和面法线造成的不连续——虽然后模糊和叠加到场景后不怎么明显。)

-----------------------------

得到AO pass的结果后,一般是立即对AO结果纹理进行垂直和水平方向上的双边高斯模糊(Bilaterial Gaussion Blur)——比起一般的高斯模糊加入了深度信息的考虑(抛弃深度相差较大的高斯采样点,这样模糊的时候就不会模糊到别的物体上,较好地保留了物体的边缘信息) ,最后与原场景混合。

总结一下,SSAO这种后处理能够增强场景的光影感,间接提高全局光照的真实感,哪怕不过是对物理光照模型的过渡中衍生出来的技术,在当前的实时渲染领域中确实有其重要的作用。本文了介绍一般的SSAO技术后,也介绍了两种当前流行的AO改进法,相信今后还会有更多学者做出更好的适应性更高的改进算法吧~

本文来源于 ZwqXin (http://www.zwqxin.com/), 转载请注明
      原文地址:http://www.zwqxin.com/archives/shaderglsl/review-screen-space-ambient-occlusion.html

  • quote 1.buyaoshuo
  • 看了一下资料原来是已经毕业的校友,握个爪。
    个人感觉AO和GI一起做比较好,但从Kajiya的方程来看,AO根本没存在的意义,但实际上不少现行的GI算法(例如各种VPL like)对visibility都选择不处理,这才使得可以用AO忽悠一个visibility出来,但随着voxel cone tracing等一些能够处理visibility的算法出现,AO估计也就是个过渡吧
  • 2013-10-15 23:08:09 回复该留言

发表评论:

◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。

IE下本页面显示有问题?

→点击地址栏右侧【兼容视图】←

日历

Search

网站分类

最新评论及回复

最近发表

Powered By Z-Blog 1.8 Walle Build 100427

Copyright 2008-2013 ZwqXin. All Rights Reserved. Theme edited from ipati.