学习OpenGL。 课程5.10-屏幕空间环境光遮挡

OGL3

SSAO


背景照明的主题是我们在照明基础知识课程中提出的,但只是通过了。 让我提醒您:照明的背景成分本质上是一个恒定值,将其添加到场景照明的所有计算中以模拟光散射的过程。 在现实世界中,光会经历许多强度不同程度的反射,这会导致场景的间接照明部分的照明不均。 显然,强度恒定的耀斑不太合理。

一种间接照明阴影近似计算的类型是环境光遮挡(AO )算法,该算法模拟在拐角,皱纹和其他表面不规则区域附近间接照明的衰减。 通常,这样的元件被相邻的几何形状显着地重叠,因此留下更少的光线逃逸到外面,使这些区域模糊。

下面是不使用和使用AO算法的渲染的比较。 请注意墙角附近的背景照明强度如何降低以及表面上的其他尖锐断裂:


尽管效果不是很明显,但是由于自阴影效果的小细节所造成的深度错觉,整个场景中效果的存在增加了真实感。


值得注意的是,用于计算AO的算法需要大量资源,因为它们需要分析周围的几何形状。 在幼稚的实现中,可以简单地在表面的每个点发射很多射线并确定其阴影程度,但是这种方法很快达到了交互式应用程序可接受的资源密集型限制。 幸运的是,在2007年,Crytek发表了一篇论文,描述了自己的方法,该方法实现了Crysis发行版中使用的屏幕空间环境光遮蔽(SSAO )算法。 该方法仅使用当前深度缓冲区而不是有关周围几何图形的实际数据来计算屏幕空间中的阴影程度。 与参考实现相比,这种优化从根本上加速了算法,并且同时给出了最合理的结果,这使这种近似背景阴影的计算方法成为事实上的标准行业。

该算法所基于的原理非常简单:对于全屏四边形的每个片段, 都会根据周围片段的深度值计算遮挡因子 。 然后,将计算出的阴影系数用于降低背景照明的强度(直至完全排除)。 获得系数需要从围绕所讨论的片段的球形区域的多个样本收集深度数据,并将这些深度值与所讨论的片段的深度进行比较。 深度大于当前片段的样本数直接确定阴影系数。 看一下这个图:


在此,每个灰点位于某个几何对象内部,因此对阴影系数的值有所贡献。 周围物体的几何形状内的样本越多,该区域中背景阴影的残留强度就越小。

显然,效果的质量和真实性直接取决于所采样的数量。 对于少量样本,由于阴影系数非常不同的区域之间的突然过渡,导致算法的准确性降低并导致出现条带或“ 条带 ”伪像。 大量样本只会破坏性能。 样本核心的随机化允许有些相似的结果,以稍微减少所需样本的数量。 暗示通过旋转到一组样本矢量的随机角度来重新定向。 但是,引入随机性会立即以明显的噪声模式的形式带来新的问题,这就需要使用模糊滤波器来平滑结果。 下面是该算法的一个示例(作者-John Chapman )及其典型问题:条带和噪声模式。


可以看出,通过引入样本方向的随机化,可以很好地消除由于样本数量少而引起的明显条带。

Crytek的特定SSAO实施具有可识别的视觉样式。 由于Crytek专家使用的是样品的球形核,因此这甚至会影响平坦的表面(如墙),使其成为阴影-因为样品核的一半体积被淹没在几何形状下。 下面是基于阴影因子值以灰色显示的《孤岛危机》场景的屏幕截图。 在这里,“灰色”的效果清晰可见:


为了避免这种影响,我们将从样品的球芯移动到沿表面法线方向的半球:


当从这样一个法向定向的半球半球采样时,在计算阴影系数时我们不必考虑位于相邻表面下的碎片。 通常,这种方法可以消除不必要的阴影,从而获得更真实的结果。 本课将使用半球方法和John Chapman出色的SSAO课程中的一些更精致的代码。

原始数据缓冲区


