相关文章: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会在顶点着色器确定顶点的位置,然后这些顶点连起来就是我们想要的图形。接着在片元着色器里面给这些图形上色:

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

    <android.opengl.GLSurfaceViewandroid:id="@+id/surface_view"android:layout_width="match_parent"android:layout_height="match_parent" />
        glView = findViewById(R.id.surface_view);glView.setEGLContextClientVersion(2);MyGLRender glVideoRenderer = new MyGLRender();//创建rendererglView.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

    @Overridepublic void onSurfaceCreated(GL10 gl10, EGLConfig eglConfig) {initGLProgram();Surface surface = crateSurface();// mediaplayer playtry {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的基础上进行矩阵转换呢?这是因为计算机图像坐标与纹理坐标的表示是不一致的。如下图:



因为我们使用的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};

创建渲染程序
如何将两个着色器代码替换到渲染管线中呢,基本流程如下图:

编译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就可以了。

代码如下:

 //创建shaderprivate 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 objectLog.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 objectGLES20.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)的位置。

aTexCoord描述纹理坐标(如上图OpenGL二维纹理坐标),我们现在要把纹理按照,左下->右下->左上->右上的顺序,贴到物体上。所以对应的顶点坐标及纹理坐标数据为:

        //顶点着色器坐标,z为0float[] 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 MediaPlayerint[] textures = new int[1];//just one texures,use external modeGLES20.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 playtry {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);

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

绘制前先清除上一帧,

        //clearGLES20.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.

完整代码如下:

    @Overridepublic void onSurfaceChanged(GL10 gl10, int width, int height) {screenWidth = width;screenHeight = height;}@Overridepublic void onDrawFrame(GL10 gl10) {synchronized (this){if(isFrameUpdate){mSurfaceTexture.updateTexImage();mSurfaceTexture.getTransformMatrix(mSTMatrix);isFrameUpdate = false;}}//update width and heightGLES20.glViewport(0, 0, screenWidth, screenHeight);//clearGLES20.glClearColor(0.0f, 0.0f, 0.0f, 1.0f);GLES20.glClear(GLES20.GL_DEPTH_BUFFER_BIT | GLES20.GL_COLOR_BUFFER_BIT);//update st mat4GLES20.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);}//drawGLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4);}@Overridepublic void onFrameAvailable(SurfaceTexture surfaceTexture) {isFrameUpdate = true;}

总结

播放效果如下:

下一章会描述如何在native层使用EGL和OpenGL,这样会对Android OpenGL ES视频渲染有更深入的了解。

Android OpenGL ES视频渲染(一)GLSurfaceView相关推荐

  1. android OpenGL ES实现渲染到透明的纹理 render to transparent texture

    PC上OpenGL渲染到纹理,很容易得到透明背景,但是在android上OpenGL ES渲染出来是黑色背景,对于这个问题,想了两个解决办法. 1> 让android的OpenGL ES环境支持 ...

  2. android 视频播放滤镜,用openGL ES+MediaPlayer 渲染播放视频+滤镜效果

    之前曾经写过用SurfaceView,TextureView+MediaPlayer 播放视频,和 ffmpeg avi解码后SurfaceView播放视频,今天再给大家来一篇openGL ES+Me ...

  3. Android OpenGL ES 学习(十一) –渲染YUV视频以及视频抖音特效

    OpenGL 学习教程 Android OpenGL ES 学习(一) – 基本概念 Android OpenGL ES 学习(二) – 图形渲染管线和GLSL Android OpenGL ES 学 ...

  4. android平台下OpenGL ES 3.0使用GLSurfaceView对相机Camera预览实时处理

    OpenGL ES 3.0学习实践 android平台下OpenGL ES 3.0从零开始 android平台下OpenGL ES 3.0绘制纯色背景 android平台下OpenGL ES 3.0绘 ...

  5. Android OpenGL ES 学习(十二) - MediaCodec + OpenGL 解析H264视频+滤镜

    OpenGL 学习教程 Android OpenGL ES 学习(一) – 基本概念 Android OpenGL ES 学习(二) – 图形渲染管线和GLSL Android OpenGL ES 学 ...

  6. Android Camera使用OpenGL ES 2.0和GLSurfaceView对预览进行实时二次处理(黑白滤镜)

    第一篇 Android Camera使用OpenGL ES 2.0和GLSurfaceView对预览进行实时二次处理(黑白滤镜) 第二篇 Android Camera使用OpenGL ES 2.0和T ...

  7. Android Camera API 2使用OpenGL ES 2.0和GLSurfaceView对预览进行实时二次处理(黑白滤镜)

    这段时间有点忙,一直没时间写第三篇教程,其实代码很早之前就写好了.本系列教程会有三篇文章讲解Android平台滤镜的实现方式,希望在阅读本文之前先阅读前面两篇文档. 第一篇 Android Camer ...

  8. Android OpenGL ES 学习(十) – GLSurfaceView 源码解析GL线程以及自定义 EGL

    OpenGL 学习教程 Android OpenGL ES 学习(一) – 基本概念 Android OpenGL ES 学习(二) – 图形渲染管线和GLSL Android OpenGL ES 学 ...

  9. Android OpenGL ES从入门到进阶(一)—— 五分钟开发一款美颜相机

    源码链接:https://github.com/smzhldr/AGLFramework 基础知识入门篇(Hello Triangle) 渲染纹理到屏幕 GLSurfaceView预览相机 简单易用的 ...

最新文章

  1. 云服务器建站 - Node Nginx MySQL
  2. 《每天学点博弈论全集》-读书总结
  3. [NOIP 2010普及组 No.3] 导弹拦截
  4. oracle字段加约束,Oracle数据库的字段约束创建和维护示例
  5. 英语笔记:词组句子:0906
  6. winfrom 从网页中通过源代码截取文章
  7. sql server 循环_学习SQL:SQL Server循环简介
  8. 点云质量评估_Chamfer Distance--倒角距离
  9. WPF ICommandSource Implementations Leak Memory!
  10. 阿里大于短信返回XML
  11. STM32F107VCTx I2C通信
  12. 数据结构之图的基本介绍
  13. ele-ui表单验证规则中的手机号码和邮箱的验证规则
  14. Win10开机黑屏进不了桌面
  15. 镜像站(整理各个镜像站资源)
  16. 2020作为一个资深的Android开发者需要掌握哪些技能?
  17. EF core和数据库, Database First
  18. matplotlib 库画云图两种方法
  19. 动态反汇编入门——扫雷游戏探雷
  20. heritrix 基本介绍

热门文章

  1. 刚刚!华为突然传来这一消息,我想辞职回家养猪了!
  2. Running pipenv gives TypeError: 'module' object is not callable
  3. 腾讯帝国的下坡路 | 畅言
  4. hive ddl语法使用详解
  5. 静态变量和静态方法编程训练—信用卡消费记录
  6. 淘宝Hadoop作业平台宙斯(zeus)开源
  7. 万元怎样保留小数点_如何让数字以万元为单位显示并保留2位小数,且有千位分隔符...
  8. Sicily2000——Toy Shopping
  9. linux下静态库、动态库总结
  10. 实现网页原路返回:从哪个页面跳转过来就返回哪个页面