7-泛光
明亮的光源和区域很难表现出来,因为显示器的亮度范围有限。一种在显示器上区分明亮光源的方式是让他们发出光芒,光芒从光源向四周发散。
这种后处理效果是“泛光(Bloom)”:
简介
泛光提供了一种针对明亮物体的视觉效果。如果用优雅的方式实现它,将会显著增强场景光照并能提供更加有张力的效果。
泛光和HDR结合使用效果最好。泛光和HDR不一样,可以用默认8位精确度的帧缓冲来实现泛光效果,也能只用HDR而不使用泛光。但有了HDR之后再实现泛光就更简单了。
实现思路
为了实现泛光,需要像平时那样渲染一个有光场景,提取出场景的HDR颜色缓冲以及只有这个场景明亮区域可见的图片。然后对提取的亮度图像进行模糊处理,并将结果添加到原始HDR场景图像的上面。
如上图所示,泛光的实现思路如下:
- 左上的图片:渲染场景到HDR颜色缓冲;
- 左下的图片:根据HDR颜色缓冲纹理,提取所有超出一定亮度的片元,这样就会获得一个只有亮光源的一片区域;
- 中下的图片:对左下的图片进行模糊处理;
- 右上的图片:将1和3结合。
提取亮色
在第一步中,要从渲染出来的场景里提取两张图片。可以渲染场景两次,每次用一个不同的着色器渲染到不同的帧缓冲中,但这样太麻烦了。
有一种叫做多渲染目标(Multiple Render Targets, MRT)的技巧,可以通过一次渲染获得多张图片。这需要我们在片段着色器上指定要输出的颜色缓冲:
layout (location = 0) out vec4 FragColor;
layout (location = 1) out vec4 BrightColor;
接下来需要为帧缓冲准备多个颜色附件:
// 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
glFramebufferTexture2D(
GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0 + i, GL_TEXTURE_2D, colorBuffers[i], 0
);
}
然后要显式通知OpenGL我们要一次性渲染到多个颜色附件上:
unsigned int attachments[2] = {GL_COLOR_ATTACHMENT0, GL_COLOR_ATTACHMENT1};
glDrawBuffers(2, attachments);
最后我们完善一下片段着色器,将过亮的像素存储到第二个颜色附件中:
void main()
{
// 求出FragColor
...
// 存储过亮的像素
float brightness = dot(FragColor.rgb, vec3(0.2126, 0.7152, 0.0722));
if (brightness > 1.0)
BrightColor = vec4(FragColor.rgb, 1.0);
else
BrightColor = vec4(vec3(0.0), 1.0);;
}
这里先将颜色转换为灰度作为片段的亮度,然后查看亮度是否超过阈值,是的话就渲染到第二个颜色附件中。两个颜色附件如下:
高斯模糊
在之前的后处理中,我们使用的是均值模糊,但效果不好。高斯模糊则基于高斯曲线,中间的值最大,随着距离增加两边的值不断减少:
为了实现高斯模糊滤波,需要一个二维滤波核,可以从二维高斯曲线方程中获得权重。过大的二维滤波核会导致性能问题,但高斯方程允许我们将大的二维滤波核分成两个一维滤波核,分别为水平和竖直方向,这样大大提高了性能。这种做法也叫做两步高斯模糊。
因此我们要模糊一张图片两次,当然要使用帧缓冲对象。我们将实现“乒乓”帧缓冲,以应用两步高斯模糊。这是一对帧缓冲对象,在这里将会交换&渲染帧缓冲图片数次,一个专门负责横向滤波,另一个专门负责竖向滤波。
首先看看高斯模糊的片段着色器实现:
#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); // 获取单个纹素的大小
vec3 result = texture(image, TexCoords).rgb * weight[0]; // 当前片段的贡献
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);
}
这里我们使用一个相对小的高斯权重,每一个都被用来指定水平/垂直样本的权重。通过设置horizontal
变量,可以选择水平/竖直滤波。
接下来看看乒乓FBO的实现,是由两个只有一个颜色附件的FBO组成:
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贴图和亮度贴图后,我们首先将乒乓FBO的其中一个绑定亮度贴图,然后将它模糊10次(5次水平,5次竖直):
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);
模糊后的贴图如下:
混合图片
接下来就要将模糊光贴图和HDR贴图混合了,在最终的片段着色器中,通过叠加的方式进行混合:
#version 330 core
out vec4 FragColor;
in vec2 TexCoords;
uniform sampler2D hdrScene;
uniform sampler2D bloomBlur;
uniform float exposure;
void main()
{
// 叠加混合
vec3 hdrColor = texture(hdrScene, TexCoords).rgb;
vec3 bloomColor = texture(bloomBlur, TexCoords).rgb;
hdrColor += bloomColor;
// exposure tone mapping
vec3 mapped = vec3(1.0) - exp(-hdrColor * exposure);
FragColor = vec4(mapped, 1.0);
}
最终的结果如下:
参考资料
- 泛光 - LearnOpenGL CN (learnopengl-cn.github.io)
- LearnOpenGL - Bloom