由于monitor监视器的强度范围有限,明亮的光源和明亮的区域通常难以传达给观看者。 区分显示器上明亮光源的一种方法是让它们发光; 然后光线在光源周围流淌。 这有效地为观看者提供了这些光源或明亮区域非常明亮的错觉。
这种光晕或发光效果是通过称为 Bloom 的后处理效果实现的。 Bloom 使场景的所有明亮区域都具有类似发光的效果。 下面是一个有光和没有光的场景示例(图片由 Epic Games 提供):
Bloom 提供了关于物体亮度的明显视觉线索。当以一种微妙的方式完成时(有些游戏完全无法做到),Bloom 会显着增强场景的照明,并允许产生大量的戏剧效果。
Bloom 与 HDR 渲染结合使用效果最佳。一个常见的误解是 HDR 与 Bloom 相同,因为许多人可以互换使用这两个术语。然而,它们是用于不同目的的完全不同的技术。可以使用默认的 8 位精度帧缓冲区来实现 Bloom,就像可以在没有 Bloom 效果的情况下使用 HDR 一样。很简单,HDR 使 Bloom 实现起来更有效(我们稍后会看到)。
为了实现 Bloom,我们像往常一样渲染一个明亮的场景,并提取场景的 HDR 颜色缓冲区和只有其明亮区域可见的场景图像。然后对提取的亮度图像进行模糊处理,并将结果添加到原始 HDR 场景图像之上。
让我们一步一步地说明这个过程。我们渲染了一个充满 4 个明亮光源的场景,可视化为彩色立方体。彩色光立方的亮度值介于 1.5 和 15.0 之间。如果我们将其渲染到 HDR 颜色缓冲区,则场景如下所示:
我们采用这个 HDR color buffer texture 颜色缓冲纹理并提取所有超过一定亮度的片段。 这为我们提供了一个图像,它只显示亮色区域,因为它们的片段强度超过了某个阈值:(光强超过某个值,才需要bloom)
然后我们采用这个阈值亮度纹理并模糊结果。 bloom效果的强度很大程度上取决于所使用的模糊滤镜的范围和强度。
由此产生的模糊纹理是我们用来获得发光或流光效果的。 此模糊纹理添加到原始 HDR 场景纹理之上。 由于模糊过滤器的作用,明亮区域在宽度和高度上都得到了扩展,因此场景的明亮区域看起来会发光或渗出光。
Bloom 本身并不是一项复杂的技术,但很难完全正确。 它的大部分视觉质量取决于用于模糊提取的亮度区域的模糊过滤器的质量和类型。 简单地调整模糊过滤器可以极大地改变布隆效果的质量。
遵循这些步骤为我们提供了 Bloom 后处理效果。 下图简要总结了实现 Bloom 所需的步骤:
第一步要求我们根据某个阈值提取场景的所有亮色。 让我们先深入研究一下。
第一步要求我们从渲染场景中提取两个图像(一个正常,一个亮色)。 我们可以渲染场景两次,都用不同的着色器渲染到不同的帧缓冲区,但我们也可以使用一个巧妙的小技巧,称为Multiple Render Targets多渲染目标 (MRT),它允许我们指定多个片段着色器输出; 这使我们可以选择在单个渲染通道中提取前两个图像(一个正常,一个亮色,分别输出到两个fragment)。 通过在片段着色器的输出之前指定布局位置说明符,我们可以控制片段着色器写入哪个颜色缓冲区:
layout (location = 0) out vec4 FragColor;
layout (location = 1) out vec4 BrightColor;
这仅在我们实际上有多个缓冲区要写入时才有效。 作为使用多个片段着色器输出的要求,我们需要将多个颜色缓冲区附加到当前绑定的帧缓冲区对象。 您可能还记得在帧缓冲区一章中我们可以在将纹理链接为帧缓冲区的颜色缓冲区时指定颜色附件编号。 到目前为止,我们一直使用 GL_COLOR_ATTACHMENT0,但通过使用 GL_COLOR_ATTACHMENT1,我们可以将两个颜色缓冲区附加到帧缓冲区对象:
// set up floating point framebuffer to render scene to
unsigned int hdrFBO;
glGenFramebuffers(1, &hdrFBO);
glBindFramebuffer(GL_FRAMEBUFFER, hdrFBO);
unsigned int colorBuffers[2];
glGenTextures(2, colorBuffers);
for (unsigned int i = 0; i < 2; i++)
{
glBindTexture(GL_TEXTURE_2D, colorBuffers[i]);
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_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
// attach texture to framebuffer
// 分别绑定不同的framebuffer
glFramebufferTexture2D(
GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0 + i, GL_TEXTURE_2D, colorBuffers[i], 0
);
}
我们必须明确地告诉 OpenGL 我们正在通过 glDrawBuffers 渲染到多个颜色缓冲区。 默认情况下,OpenGL 仅渲染到帧缓冲区的第一个color attachment颜色附件,而忽略所有其他颜色附件。 我们可以通过传递我们希望在后续操作中呈现的color attachment颜色附件枚举数组来做到这一点:
//初始化一个2个元素的数组
unsigned int attachments[2] = { GL_COLOR_ATTACHMENT0, GL_COLOR_ATTACHMENT1 };
// 对这个数组进行绑定
glDrawBuffers(2, attachments);
当渲染到这个帧缓冲区时,每当片段着色器使用布局位置说明符时,相应的颜色缓冲区用于渲染片段。 这很棒,因为这为我们节省了用于提取明亮区域的额外渲染通道,因为我们现在可以直接从要渲染的片段中提取它们:
#version 330 core
layout (location = 0) out vec4 FragColor;
layout (location = 1) out vec4 BrightColor;
[...]
void main()
{
[...] // first do normal lighting calculations and output results
FragColor = vec4(lighting, 1.0);
// check whether fragment output is higher than threshold, if so output as brightness color
// 点积就是向量每个子项相乘,然后相加,vec3(0.2126, 0.7152, 0.0722)代表一个阈值
float brightness = dot(FragColor.rgb, vec3(0.2126, 0.7152, 0.0722));
// 看点积来的值,有没有大于1。0
if(brightness > 1.0)
BrightColor = vec4(FragColor.rgb, 1.0);
else
BrightColor = vec4(0.0, 0.0, 0.0, 1.0);
}
在这里,我们首先照常计算光照,并将其传递给第一个片段着色器的输出变量 FragColor。然后我们使用当前存储在 FragColor 中的内容来确定其亮度是否超过某个阈值。我们首先通过适当地将片段转换为灰度来计算片段的亮度(通过取两个向量的点积,我们有效地将两个向量的每个单独分量相乘并将结果加在一起)。如果亮度超过某个阈值,我们将颜色输出到第二个颜色缓冲区。我们对光立方做同样的事情。
这也说明了为什么 Bloom 在 HDR 渲染中表现得非常好。因为我们在高动态范围内渲染,颜色值可以超过 1.0,这允许我们指定默认范围之外的亮度阈值,让我们更好地控制被认为是明亮的东西。如果没有 HDR,我们必须将阈值设置为低于 1.0,这仍然是可能的,但区域被认为是明亮的要快得多。这有时会导致发光效果变得过于突出(例如,想想白色的发光雪)。
有了这两个颜色缓冲区,我们就有了一个正常场景的图像,以及一个提取的明亮区域的图像;全部在单个渲染通道中生成。
有了提取的明亮区域的图像,我们现在需要模糊图像。 我们可以使用一个简单的 box filter 来做到这一点,就像我们在帧缓冲区一章的后处理部分所做的那样,但我们宁愿使用一个更高级(更好看)的模糊过滤器,称为Gaussian blur高斯模糊
在后处理章节的模糊中,我们取了图像周围所有像素的平均值。 虽然它确实让我们很容易模糊,但它并没有给出最好的结果。 高斯模糊基于高斯曲线,该曲线通常被描述为钟形曲线,在靠近其中心的地方给出高值,随着距离的增加逐渐消失。 高斯曲线可以用不同的形式在数学上表示,但一般具有以下形状:
由于高斯曲线靠近其中心的区域更大,因此使用其值作为权重来模糊图像会产生更自然的结果,因为靠近的样本具有更高的优先级。 例如,如果我们在一个片段周围对一个 32x32 的盒子进行采样,离片段的距离越大,我们会使用越来越小的权重; 这提供了更好和更逼真的模糊,称为高斯模糊。
为了实现高斯模糊滤波器,我们需要一个二维权重框,我们可以从二维高斯曲线方程中获得它。 然而,这种方法的问题在于它很快就会变得非常重性能。 以 32 x 32 的模糊内核为例,这将需要我们为每个片段对纹理总共采样 1024 次!
对我们来说幸运的是,高斯方程有一个非常简洁的性质,它允许我们将二维方程分成两个较小的一维方程:一个描述水平权重,另一个描述垂直权重。 然后我们首先使用场景纹理上的水平权重进行水平模糊,然后在生成的纹理上进行垂直模糊。 由于这个属性,结果完全相同,但这次为我们节省了令人难以置信的性能,因为与 1024 个相比,我们现在只需要做 32 + 32 个样本! 这被称为 two-pass Gaussian blur二次高斯模糊。
高斯模糊的片段着色器:
#version 330 core
out vec4 FragColor;
in vec2 TexCoords;
uniform sampler2D image;
uniform bool horizontal;
// 高斯模糊曲线
uniform float weight[5] = float[] (0.227027, 0.1945946, 0.1216216, 0.054054, 0.016216);
void main()
{
//一个像素占曲线的多少长度
vec2 tex_offset = 1.0 / textureSize(image, 0); // gets size of single texel
vec3 result = texture(image, TexCoords).rgb * weight[0]; // current fragment's contribution
//水平的采样一轮
if(horizontal)
{
for(int i = 1; i < 5; ++i)
{
result += texture(image, TexCoords + vec2(tex_offset.x * i, 0.0)).rgb * weight[i];
result += texture(image, TexCoords - vec2(tex_offset.x * i, 0.0)).rgb * weight[i];
}
}
else
{
//垂直的采样一轮
for(int i = 1; i < 5; ++i)
{
result += texture(image, TexCoords + vec2(0.0, tex_offset.y * i)).rgb * weight[i];
result += texture(image, TexCoords - vec2(0.0, tex_offset.y * i)).rgb * weight[i];
}
}
FragColor = vec4(result, 1.0);
}
在这里,我们采用相对较小的高斯权重样本,我们每个人都使用这些样本为当前片段周围的水平或垂直样本分配特定的权重。 您可以看到我们根据我们设置的水平均匀值将模糊过滤器分为水平和垂直部分。 我们将偏移距离基于纹理大小(来自纹理大小的 vec2)除以 1.0 获得的纹素的确切大小。
为了模糊图像,我们创建了两个基本的帧缓冲区,每个帧缓冲区都只有一个颜色缓冲区纹理:
unsigned int pingpongFBO[2];
unsigned int pingpongBuffer[2];
glGenFramebuffers(2, pingpongFBO);
glGenTextures(2, pingpongBuffer);
//创建两个帧缓冲区
for (unsigned int i = 0; i < 2; i++)
{
glBindFramebuffer(GL_FRAMEBUFFER, pingpongFBO[i]);
glBindTexture(GL_TEXTURE_2D, pingpongBuffer[i]);
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_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
glFramebufferTexture2D(
GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, pingpongBuffer[i], 0
);
}
然后在我们获得 HDR 纹理和提取的亮度纹理后,我们首先用亮度纹理填充一个乒乓帧缓冲区,然后对图像进行 10 次模糊处理(就是采样和平均化,水平 5 次,垂直 5 次):
bool horizontal = true, first_iteration = true;
int amount = 10;
shaderBlur.use();
//对图像,水平糊一遍,垂直糊一遍,一共糊10遍,糊的越多,越模糊
for (unsigned int i = 0; i < amount; i++)
{
glBindFramebuffer(GL_FRAMEBUFFER, pingpongFBO[horizontal]);
shaderBlur.setInt("horizontal", horizontal);
glBindTexture(
GL_TEXTURE_2D, first_iteration ? colorBuffers[1] : pingpongBuffers[!horizontal]
);
RenderQuad();
horizontal = !horizontal;
if (first_iteration)
first_iteration = false;
}
glBindFramebuffer(GL_FRAMEBUFFER, 0);
每次迭代我们根据我们想要水平模糊还是垂直模糊来绑定两个帧缓冲区之一,并将另一个帧缓冲区的颜色缓冲区绑定为要模糊的纹理。 第一次迭代我们专门绑定了我们想要模糊的纹理 (brightnessTexture),否则两个颜色缓冲区最终都会为空。 通过重复此过程 10 次,亮度图像最终会出现重复 5 次的完整高斯模糊。 这种结构允许我们根据需要随意模糊任何图像; 高斯模糊迭代次数越多,模糊越强。
通过将提取的亮度纹理模糊 5 次,我们得到了场景所有明亮区域的适当模糊图像。
完成Bloom效果的最后一步是将这种模糊的亮度纹理与原始场景的HDR纹理结合起来。
有了场景的 HDR 纹理和场景的模糊亮度纹理,我们只需要将两者结合即可实现著名的 Bloom 或发光效果。 在最终的片段着色器中(与我们在 HDR 章节中使用的非常相似),我们将两种纹理相加混合:
#version 330 core
out vec4 FragColor;
in vec2 TexCoords;
uniform sampler2D scene;
uniform sampler2D bloomBlur;
uniform float exposure;
void main()
{
const float gamma = 2.2;
vec3 hdrColor = texture(scene, TexCoords).rgb;
vec3 bloomColor = texture(bloomBlur, TexCoords).rgb;
// 把bloom和hdr颜色相叠加
hdrColor += bloomColor; // additive blending
// tone mapping
// hdr色调映射
vec3 result = vec3(1.0) - exp(-hdrColor * exposure);
// also gamma correct while we're at it
result = pow(result, vec3(1.0 / gamma));
FragColor = vec4(result, 1.0);
}
有趣的是,我们在应用色调映射之前添加了 Bloom 效果。 这样,bloom 的增加亮度也被柔和地转换到 LDR 范围,结果是具有更好的相对照明。
将这两种纹理加在一起,我们场景的所有明亮区域现在都获得了适当的发光效果:
彩色立方体现在看起来更亮,并且作为发光物体提供更好的错觉。 这是一个相对简单的场景,因此这里的 Bloom 效果并不太令人印象深刻,但在光线充足的场景中,如果配置正确,它会产生显着的差异。
通过沿更大半径采集更多样本或重复模糊过滤器额外次数,我们可以改善模糊效果。 由于模糊的质量与 Bloom 效果的质量直接相关,因此改进模糊步骤可以产生显着的改进。 其中一些改进将模糊过滤器与不同大小的模糊kernels内核相结合,或使用多条高斯曲线来选择性地组合权重。 Kalogirou 和 Epic Games 的额外资源讨论了如何通过改善高斯模糊来显着改善 Bloom 效果。