OpenGL系列教程之十:OpenGL帧缓冲区对象(FBO)

时间:2022-06-02 05:14:31

相关主题:像素缓冲区对象(PBO)

下载: fbo.zip, fboDepth.zip, fboStencil.zip

更新:由于帧缓冲区对象从OpenGL 3.0开始成为了核心的特性,所以后缀”EXT“被移除了



概述
OpenGL渲染管线中,几何数据和纹理经过了几次变换和测试最终作为二维像素被渲染到屏幕中。OpenGL渲染管线中的目的地是帧缓冲区。帧缓冲区是被OpenGL使用的二维数组的集合,包括颜色缓冲区,深度缓冲区,模板缓冲区和累积缓冲区。默认情况下OpenGL使用完全被窗口创建和管理的帧缓冲区作为渲染的目的地。默认的帧缓冲区被叫做”窗口系统提供的“帧缓冲区。
OpenGL的扩展,GL_ARB_framebuffer_object提供了接口用来创建不渲染的帧缓冲区对象(FBO)。为了和”窗口系统提供的“帧缓冲区相区分开来这个帧缓冲区叫做”应用程序创建的“帧缓冲区。通过使用帧缓冲区对象(FBO),一个应用程序可以重定向渲染结果到”应用程序创建的“帧缓冲区对象(FBO)而不是传统的”窗口系统提供的“帧缓冲区中。这个过程是完全被OpenGL控制的。
”窗口系统提供的“帧缓冲区类似,一个FBO也包含颜色缓冲区,深度缓冲区,模板缓冲区。(注意FBO中没有累积缓冲区)。FBO中的这些逻辑上的缓冲区被叫做“附加到帧缓冲区中镜像“(framebuffer-attachable images),这些二维的像素数据可以被附加到帧缓冲区对象中。
有两种“附加到帧缓冲区中”镜像:纹理镜像和渲染缓冲区镜像。如果一个纹理对象的镜像被附加到帧缓冲区,OpenGL执行”渲染到纹理“的操作。如果一个渲染缓冲区对象的镜像被附加到帧缓冲区中,那么OpenGL执行”离屏渲染“。
顺便说一下,渲染缓冲区对象是在GL_ARB_framebuffer_object扩展中定义的一种新的存储对象类型。它被用来作为在渲染过程中简单二维图片的渲染目的地。
下面这幅图显示了帧缓冲区对象(FBO),纹理对象(Texture Object),渲染缓冲区对象(Renderbuffer Object)之间的连接关系。多个纹理对象和渲染缓冲区对象可以通过附加点被附加到帧缓冲区对象中。
OpenGL系列教程之十:OpenGL帧缓冲区对象(FBO) FBO,纹理对象(Texture Object),渲染缓冲区对象(Renderbuffer Object)
一个FBO中有多个颜色附加点(GL_COLOR_ATTACHMENT0,..., GL_COLOR_ATTACHMENTn),一个深度附加点(GL_DEPTH_ATTACHMENT),一个模板附加点(DL_STENCIL_ATTACHMENT)。颜色附加点的数目是依赖于显卡的,但是每个FBO对象中至少含有一个颜色附加点。你可以使用GL_MAX_COLOR_ATTACHMENTS查询显卡支持的颜色顶点的最大数。一个FBO中有多个颜色附加点的原因是为了同时将颜色缓冲区渲染到多个目的地。GL_ARB_draw_buffers扩展实现了”多个渲染目标(mutiple render targets ,MRT)“的功能。注意帧缓冲区对象自身并不包含存储数据的二维数组,但是它有多个附加点。
帧缓冲区对象(FBO)提供了一种高效的切换机制:从帧缓冲区对象中分离之前附加的对象,或者附加一个新对象到帧缓冲区对象中。切换帧缓冲区对象中的附加对象比切换FBO快很多。FBO提供了glFramebufferTexture2D()来切换纹理对象,提供glFramebufferRenderbuffer()来切换渲染缓冲区对象。


创建FBO
创建帧缓冲区对象(FBO)和创建顶点缓冲区对象(VBO)类似。


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

