在现代游戏中,将几何体渲染到帧缓冲区并不是渲染过程的结束。在缓冲区显示之前,还会对其应用其他后处理技术,如色调映射、Bloom或模糊等。本篇将概述在图形应用程序中执行此类后处理所需的基本技术。
Frame Buffer Objects
除了用于渲染的前置和后置缓冲区,OpenGL还支持渲染到多个目标缓冲区,称为帧缓冲区对象。这些帧缓冲区对象(FBO)包含许多单独的纹理,称为附件,其中包含颜色、深度和模板信息。我们要渲染的几何图形可以被重定向到这些FBO中的任意一个,或者作为正常的渲染目标输出到屏幕上,我们可以将标准的Back Buffer视为一个默认内置的FBO,它包含了深度和模板及颜色缓冲区的组合,并且它总是可用的。
一个FBO有可选的深度,模板和颜色附件,但必须至少有一个附件存在,否则FBO的创建就没有意义。FBO也支持多个渲染目标,也就是说,一个片元着色器的处理结果可以写入多个颜色附件。支持的颜色附件的数量取决于不同计算机的GPU,但可能能支持多达8个独立的附件。为什么需要支持渲染不同的颜色附件/纹理呢?好吧,我们可能想要以每个片元为基础存储场景的法线,以便对它们进行一些额外的处理,或者将高于某个亮度的片元写入单独的渲染目标中,以便在之后的阶段模糊它们。这就是FBO渲染用处——无论我们需要什么样的渲染信息,我们都可以通过片元的处理结果存储到目标附件纹理中。
这些附件(Attachments)字面上只是普通的OpenGL 2D纹理,因此可以这样使用:比如先渲染一帧的画面到FBO,再使用该FBO的颜色附件作为另一个FBO的纹理数据源进行渲染。有一点需要注意的是,作为附件使用的纹理不能是压缩类型的,这无论如何都是不可取的。因为在渲染几何体时压缩和重新压缩像素数据是个很大的开销。此外,根据图形硬件支持的确切扩展,所有的附件一般需要具有相同的尺寸。
虽然FBO不支持压缩纹理,但它们支持打包纹理(packed textures)。当用FBO模拟常用的图形管道时,通常有一个32位的颜色附件,分别为R,G,B,A四个颜色通道提供8位大小空间,并有一个24位的深度缓冲区。然而,有些图形硬件要求所有附件具有相同的位大小。一个简单的解决方案是只指定一个32位的深度缓冲区,并利用它带来的额外精度,但如果也为一个模板附件指定每像素32位的模板数据可能就有点过分了(在Scissor Regions与Stencil Buffer中提到模板只需要8位即可)。这就是packed textures的用处所在。代替单独的模板和深度纹理,可以有一个组合的packed textures,深度信息指定24位,模板信息指定8位(如: GL_DEPTH24_STENCIL8就能保证所有附件位大小都为32位)。
Post Processing
能够将整个场景渲染成一张可访问的纹理打开了广泛的后期处理技术之门,如模糊、图像锐化、单元格阴影、运动模糊等。简单的讲,在Photoshop中可以对图像处理的任何操作,借助FBO实现的后处理技术同样可以做到。在OpenGL中执行后期处理操作的常用方法是通过渲染一个铺满屏幕大小的四边形,使用FBO颜色附件作为其纹理,并通过指定算法的后期处理着色器对其进一步处理,后期处理着色器的结果将会替换原来屏幕的画面,而该处理结果又可作为另一个后期处理着色器的输入。
通过将场景渲染成FBO,并将生成的颜色纹理绑定到一个四边形上,这个四边形可以被渲染到整个屏幕,并应用一个后期处理着色器进行加工
Ping-pong texturing
在渲染多个后期处理效果的过程中,新增的概念是ping-pong texturing。这是通过使用两张颜色纹理实现的,即在后期处理阶段交替使用它们作为输入和输出——这可以看作是渲染过程中与前后缓冲区的交换机制类似。通过该机制可以用于连续应用相同的后期处理效果的情况,如模糊,或应用大量额外的后期处理着色器-不管需要多少后期处理,只需要一个输入和一个输出纹理,然后在他们之间来回翻转处理即可完成。
带有纹理A的后处理四边形通过FBO渲染成纹理B。然后是纹理B通过另一个后处理四边形渲染到纹理A,然后再次重复这个过程
#include "Renderer.h"
Renderer::Renderer(Window& parent) : OGLRenderer(parent) {
camera = new Camera(0.0f, 135.0f, Vector3(1000, 500, 1000));
quad = Mesh::GenerateQuad();
// 加载高程图数据
heightMap = new HeightMap(TEXTUREDIR"terrain.raw");
// 设置地形纹理
heightMap -> SetTexture(
loadtexture(TEXTUREDIR"Barren Reds.JPG",
1, 1, 0));
// 设置绘制场景的着色器
sceneShader = new Shader(SHADERDIR"TexturedVertex.glsl",
SHADERDIR"TexturedFragment.glsl");
// 设置后处理着色器,对绘制后的场景图像应用高斯模糊
processShader = new Shader(SHADERDIR"TexturedVertex.glsl",
SHADERDIR"ProcessFrag.glsl");
if (!processShader -> LinkProgram() || !sceneShader -> LinkProgram() ||
!heightMap -> GetTexture()) {
return;
}
// 设置地形纹理为重复环绕方式
SetTextureRepeating(heightMap -> GetTexture(), true);
// 创建深度模板纹理
glGenTextures(1, &bufferDepthTex);
glBindTexture(GL_TEXTURE_2D, bufferDepthTex);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
// 设置深度模板的数据格式为GL_DEPTH24_STENCIL8,这就是packedTexture的实际应用,使其与颜色纹理都为32位
glTexImage2D(GL_TEXTURE_2D, 0, GL_DEPTH24_STENCIL8, width, height, 0,
GL_DEPTH_STENCIL, GL_UNSIGNED_INT_24_8, nullptr);
for (int i = 0; i < 2; ++i) {
// 创建颜色模板纹理
glGenTextures(1, &bufferColourTex[i]);
glBindTexture(GL_TEXTURE_2D, bufferColourTex[i]);
glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP);
glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP);
glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
// GL_RGBA8设置了RGBA四通道位为8,总共32位
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, width, height, 0,
GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
}
// 生成场景FBO
glGenFramebuffers(1, &bufferFBO); // We ’ll render the scene into this
// 生成后处理FBO
glGenFramebuffers(1, &processFBO); // And do post processing in this
// 默认先绘制场景,绑定场景的FBO
glBindFramebuffer(GL_FRAMEBUFFER, bufferFBO);
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT,
GL_TEXTURE_2D, bufferDepthTex, 0);
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_STENCIL_ATTACHMENT,
GL_TEXTURE_2D, bufferDepthTex, 0);
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0,
GL_TEXTURE_2D, bufferColourTex[0], 0);
// We can check FBO attachment success using this command !
if (glCheckFramebufferStatus(GL_FRAMEBUFFER) !=
GL_FRAMEBUFFER_COMPLETE || !bufferDepthTex || !bufferColourTex[0]) {
return;
}
glBindFramebuffer(GL_FRAMEBUFFER, 0);
glEnable(GL_DEPTH_TEST);
init = true;
}
Renderer ::~Renderer(void) {
delete sceneShader;
delete processShader;
currentShader = nullptr;
delete heightMap;
delete quad;
delete camera;
glDeleteTextures(2, bufferColourTex);
glDeleteTextures(1, &bufferDepthTex);
glDeleteFramebuffers(1, &bufferFBO);
glDeleteFramebuffers(1, &processFBO);
}
void Renderer::UpdateScene(float msec) {
// 第一人称控制器
camera -> UpdateCamera(msec);
// 生成视图矩阵
viewMatrix = camera -> BuildViewMatrix();
}
void Renderer::RenderScene() {
// 绑定场景FBO,先绘制地形场景,绘制后,此时bufferColourTex[0]会有图像数据
DrawScene();
// 绘制后处理阶段,会对bufferColourTex[0]与bufferColourTex[1]进行ping pong绘制
DrawPostProcess();
// 此时bufferColourTex[0]已有后处理后的结果图像,以该图像作为在屏的图像数据源
PresentScene();
SwapBuffers();
}
void Renderer::PresentScene() {
// 绘制在屏图像,bufferColourTex[0]已经得到了最终后处理图像,将会作为数据源输入
glBindFramebuffer(GL_FRAMEBUFFER, 0);
glClear(GL_DEPTH_BUFFER_BIT | GL_COLOR_BUFFER_BIT);
SetCurrentShader(sceneShader);
// 设置为平行投影,使四边形刚好铺满屏幕
projMatrix = Matrix4::Orthographic(-1, 1, 1, -1, -1, 1);
viewMatrix.ToIdentity();
UpdateShaderMatrices();
quad->SetTexture(bufferColourTex[0]);
quad->Draw();
glUseProgram(0);
}
void Renderer::DrawPostProcess() {
// 绑定后处理FBO
glBindFramebuffer(GL_FRAMEBUFFER, processFBO);
// 此时bufferColourTex[1]作为后处理图像的输出附件
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, bufferColourTex[1], 0);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
SetCurrentShader(processShader);
projMatrix = Matrix4::Orthographic(-1, 1, 1, -1, -1, 1);
viewMatrix.ToIdentity();
UpdateShaderMatrices();
// 禁用深度的原因是因为四边形作为屏幕,不需要深度信息
glDisable(GL_DEPTH_TEST);
// 设置每个像素的大小,用于图像的采样
glUniform2f(glGetUniformLocation(currentShader->GetProgram(), "pixelSize"), 1.0f / width, 1.0f / height);
for (int i = 0; i < POST_PASSES; ++i) {
// 此时bufferColourTex[1]作为后处理图像的输出附件
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, bufferColourTex[1], 0);
glUniform1i(glGetUniformLocation(currentShader->GetProgram(), "isVertical"), 0);
// 以地形绘制场景的输出结果作为纹理输入
quad->SetTexture(bufferColourTex[0]);
// 四边形绘制,四边形铺满屏幕,此时bufferColourTex[1]已出图,将会作为下面几行代码的数据源
quad->Draw();
glUniform1i(glGetUniformLocation(currentShader->GetProgram(), "isVertical"), 1);
// 此时bufferColourTex[0]作为后处理图像的输出附件
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, bufferColourTex[0], 0);
// 以bufferColourTex[1]作为数据源
quad->SetTexture(bufferColourTex[1]);
quad->Draw();
}
glBindFramebuffer(GL_FRAMEBUFFER, 0);
glUseProgram(0);
// 别忘记重新开启深度测试
glEnable(GL_DEPTH_TEST);
}
void Renderer::DrawScene() {
// 绑定场景FBO,先绘制地形场景
glBindFramebuffer(GL_FRAMEBUFFER, bufferFBO);
glClear(GL_DEPTH_BUFFER_BIT | GL_COLOR_BUFFER_BIT |
GL_STENCIL_BUFFER_BIT);
SetCurrentShader(sceneShader);
projMatrix = Matrix4::Perspective(1.0f, 10000.0f,
(float)width / (float)height, 45.0f);
UpdateShaderMatrices();
// 地形绘制
heightMap -> Draw();
glUseProgram(0);
glBindFramebuffer(GL_FRAMEBUFFER, 0);
}
main函数的调用逻辑如下:
int main() {
Window w("Post Processing!", 800,600,false);
Renderer renderer(w);
while(w.UpdateWindow() && !Window::GetKeyboard()->KeyDown(KEYBOARD_ESCAPE)){
renderer.UpdateScene(w.GetTimer()->GetTimedMS());
renderer.RenderScene();
}
return 0;
}
图像输出结果如下: