文章目录
- 一、说明
- 二、帧缓冲区
- 三、创建新的帧缓冲区
- 四、附属装饰
- 4.1 纹理图像
- 4.2 渲染缓冲区对象图像
- 五、使用帧缓冲区
- 5.1 后期处理
- 5.2 更改代码
- 六、后期处理效果
- 6.1 色彩处理
- 6.2 模糊
- 6.3 Sobel算子
- 七、结论
- 练习
一、说明
关于FrameBuffer的使用,是OpenGL的高级使用方式,而这种新诞生的功能有些未知的技术需要逐步熟悉,本篇就是针对它的图像处理功能展开陈述。
二、帧缓冲区
在前面的章节中,我们了解了 OpenGL 提供的不同类型的缓冲区:颜色、深度和模板缓冲区。这些缓冲区像任何其他 OpenGL 对象一样占用视频内存,但到目前为止,除了在创建 OpenGL 上下文时指定像素格式之外,我们几乎无法控制它们。这种缓冲区组合称为默认帧缓冲区,正如您所见,帧缓冲区是内存中可以渲染的区域。如果您想获取渲染结果并对其进行一些附加操作(例如许多现代游戏中的后处理),该怎么办?
在本章中,我们将讨论帧缓冲区对象,这是创建额外的帧缓冲区以进行渲染的一种方法。帧缓冲区的伟大之处在于它们允许您将场景直接渲染到纹理,然后可以在其他渲染操作中使用该纹理。在讨论了帧缓冲区对象的工作原理之后,我将向您展示如何使用它们对上一章的场景进行后处理。
三、创建新的帧缓冲区
您需要的第一件事是一个帧缓冲区对象来管理新的帧缓冲区。
GLuint frameBuffer;
glGenFramebuffers(1, &frameBuffer);
此时您还不能使用此帧缓冲区,因为它还不完整。在以下情况下,帧缓冲区通常是完整的:
至少已附加一个缓冲区(例如颜色、深度、模板)
必须至少有一种颜色附件(OpenGL 4.1 及更早版本)
所有附件均已完成(例如,纹理附件需要保留内存)
所有附件必须具有相同数量的多重样本
您可以随时通过调用glCheckFramebufferStatus并检查它是否返回来检查帧缓冲区是否完整GL_FRAMEBUFFER_COMPLETE。其他返回值请参阅参考资料。您不必执行此检查,但验证通常是一件好事,就像检查着色器是否成功编译一样。
现在,让我们绑定帧缓冲区来使用它。
glBindFramebuffer(GL_FRAMEBUFFER, frameBuffer);
第一个参数指定帧缓冲区应附加到的目标。OpenGL 在这里区分了GL_DRAW_FRAMEBUFFER和GL_READ_FRAMEBUFFER。绑定到 read 的帧缓冲区用于 的调用glReadPixels,但由于这种区别在普通应用程序中相当罕见,因此您可以使用 将您的操作应用于两者GL_FRAMEBUFFER。
glDeleteFramebuffers(1, &frameBuffer);
完成后别忘了清理。
四、附属装饰
仅当已分配内存来存储结果时,您的帧缓冲区才能用作渲染目标。这是通过为每个缓冲区附加图像(颜色、深度、模板或深度和模板的组合)来完成的。有两种对象可以用作图像:纹理对象和渲染缓冲区对象。前者的优点是它们可以直接在着色器中使用,如前面的章节所示,但根据您的实现,渲染缓冲区对象可能会作为渲染目标进行更优化。
4.1 纹理图像
我们希望能够渲染一个场景,然后在另一个渲染操作中使用颜色缓冲区中的结果,因此纹理在这种情况下是理想的选择。创建纹理作为新帧缓冲区颜色缓冲区的图像与创建任何纹理一样简单。
GLuint texColorBuffer;
glGenTextures(1, &texColorBuffer);
glBindTexture(GL_TEXTURE_2D, texColorBuffer);
glTexImage2D(
GL_TEXTURE_2D, 0, GL_RGB, 800, 600, 0, GL_RGB, GL_UNSIGNED_BYTE, NULL
);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
该纹理与您之前见过的纹理之间的区别在于NULLdata 参数的值。这是有道理的,因为这次将通过渲染操作动态创建数据。由于这是颜色缓冲区的图像,因此format和internalformat参数受到更多限制。该format参数通常仅限于 或GL_RGB以及GL_RGBA颜色internalformat格式。
我在这里选择了默认的 RGB 内部格式,但您可以尝试更奇特的格式,例如GL_RGB10如果您想要 10 位颜色精度。我的应用程序的分辨率为 800 x 600 像素,因此我使这个新的颜色缓冲区与该分辨率相匹配。分辨率不必与默认帧缓冲区相匹配,但glViewport如果您决定改变,请不要忘记调用。
剩下的一件事是将图像附加到帧缓冲区。
glFramebufferTexture2D(
GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, texColorBuffer, 0 );
第二个参数意味着您可以有多个颜色附件。片段着色器可以通过使用我们之前使用的函数将out变量链接到附件来向其中任何一个输出不同的数据。glBindFragDataLocation我们现在将坚持使用一种输出。最后一个参数指定图像应附加到的 mipmap 级别。 Mipmapping 没有任何用处,因为在使用颜色缓冲区图像进行后期处理时,它将以其原始大小进行渲染。
4.2 渲染缓冲区对象图像
由于我们使用深度和模板缓冲区来渲染可爱的旋转立方体,因此我们也必须创建它们。 OpenGL 允许您将它们组合成一张图像,因此我们必须再创建一张图像才能使用帧缓冲区。尽管我们可以通过创建另一个纹理来做到这一点,但将这些缓冲区存储在渲染缓冲区对象中会更有效,因为我们只对读取着色器中的颜色缓冲区感兴趣。
GLuint rboDepthStencil;
glGenRenderbuffers(1, &rboDepthStencil);
glBindRenderbuffer(GL_RENDERBUFFER, rboDepthStencil);
glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH24_STENCIL8, 800, 600);
创建渲染缓冲区对象与创建纹理非常相似,不同之处在于该对象被设计用作图像而不是像纹理那样的通用数据缓冲区。我在这里选择了GL_DEPTH24_STENCIL8内部格式,它适合分别保存 24 位和 8 位精度的深度缓冲区和模板缓冲区。
glFramebufferRenderbuffer(
GL_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_RENDERBUFFER, rboDepthStencil );
连接起来也很容易。您可以稍后通过调用删除该对象,就像删除任何其他对象一样glDeleteRenderbuffers。
五、使用帧缓冲区
选择帧缓冲区作为渲染目标非常容易,实际上只需一次调用即可完成。
glBindFramebuffer(GL_FRAMEBUFFER, frameBuffer);
在此调用之后,所有渲染操作都将其结果存储在新创建的帧缓冲区的附件中。要切换回屏幕上可见的默认帧缓冲区,只需传递0.
glBindFramebuffer(GL_FRAMEBUFFER, 0);
请注意,虽然只有默认的帧缓冲区在屏幕上可见,但您可以读取当前与调用绑定的任何帧缓冲区,glReadPixels只要它不仅绑定到GL_DRAW_FRAMEBUFFER.
5.1 后期处理
在当今的游戏中,后处理效果似乎几乎与屏幕上渲染的实际场景一样重要,并且实际上可以使用不同的技术来实现一些令人惊叹的结果。实时图形中的后处理效果通常在片段着色器中实现,并将渲染的场景作为纹理形式的输入。帧缓冲区对象允许我们使用纹理来包含颜色缓冲区,因此我们可以使用它们为后处理效果准备输入。
要使用着色器为先前渲染到纹理的场景创建后处理效果,通常将其渲染为屏幕填充的 2D 矩形。这样,应用了效果的原始场景就会以其原始大小填充屏幕,就好像它首先被渲染到默认帧缓冲区一样。
当然,您可以利用帧缓冲区发挥创意,通过从不同角度多次渲染场景并将其显示在监视器或最终图像中的其他对象上,使用它们在游戏世界中执行从门户到摄像机的任何操作。这些用途更加具体,因此我将它们作为练习留给您。
5.2 更改代码
不幸的是,在这里逐步介绍对代码的更改有点困难,特别是如果您偏离了此处的示例代码。现在您已经了解了帧缓冲区是如何创建和绑定的,并且只要小心一点,您应该能够做到这一点。让我们全局地完成这里的步骤。
首先尝试创建帧缓冲区并检查它是否完整。尝试将其绑定为渲染目标,您会看到屏幕变黑,因为场景不再渲染到默认帧缓冲区。尝试更改场景的清晰颜色并读取它以glReadPixels检查场景是否正确渲染到新的帧缓冲区。
接下来,尝试创建一个新的着色器程序、顶点数组对象和顶点缓冲区对象,以 2D(而不是 3D)渲染事物。切换回默认帧缓冲区非常有用,这样可以轻松查看结果。您的 2D 着色器不需要变换矩阵。尝试以这种方式在 3D 旋转立方体场景前面渲染一个矩形。
最后,尝试将 3D 场景渲染到您创建的帧缓冲区,并将矩形渲染到默认帧缓冲区。现在尝试使用矩形中帧缓冲区的纹理来渲染场景。
我选择仅使用 2 个位置坐标和 2 个纹理坐标进行 2D 渲染。我的 2D 着色器如下所示:
#version 150 core
in vec2 position;
in vec2 texcoord;
out vec2 Texcoord;
void main()
{
Texcoord = texcoord;
gl_Position = vec4(position, 0.0, 1.0);
}
#version 150 core
in vec2 Texcoord;
out vec4 outColor;
uniform sampler2D texFramebuffer;
void main()
{
outColor = texture(texFramebuffer, Texcoord);
}
使用此着色器,程序的输出应该与您了解帧缓冲区之前相同。渲染一帧大致如下所示:
// Bind our framebuffer and draw 3D scene (spinning cube)
glBindFramebuffer(GL_FRAMEBUFFER, frameBuffer);
glBindVertexArray(vaoCube);
glEnable(GL_DEPTH_TEST);
glUseProgram(sceneShaderProgram);
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, texKitten);
glActiveTexture(GL_TEXTURE1);
glBindTexture(GL_TEXTURE_2D, texPuppy);
// Draw cube scene here
// Bind default framebuffer and draw contents of our framebuffer
glBindFramebuffer(GL_FRAMEBUFFER, 0);
glBindVertexArray(vaoQuad);
glDisable(GL_DEPTH_TEST);
glUseProgram(screenShaderProgram);
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, texColorBuffer);
glDrawArrays(GL_TRIANGLES, 0, 6);
3D 和 2D 绘图操作都有自己的顶点数组(立方体与四边形)、着色器程序(3D 与 2D 后处理)和纹理。您可以看到,绑定颜色缓冲区纹理与绑定常规纹理一样简单。请注意,glBindTexture更改 OpenGL 状态之类的调用相对昂贵,因此请尝试将它们保持在最低限度。
我认为,无论我在这里解释程序的总体结构有多好,你们中的一些人只是喜欢看一些新的示例代码,也许还可以运行diff它以及上一章中的代码。
六、后期处理效果
我现在将讨论各种有趣的后处理效果、它们的工作原理以及它们的外观。
6.1 色彩处理
反转颜色是图像处理程序中常见的一个选项,但您也可以使用着色器自己完成!
由于颜色值是从0.0到 1.0的浮点值,因此反转通道就像计算一样简单1.0 - channel。如果对每个通道(红色、绿色、蓝色)执行此操作,您将得到反转的颜色。在片段着色器中,可以这样完成。
outColor = vec4(1.0, 1.0, 1.0, 1.0) - texture(texFramebuffer, Texcoord);
这也会影响 alpha 通道,但这并不重要,因为 alpha 混合默认情况下是禁用的。
通过计算每个通道的平均强度可以简单地实现颜色灰度化。
outColor = texture(texFramebuffer, Texcoord);
float avg = (outColor.r + outColor.g + outColor.b) / 3.0;
outColor = vec4(avg, avg, avg, 1.0);
这工作得很好,但人类对绿色最敏感,对蓝色最不敏感,因此更好的转换可以使用加权通道。
outColor = texture(texFramebuffer, Texcoord);
float avg = 0.2126 * outColor.r + 0.7152 * outColor.g + 0.0722 * outColor.b;
outColor = vec4(avg, avg, avg, 1.0);
6.2 模糊
有两种众所周知的模糊技术:框模糊和高斯模糊。后者会产生更高质量的结果,但前者更容易实现,并且仍然相当接近高斯模糊。
模糊是通过对像素周围的像素进行采样并计算平均颜色来完成的。
const float blurSizeH = 1.0 / 300.0;
const float blurSizeV = 1.0 / 200.0;
void main()
{
vec4 sum = vec4(0.0);
for (int x = -4; x <= 4; x++)
for (int y = -4; y <= 4; y++)
sum += texture(
texFramebuffer,
vec2(Texcoord.x + x * blurSizeH, Texcoord.y + y * blurSizeV)
) / 81.0;
outColor = sum;
}
您可以看到总共采集了 81 个样本。您可以更改 X 轴和 Y 轴上的样本量来控制模糊量。这些blurSize变量用于确定每个样本之间的距离。较高的样本数和较低的样本距离会产生更好的近似值,但也会迅速降低性能,因此请尝试找到一个良好的平衡点。
6.3 Sobel算子
Sobel算子常用于边缘检测算法中,我们来看看它长什么样。
片段着色器如下所示:
vec4 top = texture(texFramebuffer, vec2(Texcoord.x, Texcoord.y + 1.0 / 200.0));
vec4 bottom = texture(texFramebuffer, vec2(Texcoord.x, Texcoord.y - 1.0 / 200.0));
vec4 left = texture(texFramebuffer, vec2(Texcoord.x - 1.0 / 300.0, Texcoord.y));
vec4 right = texture(texFramebuffer, vec2(Texcoord.x + 1.0 / 300.0, Texcoord.y));
vec4 topLeft = texture(texFramebuffer, vec2(Texcoord.x - 1.0 / 300.0, Texcoord.y + 1.0 / 200.0));
vec4 topRight = texture(texFramebuffer, vec2(Texcoord.x + 1.0 / 300.0, Texcoord.y + 1.0 / 200.0));
vec4 bottomLeft = texture(texFramebuffer, vec2(Texcoord.x - 1.0 / 300.0, Texcoord.y - 1.0 / 200.0));
vec4 bottomRight = texture(texFramebuffer, vec2(Texcoord.x + 1.0 / 300.0, Texcoord.y - 1.0 / 200.0));
vec4 sx = -topLeft - 2 * left - bottomLeft + topRight + 2 * right + bottomRight;
vec4 sy = -topLeft - 2 * top - topRight + bottomLeft + 2 * bottom + bottomRight;
vec4 sobel = sqrt(sx * sx + sy * sy);
outColor = sobel;
就像模糊着色器一样,以有趣的方式采集并组合一些样本。您可以在其他地方阅读有关技术细节的更多信息。
七、结论
着色器的一个很酷的事情是,由于显卡具有巨大的并行处理能力,您可以实时地按像素操作图像。毫不奇怪,像 Photoshop 这样的新版本软件使用显卡来加速图像处理操作!还有许多更复杂的效果,例如 HDR、运动模糊和 SSAO(屏幕空间环境光遮挡),但这些效果比单个着色器涉及的工作量要多一些,因此它们超出了本章的范围。
练习
尝试通过添加另一个帧缓冲区来实现两次通过的高斯模糊效果。 (解决方案)
尝试在 3D 场景中添加一个面板,从不同角度显示该场景。 (解决方案)