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

【聲 明】

首先,這一系列文章均基于自己的理解和實(shí)踐谷丸,可能有不對(duì)的地方堡掏,歡迎大家指正。
其次刨疼,這是一個(gè)入門系列泉唁,涉及的知識(shí)也僅限于夠用鹅龄,深入的知識(shí)網(wǎng)上也有許許多多的博文供大家學(xué)習(xí)了。
最后亭畜,寫文章過(guò)程中扮休,會(huì)借鑒參考其他人分享的文章,會(huì)在文章最后列出拴鸵,感謝這些作者的分享玷坠。

碼字不易,轉(zhuǎn)載請(qǐng)注明出處宝踪!

教程代碼:【Github傳送門

目錄

一侨糟、Android音視頻硬解碼篇:
二厉膀、使用OpenGL渲染視頻畫面篇
三饭望、Android FFmpeg音視頻解碼篇
  • 1仗哨,F(xiàn)Fmpeg so庫(kù)編譯
  • 2,Android 引入FFmpeg
  • 3铅辞,Android FFmpeg視頻解碼播放
  • 4厌漂,Android FFmpeg+OpenSL ES音頻解碼播放
  • 5,Android FFmpeg+OpenGL ES播放視頻
  • 6斟珊,Android FFmpeg簡(jiǎn)單合成MP4:視屏解封與重新封裝
  • 7苇倡,Android FFmpeg視頻編碼

本文你可以了解到

本文主要介紹OpenGL相關(guān)的基礎(chǔ)知識(shí),包括坐標(biāo)系囤踩、著色器旨椒、基本渲染流程等。

一 簡(jiǎn)介

提到OpenGL堵漱,想必很多人都會(huì)說(shuō)综慎,我知道這個(gè)東西,可以用來(lái)渲染2D畫面和3D模型怔锌,同時(shí)又會(huì)說(shuō)寥粹,OpenGL很難变过、很高級(jí),不知道怎么用涝涤。

1媚狰、為什么OpenGL“感覺(jué)很難”?

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

2、OpenGL ES是什么糊肠?

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

首先弧圆,來(lái)看看什么是OpenGL赋兵。

  • CPU和GPU

在手機(jī)上,有兩大元件搔预,一個(gè)是CPU霹期,一個(gè)是GPU。而手機(jī)上顯示圖形界面也有兩種方式拯田,一個(gè)是使用CPU來(lái)渲染历造,一個(gè)是使用GPU來(lái)渲染,可以說(shuō)船庇,GPU渲染其實(shí)是一種硬件加速吭产。

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

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

  • OpenGL 與 OpenGL ES

