Osheep

时光不回头,当下最重要。

OpenGL 之 FBO--视频美颜的基础

前言

在OpenGL中,有一个非常重要的知识点就是 FBO(Frame Buffer Object)。它为做视频美颜提供了技术手段。

在网上可以找到很多介绍 FBO 的文章,但很少有将 FBO、Texture及Render Buffer 之间关系讲清楚的。

本篇文章在介绍 FBO 的同时,清楚的描述了三者之间的关系。希望本文对大家有所帮助。

FBO

OpenGL 默认把 framebuffer 当作渲染的目的地。它由窗口系统创建并管理。framebuffer Object 是个二维数组的集合,它包括 color buffers, depth buffer, stencil buffer。

OpenGL扩展,GL_ARB_framebuffer_object 提供了创建额外非可显示的 framebuffer object(FBO)的接口。FBO 称作应用程序可创建的 framebuffer 以区别默认的窗口系统提供的framebuffer。通过使用FBO,OpenGL 应用可以重定向渲染输出,让它输出到FBO而不是传统的窗口系统提供的 framebuffer.

与窗口系统提供的帧缓冲区类似,FBO包含一系列渲染目的地的集合;包括颜色,深度和模板缓冲区。 FBO中的这些逻辑缓冲区称为可附着的 frame buffer。

有两种类型的可附着的 framebuffer;纹理(Texture)和renderbuffer。如果纹理被附加到FBO,OpenGL将执行“渲染到纹理”。如果renderbuffer被附加到FBO,则OpenGL会执行“离屏渲染”。

顺便说一下,renderbuffer是在GL_ARB_framebuffer_object扩展中定义的一种新类型的存储对象。它在渲染过程中用作单个2D图像的渲染目的地。

下图显示了FBO,纹理和renderbuffer之间的连接。多个纹理对象或renderbuffer对象可以通过附着点附加到FBO上。

《OpenGL 之 FBO--视频美颜的基础》

FBO中,

  • 有多个颜色附加点(GL_COLOR_ATTACHMENT0,…,GL_COLOR_ATTACHMENTn),
  • 一个深度附加点(GL_DEPTH_ATTACHMENT)
  • 一个模板附加点(GL_STENCIL_ATTACHMENT)。

颜色附着点的数量取决于实现,但每个FBO必须至少具有一个颜色附加点。您可以使用GL_MAX_COLOR_ATTACHMENTS查询最大数量的颜色附加点,这些数据由显卡支持。

FBO具有多个颜色附加点的原因是允许在同一时间将颜色缓冲区渲染到多个目的地。这个“多个渲染目标”(MRT)可以由GL_ARB_draw_buffers扩展完成。请注意,FBO本身不存放数据,它只有多个附着点。这有点像数据结构中的指针,它只存放指针,而不存放数据。

FBO提供了一种高效的切换机制;从FBO中分离先前的帧缓冲区,并将一个新的可附着的帧缓冲图像附加到FBO中。切换可附着的帧缓冲图像比在FBO之间切换要快得多。 FBO提供glFramebufferTexture2D()来切换2D纹理对象,并将glFramebufferRenderbuffer()切换到renderbuffer对象。

创建FBO

glGenFramebuffers()

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

glGenFramebuffers() 需要2个参数:

  • 第一个是要创建的帧缓冲区的数量;
  • 第二个参数是指向GLuint变量或数组以存储单个ID或多个ID的指针。
  • 它返回未使用的framebuffer对象的ID。 ID 0表示默认的帧缓冲区,它是由窗口系统提供的帧缓冲区。
  • 而,当FBO不再使用时,可以通过调用glDeleteFramebuffers()来删除。

glBindFramebuffer()

创建FBO之后,必须先绑定FBO。

void glBindFramebuffer(GLenum target, GLuint id)
  • 第一个参数target为GL_FRAMEBUFFER;
  • 第二个参数为framebuffer对象的ID。 FBO绑定后,所有的OpenGL操作都会影响到当前绑定的FBO。
  • 对象ID,0保留给默认的窗口系统提供的帧缓冲区。因此,为了取消绑定当前帧缓冲区(FBO),请在glBindFramebuffer() 中使用ID 0。

