OpenGL ES短视频开发(预览)

2,233 阅读11分钟

本项目基于OpenGL ES完成短视频的录制,添加美颜、贴纸,放大眼睛,实现快速、慢速播放,随意添加滤镜效果等功能。

完整项目代码可前往OpenGL短视频

OpenGL ES知识点

首先,创建C/C++ Surpport的项目,并在清单文件中添加ES20支持:

<uses-feature android:glEsVersion="0x00020000" android:required="true"/>

初步了解一下几个需要用到的类、中间件:

  • GLSurfaceView

    继承至SurfaceView,它内嵌的surface专门负责OpenGL渲染。

    管理Surface与EGL

    允许自定义渲染器(render)。

    让渲染器在独立的线程里运作,和UI线程分离。

    支持按需渲染(on-demand)和连续渲染(continuous)。

  • Render(渲染器接口中的三个方法需要实现)

  • GLThread(GLSurfaceView必须在这个线程里操作,不同于Android其它的View的绘制)

  • EGL:(Embeded Graphics Library)中间层连接OpenGL ES和本地窗口系统的接口,GLSurfaceView已搭建好这个中间件

创建自己的GLSurfaceview

定义一个DouyinView继承 GLSurfaceview,并且自定义一个DouyinRender继承自 Render,把DouyinRender集成到 DouyinView

public class DouyinView extends GLSurfaceView {
    DouyinRender mRender;
    public DouyinView(Context context) {
        this(context, null);
    }

    public DouyinView(Context context, AttributeSet attrs) {
        super(context, attrs);

        setEGLContextClientVersion(2);
        mRender = new DouyinRender(this);
        setRenderer(mRender);
        //设置按需渲染,当我们调用requestRender(); 请求GLThread回调一次onDrawFrame
        setRenderMode(RENDERMODE_WHEN_DIRTY);
    }
    @Override
    public void surfaceDestroyed(SurfaceHolder holder) {
        super.surfaceDestroyed(holder);
        mRender.onSurfaceDestroyed();
    }
}
public class DouyinRender implements GLSurfaceView.Renderer{
    CameraHelper mCameraHelper;
    SurfaceTexture mSurfaceTexture;
    DouyinView mDouyinView;
    int[] mTextures;
    public DouyinRender(DouyinView douyinView){
        this.mDouyinView = douyinView;
    }
    /**
     * 创建好渲染器
     *
     * @param gl
     * @param config
     */
    @Override
    public void onSurfaceCreated(GL10 gl, EGLConfig config) {
        //初始化操作
        mCameraHelper = new CameraHelper(Camera.CameraInfo.CAMERA_FACING_BACK);
        //准备好画布
        mTextures = new int[1];
    }

    @Override
    public void onSurfaceChanged(GL10 gl, int width, int height) {

        //开启预览
        mCameraHelper.startPreview(mSurfaceTexture);
        mCameraFiliter.onReady(width, height);
        mScreenFiliter.onReady(width, height);
    }

    public void onSurfaceDestroyed() {
        mCameraHelper.stopPreview();
    }
}

着色器

OpenGL 是通过着色器来绘制图片,把物体分割成无数个三角形,把各个定点的坐标交给Vertex Shader(顶点着色器)确定图形形状,然后经过光栅化划分成无数个Fragments,然后把纹理交给Fragment Shader(片元着色器)进行上色,最终绘制到Window上去。

通过GLSL语言编写Vertex shader、Fragment shader,实现它们才是画画的重点。着色器(shader)它们都是运行在GPU 上的小程序,GPU并行处理的能力非常好。

  • 顶点着色器(vertex shader)如何处理顶点、法线等数据的小程序
  • 片元着色器(fragment shader)如何处理光、阴影、遮挡、环境等等对物体表面的影响,最终生成一副图像的小程序