OpenGL ES 全稱:OpenGL for Embedded Systems吞鸭,是OpenGL 的子集寺董,是針對(duì)手機(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和更高的版本支持這個(gè)API規(guī)范
  • 2.0:不兼容 OpenGL ES 1.x。Android 2.2(API 8)和更高的版本支持這個(gè)API規(guī)范
  • 3.0:向下兼容 OpenGL ES 2.x陶珠。Android 4.3(API 18)及更高的版本支持這個(gè)API規(guī)范
  • 3.1:向下兼容 OpenGL ES3.0/2.0挟裂。Android 5.0(API 21)和更高的版本支持這個(gè)API規(guī)范

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

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

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

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

首先來(lái)看兩個(gè)圖:

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

通過(guò)名字就可以知道架专,這是OpenGL自己世界的坐標(biāo),是一個(gè)標(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需要注意的一個(gè)地方。

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

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

那么夺饲,這兩個(gè)坐標(biāo)系究竟有什么關(guān)系呢奸汇?

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

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

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

三陆盘、OpenGL 著色器語(yǔ)言 GLSL

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

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

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

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

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

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

  • GLSL編程

下面蓝角,通過(guò)一個(gè)最簡(jiǎn)單的頂點(diǎn)著色器和片元著色器來(lái)簡(jiǎn)單介紹一下GLSL語(yǔ)言

#頂點(diǎn)著色器

attribute vec4 aPosition;

void main() {
  gl_Position = aPosition;
}
#片元著色器

void main() {
    gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0)
}

首先可以看到,GLSL語(yǔ)言是一種類C語(yǔ)言饭冬,著色器的框架基本和C語(yǔ)言一樣使鹅,在最上面聲明變量,接著是main函數(shù)昌抠。在著色器中患朱,有幾個(gè)內(nèi)建的變量,可以直接使用(這里只列出音視頻開(kāi)發(fā)常用的炊苫,還有其他的一些3D開(kāi)發(fā)會(huì)用到的):

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

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

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

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

看回上面的著色器代碼。

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

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

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

可以看到vec4是一個(gè)4維向量徘跪,可用于表示坐標(biāo)xyzw甘邀,也可用表示rgba,當(dāng)然還有vec3垮庐,vec2等松邪,可以參考這篇文章:著色器語(yǔ)言GLSL,講的非常詳細(xì)哨查,建議看看逗抑。

這樣,兩個(gè)簡(jiǎn)單的著色器串聯(lián)起來(lái)后寒亥,每一個(gè)頂點(diǎn)(像素)都會(huì)顯示一個(gè)紅點(diǎn)邮府,最后屏幕會(huì)顯示一個(gè)紅色的畫面。

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

四紊服、Android OpenGL ES渲染流程

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

接下來(lái)校赤,就進(jìn)入實(shí)戰(zhàn),一層一層扒開(kāi)OpengGL的神秘面紗重荠。

1碌嘀、初始化

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

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

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

  1. 新建頁(yè)面
class SimpleRenderActivity : AppCompatActivity() {
    //自定義的OpenGL渲染器,詳情請(qǐng)繼續(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>

頁(yè)面非常簡(jiǎn)單逐抑,放置了一個(gè)滿屏的GLSurfaceView鸠儿,初始化的時(shí)候,設(shè)置了OpenGL使用的版本為2.0厕氨,然后配置了渲染器SimpleRender进每,繼承自GLSurfaceView.Renderer

IDrawer將在繪制三角形的時(shí)候具體講解,定義該接口類只是為了方便拓展命斧,也可以直接將渲染代碼寫在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)了三個(gè)回調(diào)接口国葬,這三個(gè)接口就是Google封裝好的流程中贤徒,暴露出來(lái)的接口,留給給開(kāi)發(fā)者實(shí)現(xiàn)初始化和渲染汇四,并且這三個(gè)接口的回調(diào)都在同一個(gè)線程中接奈。

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

同時(shí),創(chuàng)建了一個(gè)紋理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>

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

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

先定義一個(gè)渲染接口類:

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)建凌唬、編譯并啟動(dòng)OpenGL著色器】
            createGLPrg()
            //【步驟3: 開(kāi)始渲染繪制】
            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)建漏麦,否則無(wú)法渲染
            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)
        //開(kāi)始繪制
        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
    }
}

雖然只是畫一個(gè)簡(jiǎn)單的三角形,代碼依然看起來(lái)很復(fù)雜撕贞。這里把它拆解為三個(gè)步驟更耻,就比較清晰明了了。

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

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

【重要提示】

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

因此链快,頂點(diǎn)坐標(biāo)也是根據(jù)三角形的連接來(lái)設(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ù)用第一個(gè)頂點(diǎn)構(gòu)成三角形
GL_TRIANGLE_FAN

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

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

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

紋理坐標(biāo)順序

由于繪制的是三角形霉祸,所以兩個(gè)坐標(biāo)如下(這里只設(shè)置xy軸坐標(biāo)筑累,忽略z軸坐標(biāo),每?jī)蓚€(gè)數(shù)據(jù)構(gòu)成一個(gè)坐標(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)建、編譯并啟動(dò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)建,否則無(wú)法渲染
        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)說(shuō)過(guò)巫橄,GLSL是針對(duì)GPU的編程語(yǔ)言,而著色器就是一段小程序茵典,為了能夠運(yùn)行這段小程序湘换,需要先對(duì)其進(jìn)行編譯和綁定,才能使用。

