【Android 音視頻開發打怪升級:OpenGL渲染視頻畫面篇】1、初步瞭解OpenGL ES

【聲 明】

首先,這一系列文章均基於本身的理解和實踐,可能有不對的地方,歡迎你們指正。
其次,這是一個入門系列,涉及的知識也僅限於夠用,深刻的知識網上也有許許多多的博文供你們學習了。
最後,寫文章過程當中,會借鑑參考其餘人分享的文章,會在文章最後列出,感謝這些做者的分享。php

碼字不易,轉載請註明出處!android

教程代碼:【Github傳送門

目錄

1、Android音視頻硬解碼篇:
2、使用OpenGL渲染視頻畫面篇
  • 1,初步瞭解OpenGL ES
  • 2,使用OpenGL渲染視頻畫面
  • 3,OpenGL渲染多視頻,實現畫中畫
  • 4,深刻了解OpenGL之EGL
  • 5,OpenGL FBO數據緩衝區
  • 6,Android音視頻硬編碼:生成一個MP4
3、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相關的基礎知識,包括座標系、着色器、基本渲染流程等。git

一 簡介

提到OpenGL,想必不少人都會說,我知道這個東西,能夠用來渲染2D畫面和3D模型,同時又會說,OpenGL很難、很高級,不知道怎麼用。github

一、爲何OpenGL「感受很難」?編程

  • 函數多且雜,渲染流程複雜
  • GLSL着色器語言很差理解
  • 面向過程的編程思惟,和Java等面向對象的編程思惟不一樣

二、OpenGL ES是什麼?小程序

爲了解決以上問題,讓OpenGL「學起來不是很難」,須要把其分解成一些簡單的步驟,而後簡單的東西串聯起來,一切就水到渠成了。數組

首先,來看看什麼是OpenGL。框架

  • CPU和GPU

在手機上,有兩大元件,一個是CPU,一個是GPU。而手機上顯示圖形界面也有兩種方式,一個是使用CPU來渲染,一個是使用GPU來渲染,能夠說,GPU渲染實際上是一種硬件加速。編程語言

爲何GPU能夠大大提升渲染速度,由於GPU最擅長的是並行浮點運算,能夠用來對許許多多的像素作並行運算。ide

OpenGL(Open Graphics Library)則是間接操做GPU的工具,是一組定義好的跨平臺和跨語言的圖形API,是可用於2D和3D畫面渲染的底層圖形庫,是由各個硬件廠家具體實現的編程接口。

  • OpenGL 與 OpenGL ES

OpenGL ES 全稱:OpenGL for Embedded Systems,是OpenGL 的子集,是針對手機 PAD等小型設備設計的,刪減了沒必要須的方法、數據類型、功能,減小了體積,優化了效率。

三、 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 目前支持最普遍的版本,後續主要以該版本爲主,進行介紹和代碼編寫。

2、OpenGL ES座標系

在音視頻開發中,涉及到的座標系主要有兩個:世界座標和紋理座標。

因爲基本不涉及3D貼圖,因此只看x/y軸座標,忽略z軸座標,涉及到3D相關知識可自行Google,不在討論範圍內。

首先來看兩個圖:

世界座標

紋理座標

  • OpenGL ES世界座標

經過名字就能夠知道,這是OpenGL本身世界的座標,是一個標準化座標系,範圍是 -1 ~ 1,原點在中間。

  • OpenGL ES紋理座標

紋理座標,其實就是屏幕座標,標準的紋理座標原點是在屏幕的左下方,而Android系統座標系的原點是在左上方的。這是Android使用OpenGL須要注意的一個地方。

紋理座標的範圍是 0 ~ 1。

注:座標系的xy軸方向很重要,決定了如何作頂點座標和紋理座標映射。

那麼,這兩個座標系究竟有什麼關係呢?

世界座標,是用於顯示的座標,即像素點應該顯示在哪一個位置由世界座標決定。

紋理座標,表示世界座標指定的位置點想要顯示的顏色,應該在紋理上的哪一個位置獲取。即顏色所在的位置由紋理座標決定。

二者之間須要作正確的映射,才能正常的顯示一張畫面。

3、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】,很是詳盡。

4、Android OpenGL ES渲染流程

OpenGL的渲染流程說實話是比較繁瑣的,也是讓不少人望而生畏的地方,可是,若是歸結起來,其實整個渲染流程基本是固定的,只要把它按照固定的流程封裝好,其實並無那麼複雜。

接下來,就進入實戰,一層一層扒開OpengGL的神祕面紗。

一、初始化

在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中,就是真正實現繪製的地方了。該接口會不停的回調,刷新繪製區域。這裏使用一個簡單的三角形繪製來講明整個繪製流程。
二、渲染一個簡單的三角形

先定義一個渲染接口類:

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則是須要定義另外的索引數組,來確認頂點的組合和繪製順序。

經過以上步驟,就能夠在屏幕上看到一個紅色的三角形了。

三角形

可能有人就有疑問了:繪製三角形的時候只是直接設置了像素點的顏色值,並無用到紋理,紋理到底有什麼用呢?

接下來,就用紋理來顯示一張圖片,看看紋理到底怎麼使用。

建議先看清楚繪製三角形的流程,繪製圖片就是基於以上流程,重複代碼就再也不貼出。

三、紋理貼圖,顯示一張圖片

如下只貼出和繪製三角形不同的部分代碼,詳細代碼請看源碼

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窗口了。這裏就涉及到了頂點座標變換的問題了,將在下一篇文章中具體講解。

5、總結

通過上面簡單的繪製三角形和紋理貼圖,能夠總結出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點,都將在下一篇說到。

6、參考文章

瞭解OpenGLES2.0

着色器語言GLSL

LearnOpenGL-CN

相關文章
相關標籤/搜索