OpenGL学习之帧缓冲——FBO

介绍

在之前的绘图过程中,我们没有注意到所绘制的颜色信息、深度信息、模板信息写到哪里去了?实际上,GLFW为我们做了完了这件事,渲染得到的信息存储到了系统默认帧缓冲(window-system-provided framebuffer)当中了。帧缓冲——framebuffer是OpenGL管线最后“一站”,它存储一系列的2D序列,最后在glfwSwapBuffers操作中将这些信息转换为屏幕上的2D像素。

在OpenGL扩展中,有这样一个对象GL_ARB_framebuffer_object,它提供了一个接口,能够产生自定义的帧缓冲——framebuffer objectd(FBO),这个帧缓冲叫做应用程序帧缓冲(Application created framebuffer),它和系统默认的帧缓冲类似,只不过不显示到屏幕上,而是进行离屏渲染,将颜色、深度、模板信息存储到这个buffer中。

注意,这里有一点要说明一下,framebuffer是一个buffer,但是真正存储信息不是它本身,而是附着到它上面的纹理、渲染缓冲对象等(后面详细讲),它只是提供一个附着点,猜想它可能是一个结构体,里面包含纹理、渲染缓冲等对象。举个例子,framebuffer是一个篮子,篮子里面可以放很多碗(纹理、渲染缓冲对象等),碗里面才是米饭和菜。

像下面这幅图,帧缓冲可以附着纹理和渲染缓冲对象。


那么使用帧缓冲有哪些好处呢?为什么使用帧缓冲呢?

之前绘制使用的纹理都是使用图片加载得到的纹理,如果我们要对纹理在着色器中做一些处理,模糊、虚化、双屏、镜子等特效,那么使用帧缓冲可以很好的实现。此外,帧缓冲提供了一个很高效的机制,它能够快速的分离和附着纹理或渲染缓冲对象,这比在帧缓冲之间切换要快得多。

帧缓冲创建

帧缓冲的创建和创建其他对象一样,像VAO、VBO等,可以使用glGenFrameBuffer函数来创建一个帧缓冲(FBO)

GLuint framebuffer;
glGenFramebuffers(1, &framebuffer);
glBindFramebuffer(GL_FRAMEBUFFER, framebuffer);//绑定
glBindFramebuffer(GL_FRAMEBUFFER, 0);//解绑定

绑定到GL_FRAMEBUFFER目标后,接下来所有的读、写帧缓冲的操作都会影响到当前绑定的帧缓冲。也可以把帧缓冲分开绑定到读或写目标上,分别使用GL_READ_FRAMEBUFFER或GL_DRAW_FRAMEBUFFER来做这件事。如果绑定到了GL_READ_FRAMEBUFFER,就能执行所有读取操作,像glReadPixels这样的函数使用了;绑定到GL_DRAW_FRAMEBUFFER上,就允许进行渲染、清空和其他的写入操作。大多数时候你不必分开用,通常绑定到GL_FRAMEBUFFER上就行。

如果要创建一个完整的帧缓冲,需要满足以下条件: 
– 至少包含一个附件(颜色、深度、或模板); 
– 其中至少有一个颜色附件; 
– 附件要在附着之前创建好,并存储在内存中; 
– 每个缓冲应该有同样的样本。

假如我们创建好一些附件之后,并已经附着到帧缓冲上了,那么接下来要对它进行检查,判断帧缓冲是否完整。

if(glCheckFramebufferStatus(GL_FRAMEBUFFER) == GL_FRAMEBUFFER_COMPLETE)

由于使用自定义的帧缓冲,渲染操作并不是对系统提供的默认的帧缓冲,所以并不会对屏幕上图像产生任何影响。如果要切回默认的帧缓冲,可以通过如下函数:

glBindFramebuffer(GL_FRAMEBUFFER, 0);

当我们做完所有帧缓冲操作,不要忘记删除帧缓冲对象:

glDeleteFramebuffers(1, &fbo);

下面介绍一下在帧缓冲上可以附着的两种附件。

纹理附件

纹理附件和通过图片加载的纹理类似,只不过这个纹理附加是通过渲染命令写入到纹理当中的,不是通过图片纹理得到的。

GLuint texBuffer;   
glGenTextures(1, &texBuffer);
glBindTexture(GL_TEXTURE_2D, texBuffer);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, WIDTH, HEIGHT, 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);
glBindTexture(GL_TEXTURE_2D, 0);