本例中的著色器就是上文提到的最簡(jiǎn)單的著色器彩倚。

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

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

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

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

編譯好著色器以后岸夯,就是綁定,連接们妥,啟用程序即可猜扮。

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

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

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

沒(méi)錯(cuò)旅赢,這就是Java和GLSL交互的通道,通過(guò)屬性可以給GLSL設(shè)置相關(guān)的值惑惶。

3) 開(kāi)始渲染繪制

private fun doDraw() {
    //啟用頂點(diǎn)的句柄
    GLES20.glEnableVertexAttribArray(mVertexPosHandler)
    GLES20.glEnableVertexAttribArray(mTexturePosHandler)
    //設(shè)置著色器參數(shù)煮盼, 第二個(gè)參數(shù)表示一個(gè)頂點(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)
    //開(kāi)始繪制
    GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 3)
}

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

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

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

通過(guò)以上步驟泛烙,就可以在屏幕上看到一個(gè)紅色的三角形了。

三角形

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

接下來(lái)帆疟,就用紋理來(lái)顯示一張圖片鹉究,看看紋理到底怎么使用。

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

3柳琢、紋理貼圖绍妨,顯示一張圖片

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

class BitmapDrawer(private val mTextureId: Int, private val mBitmap: Bitmap): IDrawer {
    //-------【注1:坐標(biāo)變更了他去,由四個(gè)點(diǎn)組成一個(gè)四邊形】-------
    // 頂點(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)建毙驯、編譯并啟動(dòng)OpenGL著色器】
            createGLPrg()
            //-------【注4:新增兩個(gè)步驟】-------
            //【步驟3: 激活并綁定紋理單元】
            activateTexture()
            //【步驟4: 綁定圖片到紋理單元】
            bindBitmapToTexture()
            //----------------------------------
            //【步驟5: 開(kāi)始渲染繪制】
            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)
        //配置邊緣過(guò)渡參數(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)?】
        //開(kāi)始繪制:最后一個(gè)參數(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)識(shí)(見(jiàn)代碼中的【注:x】)。逐個(gè)來(lái)看看:

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

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

2)著色器

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

  • attritude:一般用于各個(gè)頂點(diǎn)各不相同的量蛾号。如頂點(diǎn)顏色稠项、坐標(biāo)等。
  • uniform:一般用于對(duì)于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)入來(lái)的紋理單元
            "uniform sampler2D uTexture;" +
            //從頂點(diǎn)著色器傳遞進(jìn)來(lái)的紋理坐標(biāo)
            "varying vec2 vCoordinate;" +
            "void main() {" +
            //根據(jù)紋理坐標(biāo),從紋理單元中取色
            "  vec4 color = texture2D(uTexture, vCoordinate);" +
            "  gl_FragColor = color;" +
            "}"
}

繪制過(guò)程新增了兩個(gè)步驟:

3)激活并綁定紋理單元

private fun activateTexture() {
    //激活指定紋理單元
    GLES20.glActiveTexture(GLES20.GL_TEXTURE0)
    //綁定紋理ID到紋理單元
    GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, mTextureId)
    //將激活的紋理單元傳遞到著色器里面
    GLES20.glUniform1i(mTextureHandler, 0)
    //配置紋理過(guò)濾模式
    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)
}

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

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

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

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

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

  • 紋理過(guò)濾模式

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

一般使用這兩個(gè)模式:GL_NEAREST(鄰近過(guò)濾)辐啄、GL_LINEAR(線性過(guò)濾)

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

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

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

4)綁定圖片到紋理單元

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

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

5)繪制

繪制的時(shí)候程癌,最后一句的最后一個(gè)參數(shù)由三角形的3個(gè)頂點(diǎn)變成為長(zhǎng)方形的4個(gè)頂點(diǎn)舷嗡。如果還是填入3,你會(huì)發(fā)現(xiàn)會(huì)顯示圖片的一半嵌莉,即三角形(對(duì)角線分割開(kāi))进萄。

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

