美文网首页Android开发Android进阶之路Android技术知识
【Android 音视频开发打怪升级:OpenGL渲染视频画面篇

【Android 音视频开发打怪升级:OpenGL渲染视频画面篇

作者: 开发的猫 | 来源:发表于2019-10-22 10:05 被阅读0次

    【声 明】

    首先,这一系列文章均基于自己的理解和实践,可能有不对的地方,欢迎大家指正。
    其次,这是一个入门系列,涉及的知识也仅限于够用,深入的知识网上也有许许多多的博文供大家学习了。
    最后,写文章过程中,会借鉴参考其他人分享的文章,会在文章最后列出,感谢这些作者的分享。

    码字不易,转载请注明出处!

    教程代码:【Github传送门

    目录

    一、Android音视频硬解码篇:
    二、使用OpenGL渲染视频画面篇
    三、Android FFmpeg音视频解码篇
    • 1,FFmpeg so库编译
    • 2,Android 引入FFmpeg
    • 3,Android FFmpeg视频解码播放
    • 4,Android FFmpeg+OpenSL ES音频解码播放
    • 5,Android FFmpeg+OpenGL ES播放视频
    • 6,Android FFmpeg简单合成MP4:视屏解封与重新封装
    • 7,Android FFmpeg视频编码

    本文你可以了解到

    本文主要介绍OpenGL相关的基础知识,包括坐标系、着色器、基本渲染流程等。

    一 简介

    提到OpenGL,想必很多人都会说,我知道这个东西,可以用来渲染2D画面和3D模型,同时又会说,OpenGL很难、很高级,不知道怎么用。

    1、为什么OpenGL“感觉很难”?

    • 函数多且杂,渲染流程复杂
    • GLSL着色器语言不好理解
    • 面向过程的编程思维,和Java等面向对象的编程思维不同

    2、OpenGL ES是什么?

    为了解决以上问题,让OpenGL“学起来不是很难”,需要把其分解成一些简单的步骤,然后简单的东西串联起来,一切就水到渠成了。

    首先,来看看什么是OpenGL。

    • CPU和GPU

    在手机上,有两大元件,一个是CPU,一个是GPU。而手机上显示图形界面也有两种方式,一个是使用CPU来渲染,一个是使用GPU来渲染,可以说,GPU渲染其实是一种硬件加速。

    为什么GPU可以大大提高渲染速度,因为GPU最擅长的是并行浮点运算,可以用来对许许多多的像素做并行运算。

    OpenGL(Open Graphics Library)则是间接操作GPU的工具,是一组定义好的跨平台和跨语言的图形API,是可用于2D和3D画面渲染的底层图形库,是由各个硬件厂家具体实现的编程接口。

    • OpenGL 与 OpenGL ES

    OpenGL ES 全称:OpenGL for Embedded Systems,是OpenGL 的子集,是针对手机 PAD等小型设备设计的,删减了不必须的方法、数据类型、功能,减少了体积,优化了效率。

    3、 OpenGL ES版本

    目前主要版本有1.0/1.1/2.0/3.0/3.1

    • 1.0:Android 1.0和更高的版本支持这个API规范
    • 2.0:不兼容 OpenGL ES 1.x。Android 2.2(API 8)和更高的版本支持这个API规范
    • 3.0:向下兼容 OpenGL ES 2.x。Android 4.3(API 18)及更高的版本支持这个API规范
    • 3.1:向下兼容 OpenGL ES3.0/2.0。Android 5.0(API 21)和更高的版本支持这个API规范

    2.0 版本是 Android 目前支持最广泛的版本,后续主要以该版本为主,进行介绍和代码编写。

    二、OpenGL ES坐标系

    在音视频开发中,涉及到的坐标系主要有两个:世界坐标和纹理坐标。

    由于基本不涉及3D贴图,所以只看x/y轴坐标,忽略z轴坐标,涉及到3D相关知识可自行Google,不在讨论范围内。

    首先来看两个图:

    世界坐标 纹理坐标
    • OpenGL ES世界坐标

    通过名字就可以知道,这是OpenGL自己世界的坐标,是一个标准化坐标系,范围是 -1 ~ 1,原点在中间。

    • OpenGL ES纹理坐标

    纹理坐标,其实就是屏幕坐标,标准的纹理坐标原点是在屏幕的左下方,而Android系统坐标系的原点是在左上方的。这是Android使用OpenGL需要注意的一个地方。

    纹理坐标的范围是 0 ~ 1。

    注:坐标系的xy轴方向很重要,决定了如何做顶点坐标和纹理坐标映射。

    那么,这两个坐标系究竟有什么关系呢?

    世界坐标,是用于显示的坐标,即像素点应该显示在哪个位置由世界坐标决定。

    纹理坐标,表示世界坐标指定的位置点想要显示的颜色,应该在纹理上的哪个位置获取。即颜色所在的位置由纹理坐标决定。

    两者之间需要做正确的映射,才能正常的显示一张画面。

    三、OpenGL 着色器语言 GLSL

    在OpenGL 2.0以后,加入了新的可编程渲染管线,可以更加灵活的控制渲染。但也因此需要学习多一门针对GPU的编程语言,语法与C语言类似,名为GLSL。

    • 顶点着色器 & 片元着色器

    在介绍GLSL之前,先来看两个比较陌生的名词:顶点着色器和片元着色器。

    着色器,是一种可运行在GPU上的小程序,用GLSL语言编写。从命名上,顶点着色器是用于操控顶点的程序,而片元着色器是用于操控像素颜色属性的程序。

    简单理解:其实就是对应了以上两个坐标系:顶点着色器对应世界坐标,片元着色器对应纹理坐标。

    画面上的每个点,都会执行一次顶点和片元着色器中的程序片段,并且是并行执行,最后渲染到屏幕上。

    • GLSL编程

    下面,通过一个最简单的顶点着色器和片元着色器来简单介绍一下GLSL语言

    #顶点着色器
    
    attribute vec4 aPosition;
    
    void main() {
      gl_Position = aPosition;
    }
    
    #片元着色器
    
    void main() {
        gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0)
    }
    

    首先可以看到,GLSL语言是一种类C语言,着色器的框架基本和C语言一样,在最上面声明变量,接着是main函数。在着色器中,有几个内建的变量,可以直接使用(这里只列出音视频开发常用的,还有其他的一些3D开发会用到的):

    • 顶点着色器的内建输入变量

    gl_Position:顶点坐标
    gl_PointSize:点的大小,没有赋值则为默认值1

    • 片元着色器内建输出变量

    gl_FragColor:当前片元颜色

    看回上面的着色器代码。

    1)在顶点着色器中,传入了一个vec4的顶点坐标xyzw,然后直接传递给内建变量gl_Position,即直接根据顶点坐标渲染,不再做位置变换。

    注:顶点坐标是在Java代码中传入的,后面会讲到,另外w是齐次坐标,2D渲染没有作用

    2)在片元着色器中,直接给gl_FragColor赋值,依然是一个vec4类型的数据,这里表示rgba颜色值,为红色。

    可以看到vec4是一个4维向量,可用于表示坐标xyzw,也可用表示rgba,当然还有vec3,vec2等,可以参考这篇文章:着色器语言GLSL,讲的非常详细,建议看看。

    这样,两个简单的着色器串联起来后,每一个顶点(像素)都会显示一个红点,最后屏幕会显示一个红色的画面。

    具体GLSL关于数据类型和语法不再展开介绍,后面涉及到的GLSL代码会做更深入的讲解。更详细的可以参考这位作者的文章【着色器语言GLSL】,非常详尽。

    四、Android OpenGL ES渲染流程

    OpenGL的渲染流程说实话是比较繁琐的,也是让很多人望而生畏的地方,但是,如果归结起来,其实整个渲染流程基本是固定的,只要把它按照固定的流程封装好,其实并没有那么复杂。

    接下来,就进入实战,一层一层扒开OpengGL的神秘面纱。

    1、初始化

    在Android中,OpenGL通常配合GLSurfaceView使用,在GLSurfraceView中,Google已经封装好了渲染的基础流程。

    这里需要单独强调一下,OpenGL是基于线程的一个状态机,有关OpenGL的操作,比如创建纹理ID,初始化,渲染等,都必须要在同一个线程中完成,否则会造成异常。

    通常开发者在刚刚接触OpenGL的时候并不能深刻体会到这种机制,原因是Google在GLSurfaceView中已经帮开发者做了这部分的内容。这是OpenGL非常重要的一个方面,在后续的有关EGL的文章中会继续深入了解到。

    1. 新建页面
    class SimpleRenderActivity : AppCompatActivity() {
        //自定义的OpenGL渲染器,详情请继续往下看
        lateinit var drawer: IDrawer
    
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            setContentView(R.layout.activity_simpler_render)
    
            drawer = if (intent.getIntExtra("type", 0) == 0) {
                TriangleDrawer()
            } else {
                BitmapDrawer(BitmapFactory.decodeResource(CONTEXT!!.resources, R.drawable.cover))
            }
            initRender(drawer)
        }
    
        private fun initRender(drawer: IDrawer) {
            gl_surface.setEGLContextClientVersion(2)
            gl_surface.setRenderer(SimpleRender(drawer))
        }
    
        override fun onDestroy() {
            drawer.release()
            super.onDestroy()
        }
    }
    
    <?xml version="1.0" encoding="utf-8"?>
    <android.support.constraint.ConstraintLayout 
        xmlns:android="http://schemas.android.com/apk/res/android"
        android:layout_width="match_parent"
        android:layout_height="match_parent">
        <android.opengl.GLSurfaceView
                android:id="@+id/gl_surface"
                android:layout_width="match_parent"
                android:layout_height="match_parent"/>
    </android.support.constraint.ConstraintLayout>
    

    页面非常简单,放置了一个满屏的GLSurfaceView,初始化的时候,设置了OpenGL使用的版本为2.0,然后配置了渲染器SimpleRender,继承自GLSurfaceView.Renderer

    IDrawer将在绘制三角形的时候具体讲解,定义该接口类只是为了方便拓展,也可以直接将渲染代码写在SimpleRender中。

    1. 实现渲染接口
    class SimpleRender(private val mDrawer: IDrawer): GLSurfaceView.Renderer {
    
        override fun onSurfaceCreated(gl: GL10?, config: EGLConfig?) {
            GLES20.glClearColor(0f, 0f, 0f, 0f)
            GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT)
            mDrawer.setTextureID(OpenGLTools.createTextureIds(1)[0])
        }
    
        override fun onSurfaceChanged(gl: GL10?, width: Int, height: Int) {
            GLES20.glViewport(0, 0, width, height)
        }
    
        override fun onDrawFrame(gl: GL10?) {
            mDrawer.draw()
        }
    }
    

    注意到,实现了三个回调接口,这三个接口就是Google封装好的流程中,暴露出来的接口,留给给开发者实现初始化和渲染,并且这三个接口的回调都在同一个线程中。

    • 在onSurfaceCreated中,调用了两句OpenGL ES的代码实现清屏,清屏颜色为黑色。
    GLES20.glClearColor(0f, 0f, 0f, 0f)
    GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT)
    

    同时,创建了一个纹理ID,并设置给Drawer,如下:

    fun createTextureIds(count: Int): IntArray {
        val texture = IntArray(count)
        GLES20.glGenTextures(count, texture, 0) //生成纹理
        return texture
    }
    
    • 在onSurfaceChanged中,调用glViewport,设置了OpenGL绘制的区域宽高和位置

    这里所说的绘制区域,是指OpenGL在GLSurfaceView中的绘制区域,一般都是全部铺满。

    GLES20.glViewport(0, 0, width, height)
    
    • 在onDrawFrame中,就是真正实现绘制的地方了。该接口会不停的回调,刷新绘制区域。这里使用一个简单的三角形绘制来说明整个绘制流程。
    2、渲染一个简单的三角形

    先定义一个渲染接口类:

    interface IDrawer {
        fun draw()
        fun setTextureID(id: Int)
        fun release()
    }
    
    class TriangleDrawer(private val mTextureId: Int = -1): IDrawer {
        //顶点坐标
        private val mVertexCoors = floatArrayOf(
            -1f, -1f,
             1f, -1f,
             0f,  1f
        )
    
        //纹理坐标
        private val mTextureCoors = floatArrayOf(
            0f,   1f,
            1f,   1f,
            0.5f, 0f
        )
    
        //纹理ID
        private var mTextureId: Int = -1
    
        //OpenGL程序ID
        private var mProgram: Int = -1
    
        // 顶点坐标接收者
        private var mVertexPosHandler: Int = -1
        // 纹理坐标接收者
        private var mTexturePosHandler: Int = -1
    
        private lateinit var mVertexBuffer: FloatBuffer
        private lateinit var mTextureBuffer: FloatBuffer
    
        init {
            //【步骤1: 初始化顶点坐标】
            initPos()
        }
    
        private fun initPos() {
            val bb = ByteBuffer.allocateDirect(mVertexCoors.size * 4)
            bb.order(ByteOrder.nativeOrder())
            //将坐标数据转换为FloatBuffer,用以传入给OpenGL ES程序
            mVertexBuffer = bb.asFloatBuffer()
            mVertexBuffer.put(mVertexCoors)
            mVertexBuffer.position(0)
    
            val cc = ByteBuffer.allocateDirect(mTextureCoors.size * 4)
            cc.order(ByteOrder.nativeOrder())
            mTextureBuffer = cc.asFloatBuffer()
            mTextureBuffer.put(mTextureCoors)
            mTextureBuffer.position(0)
        }
    
        override fun setTextureID(id: Int) {
            mTextureId = id
        }
        
        override fun draw() {
            if (mTextureId != -1) {
                //【步骤2: 创建、编译并启动OpenGL着色器】
                createGLPrg()
                //【步骤3: 开始渲染绘制】
                doDraw()
            }
        }
    
        private fun createGLPrg() {
            if (mProgram == -1) {
                val vertexShader = loadShader(GLES20.GL_VERTEX_SHADER, getVertexShader())
                val fragmentShader = loadShader(GLES20.GL_FRAGMENT_SHADER, getFragmentShader())
    
                //创建OpenGL ES程序,注意:需要在OpenGL渲染线程中创建,否则无法渲染
                mProgram = GLES20.glCreateProgram()
                //将顶点着色器加入到程序
                GLES20.glAttachShader(mProgram, vertexShader)
                //将片元着色器加入到程序中
                GLES20.glAttachShader(mProgram, fragmentShader)
                //连接到着色器程序
                GLES20.glLinkProgram(mProgram)
    
                mVertexPosHandler = GLES20.glGetAttribLocation(mProgram, "aPosition")
                mTexturePosHandler = GLES20.glGetAttribLocation(mProgram, "aCoordinate")
            }
            //使用OpenGL程序
            GLES20.glUseProgram(mProgram)
        }
    
        private fun doDraw() {
            //启用顶点的句柄
            GLES20.glEnableVertexAttribArray(mVertexPosHandler)
            GLES20.glEnableVertexAttribArray(mTexturePosHandler)
            //设置着色器参数
            GLES20.glVertexAttribPointer(mVertexPosHandler, 2, GLES20.GL_FLOAT, false, 0, mVertexBuffer)
            GLES20.glVertexAttribPointer(mTexturePosHandler, 2, GLES20.GL_FLOAT, false, 0, mTextureBuffer)
            //开始绘制
            GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4)
        }
    
        override fun release() {
            GLES20.glDisableVertexAttribArray(mVertexPosHandler)
            GLES20.glDisableVertexAttribArray(mTexturePosHandler)
            GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, 0)
            GLES20.glDeleteTextures(1, intArrayOf(mTextureId), 0)
            GLES20.glDeleteProgram(mProgram)
        }
    
        private fun getVertexShader(): String {
            return "attribute vec4 aPosition;" +
                    "void main() {" +
                    "  gl_Position = aPosition;" +
                    "}"
        }
    
        private fun getFragmentShader(): String {
            return "precision mediump float;" +
                    "void main() {" +
                    "  gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);" +
                    "}"
        }
    
        private fun loadShader(type: Int, shaderCode: String): Int {
            //根据type创建顶点着色器或者片元着色器
            val shader = GLES20.glCreateShader(type)
            //将资源加入到着色器中,并编译
            GLES20.glShaderSource(shader, shaderCode)
            GLES20.glCompileShader(shader)
    
            return shader
        }
    }
    

    虽然只是画一个简单的三角形,代码依然看起来很复杂。这里把它拆解为三个步骤,就比较清晰明了了。

    1) 初始化顶点坐标

    前面我们讲到OpenGL的世界坐标和纹理坐标,在绘制前就需要先把这两个坐标确定好。

    【重要提示】

    有一点还没说的是,OpenGL ES所有的画面都是由三角形构成的,比如一个四边形由两个三角形构成,其他更复杂的图形也都可以分割为大大小小的三角形。

    因此,顶点坐标也是根据三角形的连接来设置的。其绘制方式有三种:

    • GL_TRIANGLES:独立顶点的构成三角形
    GL_TRIANGLES
    • GL_TRIANGLE_STRIP:复用顶点构成三角形
    GL_TRIANGLE_STRIP
    • GL_TRIANGLE_FAN:复用第一个顶点构成三角形
    GL_TRIANGLE_FAN

    通常情况下,一般使用GL_TRIANGLE_STRIP绘制模式。那么一个四边形的顶点顺序看起来是这样子的(v1-v2-v3)(v2-v3-v4)

    顶点坐标顺序

    对应的纹理坐标也要和顶点坐标顺序一致,否则会出现颠倒,变形等异常

    纹理坐标顺序

    由于绘制的是三角形,所以两个坐标如下(这里只设置xy轴坐标,忽略z轴坐标,每两个数据构成一个坐标点):

    //顶点坐标
    private val mVertexCoors = floatArrayOf(
        -1f, -1f,
         1f, -1f,
         0f,  1f
    )
    //纹理坐标
    private val mTextureCoors = floatArrayOf(
        0f,   1f,
        1f,   1f,
        0.5f, 0f
    )
    

    在initPos方法中,由于底层不能直接接收数组,所以将数组转换为ByteBuffer

    2) 创建、编译并启动OpenGL着色器

     private fun createGLPrg() {
        if (mProgram == -1) {
            val vertexShader = loadShader(GLES20.GL_VERTEX_SHADER, getVertexShader())
            val fragmentShader = loadShader(GLES20.GL_FRAGMENT_SHADER, getFragmentShader())
    
            //创建OpenGL ES程序,注意:需要在OpenGL渲染线程中创建,否则无法渲染
            mProgram = GLES20.glCreateProgram()
            //将顶点着色器加入到程序
            GLES20.glAttachShader(mProgram, vertexShader)
            //将片元着色器加入到程序中
            GLES20.glAttachShader(mProgram, fragmentShader)
            //连接到着色器程序
            GLES20.glLinkProgram(mProgram)
    
            mVertexPosHandler = GLES20.glGetAttribLocation(mProgram, "aPosition")
            mTexturePosHandler = GLES20.glGetAttribLocation(mProgram, "aCoordinate")
        }
        //使用OpenGL程序
        GLES20.glUseProgram(mProgram)
    }
    
    private fun getVertexShader(): String {
        return "attribute vec4 aPosition;" +
                "void main() {" +
                "  gl_Position = aPosition;" +
                "}"
    }
    
    private fun getFragmentShader(): String {
        return "precision mediump float;" +
                "void main() {" +
                "  gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);" +
                "}"
    }
    
    private fun loadShader(type: Int, shaderCode: String): Int {
        //根据type创建顶点着色器或者片元着色器
        val shader = GLES20.glCreateShader(type)
        //将资源加入到着色器中,并编译
        GLES20.glShaderSource(shader, shaderCode)
        GLES20.glCompileShader(shader)
    
        return shader
    }
    

    上面已经说过,GLSL是针对GPU的编程语言,而着色器就是一段小程序,为了能够运行这段小程序,需要先对其进行编译和绑定,才能使用。

    本例中的着色器就是上文提到的最简单的着色器。

    可以看到,着色器其实就是一段字符串

    进入loadShader中,通过GLES20.glCreateShader,根据不同类型,获取顶点着色器和片元着色器。

    然后调用以下方法,编译着色器

    GLES20.glShaderSource(shader, shaderCode)
    GLES20.glCompileShader(shader)
    

    编译好着色器以后,就是绑定,连接,启用程序即可。

    还记得上面说过,着色器中的坐标是由Java传递给GLSL吗?

    细心的你可能发现了这两句代码

    mVertexPosHandler = GLES20.glGetAttribLocation(mProgram, "aPosition")
    mTexturePosHandler = GLES20.glGetAttribLocation(mProgram, "aCoordinate")
    

    没错,这就是Java和GLSL交互的通道,通过属性可以给GLSL设置相关的值。

    3) 开始渲染绘制

    private fun doDraw() {
        //启用顶点的句柄
        GLES20.glEnableVertexAttribArray(mVertexPosHandler)
        GLES20.glEnableVertexAttribArray(mTexturePosHandler)
        //设置着色器参数, 第二个参数表示一个顶点包含的数据数量,这里为xy,所以为2
        GLES20.glVertexAttribPointer(mVertexPosHandler, 2, GLES20.GL_FLOAT, false, 0, mVertexBuffer)
        GLES20.glVertexAttribPointer(mTexturePosHandler, 2, GLES20.GL_FLOAT, false, 0, mTextureBuffer)
        //开始绘制
        GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 3)
    }
    

    首先激活着色器的顶点坐标和纹理坐标属性,然后把初始化好的坐标传递给着色器,最后启动绘制:

    GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 3)
    

    绘制有两种方式:glDrawArrays和glDrawElements,两者区别在于glDrawArrays是直接使用定义好的顶点顺序进行绘制;而glDrawElements则是需要定义另外的索引数组,来确认顶点的组合和绘制顺序。

    通过以上步骤,就可以在屏幕上看到一个红色的三角形了。

    三角形

    可能有人就有疑问了:绘制三角形的时候只是直接设置了像素点的颜色值,并没有用到纹理,纹理到底有什么用呢?

    接下来,就用纹理来显示一张图片,看看纹理到底怎么使用。

    建议先看清楚绘制三角形的流程,绘制图片就是基于以上流程,重复代码就不再贴出。

    3、纹理贴图,显示一张图片

    以下只贴出和绘制三角形不一样的部分代码,详细代码请看源码

    class BitmapDrawer(private val mTextureId: Int, private val mBitmap: Bitmap): IDrawer {
        //-------【注1:坐标变更了,由四个点组成一个四边形】-------
        // 顶点坐标
        private val mVertexCoors = floatArrayOf(
            -1f, -1f,
            1f, -1f,
            -1f, 1f,
            1f, 1f
        )
    
        // 纹理坐标
        private val mTextureCoors = floatArrayOf(
            0f, 1f,
            1f, 1f,
            0f, 0f,
            1f, 0f
        )
        
        //-------【注2:新增纹理接收者】-------
        // 纹理接收者
        private var mTextureHandler: Int = -1
    
        fun draw() {
            if (mTextureId != -1) {
                //【步骤2: 创建、编译并启动OpenGL着色器】
                createGLPrg()
                //-------【注4:新增两个步骤】-------
                //【步骤3: 激活并绑定纹理单元】
                activateTexture()
                //【步骤4: 绑定图片到纹理单元】
                bindBitmapToTexture()
                //----------------------------------
                //【步骤5: 开始渲染绘制】
                doDraw()
            }
        }
        
        private fun createGLPrg() {
            if (mProgram == -1) {
                //省略与绘制三角形一致的部分
                //......
            
                mVertexPosHandler = GLES20.glGetAttribLocation(mProgram, "aPosition")
                mTexturePosHandler = GLES20.glGetAttribLocation(mProgram, "aCoordinate")
                //【注3:新增获取纹理接收者】
                mTextureHandler = GLES20.glGetUniformLocation(mProgram, "uTexture")
            }
            //使用OpenGL程序
            GLES20.glUseProgram(mProgram)
        }
    
        private fun activateTexture() {
            //激活指定纹理单元
            GLES20.glActiveTexture(GLES20.GL_TEXTURE0)
            //绑定纹理ID到纹理单元
            GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, mTextureId)
            //将激活的纹理单元传递到着色器里面
            GLES20.glUniform1i(mTextureHandler, 0)
            //配置边缘过渡参数
            GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_LINEAR.toFloat())
            GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR.toFloat())
            GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_CLAMP_TO_EDGE)
            GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_CLAMP_TO_EDGE)
        }
    
        private fun bindBitmapToTexture() {
            if (!mBitmap.isRecycled) {
                //绑定图片到被激活的纹理单元
                GLUtils.texImage2D(GLES20.GL_TEXTURE_2D, 0, mBitmap, 0)
            }
        }
    
        private fun doDraw() {
            //省略与绘制三角形一致的部分
            //......
            
            //【注5:绘制顶点加1,变为4】
            //开始绘制:最后一个参数,将顶点数量改为4
            GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4)
        }
    
        private fun getVertexShader(): String {
            return "attribute vec4 aPosition;" +
                    "attribute vec2 aCoordinate;" +
                    "varying vec2 vCoordinate;" +
                    "void main() {" +
                    "  gl_Position = aPosition;" +
                    "  vCoordinate = aCoordinate;" +
                    "}"
        }
    
        private fun getFragmentShader(): String {
            return "precision mediump float;" +
                    "uniform sampler2D uTexture;" +
                    "varying vec2 vCoordinate;" +
                    "void main() {" +
                    "  vec4 color = texture2D(uTexture, vCoordinate);" +
                    "  gl_FragColor = color;" +
                    "}"
        }
        
        //省略和绘制三角形内容一致的部分
        //......
    }
    

    不一致的地方,代码中已经做了标识(见代码中的【注:x】)。逐个来看看:

    1)顶点坐标

    顶点坐标和纹理坐标由3个变成4个,组成一个长方形,组合方式也是GL_TRIANGLE_STRIP。

    2)着色器

    首先介绍一下GLSL中的限定符

    • attritude:一般用于各个顶点各不相同的量。如顶点颜色、坐标等。
    • uniform:一般用于对于3D物体中所有顶点都相同的量。比如光源位置,统一变换矩阵等。
    • varying:表示易变量,一般用于顶点着色器传递到片元着色器的量。
      const:常量。

    各行代码解析如下:

    private fun getVertexShader(): String {
        return  //顶点坐标
                "attribute vec2 aPosition;" +
                //纹理坐标
                "attribute vec2 aCoordinate;" +
                //用于传递纹理坐标给片元着色器,命名和片元着色器中的一致
                "varying vec2 vCoordinate;" +
                "void main() {" +
                "  gl_Position = aPosition;" +
                "  vCoordinate = aCoordinate;" +
                "}"
    }
    
    private fun getFragmentShader(): String {
        return  //配置float精度,使用了float数据一定要配置:lowp(低)/mediump(中)/highp(高)
                "precision mediump float;" +
                //从Java传递进入来的纹理单元
                "uniform sampler2D uTexture;" +
                //从顶点着色器传递进来的纹理坐标
                "varying vec2 vCoordinate;" +
                "void main() {" +
                //根据纹理坐标,从纹理单元中取色
                "  vec4 color = texture2D(uTexture, vCoordinate);" +
                "  gl_FragColor = color;" +
                "}"
    }
    

    绘制过程新增了两个步骤:

    3)激活并绑定纹理单元

    private fun activateTexture() {
        //激活指定纹理单元
        GLES20.glActiveTexture(GLES20.GL_TEXTURE0)
        //绑定纹理ID到纹理单元
        GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, mTextureId)
        //将激活的纹理单元传递到着色器里面
        GLES20.glUniform1i(mTextureHandler, 0)
        //配置纹理过滤模式
        GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_LINEAR.toFloat())
        GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR.toFloat())
        //配置纹理环绕方式
        GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_CLAMP_TO_EDGE)
        GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_CLAMP_TO_EDGE)
    }
    

    由于显示图片需要用到纹理单元来传递整张图片的内容,所以首先需要激活一个纹理单元。

    为什么说是一个纹理单元?
    因为OpenGL ES中内置了很多个纹理单元,并且是连续,比如GLES20.GL_TEXTURE0,GLES20.GL_TEXTURE1,GLES20.GL_TEXTURE3...可以选择其中一个,一般默认选第一个GLES20.GL_TEXTURE0,并且OpenGL默认激活的就是第一个纹理单元。
    另外,纹理单元GLES20.GL_TEXTURE1 = GLES20.GL_TEXTURE0 + 1,以此类推。

    激活指定的纹理单元后,需要把它和纹理ID做绑定,并且在传递到着色器中的时候:GLES20.glUniform1i(mTextureHandler, 0),第二个参数索引需要和纹理单元索引保持一致。

    到这里,可以发现,OpenGL方法的命名都是比较规律的,比如GLES20.glUniform1i对应的是GLSL中的uniform限定符变量;ES20.glGetAttribLocation对应GLSL中的attribute限定符变量等等

    最后四行代码,用于配置纹理过滤模式和纹理环绕方式(对于这两个模式的介绍引用自【LearnOpenGL-CN】)

    • 纹理过滤模式

    纹理坐标不依赖于分辨率,它可以是任意浮点值,所以OpenGL需要知道怎样将纹理像素映射到纹理坐标。

    一般使用这两个模式:GL_NEAREST(邻近过滤)、GL_LINEAR(线性过滤)

    当设置为GL_NEAREST的时候,OpenGL会选择中心点最接近纹理坐标的那个像素。

    当设置为GL_LINEAR的时候,它会基于纹理坐标附近的纹理像素,计算出一个插值,近似出这些纹理像素之间的颜色。

    来源LearnOpenGL-CN
    • 纹理环绕方式
    环绕方式 描述
    GL_REPEAT 对纹理的默认行为。重复纹理图像。
    GL_MIRRORED_REPEAT 和GL_REPEAT一样,但每次重复图片是镜像放置的。
    GL_CLAMP_TO_EDGE 纹理坐标会被约束在0到1之间,超出的部分会重复纹理坐标的边缘,产生一种边缘被拉伸的效果。
    GL_CLAMP_TO_BORDER 超出的坐标为用户指定的边缘颜色。
    来源LearnOpenGL-CN

    4)绑定图片到纹理单元

    激活了纹理单元以后,调用texImage2D方法,就可以把bmp绑定到指定的纹理单元上面了。

    GLUtils.texImage2D(GLES20.GL_TEXTURE_2D, 0, mBitmap, 0)
    

    5)绘制

    绘制的时候,最后一句的最后一个参数由三角形的3个顶点变成为长方形的4个顶点。如果还是填入3,你会发现会显示图片的一半,即三角形(对角线分割开)。

    GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4)
    

    至此,一张图片就通过纹理贴图显示出来了。

    纹理贴图

    当然,你会发现,这张图片是变形的,铺满整个GLSurfaceView窗口了。这里就涉及到了顶点坐标变换的问题了,将在下一篇文章中具体讲解。

    五、总结

    经过上面简单的绘制三角形和纹理贴图,可以总结出Android中OpenGL ES的2D绘制流程:

    1. 通过GLSurfaceView配置OpenGL ES版本,指定Render
    2. 实现GLSurfaceView.Renderer,复写暴露的方法,并配置OpenGL显示窗口,清屏
    3. 创建纹理ID
    4. 配置好顶点坐标和纹理坐标
    5. 初始化坐标变换矩阵
    6. 初始化OpenGL程序,并编译、链接顶点着色和片段着色器,获取GLSL中的变量属性
    7. 激活纹理单元,绑定纹理ID,配置纹理过滤模式和环绕方式
    8. 绑定纹理(如将bitmap绑定给纹理)
    9. 启动绘制

    以上基本是一个通用的流程,当然渲染图片和渲染视频稍有不同,以及第5点,都将在下一篇说到。

    六、参考文章

    了解OpenGLES2.0

    着色器语言GLSL

    LearnOpenGL-CN

    相关文章

      网友评论

        本文标题:【Android 音视频开发打怪升级:OpenGL渲染视频画面篇

        本文链接:https://www.haomeiwen.com/subject/yghqmctx.html