【Android 音視頻開發(fā)打怪升級:OpenGL渲染視頻畫面篇】一、初步了解OpenGL ES

【聲 明】

首先,這一系列文章均基于自己的理解和實(shí)踐,可能有不對的地方,歡迎大家指正。
其次,這是一個入門系列,涉及的知識也僅限于夠用,深入的知識網(wǎng)上也有許許多多的博文供大家學(xué)習(xí)了。
最后,寫文章過程中,會借鑒參考其他人分享的文章,會在文章最后列出,感謝這些作者的分享。

碼字不易,轉(zhuǎn)載請注明出處!

教程代碼:【Github傳送門

目錄

一、Android音視頻硬解碼篇:
二、使用OpenGL渲染視頻畫面篇
三、Android FFmpeg音視頻解碼篇
  • 1,F(xiàn)Fmpeg 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相關(guān)的基礎(chǔ)知識,包括坐標(biāo)系、著色器、基本渲染流程等。

一 簡介

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

1、為什么OpenGL“感覺很難”?

  • 函數(shù)多且雜,渲染流程復(fù)雜
  • GLSL著色器語言不好理解
  • 面向過程的編程思維,和Java等面向?qū)ο蟮木幊趟季S不同

2、OpenGL ES是什么?

為了解決以上問題,讓OpenGL“學(xué)起來不是很難”,需要把其分解成一些簡單的步驟,然后簡單的東西串聯(lián)起來,一切就水到渠成了。

首先,來看看什么是OpenGL。

  • CPU和GPU

在手機(jī)上,有兩大元件,一個是CPU,一個是GPU。而手機(jī)上顯示圖形界面也有兩種方式,一個是使用CPU來渲染,一個是使用GPU來渲染,可以說,GPU渲染其實(shí)是一種硬件加速。

為什么GPU可以大大提高渲染速度,因?yàn)镚PU最擅長的是并行浮點(diǎn)運(yùn)算,可以用來對許許多多的像素做并行運(yùn)算。

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

  • OpenGL 與 OpenGL ES

OpenGL ES 全稱:OpenGL for Embedded Systems,是OpenGL 的子集,是針對手機(jī) PAD等小型設(shè)備設(shè)計(jì)的,刪減了不必須的方法、數(shù)據(jù)類型、功能,減少了體積,優(yōu)化了效率。

3、 OpenGL ES版本

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

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

2.0 版本是 Android 目前支持最廣泛的版本,后續(xù)主要以該版本為主,進(jìn)行介紹和代碼編寫。

二、OpenGL ES坐標(biāo)系

在音視頻開發(fā)中,涉及到的坐標(biāo)系主要有兩個:世界坐標(biāo)和紋理坐標(biāo)。

由于基本不涉及3D貼圖,所以只看x/y軸坐標(biāo),忽略z軸坐標(biāo),涉及到3D相關(guān)知識可自行Google,不在討論范圍內(nèi)。

首先來看兩個圖:

世界坐標(biāo)
紋理坐標(biāo)
  • OpenGL ES世界坐標(biāo)

通過名字就可以知道,這是OpenGL自己世界的坐標(biāo),是一個標(biāo)準(zhǔn)化坐標(biāo)系,范圍是 -1 ~ 1,原點(diǎn)在中間。

  • OpenGL ES紋理坐標(biāo)

紋理坐標(biāo),其實(shí)就是屏幕坐標(biāo),標(biāo)準(zhǔn)的紋理坐標(biāo)原點(diǎn)是在屏幕的左下方,而Android系統(tǒng)坐標(biāo)系的原點(diǎn)是在左上方的。這是Android使用OpenGL需要注意的一個地方。

紋理坐標(biāo)的范圍是 0 ~ 1。

注:坐標(biāo)系的xy軸方向很重要,決定了如何做頂點(diǎn)坐標(biāo)和紋理坐標(biāo)映射。

那么,這兩個坐標(biāo)系究竟有什么關(guān)系呢?

世界坐標(biāo),是用于顯示的坐標(biāo),即像素點(diǎn)應(yīng)該顯示在哪個位置由世界坐標(biāo)決定。

紋理坐標(biāo),表示世界坐標(biāo)指定的位置點(diǎn)想要顯示的顏色,應(yīng)該在紋理上的哪個位置獲取。即顏色所在的位置由紋理坐標(biāo)決定。

兩者之間需要做正確的映射,才能正常的顯示一張畫面。

