相关文章:Android OpenGL ES视频渲染(二)EGL+OpenGL

Android中视频渲染有几种方式,之前的文章使用的是nativewindow(包括softwareRender)。今天介绍另一总视频渲染的方式——OpenGL ES。
阅读本文之前需要对OpenGL有一定的了解,可以参考https://www.jianshu.com/p/99daa25b4573

在Android中使用OpenGL的方法有两种,一种是在native层使用EGL+OpenGL来实现,另一种则是GLSurfaceView。
本文将使用GLSurfaceView+MediaPlayer实现播放,并通过OpenGL进行简单的滤镜处理,以此来说明如何使用GLSurfaceView。

题外话:nativewindow和OpenGL渲染视频的代码,可以参考ijkplayer的实现。

OpenGL

OpenGL引擎渲染图像的流程比较复杂,简单来说是以下几步。(引用自https://www.jianshu.com/p/99daa25b4573)
但我们最主要先了解顶点处理阶段及片元处理阶段。

阶段一:指定几何对象
所谓几何对象,就是点,直线,三角形,这里将根据具体执行的指令绘制几何图元。比如,OpenGL提供给开发者的绘制方法glDrawArrays,这个方法里的第一个参数是mode,就是制定绘制方式,可选值有一下几种。

GL_POINT:以点的形式进行绘制,通常用在绘制粒子效果的场景中。
GL_LINES:以线的形式进行绘制,通常用在绘制直线的场景中。
GL_TRIANGLE_STRIP:以三角形的形式进行绘制,所有二维图像的渲染都会使用这种方式。

阶段二:顶点处理
不论以上的几何对象是如何指定的,所有的几何数据都将会经过这个阶段。这个阶段所做的操作就是,根据模型视图和投影矩阵进行变换来改变顶点的位置,根据纹理坐标与纹理矩阵来改变纹理坐标的位置,如果涉及三维的渲染,那么这里还要处理光照计算与法线变换。
一般输出是以gl_Position来表示具体的顶点位置的,如果是以点来绘制几何图元,那么还应该输出gl_PointSize。

阶段三:图元组装
在经过阶段二的顶点处理操作之后,还是纹理坐标都是已经确定好了的。在这个阶段,顶点将会根据应用程序送往图元的规则(如GL_POINT、GL_TRIANGLE_STRIP),将纹理组装成图元。

阶段四:栅格化操作
由阶段三传递过来的图元数据,在此将会分解成更小的单元并对应于帧缓冲区的各个像素。这些单元称为片元,一个片元可能包含窗口颜色、纹理坐标等属性。片元的属性是根据顶点坐标利用插值来确定的,这就是栅格化操作,也就是确认好每一个片元是什么。

阶段五:片元处理
通过纹理坐标取得纹理(texture)中相对应的片元像素值(texel),根据自己的业务处理(比如提亮、饱和度调节、对比度调节、高斯模糊)来变换这个片元的颜色。这里的输出是gl_FragColor,用于表示修改之后的像素的最终结果。

阶段六:帧缓冲操作
该阶段主要执行帧缓冲的写入操作,这也是渲染管线的最后一步,负责将最终的像素值写入到帧缓冲区中。

OpenGL ES提供了可编程的着色器来代替渲染管线的某个阶段。
Vertex Shader(顶点着色器)用来替代顶点处理阶段。
Fragment Shader(片元着色器,又称为像素着色器)用来替换片元处理阶段。

简单来讲就是OpenGL会在顶点着色器确定顶点的位置,然后这些顶点连起来就是我们想要的图形。接着在片元着色器里面给这些图形上色:

Android OpenGL ES视频渲染(一)GLSurfaceView_第1张图片

GLSurfaceView

GLSurfaceView看名字就是可以使用OpenGL的SurfaceView,也确实如此,它继承自SurfaceView,具备SurfaceView的特性,并加入了EGL的管理,它自带了一个GLThread绘制线程(EGLContext创建GL环境所在线程即为GL线程),绘制的工作直接通过OpenGL在绘制线程进行,不会阻塞主线程,绘制的结果输出到SurfaceView所提供的Surface上。
所以为什么我们不直接用surfaceView来进行播放呢?有以下两个好处:

  1. 通过GLSurfaceView进行视频渲染,可以使用GPU加速,相对于SurfaceView使用画布进行绘制,OpenGL的绘制关联到GPU,效率更高。
  2. 可以定制render(渲染器),从而可以实现定制效果。

使用流程:

创建一个GLSurfaceView用来承载视频
->设置render(实现OpenGL着色器代码)
->创建SurfaceTexture,绑定的外部Texture
->将SurfaceTexture的surface设置给MediaPlayer,启动播放
->在render的onDrawFrame中更新Texture,绘制新画面。

其中,render是最核心部分。

1、创建GLSurfaceView

    
        glView = findViewById(R.id.surface_view);        glView.setEGLContextClientVersion(2);        MyGLRender glVideoRenderer = new MyGLRender();//创建renderer        glView.setRenderer(glVideoRenderer);//设置renderer

创建GLSurfaceView后,设置其OpenGL版本为2.0,然后设置render。下面介绍MyGLRender。

2、创建render

render需要实现GLSurfaceView.Renderer的三个接口:

    public interface Renderer {        void onSurfaceCreated(GL10 var1, EGLConfig var2);        void onSurfaceChanged(GL10 var1, int var2, int var3);        void onDrawFrame(GL10 var1);    }

onSurfaceCreated进行渲染程序的初始化,创建Surface,启动MediaPlayer

    @Override    public void onSurfaceCreated(GL10 gl10, EGLConfig eglConfig) {        initGLProgram();        Surface surface = crateSurface();        // mediaplayer play        try {            mPlayer.setSurface(surface);            mPlayer.prepare();            mPlayer.start();        } catch (IOException e) {            e.printStackTrace();        }    }

渲染程序的初始化

initGLProgram()中创建顶点着色器和片元着色器代码,一步步看:

顶点着色器

    private final String VSH_CODE =  "uniform mat4 uSTMatrix;\n"+                                        "attribute vec4 aPosition;\n"+                                        "attribute vec4 aTexCoord;\n"+                                        "varying vec2 vTexCoord;\n"+                                        "void main(){\n"+                                            "vTexCoord = (uSTMatrix*aTexCoord).xy;\n"+                                            "gl_Position = aPosition;\n"+                                        "}";

OpenGL会将每个顶点的坐标传递给顶点着色器,我们可以在这里改变顶点的位置。例如我们给每个顶点都加上一个偏移,就能实现整个图形的移动。

aPosition为顶点坐标,赋值给gl_Position ,表示物体位置,构成图元,可由外部传入。
aTexCoord为纹理坐标,纹理坐标描述纹理该如何在图元上贴图,可由外部传入。
vTexCoord为最终要传递给片元着色器的纹理坐标,为什么要在aTexCoord的基础上进行矩阵转换呢?这是因为计算机图像坐标与纹理坐标的表示是不一致的。如下图:

Android OpenGL ES视频渲染(一)GLSurfaceView_第2张图片
Android OpenGL ES视频渲染(一)GLSurfaceView_第3张图片
因为我们使用的texture是从外部得到的,其对应的是计算机坐标系,所以需要矩阵转换,这个矩阵可通过SurfaceTexture.getTransformMatrix函数获取到。

片元着色器

    private  final String FSH_CODE = "#extension GL_OES_EGL_image_external : require\n"+                                        "precision mediump float;\n"+                                        "varying vec2 vTexCoord;\n"+                                        "uniform mat4 uColorMatrix;\n"+                                        "uniform samplerExternalOES sTexture;\n"+                                        "void main() {\n"+                                            "gl_FragColor=uColorMatrix*texture2D(sTexture, vTexCoord).rgba;\n"+                                            //"gl_FragColor = texture2D(sTexture, vTexCoord);\n"+                                        "}";

片元着色器要注意的是#extension GL_OES_EGL_image_external : require,因为使用的是外部纹理samplerExternalOES类型的纹理sTexture,所以需要加上。
vTexCoord是从顶点着色器传过来的纹理坐标。
texture2D函数可以从该坐标获取到对应的颜色,这里我们加入了颜色转换矩阵uColorMatrix,这样就能进行一些效果处理。最后将颜色赋值给gl_FragColor。

颜色效果矩阵如下:

   private static float[] COLOR_MATRIX3 = {        // 怀旧效果矩阵            0.393f,0.349f, 0.272f,0.0f ,            0.769f,0.686f,0.534f,0.0f,            0.189f,0.168f,0.131f,0.0f,            0.0f,0.0f,0.0f,1.0f    };

创建渲染程序
如何将两个着色器代码替换到渲染管线中呢,基本流程如下图:
Android OpenGL ES视频渲染(一)GLSurfaceView_第4张图片
编译shader程序(compileShader代码)

  1. glCreateShader创建shader,参数为类型,指定顶点着色器还是片元着色器;
  2. glShaderSource加载shader代码;
  3. glCompileShader编译代码,并glGetShaderiv通过GL_COMPILE_STATUS获取编译是否正确;
  4. 得到一个shader程序的ID。

创建渲染程序(buildProgram代码)

  1. glCreateProgram创建program;
  2. glAttachShader通过shader程序的ID,把shader程序附进来;
  3. glLinkProgram链接程序,并glGetProgramiv通过GL_LINK_STATUS获取链接是否正确。
  4. 得到一个渲染程序的ID。

最后调用glUseProgram,传入渲染程序的ID就可以了。

代码如下:

//创建shader    private int compileShader(int type, String code){        int shaderObjectId = GLES20.glCreateShader(type);        if (shaderObjectId == 0){            Log.d(TAG, "compileShader: glCreateShader err");            return 0;        }        GLES20.glShaderSource(shaderObjectId, code);        GLES20.glCompileShader(shaderObjectId);        int[] compileStatus = new int[1];        GLES20.glGetShaderiv(shaderObjectId, GLES20.GL_COMPILE_STATUS, compileStatus, 0);        if (compileStatus[0] == 0){            // if it failed, delete the shader object            Log.d(TAG, "compileShader: glCompileShader err");            GLES20.glDeleteShader(shaderObjectId);            return 0;        }        Log.d(TAG, "compileShader: success: "+shaderObjectId);        return shaderObjectId;    }    //创建渲染程序    private int buildProgram(int vertexShaderId, int fragmentShaderId){        int programObjectId = GLES20.glCreateProgram();        if(programObjectId == 0){            Log.d(TAG, "buildProgram: glCreateProgram err");            return 0;        }        GLES20.glAttachShader(programObjectId, vertexShaderId);        GLES20.glAttachShader(programObjectId, fragmentShaderId);        GLES20.glLinkProgram(programObjectId);        int[] linkStatus = new int[1];        GLES20.glGetProgramiv(programObjectId, GLES20.GL_LINK_STATUS, linkStatus, 0);        if (linkStatus[0] == 0){            // if it failed, delete the shader object            GLES20.glDeleteProgram(programObjectId);            Log.d(TAG, "buildProgram: glLinkProgram err");            return 0;        }        Log.d(TAG, "buildProgram: success: "+programObjectId);        return programObjectId;    }

填充顶点坐标及纹理坐标
完成顶点着色器及片元着色器后,创建渲染程序,接下来我们要填充顶点信息:
顶点着色器中,aPosition表示物体位置坐标,坐标系中x轴从左到右是从-1到1变化的,y轴从下到上是从-1到1变化的,物体的中心点恰好是(0,0)的位置。
Android OpenGL ES视频渲染(一)GLSurfaceView_第5张图片
aTexCoord描述纹理坐标(如上图OpenGL二维纹理坐标),我们现在要把纹理按照,左下->右下->左上->右上的顺序,贴到物体上。所以对应的顶点坐标及纹理坐标数据为:

        //顶点着色器坐标,z为0        float[] vers = {                -1.0f, -1.0f, 0.0f,                1.0f, -1.0f, 0.0f,                -1.0f, 1.0f, 0.0f,                1.0f, 1.0f, 0.0f,        };   //纹理坐标,texture坐标ST,需要根据图像进行转换        float[] txts = {                0.0f, 0.0f,                1.0f, 0.0f,                0.0f, 1.0f,                1.0f, 1.0f        };

通过 GLES20.glEnableVertexAttribArray及GLES20.glVertexAttribPointer两个函数,完成顶点信息设置。

设置颜色效果
通过glGetUniformLocation获取到uColorMatrix矩阵的句柄,将颜色矩阵设赋值给它就行。这样就会在片元着色器中生效。

        //设置颜色效果        int colorMatrixHandle = GLES20.glGetUniformLocation(programId, "uColorMatrix");        GLES20.glUniformMatrix4fv(colorMatrixHandle, 1, false, COLOR_MATRIX3, 0);

完整代码:

private void initGLProgram(){        int vertexShader = compileShader(GLES20.GL_VERTEX_SHADER, VSH_CODE);        int fragmentShader = compileShader(GLES20.GL_FRAGMENT_SHADER, FSH_CODE);        int programId = buildProgram(vertexShader, fragmentShader);        if(programId == 0)            return;        GLES20.glUseProgram(programId);        mSTMatrixHandle = GLES20.glGetUniformLocation(programId, "uSTMatrix");//转换矩阵        //顶点着色器坐标        float[] vers = {                -1.0f, -1.0f, 0.0f,                1.0f, -1.0f, 0.0f,                -1.0f, 1.0f, 0.0f,                1.0f, 1.0f, 0.0f,        };        FloatBuffer vertexBuffer = ByteBuffer.allocateDirect(vers.length * 4)                .order(ByteOrder.nativeOrder())                .asFloatBuffer()                .put(vers);        vertexBuffer.position(0);        //纹理坐标,texture坐标ST,需要根据图像进行转换        float[] txts = {                0.0f, 0.0f,                1.0f, 0.0f,                0.0f, 1.0f,                1.0f, 1.0f        };        FloatBuffer textureVertexBuffer = ByteBuffer.allocateDirect(txts.length * 4)                .order(ByteOrder.nativeOrder())                .asFloatBuffer()                .put(txts);        textureVertexBuffer.position(0);        //设置顶点坐标和纹理坐标        int apos = GLES20.glGetAttribLocation(programId, "aPosition");        GLES20.glEnableVertexAttribArray(apos);        GLES20.glVertexAttribPointer(apos, 3, GLES20.GL_FLOAT, false, 12, vertexBuffer);        int atex = GLES20.glGetAttribLocation(programId, "aTexCoord");        GLES20.glEnableVertexAttribArray(atex);        GLES20.glVertexAttribPointer(atex, 2, GLES20.GL_FLOAT, false, 8, textureVertexBuffer);        //设置颜色效果        int colorMatrixHandle = GLES20.glGetUniformLocation(programId, "uColorMatrix");        GLES20.glUniformMatrix4fv(colorMatrixHandle, 1, false, COLOR_MATRIX3, 0);    }

3、创建SurfaceTexture,绑定外部纹理

glGenTextures创建Texture,我们使用的是外部纹理,所以只需要一个即可。
glBindTexture绑定纹理,要注意这里需要设置GL_TEXTURE_EXTERNAL_OES标志。
glTexParameterf设置一些属性,这里设置的是缩放的算法。
然后根据mTextureID创建SurfaceTexture,然后创建Surface,Surface就可以设置给MeidaPlayer。

完整代码:

    private Surface crateSurface(){        // Create SurfaceTexture that will feed this textureId and pass to MediaPlayer        int[] textures = new int[1];//just one texures,use external mode        GLES20.glGenTextures(1, textures, 0);        mTextureID = textures[0];        GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, mTextureID);        GLES20.glTexParameterf(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_NEAREST);        GLES20.glTexParameterf(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR);        mSurfaceTexture = new SurfaceTexture(mTextureID);        mSurfaceTexture.setOnFrameAvailableListener(this);        Surface surface = new Surface(mSurfaceTexture);        return surface;    }

4、Surface设置给MediaPlayer,启动播放

没什么可以说道的,就是把上面创建的surface设置给播放器,同步的prepare,加上start。

    // mediaplayer play    try {        mPlayer.setSurface(surface);        mPlayer.prepare();        mPlayer.start();    } catch (IOException e) {        e.printStackTrace();    }

5、onDrawFrame中更新Texture,绘制新画面

上面创SurfaceTexture时通过setOnFrameAvailableListener设置了监听器,监听纹理的更新,更新了,我们就设置isFrameUpdate为true。
onDrawFrame是render进行绘制时会调用,当isFrameUpdate为true,意味着我们可以进行绘制了。

先通过SurfaceTexture.updateTexImage()更新纹理,然后glViewport设置绘制的窗口大小。

OpenGL虽然是在Surface上绘制,但我们可以不铺满整个Surface,可以只在它的某部分绘制,例如我们可以用下面代码只用TextureSurface的左下角的四分之一去显示OpenGL的画面:

//width、height是TextureView的宽高 GLES20.glViewport(0, 0, width/2, height/2); 

Android OpenGL ES视频渲染(一)GLSurfaceView_第6张图片

我们这里还是铺满整个View,宽高可以在onSurfaceChanged中获取到。

绘制前先清除上一帧,

        //clear        GLES20.glClearColor(0.0f, 0.0f, 0.0f, 1.0f);        GLES20.glClear(GLES20.GL_DEPTH_BUFFER_BIT | GLES20.GL_COLOR_BUFFER_BIT);

当然这里还可以再清空片元着色器的外部纹理。

设置纹理变换矩阵,矩阵在SurfaceTexture.getTransformMatrix获取到
激活绑定纹理,然后就可以绘制了。
绘制采用的三角形方式GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4);

render缺省模式是 RENDERMODE_CONTINUOUSLY,就是说 surface绘制线程不停循环调用onDrawFrame。所以帧频控制取决于每帧的绘制时间,通常都是在onDrawFrame里加延时来控制的。
当设置为RENDERMODE_WHEN_DIRTY时,就是通常的事件驱动模式来绘制。画面重新显示出来或 requestRender()时才会调用onDrawFrame.

完整代码如下:

    @Override    public void onSurfaceChanged(GL10 gl10, int width, int height) {        screenWidth = width;        screenHeight = height;    }    @Override    public void onDrawFrame(GL10 gl10) {        synchronized (this){            if(isFrameUpdate){                mSurfaceTexture.updateTexImage();                mSurfaceTexture.getTransformMatrix(mSTMatrix);                isFrameUpdate = false;            }        }        //update width and height        GLES20.glViewport(0, 0, screenWidth, screenHeight);        //clear        GLES20.glClearColor(0.0f, 0.0f, 0.0f, 1.0f);        GLES20.glClear(GLES20.GL_DEPTH_BUFFER_BIT | GLES20.GL_COLOR_BUFFER_BIT);        //update st mat4        GLES20.glUniformMatrix4fv(mSTMatrixHandle, 1, false, mSTMatrix, 0);        //bind and active, juest one time        {            GLES20.glActiveTexture(GLES20.GL_TEXTURE0);            GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, mTextureID);        }        //draw        GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4);    }    @Override    public void onFrameAvailable(SurfaceTexture surfaceTexture) {        isFrameUpdate = true;    }

总结

播放效果如下:
Android OpenGL ES视频渲染(一)GLSurfaceView_第7张图片
下一章会描述如何在native层使用EGL和OpenGL,这样会对Android OpenGL ES视频渲染有更深入的了解。

更多相关文章

  1. android中度量坐标 传感器应用的开发
  2. android Draw Rect 坐标图示以及DrawOval的椭圆坐标说明
  3. Android屏幕坐标和LCD坐标的转换
  4. Android之获取控件的坐标
  5. 【Android】手机地图功能——利用手机GPS获取用户地理坐标
  6. android坐标图解
  7. Android 的坐标系及矩阵变换
  8. Android进阶之光读书笔记:View体系(一) View与 ViewGroup、View坐标
  9. 获取组件坐标系

随机推荐

  1. The Saygus VPhone V1 clears FCC, Will
  2. Android(安卓)onSaveInstanceState和onRe
  3. android Gridview生成程序快捷键的简单方
  4. Android(安卓)Applications Tutorial 13.
  5. ANDROID轮播广告图片
  6. android安装apk程序
  7. android屏蔽Home键
  8. 2013.09.22——— android GridView行背
  9. android中隐藏以及显示软键盘代码
  10. Speed Up and Back Up Your Rooted Andro