和普通纹理类似,只要区别是glTexImage2D这个函数的参数中internalFormat、width、height、format、type、data:

参数说明 void glTexImage2D( GLenum target,
GLint level,
GLint internalFormat,
GLsizei width,
GLsizei height,
GLint border,
GLenum format,
GLenum type,
const GLvoid * data);
1.internalFormat表示的是OpenGL中内存纹理的格式,表示纹理颜色成分的信息
2.format表示内存中图像像素的格式
3.type表示内存中图像的类型
4.data表示指向内存的指针
5.width和height是纹理的尺寸,可以通过设置glViewport来设置改变。

附着纹理到帧缓冲上可以通过glFramebufferTexture2D

glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0,GL_TEXTURE_2D, texBuffer, 0);

参数说明
1.target:帧缓冲
2.attachment:附件类型。现在附加的是一个颜色附件。需要注意,最后的那个0是表示可以附加1个以上颜色的附件。
3.textarget:附加的纹理类型。
4.texture:附加的实际纹理。
5.level:Mipmap level。一般设置为0。

注意:除了附加颜色附件之外,还可以附件深度和模板纹理附件。例如,当我们开启了深度测试时,就需要附着一个深度附件,来表示深度信息,以便进行深度测试。为了附加一个深度缓冲,可用GL_DEPTH_ATTACHMENT作为附件类型。此时纹理格式和内部格式类型(internalformat)就成了 GL_DEPTH_COMPONENT去反应深度缓冲的存储格式。附加一个模板缓冲,要用GL_STENCIL_ATTACHMENT作为第二个参数,把纹理格式指定为 GL_STENCIL_INDEX。也可同时附加一个深度缓冲和一个模板缓冲为一个单独的纹理。这样纹理的每32位数值就包含了24位的深度信息和8位的模板信息。为了把一个深度和模板缓冲附加到一个单独纹理上,使用GL_DEPTH_STENCIL_ATTACHMENT类型配置纹理格式以包含深度值和模板值信息。


glTexImage2D( GL_TEXTURE_2D, 0, GL_DEPTH24_STENCIL8, WIDTH, HRIGHT, 0, GL_DEPTH_STENCIL, GL_UNSIGNED_INT_24_8, NULL );
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_TEXTURE_2D, texDepthAndStencil, 0);//texDepthAndStencil是包含深度和模板信息的纹理

渲染缓冲对象(RBO)

渲染缓冲对象(RenderBuffer Object,简称RBO)是一个OpenGL格式的缓冲,即以OpenG原生(native)格式存储它的数据,因此它相当于是优化过的内部数据。当它绑定到FrameBuffer上时,渲染的像素信息就写到RBO中。
渲染缓冲对象将渲染数据存储到缓冲中,并且以原生格式存储,所以它成为一种快速可写入的介质。但是,只能写入,不能修改。RBO常常用来存储深度和模板信息,用于深度测试和模板测试,而且比用纹理存储的深度和模板方式要快得多。RBO可以用来实现双缓冲(double buffer)。
同样,渲染缓冲对象也可以写入颜色信息,然后将图像信息显示在屏幕上。

貌似RBO比纹理有点多,但也不是万能的,纹理自有纹理的优点,纹理能够在shader中进行操作或者需要读取像素时,做一些处理,此时RBO就无能为力了。

下面来看一下RBO的创建过程。

GLuint rbo;
glGenRenderbuffers(1, &rbo);//创建渲染缓冲对象
glBindRenderbuffer(GL_RENDERBUFFER, rbo);//绑定渲染缓冲对象
glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH24_STENCIL8, WIDTH, HEIGHT);//为渲染缓冲对象分配内存
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_RENDERBUFFER, rbo);//绑定渲染缓冲对象到帧缓冲上

Example

好了,知道了帧缓冲的基本用法,下面用实例来验证一下。我们先把场景渲染到纹理上,然后再利用该纹理贴到一个矩形上,最后在屏幕上显示出来。下面会有三种方式:
(1)帧缓冲上只附加颜色纹理
(2)帧缓冲上附加颜色纹理+RBO
(3)帧缓冲上附加颜色纹理+深度模板纹理

然后看一下效果如何?

使用我们自己创建的帧缓冲区,就要在渲染前着切换到帧缓冲区,然后执行渲染操作,当要利用系统提供的默认缓冲区时,还要切换回来,因为我们要用默认的帧缓冲来显示图像,主要的代码部分如下:

glBindFramebuffer(GL_FRAMEBUFFER, framBuffer);//切换到自己创建的帧缓冲区

glClearColor(0.16f, 0.05f, 0.15f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
glEnable(GL_DEPTH_TEST);//开启深度测试
//绘制场景
...

glBindFramebuffer(GL_FRAMEBUFFER, 0);
glClearColor(1.0f, 1.0f, 1.0f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);
glDisable(GL_DEPTH_TEST);//关闭深度测试,绘制矩形框不需要深度

glBindVertexArray(quadVAO);
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, texBuffer);//texBuffer是前面帧缓冲区中渲染的纹理
glDrawArrays(GL_TRIANGLES, 0, 6);

第一种情况:只附加颜色纹理

GLuint texBuffer;
glGenTextures(1, &texBuffer);
glBindTexture(GL_TEXTURE_2D, texBuffer);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, WIDTH, HEIGHT, 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);
glBindTexture(GL_TEXTURE_2D, 0);
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, texBuffer, 0);

第二种情况:附加颜色纹理和RBO,由于有了渲染缓冲对象,所以可以进行深度测试和模板测试,该例中我们没有使用模板测试。

GLuint rbo;
glGenRenderbuffers(1, &rbo);
glBindRenderbuffer(GL_RENDERBUFFER, rbo);
glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH24_STENCIL8, WIDTH, HEIGHT);
glBindRenderbuffer(GL_RENDERBUFFER, 0);
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_RENDERBUFFER, rbo);//绑定深度缓冲对象rbo

下面的图显示的是有深度信息的图像,与上面没有深度信息的图像明显感觉到差别。

第三种情况:附加颜色纹理和带有深度和模板的纹理,相当于将深度和模板信息写入到纹理在中,这种方式渲染出来的效果和使用渲染缓冲对象的方式相同。这种方式效率没有使用渲染缓冲对象高,但是如果想要读取深度信息和模板信息时,就需要使用这种方式。

GLuint texDepthAndStencil;
glGenTextures(1, &texDepthAndStencil);
glBindTexture(GL_TEXTURE_2D, texDepthAndStencil);
glTexImage2D(GL_TEXTURE_2D, 0, GL_DEPTH24_STENCIL8, WIDTH, HEIGHT, 0, GL_DEDTH_STENCIL, GL_UNSIGNED_INT_24_8, NULL);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glBindTexture(GL_TEXTURE_2D, 0);
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_TEXTURE_2D, texDepthAndStencil, 0);//绑定深度模板纹理

既然有了深度模板纹理,就可以从纹理中读取深度信息和模板信息,我们绑定一下深度模板纹理,代替颜色纹理,尝试读取出深度信息,看一下有什么效果?

glBindTexture(GL_TEXTURE_2D, texDepthAndStencil);//绑定深度模板纹理

替换颜色纹理后,通过移动相机位置,发现当相机远离时,深度值接近1,所以采样的颜色为(1.0,0.0,0.0,1.0),呈现红色;而相机靠近时,深度值接近0.0,即(0.0,0.0,0.0,1.0)呈现黑色。

其他

通过使用帧缓冲区,我们就可以得到纹理,有了纹理,就可以通过对纹理图像做后期的处理,如模糊、边缘检测。 
下面是一个简单双屏的小例子,通过使用纹理,绘制左右两个矩形框,每个矩形贴上相同的纹理,就达到了左右屏的效果,矩形顶点坐标如下:

GLfloat quadVertices[] = {  
    // Positions   // TexCoords
    -1.0f, 1.0f, 0.0f, 1.0f,
    -1.0f, -1.0f, 0.0f, 0.0f,
    0.0f, -1.0f, 1.0f, 0.0f,

    -1.0f, 1.0f, 0.0f, 1.0f,
    0.0f, -1.0f, 1.0f, 0.0f,
    0.0f, 1.0f, 1.0f, 1.0f,

    0.0f, 1.0f, 0.0f, 1.0f,
    0.0f, -1.0f, 0.0f, 0.0f,
    1.0f, -1.0f, 1.0f, 0.0f,

    1.0f, -1.0f, 1.0f, 0.0f,
    1.0f, 1.0f, 1.0f, 1.0f,
    0.0f, 1.0f, 0.0f, 1.0f
};

其他特效在这里就不展示了,感兴趣的话可以去尝试下,主要通过修改Shader得到想要的特效。

参考资料

[1] Learn Opengl 
[2]OpenGL Frame Buffer Object (FBO)