Renderbuffer

另外,renderBuffer对象是新引入的用于离屏渲染。它允许将场景直接渲染到renderbuffer对象,而不是渲染到纹理对象。

Renderbuffer只是一个包含可渲染内部格式的单个映像的数据存储对象。它用于存储没有相应纹理格式的OpenGL逻辑缓冲区,如模板或深度缓冲区。

glGenRenderbuffers()

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

一旦创建了一个renderbuffer,它返回非零正整数。 ID 0为OpenGL保留。

glBindRenderbuffer()

void glBindRenderbuffer(GLenum target, GLuint id)

与其他OpenGL对象相同,您必须在引用之前绑定当前的renderbuffer对象。 renderbuffer对象的目标参数应为GL_RENDERBUFFER。

glRenderbufferStorage()

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

当创建一个renderbuffer对象时,它没有任何数据存储,所以我们必须为它分配一个内存空间。这可以通过使用glRenderbufferStorage()来完成。

  • 第一个参数必须是GL_RENDERBUFFER;
  • 第二个参数是可渲染颜色(GL_RGB,GL_RGBA等),可渲染深度(GL_DEPTH_COMPONENT)或可渲染模板(GL_STENCIL_INDEX);
  • width和height是以像素为单位的renderbuffer图像的尺寸。

宽度和高度应小于GL_MAX_RENDERBUFFER_SIZE,否则会生成GL_INVALID_VALUE错误。

glGetRenderbufferParameteriv()

void glGetRenderbufferParameteriv(GLenum target,
                                  GLenum param,
                                  GLint* value)

您还可以获取当前绑定的renderbuffer对象的各种参数。

  • 目标应该是GL_RENDERBUFFER;
  • 第二个参数是参数的名称;
  • 最后一个是指向整数变量的指针,用于存储返回的值。

renderbuffer参数的可用名称为:

GL_RENDERBUFFER_WIDTH
GL_RENDERBUFFER_HEIGHT
GL_RENDERBUFFER_INTERNAL_FORMAT
GL_RENDERBUFFER_RED_SIZE
GL_RENDERBUFFER_GREEN_SIZE
GL_RENDERBUFFER_BLUE_SIZE
GL_RENDERBUFFER_ALPHA_SIZE
GL_RENDERBUFFER_DEPTH_SIZE
GL_RENDERBUFFER_STENCIL_SIZE

将图像附加到FBO

FBO本身不存放作何数据。相反,我们必须在FBO上附加可附加的framebuffer图像(纹理或renderbuffer对象)。该机制允许FBO快速切换(拆除和附加)FBO中的可附着的帧缓冲图像。切换可附加的帧缓冲区比在FBO之间切换要快得多。并且,它可以节省不必要的数据副本和内存消耗。例如,纹理可以附加到多个FBO,并且其图像可以由多个FBO共享。

将2D纹理图像附加到FBO

glFramebufferTexture2D(GLenum target,
                       GLenum attachmentPoint,
                       GLenum textureTarget,
                       GLuint textureId,
                       GLint  level)

glFramebufferTexture2D()是将2D纹理图像附加到FBO。

  • 第一个参数必须是GL_FRAMEBUFFER;
  • 第二个参数是连接纹理图像的连接点。 FBO具有多个颜色附加点(GL_COLOR_ATTACHMENT0,…,GL_COLOR_ATTACHMENTn),GL_DEPTH_ATTACHMENT和GL_STENCIL_ATTACHMENT;
  • 第三个参数“textureTarget”在大多数情况下是GL_TEXTURE_2D;
  • 第四个参数是纹理对象的标识符;
  • 最后一个参数是要附加的纹理的mipmap级别。

如果textureId参数设置为0,则纹理图像将从FBO中分离。如果纹理对象被删除时仍然附着在FBO上,则纹理图像将自动从当前绑定的FBO中分离。但是,如果它附加到多个FBO并被删除,那么它将仅从绑定的FBO分离,但不会与任何其他无约束的FBO分离。

