我们在基本照明一章中简要介绍了该主题:ambient lighting环境光Ambient lighting环境光是一个固定的光常数,我们添加到场景的整体照明中以模拟光的scattering散射。 实际上,光以不同的强度向各种方向散射,因此场景的indirectly间接照明部分也应该具有不同的强度。 一种类型的间接照明近似称为ambient occlusion环境光遮蔽,它试图通过使彼此靠近的creases折痕、孔洞和表面变暗来近似间接照明。 这些区域很大程度上被周围的几何图形遮挡,因此光线可以逃逸的地方更少,因此这些区域看起来更暗。 看看你房间的角落和夹角处,看看那里的光线似乎有点暗

下面是具有和不具有环境光遮挡的场景的示例图像。 请注意,尤其是在折痕之间,(环境)光更加被遮挡:

虽然效果不是很明显,但启用环境光遮蔽的图像确实感觉更逼真,因为这些小的类似遮蔽的细节让整个场景更有深度感。

Ambient occlusio环境遮挡技术很昂贵,因为它们必须考虑周围的几何形状。可以为空间中的每个点拍摄大量光线以确定其遮挡量,但对于实时解决方案而言,这很快变得在计算上不可行。 2007 年,Crytek 发布了一种称为 screen-space ambient occlusion屏幕空间环境光遮蔽 (SSAO) 的技术,用于他们的标题《孤岛危机》。该技术使用屏幕空间中的场景深度缓冲区来确定遮挡量,而不是实际的几何数据。与真实环境光遮蔽相比,这种方法速度非常快,并且给出了合理的结果,使其成为近似实时环境光遮蔽的事实上的标准。

屏幕空间环境遮挡背后的基础很简单:对于屏幕填充四边形上的每个片段,我们根据片段的周围深度值计算遮挡因子。然后使用遮挡因子来减少或消除片段的环境照明分量遮挡因子是通过在片段位置周围的球形样本内核中获取多个深度样本并将每个样本与当前片段的深度值进行比较来获得的。深度值高于片段深度的样本数表示遮挡因子

(就是生成一个球,然后在球里面随机拿样本,看这些样本的深度对比自己的深度,来生成遮挡因子)

几何内部的每个灰度深度样本都对总遮挡因子有贡献; 我们在几何体中找到的样本越多,片段最终应该接收到的环境光照就越少。

很明显,效果的质量和精度与我们采集的周围样本数量直接相关。 如果样本计数太低,精度会急剧下降,我们会得到一个称为banding条带的artifact; 如果它太高,我们就会失去性能。 我们可以通过在样本核中引入一些随机性来减少我们必须测试的样本数量。 通过随机旋转每个片段的样本内核,我们可以用更少的样本获得高质量的结果。 这确实是有代价的,因为随机性引入了明显的noise pattern噪声模式,我们必须通过模糊结果来修复它。 下面是一张图片(由 John Chapman 提供)展示了banding effect条带效应和随机性对结果的影响:

1.low sample 'banding' :几何体内的样本计数太低,就会造成banding条带的artifact人工制品

2.random rotation = noise: 引入随机性,就会产生noise pattern噪声模式

3.blur = acceptable: 模糊结果,效果就可接受了

即使由于样本数低,我们在 SSAO 结果上得到了明显的条带,但通过引入一些随机性,条带效应完全消失了。

Crytek 开发的 SSAO 方法具有一定的视觉风格。 因为使用的样本内核是一个球体,它导致平坦的墙壁看起来是灰色的,因为一半的内核样本始终位于周围的几何体中(解释:比如某个面,法线朝上,那么下半部分始终都是实心的,depth始终比样本点大,深,所以如果直到法线向量,那下半球都没必要计算了,已经直到结果了)。 下面是 Crysis 屏幕空间环境光遮蔽的图像,清楚地描绘了这种灰色感觉:

出于这个原因,我们不会使用球体样本内核,而是使用沿surface's normal vector 表面法线向量oriented定向hemisphere sample kernel半球样本内核

