OpenGL核心之SSAO技术解说(一)

笔者介绍:姜雪伟,IT公司技术合伙人。IT高级讲师,CSDN社区专家,特邀编辑,畅销书作者,国家专利发明人;已出版书籍:《手把手教你架构3D游戏引擎》电子工业出版社和《Unity3D实战核心技术具体解释》电子工业出版社等。

CSDN视频网址:http://edu.csdn.net/lecturer/144

在使用引擎开发产品时,我们常常会使用环境光作为游戏场景的太阳光使用。环境光照是我们添加场景整体光照中的一个固定光照常量。它被用来模拟光的散射(Scattering)。在现实中。光线会以随意方向散射,它的强度是会一直改变的。所以间接被照到的那部分场景也应该有变化的强度,而不是一成不变的环境光。

当中一种间接光照的模拟叫做环境光遮蔽(Ambient Occlusion)。它的原理是通过将褶皱、孔洞和非常靠近的墙面变暗的方法近似模拟出间接光照。这些区域非常大程度上是被周围的几何体遮蔽的,光线会非常难流失,所以这些地方看起来会更暗一些。

站起来看一看你房间的拐角或者是褶皱。是不是这些地方会看起来有一点暗?

以下这幅图展示了在使用和不使用SSAO时场景的不同。特别注意对照褶皱部分。你会发现(环境)光被遮蔽了很多:

虽然这不是一个非常明显的效果,启用SSAO的图像确实给我们更真实的感觉。这些小的遮蔽细节给整个场景带来了更强的深度感。

环境光遮蔽这一技术会带来非常大的性能开销,由于它还须要考虑周围的几何体。我们能够对空间中每一点发射大量光线来确定其遮蔽量,可是这在实时运算中会非常快变成大问题。在2007年。Crytek公司公布了一款叫做屏幕空间环境光遮蔽(Screen-Space Ambient Occlusion, SSAO)的技术。并用在了他们的看家作孤岛危机上。这一技术使用了屏幕空间场景的深度而不是真实的几何体数据来确定遮蔽量。

这一做法相对于真正的环境光遮蔽不但速度快,并且还能获得非常好的效果,使得它成为近似实时环境光遮蔽的标准。

SSAO背后的原理非常easy:对于铺屏四边形(Screen-filled Quad)上的每个片段。我们都会依据周边深度值计算一个遮蔽因子(Occlusion Factor)。这个遮蔽因子之后会被用来降低或者抵消片段的环境光照分量。遮蔽因子是通过採集片段周围球型核心(Kernel)的多个深度样本。并和当前片段深度值对照而得到的。

高于片段深度值样本的个数就是我们想要的遮蔽因子。

上图中在几何体内灰色的深度样本都是高于片段深度值的,他们会添加遮蔽因子;几何体内样本个数越多,片段获得的环境光照也就越少。

非常明显,渲染效果的质量和精度与我们採样的样本数量有直接关系。假设样本数量太低,渲染的精度会急剧降低,我们会得到一种叫做波纹(Banding)的效果。假设它太高了,反而会影响性能。

我们能够通过引入随机性到採样核心(Sample Kernel)的採样中从而降低样本的数目。

通过随机旋转採样核心。我们能在有限样本数量中得到高质量的结果。

然而这仍然会有一定的麻烦。由于随机性引入了一个非常明显的噪声图案,我们将须要通过模糊结果来修复这一问题。以下这幅图片展示了波纹效果还有随机性造成的效果:


你能够看到,虽然我们在低样本数的情况下得到了非常明显的波纹效果,引入随机性之后这些波纹效果就全然消失了。

Crytek公司开发的SSAO技术会产生一种特殊的视觉风格。

由于使用的採样核心是一个球体。它导致平整的墙面也会显得灰蒙蒙的,由于核心中一半的样本都会在墙这个几何体上。以下这幅图展示了孤岛危机的SSAO。它清晰地展示了这样的灰蒙蒙的感觉:


由于这个原因,我们将不会使用球体的採样核心,而使用一个沿着表面法向量的半球体採样核心。