附加到Renderbuffer图像到FBO

void glFramebufferRenderbuffer(GLenum target,
                               GLenum attachmentPoint,
                               GLenum renderbufferTarget,
                               GLuint renderbufferId)

可以通过调用glFramebufferRenderbuffer()来附加renderbuffer。

  • 第一和第二个参数与glFramebufferTexture2D()相同;
  • 第三个参数必须是GL_RENDERBUFFER;
  • 最后一个参数是renderbuffer对象的ID。

如果renderbufferId参数设置为0,则renderbuffer图像将从FBO中的附加点分离。如果renderbuffer对象被删除时,仍然附着在FBO上,那么它将自动从绑定的FBO中分离出来。但是,它不会与任何其他无约束的FBO分离。

检查FBO状态

一旦可连接的图像(纹理和renderbuffer)附加到FBO,并且在执行FBO操作之前,必须使用glCheckFramebufferStatus()验证FBO状态是否完整。如果FBO未完成,则任何绘图和读取命令(glBegin(),glCopyTexImage2D()等)将失败。

GLenum glCheckFramebufferStatus(GLenum target)

glCheckFramebufferStatus()验证当前绑定的FBO上的所有附加图像和帧缓冲区参数。而且,这个函数不能在glBegin()/ glEnd()对中调用。目标参数应为GL_FRAMEBUFFER。在检查FBO后返回非零值。如果满足所有要求和规则,则返回GL_FRAMEBUFFER_COMPLETE。否则,它返回一个相关的错误值,它告诉什么规则被违反。

FBO完整性规则为:

  • framebuffer可附加图像的宽度和高度必须不为零。
  • 如果图像附加到颜色附着点,则图像必须具有可呈现颜色的内部格式。 (GL_RGBA,GL_DEPTH_COMPONENT,GL_LUMINANCE等)
  • 如果图像附加到GL_DEPTH_ATTACHMENT,则图像必须具有深度可渲染内部格式。 (GL_DEPTH_COMPONENT,GL_DEPTH_COMPONENT24等)
  • 如果图像附加到GL_STENCIL_ATTACHMENT,则图像必须具有模板可渲染内部格式。 (GL_STENCIL_INDEX,GL_STENCIL_INDEX8等)
  • FBO必须至少安装一张图片。
  • 附加FBO的所有图像必须具有相同的宽度和高度。
  • 附加颜色附件点的所有图像必须具有相同的内部格式。

请注意,即使满足上述所有条件,OpenGL驱动程序也可能不支持某些内部格式和参数的组合。如果OpenGL驱动程序不支持特定的实现,那么glCheckFramebufferStatus()返回GL_FRAMEBUFFER_UNSUPPORTED。

例子

有时,需要动态生成动态纹理。最常见的示例是生成镜像/反射效果,动态多维数据集/环境映射和阴影贴图。动态纹理可以通过将场景渲染到纹理来实现。渲染到纹理的一种传统方法是像一般的draw缓冲区,然后使用glCopyTexSubImage2D()将framebuffer图像复制到纹理。

使用FBO,我们可以将场景直接渲染到纹理,所以我们不必使用窗口系统提供的帧缓存。更进一步,我们可以消除额外的数据拷贝(从帧缓存到纹理)。

使用FBO还有另一个优势。在传统情况下,如果纹理分辨率大于渲染窗口的大小,则窗口区域中的区域将被剪切。然而,FBO并不受这类问题的影响。您可以创建一个大于显示窗口的framebuffer-renderable图像。

以下代码是在渲染循环开始之前设置FBO和可附加的可帧缓冲图像。请注意,不仅纹理图像附加到FBO,而且renderBuffer图像也附加到FBO的深度附着点上。我们实际上并没有使用这种深度缓冲区,但是FBO本身需要深度测试。如果我们不将此可渲染深度图像附加到FBO,则由于缺少深度测试,渲染输出将被损坏。如果在FBO渲染期间也需要模板测试,则附加的渲染缓存图像应附加到GL_STENCIL_ATTACHMENT。