通过围绕这个 normal-oriented hemisphere面向法线的半球进行采样,我们不认为片段的底层几何形状对遮挡因子有贡献。 这消除了ambient occlusion环境遮挡的灰色感觉,通常会产生更真实的结果。 本章的技术基于这种normal-oriented hemisphere面向法线的半球方法和 John Chapman 出色的 SSAO 教程的略微修改版本。

1.Sample buffers(采样的buffer)

SSAO 需要几何信息,因为我们需要一些方法来确定片段的遮挡因子。 对于每个片段,我们将需要以下数据:

  • A per-fragment position vector. 每个fragment的位置
  • A per-fragment normal vector. 每个fragment的法线
  • A per-fragment albedo color. 每个fragment 反射率
  • sample kernel.  一个采样内核(一个圆体)
  • A per-fragment random rotation vector used to rotate the sample kernel. 用于旋转sample kernel的随机旋转向量

使用每个片段的view-space position视图空间位置,我们可以将sample hemisphere kernel样本半球内核around定向在片段的view-space surface normal视图空间表面法线周围,并使用该内核以varying offsets不同的偏移量对位置缓冲区纹理进行采样。 对于每个片段内核样本,我们将其深度与其在位置缓冲区中的深度进行比较,以确定遮挡量。 然后使用得到的遮挡因子限制最终的环境照明分量。 通过还包括每个片段的旋转向量,我们可以显着减少我们需要采集的样本数量,我们很快就会看到。

(图中,G-Buffer中,一张position位置,一张normal vectors法线向量,一张白纸,外加SSAO下方,一个64位KERNEL_SAMPLES核心采样,一个随机旋转vector)

由于 SSAO 是一种屏幕空间技术,我们计算其对屏幕填充 2D 四边形上每个片段的影响。 这确实意味着我们没有场景的几何信息。 我们可以做的是将每个片段的geometrical information几何数据渲染到屏幕空间纹理中,然后我们将其发送到 SSAO 着色器,这样我们就可以访问每个片段的几何数据。 如果您按照前一章的内容进行操作,您会发现这看起来很像延迟渲染器的 G 缓冲区设置。 出于这个原因,SSAO 非常适合与延迟渲染结合使用,因为我们已经在 G 缓冲区中拥有了position位置和normal vectors法线向量

在本章中,我们将在延迟着色章节中的延迟渲染器的略微简化版本之上实现 SSAO。

由于我们应该从场景对象中获得每个片段的位置和法线数据,几何阶段的片段着色器相当简单:


#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()
{    // store the fragment position vector in the first gbuffer texture// 位置gPosition = FragPos;// also store the per-fragment normals into the gbuffer//法线gNormal = normalize(Normal);// and the diffuse per-fragment color, ignore specular// albedo 反射率gAlbedoSpec.rgb = vec3(0.95);
}  

由于 SSAO 是一种从可见视图计算遮挡的屏幕空间技术,因此在视图空间中实现该算法是有意义的。 因此,几何阶段的顶点着色器提供的 FragPos 和 Normal 被转换到视图空间(也乘以视图矩阵)。

使用 Matt Pettineo 在他的博客 blog中描述的一些巧妙技巧,可以仅从深度值重建位置向量。 这需要在着色器中进行一些额外的计算,但我们不必将位置数据存储在 G 缓冲区中(这会占用大量内存)。 为了一个更简单的例子,我们将把这些优化放在本章之外。

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

glGenTextures(1, &gPosition);
glBindTexture(GL_TEXTURE_2D, gPosition);
// 配置浮点 framebuffer 作为g-buffer
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.0,1.0],我们需要更高的精度。 还要注意 GL_CLAMP_TO_EDGE 的纹理包裹方法。 这确保了我们不会意外地对纹理默认坐标区域之外的屏幕空间中的位置/深度值进行过采样

接下来,我们需要实际的半球采样内核和一些随机旋转它的方法

2.Normal-oriented hemisphere 法线方向的半球

我们需要生成许多 samples oriented along the normal of a surface沿曲面法线定向的样本。 正如我们在本章开头简要讨论的那样,我们想要生成形成一个半球的样本。 由于为每个表面法线方向生成样本核既困难又不合理,我们将在切线空间中生成样本核,法向量指向正 z 方向

