OpenGL之帧缓冲

本文主要包括帧缓冲的基本概念、函数使用,以及引出离屏渲染和后处理的介绍。

概要

所谓帧缓冲是做什么用的,就是用来存储渲染数据的地方,可以理解为显存。几何数据(顶点坐标、纹理坐标等)和纹理经过一系列渲染管道最终计算出屏幕上的所有像素点,它们需要一个地方来存放,这个地方就是帧缓冲。帧缓冲中的数据会被显示器读取来刷新显示。

OpenGL可以同时存在多个帧缓冲,创建窗口时OpenGL会自动创建一个默认帧缓冲,专门用于该窗口的渲染显示。其它的需要用户创建自定义帧缓冲。

帧缓冲中包括有颜色缓冲区、深度缓冲区、模板缓冲区这三类缓冲区。

我用下图介绍一下帧缓冲的用处,以深度缓冲为例。
在由三维映射至二维空间时,Z坐标将会被转换为深度值写入深度缓冲中,测试和混合阶段执行后,像素点的颜色值会被写入帧缓冲的颜色附件中,最终帧缓冲会显示到显示器上。
OpenGL之帧缓冲

帧缓冲

附件

要注意,帧缓冲并不包含缓冲数据的内存,它只是包含颜色缓冲区、深度缓冲区、模板缓冲区的附加点。这些对象才是真正的缓冲,它们被称之为附件。绑定到帧缓冲的这个过程叫做“附加”。

例如下图中,帧缓冲中包含有多个附加点可将真正的缓冲区附加上去。

OpenGL之帧缓冲

OpenGL定义,一个帧缓冲对象包括有

  1. 多个颜色缓冲附件,
  2. 一个深度缓冲附件,
  3. 一个模板缓冲附件。

可以通过一个结构体来理解它(仅用于理解):

struct FrameBuffer
{
    int[] color;
    int depth;
    int stencil;
};

注意到颜色缓冲区可以是一个列表,这是由OpenGL定义的,允许用户同一时间将颜色缓冲区渲染到多个目标(multiple render targets,MRT)。

MRT是什么意思呢?
比方说,你可以附加两个颜色附件,然后在这两个附件上渲染出整个场景。可能一个是完整版场景,一个低采样版场景,分别作不同用途,可能其中带有遮罩或者用于其它后处理。

此外OpenGL定义,一个完整的帧缓冲对象至少要有一个颜色附件,否则将无法被使用。

函数


创建帧缓冲和创建VBO时非常类似地。

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

glGenFramebuffers第一个参数是创建帧缓冲的数目,第二参数是存储帧缓冲区对象的ID。


绑定帧缓冲到OpenGL目标上:

void glBindFramebuffer(GLenum target, GLuint id)

将这个帧缓冲绑定到一个目标上,将它指定为**的帧缓冲,这个目标会影响OpenGL对帧缓冲r的使用,也可以看做目标=类型。

  • GL_FRAMEBUFFER:所有的读取和写入帧缓冲的操作将会影响当前绑定的帧缓冲
  • GL_READ_FRAMEBUFFER:所有的读取操作会从这个帧缓冲中读取
  • GL_Draw_FRAMEBUFFER):被用作渲染、清除等写入操作的目标

附件有两种类型,分别是纹理附件和渲染缓冲对象(Render Buffer Object,RBO)附件。先说纹理附件。

纹理附件

用一个纹理对象来作为一个附件,这称之为纹理附件。所有渲染操作的结果将会被储存在这个纹理图像中。

纹理可能有立方体纹理,1D、2D、3D纹理等类型,此外还可以使用Mipmap。

在RBO出来之前,是只能使用纹理作为附件的,但RBO并不意味着纹理附件就“过时”了,纹理附件的优点在于纹理是一种标准格式,在片段着色器中我们可以快速读取到纹理上的像素。(读取比RBO快)


纹理附件的创建和一般的纹理相同。

glGenTextures
glBindTexture
glTexImage2D
glTexParameteri

附加纹理到帧缓冲:

glFramebufferTexture2D(GLenum target, 
                    GLenum attachmentPoint, 
                    GLenum textureTarget, 
                    GLuint textureId, 
                    GLint level)
  • target:帧缓冲的目标(即我们前面说到的帧缓冲类型)
  • attachmentPoint:附件的类型。颜色、深度或是模板缓冲附件
    GL_COLOR_ATTACHMENT0, …, GL_COLOR_ATTACHMENT_n_), GL_DEPTH_ATTACHMENT, 和 GL_STENCIL_ATTACHMENT, GL_DEPTH_STENCIL_ATTACHMENT
  • textureTarget:附件的纹理类型,大部分时候都是GL_TEXTURE_2D
  • textureId:要附加的纹理对象
  • level:多级渐远纹理的级别。一般保留为0

渲染缓冲对象附件

渲染缓冲对象(Render Buffer Object,RBO)是后来者,这是为了解决帧缓冲渲染到纹理时,纹理使用标准格式,所以速度可能较慢。

而RBO使用OpenGL原生的格式存储像素值,因此它针对屏幕外渲染进行了优化。换句话,绘制到RBO比绘制到纹理要快得多,即写入数据/复制数据会更快。

而它的缺点也在于像素用的是原生的、与实现相关的格式,因此从RBO读取比从纹理读取要困难得多。