三、OpenGL 著色器語言 GLSL

在OpenGL 2.0以后,加入了新的可編程渲染管線,可以更加靈活的控制渲染。但也因此需要學(xué)習(xí)多一門針對GPU的編程語言,語法與C語言類似,名為GLSL。

  • 頂點(diǎn)著色器 & 片元著色器

在介紹GLSL之前,先來看兩個比較陌生的名詞:頂點(diǎn)著色器和片元著色器。

著色器,是一種可運(yùn)行在GPU上的小程序,用GLSL語言編寫。從命名上,頂點(diǎn)著色器是用于操控頂點(diǎn)的程序,而片元著色器是用于操控像素顏色屬性的程序。

簡單理解:其實(shí)就是對應(yīng)了以上兩個坐標(biāo)系:頂點(diǎn)著色器對應(yīng)世界坐標(biāo),片元著色器對應(yīng)紋理坐標(biāo)。

畫面上的每個點(diǎn),都會執(zhí)行一次頂點(diǎn)和片元著色器中的程序片段,并且是并行執(zhí)行,最后渲染到屏幕上。

  • GLSL編程

下面,通過一個最簡單的頂點(diǎn)著色器和片元著色器來簡單介紹一下GLSL語言

#頂點(diǎn)著色器

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函數(shù)。在著色器中,有幾個內(nèi)建的變量,可以直接使用(這里只列出音視頻開發(fā)常用的,還有其他的一些3D開發(fā)會用到的):

  • 頂點(diǎn)著色器的內(nèi)建輸入變量

gl_Position:頂點(diǎn)坐標(biāo)
gl_PointSize:點(diǎn)的大小,沒有賦值則為默認(rèn)值1

  • 片元著色器內(nèi)建輸出變量

gl_FragColor:當(dāng)前片元顏色

看回上面的著色器代碼。

1)在頂點(diǎn)著色器中,傳入了一個vec4的頂點(diǎn)坐標(biāo)xyzw,然后直接傳遞給內(nèi)建變量gl_Position,即直接根據(jù)頂點(diǎn)坐標(biāo)渲染,不再做位置變換。

注:頂點(diǎn)坐標(biāo)是在Java代碼中傳入的,后面會講到,另外w是齊次坐標(biāo),2D渲染沒有作用

2)在片元著色器中,直接給gl_FragColor賦值,依然是一個vec4類型的數(shù)據(jù),這里表示rgba顏色值,為紅色。

可以看到vec4是一個4維向量,可用于表示坐標(biāo)xyzw,也可用表示rgba,當(dāng)然還有vec3,vec2等,可以參考這篇文章:著色器語言GLSL,講的非常詳細(xì),建議看看。

這樣,兩個簡單的著色器串聯(lián)起來后,每一個頂點(diǎn)(像素)都會顯示一個紅點(diǎn),最后屏幕會顯示一個紅色的畫面。

具體GLSL關(guān)于數(shù)據(jù)類型和語法不再展開介紹,后面涉及到的GLSL代碼會做更深入的講解。更詳細(xì)的可以參考這位作者的文章【著色器語言GLSL】,非常詳盡。

四、Android OpenGL ES渲染流程

OpenGL的渲染流程說實(shí)話是比較繁瑣的,也是讓很多人望而生畏的地方,但是,如果歸結(jié)起來,其實(shí)整個渲染流程基本是固定的,只要把它按照固定的流程封裝好,其實(shí)并沒有那么復(fù)雜。

接下來,就進(jìn)入實(shí)戰(zhàn),一層一層扒開OpengGL的神秘面紗。

1、初始化

在Android中,OpenGL通常配合GLSurfaceView使用,在GLSurfraceView中,Google已經(jīng)封裝好了渲染的基礎(chǔ)流程。

這里需要單獨(dú)強(qiáng)調(diào)一下,OpenGL是基于線程的一個狀態(tài)機(jī),有關(guān)OpenGL的操作,比如創(chuàng)建紋理ID,初始化,渲染等,都必須要在同一個線程中完成,否則會造成異常。

通常開發(fā)者在剛剛接觸OpenGL的時候并不能深刻體會到這種機(jī)制,原因是Google在GLSurfaceView中已經(jīng)幫開發(fā)者做了這部分的內(nèi)容。這是OpenGL非常重要的一個方面,在后續(xù)的有關(guān)EGL的文章中會繼續(xù)深入了解到。

  1. 新建頁面
