屏幕空间环境光遮蔽(SSAO)算法的实现

SSAO

 

SSAO介绍

之前写SSAO的时候最后一直没达到想要的效果,最近闲下来又重新写了下,才发现自己之前真的蠢- -!(位置和法线忘转到视口坐标)。这里就好好整理一下这个算法,以免下次想拿起来又不知道怎么入手。

SSAO,即屏幕空间环境光遮蔽(Screen-Space Ambient Occlusion),说到SSAO,在这之前就需要了解AO(Ambient Occlusion),即环境光遮蔽。在很多古老的游戏里,我们所知道的环境光,游戏开发人员通常只给了一个单一的颜色值,但这实际上效果远差于真实场景。比如在我们的房间里,四个角落的光线会远远暗于其他地方。我们的光在散射的时候,在这种褶皱,孔洞中通常会难以反射出来到达我们的眼睛,所以这些地方看着会更加的暗。

如下图:

我们发现相比前两幅图,加上SSAO的第三幅图有更加真实的感觉,没有添加SSAO的图一和图二会给我们一种物体浮在空中的感觉,而图三则会给人正确放置的感觉。

但是环境光遮蔽采用对空间中每一点发射大量光线,并在片元周围几何体计算光线丢失,这会带来极大的性能开销。在2007年,Crytek公司发布了一款叫做屏幕空间环境光遮蔽(Screen-Space Ambient Occlusion, SSAO)的技术,并用在了他们的看家作孤岛危机上。这一技术使用了屏幕空间场景的深度而不是真实的几何体数据来确定遮蔽量。这一做法相对于真正的环境光遮蔽不但速度快,而且还能获得很好的效果,使得它成为近似实时环境光遮蔽的标准。

SSAO的原理比较简单:对于铺屏四边形(Screen-filled Quad)上的每一个片段,我们都会采样周边的深度值计算一个遮蔽因子(Occlusion Factor)。这个遮蔽因子之后会被用来减少片段的环境光照分量。遮蔽因子是通过采集片段周围半球型核心(Kernel)的多个深度样本,并和当前片段深度值对比而得到的。高于片段深度值样本的个数就是我们想要的遮蔽因子。

如上图,图片中灰色的点则是采样点的深度大于该片段的深度(即表面)。因此当灰色的点越多,则表明当前片段点遮蔽因子越大。

 

SSAO算法实现

我们是从给个片段入手计算遮蔽因子,因此我们的每个片段要有如下数据:

  • 逐片段位置向量
  • 逐片段的法线向量
  • 逐片段的反射颜色
  • 采样核心
  • 用来旋转采样核心的随机旋转矢量

通过使用一个逐片段观察空间位置,我们可以将一个采样半球核心对准片段的观察空间表面法线。对于每一个核心样本我们会采样线性深度纹理来比较结果。采样核心会根据旋转矢量稍微偏转一点;我们所获得的遮蔽因子将会之后用来限制最终的环境光照分量。

如上图所示,我用了bunny作为模型实现改方法。该图右边一系列小图,是我们用一个桢缓冲保存相应的数据,我将其可视化,我们称它为G缓冲。右一是深度图,保存了每个片元的线形深度。图二为位置图,保存了每个片元的相对于摄像机的位置。图三为法线图,保存每个片元的法线。图四则是我们自己生成的一个随机转动量(为一个vec2值)。图五则是我们计算出来的ssao图。而我们屏幕中间的图,则是经过模糊的ssao图。

这里有个十分要注意的点,因为这是屏幕空间的环境光遮蔽,因此我们所有的计算都是在屏幕空间上的,所以我们在生成G缓冲的位置,法线,和深度都应该乘上视口矩阵view,使之转变到视口空间下计算。

因此在G缓冲的顶点着色器我们需要做相应的矩阵变换:

因为我们要采样当前片段周围的深度值,因此我们每次都要事先知道当前片元的位置,所以我们在G缓冲需要事先保存位置。

又因为我们需要深度值的对比,所以我们需要获取每个片元的深度值。再加上我们需要半球采样器的法线和在最后给图形着色,应用光照模型(如 Blinn-Phong),所以法线是必不可少的。因此我们G缓冲带的片段着色器:

这里我们使用了一个LinearizeDepth函数,它是将我们的深度值从非线性到线形到转化。它的非线性是由于projection矩阵的关系导致的,在我之前讲的opengl的投影矩阵里有讲解。之后我们用vec4来保存输出的位置(gPositon)其中x,y,z用来保存位置坐标,而第四维a来保存当前的深度。

法向半球

接下来讲解法向半球如何实现,其实这里的法向半球的实现非常简单,我们用一个随机值生成器,生成一个64个样本值的半球采样器。

如图所示,我们随机生成64个vec2采样向量,对其单位化后再对长度进行随机化,我们之后再进行加速插值,使其更加靠近原点。其中lerp为:

我们加速插值后的采样点更加围绕原点:

由于我们所有的片元都应用该采样器,因此避免重复,我们增加了随机核心转动:

这里我们设置了转动值为一个vec3,但其z值为0。为什么z值为0呢,其实因为我们的半球采样器是根据片元的法线,然后应用切空间矩阵(TBN),将我们的半球采用器从切空间转换成视口空间。所以我们只要提供一个x,y的向量和法线向量计算出一个正交基,我们通过该正交基做空间变换时,就能达到随机旋转的效果。这点在后面会更详细的讲解。

 

SSAO着色器

ssao着色器会用到我们之前在G缓冲中保存到数据。我看顶点着色器:

它什么都不做,就是输出铺屏四边形的点以及将纹理坐标发送至片元着色器。

再看我们的ssao片元着色器:

 

我们可以看到,我们的纹理采样器,采样了三张纹理图。分别为gPosition(位置与深度),gNormal(法线),noiseR(随机转动向量)。还传入了半球采样器的64个随机样本。我们还定义了噪声(随机转动向量)缩放比,通过屏幕大小除以噪声图大小以达到纹理能平铺的目的。我们还定义了半球采样器中随机纹理的个数kernelSize,半球采样器的半径radius,以及偏移量bias。

然后在主函数中,我们首先获取纹理中当前纹理坐标的位置(fragPos),法线(normal),随机噪声向量(randomVec)。我们这里使用Schmidt正交化,通过法线和随机转动向量构成TBN矩阵。之后定义occlusion的累加值,并赋初值0。然后我们对半球采样器中的每个样本进行迭代。我们用TBN矩阵将样本向量从切空间转换至视口空间,这里值得注意的是,由于我们在得到TBN矩阵的时候切向量采用的是随机转动向量,并不是和目标片元的几何边沿相对齐的,因此此时的TBN在空间转换的过程自然的完成了随机转动的目的。然后fragPos+samp*radius得出的就是我们最后样本要采样的位置。然而我们要在深度纹理通过纹理坐标采样深度,因此需要将位置坐标进行透视划分。而通常我们在顶点着色器中将裁剪空间的位置交给gl_Position的时候,opengl后续会自动进行透视划分。然而在这个片元着色器我们需要手动的获得透视划分后的坐标,使其变换到值域[0,1]中。之后便可以使用offset.xy对深度纹理取当前深度值。由于要拿深度与当前samp.z进行对比,由于采样的深度是正的,而samp.z在视口空间中(z指向屏幕为正)是负值,所以我们对采样的深度乘上-1。

在遮蔽值的叠加中,我们引入了范围检测。这里我们使用了GLSL的smoothstep函数,它非常光滑地在第一和第二个参数范围内插值了第三个参数。如果深度差因此最终取值在radius之间,它们的值将会光滑地根据下面这个曲线插值在0.0和1.0之间:

当检测一个靠近表面边缘的片段时,如果没加入范围检测,它将会考虑测试表面之下的表面的深度值;这些值将会(不正确地)音响遮蔽因子,如下图

         

其中左图为添加了范围检测,右图无范围检测,我们会发现右图的bunny轮廓上都会有较大的遮蔽值,这显然是错误的。

最后我们用1减去遮蔽值来获得最后的遮蔽权值,该值便可以直接用来和环境光相乘,得到的值就是环境光经过遮蔽的值。

 

SSAO模糊

我们仔细观察上述的ssao图,我们会发现明显的噪声,这是由于我们的噪声(随机转动向量)纹理是重复平铺的,所以我们可以通过对ssao生成的桢缓冲进行模糊。

我们通过上述代码,简单的对每个像素取周围64个像素进行取平均值,来获得最后的像素颜色值。完成如下图。

 

到这里我们的SSAO就都完成了。

  • 6
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 4
    评论
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值