...
// create a texture object
GLuint textureId;
glGenTextures(1, &textureId);
glBindTexture(GL_TEXTURE_2D, textureId);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_GENERATE_MIPMAP, GL_TRUE); // automatic mipmap
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, TEXTURE_WIDTH, TEXTURE_HEIGHT, 0,
             GL_RGBA, GL_UNSIGNED_BYTE, 0);
glBindTexture(GL_TEXTURE_2D, 0);

// create a renderbuffer object to store depth info
GLuint rboId;
glGenRenderbuffers(1, &rboId);
glBindRenderbuffer(GL_RENDERBUFFER, rboId);
glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_COMPONENT,
                      TEXTURE_WIDTH, TEXTURE_HEIGHT);
glBindRenderbuffer(GL_RENDERBUFFER, 0);

// create a framebuffer object
GLuint fboId;
glGenFramebuffers(1, &fboId);
glBindFramebuffer(GL_FRAMEBUFFER, fboId);

// attach the texture to FBO color attachment point
glFramebufferTexture2D(GL_FRAMEBUFFER,        // 1. fbo target: GL_FRAMEBUFFER 
                       GL_COLOR_ATTACHMENT0,  // 2. attachment point
                       GL_TEXTURE_2D,         // 3. tex target: GL_TEXTURE_2D
                       textureId,             // 4. tex ID
                       0);                    // 5. mipmap level: 0(base)

// attach the renderbuffer to depth attachment point
glFramebufferRenderbuffer(GL_FRAMEBUFFER,      // 1. fbo target: GL_FRAMEBUFFER
                          GL_DEPTH_ATTACHMENT, // 2. attachment point
                          GL_RENDERBUFFER,     // 3. rbo target: GL_RENDERBUFFER
                          rboId);              // 4. rbo ID

// check FBO status
GLenum status = glCheckFramebufferStatus(GL_FRAMEBUFFER);
if(status != GL_FRAMEBUFFER_COMPLETE)
    fboUsed = false;

// switch back to window-system-provided framebuffer
glBindFramebuffer(GL_FRAMEBUFFER, 0);
...

渲染到纹理的渲染过程与普通绘图几乎相同。我们只需要将渲染目的地从窗口系统提供的framebuffer切换到FBO即可。

...
// set rendering destination to FBO
glBindFramebuffer(GL_FRAMEBUFFER, fboId);

// clear buffers
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

// draw a scene to a texture directly
draw();

// unbind FBO
glBindFramebuffer(GL_FRAMEBUFFER, 0);

// trigger mipmaps generation explicitly
// NOTE: If GL_GENERATE_MIPMAP is set to GL_TRUE, then glCopyTexSubImage2D()
// triggers mipmap generation automatically. However, the texture attached
// onto a FBO should generate mipmaps manually via glGenerateMipmap().
glBindTexture(GL_TEXTURE_2D, textureId);
glGenerateMipmap(GL_TEXTURE_2D);
glBindTexture(GL_TEXTURE_2D, 0);
...

请注意,glGenerateMipmap()也作为FBO扩展的一部分包含,以便在修改基本级别的纹理图像之后显式生成mipmap。如果GL_GENERATE_MIPMAP设置为GL_TRUE,则glTex {Sub} Image2D()和glCopyTex {Sub} Image2D()触发自动mipmap生成(在OpenGL 1.4或更高版本中)。但是,由于FBO不调用glCopyTex {Sub} Image2D()来修改纹理,因此FBO操作不会自动生成基本层次的纹理。因此,必须显式地调用glGenerateMipmap()来生成mipmap。

另外,如果您需要对纹理进行后处理,则可以与像素缓冲区对象(PBO)组合,以有效地修改纹理。

小结

本文主要介绍了OpenGL中FBO是什么,同时讲清了 FBO与 纹理及Render Buffer之间的关系。最后通过实例说明了 FBO的使用。

点赞