10 SSAO

我们之前对环境光的模拟采用的是一个定值,但实际上在墙角,褶皱这些地方光很难流失,这些地方看起来就会暗一些,这时用恒定的环境光就不合适了。针对环境光遮蔽,我们使用 SSAO 技术,来近似地模拟环境光遮蔽。按道理来说我们应当根据真实的几何关系来确定遮蔽量,SSAO则直接根据屏幕空间场景的深度来确定遮蔽量。

原理就是根据屏幕中某个点周围的深度值来计算遮蔽因子,高于片段深度的样本个数就是想要的遮蔽因子,这样的样本越多,该片段的环境光就会越暗。

【上图中,灰色部分的取样点就比片段深度要深,于是加入遮蔽因子】

很明显,渲染的质量与采样数量有密切关系,我们可以通过引入随机性到采样核心(Sample Kernel)的采样中从而减少样本的数目。通过随机旋转采样核心,我们能在有限样本数量中得到高质量的结果。然而这仍然会有一定的麻烦,因为随机性引入了一个很明显的噪声图案,我们将需要通过模糊结果来修复这一问题。

注意我们采取的半球体的采样核心,因为核心中至少有一半的样本都在几何体中。

实现SSAO有很多细节的问题,我们结合代码仔细讨论,同时我们会复习几个着色器的相关内容与一些GLSL的内建变量。

整个着色过程框架如下

glBindFramebuffer(GL_FRAMEBUFFER, gBuffer);
[正常渲染以得到G缓冲]
glBindFramebuffer(GL_FRAMEBUFFER, 0);
glBindFramebuffer(GL_FRAMEBUFFER, ssaoFBO);
[根据G缓冲的内容得到ssao]
glBindFramebuffer(GL_FRAMEBUFFER, 0);
glBindFramebuffer(GL_FRAMEBUFFER, ssaoBlurFBO);
[对ssao进行模糊化处理]
glBindFramebuffer(GL_FRAMEBUFFER, 0);
[最后渲染屏幕即可]

怎么利用 G 缓冲生成 ssao?

# version 330 core
<div style="margin-top: -15px; font-size: 0.75em; opacity: 0.7;">
    文本统计:约 752 个字    82 行代码 
</div>

//我们要输出的结果是遮蔽结果
out float FragColor;
in vec2 TexCoords;

//把相关数据传入
uniform sampler2D gPosition;
uniform sampler2D gNormal;
uniform sampler2D texNoise;

uniform mat4 projection;

uniform vec3 samples[64];

int kernelSize = 64;
float radius = 0.5;
float bias = 0.025;

const vec2 noiseScale = vec2(800.0 / 4.0, 600.0 / 4.0);

void main()
{
    //获取片段位置和法线向量
    vec3 fragPos = texture(gPosition, TexCoords).rgb;
    vec3 normal = texture(gNormal, TexCoords).rgb;
    //获取噪声向量
    vec3 randomVec = texture(texNoise, TexCoords * noiseScale).rgb;
    //利用施密特正交化方法得到一个局部坐标系
    vec3 tangent = normalize(randomVec - normal * dot(randomVec, normal));
    vec3 bitangent = cross(normal, tangent);
    mat3 TBN = mat3(tangent, bitangent, normal);

    float occlusion = 0.0;

    for (int i = 0; i < kernelSize; i++)
    {
        vec3 samplePos = TBN * samples[i];
        samplePos = fragPos + samplePos * radius;//得到取样位置

        vec4 offset = vec4(samplePos, 1.0);
        offset = projection * offset;
        offset.xyz /= offset.w;
        offset.xyz = offset.xyz * 0.5 + 0.5;//这样操作之后就得到的是0~1之间的值了

        float sampleDepth = texture(gPosition, offset.xy).z;

        float rangeCheck = smoothstep(0.0, 1.0, radius / abs(fragPos.z - sampleDepth));
        occlusion += (sampleDepth >= samplePos.z + bias ? 1.0 : 0.0) * rangeCheck;
    }

    occlusion = 1.0 - (occlusion / kernelSize);
    FragColor = occlusion;
}

搞明白这个着色器程序,尤其要注意 gPosition 颜色附件中存储的是什么,并关注一下这部分代码

vec3 samplePos = TBN * samples[i];
samplePos = fragPos + samplePos * radius;//得到取样位置

vec4 offset = vec4(samplePos, 1.0);
offset = projection * offset;
offset.xyz /= offset.w;
offset.xyz = offset.xyz * 0.5 + 0.5;//这样操作之后就得到的是0~1之间的值了
  • gPosition 存储的是投影空间下某个点在视图空间下的坐标,注意投影空间的坐标是 \([-1,1]^3\)

  • 噪声函数旋转的是 TBN 正交坐标系

  • 边缘检测函数,是为了避免出现在物体边缘处而导致错误地计算遮蔽因子,就比如说下面这两张图,和尚头顶的那部分本来不应该被检测为边缘的,但是由于没有边缘检测,墙面那边的片段周围的取样点都比记录的深度要深,于是错误的计算了遮蔽因子。这时候我们引入一个rangecheck,在radius 范围内的记为1,超出radius的使用一个smoothstep的函数缓慢将权重降为0.

由于噪声函数的重复性,所以我们还需要一个着色器来模糊这些噪声的部分。

# version 330 core

in vec2 TexCoords;
out float FragColor;

uniform sampler2D ssaoInput;

void main()
{
    vec2 texelSize = 1.0 / textureSize(ssaoInput, 0);
    float result = 0.0;
    for(int x = -2; x < 2; ++x)
    {
        for(int y = -2; y < 2; ++y)
        {
            vec2 offset = vec2(float(x), float(y)) * texelSize;
            result += texture(ssaoInput, TexCoords + offset).r;
        }
    }
    FragColor = result / (4.0 * 4.0);
}

下面展示一下使用ssao的明显效果

很明显在墙角感受到了深度的感觉。

评论区

对你有帮助的话请给我个赞和 star => GitHub stars
欢迎跟我探讨!!!