至此,一張圖片就通過(guò)紋理貼圖顯示出來(lái)了锐峭。

紋理貼圖

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

五惫搏、總結(jié)

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

  1. 通過(guò)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峦椰,配置紋理過(guò)濾模式和環(huán)繞方式
  8. 綁定紋理(如將bitmap綁定給紋理)
  9. 啟動(dòng)繪制

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

六控轿、參考文章

了解OpenGLES2.0

著色器語(yǔ)言GLSL

LearnOpenGL-CN

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
禁止轉(zhuǎn)載冤竹,如需轉(zhuǎn)載請(qǐng)通過(guò)簡(jiǎn)信或評(píng)論聯(lián)系作者。
  • 序言:七十年代末茬射,一起剝皮案震驚了整個(gè)濱河市鹦蠕,隨后出現(xiàn)的幾起案子在抛,更是在濱河造成了極大的恐慌,老刑警劉巖刚梭,帶你破解...
    沈念sama閱讀 218,755評(píng)論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異屹徘,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)噪伊,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,305評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門氮唯,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人惩琉,你說(shuō)我怎么就攤上這事豆励。” “怎么了瞒渠?”我有些...
    開(kāi)封第一講書人閱讀 165,138評(píng)論 0 355
  • 文/不壞的土叔 我叫張陵良蒸,是天一觀的道長(zhǎng)技扼。 經(jīng)常有香客問(wèn)我,道長(zhǎng)嫩痰,這世上最難降的妖魔是什么淮摔? 我笑而不...
    開(kāi)封第一講書人閱讀 58,791評(píng)論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮始赎,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘仔燕。我一直安慰自己造垛,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,794評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布晰搀。 她就那樣靜靜地躺著五辽,像睡著了一般。 火紅的嫁衣襯著肌膚如雪外恕。 梳的紋絲不亂的頭發(fā)上杆逗,一...
    開(kāi)封第一講書人閱讀 51,631評(píng)論 1 305
  • 那天,我揣著相機(jī)與錄音鳞疲,去河邊找鬼罪郊。 笑死,一個(gè)胖子當(dāng)著我的面吹牛尚洽,可吹牛的內(nèi)容都是我干的悔橄。 我是一名探鬼主播,決...
    沈念sama閱讀 40,362評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼腺毫,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼癣疟!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起潮酒,我...
    開(kāi)封第一講書人閱讀 39,264評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎急黎,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體委乌,經(jīng)...
    沈念sama閱讀 45,724評(píng)論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡遭贸,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,900評(píng)論 3 336
  • 正文 我和宋清朗相戀三年壕吹,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了耳贬。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,040評(píng)論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡顷蟆,死狀恐怖帐偎,靈堂內(nèi)的尸體忽然破棺而出削樊,到底是詐尸還是另有隱情漫贞,我是刑警寧澤育叁,帶...
    沈念sama閱讀 35,742評(píng)論 5 346
  • 正文 年R本政府宣布豪嗽,位于F島的核電站昵骤,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏成榜。R本人自食惡果不足惜赎婚,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,364評(píng)論 3 330
  • 文/蒙蒙 一挣输、第九天 我趴在偏房一處隱蔽的房頂上張望撩嚼。 院中可真熱鬧完丽,春花似錦、人聲如沸蜻底。這莊子的主人今日做“春日...
    開(kāi)封第一講書人閱讀 31,944評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至枪芒,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間纽甘,已是汗流浹背悍赢。 一陣腳步聲響...
    開(kāi)封第一講書人閱讀 33,060評(píng)論 1 270
  • 我被黑心中介騙來(lái)泰國(guó)打工左权, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留赏迟,地道東北人蠢棱。 一個(gè)月前我還...
    沈念sama閱讀 48,247評(píng)論 3 371
  • 正文 我出身青樓泻仙,卻偏偏與公主長(zhǎng)得像玉转,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子饶套,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,979評(píng)論 2 355