假设我们有一个单位半球,我们可以得到一个最大64个样本值的样本核如下:

std::uniform_real_distribution<float> randomFloats(0.0, 1.0); // random floats between [0.0, 1.0]
std::default_random_engine generator;
//存储随机采样的数组
std::vector<glm::vec3> ssaoKernel;
// 生成64个随机采样vector3
for (unsigned int i = 0; i < 64; ++i)
{//随机生成方向向量(注意:这个向量只会指向上半球)glm::vec3 sample(//x向量-1到1randomFloats(generator) * 2.0 - 1.0, //y向量-1到1randomFloats(generator) * 2.0 - 1.0, //z向量0到1randomFloats(generator));// 单位化随机生成的方向向量sample  = glm::normalize(sample);// 随机生成一个长度 0-1sample *= randomFloats(generator);ssaoKernel.push_back(sample);
}

我们在 -1.0 和 1.0 之间改变切线空间中的 x 和 y 方向,并在 0.0 和 1.0 之间改变样本的 z 方向(如果我们也在 -1.0 和 1.0 之间改变 z 方向,我们将有一个球体样本内核 )。 由于样本内核将沿表面法线定向,因此生成的样本向量将全部位于半球中。

目前,所有样本都随机分布在样本内核中,但我们宁愿在靠近实际片段的遮挡上放置更大的权重。 我们希望将更多内核样本分布在更接近原点的位置。 我们可以使用accelerating interpolation function加速插值函数来做到这一点:

   //这样一搞,sample离0更近的就更多了,离1更近的就少了很多// 例如for 遍历到i= 32的时候,scale = 32/64, scale * scale = 0.25, 结果i=25的// sample值被缩减成了比原先只有1/4了float scale = (float)i / 64.0; 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);
}  

这为我们提供了一个内核分布,使大多数样本更接近其原点。

每个内核样本将用于偏移视图空间片段位置以对周围的几何图形进行采样。 为了获得真实的结果,我们确实需要在视图空间中有相当多的样本,这可能对性能造成太大影响。 但是,如果我们可以在每个per-fragment basis片段的基础上引入一些 semi-random rotation/noise半随机旋转/噪声,我们可以显着减少所需的样本数量

3.Random kernel rotations (随机内核旋转)

通过在样本内核中引入一些随机性,我们大大减少了获得良好结果所需的样本数量。 我们可以为场景的每个片段创建一个随机旋转向量,但这很快就会占用内存。 创建一个小的随机旋转矢量纹理更有意义,我们平铺在屏幕上。

我们创建一个 4x4 的随机旋转向量数组,围绕切线空间表面法线:

噪声方向向量:

// 噪声 方向 向量
std::vector<glm::vec3> ssaoNoise;
for (unsigned int i = 0; i < 16; i++)
{//创建16个,只在x,y方向有值的随机旋转向量,用来给那些采样向量进行旋转生成更多新的采样向量glm::vec3 noise(randomFloats(generator) * 2.0 - 1.0, randomFloats(generator) * 2.0 - 1.0, 0.0f); ssaoNoise.push_back(noise);
}  

由于样本内核在切线空间中沿正 z 方向定向,因此我们将 z 分量保留为 0.0,因此我们围绕 z 轴旋转。

然后我们创建一个包含随机旋转向量的 4x4 纹理; 确保将其包装方法设置为 GL_REPEAT 以便正确平铺在屏幕上。(这个4x4 texure重复平铺,所以可以随便映射)

噪声纹理:

unsigned int noiseTexture;
glGenTextures(1, &noiseTexture);
glBindTexture(GL_TEXTURE_2D, noiseTexture);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA16F, 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);  

4.The SSAO shader (Screen Space Ambient Occlusion Shader)

SSAO 着色器在 2D 屏幕填充四边形上运行,计算每个片段的遮挡值。 由于我们需要存储 SSAO 阶段的结果(用于最终的光照着色器),我们创建了另一个帧缓冲区对象:

unsigned int ssaoFBO;
glGenFramebuffers(1, &ssaoFBO);
glBindFramebuffer(GL_FRAMEBUFFER, ssaoFBO);//创建计算ssao的framebuffer
unsigned int ssaoColorBuffer;
glGenTextures(1, &ssaoColorBuffer);
glBindTexture(GL_TEXTURE_2D, ssaoColorBuffer);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RED, SCR_WIDTH, SCR_HEIGHT, 0, GL_RED, 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);  

由于环境光遮蔽结果是单个灰度值,我们只需要纹理的红色分量,因此我们将颜色缓冲区的内部格式设置为 GL_RED

渲染 SSAO 的完整过程看起来有点像这样:

// geometry pass: render stuff into G-buffer
// 几何通道,渲染各种参数到G-buffer
glBindFramebuffer(GL_FRAMEBUFFER, gBuffer);[...]
glBindFramebuffer(GL_FRAMEBUFFER, 0);  // use G-buffer to render SSAO texture
// 用G-buffer 来渲染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);// lighting pass: render scene lighting
// 渲染场景光照
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
shaderLightingPass.use();
[...]
glActiveTexture(GL_TEXTURE3);
glBindTexture(GL_TEXTURE_2D, ssaoColorBuffer);
[...]
RenderQuad();  

shaderSSAO 着色器将相关的 G-buffer纹理、ssaoNoise噪声纹理和面向法线的半球内核采样作为输入:

#version 330 core
out float FragColor;in vec2 TexCoords;//位置
uniform sampler2D gPosition;
// 法线
uniform sampler2D gNormal;
// 噪声texture
uniform sampler2D texNoise;
// 64个采样点
uniform vec3 samples[64];
uniform mat4 projection;// tile noise texture over screen, based on screen dimensions divided by noise size
// 根据屏幕大小调整噪声纹理大小
const vec2 noiseScale = vec2(800.0/4.0, 600.0/4.0); // screen = 800x600void main()
{[...]
}

这里需要注意的是noiseScale 变量。 我们想在整个屏幕上平铺噪声纹理,但由于 TexCoords 在 0.0 和 1.0 之间变化,texNoise 纹理平铺了没用。 因此,我们将通过将屏幕尺寸除以噪声纹理大小来计算缩放 TexCoords 所需的量。(因为我们的fragment的texCoords纹理坐标,一般都是0-1的,如何让它随机地映射到4*4的随机噪音向量上呢?先把texCoords放大到屏幕级别,由0-1变成0-800,然后我们的noiseTexture由4*4平铺无限循环,这样一映射,就随机地对应上去了)

// 根据输入的texCoords,贴图坐标,求出随机向量vector,这个向量长达屏幕宽度
vec3 fragPos   = texture(gPosition, TexCoords).xyz;
vec3 normal    = texture(gNormal, TexCoords).rgb;
// 把texCoords放大到屏幕级别,由0-1变成0-800,然后我们的noiseTexture由4*4平铺无限循环,这样一映射,就随机地对应上去了
vec3 randomVec = texture(texNoise, TexCoords * noiseScale).xyz; 

我们将 texNoise 的平铺参数设置为 GL_REPEAT 时,随机值将在整个屏幕上重复。 连同 fragPos 和法线向量,我们就有足够的数据来创建一个 TBN 矩阵,它将任何向量从切线空间转换到视图空间:

// 坐标系的横向量 ,利用Gramm-Schmidt process格拉姆-施密特过程 计算
vec3 tangent   = normalize(randomVec - normal * dot(randomVec, normal));
// 坐标系的竖向量
vec3 bitangent = cross(normal, tangent);
// 外加坐标系的法向量
mat3 TBN       = mat3(tangent, bitangent, normal); 

使用称为 Gramm-Schmidt process格拉姆-施密特过程的过程,我们创建一个正交基,每次根据 randomVec 的值略微倾斜。 请注意,因为我们使用随机向量来构造切线向量,所以不需要将 TBN 矩阵与几何体的表面完全对齐,因此不需要每个顶点的切线(和双切线)向量。

接下来我们遍历每个内核样本,将样本从切线转换到视图空间,将它们添加到当前片段位置,并将片段位置的深度与存储在视图空间位置缓冲区中的样本深度进行比较。 让我们逐步讨论这个问题:

