Frame Buffer Object
帧缓冲对象:FBO。默认情况下,我们在GLSurfaceView中绘制的结果是显示到屏幕上,然而实际中有很多情况并不需要渲染到屏幕上,这个时候使用FBO就可以很方便的实现这类需求。FBO可以让我们的渲染不渲染到屏幕上,而是渲染到离屏Buffer中先将图像画到 FBO,添加滤镜之后,然后展示到屏幕上去。
在上节课中我们创建了一个ScreenFilter类用来封装将摄像头数据显示当屏幕上,然而我们需要在显示之前增加各种**"效果",如果我们只存在一个ScreenFilter,那么所有的"效果"**都会积压在这个类中,同时也需要大量的if/else来判断是否开启效果。
我们可以将每种效果写到单独的一个Filter中去,并且在ScreenFilter之前的所有Filter都不需要显示到屏幕中,所以在ScreenFilter之前都将其使用FBO进行缓存。
需要注意的是: 摄像头画面经过FBO的缓存时候,我们再从FBO绘制到屏幕,这时候就不需要再使用
samplerExternalOES
与变换矩阵了。这意味着ScreenFilter
,使用的采样器就是正常的sampler2D
,也不需要#extension GL_OES_EGL_image_external : require
。然而在最原始的状态下是没有开启任何效果的,所以ScreenFilter就比较尴尬。
1、开启效果: 使用
sampler2D
2、未开启效果: 使用
samplerExternalOES
那么就需要在
ScreenFilter
中使用if else
来进行判断,但这个判断稍显麻烦,所以这里我选择使用:
从摄像头使用的纹理首先绘制到CameraFilter
的FBO中, 这样无论是否开启效果ScreenFilter
都是以sampler2D
来进行采样。
创建 CameraFiliter:以纹理坐标为基准。
在onReady中创建 FBO, 以及创建FBO的纹理 mFrameBufferTextur
//fbo的创建 (缓存)
//1、创建fbo (离屏屏幕)
mFrameBuffers = new int[1];
// 1、创建几个fbo 2、保存fbo id的数据 3、从这个数组的第几个开始保存
GLES20.glGenFramebuffers(mFrameBuffers.length,mFrameBuffers,0);
//2、创建属于fbo的纹理
mFrameBufferTextures = new int[1]; //用来记录纹理id
//创建纹理,在OpenGLUtils里进行配置。
OpenGLUtils.glGenTextures(mFrameBufferTextures);
复制代码
对比之前的Render中不用配置:
@Override
public void onSurfaceCreated(GL10 gl, EGLConfig config) {
//初始化操作
mCameraHelper = new CameraHelper(Camera.CameraInfo.CAMERA_FACING_BACK);
//准备好画布
mTextures = new int[1];
//这里创建了纹理,直接应用了,没有配置。
GLES20.glGenTextures(mTextures.length, mTextures, 0);
。。。。
}
复制代码
配置纹理:设置纹理参数(一个四个参数)
//创建并配置纹理
public static void glGenTextures(int[] textures){
//创建
GLES20.glGenTextures(textures.length, textures, 0);
//配置
for (int i = 0; i < textures.length; i++) {
// opengl的操作 面向过程的操作
//bind 就是绑定 ,表示后续的操作就是在这一个 纹理上进行
// 后面的代码配置纹理,就是配置bind的这个纹理
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D,textures[i]);
/**
* 过滤参数
* 当纹理被使用到一个比他大 或者比他小的形状上的时候 该如何处理
*/
// 放大
// GLES20.GL_LINEAR : 使用纹理中坐标附近的若干个颜色,通过平均算法 进行放大
// GLES20.GL_NEAREST : 使用纹理坐标最接近的一个颜色作为放大的要绘制的颜色
GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D,GLES20.GL_TEXTURE_MAG_FILTER,
GLES20.GL_NEAREST);
GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D,GLES20.GL_TEXTURE_MIN_FILTER,
GLES20.GL_NEAREST);
/*设置纹理环绕方向*/
//纹理坐标 一般用st表示,其实就是x y
//纹理坐标的范围是0-1。超出这一范围的坐标将被OpenGL根据GL_TEXTURE_WRAP参数的值进行处理
//GL_TEXTURE_WRAP_S, GL_TEXTURE_WRAP_T 分别为x,y方向。
//GL_REPEAT:平铺
//GL_MIRRORED_REPEAT: 纹理坐标是奇数时使用镜像平铺
//GL_CLAMP_TO_EDGE: 坐标超出部分被截取成0、1,边缘拉伸
GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_REPEAT);
GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_REPEAT);
//解绑
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D,0);
}
}
复制代码
绑定FBO跟纹理,创建一个2D的图像。
//绑定fbo纹理
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, mFrameBufferTextures[0]);
//创建一个 2d的图像 目标 2d纹理+等级 + 格式 +宽、高+ 格式 + 数据类型(byte) + 像素数据
GLES20.glTexImage2D(GLES20.GL_TEXTURE_2D,0,GLES20.GL_RGBA,mOutputWidth, mOutputHeight,
0,GLES20.GL_RGBA,GLES20.GL_UNSIGNED_BYTE, null);
// 让fbo与纹理绑定起来,后续的操作就是在操作fbo与这个纹理上了
GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, mFrameBuffers[0]);
GLES20.glFramebufferTexture2D(GLES20.GL_FRAMEBUFFER, GLES20.GL_COLOR_ATTACHMENT0,
GLES20.GL_TEXTURE_2D, mFrameBufferTextures[0], 0);
//解绑
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D,0);
GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER,0);
复制代码
重写父类的 onDrawFrame:因为这里是接近Camera的第一层所以使用GL_TEXTURE_EXTERNAL_OES采样器
@Override
public int onDrawFrame(int textureId) {
//设置显示窗口
GLES20.glViewport(0, 0, mOutputWidth, mOutputHeight);
//不调用的话就是默认的操作glsurfaceview中的纹理了。显示到屏幕上了
//这里我们还只是把它画到fbo中(缓存)
GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER,mFrameBuffers[0]);
//使用着色器
GLES20.glUseProgram(mGLProgramId);
//传递坐标
mGLVertexBuffer.position(0);
GLES20.glVertexAttribPointer(vPosition, 2, GLES20.GL_FLOAT, false, 0, mGLVertexBuffer);
GLES20.glEnableVertexAttribArray(vPosition);
mGLTextureBuffer.position(0);
GLES20.glVertexAttribPointer(vCoord, 2, GLES20.GL_FLOAT, false, 0, mGLTextureBuffer);
GLES20.glEnableVertexAttribArray(vCoord);
//变换矩阵
GLES20.glUniformMatrix4fv(vMatrix,1,false, matrix,0);
GLES20.glActiveTexture(GLES20.GL_TEXTURE0);
//因为这一层是摄像头后的第一层,所以需要使用扩展的 GL_TEXTURE_EXTERNAL_OES
GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, textureId);
GLES20.glUniform1i(vTexture, 0);
//参数传完了 通知opengl 画画 从第0点开始 共4个点
GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4);
//解绑
GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES,0);
GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER,0);
//返回fbo的纹理id
return mFrameBufferTextures[0];
}
复制代码
在Render的onSurfaceCreate中创建Filter,因为需要在GLThread中使用
/**
* 创建好渲染器
*/
@Override
public void onSurfaceCreated(GL10 gl, EGLConfig config) {
。。。。
//注意,必须在Gl线程中创建文件
mCameraFiliter = new CameraFilter(mDouyinView.getContext());
mScreenFiliter = new ScreenFiliter(mDouyinView.getContext());
。。。。
}
复制代码
现在只有 CameraFilter需要变换矩阵了
//进行画画
mCameraFiliter.setMatrix(mtx);
复制代码
同样需要做调整的是 Shader,Camera对应的Vertex、Fragment中Vertex包含Matrix,需要处理矩阵变换。而其它的直接从CameraFilter中的FBO采样,所以不用Matrix。
CameraFilter的 Vertex Shader:
attribute vec4 vPosition; //把顶点坐标给这个变量, 确定要画画的形状
attribute vec4 vCoord;//接受纹理坐标,接受采样器采样图片的坐标, 这个在顶点着色器没有用,传给片元着色器
uniform mat4 vMatrix; //变换矩阵, 需要将原本的 vCood(01, 11, 00, 10)与矩阵相乘才能够得到surfacetexture的采样坐标
varying vec2 aCoord;//传给片云着色器 像素点()
void main() {
//gl_Position 为内置变量,我们把订点数据给这个变量,opengl就知道它要画什么形状了。
gl_Position = vPosition;
aCoord = (vMatrix * vCoord).xy;
//aCoord = vec2((vMatrix * vCoord).x,(vMatrix * vCoord).y);
}
复制代码
CameraFilter的 Fragment Shader:
#extension GL_OES_EGL_image_external : require
//SurfaceTexture比较特殊
//float数据是什么精度的
precision mediump float;
//采样点的坐标
varying vec2 aCoord;
//采样器
uniform samplerExternalOES vTexture;
void main(){
//变量 接收像素值
// texture2D:采样器 采集 aCoord的像素
//赋值给 gl_FragColor 就可以了
gl_FragColor = texture2D(vTexture,aCoord);
}
复制代码
其它的Filter的Vertex Shader:
// 把顶点坐标给这个变量, 确定要画画的形状
attribute vec4 vPosition;
//接收纹理坐标,接收采样器采样图片的坐标
//不用和矩阵相乘了,接收一个点只要2个float就可以了,所以写成了vec2,而不是上节课的vec4
attribute vec2 vCoord;
//传给片元着色器 像素点
varying vec2 aCoord;
void main(){
//内置变量 gl_Position ,我们把顶点数据赋值给这个变量 opengl就知道它要画什么形状了
gl_Position = vPosition;
// 进过测试 和设备有关(有些设备直接就采集不到图像,有些呢则会镜像)
aCoord = vCoord;
}
复制代码
其它Filter的Fragment Shader:
//SurfaceTexture比较特殊
//float数据是什么精度的
precision mediump float;
//采样点的坐标
varying vec2 aCoord;
//采样器 不是从android的surfaceTexure中的纹理 采数据了,所以不再需要android的扩展纹理采样器了
//使用正常的 sampler2D
uniform sampler2D vTexture;
void main(){
//变量 接收像素值
// texture2D:采样器 采集 aCoord的像素
//赋值给 gl_FragColor 就可以了
gl_FragColor = texture2D(vTexture,aCoord);
}
复制代码
这一章节主要采用以上图片中的结构处理分开了Filter, 第一层为CameraFilter,把图片画到FBO上去,最后一层是ScreenFilter,渲染到屏幕上去。两者之前添加各种滤镜。ScreenFilter从FBO上采集数据,不需要扩展的SampleExternalOES来采集,而是直接使用Sample2D采集
假如没有一层CameraFilter的时候,需要添加效果的时候其它滤镜在FBO上处理,ScreenFilter从FBO中采集,这时候不需要扩展采集。而当没有开启效果滤镜的时候,就没有在FBO处理数据直接从Camera的SurfaceTexture中采集,则需要用扩展的SampleExternalOES来采集。这样对于ScreenFilter比较复杂,所以抽出来一层CameraFilter来处理好逻辑,这样ScreenFilter只需要从FBO中采集了,不再需要SampleExternalOES了。