通过在法向半球体(Normal-oriented Hemisphere)周围採样,我们将不会考虑到片段底部的几何体.它消除了环境光遮蔽灰蒙蒙的感觉,从而产生更真实的结果。


SSAO须要获取几何体的信息。由于我们须要一些方式来确定一个片段的遮蔽因子。对于每个片段。我们将须要这些数据: - 逐片段位置向量 - 逐片段的法线向量 - 线性深度纹理 - 採样核心 - 用来旋转採样核心的逐片段随机旋转矢量

通过使用一个逐片段观察空间位置,我们能够将一个採样半球核心对准片段的观察空间表面法线。

对于每个核心样本我们会採样线性深度纹理来比較结果。採样核心会依据旋转矢量略微偏转一点;我们所获得的遮蔽因子将会之后用来限制终于的环境光照分量。

由于SSAO是一种屏幕空间技巧,我们对铺屏2D四边形上每个片段计算这一效果;也就是说我们没有场景中几何体的信息。

我们能做的仅仅是渲染几何体数据到屏幕空间纹理中,我们之后再会将此数据发送到SSAO着色器中,之后我们就能訪问到这些几何体数据了。

假设你看了前面一篇教程,你会发现这和延迟渲染非常类似。

这也就是说SSAO和延迟渲染能完美地兼容,由于我们已经存位置和法线向量到G缓冲中了。

由于我们已经有了逐片段位置和法线数据(G缓冲中),我们仅仅须要更新一下几何着色器,让它包括片段的线性深度即可了。回顾我们在深度測试那一节学过的知识,我们能够从gl_FragCoord.z中提取线性深度:

#version 330 core
layout (location = 0) out vec4 gPositionDepth;
layout (location = 1) out vec3 gNormal;
layout (location = 2) out vec4 gAlbedoSpec;

in vec2 TexCoords;
in vec3 FragPos;
in vec3 Normal;

const float NEAR = 0.1; // 投影矩阵的近平面
const float FAR = 50.0f; // 投影矩阵的远平面
float LinearizeDepth(float depth)
{
    float z = depth * 2.0 - 1.0; // 回到NDC
    return (2.0 * NEAR * FAR) / (FAR + NEAR - z * (FAR - NEAR));    
}

void main()
{    
    // 储存片段的位置矢量到第一个G缓冲纹理
    gPositionDepth.xyz = FragPos;
    // 储存线性深度到gPositionDepth的alpha分量
    gPositionDepth.a = LinearizeDepth(gl_FragCoord.z); 
    // 储存法线信息到G缓冲
    gNormal = normalize(Normal);
    // 和漫反射颜色
    gAlbedoSpec.rgb = vec3(0.95);
}
提取出来的线性深度是在观察空间中的,所以之后的运算也是在观察空间中。确保G缓冲中的位置和法线都在观察空间中(乘上观察矩阵也一样)。观察空间线性深度值之后会被保存在gPositionDepth颜色缓冲的alpha分量中,省得我们再声明一个新的颜色缓冲纹理。


gPositionDepth颜色缓冲纹理被设置成了以下这样:

glGenTextures(1, &gPositionDepth);
glBindTexture(GL_TEXTURE_2D, gPositionDepth);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA16F, SCR_WIDTH, SCR_HEIGHT, 0, GL_RGBA, GL_FLOAT, NULL);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);

这给了我们一个线性深度纹理,我们能够用它来对每个核心样本获取深度值。注意我们把线性深度值存储为了浮点数据;这样从0.1到50.0范围深度值都不会被限制在[0.0, 1.0]之间了。假设你不用浮点值存储这些深度数据。确保你首先将值除以FAR来标准化它们,再存储到gPositionDepth纹理中。并在以后的着色器中用类似的方法重建它们。

相同须要注意的是GL_CLAMP_TO_EDGE的纹理封装方法。这保证了我们不会不小心採样到在屏幕空间中纹理默认坐标区域之外的深度值。

接下来我们须要真正的半球採样核心和一些方法来随机旋转它。


原文地址:https://www.cnblogs.com/claireyuancy/p/7345122.html