计算每个片段中的阴影因子的过程需要有关周围几何图形的数据可用性。 具体来说,我们需要以下数据:

  • 每个片段的位置向量;
  • 每个片段的法线向量;
  • 每个片段的颜色漫反射;
  • 样本的核心
  • 用于重新定向样本核心的每个片段的随机旋转向量。

使用有关物种空间中片段坐标的数据,我们可以将样本核心的半球沿着当前空间的物种空间中指定的法向矢量定向。 然后,将所得的核用于从具有存储片段坐标坐标数据的纹理偏移各种偏移的样本中进行采样。 我们在每个片段中制作许多样本,对于我们制作的每个样本,我们将其深度值与片段坐标缓冲区中的深度值进行比较,以估计阴影量。 然后,将所得值用于限制最终照明计算中背景成分的贡献。 使用分段随机旋转向量,我们可以显着减少所需的样本数量以获得不错的结果,然后将对此进行演示。


由于SSAO是在屏幕空间中实现的效果,因此可以通过渲染全屏四边形来执行直接计算。 但是,那时我们将没有场景几何数据。 为了解决此限制,我们将在纹理中渲染所有必要的信息,稍后将在SSAO着色器中使用这些信息来访问有关场景的几何信息和其他信息。 如果您认真地学习了这些课程,那么您应该已经在描述的方法中知道了延迟着色算法的外观。 这很大程度上是为什么SSAO效果作为本机效果出现在带有延迟阴影的渲染中的原因-毕竟,存储坐标和法线的纹理在G缓冲区中已经可用。

在本课程中,将在延迟照明的课程的代码的稍微简化后的版本上实现效果 。 如果您还不熟悉延迟照明的原理,强烈建议您阅读本课。

由于有了G缓冲区,可以访问有关坐标和法线的片段信息,因此几何处理阶段的片段着色器非常简单:

#version 330 core layout (location = 0) out vec4 gPosition; layout (location = 1) out vec3 gNormal; layout (location = 2) out vec4 gAlbedoSpec; in vec2 TexCoords; in vec3 FragPos; in vec3 Normal; void main() { //        gPosition = FragPos; //       gNormal = normalize(Normal); //    -   gAlbedoSpec.rgb = vec3(0.95); } 

由于SSAO算法在屏幕空间中起作用,并且阴影因子是根据场景的可见区域计算的,因此在视图空间中进行计算是有意义的。 在这种情况下,从顶点着色器获得的FragPos变量将位置精确存储在视口中。 值得确保将坐标和法线存储在视图空间的G缓冲区中,因为将在其中进行所有进一步的计算。

有可能仅基于已知的片段深度和一定数量的数学魔术来恢复位置矢量,例如,在Matt Pettineo的博客中对此进行了描述。 当然,这需要大量的计算成本,但是它消除了将位置数据存储在G缓冲区中的需要,而G缓冲区占用了大量的视频内存。 但是,为了简化示例代码,我们将把这种方法留给个人研究。

gPosition颜色缓冲区纹理配置如下:

 glGenTextures(1, &gPosition); glBindTexture(GL_TEXTURE_2D, gPosition); glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB16F, SCR_WIDTH, SCR_HEIGHT, 0, GL_RGB, 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.]。 还请注意重复模式-设置了GL_CLAMP_TO_EDGE 。 这对于消除故意不对屏幕空间进行过度采样的可能性是必要的。 超出纹理坐标的主要间隔会给我们错误的位置和深度数据。

接下来,我们将参与样品半球核心的形成以及随机定向方法的创建。

创建法线导向的半球


因此,任务是在沿表面法线定向的半球内创建一组采样点。 由于无法在法线的所有可能方向上创建样本核,因此我们使用了到切线空间的过渡,在该切线空间中 ,法线始终表示为正半轴Z方向上的向量