class SimpleRenderActivity : AppCompatActivity() {
    //自定義的OpenGL渲染器,詳情請繼續(xù)往下看
    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,初始化的時候,設(shè)置了OpenGL使用的版本為2.0,然后配置了渲染器SimpleRender,繼承自GLSurfaceView.Renderer

IDrawer將在繪制三角形的時候具體講解,定義該接口類只是為了方便拓展,也可以直接將渲染代碼寫在SimpleRender中。

  1. 實(shí)現(xiàn)渲染接口
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()
    }
}

注意到,實(shí)現(xiàn)了三個回調(diào)接口,這三個接口就是Google封裝好的流程中,暴露出來的接口,留給給開發(fā)者實(shí)現(xiàn)初始化和渲染,并且這三個接口的回調(diào)都在同一個線程中。

  • 在onSurfaceCreated中,調(diào)用了兩句OpenGL ES的代碼實(shí)現(xiàn)清屏,清屏顏色為黑色。
GLES20.glClearColor(0f, 0f, 0f, 0f)
GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT)

同時,創(chuàng)建了一個紋理ID,并設(shè)置給Drawer,如下:

fun createTextureIds(count: Int): IntArray {
    val texture = IntArray(count)
    GLES20.glGenTextures(count, texture, 0) //生成紋理
    return texture
}
  • 在onSurfaceChanged中,調(diào)用glViewport,設(shè)置了OpenGL繪制的區(qū)域?qū)捀吆臀恢?/li>

這里所說的繪制區(qū)域,是指OpenGL在GLSurfaceView中的繪制區(qū)域,一般都是全部鋪滿。

GLES20.glViewport(0, 0, width, height)
  • 在onDrawFrame中,就是真正實(shí)現(xiàn)繪制的地方了。該接口會不停的回調(diào),刷新繪制區(qū)域。這里使用一個簡單的三角形繪制來說明整個繪制流程。
2、渲染一個簡單的三角形

先定義一個渲染接口類:

interface IDrawer {
    fun draw()
    fun setTextureID(id: Int)
    fun release()
}
class TriangleDrawer(private val mTextureId: Int = -1): IDrawer {
    //頂點(diǎn)坐標(biāo)
    private val mVertexCoors = floatArrayOf(
        -1f, -1f,
         1f, -1f,
         0f,  1f
    )

    //紋理坐標(biāo)
    private val mTextureCoors = floatArrayOf(
        0f,   1f,
        1f,   1f,
        0.5f, 0f
    )

    //紋理ID
    private var mTextureId: Int = -1

    //OpenGL程序ID
    private var mProgram: Int = -1

    // 頂點(diǎn)坐標(biāo)接收者
    private var mVertexPosHandler: Int = -1
    // 紋理坐標(biāo)接收者
    private var mTexturePosHandler: Int = -1

    private lateinit var mVertexBuffer: FloatBuffer
    private lateinit var mTextureBuffer: FloatBuffer

    init {
        //【步驟1: 初始化頂點(diǎn)坐標(biāo)】
        initPos()
    }

    private fun initPos() {
        val bb = ByteBuffer.allocateDirect(mVertexCoors.size * 4)
        bb.order(ByteOrder.nativeOrder())
        //將坐標(biāo)數(shù)據(jù)轉(zhuǎn)換為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: 創(chuàng)建、編譯并啟動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())

            //創(chuàng)建OpenGL ES程序,注意:需要在OpenGL渲染線程中創(chuàng)建,否則無法渲染
            mProgram = GLES20.glCreateProgram()
            //將頂點(diǎn)著色器加入到程序
            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() {
        //啟用頂點(diǎn)的句柄
        GLES20.glEnableVertexAttribArray(mVertexPosHandler)
        GLES20.glEnableVertexAttribArray(mTexturePosHandler)
        //設(shè)置著色器參數(shù)
        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 {
        //根據(jù)type創(chuàng)建頂點(diǎn)著色器或者片元著色器
        val shader = GLES20.glCreateShader(type)
        //將資源加入到著色器中,并編譯
        GLES20.glShaderSource(shader, shaderCode)
        GLES20.glCompileShader(shader)

        return shader
    }
}

雖然只是畫一個簡單的三角形,代碼依然看起來很復(fù)雜。這里把它拆解為三個步驟,就比較清晰明了了。

1) 初始化頂點(diǎn)坐標(biāo)

前面我們講到OpenGL的世界坐標(biāo)和紋理坐標(biāo),在繪制前就需要先把這兩個坐標(biāo)確定好。

【重要提示】

有一點(diǎn)還沒說的是,OpenGL ES所有的畫面都是由三角形構(gòu)成的,比如一個四邊形由兩個三角形構(gòu)成,其他更復(fù)雜的圖形也都可以分割為大大小小的三角形。

因此,頂點(diǎn)坐標(biāo)也是根據(jù)三角形的連接來設(shè)置的。其繪制方式有三種:

  • GL_TRIANGLES:獨(dú)立頂點(diǎn)的構(gòu)成三角形
GL_TRIANGLES
  • GL_TRIANGLE_STRIP:復(fù)用頂點(diǎn)構(gòu)成三角形
GL_TRIANGLE_STRIP
  • GL_TRIANGLE_FAN:復(fù)用第一個頂點(diǎn)構(gòu)成三角形
GL_TRIANGLE_FAN

通常情況下,一般使用GL_TRIANGLE_STRIP繪制模式。那么一個四邊形的頂點(diǎn)順序看起來是這樣子的(v1-v2-v3)(v2-v3-v4)

頂點(diǎn)坐標(biāo)順序

對應(yīng)的紋理坐標(biāo)也要和頂點(diǎn)坐標(biāo)順序一致,否則會出現(xiàn)顛倒,變形等異常

紋理坐標(biāo)順序

由于繪制的是三角形,所以兩個坐標(biāo)如下(這里只設(shè)置xy軸坐標(biāo),忽略z軸坐標(biāo),每兩個數(shù)據(jù)構(gòu)成一個坐標(biāo)點(diǎn)):

//頂點(diǎn)坐標(biāo)
private val mVertexCoors = floatArrayOf(
    -1f, -1f,
     1f, -1f,
     0f,  1f
)
//紋理坐標(biāo)
private val mTextureCoors = floatArrayOf(
    0f,   1f,
    1f,   1f,
    0.5f, 0f
)

在initPos方法中,由于底層不能直接接收數(shù)組,所以將數(shù)組轉(zhuǎn)換為ByteBuffer

2) 創(chuàng)建、編譯并啟動OpenGL著色器

 private fun createGLPrg() {
    if (mProgram == -1) {
        val vertexShader = loadShader(GLES20.GL_VERTEX_SHADER, getVertexShader())
        val fragmentShader = loadShader(GLES20.GL_FRAGMENT_SHADER, getFragmentShader())

        //創(chuàng)建OpenGL ES程序,注意:需要在OpenGL渲染線程中創(chuàng)建,否則無法渲染
        mProgram = GLES20.glCreateProgram()
        //將頂點(diǎn)著色器加入到程序
        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 {
    //根據(jù)type創(chuàng)建頂點(diǎn)著色器或者片元著色器
    val shader = GLES20.glCreateShader(type)
    //將資源加入到著色器中,并編譯
    GLES20.glShaderSource(shader, shaderCode)
    GLES20.glCompileShader(shader)

    return shader
}

上面已經(jīng)說過,GLSL是針對GPU的編程語言,而著色器就是一段小程序,為了能夠運(yùn)行這段小程序,需要先對其進(jìn)行編譯和綁定,才能使用。

本例中的著色器就是上文提到的最簡單的著色器。

可以看到,著色器其實(shí)就是一段字符串

進(jìn)入loadShader中,通過GLES20.glCreateShader,根據(jù)不同類型,獲取頂點(diǎn)著色器和片元著色器。

然后調(diào)用以下方法,編譯著色器

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

編譯好著色器以后,就是綁定,連接,啟用程序即可。

還記得上面說過,著色器中的坐標(biāo)是由Java傳遞給GLSL嗎?

細(xì)心的你可能發(fā)現(xiàn)了這兩句代碼

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

沒錯,這就是Java和GLSL交互的通道,通過屬性可以給GLSL設(shè)置相關(guān)的值。

3) 開始渲染繪制

private fun doDraw() {
    //啟用頂點(diǎn)的句柄
    GLES20.glEnableVertexAttribArray(mVertexPosHandler)
    GLES20.glEnableVertexAttribArray(mTexturePosHandler)
    //設(shè)置著色器參數(shù), 第二個參數(shù)表示一個頂點(diǎn)包含的數(shù)據(jù)數(shù)量,這里為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)
}

首先激活著色器的頂點(diǎn)坐標(biāo)和紋理坐標(biāo)屬性,然后把初始化好的坐標(biāo)傳遞給著色器,最后啟動繪制:

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

繪制有兩種方式:glDrawArrays和glDrawElements,兩者區(qū)別在于glDrawArrays是直接使用定義好的頂點(diǎn)順序進(jìn)行繪制;而glDrawElements則是需要定義另外的索引數(shù)組,來確認(rèn)頂點(diǎn)的組合和繪制順序。

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

三角形

可能有人就有疑問了:繪制三角形的時候只是直接設(shè)置了像素點(diǎn)的顏色值,并沒有用到紋理,紋理到底有什么用呢?

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

建議先看清楚繪制三角形的流程,繪制圖片就是基于以上流程,重復(fù)代碼就不再貼出。

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

以下只貼出和繪制三角形不一樣的部分代碼,詳細(xì)代碼請看源碼

class BitmapDrawer(private val mTextureId: Int, private val mBitmap: Bitmap): IDrawer {
    //-------【注1:坐標(biāo)變更了,由四個點(diǎn)組成一個四邊形】-------
    // 頂點(diǎn)坐標(biāo)
    private val mVertexCoors = floatArrayOf(
        -1f, -1f,
        1f, -1f,
        -1f, 1f,
        1f, 1f
    )

    // 紋理坐標(biāo)
    private val mTextureCoors = floatArrayOf(
        0f, 1f,
        1f, 1f,
        0f, 0f,
        1f, 0f
    )
    
    //-------【注2:新增紋理接收者】-------
    // 紋理接收者
    private var mTextureHandler: Int = -1

    fun draw() {
        if (mTextureId != -1) {
            //【步驟2: 創(chuàng)建、編譯并啟動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)
        //配置邊緣過渡參數(shù)
        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:繪制頂點(diǎn)加1,變?yōu)?】
        //開始繪制:最后一個參數(shù),將頂點(diǎn)數(shù)量改為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;" +
                "}"
    }
    
    //省略和繪制三角形內(nèi)容一致的部分
    //......
}

不一致的地方,代碼中已經(jīng)做了標(biāo)識(見代碼中的【注:x】)。逐個來看看:

1)頂點(diǎn)坐標(biāo)