RBO与纹理附件最大的两个区别在于:

  1. RBO没有Mipmap,所以它只是一个图像
  2. 不能使用纹理访问或其它方式来采样(尽管可以使用 glReadPixels 函数来读取,只是不能在着色器中读取)

在深度测试和模板测试中,我们只关心测试结果,通常我们不需要从深度或模板中采样,所以对于深度缓冲对象和模板缓冲对象我们一般采用RBO。


RBO同一般缓冲区的用法差不多

void glGenRenderbuffers(GLsizei n, GLuint* ids)

void glDeleteRenderbuffers(GLsizei n, const Gluint* ids)

void glBindRenderbuffer(GLenum target, GLuint id)

当创建RBO时有一点特殊,它没有任何数据存储空间,必须要为它分配内存:

void glRenderbufferStorage(GLenum  target,
                           GLenum  internalFormat,
                           GLsizei width,
                           GLsizei height)
  • target:RBO的类型,只有一种,必须是GL_RENDERBUFFER
  • internalFormat:内部格式,用于颜色的(GL_RGBA4/GL_RGB5_A1/GL_RGB565等)、用于深度的(GL_DEPTH_COMPONENT16)或用于模板格式(GL_STENCIL_INDEX8),还可以用于深度+模板的格式(GL_DEPTH24_STENCIL8)
  • width:RBO的宽
  • height:RBO的高

附加RBO到帧缓冲上:

void glFramebufferRenderbuffer(GLenum target,
                               GLenum attachmentPoint,
                               GLenum renderbufferTarget,
                               GLuint renderbufferId)
  • target:RBO的类型,只有一种,必须是GL_RENDERBUFFER
  • attachmentPoint:附加点格式,GL_COLOR_ATTACHMENT0, …, GL_COLOR_ATTACHMENT_n_), GL_DEPTH_ATTACHMENT,和 GL_STENCIL_ATTACHMENT, GL_DEPTH_STENCIL_ATTACHMENT
  • renderbufferTarget:当第四个参数不为0时,这个值必须是GL_RENDERBUFFER。否则随便。
  • renderbufferId:要附加的RBO对象

附件类型的选择

通常的规则是,如果不需要从一个缓冲中采样数据,那么使用RBO。例如深度缓冲和模板缓冲。
如果你需要从缓冲中采样颜色或深度值等数据,那么你应该选择纹理附件。一般来说后处理都会需要这种。

后处理

在顶点着色器对每一个像素点,进行各种计算处理,这叫后处理。
例如下图简单的处理,对纹理中每个像素点进行反相处理。
OpenGL之帧缓冲

离屏渲染

在默认帧缓冲中绘制对象,这叫当前屏幕渲染(On-screen Rendering)。

把渲染计算结果放在非默认帧缓冲中,这叫离屏渲染(Off-screen Rendering)。
一般要将自定义缓帧缓冲的数据渲染出来,还需要切回默认帧缓冲,取出自定义帧缓冲的颜色附件,再将其渲染。

例如下图中,将自定义帧缓冲中的颜色附件拿出来,单独绘制在一个只有屏幕1/4大小的长方形上。
OpenGL之帧缓冲

对于普通的当前屏幕渲染来说,这一整套绘制流水线就是一个Rendering Pass,而离屏渲染就存在多个Rendering Pass了。

离屏渲染性能消耗是比较大的,主要在于两点:

  1. 更多的Rendering Pass,GPU工作量增大
  2. Rendering Pass之间的环境切换相当耗时,从自定义缓冲区转换到系统帧缓冲区。

那为什么还要用它呢?
大部分后处理都需要用到离屏渲染,这在游戏中是非常常见的功能。
像是遮罩
OpenGL之帧缓冲
阴影
OpenGL之帧缓冲
bloom泛光
OpenGL之帧缓冲
light shafts体积光,像上次分享会提到的第二种渲染体积光的方式:后处理中的径向模糊。
OpenGL之帧缓冲
亦或是outline描边
OpenGL之帧缓冲
补充一句:OpenGL中模板测试可以实现描边的效果,但是具有很多的限制,效果不理想。
常见的做法是用两个Pass。
第一个 Pass ,沿法线挤压,稍大一点,剔除正面,就可以看到它的背面
第二个 Pass ,正常渲染,剔除背面,遮挡上一个 Pass 的中间部分

使用纹理附件作为颜色附件,这在后处理中有一个好处,可以在这个像素点取一小块区域采样,来进行复杂的效果例如核效果、高斯模糊等。

离屏渲染与双缓冲

离屏渲染要求用户必须自定义创建一个帧缓冲并与默认帧缓冲配合使用。这和OpenGL中的双缓冲机制有什么关系呢?

用一张图来表示双缓冲机制。如图,在每一帧中我们的渲染的结果会保存在后缓冲中,在每帧结束前需要交换前后缓冲来刷新显示。也就是说,系统默认帧缓冲就是后缓冲。

离屏渲染需要创建一个新的帧缓冲,也并不影响双缓冲机制。

OpenGL之帧缓冲

所以答案是双缓冲机制与离屏渲染并没有什么关系。

来源

  1. https://learnopengl-cn.github.io/04 Advanced OpenGL/05 Framebuffers/
  2. http://www.songho.ca/opengl/gl_fbo.html
  3. https://*.com/questions/9850803/glsl-renderbuffer-really-required
  4. https://*.com/questions/2213030/whats-the-concept-of-and-differences-between-framebuffer-and-renderbuffer-in-op