glGenFramebuffers()需要两个参数:第一个参数标示要创建的帧缓冲区的数目,第二个参数表示存放生成的帧缓冲区ID的GLuint类型的变量或数组的地址。它返回为使用的帧缓冲区对象的ID。ID为0标示默认的帧缓冲区,就是”窗口系统提供的“帧缓冲区。
当FBO不再使用的时候可以使用glDeleteFramebuffers()来删除它。


glBindFramebuffer()

当FBO创建以后,在使用它之前需要先绑定它。
void glBindFramebuffer(GLenum target, GLuint id)

第一个参数target需要是GL_FRAMEBUFFER,第二个参数是帧缓冲区对象的ID。当一个FBO绑定以后,所有的OpenGL操作将会作用在这个绑定的帧缓冲区对象上。ID为0的帧缓冲区对象是”窗口系统提供的“帧缓冲区。因此为了解除当前正在使用的帧缓冲区(FBO),使用glBindFramebuffer()绑定到0。


渲染缓冲区对象 另外,渲染缓冲区对象(renderbuffer object)是在离屏渲染(offscreen rendering)中新介绍到的。它允许将一个场景直接渲染到渲染缓冲区对象而不是纹理对象中。缓冲缓冲区对象是一个包含可渲染的内部格式镜像的数据存储区。它被用来存储OpenGL中没有相关联的的纹理格式的逻辑缓冲区,如模板缓冲区和深度缓冲区。


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

当一个渲染缓冲区创建以后,它会返回一个非零的正数。0为OpenGL保存。


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

当一个渲染缓冲区对象被创建以后,它不包含任何的数据存储区,因此我们必须为它分配空间。这可以使用glRenderbufferStorage()来实现。第一个参数必须是GL_RENDERBUFFER。第二个参数可以是渲染颜色(GL_RGB,GL_RGBA,等),渲染深度(GL_DEPTH_COMPONENT),或者渲染模板格式(GL_STENCIL_INDEX)。width和height是渲染缓冲区镜像的尺寸(以像素为单位)。

width和height的值应该小于GL_MAX_RENDERBUFFER_SIZE的值,否则会产生一个GL_INVALID_VALUE的错误。


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

你也可以得到现在正在使用的渲染缓冲区的参数。target必须是GL_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自身并没有缓冲区,而是由我们将”可以附加到帧缓冲区“的镜像(framebuffer-attachable iamges,纹理对象或渲染缓冲区对象)附加到FBO中。这种机制允许FBO快速切换(detach and attach,分离和附加)在FBO中的”可以附加到帧缓冲区“的镜像。切换这种绑定的镜像比在FBO中更加快速。并且,减少了没必要的数据拷贝和内存消耗。例如,一个纹理对象可以附加到多个帧缓冲区对象中,它的缓冲区可以被多个FBO共享。


附加二维纹理镜像到FOB中
glFramebufferTexture2D(GLenum target,
GLenum attachmentPoint,
GLenum textureTarget,
GLuint textureId,
GLint level)

glFramebufferTexture2D()将一个二维纹理镜像附加到FBO中。第一个参数必须是GL_FRAMEBUFFER。第二个参数是连接纹理镜像的附加点,一个FBO中有多个颜色附加点(GL_COLOR_ATTACHMENT0, ...,GL_COLOR_ATTACHMENTn),一个深度附加点(GL_DEPTH_ATTACHMENT),和一个模板附加点(GL_STENCIL_ATTACHMENT)。第三个参数,"texture Target"大多数情况下为GL_TEXTURE_2D。第四个参数是纹理对象的标示符。最后一个参数附加纹理的映射级别。
如果textureId的值为0,那么纹理镜像会从PBO中分离。如果一个纹理对象在它还附加到FBO中时被删除了,那么这个纹理对象会自动从当前绑定的FBO中分离。然而,如果它被附加到多个FBO并且被删除了,那么将只会从当前被绑定的FBO中删除,而不会从未绑定的FBO中删除。



附加渲染缓冲区镜像到FBO中
void glFramebufferRenderbuffer(GLenum target,
GLenum attachmentPoint,
GLenum renderbufferTarget,
GLuint renderbufferId)

