OpenGL ES 帧缓冲对象介绍和使用示例
一、介绍
1. 帧缓冲对象
默认情况下,OpenGL渲染的目标是屏幕,但如果你不想直接渲染到屏幕上,还需要对渲染结果做某些后期处理、渲染到纹理、阴影映射等操作,便可以使用帧缓冲对象,实现离屏渲染。
帧缓冲对象(Frame Buffer Object,FBO)是一个概念容器,它可以包含颜色缓冲区、深度缓冲区、模板缓冲区等,形成一个完整的渲染目标。
通过使用帧缓冲对象,可以实现离屏渲染、多重渲染目标(MRT)等高级渲染技术,而不必直接渲染到屏幕。
2. 相关概念
以下是帧缓冲对象的一些基本概念:
- 颜色缓冲区(Color Buffer):存储渲染的颜色信息。一个帧缓冲对象可以包含多个颜色缓冲区,用于实现多重渲染目标。
- 深度缓冲区(Depth Buffer): 存储每个像素的深度信息,用于实现深度测试,确保正确的渲染顺序。
- 模板缓冲区(Stencil Buffer): 存储模板测试的结果,用于实现各种复杂的渲染效果。
- 渲染缓冲区(Renderbuffer): 实际上是OpenGL ES 3.0中用于存储颜色、深度和模板信息的内存对象。它可以附加到帧缓冲对象的不同附着点。
3. 渲染缓冲对象
渲染缓冲对象(Render Buffer Object,RBO)是一种特殊类型的对象,用于存储图形渲染过程中的像素数据,优点是它的存储格式是优化过的,性能更好,适合渲染而不是读取,所以不可以直接读取。
用途一般是作为帧缓冲对象的附着点,提供一个高效的存储区域,用于存储渲染结果。 渲染缓冲对象通常用于只写入不读取的场景,而纹理可以作为可读取的附着点。于是渲染缓冲对象经常作为深度和模板附件来使用,因为大多数时候并不需要从深度和模板缓冲中读取数据。
4. 关系图
图源:OPENGL ES 3.0 编程指南 原书第2版
二、使用
帧缓冲对象的使用通常包括以下步骤:
- 创建帧缓冲对象: 使用
glGenFramebuffers
函数创建一个帧缓冲对象,其值是一个非零的无符号整数。 - 绑定帧缓冲对象: 使用
glBindFramebuffer
函数将帧缓冲对象绑定为当前渲染目标。 - 创建和附着附加点: 创建颜色缓冲区和深度缓冲区,然后将它们附着到帧缓冲对象的附着点上。
- 渲染: 执行渲染操作,渲染结果将存储在帧缓冲对象中。
- 解绑帧缓冲对象: 使用
glBindFramebuffer(GL_FRAMEBUFFER, 0)
将帧缓冲对象解绑,将渲染目标切换回屏幕,数字0即表示屏幕。 - 删除帧缓冲对象: 使用
glDeleteFramebuffers
函数创建一个帧缓冲对象。
代码示例:
#include <GLES3/gl3.h>
// 定义帧缓冲对象和渲染缓冲对象的标识符
GLuint framebuffer;
GLuint renderbuffer;
// 定义纹理对象的标识符
GLuint texture;
// 图像的宽度和高度
int screenWidth = 1080;
int screenHeight = 1440;
void init()
{
// 创建并绑定帧缓冲对象
glGenFramebuffers(1, &framebuffer);
glBindFramebuffer(GL_FRAMEBUFFER, framebuffer);
// 创建并绑定渲染缓冲对象用于深度和模板信息
glGenRenderbuffers(1, &renderbuffer);
glBindRenderbuffer(GL_RENDERBUFFER, renderbuffer);
// 分配存储空间
glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH24_STENCIL8, screenWidth, screenHeight);
// 将渲染缓冲对象附着到帧缓冲的深度和模板附着点
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, renderbuffer);
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_STENCIL_ATTACHMENT, GL_RENDERBUFFER, renderbuffer);
// 创建并绑定纹理对象用于颜色附着点
glGenTextures(1, &texture);
glBindTexture(GL_TEXTURE_2D, texture);
// 分配存储空间
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, screenWidth, screenHeight, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
// 设置纹理参数
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);
// 检查帧缓冲完整性
if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) {
// 处理错误
// ...
}
// 解绑帧缓冲对象
glBindFramebuffer(GL_FRAMEBUFFER, 0);
}
void render()
{
// 绑定帧缓冲对象
glBindFramebuffer(GL_FRAMEBUFFER, framebuffer);
// 渲染操作,将渲染结果存储在帧缓冲对象中
// 解绑帧缓冲对象
glBindFramebuffer(GL_FRAMEBUFFER, 0);
// 使用帧缓冲对象的渲染结果进行后期处理或直接显示在屏幕上
}
三、应用实例
1. 保存纹理数据
利用FBO绑定纹理到颜色附着上,便可以方便的把纹理数据读取出来了。
代码示例:
void textureToBuffer(int textureId, int x, int y, int width, int height, unsigned char *buffer) {
// 创建FBO
GLuint fbo[1];
glGenFramebuffers(1, fbo);
// 绑定
glBindFramebuffer(GL_FRAMEBUFFER, fbo[0]);
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, textureId, 0);
// 读取数据
glReadPixels(x, y, width, height, GL_RGBA, GL_UNSIGNED_BYTE, buffer); // 这里的format和type需要和纹理一致
// 解绑和释放
glBindFramebuffer(GL_FRAMEBUFFER, 0); // unbind
glDeleteFramebuffers(1, fbo);
}
2. 渲染结果还需后处理
当需要多次渲染(渲染结果还需后处理)才能完成的任务时,此时无需次次都渲染到屏幕上,而是可以选择进行一次或多次离屏渲染,最终再渲染到屏幕上。
下面看一个图像虚化的例子来感受一下。(关于虚化方法这里不做过多解释,可以查看其他文章,如:opencv图像卷积操作和常用的图像滤波函数)
方案一:单次滤波完成虚化
使用 BoxFilter 的 fragment shader
文件如下:
#version 300 es
precision mediump float;
layout (location = 2) uniform sampler2D s_texture;
layout (location = 3) uniform int u_kernelSize;
in vec2 v_texCoor;
out vec4 fragColor
vec4 boxFilter() {
if (u_kernelSize <= 1) {
return texture(s_texture, v_texCoor);
}
ivec2 texSize = textureSize(s_texture, 0);
float xStep = 1.0 / float(texSize.x);
float yStep = 1.0 / float(texSize.y);
vec4 sum = vec4(0.0);
int num = 0;
// 复杂度:N^2
for (int i = -u_kernelSize; i <= u_kernelSize; i++) {
for (int j = -u_kernelSize; j <= u_kernelSize; j++) {
float x = v_texCoor.x + float(i) * xStep;
float y = v_texCoor.y + float(j) * yStep;
sum += texture(s_texture, vec2(x, y));
num++;
}
}
return sum / float(num);
}
void main() {
fragColor = boxFilter();
}
我们通过对周围像素采样,并求平均值的方式获取当前点虚化后的值。由于实现上是嵌套的两层for循环,随着 u_kernelSize
的增大,其耗时将急剧增大。
方案二:横纵方向两次滤波完成虚化
之前我们是对 N*N 的区域进行采样并求平均值,其时间复杂度为 N^2。但如果我们先对横向进行一次采样求平均值,再对输出的结再进行一次纵向的采样求平均值,这样只需要 N*2 即可以达到相同的效果。在高通6225机器上测试 KernelSize = 7 时,耗时从44ms降低到9ms。
优化后的 fragment shader
文件如下:
#version 300 es
precision mediump float;
layout (location = 2) uniform sampler2D s_texture;
layout (location = 3) uniform int u_kernelSize;
layout (location = 4) uniform int u_boxFilterType;
in vec2 v_texCoor;
out vec4 fragColor
vec4 boxFilterHorizontal() {
if (u_kernelSize <= 1) {
return texture(s_texture, v_texCoor);
}
ivec2 texSize = textureSize(s_texture, 0);
float xStep = 1.0 / float(texSize.x);
vec4 sum = vec4(0.0);
int num = 0;
// 复杂度:N
for (int i = -u_kernelSize; i <= u_kernelSize; i++) {
float x = v_texCoor.x + float(i) * xStep;
sum += texture(s_texture, vec2(x, v_texCoor.y));
num++;
}
return sum / float(num);
}
vec4 boxFilterVertical() {
if (u_kernelSize <= 1) {
return texture(s_texture, v_texCoor);
}
ivec2 texSize = textureSize(s_texture, 0);
float yStep = 1.0 / float(texSize.y);
vec4 sum = vec4(0.0);
int num = 0;
// 复杂度:N
for (int i = -u_kernelSize; i <= u_kernelSize; i++) {
float y = v_texCoor.y + float(i) * yStep;
if (y < 0.0 || y > 1.0) {
continue;
}
sum += texture(s_texture, vec2(v_texCoor.x, y));
num++;
}
return sum / float(num);
}
void main() {
if (u_boxFilterType == 1) {
fragColor = boxFilter();
} else if (u_boxFilterType == 2) {
fragColor = boxFilterHorizontal();
} else if (u_boxFilterType == 3){
fragColor = boxFilterVertical();
} else {
fragColor = texture(s_texture, v_texCoor); // origin
}
}
如上我们通过指定 u_boxFilterType
的值来实现使用不同的函数来完成滤波(当然你也可以写几个不同的 fragment shader
来实现)。
部分调用代码如下(完整代码链接在文章末尾):
public void drawBitmapUseFBO() {
if (mGLProgramBlur <= 0) {
Log.e(TAG, "mGLProgram not create!");
return;
}
GLES30.glFinish();
long startTime = System.currentTimeMillis();
GLES30.glUseProgram(mGLProgramBlur); // 指定使用的program
GLES30.glEnable(GLES30.GL_CULL_FACE); // 启动剔除
// init vertex
GLES30.glEnableVertexAttribArray(0);
GLES30.glEnableVertexAttribArray(1);
GLES30.glVertexAttribPointer(0, VERTEX_SIZE, GLES30.GL_FLOAT, false, VERTEX_STRIDE, mVertexBuffer);
GLES30.glVertexAttribPointer(1, VERTEX_SIZE, GLES30.GL_FLOAT, false, VERTEX_STRIDE, mTextureBitmapBuffer);
// 先进行boxFilterHorizontal,渲染到FBO上,绑定颜色附着的纹理
GLES30.glActiveTexture(GLES30.GL_TEXTURE0);
GLES30.glBindTexture(GLES30.GL_TEXTURE_2D, mTextures[0]);
GLES30.glUniform1i(2, 0);
GLES30.glUniform1i(3, mKernelSize); // set u_kernelSize
GLES30.glUniform1i(4, 2); // set u_boxFilterType to boxFilterHorizontal
GLES30.glBindFramebuffer(GLES30.GL_FRAMEBUFFER, mFBO[0]);
GLES30.glFramebufferTexture2D(GLES30.GL_FRAMEBUFFER, GLES30.GL_COLOR_ATTACHMENT0, GLES30.GL_TEXTURE_2D, mTextures[1], 0);
GLES30.glDrawElements(GLES30.GL_TRIANGLE_FAN, VERTEX_ORDER.length, GLES30.GL_UNSIGNED_BYTE, mDrawListBuffer);
// 再进行boxFilterVertical,渲染到屏幕
GLES30.glBindFramebuffer(GLES30.GL_FRAMEBUFFER, 0); // 解绑,即重新绑定回屏幕
// GLES30.glVertexAttribPointer(0, VERTEX_SIZE, GLES30.GL_FLOAT, false, VERTEX_STRIDE, mVertexBuffer);
// 这次不需要再对纹理进行上下翻转了,重新设置下纹理坐标的值
GLES30.glVertexAttribPointer(1, VERTEX_SIZE, GLES30.GL_FLOAT, false, VERTEX_STRIDE, mTextureRotation0Buffer);
GLES30.glActiveTexture(GLES30.GL_TEXTURE1);
GLES30.glBindTexture(GLES30.GL_TEXTURE_2D, mTextures[1]);
GLES30.glUniform1i(2, 1);
GLES30.glUniform1i(3, mKernelSize); // set u_kernelSize
GLES30.glUniform1i(4, 3); // set u_boxFilterType to boxFilterVertical
GLES30.glDrawElements(GLES30.GL_TRIANGLE_FAN, VERTEX_ORDER.length, GLES30.GL_UNSIGNED_BYTE, mDrawListBuffer);
GLES30.glDisableVertexAttribArray(0);
GLES30.glDisableVertexAttribArray(1);
GLES30.glFinish();
long endTime = System.currentTimeMillis();
Log.i(TAG, "drawBitmapUseFBO time(ms): " + (endTime - startTime));
}
先进行 boxFilterHorizontal
,渲染到FBO上,再进行 boxFilterVertical
,渲染到屏幕。
3. 多重渲染目标(MRT)
通常情况下,片段着色器只有一个输出颜色。如果想要同时输出多个颜色,则可以使用多重渲染目标来实现,通过多重渲染目标,片段着色器输出多个颜色(用于保存RGBA颜色、法线、深度或纹理坐标)。MRT 用于多种高级渲染算法中,例如延迟着色和快速环境遮蔽估算(SSAO)。
使用示例:
fragment shader
中指定多个输出颜色
#version 300 es
precision mediump float;
layout (location = 2) uniform sampler2D s_texture;
in vec2 v_texCoor;
out vec4 fragColor1; // 输出到第一个颜色附着点
out vec4 fragColor2; // 输出到第二个颜色附着点
out vec4 fragColor3; // 输出到第三个颜色附着点
void main() {
vec4 color = texture(s_texture, v_texCoor); // origin
fragColor1 = vec4(1.0) - color; // inverted
fragColor2 = mix(color, vec4(1.0, 0.0, 0.0, 1.0), 0.5); // mix with color red
fragColor3 = mix(color, vec4(0.0, 0.0, 1.0, 1.0), 0.5); // mix with color blur
}
部分调用代码如下(完整代码链接在文章末尾):
public void drawBitmapUseMRT(int targetIndex) {
if (mGlProgramMRT <= 0) {
Log.e(TAG, "mGLProgram not create!");
return;
}
GLES30.glFinish();
long startTime = System.currentTimeMillis();
Log.d(TAG, "drawBitmapUseMRT mGLProgram: " + mGlProgramMRT);
GLES30.glUseProgram(mGlProgramMRT); // 指定使用的program
GLES30.glEnable(GLES30.GL_CULL_FACE); // 启动剔除
// init vertex
GLES30.glEnableVertexAttribArray(0);
GLES30.glEnableVertexAttribArray(1);
GLES30.glVertexAttribPointer(0, VERTEX_SIZE, GLES30.GL_FLOAT, false, VERTEX_STRIDE, mVertexBuffer);
GLES30.glVertexAttribPointer(1, VERTEX_SIZE, GLES30.GL_FLOAT, false, VERTEX_STRIDE, mTextureBitmapBuffer);
// bind texture
GLES30.glActiveTexture(GLES30.GL_TEXTURE0);
GLES30.glBindTexture(GLES30.GL_TEXTURE_2D, mTextures[0]);
GLES30.glUniform1i(2, 0);
// bind FBO and color attachment
GLES30.glBindFramebuffer(GLES30.GL_FRAMEBUFFER, mFBO[0]);
GLES30.glFramebufferTexture2D(GLES30.GL_FRAMEBUFFER, GLES30.GL_COLOR_ATTACHMENT0, GLES30.GL_TEXTURE_2D, mTextures[1], 0);
GLES30.glFramebufferTexture2D(GLES30.GL_FRAMEBUFFER, GLES30.GL_COLOR_ATTACHMENT1, GLES30.GL_TEXTURE_2D, mTextures[2], 0);
GLES30.glFramebufferTexture2D(GLES30.GL_FRAMEBUFFER, GLES30.GL_COLOR_ATTACHMENT2, GLES30.GL_TEXTURE_2D, mTextures[3], 0);
// 假设有两个颜色附着点,分别对应 GL_COLOR_ATTACHMENT0 和 GL_COLOR_ATTACHMENT1
int drawBuffers[] = { GLES30.GL_COLOR_ATTACHMENT0, GLES30.GL_COLOR_ATTACHMENT1, GLES30.GL_COLOR_ATTACHMENT2 };
// 将颜色附着点指定给帧缓冲对象
GLES30.glDrawBuffers(drawBuffers.length, drawBuffers, 0);
GLES30.glDrawElements(GLES30.GL_TRIANGLE_FAN, VERTEX_ORDER.length, GLES30.GL_UNSIGNED_BYTE, mDrawListBuffer);
GLES30.glFinish();
long endTime = System.currentTimeMillis();
Log.i(TAG, "drawBitmapUseMRT time(ms): " + (endTime - startTime));
}
四、示例代码地址
https://github.com/afei-cn/OpenGLSample/tree/master/fbodemo
其中shader代码在 fbodemo/src/main/assets 文件夹下,调用代码在 fbodemo/src/main/java/com/afei/fbodemo/JavaDrawer.java 中。