如何编写Shader?
  • AS安装插件:GLSL Surpport,高亮显示

  • GLSL:OpenGL 着色语言(OpenGL shading Language)

  • 数据类型

    float

    vec2 2个float类型

    vec4 4个float类型

    sampler2D 2D纹理采样器

  • 修饰符

    1. attribute 属性变量。只能用于顶点着色器中。 一般用该变量来表示一些顶点数据,如:顶点坐标、纹理坐标、颜色等
    2. uniforms 一致变量。在着色器执行期间一致变量的值是不变的。与const常量不同的是,这个值在编译时期是未知的是由着色器外部初始化的。
    3. varying 易变变量。是从顶点着色器传递到片元着色器的数据变量。

编写shader,在资源文件raw下面创建 *.vert, *.frag 文件,后缀名无所谓,主要是给AS中GLSL插件高亮用的

如下的vertex shader文件base_vertex.vert

// 把顶点坐标给这个变量, 确定要画画的形状
attribute vec4 vPosition;
//接收纹理坐标,接收采样器采样图片的坐标
//不用和矩阵相乘了,接收一个点只要2个float就可以了,所以写成了vec2,而不是上节课的vec4
attribute vec2 vCoord;

//传给片元着色器 像素点
varying vec2 aCoord;

void main(){
    //内置变量 gl_Position ,我们把顶点数据赋值给这个变量 opengl就知道它要画什么形状了
    gl_Position = vPosition;
    // 进过测试 和设备有关(有些设备直接就采集不到图像,有些呢则会镜像)
    aCoord = vCoord;
}

Fragment shader文件 base_frag.frag

//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);
}

说明:varying变量将aCoord的值从Vertex shader传给 Fragment Shader; gl_Position, gl_FragColor 是内置变量。

编写Render

渲染需要首页用到Camera预览摄像,然后进行render,通过之前的项目了解到预览的三种方式:

  1. camera + surfaceview
  2. Camera + surfaceTexture + NativeWindow
  3. Camera + surfaceTexture + Opengl

同样,首先创建CameraHelper,在Render的onSurfaceCreate中初始化,在onSurfaceChange中开启渲染。

这里用上面的第三种方式,通过OpenGL 创建纹理 Texture,这里是传入数组。

 /**
     * 创建好渲染器
     */
    @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);
        mSurfaceTexture = new SurfaceTexture(mTextures[0]);
        mSurfaceTexture.setOnFrameAvailableListener(this);
    }

    @Override
    public void onSurfaceChanged(GL10 gl, int width, int height) {
        //开启预览
        mCameraHelper.startPreview(mSurfaceTexture);
    }

这里总体的渲染流程结构如下图,这里不能用ANative_Window, 因为图像被直接显示出来了

设置的接口 mSurfaceTexture.setOnFrameAvailableListener(this); 接口回调拿到有效的Frame的时候通过mDouyinview.requestRender()实现渲染,这样就刚好构成一个循环闭环,按需渲染,节省资源。

//有一个新的有效的图片的时候调用,让它调用onDrawFrame方法,通过GLSurfaceview的 requestRender()
    @Override
    public void onFrameAvailable(SurfaceTexture surfaceTexture) {
        //有数据时调用,省资源省电,然后调用 onDrawFrame,构成循环。
        mDouyinView.requestRender();
   }

渲染核心

这里在Render的onDrawFrame中实现,首先告诉opengl按照RGBA清理屏幕,运行后,就会把你给的颜色绘到屏幕上

 @Override
    public void onDrawFrame(GL10 gl) {
        //清理屏幕, 告诉opengl需要把屏幕清理成什么颜色
        GLES20.glClearColor(0, 0, 0, 0);
        //执行上一个:glClearColor配置的屏幕颜色
        GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT);
        ......    
    }

在这里我们需要把Camera预览的data到这里渲染出来,Camera现在的数据现在都在SurfaceTexture中,我们需要更新纹理,然后从它当中获得数据进行渲染:

 @Override
    public void onDrawFrame(GL10 gl) {
          ...... 
        //把摄像头的数据显示出来
        //更新纹理,然后我们才能使用opengl从surfaceTexture当中获得数据进行渲染
        mSurfaceTexture.updateTexImage(); 
        //SurfaceTexture比较特殊, 采用的是sampleExtension(而不是用的Sample2)
        //获得变换矩阵, 变换矩阵是一个 4 * 4 的矩阵
        mSurfaceTexture.getTransformMatrix(mtx);
        //进行画画
        mCameraFiliter.setMatrix(mtx);
    }