可以使用glFramebufferRenderbuffer()将一个渲染缓冲区镜像附加到FBO中。第一个参数和第二个参数和glFramebufferTexture2D()一样。第三个参数必须是GL_RENDERBUFFER。最后一个参数是渲染缓冲区对象的标示。
如果renderbufferId参数被设置成0,那么渲染缓冲区对象将会从FBO中的附加点分离。如果一个渲染缓冲被附加到FBO时被删除了,它会自动从当前被绑定的FBO中分离开来。然而,它不会从其他未被绑定的FBO中分离出来。


检查FBO的状态
当一个可附加的镜像(纹理对象或渲染缓冲区对象)被附加到FBO中以后并且在FBO执行操作之前,你必须使用glCheckFramebufferStatus()来检查FBO的状态是否完整。如果FOB不是完整的,那么任何绘制或读取的命令(glBegin,glCopyTexImage2D()等)都会失败。
GLenum glCheckFramebufferStatus(GLenum target)

glCheckFramebufferStatus()检查附加到当前被绑定的FBO的镜像和帧缓冲的参数。并且这个函数不能在glBegin()和glEnd()之间。target参数必须是GL_FRAMEBUFFER。检查完FBO后会返回一个非零的值。如果所有的条件和规则都满足,会返回GL_FRAMEBUFFER_COMPLETE。否则,它返回一个有关的错误值,这个错误值会告诉违反了那条规则。

FBO中完整的规则如下:
  • 附加到帧缓冲区镜像的width和height必须是非零的。
  • 如果一个镜像被附加到颜色附加点,那么这个镜像必须有一个可渲染颜色(color-renderable)的内部格式。(GL_RGBA,GL_DEPTH_COMPONENT,GL_LUMINANCE等)
  • 如果一个镜像被附加到GL_DEPTH_ATTACHMENT,那么这个镜像必须有可渲染深度(depth-renderable)的内部格式。(GL_DEPTH_COMONENT,GL_DEPTH_COPONENT24等)
  • 如果一个镜像被附加到GL_STENCIL_ATTACHMENT,那么这个镜像必须有可渲染模板(stencil-renderable)的内部格式。(GL_STENCIL_INDEX,GL_STENCIL_INDEX8等)
  • FBO必须至少有一个附加的镜像。
  • 所有附加到FBO的镜像必须有相同的宽体和高度。
  • 所有附加到颜色附加点的镜像必须有相同的内部格式。

注意即使上面所有的条件都满足,你的OpenGL驱动(GPU)可能不支持某些内部的格式或参数。如果某个特殊的实现不被OpenGL驱动支持,那么glCheckFramebufferStatus()会返回GL_FRAMEBUFFER_UNSUPPORTED。

sample code提供了一些工具函数来显示当前FBO的信息:printFramebufferInfo()和checkFramebufferStatus()。




例子:渲染到纹理

OpenGL系列教程之十:OpenGL帧缓冲区对象(FBO)OpenGL系列教程之十:OpenGL帧缓冲区对象(FBO)


下载源文件和可执行文件:fbo.zip(更新:2012-07-08)

附加:

-只渲染到深度缓冲区: fboDepth.zip

-使用模板缓冲区渲染物体的轮廓: fboStencil.zip

-使用glBlitFramebuffer()在两个FBO中传输:fboBlit.zip


有时,你需要生成动态的纹理。最普遍的例子是生成镜像/反射的效果,动态的立方体/环境映射和阴影映射。动态的纹理可以使用渲染场景到纹理中来生成。一个传统的渲染到纹理的方法是按照正常的步骤绘制一个场景到帧缓冲区中,然后使用glCopyTexSubImage2D()将帧缓冲区中的镜像复制到纹理中。


使用FBO,我们可以直接将一个场景渲染到纹理中,因此我们完全不需要使用”窗口系统提供的“帧缓冲区。除此之外,我们还可以减少额外的数据复制(从帧缓冲区到纹理)。