假设半球半径是一个单一的过程,则由64个点组成的样本核心的形成如下所示:

 //      0.0 - 1.0 std::uniform_real_distribution<float> randomFloats(0.0, 1.0); std::default_random_engine generator; std::vector<glm::vec3> ssaoKernel; for (unsigned int i = 0; i < 64; ++i) { glm::vec3 sample( randomFloats(generator) * 2.0 - 1.0, randomFloats(generator) * 2.0 - 1.0, randomFloats(generator) ); sample = glm::normalize(sample); sample *= randomFloats(generator); float scale = (float)i / 64.0; ssaoKernel.push_back(sample); } 

在这里,我们随机选择间隔[-1。,1.]中的xy坐标,以及间隔[0.,1.]中的z坐标(如果间隔与xy相同,则将得到球心采样)。 最终的样本矢量将限于半球,因为样本的核心最终将沿着表面的法线定向。

目前,所有采样点都随机分布在核心内部,但是出于效果的考虑,更靠近内核原点的采样应该对阴影系数的计算做出更大的贡献。 这可以通过增加形成的采样点在原点附近的密度来改变其分布来实现。 使用加速度插值功能可以轻松完成此任务:

 scale = lerp(0.1f, 1.0f, scale * scale); sample *= scale; ssaoKernel.push_back(sample); } 

lerp()函数定义为:

 float lerp(float a, float b, float f) { return a + f * (b - a); } 

这种技巧为我们提供了一种修改后的分布,其中大多数采样点位于内核原点附近。


每个获得的样本矢量将用于在物种空间中移动片段的坐标,以获得有关周围几何体的数据。 为了在视口中工作时获得良好的结果,您可能需要大量的样本,这必然会影响性能。 但是,在每个经过处理的片段中引入伪随机噪声或样本矢量旋转都会显着减少具有可比质量的所需样本数量。

样品芯随机旋转


因此,在样本核心中的点分布中引入随机性可以显着降低对这些点的数量的要求,以获得良好的质量效果。 可以为场景的每个片段创建一个随机的旋转矢量,但是从内存上来说太昂贵了。 创建包含一组随机旋转矢量的小纹理,然后将其与GL_REPEAT重复模式一起使用, 效率更高

创建一个4x4数组,并用切线空间中沿法向矢量定向的随机旋转矢量填充它:

 std::vector<glm::vec3> ssaoNoise; for (unsigned int i = 0; i < 16; i++) { glm::vec3 noise( randomFloats(generator) * 2.0 - 1.0, randomFloats(generator) * 2.0 - 1.0, 0.0f); ssaoNoise.push_back(noise); } 

由于核心在切线空间中沿着正半轴Z对齐,因此我们使z分量等于零-这将确保仅绕Z轴旋转。

接下来,创建一个4x4纹理,并用我们的旋转矢量数组填充它。 确保使用GL_REPEAT重播模式来纹理平铺:

 unsigned int noiseTexture; glGenTextures(1, &noiseTexture); glBindTexture(GL_TEXTURE_2D, noiseTexture); glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB16F, 4, 4, 0, GL_RGB, GL_FLOAT, &ssaoNoise[0]); 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_REPEAT); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT); 

好了,现在我们有了直接实现SSAO算法所需的所有数据!

Shader SSAO


将为全屏四边形的每个片段执行一个效果着色器,计算每个片段的阴影系数。 由于结果将在创建最终照明的另一个渲染阶段中使用,因此我们将需要创建另一个framebuffer对象来存储着色器的结果:

 unsigned int ssaoFBO; glGenFramebuffers(1, &ssaoFBO); glBindFramebuffer(GL_FRAMEBUFFER, ssaoFBO); unsigned int ssaoColorBuffer; glGenTextures(1, &ssaoColorBuffer); glBindTexture(GL_TEXTURE_2D, ssaoColorBuffer); glTexImage2D(GL_TEXTURE_2D, 0, GL_RED, SCR_WIDTH, SCR_HEIGHT, 0, GL_RGB, GL_FLOAT, NULL); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, ssaoColorBuffer, 0); 

由于算法的结果是[0.,1.]中的唯一实数,因此对于存储来说,使用唯一可用的分量创建纹理就足够了。 这就是为什么将GL_RED设置为颜色缓冲区的内部格式的原因。