頂點(diǎn)坐標(biāo)和紋理坐標(biāo)由3個變成4個,組成一個長方形,組合方式也是GL_TRIANGLE_STRIP。

2)著色器

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

  • attritude:一般用于各個頂點(diǎn)各不相同的量。如頂點(diǎn)顏色、坐標(biāo)等。
  • uniform:一般用于對于3D物體中所有頂點(diǎn)都相同的量。比如光源位置,統(tǒng)一變換矩陣等。
  • varying:表示易變量,一般用于頂點(diǎn)著色器傳遞到片元著色器的量。
    const:常量。

各行代碼解析如下:

private fun getVertexShader(): String {
    return  //頂點(diǎn)坐標(biāo)
            "attribute vec2 aPosition;" +
            //紋理坐標(biāo)
            "attribute vec2 aCoordinate;" +
            //用于傳遞紋理坐標(biāo)給片元著色器,命名和片元著色器中的一致
            "varying vec2 vCoordinate;" +
            "void main() {" +
            "  gl_Position = aPosition;" +
            "  vCoordinate = aCoordinate;" +
            "}"
}

private fun getFragmentShader(): String {
    return  //配置float精度,使用了float數(shù)據(jù)一定要配置:lowp(低)/mediump(中)/highp(高)
            "precision mediump float;" +
            //從Java傳遞進(jìn)入來的紋理單元
            "uniform sampler2D uTexture;" +
            //從頂點(diǎn)著色器傳遞進(jìn)來的紋理坐標(biāo)
            "varying vec2 vCoordinate;" +
            "void main() {" +
            //根據(jù)紋理坐標(biāo),從紋理單元中取色
            "  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())
    //配置紋理環(huán)繞方式
    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)
}

由于顯示圖片需要用到紋理單元來傳遞整張圖片的內(nèi)容,所以首先需要激活一個紋理單元。

為什么說是一個紋理單元?
因?yàn)镺penGL ES中內(nèi)置了很多個紋理單元,并且是連續(xù),比如GLES20.GL_TEXTURE0,GLES20.GL_TEXTURE1,GLES20.GL_TEXTURE3...可以選擇其中一個,一般默認(rèn)選第一個GLES20.GL_TEXTURE0,并且OpenGL默認(rèn)激活的就是第一個紋理單元。
另外,紋理單元GLES20.GL_TEXTURE1 = GLES20.GL_TEXTURE0 + 1,以此類推。

