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的明显效果
很明显在墙角感受到了深度的感觉。