之前的博客我们所做的示例都是直接渲染到屏幕上的,如果我们并不需要渲染到屏幕上,也就是离屏渲染,该怎么做呢?FBO离屏渲染是一个很好的选择。在这篇博客中,我们将以渲染摄像头数据为例,使用FBO进行离屏渲染。
关于FBO离屏渲染
所谓的FBO就是Frame Buffer Object。之前我们使用OpenGLES渲染,都是直接渲染到屏幕上,FBO可以让我们的渲染不渲染到屏幕上,而是渲染到离屏Buffer中。这样的作用是什么呢?比如我们需要处理一张图片,在上传时增加时间的水印,这个时候不需要显示出来的。再比如我们需要对摄像头采集的数据,一个彩色原大小的显示出来,一个黑白的长宽各一半录制成视频。
像这些情况,我们就可以使用到FBO离屏渲染技术了,当然FBO并不是仅仅局限于此。
FBO渲染步骤
如果这时候,你正在寻求关于离屏渲染的帮助,并且看到了这篇博客,那么我想你应该是能够把图像直接渲染到屏幕上的。不能的话可以Google、百度或者直接看看前面的博客。
图像直接渲染到屏幕上的步骤:
- 编写Shader。(检查支持、权限什么的就不再提了)
- 创建GL环境,直接使用GLSurfaceView,GLSurfaceView内部实现了创建GL环境。
- GL环境创建后,编译Shader,创建GL Program。获取可用Texture,设置渲染参数。(onSurfaceCreated中)
- 设置ViewPort。(onSurfaceChanged中)
- 清屏(onDrawFrame中)
- 启用必要的属性,useProgram,绑定纹理,传入参数(顶点坐标、纹理坐标、变换矩阵等)。(onDrawFrame中)
- 绘制。(onDrawFrame中)
- 下一帧数据,requestRender,再一次从第5步开始执行。
FBO离屏渲染我们需要改动的地方为:
- 获取可用的Texture,不再只获取一个,针对我们假设的需求可以获取两个。一个是作为数据源的texture,另外一个是用来作为输出图像的texture,这时候这个texture相当于是一块还没画东西的画布。获取一个可用的FrameBuffer,方法名和获取可用Texture类似,为glGenFrameBuffers。
- 绘制前先绑定FrameBuffer、RenderBuffer、Texture,并将RenderBuffer和Texture挂载到FrameBuffer上。
这只是个大概的说法,并不太准确。具体代码可参看Demo。
纹理(Texture)的使用
纹理在之前的图片处理、Camera预览中就使用到了,一直都是默默的用,没详细说明,在这里补充一下。纹理的使用通常为:在GL线程创建成功后,在GL线程中生成纹理,并设置纹理参数,然后在渲染时启用纹理,绑定纹理,并将纹理传入Shader(告诉Shader,采样器是哪个)。
生成纹理
OpenGL生成纹理,其实是从未被使用的“纹理堆”(姑且这样理解吧)中获取指定个数的纹理,这些纹理不一定是连续的。生成纹理的方法为GLES20.glGenTextures(int size,int[] textures,int start)
,其C函数为void glGenTextures(GLsizei n,GLuint * textures);
。Android版的第一个参数为需要的纹理数,第二个参数为存储获得的纹理ID的数组,第三个参数为数组的起始位置。
gl的命名很有规律,类似于生成纹理的还有生成FrameBuffer的GLES20.glGenFrameBuffers
、生成RenderBuffer的GLES20.glGenRenderBuffers
等待。其他的方法基本也可以依次类推。
纹理过滤参数设置
glTexParameteri和glTexParameterf为纹理过滤设置函数,设置纹理参数前需要先绑定纹理,让GPU明白需要设置的是哪个纹理,绑定纹理的方法为GLES20.glBindTexture(int target,int texture)
使用示例如下:
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, texture[0]);
//设置缩小过滤为使用纹理中坐标最接近的一个像素的颜色作为需要绘制的像素颜色
GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MIN_FILTER,GLES20.GL_NEAREST);
//设置放大过滤为使用纹理中坐标最接近的若干个颜色,通过加权平均算法得到需要绘制的像素颜色
GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MAG_FILTER,GLES20.GL_LINEAR);
//设置环绕方向S,截取纹理坐标到[1/2n,1-1/2n]。将导致永远不会与border融合
GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_S,GLES20.GL_CLAMP_TO_EDGE);
//设置环绕方向T,截取纹理坐标到[1/2n,1-1/2n]。将导致永远不会与border融合
GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_T,GLES20.GL_CLAMP_TO_EDGE);
以上为2D纹理GLES20.GL_TEXTURE_2D
的绑定和参数设置,如果是相机,我们通常使用的是扩展纹理GLES11Ext.GL_TEXTURE_EXTERNAL_OES
。详细用法及使用区别参考Demo中的相机预览和图片处理。
使用纹理
我们在使用纹理时,一般需要用GLES20.glActiveTexture
指明启用的纹理单元,说启用其实也不太合适,GLES20.glActiveTexture
并不是激活纹理单元,而是选择当前活跃的纹理单元。默认情况下当前活跃的纹理单元为GLES20.GL_TEXTURE0
。使用示例如下:
//激活纹理单元1
GLES20.glActiveTexture(GLES20.GL_TEXTURE1);
//绑定2D纹理
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D,getTextureId());
//将纹理设置给Shader
GLES20.glUniform1i(mHTexture,1);
Frame Buffer和Render Buffer
Frame Buffer Object(FBO)即为帧缓冲对象,用于离屏渲染缓冲。相对于其它同类技术,如数据拷贝或交换缓冲区等,使用FBO技术会更高效并且更容易实现。而且FBO不受窗口大小限制。FBO可以包含许多颜色缓冲区,可以同时从一个片元着色器写入。FBO是一个容器,自身不能用于渲染,需要与一些可渲染的缓冲区绑定在一起,像纹理或者渲染缓冲区。
Render Buffer Object(RBO)即为渲染缓冲对象,分为color buffer(颜色)、depth buffer(深度)、stencil buffer(模板)。
在使用FBO做离屏渲染时,可以只绑定纹理,也可以只绑定Render Buffer,也可以都绑定或者绑定多个,视使用场景而定。如只是对一个图像做变色处理等,只绑定纹理即可。如果需要往一个图像上增加3D的模型和贴纸,则一定还要绑定depth Render Buffer。
同Texture使用一样,FrameBuffer使用也需要调用GLES20.glGenFrameBuffers
生成FrameBuffer,然后在需要使用的时候调用GLES20.glBindFrameBuffer
。
Frame Buffer只与Texture绑定
FrameBuffer的创建,Texture的创建及参数设置,代码如下:
GLES20.glGenFramebuffers(1, fFrame, 0);
GLES20.glGenTextures(2, textures, start);
for (int i = 0; i < 2; i++) {
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textures[i]);
GLES20.glTexImage2D(GLES20.GL_TEXTURE_2D, 0,gl_format, width, height,
0, gl_format, GLES20.GL_UNSIGNED_BYTE, null);
GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MIN_FILTER,GLES20.GL_NEAREST);
GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MAG_FILTER,GLES20.GL_LINEAR);
GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_S,GLES20.GL_CLAMP_TO_EDGE);
GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_T,GLES20.GL_CLAMP_TO_EDGE);
}
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D,0);
创建完毕后,在渲染时使用:
//绑定FrameBuffer
GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, fFrame[0]);
//为FrameBuffer挂载Texture[1]来存储颜色
GLES20.glFramebufferTexture2D(GLES20.GL_FRAMEBUFFER, GLES20.GL_COLOR_ATTACHMENT0,
GLES20.GL_TEXTURE_2D, textures[1], 0);
//绑定FrameBuffer后的绘制会绘制到textures[1]上了
GLES20.glViewport(0,0,w,h);
mFilter.draw();
//解绑FrameBuffer
GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER,0);
Frame Buffer与Texture、Render Buffer绑定
有时候只绑定纹理并不能满足我们的要求,当我们需要深度的时候,还需要绑定Render Buffer。在绑定纹理的基础上再绑定RenderBuffer其实也就比较简单了。首先使用前还是先生成RenderBuffer,并为RenderBuffer建立数据存储的格式和渲染对象的尺寸:
//生成Render Buffer
GLES20.glGenRenderbuffers(1,fRender,0);
//绑定Render Buffer
GLES20.glBindRenderbuffer(GLES20.GL_RENDERBUFFER,fRender[0]);
//设置为深度的Render Buffer,并传入大小
GLES20.glRenderbufferStorage(GLES20.GL_RENDERBUFFER,GLES20.GL_DEPTH_COMPONENT16,
width, height);
//为FrameBuffer挂载fRender[0]来存储深度
GLES20.glFramebufferRenderbuffer(GLES20.GL_FRAMEBUFFER, GLES20.GL_DEPTH_ATTACHMENT,
GLES20.GL_RENDERBUFFER, fRender[0]);
//解绑Render Buffer
GLES20.glBindRenderbuffer(GLES20.GL_RENDERBUFFER,0);
然后渲染时,增加深度绑定和解绑:
GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, frameBufferId);
GLES20.glFramebufferTexture2D(GLES20.GL_FRAMEBUFFER, GLES20.GL_COLOR_ATTACHMENT0,
GLES20.GL_TEXTURE_2D, textureId, 0);
//为FrameBuffer挂载fRender[0]来存储深度
GLES20.glFramebufferRenderbuffer(GLES20.GL_FRAMEBUFFER, GLES20.GL_DEPTH_ATTACHMENT,
GLES20.GL_RENDERBUFFER, fRender[0]);
GLES20.glViewport(0,0,width,height);
mFilter.draw();
GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER,0);
//解绑Render Buffer
GLES20.glBindRenderbuffer(GLES20.GL_RENDERBUFFER,0);
最后的处理
不管是FrameBuffer、RenderBuffer还是Texture不再使用时,都应该删除掉,删除的方法类似:
//删除Render Buffer
GLES20.glDeleteRenderbuffers(1, fRender, 0);
//删除Frame Buffer
GLES20.glDeleteFramebuffers(1, fFrame, 0);
//删除纹理
GLES20.glDeleteTextures(1, fTexture, 0);
源码
所有的代码全部在一个项目中,托管在Github上——Android OpenGLES 2.0系列博客的Demo
工程内容是将一张图片,使用FBO转为黑白图片并保存的例子。为了方便,直接使用GLSurfaceView来提供GL环境。
欢迎转载,转载请保留文章出处。湖广午王的博客[http://blog.csdn.net/junzia/article/details/53861519]