Learning OpenGL:帧缓冲对象

背景

使用OpenGL渲染时,一般情况下我们使用的是默认的帧缓冲区(一般指的是 FrameBuffer Object Id 为 0)。但如果我们想实现一些后处理操作,如边缘检测,镜面,离屏渲染等,就需要我们自己创建自定义帧缓冲区,使用自定义帧缓冲区来进行后处理操作。

FBO

在OpenGL中,渲染管线中的顶点,纹理等经过一系列的处理之后,最终显示在2D屏幕设备上,渲染管线的最终目的地是帧缓冲区。OpenGL中使用到帧缓冲包括三个:颜色缓冲,深度缓冲,模版缓冲。系统自身会创建一个默认缓冲区,OpenGL允许我们手动创建自定义帧缓冲区,并将渲染结果重定向到这个缓冲区。

帧缓冲对象中包括两种类型的附加图像:纹理图像和RenderBuffer图像。附加纹理时,OpenGL渲染到这个纹理图像上,在着色器中可以访问这个纹理图像。附加RenderBuffer时,OpenGL执行离屏渲染(offscreen, rendering)。

帧缓冲对象可以附加多个缓冲区,且可灵活地在缓冲区中切换。帧缓冲对象中包含一个以上的颜色附加点,而深度和模版都只有一个附加点。如下图所示:

>注:OpenGL es 2.0 也只有一个颜色附加点

framebuffer object

从上图可知,帧缓冲对象本身不包含任何缓冲对象,实际上是通过附加点指向实际的缓冲对象。

创建 FBO

创建和销毁FBO的步骤很简单:

void glGenFramebuffers(GLsizei n, GLuint * ids);
void glDeleteFramebuffers(GLsizei n, const GLuint * ids);

将FBO绑定到目标对象:

void glBindFramebuffer(GLenum target, GLuint id)
  • target分为三种类型:GL_FRAMEBUFFER-缓冲区用来进行读和写操作;GL_READ_FRAMEBUUFER–缓冲区支持glReadPixels读操作;GL_DRAW_FRAMEBUFFER-缓冲区支持渲染,清除操作。
  • id 即为创建出来的帧缓冲id

通过绑定GL_FRAMEBUFFER,接下来所有的读和写操作都是在当前的帧缓冲上进行。

OpenGL要求,一个完整的FBO需要满足以下条件:

  • 至少附加一个缓冲区(颜色,深度或模版)
  • 至少有一个颜色附加
  • 所有的附加必须完整(预分配了内存)

    if(glCheckFramebufferStatus(GL_FRAMEBUFFER) == GL_FRAMEBUFFER_COMPLETE) 来判断一个FBO是否完整

  • 每个缓冲区的采样需要一致

接下里所有的渲染操作都将会渲染到当前绑定的帧缓冲中去。由于当前的帧缓冲不是默认帧缓冲,渲染指令将不会影响到屏幕上的显示内容,所以我们把渲染到非默认帧缓冲叫做离屏渲染(off-screen rendering).为了保证所有的渲染指令最终会呈现在设备屏幕上,我们需要重新绑定激活默认帧缓冲:

glBindFramebuffer (GL_FRAMEBUFFER, 0);

创建纹理附加图像

创建FBO的附加纹理如同平常使用纹理一样,不同的是,这里只为纹理分配空间,而不填充实际纹理图像内容,因为当使用FBO渲染时渲染结果将会写入到我们创建的这个纹理上去

glFramebufferTexture2D(GLenum target, GLenum attachment, GLenum textarget, GLuint texture,GLint level);
  • target: 绑定目标,一般为 GL_FRAMEBUFFER
  • attachment:附加点,可选为GL_COLOR_ATTACHMENT0,GL_DEPTH_ATTACHMENT,GL_STENCIK_ATTACHMENT
  • textTarget: 纹理的绑定目标,一般为 GL_TEXTURE_2D
  • texture:实际的纹理对象
  • level:mipmap级别,一般为0
GLuint texture;
glGenTextures(1, &texture);
glBindTexture(GL_TEXTURE_2D, texture);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RBG, 1280, 960, 0, GL_RGB, GL_UNSIGNED_BYTE, NULL); //末尾的NULL表示我们只预分配空间,而不实际加载纹理
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);

上面创建的纹理图像,可以附加到FBO:

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

FBO创建和准备工作完成额,在利用FBO作图前,我们需要介绍另一个附加图像-RenderBuffer。

创建Renderbuffer附加图像

和纹理图像一样,一个renderbuffer对象也是一个缓冲,它可以是一堆字节,整数,像素等。renderbuffer的优点是:它以OpenGL原生渲染格式存储它的数据,因此在离屏渲染到帧缓冲的时候,这些数据相当于被优化过。

renderbuffer对象将所有渲染数据直接储存在它们的缓冲里,而不会进行针对特定纹理格式的任何转换,这样它们就成了一种快速可写的存储介质了。然后,renderbuffer对象通常是只写的,不能修改它们。

因为renderbuffer对象中的数据已经是原生格式了,在写入或者把它们的数据拷贝到其他缓冲区的时候非常快。像切换缓冲区这种操作变得异常高速。因此我们在每个渲染迭代末尾的地方,可以用renderbuffer来实现缓冲区数据的交换操作。

要了解为什么渲染的末尾需要交换帧缓冲数据,和谁交换帧缓冲数据,需要理解什么叫双缓存。所谓双缓存机制指的是显示系统通常会引入两个缓冲区,在这种情况下,GPU会预先渲染好一帧放入一个缓冲区内,让视频控制器读取,当下一帧渲染好后,GPU会直接把视频控制器的指针指向第二个缓冲器。

创建和销毁RenderBuffer也很简单:

void glGenRenderbuffers(GLsizei n, GLuint * ids);
void glDeleteRenderbuffers(GLsizei n, const GLuint * ids);

绑定到目标对象:

glBindRenderbuffer (GL_RENDERBUFFER,rbo);

需为 RBO 预分配内存空间:

void glRenderbufferStorage (GLenum target, GLenum internalFormat, GLsizei width, GLsizei height);

RBO绑定到FBO

glFramebufferRenderbuffer (GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, rboId);

在帧缓冲项目中,renderbuffer可以优化渲染,但重要的是要懂得何时使用renderbuffer对象,何时使用纹理。通常规则是:如果你永远都不需要从特定的缓冲中进行采样,renderbuffer对特定缓冲是更明智的选择。如果需要从比如颜色或深度值这样的特定缓冲采样数据的话,最好使用纹理。

渲染到纹理

到此为止,我们了解了帧缓冲如何工作,是时候看看如何运用上述知识了。接下来,我们把场景渲染到一个颜色纹理上,这个纹理附加到一个我们创建的帧缓冲上,然后把纹理绘制到一个简单的铺满屏幕的四边形上。输出的图像看似和没有使用帧缓冲一样,但这次输出是打印在最上层的四边形上的。

Step1: 创建并绑定帧缓冲

GLuint framebuffer;
glGenFramebuffers(1, &framebuffer);
glBindFramebuffer(GL_FRAMEBUFFER, framebuffer);

Step2: 创建一个纹理

// Generate texture
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);
glBindTexture(GL_TEXTURE_2D, 0);

// Attach it to currently bound framebuffer object
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, texColorBuffer, 0);

Step3 创建renderbuffer来进行深度测试

为了让OpenGL进行深度测试,我们必须要确保向帧缓冲中添加一个深度附件。由于我们只采样颜色缓冲,并不采样深度,模版缓冲,所以我们可以创建一个渲染缓冲对象来达到这个目的。

GLuint rbo;
glGenRenderbuffers(1, &rbo);
glBindRenderbuffer(GL_RENDERBUFFER, rbo);
glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH24_STENCIL8, 800, 600);  
glBindRenderbuffer(GL_RENDERBUFFER, 0);

把renderbuffer对象附加到帧缓冲的深度和模版附件上:

glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_RENDERBUFFER, rbo);

所以,为了把场景绘制到一个单独的纹理,我们必须以下面步骤来做:

  • 创建并绑定一个新的帧缓冲,并在这个帧缓冲上做相关渲染操作
  • 绑定默认帧缓冲
  • 绘制一个铺满屏幕的四边形,并用新的帧缓冲的颜色缓冲作为输入的纹理

以下是每一帧渲染的伪代码:

//First pass
glBindFramebuffer(GL_FRAMEBUFFER, framebuffer);
glClearColor(0.f,0.f,0.f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT|GL_DEPTH_BUFFER_BIT);
glEnable(GL_DEPTH_TEST);
DrawScene();

// Second pass
glBindFramebuffer(GL_FRAMEBUFFER, 0); // back to default
glClearColor(1.0f, 1.0f, 1.0f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);

screenShader.Use();  
glBindVertexArray(quadVAO);
glDisable(GL_DEPTH_TEST);
glBindTexture(GL_TEXTURE_2D, textureColorbuffer);
glDrawArrays(GL_TRIANGLES, 0, 6);
glBindVertexArray(0);

后处理

>待续