帧缓冲(Framebuffer Object,FBO)是OpenGL中实现后处理效果的核心技术。我们常用到的颜色缓冲、深度缓冲等都是默认使用帧缓冲,在主循环中实现渲染反映在屏幕上。这里顺便提下,与此对应另一个常用术语「离屏渲染」是什么概念呢?就是如果我们自定义一个帧缓冲对象用来做绘制的载体,在当前的帧缓冲之外渲染以实现一些特殊效果(比如美颜处理、滤镜处理、贴纸等效果),这时候是不会直接显示在主窗口上的,需要通过其他操作将绘制内容显示到屏幕上,这种情况就称做「离屏渲染」。
1. 基本概念
1.1. 帧缓冲
帧缓冲在渲染绘制中,图像最终都是绘制到FBO上的,也就是我们的屏幕。FBO由颜色附件、深度附件、模版附件组成,作为着色器各方面(一般包括颜色、深度、深度值)绘制结果存储的逻辑对象。
1.2 纹理
纹理(Texture) ,通俗讲就像是贴纸一样贴在物体表面,使得物体表面拥有图案。但实际上在OpenGL中,纹理的作用不仅限于此,它可以用作FBO的颜色和深度附件来存储大量的数据。在OpenGL中,我们通常将纹理中的像素将按照纹理坐标进行编址,纹理坐标系是一个空间直角坐标系,横轴为S轴,纵轴为T轴,垂直于屏幕的坐标轴为R轴。在我们的2D纹理中,由于没有R轴,我们也可以将横轴称为U 纵轴称为V轴,也就是我们所说的UV坐标系。但和OpenGL坐标系所不同的是:纹理坐标系的(0,0)点位于纹理的左下角,而(1,1)点位于纹理的右上角。而通过纹理坐标获取像素颜色信息的过程则称为是采样。最典型的例子就是利用纹理存储地形信息了。
1.3 渲染缓冲
渲染缓冲(Renderbuffer Object,RBO),是由应用程序分配的2D图像缓冲区,常用于分配和存储深度和模版值,也可用作FBO的深度或者模版附件。渲染缓冲是在纹理之后引入到OpenGL中,作为一个可用的帧缓冲附件类型的,所以在过去纹理是唯一可用的附件。和纹理图像一样,渲染缓冲对象是一个真正的缓冲,即一系列的字节、整数、像素等。渲染缓冲对象附加的好处是,它会将数据储存为OpenGL原生的渲染格式,它是为离屏渲染到帧缓冲优化过的。
渲染缓冲对象直接将所有的渲染数据储存到它的缓冲中,不会做任何针对纹理格式的转换,让它变为一个更快的可写储存介质。然而,渲染缓冲对象通常都是只写的,所以你不能读取它们(比如使用纹理访问)。当然你仍然还是能够使用glReadPixels来读取它,这会从当前绑定的帧缓冲,而不是附件本身,中返回特定区域的像素。因为它的数据已经是原生的格式了,当写入或者复制它的数据到其它缓冲中时是非常快的。所以,交换缓冲这样的操作在使用渲染缓冲对象时会非常快。我们在每个渲染迭代最后使用的glfwSwapBuffers,也可以通过渲染缓冲对象实现:只需要写入一个渲染缓冲图像,并在最后交换到另外一个渲染缓冲就可以了。
2. 渲染流程
- 创建帧缓冲对象;创建渲染缓冲对象;
- 添加附件,比如颜色附件或者深度附件;
- 切换到帧缓冲渲染,在帧缓冲中进行绘制;此时绘制的内容都是记录在上一步添加的颜色附件或者深度附件上;
- 切换到屏幕的缓冲区,读取帧缓冲中的信息(颜色、深度信息等);
- 绘制到屏幕上;
3. 代码解析渲染步骤
1)创建帧缓冲 和 渲染缓冲
函数原型
GL_API void GL_APIENTRY glGenFramebuffers (GLsizei n, GLuint* framebuffers) OPENGLES_DEPRECATED(ios(3.0, 12.0), tvos(9.0, 12.0));
示例
/**************创建单个**************/
//创建帧缓冲
GLuint _frameBuffer;
glGenFramebuffers(1, &_frameBuffer);
//创建渲染缓冲
glGenRenderbuffers(1, &_renderBuffer);
/**************创建多个**************/
int[] frameBuffers = new int[1];
glGenFramebuffers(1, frameBuffers,0);
2)绑定渲染缓冲
函数原型
GL_API void GL_APIENTRY glBindFramebuffer (GLenum target, GLuint framebuffer) OPENGLES_DEPRECATED(ios(3.0, 12.0), tvos(9.0, 12.0));
GL_API void GL_APIENTRY glRenderbufferStorage (GLenum target, GLenum internalformat, GLsizei width, GLsizei height) OPENGLES_DEPRECATED(ios(3.0, 12.0), tvos(9.0, 12.0));
GL_API void GL_APIENTRY glFramebufferRenderbuffer (GLenum target, GLenum attachment, GLenum renderbuffertarget, GLuint renderbuffer) OPENGLES_DEPRECATED(ios(3.0, 12.0), tvos(9.0, 12.0));
GL_API GLenum GL_APIENTRY glCheckFramebufferStatus (GLenum target) OPENGLES_DEPRECATED(ios(3.0, 12.0), tvos(9.0, 12.0));
绑定帧缓冲之后,我们之后所做的渲染都是在 FBO 缓冲上,如果我们要返回默认缓冲,则使用:glBindFramebuffer(GL_FRAMEBUFFER, 0)。
可以使用GL_READ_FRAMEBUFFER或GL_DRAW_FRAMEBUFFER,将一个帧缓冲分别绑定到读取目标或写入目标。绑定到GL_READ_FRAMEBUFFER的帧缓冲将会使用在所有像是glReadPixels的读取操作中,而绑定到GL_DRAW_FRAMEBUFFER的帧缓冲将会被用作渲染、清除等写入操作的目标。一般情况下都不需要区分它们,通常都会使用GL_FRAMEBUFFER,绑定到两个上。
示例
//绑定当前帧缓冲对象
glBindFramebuffer(GL_FRAMEBUFFER, _frameBuffer);
//绑定当前渲染缓冲对象
glBindRenderbuffer(GL_RENDERBUFFER, _renderBuffer);
//作用将可绘制对象的存储绑定到OpenGL ES renderbuffer对象。
/*
为了创建一个可以呈现给屏幕的renderbuffer,您可以绑定renderbuffer,然后通过调用此方法为其分配共享存储空间。此方法调用是为了替换正常情况下的glRenderbufferStorage方法(对于非iOS平台的其他平台)。其存储已分配了此方法的renderbuffer可以随后通过调用 presentRenderbuffer:来显示renderbuffer的内容。
宽度,高度和内部颜色缓冲区格式是从CAEAGLLayer对象的属性中导出的。在调用此方法之前,可以通过在可绘制对象的drawableProperties字典中添加一个kEAGLDrawablePropertyColorFormat键来覆盖内部颜色缓冲区格式。
要使OpenGL ES renderbuffer与CAEAGLLayer对象分离,请将drawable参数设置为nil即可将两者分离。
*/
if (![_eaglContext renderbufferStorage:GL_RENDERBUFFER fromDrawable:(CAEAGLLayer *)self.layer]) {
printf(this,"_eaglContext renderbufferStorage failed");
}
//初始化渲染数据存储,这里将可视的视图的CAEAGLLayer和用于渲染的EAGLContext绑定,render
//glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH24_STENCIL8, width, height);//设置内部格式为:GL_DEPTH24_STENCIL8 (即 24 位深度 + 8 位模板缓冲)
// 将渲染缓冲添加到FBO上
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, _renderBuffer);
//判断帧缓冲是否完整
if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE)
{
printf(this, "Failed to make complete framebuffer object %x", glCheckFramebufferStatus(GL_FRAMEBUFFER));
}
3) 帧缓冲绑定纹理,读取帧缓冲中的信息初始化纹理数据
函数原型
GL_API void GL_APIENTRY glActiveTexture (GLenum texture) OPENGLES_DEPRECATED(ios(3.0, 12.0), tvos(9.0, 12.0));
//绑定纹理
GL_API void GL_APIENTRY glBindTexture (GLenum target, GLuint texture) OPENGLES_DEPRECATED(ios(3.0, 12.0), tvos(9.0, 12.0));
GL_API void GL_APIENTRY glTexParameterf (GLenum target, GLenum pname, GLfloat param) OPENGLES_DEPRECATED(ios(3.0, 12.0), tvos(9.0, 12.0));
GL_API void GL_APIENTRY glTexParameterfv (GLenum target, GLenum pname, const GLfloat* params) OPENGLES_DEPRECATED(ios(3.0, 12.0), tvos(9.0, 12.0));
GL_API void GL_APIENTRY glTexParameteri (GLenum target, GLenum pname, GLint param) OPENGLES_DEPRECATED(ios(3.0, 12.0), tvos(9.0, 12.0));
GL_API void GL_APIENTRY glTexParameteriv (GLenum target, GLenum pname, const GLint* params) OPENGLES_DEPRECATED(ios(3.0, 12.0), tvos(9.0, 12.0));
创建了缓冲之后,我们需要为缓冲添加内容,现在的它还不完整(Complete),一个完整的帧缓冲需要满足以下的条件:
附加至少一个缓冲(颜色、深度或模板缓冲);
至少有一个颜色附件(Attachment);
所有的附件都必须是完整的(保留了内存);
每个缓冲都应该有相同的样本数;
现在,为帧缓冲添加一个纹理贴图附件,首先创建纹理。通过CVPixelBufferRef转换成一个 openGL texture的示例如下
glActiveTexture(GL_TEXTURE0);
/*创建并初始化纹理数据
使用到的库为CoreVideo.famework 和 VideoToolbox.framework。本文主要对OpenGLES.framework中方法做说明。
这里_videoTextureCache 的代表一个 Texture缓存,每次生产的Texture都是从缓存获取的,这样可以省掉反复创建Texture的开销,_videoTextureCache要提前创建好:
CVOpenGLESTextureCacheRef _videoTextureCache;
CVReturn err = CVOpenGLESTextureCacheCreate(kCFAllocatorDefault, NULL, _eaglContext, NULL, &_videoTextureCache);
其中 _eaglContext 是 openGL的 context,在iOS里就是 EAGLContext *;
*/
CVReturn err = CVOpenGLESTextureCacheCreateTextureFromImage(kCFAllocatorDefault,
_videoTextureCache,
pixelBuffer,
NULL,
GL_TEXTURE_2D,
GL_RED_EXT,
frameWidth,
frameHeight,
GL_RED_EXT,
GL_UNSIGNED_BYTE,
0,
&_lumaTexture);//_lumaTexture还不是openGL的Texture,调用CVOpenGLESTextureGetName才能获得在openGL可以使用的Texture ID。
if (err)
{
printf(this, "Error at CVOpenGLESTextureCacheCreateTextureFromImage %d", err);
}
//绑定纹理
glBindTexture(CVOpenGLESTextureGetTarget(_lumaTexture), CVOpenGLESTextureGetName(_lumaTexture));
//纹理过滤 .图象从纹理图象空间映射到帧缓冲图象空间(映射需要重新构造纹理图像,这样就会造成应用到多边形上的图像失真),用glTexParmeteri()来确定如何把纹理象素映射成像素.
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_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);
如果直接通过OpenGL创建纹理,方法如下
GLuint texture;
glGenTextures(1, &texture);
glBindTexture(GL_TEXTURE_2D, texture);
glTexImage2D(
GL_TEXTURE_2D,
0,
GL_RGB,
WIDTH,
HEIGHT,
0,
GL_RGB,
GL_UNSIGNED_BYTE,
nullptr);//参数设置了 nullptr ,是因为我们要将纹理绑定为帧缓冲的附件,使得我们所渲染的画面成为一个纹理(然后可以将它作为贴图贴到其他地方)。
//绑定纹理
glFramebufferTexture2D(
GL_FRAMEBUFFER,//帧缓冲的目标(绘制、读取或者两者皆有)
GL_COLOR_ATTACHMENT0, //我们想要附加的附件类型。当前我们正在附加一个颜色附件。注意最后的0意味着我们可以附加多个颜色附件。我们将在之后的教程中提到。
GL_TEXTURE_2D, //希望附加的纹理类型
texture, // 要附加的纹理本身
0);//多级渐远纹理的级别。我们将它保留为0。
//添加深度缓冲和模板缓冲
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_DEPTH_COMPONENT, texture, 0);
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_STENCIL_ATTACHMENT, GL_STENCIL_INDEX, texture, 0);
4)切换到帧缓冲
函数原型
GL_API void GL_APIENTRY glBindFramebuffer (GLenum target, GLuint framebuffer) OPENGLES_DEPRECATED(ios(3.0, 12.0), tvos(9.0, 12.0));
如果 framebuffer 参数为 0 ,则代表切换到默认的屏幕上进行绘制。
glBindFramebuffer(GL_FRAMEBUFFER, _frameBuffer);
5) 绘制
OpenGL大部分的绘制命令都是以Draw这个单词开始的。绘制命令大致分为两种:索引形式的绘制和非索引形式绘制。所有看起来更为复杂的OpenGL绘制函数,在本质上都是基于这两个函数来完成功能实现的。
函数原型
GL_API void GL_APIENTRY glDrawArrays (GLenum mode, GLint first, GLsizei count) OPENGLES_DEPRECATED(ios(3.0, 12.0), tvos(9.0, 12.0));
GL_API void GL_APIENTRY glDrawElements (GLenum mode, GLsizei count, GLenum type, const GLvoid* indices) OPENGLES_DEPRECATED(ios(3.0, 12.0), tvos(9.0, 12.0));
非索引形式的绘制
非索引形式的绘制则不需要使用GL_ELEMENT_ARRAY_BUFFER,只需要简单的按照顺序读取顶点数据即可,直接将缓存对象中的顶点属性按照自身的排列顺序来读取顶点的信息,然后使用它们来构建mode指定的图元信息。
glDrawArrays(
GL_TRIANGLE_STRIP, //需要构建的图元类型为GL_TRIANGLE_STRIP
0, //每个启用的数组中的起始位置为0
4);//结束位置为3(=0+4-1),;
glBindRenderbuffer(GL_RENDERBUFFER, _renderBuffer);
//这里将renderbufferStorage绑定的缓冲渲染到屏幕,显示出来图像
[_eaglContext presentRenderbuffer:GL_RENDERBUFFER];
glFlush();
索引形式的绘制
索引形式的绘制需要绑定GL_ELEMENT_ARRAY_BUFFER的缓存对象中存储的索引数组,它可以用来间接的对已经启用的顶点数组进行索引读取顶点的信息,然后使用它们来构建mode指定的图元信息。
// 顶点坐标数组
const GLfixed vers[] = {
F2X(0.25), F2X(0.25), F2X(0.0), // 第1个顶点坐标
F2X(0.75), F2X(0.25), F2X(0.0), // 第2个顶点坐标
F2X(0.25), F2X(0.75), F2X(0.0), // 第3个顶点坐标
F2X(0.75), F2X(0.75), F2X(0.0) // 第4个顶点坐标
};
glVertexPointer(
3, // 表示每一个顶点由3维坐标构成
GL_FIXED, // 顶点坐标数组中的元素是 GL_FIXED 类型
0, // 从顶点坐标数组的第0个元素开始读取数据
vers // 指向顶点坐标数组的指针
);
// 等效替换 glDrawArrays(GL_TRIANGLE_STRIP, 0, 4) ++
/* 索引数组. 此索引数组表示依次是
第0个顶点{F2X(0.25), F2X(0.25), F2X(0.0)},
第1个顶点{F2X(0.75), F2X(0.25), F2X(0.0)},
第2个顶点{F2X(0.25), F2X(0.75), F2X(0.0)},
第3个顶点{F2X(0.75), F2X(0.75), F2X(0.0)} */
const GLubyte indices[] = {0, 1, 2, 3};
glDrawElements(
GL_TRIANGLE_STRIP, // 绘图模式
4, // 依次从索引数组中读取4个顶点来进行绘制
GL_UNSIGNED_BYTE, // 索引数组中存放的元素的类型
indices // 指向索引数组的指针
);