
布卢姆
由于常规监视器可用的亮度范围有限,因此很难说服令人信服地显示明亮的光源和明亮的表面。 在监视器上突出显示明亮区域的常用方法之一是一种在明亮物体周围增加光晕的技术,使光在光源外部“散布”。 结果,观察者给人这种照明区域或光源高亮度的印象。
所描述的光晕和光从光源射出的效果是通过称为
Bloom的后处理技术实现的。 应用效果会在显示的场景的所有明亮区域中添加特有的发光光晕,可以在以下示例中看到:
Bloom会从图像上为图像增添独特的视觉线索,即光晕所覆盖物体的显着亮度。 通过选择性地并精确地应用(很多游戏,可惜无法应付),该效果可以显着改善场景中使用的照明的视觉表现力,并在某些情况下增加戏剧性。
这项技术与
HDR渲染结合使用几乎是不言而喻的。 显然,因此,许多人错误地将这两个术语混为一谈,以实现完全互换性。 但是,这些技术是完全独立的,可用于不同的目的。 可以使用默认的具有8位色深的帧缓冲区来实现Bloom,就像应用HDR渲染而无需诉诸Bloom一样。 唯一的事情是HDR渲染使您能够以更有效的方式实现效果(我们将在后面看到)。
为了实现绽放,首先以常规方式渲染照明场景。 接下来,提取HDR颜色缓冲器和仅包含场景的明亮部分的颜色缓冲器。 然后,将提取的亮部分图像模糊并覆盖在场景的原始HDR图像上。
为了更加清楚,我们将逐步分析过程。 渲染包含4个明亮的光源的场景,显示为彩色立方体。 它们的亮度值都在1.5到15.0的范围内。 如果将颜色缓冲区输出到HDR,则结果如下:
从此HDR颜色缓冲区中,我们提取亮度超过预定限制的所有片段。 结果是只包含明亮区域的图像:
此外,明亮区域的图像模糊。 效果的严重程度基本上取决于所应用的模糊滤镜的强度和半径:
明亮区域产生的模糊图像是明亮物体周围光环最终效果的基础。 该纹理仅与场景的原始HDR图像混合。 由于明亮的区域模糊,因此它们的大小增加了,最终提供了超出光源范围的光度视觉效果:
如您所见,bloom不是最复杂的技术,但是实现其高视觉质量和可靠性并不总是那么容易。 在大多数情况下,效果取决于所应用的模糊滤镜的质量和类型。 即使过滤器参数发生很小的变化,也会极大地改变设备的最终质量。
因此,以上动作为我们提供了针对光晕效果的后处理效果的分步算法。 下图总结了所需的操作:
首先,我们需要基于给定的阈值获取有关场景明亮部分的信息。 这就是我们要做的。
提取亮点
因此,对于初学者来说,我们需要根据场景获取两个图像。 渲染两次会很幼稚,但是使用更高级的“
多重渲染目标” (
MRT )方法:我们在最终的片段着色器中指定了多个输出,因此,一次可以提取两个图像! 若要指定将在哪个颜色缓冲区中输出着色器,请使用
布局说明符:
layout (location = 0) out vec4 FragColor; layout (location = 1) out vec4 BrightColor;
当然,该方法仅在我们准备好几个写入缓冲区的情况下才有效。 换句话说,要实现片段着色器的多个输出,此刻使用的帧缓冲区应包含足够数量的已连接颜色缓冲区。 如果我们转向有关
帧缓冲区的课程,那么可以回想一下,将纹理绑定为颜色缓冲区时,我们可以指示
颜色附加编号 。 到目前为止,我们不需要使用
GL_COLOR_ATTACHMENT0以外的附件,但是这次
GL_COLOR_ATTACHMENT1将派上用场,因为我们需要一次记录两个目标:
另外,通过调用
glDrawBuffers ,您将需要明确告诉OpenGL我们将输出到多个缓冲区。 否则,该库仍将仅输出到第一个附件,而忽略对其他附件的写入操作。 作为该函数的参数,传递了来自相应枚举的已使用附件的标识符的数组:
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() { [...]
在此片段中,省略了包含用于计算照明的典型代码的部分。 其结果将写入着色器的第一个输出
-FragColor变量。 接下来,片段的所得颜色用于计算亮度值。 为此,进行了灰度级的加权平移(通过标量乘法,我们将向量的相应分量相乘并将它们相加在一起,从而得到单个值)。 然后,当超过某个阈值的片段的亮度超过时,我们将其颜色记录在着色器的第二个输出中。 对于替换光源的立方体,还将执行此着色器。
弄清楚了算法之后,我们可以理解为什么这种技术在HDR渲染中如此有效。 使用HDR格式进行渲染时,颜色分量可以超过1.0的上限,这使您可以在标准间隔[0.,1.]之外更灵活地调整亮度阈值,从而可以微调场景中被认为是明亮的部分。 如果不使用HDR,您将必须满足[0.,1.]区间的亮度阈值,这是可以接受的,但是会导致亮度的“锐利”截止,这通常会使绽放的效果过于刺眼和浮华(想象自己在高山雪场上) 。
在执行着色器后,两个目标缓冲区将包含场景的普通图像以及仅包含明亮区域的图像。
现在应该使用模糊处理明亮区域的图像。 您可以使用一个简单的矩形(
框形 )过滤器来完成此操作,该过滤器在
帧缓冲区课程的后处理部分中使用过。 但是通过
高斯滤波可以获得更好的结果。
高斯模糊
后处理课程为我们提供了使用相邻图像片段的简单颜色平均进行模糊处理的想法。 这种模糊方法很简单,但是生成的图像可能看起来更具吸引力。 高斯模糊基于同名的钟形分布曲线:函数的较高值位于曲线的中心附近,并且落在曲线的两侧。 在数学上,高斯曲线可以用不同的参数表示,但是曲线的一般形式仍然如下:
基于高斯曲线值的权重模糊看起来比矩形滤镜好得多:由于该曲线在其中心附近具有较大的面积,这对应于滤芯中心附近的片段的权重较大。 以32x32内核为例,我们将使用权重因子越小,则片段离中央片段越远。 正是这种滤波器特性在视觉上提供了令人满意的高斯模糊效果。
滤波器的实现将需要一个加权系数的二维数组,该数组可以基于描述高斯曲线的二维表达式进行填充。 但是,我们将立即遇到性能问题:即使32x32片段中的模糊核心相对较小,处理后的图像的每个片段也需要1024个纹理样本!
对我们来说幸运的是,高斯曲线的表达式具有非常方便的数学特性-可分离性,这将使从一个描述水平分量和垂直分量的二维表达式中生成两个一维表达式成为可能。 这将允许依次采用两种方法进行模糊处理:水平,然后垂直使用对应于每个方向的权重集进行模糊。 生成的图像与处理二维算法时的图像相同,但是所需的视频处理器处理能力要低得多:我们不需要纹理中的1024个样本,而只需32 + 32 = 64! 这是两遍高斯滤波的本质。
对我们来说,所有这些都意味着一件事:一张图像的模糊将必须进行两次,并且在这里使用帧缓冲对象将非常方便。 我们应用所谓的乒乓技术:有几个帧缓冲区对象,并且通过一些处理将一个帧缓冲区的颜色缓冲区的内容渲染到当前帧缓冲区的颜色缓冲区中,然后将源帧缓冲区和帧缓冲区接收器互换,并重复此过程给定次数。 实际上,仅切换了用于显示图像的当前帧缓冲区,并由此切换了从中执行采样以进行渲染的当前纹理。 该方法允许您通过将原始图像放在第一个帧缓冲区中来对其进行模糊处理,然后对第一个帧缓冲区的内容进行模糊处理,然后将其放在第二个中,然后对第二个图像进行模糊处理,然后将其放在第一个中,依此类推。
在转到帧缓冲区调整代码之前,让我们看一下高斯模糊着色器代码:
#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() {
如您所见,我们使用了一个很小的高斯曲线系数样本,用作相对于当前片段水平或垂直采样的权重。 该代码有两个主要分支,它们根据
水平均匀值将算法分为垂直和水平通过。 每个样本的偏移设置为等于纹理像素大小,该纹理大小定义为纹理大小的倒数(由
textureSize ()函数返回的
vec2类型的值)。
创建两个基于纹理的帧缓冲区,其中包含一个颜色缓冲区:
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_RGB16F, SCR_WIDTH, SCR_HEIGHT, 0, GL_RGB, 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纹理并提取明亮区域的纹理之后,我们用亮度纹理填充一对准备好的帧缓冲区之一的颜色缓冲区,并开始乒乓处理十次(垂直五次,水平五次):
bool horizontal = true, first_iteration = true; int amount = 10; shaderBlur.use(); 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 )-否则两个乒乓帧缓冲区都将保持空白。 经过十次传递后,原始图像采用了由完整的高斯滤镜模糊处理五次的形式。 使用的方法使我们可以轻松地更改模糊程度:乒乓迭代次数越多,模糊程度就越强。
在我们的例子中,模糊结果看起来像这样:
为了完成效果,仅需将模糊图像与场景的原始HDR图像结合在一起。
纹理融合
拥有渲染场景的HDR纹理和过度曝光区域的模糊纹理之后,您要实现著名的光晕效果或光晕,只需要将这两个图像组合即可。 最终的片段着色器(与本课程中有关
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; hdrColor += bloomColor;
查找内容:混合是在应用
色调映射之前完成的。 这将正确地将效果中的额外亮度转换为LDR(
低动态范围 )范围,同时保持场景中的相对亮度分布。
处理的结果-所有亮区均收到明显的发光效果:
现在,替换光源的多维数据集看起来更亮,可以更好地传达光源的印象。 这个场景是很原始的,因为不会引起特殊热情的影响,但是在复杂的场景中,如果考虑周到的灯光,定性实现的绽放可能是增加戏剧性的关键视觉元素。
示例的源代码在
这里 。
我注意到,该课程使用了一个非常简单的过滤器,每个方向上只有五个样本。 通过在更大的半径中制作更多的样本或对滤镜进行几次迭代,可以从视觉上改善效果。 同样,值得一提的是,视觉上整个效果的质量直接取决于所使用的模糊算法的质量。 通过改进过滤器,可以实现显着的改进和整体效果。 例如,将几个具有不同磁芯尺寸或不同高斯曲线的滤波器组合在一起,将显示出更令人印象深刻的结果。 以下是Kalogirou和EpicGames的其他资源,这些资源介绍了如何通过修改高斯模糊来改善光晕质量。
其他资源