这个例子程序使用用FBO和不用FBO两种方式执行了渲染到纹理的操作,并比较了它们的性能差异。除了性能方面的区别外,FBO还有另外一个优点,在传统的渲染到纹理的模式中如果纹理的分辨率比渲染窗口大,那么纹理中窗口区域之外的部分会被裁剪掉,然后使用FBO不会产生这种裁剪的问题。你可以创建一个比显示窗口大的可渲染的帧缓冲区镜像。


下面的代码在渲染操作开始之前设置了一个FBO和附加到帧缓冲区的镜像。注意不仅纹理镜像可以附加到FBO中,渲染缓冲区镜像(深度缓冲,模板缓冲)也可以被附加到FBO。我们不会实际使用深度缓冲,然而FBO需要它进行深度测试。如果我们不将深度缓冲附加到FBO中,那么渲染输出的结果会被破坏因为缺少了深度测试。如果在FBO渲染时需要模板测试,那么额外的模板缓冲需要附加到GL_STENCIL_ATTACHMENT附加点。


...
// 创建一个纹理对象
GLuint textureId;
glGenTextures(1, &textureId);
glBindTexture(GL_TEXTURE_2D, textureId);
glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_GENERATE_MIPMAP, GL_TRUE); // 自动贴图
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, TEXTURE_WIDTH, TEXTURE_HEIGHT, 0,
GL_RGBA, GL_UNSIGNED_BYTE, 0);
glBindTexture(GL_TEXTURE_2D, 0);

// 创建一个渲染缓冲区对象来存储深度信息
GLuint rboId;
glGenRenderbuffers(1, &rboId);
glBindRenderbuffer(GL_RENDERBUFFER, rboId);
glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_COMPONENT,
TEXTURE_WIDTH, TEXTURE_HEIGHT);
glBindRenderbuffer(GL_RENDERBUFFER, 0);

// 创建一个帧缓冲区对象
GLuint fboId;
glGenFramebuffers(1, &fboId);
glBindFramebuffer(GL_FRAMEBUFFER, fboId);

// 将纹理对象附加到FBO的颜色附加点上
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)

// 将渲染缓冲区对象附加到FBO的深度附加点上
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

// 检查FBO的状态
GLenum status = glCheckFramebufferStatus(GL_FRAMEBUFFER);
if(status != GL_FRAMEBUFFER_COMPLETE)
fboUsed = false;

// 切换到窗口系统提供的帧缓冲区中
glBindFramebuffer(GL_FRAMEBUFFER, 0);
...


渲染到纹理的过程和普通的绘制过程基本一样。我们只需要将渲染的目的地由”窗口系统提供的“帧缓冲区改变成"应用程序创建的"帧缓冲区(FBO)中。


...
// 设置FBO为渲染的目的地
glBindFramebuffer(GL_FRAMEBUFFER, fboId);

// 清除缓冲区
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

// 直接绘制一个场景到纹理中
draw();

// 解除FBO的绑定
glBindFramebuffer(GL_FRAMEBUFFER, 0);

// 贴图生成
// 注意:如果GL_GENERATE_MIPMAP被设置成 GL_TRUE, 那么glCopyTexSubImage2D()
//会自动生成贴图. 然而, 附加到FBO的纹理需要使用glGenerateMipmap()来生成贴图
glBindTexture(GL_TEXTURE_2D, textureId);
glGenerateMipmap(GL_TEXTURE_2D);
glBindTexture(GL_TEXTURE_2D, 0);
...

注意为了在修改纹理镜像的基础级别后显示地生成贴图glGenerateMipmap()也被当做FBO的扩展。如果GL_GENERATE_MIPMAP被设置为GL_TRUE,那么glTex{Sub}Image2D()和glCopyTex{Sub}Image2D()会自动触发生成贴图(在OpenGL 1.4版本或更高的版本中)。然而,FBO当它的基础纹理被更改后不会自动生成它的贴图这是因为FBO不会调用glCopyTex{Sub}Image2D()来修改纹理。因此,glGenerateMipmap()必须被显示地调用。


如果你需要快速地出来纹理,可以联合像素缓冲区对象(PBO)一起修改纹理。