激活指定的紋理單元后,需要把它和紋理ID做綁定,并且在傳遞到著色器中的時候:GLES20.glUniform1i(mTextureHandler, 0),第二個參數(shù)索引需要和紋理單元索引保持一致。

到這里,可以發(fā)現(xiàn),OpenGL方法的命名都是比較規(guī)律的,比如GLES20.glUniform1i對應(yīng)的是GLSL中的uniform限定符變量;ES20.glGetAttribLocation對應(yīng)GLSL中的attribute限定符變量等等

最后四行代碼,用于配置紋理過濾模式和紋理環(huán)繞方式(對于這兩個模式的介紹引用自【LearnOpenGL-CN】)

  • 紋理過濾模式

紋理坐標(biāo)不依賴于分辨率,它可以是任意浮點(diǎn)值,所以O(shè)penGL需要知道怎樣將紋理像素映射到紋理坐標(biāo)。

一般使用這兩個模式:GL_NEAREST(鄰近過濾)、GL_LINEAR(線性過濾)

當(dāng)設(shè)置為GL_NEAREST的時候,OpenGL會選擇中心點(diǎn)最接近紋理坐標(biāo)的那個像素。

當(dāng)設(shè)置為GL_LINEAR的時候,它會基于紋理坐標(biāo)附近的紋理像素,計(jì)算出一個插值,近似出這些紋理像素之間的顏色。

來源LearnOpenGL-CN
  • 紋理環(huán)繞方式
環(huán)繞方式 描述
GL_REPEAT 對紋理的默認(rèn)行為。重復(fù)紋理圖像。
GL_MIRRORED_REPEAT 和GL_REPEAT一樣,但每次重復(fù)圖片是鏡像放置的。
GL_CLAMP_TO_EDGE 紋理坐標(biāo)會被約束在0到1之間,超出的部分會重復(fù)紋理坐標(biāo)的邊緣,產(chǎn)生一種邊緣被拉伸的效果。
GL_CLAMP_TO_BORDER 超出的坐標(biāo)為用戶指定的邊緣顏色。
來源LearnOpenGL-CN

4)綁定圖片到紋理單元

激活了紋理單元以后,調(diào)用texImage2D方法,就可以把bmp綁定到指定的紋理單元上面了。

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

5)繪制

繪制的時候,最后一句的最后一個參數(shù)由三角形的3個頂點(diǎn)變成為長方形的4個頂點(diǎn)。如果還是填入3,你會發(fā)現(xiàn)會顯示圖片的一半,即三角形(對角線分割開)。

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

至此,一張圖片就通過紋理貼圖顯示出來了。

紋理貼圖

當(dāng)然,你會發(fā)現(xiàn),這張圖片是變形的,鋪滿整個GLSurfaceView窗口了。這里就涉及到了頂點(diǎn)坐標(biāo)變換的問題了,將在下一篇文章中具體講解。

五、總結(jié)

經(jīng)過上面簡單的繪制三角形和紋理貼圖,可以總結(jié)出Android中OpenGL ES的2D繪制流程:

  1. 通過GLSurfaceView配置OpenGL ES版本,指定Render
  2. 實(shí)現(xiàn)GLSurfaceView.Renderer,復(fù)寫暴露的方法,并配置OpenGL顯示窗口,清屏
  3. 創(chuàng)建紋理ID
  4. 配置好頂點(diǎn)坐標(biāo)和紋理坐標(biāo)
  5. 初始化坐標(biāo)變換矩陣
  6. 初始化OpenGL程序,并編譯、鏈接頂點(diǎn)著色和片段著色器,獲取GLSL中的變量屬性
  7. 激活紋理單元,綁定紋理ID,配置紋理過濾模式和環(huán)繞方式
  8. 綁定紋理(如將bitmap綁定給紋理)
  9. 啟動繪制

以上基本是一個通用的流程,當(dāng)然渲染圖片和渲染視頻稍有不同,以及第5點(diǎn),都將在下一篇說到。

六、參考文章

了解OpenGLES2.0

著色器語言GLSL

LearnOpenGL-CN

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。
禁止轉(zhuǎn)載,如需轉(zhuǎn)載請通過簡信或評論聯(lián)系作者。

友情鏈接更多精彩內(nèi)容