本文原链接:http://www.songho.ca/opengl/gl_fbo.html,出于兴趣做了翻译。受限于个人能力,翻译中难免出现一些错误,仅供参考,不保证文章的正确性。
Related Topics: Pixel Buffer Object (PBO)
Download: fbo.zip, fboDepth.zip, fboStencil.zip, fboBlit.zip, fboMsaa.zip
目录
概述
创建FBO
glGenFramebuffers()
glBindFramebuffer()
Renderbuffer Object
glGenRenderbuffers()
glBindRenderbuffer()
glRenderbufferStorage()
glGetRenderbufferParameteriv()
Attaching images to FBO
Attaching a 2D texture image to FBO
FBO with MSAA (Multi Sample Anti Aliasing)
Checking FBO Status
GLenum glCheckFramebufferStatus(GLenum target)
Example: Render To Texture
个人总结
在OpenGL的渲染管道中,几何数据和纹理数据通过几次测试后最终渲染到屏幕上,以2D的pixels形式存在。OpenGL渲染管道最终渲染的“目的地”称为帧缓冲区(原文为framebuffer,下面就沿用framebuffer)。framebuffer是一个OpenGL实现的2D数组/存储空间的合集,包括颜色缓冲区,深度缓冲区,模板缓冲区和accumulation 缓冲区。OpenGL默认使用由操作系统创建管理的framebuffer,被称为window-system-provided framebuffer。
OpenGL的扩展中,GL_ARB_framebuffer_object提供了用于创建其他的非显示的framebuffer object。为了区别于上文提到的window-system-provided framebuffer,我们称它为application-created framebuffer。我们可以使用相关函数使OpenGL的渲染目的地从默认的window-system-provided framebuffer重定向到我们自己创建的FBO(Framebuffer Object),并通过OpenGL完全控制它。
与window-system-provided framebuffer类似,一个FBO包含一个渲染“目的地”的合集:颜色缓冲区、深度缓冲区、模板缓冲区(FBO中没有定义accumulation buffer)。这些缓冲区被称为framebuffer-attachable image,是用于绑定到一个FBO的2D数组。
有两种用于绑定到FBO的image,纹理图像(texture image)和渲染图像(renderbuffer image)。如果纹理图像绑定到framebuffer,OpenGL执行“渲染到纹理”;如果一个renderbuffer对象绑定到framebuffer,OpenGL执行“离屏渲染”。
顺便说一句,renderbuffer object是GL扩展中定义的一种新的存储对象,用于在渲染流程中作为一个2D图像的渲染“目的地”。
下表说明了framebuffer对象,纹理对象和renderbuffer对象之间的联系,多重纹理对象或者renderbuffer对象可通过绑定点绑定到framebuffer对象上。
一个FBO中有多个颜色缓冲区绑定点,一个深度缓冲区绑定点和一个模板缓冲区绑定点。颜色缓冲区绑定点数量与系统相关,但是不少于一个。你可以使用GL_MAX_COLOR_ATTACHMENTS参数查询系统支持的颜色缓冲区绑定点数量,这由显卡支持。FBO有多个颜色缓冲区绑定点的目的在于支持同时渲染到多个“目的地”,结合GL_ARB_draw_buffers()实现多目标渲染(multiple render targets, MRT)。注意FBO本身没有任何的存储空间,它有的只是绑定点。
FBO提供了一种高效的切换机制,我们可以把之前绑定了的image从FBO中解绑然后绑定一个新的image。切换绑定的image比切换FBO要高效,FBO提供了glFramebufferTexture2D()来切换2D纹理对象,和glFramebufferRenderbuffer()来切换renderbuffer对象
创建FBO和创建VBO比较相似,调用glGenFramebuffers()
void glGenFramebuffers(GLsizei n, GLuint* ids) void glDeleteFramebuffers(GLsizei n, const GLuint* ids)
glGenFramebuffers()需要两个参数,第一个是要创建的FBO的数量,第二个是一个GLuint变量或者数组的指针,用于存储一个或者多个创建了的FBO的ID。系统返回未被使用的FBO的ID,0代表系统默认的FBO,就是window-system-provided FBO,当不再使用FBO时调用glDeleteFramebuffers()来删除它。
创建FBO后需要绑定它才能使用,调用glBindFramebuffer()
void glBindFramebuffer(GLenum target, GLuint id)
第一个参数应该赋值为GL_FRAMEBUFFER,第二个参数是FBO的ID。一旦FBO被绑定,所有的OpenGL操作将作用于它。ID 0 被保留用于绑定系统默认的window-system-provided FBO,因此可以将ID设为0调用该函数来解绑之前绑定的FBO
renderbuffer object是针对离屏渲染引入的新设定,它使得程序可以直接渲染场景到一个renderbuffer object,而不是一个texture object。Renderbuffer就是一个数据存储对象,其中包含具有可渲染的内部格式的一幅“图像”。它用于保存OpenGL不具有对应的纹理格式的逻辑缓存,譬如模板缓存或者深度缓存。
void glGenRenderbuffers(GLsizei n, GLuint* ids) void glDeleteRenderbuffers(GLsizei n, const Gluint* ids)
一旦创建了renderbuffer,该函数返回正整数的ID,0的ID 被OpenGL保留
void glBindRenderbuffer(GLenum target, GLuint id)
和其他的对象一样,在使用renderbuffer之前需要绑定它,函数的第一个参数应该赋值为GL_RENDERBUFFER
void glRenderbufferStorage(GLenum target, GLenum internalFormat, GLsizei width, GLsizei height)
创建了一个renderbuffer object之后它并没有任何存储空间,所以我们通过该函数为它分配空间。第一个参数需要赋值为GL_RENDERBUFFER,第二个参数可以是保存颜色渲染结果的格式(GL_RGB,GL_RGBA等),保存深度渲染结果的格式(GL_DEPTH_COMPONENT),或者保存模板渲染结果的格式(GL_STENCIL_INDEX)。width和height是renderbuffer中“图像”的像素宽高,不能超过GL_MAX_RENDERBUFFER_SIZE,否则会产生GL_INVALID_VALUE 的错误。
void glGetRenderbufferParameteriv(GLenum target, GLenum param, GLint* value)
通过该函数可以获得当前绑定的renderbuffer object的许多参数。target应该赋值为GL_RENDERBUFFER,第二个参数赋值系统定义的下参数集合,value是系统返回的参数值的指针。第二个参数可赋值的参数定义如下
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自身没有图像存储,我们需要绑定纹理或者renderbuffer object给它。这种机制使得可以在一个FBO内快速地切换不同的image,这比切换FBO要快得多。同时这也避免了一些不必要的内存拷贝与开销。譬如一个纹理可以绑定到不同的FBO,所以它的存储空间可以被多个FBO共享。
void 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。第三个参数一般是GL_TEXTURE_2D,第四个参数时纹理对象的ID,最后一个参数时纹理的贴图细化等级。
如果textureId设为0,该纹理将从FBO解绑。如果一个纹理在绑定到一个FBO时被删除,它会被自动的从FBO解绑。然而如果它被绑定到多个FBO,删除该纹理时系统只会自动的从当前正在使用的FBO上解绑它,而不会从那些未使用的FBO上解绑。
当渲染到一个FBO时,即使你使用多重采样属性为window-system-provided framebuffer创建了渲染上下文,也不会自动开启抗锯齿。为了使用多重采样抗锯齿,我们需要给FBO的颜色和/或深度绑定点绑定多个图像存储。
使用glRenderbufferStorageMultisample()来为FBO绑定存储空间实现多重采样抗锯齿
void glRenderbufferStorageMultisample(GLenum target, GLsizei samples, GLenum internalFormat, GLsizei width, GLsizei height)
相比于glRenderbufferStorage()其中增加的参数samples是多重采样的次数,如果设置为0则不开启该功能,等同于调用glRenderbufferStorage()。可以使用glGetIntegerv()调用GL_MAX_SAMPLES 获取系统支持的最大采样次数。
下面的代码中创建了一个FBO绑定了多重采样的颜色缓存和深度缓存。注意如果有一个多重采样的缓存绑定了FBO,那么其他绑定了的缓存需要具有同等采样次数,否则FBO配置不完整。
// 创建一个采样层数为4的renderbuffer对象用于存储颜色缓存
int msaa = 4;
GLuint rboColorId;
glGenRenderbuffers(1, &rboColorId);
glBindRenderbuffer(GL_RENDERBUFFER, rboColorId);
glRenderbufferStorageMultisample(GL_RENDERBUFFER, msaa, GL_RGB8, width, height);
// 创建一个采样层数为4的renderbuffer对象用于存储深度缓存
GLuint rboDepthId;
glGenRenderbuffers(1, &rboDepthId);
glBindRenderbuffer(GL_RENDERBUFFER, rboDepthId);
glRenderbufferStorageMultisample(GL_RENDERBUFFER, msaa, GL_DEPTH_COMPONENT, width, height);
// 创建一个采样层数为4的FBO
GLuint fboMsaaId;
glGenFramebuffers(1, &fboMsaaId);
glBindFramebuffer(GL_FRAMEBUFFER, fboMsaaId);
// 绑定颜色缓存到FBO
glFramebufferRenderbuffer(GL_FRAMEBUFFER, // 1. fbo target: GL_FRAMEBUFFER
GL_COLOR_ATTACHMENT0, // 2. color attachment point
GL_RENDERBUFFER, // 3. rbo target: GL_RENDERBUFFER
rboColorId); // 4. rbo ID
// 绑定深度缓存到FBO
glFramebufferRenderbuffer(GL_FRAMEBUFFER, // 1. fbo target: GL_FRAMEBUFFER
GL_DEPTH_ATTACHMENT, // 2. depth attachment point
GL_RENDERBUFFER, // 3. rbo target: GL_RENDERBUFFER
rboDepthId); // 4. rbo ID
// 检测FBO状态
GLenum status = glCheckFramebufferStatus(GL_FRAMEBUFFER);
if(status != GL_FRAMEBUFFER_COMPLETE)
fboUsed = false;
注意glRenderbufferStorageMultisample()只开启了渲染到FBO时的多重采样抗锯齿效果,但我们不能直接使用它的结果。如果需要将渲染结果保存到一个纹理或者其他的不具有多重采样属性的FBO上,需要使用glBlitFramebuffer()将结果转换为单次采样的数据。
void glBlitFramebuffer(GLint srcX0, GLint srcY0, GLint srcX1, GLint srcY1,// src rect
GLint dstX0, GLint dstY0, GLint dstX1, GLint dstY1, // destination rect
GLbitfield mask, GLenum filter)
glBlitFramebuffer()从source处拷贝一个矩形区域的内存到目标的缓存中,"mask"指定哪部分数据被拷贝,GL_COLOR_BUFFER_BIT, GL_DEPTH_BUFFER_BIT 和/或 GL_STENCIL_BUFFER_BIT。最后一个参数"filter"指定当source和destination的内存区域大小不一致时的差值方式,可以是GL_NEAREST 或者GL_LINEAR。
下面的代码用于将一个具有多重采样属性FBO的图像拷贝到另一个不具有多重采样属性的FBO上。注意这里需要一个额外的FBO用于储存多重采样抗锯齿渲染的结果。可以参考fboMsaa.zip查看更多细节。
// copy rendered image from MSAA (multi-sample) to normal (single-sample)
// NOTE: The multi samples at a pixel in read buffer will be converted
// to a single sample at the target pixel in draw buffer.
glBindFramebuffer(GL_READ_FRAMEBUFFER, fboMsaaId); // src FBO (multi-sample)
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, fboId); // dst FBO (single-sample)
glBlitFramebuffer(0, 0, width, height, // src rect
0, 0, width, height, // dst rect
GL_COLOR_BUFFER_BIT, // buffer mask
GL_LINEAR); // scale filter
当可绑定的图像(纹理/renderbuffer)绑定到FBO上后,在使用FBO之前需要使用glCheckFramebufferStatus()确认FBO的状态。如果FBO状态不完整,那些绘制和读取指令(glBegin()、glCopyTexImage2D())会失败
glCheckFramebufferStatus()会验证绑定到FBO上的图像的参数,该函数不能在glBegin()/glEnd()之间调用,target参数应该赋值为GL_FRAMEBUFFER,函数返回非零值。如果所有需求都满足,函数返回GL_FRAMEBUFFER_COMPLETE,否则它会返回对应的错误值。
FBO配置完整性的规则如下:
1)用于绑定的图像存储的宽和高必须为非0值
2)如果一个图像存储被绑定到颜色绑定点,它应该具备可用于渲染到颜色的内部格式(GL_RGBA、GL_DEPTH_COMPONENT、GL_LUMINANCE等)
3)如果一个图像存储被绑定到深度绑定点,它应该具备可用于渲染到深度的内部格式(GL_DEPTH_COMPONENT, GL_DEPTH_COMPONENT24等)
4)FBO至少要绑定一个图像存储
5)所有绑定到FBO的图像存储具有相同的宽和高
6)所有绑定到FBO颜色绑定点的图像存储具有相同的内部格式
注意即使上述所有条件都满足了,OpenGL也有可能不支持一些参数的组合。如果某种参数组合不被支持,glCheckFramebufferStatus()将返回GL_FRAMEBUFFER_UNSUPPORTED
示例代码sample code 提供了一些实用函数来输出当前使用的FBO的相关信息,主要是printFramebufferInfo()和checkFramebufferStatus()
下载源码工程 fbo.zip (Updated: 2016-11-14)
其他代码工程:
- 只渲染到深度缓存: fboDepth.zip
- 使用模板缓存渲染到物体的轮廓: fboStencil.zip
- 使用glBlitFramebuffer()在两个FBO间传入数据: fboBlit.zip
- 多重采样抗锯齿渲染到纹理: fboMsaa.zip
有时候我们需要在流程中产生动态的纹理,譬如产生镜像/反射效果,动态的环境贴图和阴影贴图等。可以利用渲染场景到一个纹理来实现动态纹理,传统方法是绘制场景到一个FBO,然后调用glCopyTexSubImage2D()拷贝数据到一个纹理中。
但是使用FBO时我们可以直接将场景渲染到一个纹理中,这样我们就不需要使用window-system-provided FBO。更进一步,我们可以避免额外的数据拷贝(从FBO到纹理)。
下面的例程中展示了使用/不使用FBO实现渲染到纹理的操作,然后比较其性能。除了性能上的优势,使用FBO还有另外一个优势。传统的不使用FBO的方法中如果纹理分辨率比渲染窗口大,超出窗口的区域会被裁剪。但是使用FBO时不存在这个问题,我们可以创建比显示窗口大得多的framebuffer-renderable 图像。
下面的代码在渲染循环开始之前创建一个FBO并绑定了用于渲染的图像。注意除了昂定了纹理图像,还绑定了renderbuffer图像到深度绑定点。代码中我们没有用到这个深度buffer,但是FBO本身需要用它做深度测试。如果我们不绑定enderbuffer图像到深度绑定点,渲染输出会因为缺失深度测试而崩溃。类似的如果在FBO渲染中需要做模板测试,还需要绑定renderbuffer图像到GL_STENCIL_ATTACHMENT
...
// 创建一个纹理对象
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);
// 创建一个renderbuffer对象保存深度信息
GLuint rboId;
glGenRenderbuffers(1, &rboId);
glBindRenderbuffer(GL_RENDERBUFFER, rboId);
glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_COMPONENT,
TEXTURE_WIDTH, TEXTURE_HEIGHT);
glBindRenderbuffer(GL_RENDERBUFFER, 0);
// 创建一个FBO
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)
// 绑定renderbuffer到深度绑定点
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;
// 切换到系统提供的FBO
glBindFramebuffer(GL_FRAMEBUFFER, 0);
...
渲染到纹理的流程和正常渲染相似,我们只需要将渲染目标由系统提供的FBO更换为不显示的,应用创建的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()来在修改了纹理图像的基础层级之后显式触发多重采样。如果GL_GENERATE_MIPMAP被设为GL_TRUE,那么调用glTex{Sub}Image2D()和glCopyTex{Sub}Image2D()会触发自动的多重采样(在1.4版本或之后)。但是在纹理的基础层级改变时FBO不会自动触发多重采样,因为FBO不会调用glCopyTex{Sub}Image2D()来修改纹理,因此需要显式调用glGenerateMipmap()来触发多重采样。
如果需要对纹理做进一步操作,可以配合 Pixel Buffer Object (PBO)高效地操作纹理。
自己使用opengl过程中,FBO主要用在:1)离屏渲染,可在允许范围内随意创建离屏渲染时目标图像的大小,且其绑定的颜色缓冲区、深度缓冲区、模板缓冲区可自行配置;2)渲染到纹理,就像文中说的opengl渲染的结果只是一个“中间结果”,还需进一步处理时,此时利用该方法可以实现高效地渲染到纹理。