通常,SSAO阶段渲染过程如下所示:

 //  :  G- glBindFramebuffer(GL_FRAMEBUFFER, gBuffer); [...] glBindFramebuffer(GL_FRAMEBUFFER, 0); //  G-      SSAO glBindFramebuffer(GL_FRAMEBUFFER, ssaoFBO); glClear(GL_COLOR_BUFFER_BIT); glActiveTexture(GL_TEXTURE0); glBindTexture(GL_TEXTURE_2D, gPosition); glActiveTexture(GL_TEXTURE1); glBindTexture(GL_TEXTURE_2D, gNormal); glActiveTexture(GL_TEXTURE2); glBindTexture(GL_TEXTURE_2D, noiseTexture); shaderSSAO.use(); SendKernelSamplesToShader(); shaderSSAO.setMat4("projection", projection); RenderQuad(); glBindFramebuffer(GL_FRAMEBUFFER, 0); //  :    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); shaderLightingPass.use(); [...] glActiveTexture(GL_TEXTURE3); glBindTexture(GL_TEXTURE_2D, ssaoColorBuffer); [...] RenderQuad(); 

shaderSSAO着色器接受所需的G缓冲区纹理作为输入,以及噪波纹理和样本核心:

 #version 330 core out float FragColor; in vec2 TexCoords; uniform sampler2D gPosition; uniform sampler2D gNormal; uniform sampler2D texNoise; uniform vec3 samples[64]; uniform mat4 projection; //             //      1280x720 const vec2 noiseScale = vec2(1280.0/4.0, 720.0/4.0); void main() { [...] } 

注意变量noiseScale 。 我们带有噪点的小纹理应该平铺在屏幕的整个表面上,但是由于TexCoords的纹理坐标在[0.,1.]范围内,因此如果没有我们的干预就不会发生。 为此,我们计算纹理坐标的因数,该因数是屏幕尺寸与噪声纹理尺寸的比值:

 vec3 fragPos = texture(gPosition, TexCoords).xyz; vec3 normal = texture(gNormal, TexCoords).rgb; vec3 randomVec = texture(texNoise, TexCoords * noiseScale).xyz; 

由于创建texNoise噪声纹理时,我们将重复模式设置为GL_REPEAT ,现在它将在屏幕表面重复很多次。 有了randomVecfragPos法线值,我们可以创建从切线到物种空间的TBN转换矩阵:

 vec3 tangent = normalize(randomVec - normal * dot(randomVec, normal)); vec3 bitangent = cross(normal, tangent); mat3 TBN = mat3(tangent, bitangent, normal); 

使用Gram-Schmidt过程,我们基于随机值randomVec创建在每个片段中随机倾斜的正交基础。 重要的一点是:因为在这种情况下,TBN矩阵沿三角形的表面精确定向(对于视差映射而言,大约为1/3)对我们来说并不重要。

接下来,我们遍历样本核的数组,将每个样本向量从切线空间转换为物种空间,并获得其与当前片段位置的和。 然后,我们将结果量的深度值与通过从相应的G缓冲区纹理采样获得的深度值进行比较。