float occlusion = 0.0;
//对采样核心数量进行遍历
for(int i = 0; i < kernelSize; ++i)
{// get sample position// 获得采样的坐标 TBN为变换矩阵,将切线空间下的法线变换到物体空间下的法线vec3 samplePos = TBN * samples[i]; // from tangent to view-space// 采样点位置计算samplePos = fragPos + samplePos * radius; [...]
}  

这里 kernelSize 和 radius 是我们可以用来调整效果的变量; 在这种情况下,值分别为 64 和 0.5。 对于每次迭代,我们首先将相应的样本转换到视图空间。 然后我们将视图空间内核偏移样本添加到视图空间片段位置。 然后我们将偏移样本乘以半径来增加(或减少)SSAO的有效样本半径。

接下来我们要将样本转换为屏幕空间,这样我们就可以对样本的位置/深度值进行采样,就好像我们将其位置直接渲染到屏幕上一样。 由于向量当前位于视图空间中,我们将首先使用投影矩阵统一将其转换为剪辑空间:

这一段,是对采样点进行深度采样:

//这一段,是对采样点进行深度采样
vec4 offset = vec4(samplePos, 1.0);
// 转换为剪辑空间
offset      = projection * offset;    // from view to clip-space
// 透视配置
offset.xyz /= offset.w;               // perspective divide
// 转换为0-1
offset.xyz  = offset.xyz * 0.5 + 0.5; // transform to range 0.0 - 1.0  

将变量转换为剪辑空间后,我们通过将其 xyz 分量除以 w 分量来执行透视除法步骤。 然后将生成的标准化设备坐标转换为 [0.0, 1.0] 范围,以便我们可以使用它们对位置纹理进行采样

// 采样点位置的最大depth值
float sampleDepth = texture(gPosition, offset.xy).z; 

我们使用偏移向量的 x 和 y 分量对位置纹理进行采样,以检索从观察者视角(第一个未遮挡的可见片段)看到的采样位置的深度(或 z 值)。 然后我们检查样本的当前深度值是否大于存储的深度值,如果是,我们添加到最终贡献因子:

//利用采样点最大depth值,与采样点本身的z值计算出贡献因子
//如果采样点的z值小于最大depth值,相当于此处被遮住了,属于石头而不是空气,就要算SSAO的贡献了
occlusion += (sampleDepth >= samplePos.z + bias ? 1.0 : 0.0);  

请注意,我们在此处为原始片段的深度值添加了一个小偏差(在此示例中设置为 0.025)。 偏差并不总是必要的,但它有助于在视觉上调整 SSAO 效果并解决基于场景复杂性可能出现的粉刺效果。

我们还没有完全完成,因为还有一个小问题需要考虑。 每当一个片段被测试为靠近表面边缘对齐的环境遮挡时,它也会考虑远离测试表面的表面的深度值; 这些值将(错误地)影响遮挡因子。我们可以通过引入范围检查来解决这个问题,如下图(由 John Chapman 提供)所示:

(例如佛像身后的那本书,按当前算法,佛像挡住了书,书需要显示SSAO,但实际上,佛像距离书太远了,根本不需要显示SSAO,所以, 需要加一个距离检测)

 我们引入了范围检查,以确保如果片段的深度值在样本的半径范围内,则该片段对遮挡因子有贡献。 我们将最后一行更改为:

// 检测
// smoothstep平滑过渡   当sampleDepth采样点深度,接近fragPos.z,片元深度的时候
// rangeCheck就接近1,当fragPos.z与sampleDepth远离的时候,smoothStep就接近0
float rangeCheck = smoothstep(0.0, 1.0, radius / abs(fragPos.z - sampleDepth));//如果采样点的z值小于最大depth值,相当于此处被遮住了,属于石头而不是空气,就要算SSAO的贡献了
occlusion       += (sampleDepth >= samplePos.z + bias ? 1.0 : 0.0) * rangeCheck;   

 smoothstep平滑过渡

如果我们要使用硬截止范围检查,如果深度值在半径之外,则会突然消除遮挡贡献,我们会在应用范围检查的位置看到明显(不吸引人的)边界。

作为最后一步,我们通过内核大小对遮挡贡献进行归一化并输出结果。 请注意,我们从 1.0 中减去了遮挡因子,因此我们可以直接使用遮挡因子来缩放环境照明组件。

}
//归一化,根据kernelSize大小映射到0-1的范围
occlusion = 1.0 - (occlusion / kernelSize);
FragColor = occlusion;  

如果我们想象一个我们最喜欢的背包模型正在小睡的场景,环境光遮蔽着色器会产生以下纹理:

5.Ambient occlusion blur (环境遮挡模糊)

在 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_RED, 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);

因为平铺的随机矢量纹理为我们提供了一致的随机性,我们可以利用这个属性来创建一个简单的模糊着色器:(4*4模糊采样)fs:

#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);
}  

在这里,我们遍历 -2.0 和 2.0 之间的周围 SSAO 纹素,对 SSAO 纹理进行采样,其数量与噪声纹理的尺寸相同。 我们使用返回给定纹理尺寸的 vec2 的 textureSize 将每个纹理坐标偏移单个纹素的确切大小。 我们平均获得的结果以获得简单但有效的模糊:

我们开始了,一个带有每个片段环境遮挡数据的纹理; 准备在照明通道中使用。

6. Applying ambient occlusion (应用环境光遮蔽)

将遮挡因子应用于光照方程非常简单:我们所要做的就是将每个片段的环境遮挡因子乘以光照的环境分量,我们就完成了。 如果我们把上一章的 Blinn-Phong 延迟光照着色器稍微调整一下,我们会得到以下片段着色器:

fs:

#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()
{             // retrieve data from gbuffervec3 FragPos = texture(gPosition, TexCoords).rgb;vec3 Normal = texture(gNormal, TexCoords).rgb;vec3 Diffuse = texture(gAlbedo, TexCoords).rgb;float AmbientOcclusion = texture(ssao, TexCoords).r;// blinn-phong (in view-space)vec3 ambient = vec3(0.3 * Diffuse * AmbientOcclusion); // here we add occlusion factorvec3 lighting  = ambient; vec3 viewDir  = normalize(-FragPos); // viewpos is (0.0.0) in view-space// diffusevec3 lightDir = normalize(light.Position - FragPos);vec3 diffuse = max(dot(Normal, lightDir), 0.0) * Diffuse * light.Color;// specularvec3 halfwayDir = normalize(lightDir + viewDir);  float spec = pow(max(dot(Normal, halfwayDir), 0.0), 8.0);vec3 specular = light.Color * spec;// attenuation // 点光源的衰减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。 在场景中使用单个蓝色点光源,我们将得到以下结果:

屏幕空间环境光遮蔽是一种高度可定制的效果,很大程度上依赖于根据场景类型调整其参数。 每种场景都没有完美的参数组合。 一些场景只适用于小半径,而其他场景需要更大的半径和更大的样本数才能看起来逼真。 当前的demo使用了64个样本,有点多; 尝试使用较小的内核大小并尝试获得良好的结果。

您可以调整一些参数(例如通过使用制服):内核大小、半径、偏差和/或噪声内核的大小。 您还可以将最终遮挡值提高到用户定义的幂以增加其强度:

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

玩转不同的场景和不同的参数,以欣赏 SSAO 的可定制性。

尽管 SSAO 是一种不太明显的微妙效果,但它为适当照明的场景添加了大量的真实感,并且绝对是您希望在您的工具包中拥有的一种技术。

Learn OpenGL 笔记6.10 SSAO(Screen Space Ambient Occlusion屏幕空间环境光遮蔽)相关推荐

  1. DirectX 11 学习笔记-Part2-4【Cubemap/贴图置换/阴影/屏幕空间环境光遮蔽】【原理篇】

    代码已上传至GitHub,这部分对应的项目为NormalDisplacementSsao.运行项目时可以使用数字键1-3切换渲染的shader,另外按下z键可以看到SSAO生成的遮罩贴图. 这部分内容 ...

  2. OpenGL进阶之SSAO屏幕空间环境光遮蔽

    参考: https://learnopenglcn.github.io/05%20Advanced%20Lighting/09%20SSAO/ 环境光照是我们加入场景总体光照中的一个固定光照常量,它被 ...

  3. OpenGL SSAO屏幕空间环境光遮蔽的实例

    OpenGL SSAO屏幕空间环境光遮蔽 先上图,再解答. 完整主要的源代码 源代码剖析 先上图,再解答. 完整主要的源代码 #include <glad/glad.h> #include ...

  4. 【技美百人计划】图形 4.2 SSAO算法 屏幕空间环境光遮蔽(&HBAO)

    笔记 SSAO介绍 AO 环境光遮蔽,AmbientOcclusion.一种模拟光线到达物体的能力和粗略的全局方法. SSAO 屏幕环境光遮蔽,Screen Space Ambient Occlusi ...

  5. DirectX11 With Windows SDK--32 SSAO(屏幕空间环境光遮蔽)

    前言 由于性能的限制,实时光照模型往往会忽略间接光因素(即场景中其他物体所反弹的光线).但在现实生活中,大部分光照其实是间接光.在第7章里面的光照方程里面引入了环境光项: C a = A L ⊗ m ...

  6. 【TA-霜狼_may-《百人计划》】图形4.2 SSAO算法 屏幕空间环境光遮蔽

    [TA-霜狼_may-<百人计划>]图形4.2 SSAO算法 屏幕空间环境光遮蔽 @[TOC]([TA-霜狼_may-<百人计划>]图形4.2 SSAO算法 屏幕空间环境光遮蔽 ...

  7. 图形 4.2 SSAO算法 屏幕空间环境光遮蔽

    链接: SSAO算法 屏幕空间环境光遮蔽思维导图. SSAO算法 屏幕空间环境光遮蔽 SSAO介绍 什么是AO SSAO原理详解 SSAO介绍 SSAO原理 计算近似AO SSAO算法实现 比较与分析 ...

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

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

  9. 实时全局光照Screen Space Ambient Occlusion(SSAO)

    屏幕空间 可以拿到的所有信息只能从屏幕中所看到的获取,即在做全局光照之前能在屏幕上看到的->直接光照. 换句话说,对现有的渲染进行后期处理. 环境光遮蔽 为什么要做环境遮蔽? 实施成本低 增加了 ...

最新文章

  1. Python3基础笔记---面向对象
  2. php自定义函数出现乱码,php的imagettftext 函数出现乱码的解决方法
  3. python编程基础与应用-Python程序设计:从编程基础到专业应用
  4. ASP.NET MVC+EF框架+EasyUI实现权限管理(附源码)
  5. 广搜--(搜索的第一道题)图像有用区域
  6. uvali5697(DP)
  7. Linkis1.0下载地址
  8. kindeditor图片上传 struts2实现
  9. eclipse编码页面中文乱码在哪更改编码?
  10. 你知道WPF这三大模板实例运用吗?
  11. stm32F4驱动AD7793程序-ADC模拟前端-应用详解,应该是最全了
  12. mongoDB--1 概念
  13. win98万能显卡驱动_win98万能显卡驱动-万能显卡驱动精灵
  14. 维纳滤波python 函数_图像维纳滤波实现(1)
  15. C/C++,pascal函数调用约定
  16. 考题篇(6.2) 07 ❀ FortiGate ❀ Fortinet 网络安全专家 NSE 4
  17. xnote1.5——WebShell
  18. pic57 c语言编程,PIC16C57C初始化头文件
  19. java计算器课程_Java课程设计——计算器团队博客
  20. 深度学习之数据处理方法概述

热门文章

  1. 货币型会员积分设计指南
  2. 精心整理 | R语言中文社区历史文章整理(类型篇)
  3. 在VMware搭建虚拟网络和NAT路由器
  4. create-react-app项目打包后遇到问题
  5. 组队学习李宏毅的深度学习-1
  6. etcd 介绍与使用
  7. 5个实用的地理位置API推荐
  8. apache的开源工具common-fileupload实现文件上传和下载
  9. java 创建gbase_GBase8s + MyBatis 操作示例
  10. 计算机应用专业国家教学标准,计算机应用专业技能教学标准