这里为什么要给SurfaceTexture设置一个变换矩阵呢?先看一下OpenGL的坐标系。

OpenGL坐标系(二维)

  1. 世界坐标 定点坐标是按照 世界坐标来确定的。相当于我们画布的坐标,vertex shader绘制的坐标确定形状依赖世界坐标确定的。比如给一个三角形,需要给坐标的三个点。而项目中给的是Camera的预览,是个矩形,所以给的是以下四个点的坐标,构成两个三角形。

  1. 纹理坐标

  1. android屏幕坐标, 这个坐标我们很清楚。现在我们需要画矩形 , 由两个三角形来确定的,画好定点后,把Android屏幕坐标贴图到 世界坐标,需要矩阵变换。

矩阵变换

介绍完这些坐标,SurfaceTexture确定坐标需要一个变换举证,一般的我们采用Sample2D的采样器,而它需要用到的是SamplerExternalOES,如何运用这个变换矩阵呢?我们先设置一个顶点着色器的GLSL, 通过Java把 顶点的vPosition传入4个点的坐标这里对应的是世界坐标{ (-1,-1), (1, -1), (-1, 1), (1, 1) },然后给到 内置变量gl_Position,这样就 opengl就知道它要画什么形状了。

// 把顶点坐标给这个变量, 确定要画画的形状
attribute vec4 vPosition;
void main(){
    //内置变量 gl_Position ,我们把顶点数据赋值给这个变量 opengl就知道它要画什么形状了
    gl_Position = vPosition;
}

再设置一个变量接受采样器采样图片的坐标,正常预览是android 屏幕坐标的4个点(01,11,00,10)。这几个值在顶点着色器是没有用的,通过定义的aCoord传给 Fragment Shader,通过 aCoord = (vMatrix*vCoord).xy;取到aCoord的值,它是片元着色器,只有两个坐标,对应的是像素点。顶点着色器通过光栅化会产生无数个点,每个点(x, y)就对应Fragment Shader的坐标。

//把顶点坐标给这个变量, 确定要画画的形状
attribute vec4 vPosition;
//接收纹理坐标,接收采样器采样图片的坐标
attribute vec4 vCoord;
//变换矩阵,需要将原本的vCoord(01,11,00,10)与矩阵相乘才能够得到surfacetexture
uniform mat4 vMatrix;
//传给片元着色器 像素点
varying vec2 aCoord;

void main(){
    //内置变量 gl_Position ,我们把顶点数据赋值给这个变量 opengl就知道它要画什么形状了
    gl_Position = vPosition;
    // 进过测试 和设备有关(有些设备直接就采集不到图像,有些呢则会镜像)
    aCoord = (vMatrix*vCoord).xy;
}

然后来写对应的 Fragment Shader, 参照以下代码中的注释了解片元的写法。

#extension GL_OES_EGL_image_external:require
//SurfaceTexture比较特殊
//float数据是什么精度的
precision mediump float;
//采样点的坐标
varying vec2 aCoord;
//采样器 android的surfaceTexure中的纹理 采数据需要android的扩展纹理采样器了
uniform samplerExternalOES vTexture;

void main(){
    //变量 接收像素值
    // texture2D:采样器 采集 aCoord的像素
    //赋值给 gl_FragColor 就可以了
    gl_FragColor = texture2D(vTexture,aCoord);
}

写了Vertex, Fragment Shader之后,OpenGL其实就可以实现将我们采集的摄像头的图像Render到我们的屏幕上去了,现在唯一要实现的是给 Vertex, Fragment Shader中的变量赋值,唯独除了varying vec2 aCoord,它负责从Vertex到Fragment内部传值的。

这里先创建渲染器 ScreenFiliter,首先需要读取Shader文件,通过工具包OpenGLUtils读取着色器的代码,然后渲染到屏幕:

//OpenGLUtils中代码
//从 shader文件读出 字符串
    public static String readRawTextFile(Context context, int rawId) {
        InputStream is = context.getResources().openRawResource(rawId);
        BufferedReader br = new BufferedReader(new InputStreamReader(is));
        String line;
        StringBuilder sb = new StringBuilder();
        try {
            while ((line = br.readLine()) != null) {
                sb.append(line);
                sb.append("\n");
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        try {
            br.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return sb.toString();
    }

使用两个文件的字符串,创建顶点着色器,片元着色器、着色器程序,这里其实给的是对应的ID。

 //顶点着色
    protected int mVertexShaderId;
    //片段着色
    protected int mFragmentShaderId;
    //着色器程序
    protected int mGLProgramId;

//读取GLSL文件
    protected void initilize(Context context) {
        String vertexSharder = OpenGLUtils.readRawTextFile(context, mVertexShaderId);
        String framentShader = OpenGLUtils.readRawTextFile(context, mFragmentShaderId);
        mGLProgramId = OpenGLUtils.loadProgram(vertexSharder, framentShader);
     	........
        // 获得着色器中的 attribute 变量 position 的索引值
        vPosition = GLES20.glGetAttribLocation(mGLProgramId, "vPosition");
        vCoord = GLES20.glGetAttribLocation(mGLProgramId,"vCoord");
        vMatrix = GLES20.glGetUniformLocation(mGLProgramId,"vMatrix");
        // 获得Uniform变量的索引值
        vTexture = GLES20.glGetUniformLocation(mGLProgramId,"vTexture");
    }

OpenGLUtils.loadProgram:把创建的vShader, fShader给到着色器程序 program,它是运行在GPU上的。

public static int loadProgram(String vSource,String fSource){
    //顶点着色器
    int vShader = GLES20.glCreateShader(GLES20.GL_VERTEX_SHADER);
    //加载着色器代码
    GLES20.glShaderSource(vShader,vSource);
    //编译(配置)
    GLES20.glCompileShader(vShader);
    //查看配置 是否成功
    int[] status = new int[1];
    GLES20.glGetShaderiv(vShader,GLES20.GL_COMPILE_STATUS,status,0);
    if(status[0] != GLES20.GL_TRUE){//失败
      throw new IllegalStateException("load vertex shader:"+ GLES20.glGetShaderInfoLog(vShader));
    }
    //片元着色器 流程和上面一样
    int fShader = GLES20.glCreateShader(GLES20.GL_FRAGMENT_SHADER);
    //加载着色器代码
    GLES20.glShaderSource(fShader,fSource);
    //编译(配置)
    GLES20.glCompileShader(fShader);

    //查看配置 是否成功
    GLES20.glGetShaderiv(fShader,GLES20.GL_COMPILE_STATUS,status,0);
    if(status[0] != GLES20.GL_TRUE){//失败
      throw new IllegalStateException("load fragment shader:"+GLES20.glGetShaderInfoLog(vShader));
    }

    //创建着色器程序
    int program = GLES20.glCreateProgram();
    //绑定顶点和片元
    GLES20.glAttachShader(program,vShader);
    GLES20.glAttachShader(program,fShader);
    //链接着色器程序
    GLES20.glLinkProgram(program);
    //获得状态
    GLES20.glGetProgramiv(program,GLES20.GL_LINK_STATUS,status,0);
    if(status[0] != GLES20.GL_TRUE){
      throw new IllegalStateException("link program:"+GLES20.glGetProgramInfoLog(program));
    }
    GLES20.glDeleteShader(vShader);
    GLES20.glDeleteShader(fShader);
  
    return program;
}

然后通过 program进行画画,只需将mGLProgramId给到opengl,将Vertex, Fragment需要的变量值传进去就OK了, 这里先拿到 里面变量的索引,这个之前在init方法里实现了。

  /**
     * 顶点着色器
     * attribute vec4 position;
     * 赋值给gl_Position(顶点)
     */
    protected int vPosition;
    /**
     * varying vec2 textureCoordinate;
     */
    protected int vCoord;
    /**
     * uniform mat4 vMatrix;
     */
    protected int vMatrix;
    /**
     * 片元着色器
     * Samlpe2D 扩展 samplerExternalOES
     */
    protected int vTexture;

//读取GLSL文件
   protected void initilize(Context context) {
     	........
        // 获得着色器中的 attribute 变量 position 的索引值
        vPosition = GLES20.glGetAttribLocation(mGLProgramId, "vPosition");
        vCoord = GLES20.glGetAttribLocation(mGLProgramId,"vCoord");
        vMatrix = GLES20.glGetUniformLocation(mGLProgramId,"vMatrix");
        // 获得Uniform变量的索引值
        vTexture = GLES20.glGetUniformLocation(mGLProgramId,"vTexture");
    }

这里需要用到NIO的Buffer来存储数据,通过Buffer来传给对应的着色器

protected FloatBuffer mGLVertexBuffer;
protected FloatBuffer mGLTextureBuffer;

public ScreenFiliter(Context context, int vertexShaderId, int fragmentShaderId) {
        this.mVertexShaderId = vertexShaderId;
        this.mFragmentShaderId = fragmentShaderId;
        // 4个点 x,y = 4*2 float 4字节 所以 4*2*4
        mGLVertexBuffer = ByteBuffer.allocateDirect(4 * 2 * 4)
                .order(ByteOrder.nativeOrder())
                .asFloatBuffer();
        mGLVertexBuffer.clear();
        float[] VERTEX = {
                -1.0f, -1.0f,
                1.0f, -1.0f,
                -1.0f, 1.0f,
                1.0f, 1.0f
        };
        mGLVertexBuffer.put(VERTEX);
        mGLTextureBuffer = ByteBuffer.allocateDirect(4 * 2 * 4)
                .order(ByteOrder.nativeOrder())
                .asFloatBuffer();
        mGLTextureBuffer.clear();
        float[] TEXTURE = {
                0.0f, 1.0f,
                1.0f, 1.0f,
                0.0f, 0.0f,
                1.0f, 0.0f
        };
        mGLTextureBuffer.put(TEXTURE);
        initilize(context);
        initCoordinate();
}

将以上的 Vertex,Fragment中对应需要接受值传递的 上面的索引,以及给了值Buffer应用到OpenGL

  • [ ] ```Java public int onDrawFrame(int textureId) { //设置显示窗口,这里传入的是全屏 GLES20.glViewport(0, 0, mOutputWidth, mOutputHeight); //使用着色器 GLES20.glUseProgram(mGLProgramId); //传递坐标 mGLVertexBuffer.position(0); GLES20.glVertexAttribPointer(vPosition,2,GLES20.GL_FLOAT, false,0,mGLVertexBuffer); GLES20.glEnableVertexAttribArray(vPosition);

      //2、将纹理坐标传入,采样坐标
      mGLTextureBuffer.position(0);
      GLES20.glVertexAttribPointer(vCoord,2,GLES20.GL_FLOAT,false, 0, mGLTextureBuffer);
      //传入数据后,激活
      GLES20.glEnableVertexAttribArray(vCoord);
      
      //片元 vTexture 绑定图像数据到采样器
      //激活图层, 第0层。它本身可以放多层数据
      GLES20.glActiveTexture(GLES20.GL_TEXTURE0);
      //图像数据,将
      //正常传GL_TEXTURE_2D, 这里需要穿GL_TEXTURE_external_oes
      GLES20.glBindTexture(GLES20.GL_TEXTURE_EXTERNAL_OES, textureId);
      //传递参数, 对应上面的第0层, 跟上面的对应
      GLES20.glUniform1i(vTexture, 0);
      //参数传完了 通知opengl 画画 从第0点开始 共4个点
      GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4);
      GLES20.glBindTexture(GLES20.GL_TEXTURE_EXTERNAL_OES, 0);
      return textureId;
    }
    ```
    

上面的这些步骤都是在给 着色器传值,着色器已经给到 Program,最后通知OpenGL进行画画。GLES20.glBindTexture(GLES20.GL_TEXTURE_EXTERNAL_OES, 0);

这样就完成把Camera扫描的数据通过OpenGL绘制到屏幕上去了。