虽然听起来令人困惑,但让我们逐步执行以下步骤:

 float occlusion = 0.0; for(int i = 0; i < kernelSize; ++i) { //     vec3 sample = TBN * samples[i]; //      - sample = fragPos + sample * radius; [...] } 

在此, kernelSizeradius是控制效果特征的变量。 在这种情况下,它们分别是64和0.5。 在每次迭代中,我们将样本核心向量转换到物种空间中。 接下来,我们将获得的样本在物种空间中的位移值加上片段在物种空间中的位置值。 在这种情况下,偏移值将乘以半径变量,该变量控制SSAO效果样本的核心半径。

完成这些步骤之后,我们应该将所得的样本矢量转换为屏幕空间,以便我们可以使用获得的投影值从G缓冲区纹理中进行选择,该纹理存储片段的位置和深度。 由于样本在视口中,因此我们需要投影投影矩阵:

 vec4 offset = vec4(sample, 1.0); offset = projection * offset; //     offset.xyz /= offset.w; //   offset.xyz = offset.xyz * 0.5 + 0.5; //    [0., 1.] 

转换为剪辑空间后,我们通过将xyz分量除以w分量来手动执行透视划分。 归一化的设备坐标( NDC )中的结果矢量被转换为值[0.,1.]的间隔,因此可以将其用作纹理坐标:

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

我们使用样本向量的xy分量从纹理中选择G缓冲区的位置。 从观察者的位置(这是第一个未屏蔽的可见片段)观察时,我们获得与样本矢量对应的深度值( z分量)。 如果同时获得的采样深度大于存储的深度,则我们增加阴影系数:

 occlusion += (sampleDepth >= sample.z + bias ? 1.0 : 0.0); 

注意偏置偏移,它已添加到原始片段深度(在示例中设置为0.025)。 并非总是需要此偏移量,但是变量的存在使您可以控制SSAO效果的外观,并且在某些情况下还可以消除阴影区域中的波纹问题。

但这还不是全部,因为这样的实现会导致明显的工件。 当考虑到某个表面边缘附近的碎片时,它就会显现出来。 在这种情况下,当比较深度时,该算法将不可避免地捕获表面的深度,该深度可能远远超出所考虑的深度。 在这些地方,该算法将错误地大大增加阴影的程度,这将在对象的边缘产生明显的暗晕。 通过引入额外的距离检查来处理工件(例如John Chapman的示例):


该检查将仅对位于样本半径内的深度值限制对阴影系数的贡献:

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

我们还使用GLSL smoothstep()函数,该函数在第一个和第二个之间实现了第三个参数的平滑插值。 同时,如果第三个参数小于或等于第一个,则返回0;如果第三个参数大于或等于第二个,则返回1。 如果深度差在radius内,则将按照以下曲线在[0.,1.]区间内平滑其值:


如果在检查深度的条件下使用清晰的边界,则会在深度差值超出radius限制的那些地方以尖锐边界的形式添加伪影。

最后,我们使用样本核心的大小对阴影系数的值进行归一化,并记录结果。 我们还通过从单位减去最终值来反转最终值,以便您可以直接使用最终值来调制照明的背景成分,而无需执行其他步骤:

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

对于我们熟悉的具有躺着的纳米套装的场景,执行SSAO着色器将产生以下纹理:


如您所见,背景阴影效果会产生良好的深度错觉。 仅着色器的输出图像已经允许您区分服装的细节,并确保服装确实位于地板上,并且不会与服装保持一定距离。

然而,由于随机旋转矢量的纹理引入的噪声模式很容易注意到,因此效果远非理想。 为了平滑SSAO计算的结果,我们应用了模糊滤镜。

模糊背景底纹


建立SSAO的结果之后,在最终混合照明之前,有必要对存储有关阴影系数数据的纹理进行模糊处理。 为此,我们将有另一个帧缓冲区:

 unsigned int ssaoBlurFBO, ssaoColorBufferBlur; glGenFramebuffers(1, &ssaoBlurFBO); glBindFramebuffer(GL_FRAMEBUFFER, ssaoBlurFBO); glGenTextures(1, &ssaoColorBufferBlur); glBindTexture(GL_TEXTURE_2D, ssaoColorBufferBlur); glTexImage2D(GL_TEXTURE_2D, 0, GL_RED, SCR_WIDTH, SCR_HEIGHT, 0, GL_RGB, GL_FLOAT, NULL); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, ssaoColorBufferBlur, 0); 

在屏幕空间中平铺噪声纹理可提供定义良好的随机性特征,您可以在创建模糊滤镜时利用该特征:

 #version 330 core out float FragColor; in vec2 TexCoords; uniform sampler2D ssaoInput; void main() { vec2 texelSize = 1.0 / vec2(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纹理的纹理元素从-2偏移到+2,这与噪声纹理的实际大小相对应。 偏移量等于一个纹理元素的确切大小:textureSize ()函数用于计算,该函数返回带有指定纹理尺寸的vec2 。 T.O. 着色器仅对存储在纹理中的结果进行平均,即可提供快速有效的模糊效果:


总的来说,我们在屏幕上的每个片段都有一个带有背景阴影数据的纹理-一切准备就绪,可以进行最终图像缩小!

应用背景底纹


在照明的最终计算中应用阴影系数的步骤非常简单:对于每个片段,只需将光源的背景成分的值乘以准备好的纹理的阴影系数就足够了。 您可以使用关于延迟着色的课程中的Blinn-Fong模型获取现成的着色器,并进行一些校正:

 #version 330 core out vec4 FragColor; in vec2 TexCoords; uniform sampler2D gPosition; uniform sampler2D gNormal; uniform sampler2D gAlbedo; uniform sampler2D ssao; struct Light { vec3 Position; vec3 Color; float Linear; float Quadratic; float Radius; }; uniform Light light; void main() { //    G- vec3 FragPos = texture(gPosition, TexCoords).rgb; vec3 Normal = texture(gNormal, TexCoords).rgb; vec3 Diffuse = texture(gAlbedo, TexCoords).rgb; float AmbientOcclusion = texture(ssao, TexCoords).r; //   -    //   :   -  vec3 ambient = vec3(0.3 * Diffuse * AmbientOcclusion); vec3 lighting = ambient; //    (0, 0, 0)   - vec3 viewDir = normalize(-FragPos); //   vec3 lightDir = normalize(light.Position - FragPos); vec3 diffuse = max(dot(Normal, lightDir), 0.0) * Diffuse * light.Color; //   vec3 halfwayDir = normalize(lightDir + viewDir); float spec = pow(max(dot(Normal, halfwayDir), 0.0), 8.0); vec3 specular = light.Color * spec; //   float dist = length(light.Position - FragPos); float attenuation = 1.0 / (1.0 + light.Linear * dist + light.Quadratic * dist * dist); diffuse *= attenuation; specular *= attenuation; lighting += diffuse + specular; FragColor = vec4(lighting, 1.0); } 

仅存在两个主要变化:在视口中过渡到计算,以及将背景照明分量乘以AmbientOcclusion的值。 带有单个蓝点灯的场景示例:


完整的源代码在这里

SSAO效果的表现在很大程度上取决于参数,例如kernelSizeradiusbias ,经常对它们进行微调是艺术家确定特定位置/场景的理所当然的事情。 没有参数的“最佳”和通用组合:对于某些场景,样本核心的半径较小是好的,而另一些则受益于样本半径和数量增加。 该示例使用了64个采样点,坦率地说,这是多余的,但是您始终可以编辑代码,并查看使用较少数量的采样会发生的情况。

除了列出的负责设定效果的制服外,还可以明确控制背景阴影效果的严重性。 为此,将系数提高到另一个制服控制的程度就足够了:

 occlusion = 1.0 - (occlusion / kernelSize); FragColor = pow(occlusion, power); 

我建议您花一些时间在游戏上进行设置,因为这样可以更好地理解最终图片中更改的性质。

总而言之,值得一提的是,尽管使用SSAO的视觉效果相当微妙,但是在光线充足的场景中,无疑会增加明显的真实感。在您的武器库中拥有这样的工具肯定是有价值的。

其他资源


  1. SSAO教程:John Chapman的精彩课程文章,在此基础上构建了该课程的代码。
  2. 了解您的SSAO工件:一篇非常有价值的文章,清楚地介绍了SSAO质量最紧迫的问题,以及解决这些问题的方法。推荐阅读。
  3. 具有深度重建的SSAO:OGLDev在SSAO主要课程的附录中,介绍了一种基于深度恢复片段坐标的常用技术。这种方法的重要性是由于无需在G缓冲区中存储位置而节省了大量内存。该方法是如此普遍,就其而言,它适用于SSAO。

PS:我们有一个电报会议,以协调转账。如果您有强烈的帮助翻译的愿望,欢迎您!

Source: https://habr.com/ru/post/zh-CN421385/


All Articles