OpenGL ES短视频开发(FBO)

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来进行判断,但这个判断稍显麻烦,所以这里我选择使用:

从摄像头使用的纹理首先绘制到CameraFilterFBO中, 这样无论是否开启效果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了。

分类:
Android
标签: