Loading... OpenGL中有很多常用的屏幕缓冲:颜色缓冲、深度缓冲、模板缓冲等。这些缓冲结合起来的话统称为:**帧缓冲**(Frame Buffer)。我们可以定义自己的帧缓冲。 OpenGL程序有一个**默认帧缓冲**,它是在创建窗口时候就自动生成的(GLFW),在自定义配置帧缓冲之前,所有的操作都是在默认帧缓冲上进行。 ## 操作帧缓冲对象 ### 创建一个帧缓冲 `void glGenFramebuffers( GLsizei n, GLuint *ids )` 将返回n个FBO(帧缓冲对象,下称FBO)的句柄。 ```cpp unsigned int fbo; glGenFramebuffers(1, &fbo); ``` ### 绑定帧缓冲 和其他对象的操作类似,FBO也需要绑定激活和解绑的操作。 `void glBindFramebuffer( GLenum target, GLuint framebuffer )` 函数会将FBO绑定在指定的目标上。 可选的目标:`GL_DRAW_FRAMEBUFFER`、`GL_READ_FRAMEBUFFER`、`GL_FRAMEBUFFER` 在绑定到`GL_FRAMEBUFFER`目标之后,所有的**读取**和**写入**帧缓冲的操作都将会影响当前绑定的帧缓冲。 我们也可以使用`GL_READ_FRAMEBUFFER`或`GL_DRAW_FRAMEBUFFER`,将一个帧缓冲分别绑定到读取目标或写入目标。 不过一般都不需要区别,都会使用绑定到`GL_FRAMEBUFFER`。 ```cpp glBindFramebuffer(GL_FRAMEBUFFER, fbo); ``` ### 检查FBO状态 FBO在使用前必须保证它是**完整**(complete)的。 一个完整的FBO需要满足以下的条件: - **附加**至少一个缓冲(颜色、深度或模板缓冲)。 - 至少有一个颜色**附件** (Attachment)。 - 所有的附件都必须是完整的(保留了内存)。 - 每个缓冲都应该有相同的样本数(MSAA)。 可以用函数 `GLenum glCheckFramebufferStatus( GLenum target )` 检查绑定的FBO的状态。 `GLenum glCheckNamedFramebufferStatus( GLuint framebuffer, GLenum target )`可以检查指定名称的FBO的状态(OpenGL4.5版本以后支持) 如果它们返回的是**GL_FRAMEBUFFER_COMPLETE**,帧缓冲就是完整的了。 ```cpp if(glCheckFramebufferStatus(GL_FRAMEBUFFER) == GL_FRAMEBUFFER_COMPLETE) // 成功 ``` 其他的错误状态可以查阅:[docs.gl](https://docs.gl/gl4/glCheckFramebufferStatus) ### 离屏渲染 绑定完之后所有的渲染操作将会渲染到当前绑定帧缓冲的附件中。 由于我们的帧缓冲不是默认帧缓冲,渲染指令将不会对窗口的视觉输出有任何影响。出于这个原因,渲染到一个不同的帧缓冲被叫做**离屏渲染**(Off-screen Rendering) 如果要保证所有的渲染操作在主窗口中有视觉效果,我们需要再次激活默认帧缓冲,将它绑定到`0`(0即为默认FBO的句柄),这一操作即为**解绑**之前绑定的自定义的FBO。 ```cpp glBindFramebuffer(GL_FRAMEBUFFER, 0); ``` ### 删除 用完FBO之后不要忘记删除掉它 ```cpp glDeleteFramebuffers(1, &fbo); ``` ## 附件 刚刚提到帧缓冲要**至少附加一个颜色附件**(Attachment)。附件是一个内存位置,它能够作为帧缓冲的一个缓冲,它可以是颜色、深度、模板类型,可以将它想象为一个图像。 当创建一个附件的时候我们有两个选项:**纹理**或**渲染缓冲对象(Renderbuffer Object)**。  ### 纹理附件 如果将纹理作为附件绑定到FBO上,那么所有的缓冲内容都会保存在这个纹理当中。 以下函数可以用于绑定纹理附件: ```cpp void glFramebufferTexture( GLenum target, GLenum attachment, GLuint texture, GLint level) void glFramebufferTexture1D( GLenum target, GLenum attachment, GLenum textarget, GLuint texture, GLint level) void glFramebufferTexture2D( GLenum target, GLenum attachment, GLenum textarget, GLuint texture, GLint level) void glFramebufferTexture3D( GLenum target, GLenum attachment, GLenum textarget, GLuint texture, GLint level, GLint layer) // OpenGL4.5版本后支持 void glNamedFramebufferTexture( GLuint framebuffer, GLenum attachment, GLuint texture, GLint level) ``` 用例: ```cpp // 创建纹理对象 unsigned int texture; glGenTextures(1, &texture); glBindTexture(GL_TEXTURE_2D, texture); // 指定纹理图像格式,https://docs.gl/gl4/glTexImage2D glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, 800, 600, 0, GL_RGB, GL_UNSIGNED_BYTE, NULL); // 设定LOD过滤方式,https://docs.gl/gl4/glTexParameter glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); // 绑定纹理附件 glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, texture, 0); ``` 这些函数的参数: - `target`: 同上,帧缓冲的目标:`GL_DRAW_FRAMEBUFFER`、`GL_READ_FRAMEBUFFER`、`GL_FRAMEBUFFER` - `attachment`:我们想要附加的附件类型,有`GL_COLOR_ATTACHMENTx`、`GL_DEPTH_ATTACHMENT`、`GL_STENCIL_ATTACHMENT`。上面用例附加了一个颜色附件。注意颜色附件最后的x是一个正整数,意味着我们**可以附加多个颜色附件**。 - `textarget`:用于 `glFramebufferTexture1D`、`glFramebufferTexture2D` 和 `glFramebufferTexture3D`,声明纹理的类型 - `texture`:要附加的纹理对象 - `level`:mipmap的级别 - `framebuffer`:用于`glNamedFramebufferTexture`指定要绑定的FBO 要附加深度缓冲的话,我们将附件类型设置为**GL_DEPTH_ATTACHMENT**,同时在声明纹理的时候,注意纹理的格式(Format)和内部格式(Internalformat)类型将变为**GL_DEPTH_COMPONENT**,来反映深度缓冲的储存格式。要附加模板缓冲的话,你要将第二个参数设置为**GL_STENCIL_ATTACHMENT**,并将纹理的格式设定为**GL_STENCIL_INDEX**。 也可以用一个纹理附件同时附加到深度缓冲和模板缓冲。纹理的每32位数值将包含24位的深度信息和8位的模板信息。要将深度和模板缓冲附加为一个纹理的话,我们使用**GL_DEPTH_STENCIL_ATTACHMENT**类型,并配置纹理的内格式为**GL_DEPTH24_STENCIL8**,让它包含合并的深度和模板值。将一个深度和模板缓冲附加为一个纹理到帧缓冲的例子可以在下面找到: ```cpp glTexImage2D( GL_TEXTURE_2D, 0, GL_DEPTH24_STENCIL8, 800, 600, 0, GL_DEPTH_STENCIL, GL_UNSIGNED_INT_24_8, NULL ); glFramebufferTexture2D(GL_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_TEXTURE_2D, texture, 0); ``` > 上述用例都是设置了一个和屏幕大小一样的纹理,如果你想将你的屏幕渲染到一个更小或更大的纹理上,你需要(在渲染到你的帧缓冲之前)再次调用glViewport,使用纹理的大小作为参数,否则只有一小部分的纹理或屏幕会被渲染到这个纹理上。 ### 渲染缓冲对象附件 渲染缓冲对象(Renderbuffer Object)是在纹理之后引入到OpenGL中,作为一个可用的帧缓冲附件类型的,所以在过去纹理是唯一可用的附件。和纹理图像一样,渲染缓冲对象是一个真正的缓冲,即一系列的字节、整数、像素等。**渲染缓冲对象附加的好处是,它会将数据储存为OpenGL原生的渲染格式,它是为离屏渲染到帧缓冲优化过的。** 渲染缓冲对象(下称RBO)直接将所有的渲染数据储存到它的缓冲中,不会做任何针对纹理格式的转换,让它变为一个更快的可写储存介质。 然而,RBO通常都是只写的,所以你不能读取它们(比如使用纹理访问)。当然你仍然还是能够使用glReadPixels来读取它,这会从当前绑定的FBO中返回特定区域的像素,而不是附件本身。 `glfwSwapBuffers` 的双缓冲交换机制的效果就可以用RBO离屏渲染来实现(不过两者并没有什么关系) 生成RBO的操作与FBO类似: ```cpp unsigned int rbo; glGenRenderbuffers(1, &rbo); glBindRenderbuffer(GL_RENDERBUFFER, rbo); ``` 我们可以用函数`void glRenderbufferStorage(GLenum target, GLenum internalformat, GLsizei width, GLsizei height)` 来给RBO分配空间 比如创建一个深度和模板渲染缓冲对象可以通过调用该函数来完成,内格式为**GL_DEPTH24_STENCIL8**: ```cpp glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH24_STENCIL8, 800, 600); ``` 最后,将RBO附加到FBO上需要用以下函数: ```cpp void glFramebufferRenderbuffer( GLenum target, GLenum attachment, GLenum renderbuffertarget, GLuint renderbuffer); // OpenGL4.5版本后支持 void glNamedFramebufferRenderbuffer( GLuint framebuffer, GLenum attachment, GLenum renderbuffertarget, GLuint renderbuffer); ``` 比如: ```cpp glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_RENDERBUFFER, rbo); ``` 由于RBO通常都是只写的,它们会**经常用于深度和模板附件**,因为大部分时间我们都不需要从深度和模板缓冲中读取值,只关心深度和模板测试。我们需要深度和模板值**用于测试阶段**,而不需要对它们进行**采样**,所以RBO非常适合它们。当我们不需要从这些缓冲中采样的时候,通常都会选择RBO,因为能为你的FBO提供一些优化。 ## 离屏渲染到纹理 接下来我们将场景渲染到一个附加到帧缓冲对象上的颜色纹理中,之后将在一个横跨整个屏幕的四边形上绘制这个纹理。 (场景 —【FBO】—> 纹理 —【四边形】—> 屏幕) 1. 先生成一个FBO,将其绑定 ```cpp unsigned int framebuffer; glGenFramebuffers(1, &framebuffer); glBindFramebuffer(GL_FRAMEBUFFER, framebuffer); ``` 2. 生成一个纹理图像,附加到FBO的**颜色附件**上 ```cpp // 生成纹理 unsigned int 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); // 将它附加到当前绑定的帧缓冲对象 glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, texColorBuffer, 0); ``` 3. 添加深度和模板附件,这里不需要读深度模板,所以用的是RBO ```cpp unsigned int rbo; glGenRenderbuffers(1, &rbo); glBindRenderbuffer(GL_RENDERBUFFER, rbo); glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH24_STENCIL8, 800, 600); glBindRenderbuffer(GL_RENDERBUFFER, 0); glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_RENDERBUFFER, rbo); ``` 4. 检查帧缓冲是否是完整的 ```cpp if(glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) std::cout << "ERROR::FRAMEBUFFER:: Framebuffer is not complete!" << std::endl; // 记得要解绑帧缓冲 glBindFramebuffer(GL_FRAMEBUFFER, 0); ``` 5. 绑定自定义的帧缓冲,开始渲染场景 ```cpp glBindFramebuffer(GL_FRAMEBUFFER, framebuffer); renderScene(); ``` 6. 绑定回默认帧缓冲,将其颜色附件作为纹理绘制在主屏幕上: ```cpp glBindFramebuffer(GL_FRAMEBUFFER, 0); glBindTextureUnit(0, texColorBuffer); drawQuad(); ``` 其中绘制主屏幕的着色器: ```glsl #version 450 core layout (location = 0) in vec2 aPos; layout (location = 1) in vec2 aTexCoords; out vec2 screenPosition; void main() { screenPosition = aTexCoords; gl_Position = vec4(aPos.x, aPos.y, 0.0, 1.0); } ``` ```glsl #version 450 core layout(binding=0) uniform sampler2D sceneColor; in vec2 screenPosition; void main() { vec3 color = texture(sceneColor, screenPosition).rgb * exposure; outColor = vec4(color, 1.0); } ``` 最后修改:2022 年 03 月 08 日 09 : 52 PM © 允许规范转载