【Android 音視頻開發(fā)打怪升級:OpenGL渲染視頻畫面篇】五疟呐、OpenGL FBO數(shù)據(jù)緩沖區(qū)

【聲 明】

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

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

教程代碼:【Github傳送門

目錄

一帝嗡、Android音視頻硬解碼篇:

二、使用OpenGL渲染視頻畫面篇

三菱皆、Android FFmpeg音視頻解碼篇


本文你可以了解到

本文將介紹如何使用FBO,F(xiàn)BO可以實現(xiàn)什么效果疲陕,以及如何在著色器中使用多個紋理單元方淤。

先來看看利用FBO實現(xiàn)的靈魂出竅效果:

靈魂出竅

一、FBO與EGL的離屏渲染的區(qū)別

上一篇文章蹄殃,講解了如何使用EGL携茂,并且提到EGL可以建立一個離屏渲染的緩沖區(qū),這種離屏渲染的方式通常用于模擬整個渲染窗口诅岩,比如可以用于FFmpeg軟編碼讳苦,將顯示在虛擬窗口中的畫面編碼成H264。

與此同時吩谦,OpenGL也提供另外一種離屏渲染方式鸳谜,即FBO。FBO不僅可以實現(xiàn)離屏渲染整個OpenGL窗口式廷,也可以用于處理碎片畫面咐扭,即窗口中的小畫面。

關于EGL的離屏渲染滑废,將會在后面關于FFmpeg的文章中使用到蝗肪,這里暫且不論。

而在視頻編輯當中策严,F(xiàn)BO離屏渲染扮演著很重要的角色穗慕,許多的視頻濾鏡都會用到,接下來就來看看FBO如何使用吧妻导。

二逛绵、FBO簡介

OpenGL 在渲染到系統(tǒng)窗口之前怀各,都會將數(shù)據(jù)送到 FBO 上,也就是說术浪,FBO 其實一直在默默的為我們服務瓢对。
所以,OpenGL 在一開始就創(chuàng)建了一個默認的 FBO胰苏。

FBO:Frame Buffer Object硕蛹,幀緩存對象。

從名字上看硕并,往往很容易讓人誤解這是一個緩存空間法焰,但實際上,F(xiàn)BO很重要的在最后面的Object上倔毙。這是一個緩存對象埃仪,包含了多個緩沖索引,分別為顏色緩沖(Color buffers), 深度緩沖(Depth buffer), 模板緩沖(Stencil buffer)陕赃。

之所以說是緩沖索引卵蛉,是因為FBO并不包含這些緩沖數(shù)據(jù),僅僅保存了緩沖數(shù)據(jù)的索引地址么库。

FBO和這些緩沖區(qū)則通過附著點進行連接傻丝。

可以看到FBO中包含了:

1. 多個顏色附著點(GL_COLOR_ATTACHMENT0、GL_COLOR_ATTACHMENT1...)
2. 一個深度附著點(GL_DEPTH_ATTACHMENT)
3. 一個模板附著點(GL_STENCIL_ATTACHMENT)

可以劃分為兩類:

紋理附著(顏色附著):主要用于將顏色渲染到紋理中诉儒。

渲染緩沖對象RBO(Render Buffer Objecgt):主要用于渲染深度信息和模板信息葡缰。

在2D中,通常只用到了顏色附著允睹,另外兩種附著通常在3D渲染中使用运准。

上面說了,F(xiàn)BO可用于離屏渲染缭受,下面就來看看如何通過FBO將畫面渲染到一個“后臺”的紋理中。

這里的后臺该互,指不用于顯示到窗口的紋理米者。

三、如何使用FBO

1. 新建紋理

fun createFBOTexture(width: Int, height: Int): IntArray {
    // 新建紋理ID
    val textures = IntArray(1)
    GLES20.glGenTextures(1, textures, 0)
    
    // 綁定紋理ID
    GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textures[0])
    
    // 根據(jù)顏色參數(shù)宇智,寬高等信息蔓搞,為上面的紋理ID,生成一個2D紋理
    GLES20.glTexImage2D(GLES20.GL_TEXTURE_2D, 0, GLES20.GL_RGBA, width, height,
        0, GLES20.GL_RGBA, GLES20.GL_UNSIGNED_BYTE, null)
        
    // 設置紋理邊緣參數(shù)
    GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_NEAREST.toFloat())
    GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MAG_FILTER,GLES20.GL_LINEAR.toFloat())
    GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_S,GLES20.GL_CLAMP_TO_EDGE.toFloat())
    GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_T,GLES20.GL_CLAMP_TO_EDGE.toFloat())
    
    // 解綁紋理ID
    GLES20.glBindTexture(GLES20.GL_TEXTURE_2D,0)
    return textures
}

生成一個用于FBO的紋理和普通的紋理其實差不多随橘。

首先喂分,生成一個紋理ID,并綁定到OpenGL中机蔗。

其次蒲祈,給這個紋理ID生成對應的紋理甘萧。

這里使用的是 GLES20.glTexImage2D ,在渲染圖片紋理的時候梆掸,使用的是 GLUtils.texImage2D 扬卷。

關于創(chuàng)建紋理的寬高問題,這里說明一下:
FBO創(chuàng)建的是一個虛擬的窗口酸钦,所以怪得,大小是可以根據(jù)自己的需求設置的,可以比實際系統(tǒng)窗口大卑硫。為了視頻畫面比例正常徒恋,可以把OpenGL的窗口寬高,以及紋理的寬高都設置為視頻的寬高欢伏。因此入挣,OpenGL在渲染的時候,我們也把無需再通過矩陣變換來矯正比例颜懊,直接拉伸就可以财岔。

最后,設置紋理邊緣參數(shù)河爹,然后解綁匠璧。

2. 新建FrameBuffer

fun createFrameBuffer(): Int {
    val fbs = IntArray(1)
    GLES20.glGenFramebuffers(1, fbs, 0)
    return fbs[0]
}

新建FrameBuffer類似新建紋理ID,最后返回FBO索引

3. 綁定FBO

fun bindFBO(fb: Int, textureId: Int) {
    GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, fb)
    GLES20.glFramebufferTexture2D(GLES20.GL_FRAMEBUFFER, GLES20.GL_COLOR_ATTACHMENT0,
        GLES20.GL_TEXTURE_2D, textureId, 0)
}

先綁定上面創(chuàng)建的FBO咸这,接著將FBO和上面創(chuàng)建的紋理通過顏色附著點 GLES20.GL_COLOR_ATTACHMENT0 綁定起來夷恍。

4. 解綁FBO

fun unbindFBO() {
    GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, GLES20.GL_NONE)
    GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, 0)
}

解綁FBO比較簡單,其實就是將FBO綁定到默認的窗口上媳维。

這里的 GLES20.GL_NONE 其實就是 0 酿雪,也就是系統(tǒng)默認的窗口的 FBO 。

5. 刪除FBO

fun deleteFBO(frame: IntArray, texture:IntArray) {
    //刪除Frame Buffer
    GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, GLES20.GL_NONE)
    GLES20.glDeleteFramebuffers(1, frame, 0)
    //刪除紋理
    GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, 0)
    GLES20.glDeleteTextures(1, texture, 0)
}

以上侄刽,其實就是使用FBO的流程了:

  1. 新建紋理
  2. 新建FBO
  3. 綁定將紋理附著到FBO的顏色附著點上
  4. 【渲染】
  5. 解綁FBO
  6. 刪除FBO

除了第4步以外指黎,其他都是上面的封裝好的方法。

那么接下就來看看州丹,如何將畫面渲染到FBO連接的紋理上醋安。

為了更好的理解整個渲染的過程,下面通過一個非常經(jīng)典的濾鏡來演示這個渲染的流程墓毒。

三吓揪、使用FBO實現(xiàn)“靈魂出竅”濾鏡

1. 如何實現(xiàn)靈魂出竅

  • 靜態(tài)圖靈魂出竅
靈魂出竅

這個效果可以拆分為3個效果:

  1. 底層靜態(tài)圖
  2. 上層放大
  3. 上層半透明

進而拆分為2個組合:

  1. 底層靜態(tài)圖
  2. 上層不斷放大,并且隨著放大增加透明度
  • 視頻靈魂出竅

根據(jù)靜態(tài)圖的靈魂出竅效果所计,可以知道柠辞,上層的靈魂出竅效果是根據(jù)原圖而來的,就是說主胧,靈魂的基礎圖片是不會變化的叭首。

而視頻的每一幀都是在變化的习勤。

所以,為了使上層的“靈魂”達到比較平滑的放大效果放棒,需要把一幀保持住一段時間姻报,讓這一幀完成完整的放大過程。

這里就遇到了一個問題:如何保存視頻的某一幀间螟?

FBO 就是解決這個問題的關鍵吴旋。

2. 封裝FBO工具

為了可以方便的使用FBO相關的方法,我們將上面的方法都封裝在一個靜態(tài)工具中 OpenGLTools 厢破。

object OpenGLTools {
    fun createFBOTexture(width: Int, height: Int): IntArray {
        // 新建紋理ID
        val textures = IntArray(1)
        GLES20.glGenTextures(1, textures, 0)
        
        // 綁定紋理ID
        GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textures[0])
        
        // 根據(jù)顏色參數(shù)荣瑟,寬高等信息,為上面的紋理ID摩泪,生成一個2D紋理
        GLES20.glTexImage2D(GLES20.GL_TEXTURE_2D, 0, GLES20.GL_RGBA, width, height,
            0, GLES20.GL_RGBA, GLES20.GL_UNSIGNED_BYTE, null)
            
        // 設置紋理邊緣參數(shù)
        GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_NEAREST.toFloat())
        GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MAG_FILTER,GLES20.GL_LINEAR.toFloat())
        GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_S,GLES20.GL_CLAMP_TO_EDGE.toFloat())
        GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_T,GLES20.GL_CLAMP_TO_EDGE.toFloat())
        
        // 解綁紋理ID
        GLES20.glBindTexture(GLES20.GL_TEXTURE_2D,0)
        return textures
    }
    
    fun createFrameBuffer(): Int {
        val fbs = IntArray(1)
        GLES20.glGenFramebuffers(1, fbs, 0)
        return fbs[0]
    }
    
    fun bindFBO(fb: Int, textureId: Int) {
        GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, fb)
        GLES20.glFramebufferTexture2D(GLES20.GL_FRAMEBUFFER, GLES20.GL_COLOR_ATTACHMENT0,
            GLES20.GL_TEXTURE_2D, textureId, 0)
    }
    
    fun unbindFBO() {
        GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, GLES20.GL_NONE)
        GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, 0)
    }
    
    fun deleteFBO(frame: IntArray, texture:IntArray) {
        //刪除Frame Buffer
        GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, GLES20.GL_NONE)
        GLES20.glDeleteFramebuffers(1, frame, 0)
        //刪除紋理
        GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, 0)
        GLES20.glDeleteTextures(1, texture, 0)
    }
}

3. 在視頻渲染器中笆焰,接入FBO

  • 新建渲染器 SoulVideoDrawer

這里將之前的VideoDrawer直接復制過來,如果大家閱讀過之前的文章见坑,相信對VideoDrawer應該不會陌生了嚷掠。所以這里就不再貼完整代碼了。詳情請查看之前的文章荞驴,或者直接看源碼:VideoDrawer不皆。

其實 SoulVideoDrawer 大部分代碼和 VideoDrawer 一致,這里查看完整源碼:SoulVideoDrawer熊楼。

這次霹娄,不再像之前那樣一次性貼出完整的代碼,一步步來看下如何使用 FBO 鲫骗。

class SoulVideoDrawer : IDrawer {

    // ......
    
    // 省略和VideoDrawer一樣成員變量
    
    // ......

//-------------靈魂出竅相關的變量--------------

    /**上下顛倒的頂點矩陣*/
    private val mReserveVertexCoors = floatArrayOf(
        -1f, 1f,
        1f, 1f,
        -1f, -1f,
        1f, -1f
    )

    private val mDefVertexCoors = floatArrayOf(
        -1f, -1f,
        1f, -1f,
        -1f, 1f,
        1f, 1f
    )

    // 頂點坐標
    private var mVertexCoors = mDefVertexCoors
    
    // 靈魂幀緩沖
    private var mSoulFrameBuffer: Int = -1

    // 靈魂紋理ID
    private var mSoulTextureId: Int = -1

    // 靈魂紋理接收者
    private var mSoulTextureHandler: Int = -1

    // 靈魂縮放進度接收者
    private var mProgressHandler: Int = -1

    // 是否更新FBO紋理
    private var mDrawFbo: Int = 1

    // 更新FBO標記接收者
    private var mDrawFobHandler: Int = -1

    // 一幀靈魂的時間
    private var mModifyTime: Long = -1
    
    override fun draw() {
        if (mTextureId != -1) {
            initDefMatrix()
            //【步驟1: 創(chuàng)建犬耻、編譯并啟動OpenGL著色器】
            createGLPrg()
            
            // -------【步驟2:新增FBO部分】-----
            //【步驟2.1: 更新靈魂紋理】
            updateFBO()
            //【步驟2.2: 激活靈魂紋理單元】
            activateSoulTexture()
            // ---------------------------
            
            //【步驟3: 激活并綁定紋理單元】
            activateDefTexture()
            //【步驟4: 綁定圖片到紋理單元】
            updateTexture()
            //【步驟5: 開始渲染繪制】
            doDraw()
        }
    }
    
    // ......
}

增加了和FBO、實現(xiàn)靈魂出竅效果相關的成員變量执泰。

重點關注 draw 方法枕磁,有5個步驟,但真正增加的其實就是第2個步驟:

步驟2: 新增FBO部分
- 2.1: 更新靈魂紋理【updateFBO】
- 2.2: 激活靈魂紋理單元【activateSoulTexture】

先來看2.1术吝。

  • 更新附著在FBO上的紋理
class SoulVideoDrawer : IDrawer {

    // ......
    
    private fun updateFBO() {
        //【1透典,創(chuàng)建FBO紋理】
        if (mSoulTextureId == -1) {
            mSoulTextureId = OpenGLTools.createFBOTexture(mVideoWidth, mVideoHeight)
        }
        // 【2,創(chuàng)建FBO】
        if (mSoulFrameBuffer == -1) {
            mSoulFrameBuffer = OpenGLTools.createFrameBuffer()
        }
        // 【3顿苇,渲染到FBO】
        if (System.currentTimeMillis() - mModifyTime > 500) {
            mModifyTime = System.currentTimeMillis()
            // 綁定FBO
            OpenGLTools.bindFBO(mSoulFrameBuffer, mSoulTextureId)
            // 配置FBO窗口
            configFboViewport()
            
//--------執(zhí)行正常畫面渲染,畫面將渲染到FBO上--------------

            // 激活默認的紋理
            activateDefTexture()
            // 更新紋理
            updateTexture()
            // 繪制到FBO
            doDraw()
            
//---------------------------------------------------

            // 解綁FBO
            OpenGLTools.unbindFBO()
            // 恢復默認繪制窗口
            configDefViewport()
        }
    }

    /**
     * 配置FBO窗口
     */
    private fun configFboViewport() {
        mDrawFbo = 1
        // 將變換矩陣回復為單位矩陣(將畫面拉升到整個窗口大小税弃,設置窗口比例和FBO紋理比例一致纪岁,畫面剛好可以正常繪制到FBO紋理上)
        Matrix.setIdentityM(mMatrix, 0)
        // 設置顛倒的頂點坐標
        mVertexCoors = mReserveVertexCoors
        //重新初始化頂點坐標
        initPos()
        GLES20.glViewport(0, 0, mVideoWidth, mVideoHeight)
        //設置一個顏色狀態(tài)
        GLES20.glClearColor(0.0f, 0.0f, 0.0f, 0.0f)
        //使能顏色狀態(tài)的值來清屏
        GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT)
    }

    /**
     * 配置默認顯示的窗口
     */
    private fun configDefViewport() {
        mDrawFbo = 0
        mMatrix = null
        // 恢復頂點坐標
        mVertexCoors = mDefVertexCoors
        initPos()
        initDefMatrix()
        // 恢復窗口
        GLES20.glViewport(0, 0, mWorldWidth, mWorldHeight)
    }

    private fun activateDefTexture() {
        activateTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, mTextureId, 0, mTextureHandler)
    }

    private fun activateSoulTexture() {
        activateTexture(GLES11.GL_TEXTURE_2D, mSoulTextureId, 1, mSoulTextureHandler)
    }

    private fun activateTexture(type: Int, textureId: Int, index: Int, textureHandler: Int) {
        //激活指定紋理單元
        GLES20.glActiveTexture(GLES20.GL_TEXTURE0 + index)
        //綁定紋理ID到紋理單元
        GLES20.glBindTexture(type, textureId)
        //將激活的紋理單元傳遞到著色器里面
        GLES20.glUniform1i(textureHandler, index)
        //配置邊緣過渡參數(shù)
        GLES20.glTexParameterf(type, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_LINEAR.toFloat())
        GLES20.glTexParameterf(type, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR.toFloat())
        GLES20.glTexParameteri(type, GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_CLAMP_TO_EDGE)
        GLES20.glTexParameteri(type, GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_CLAMP_TO_EDGE)
    }
    
    // ......
}

updateFBO 方法,3個步驟:

  1. 創(chuàng)建紋理
  2. 創(chuàng)建FBO
  3. 將圖像渲染到FBO的紋理上

前面2個步驟则果,在之前已經(jīng)介紹過幔翰,不再贅述漩氨。

重點看第3步。

這里讓一幀圖像保持500ms遗增,我們用一個變量 mModifyTime 來記錄當前這一幀渲染時候的時間叫惊,只要過了500ms,就刷新一次畫面做修。

來看渲染FBO的過程:

if (System.currentTimeMillis() - mModifyTime > 500) {
    // 記錄時間
    mModifyTime = System.currentTimeMillis()
    // 綁定FBO
    OpenGLTools.bindFBO(mSoulFrameBuffer, mSoulTextureId)
    // 配置FBO窗口
    configFboViewport()
//--------執(zhí)行正常畫面渲染霍狰,畫面將渲染到FBO上--------------
    // 激活默認的紋理
    activateDefTexture()
    // 更新紋理
    updateTexture()
    // 繪制到FBO
    doDraw()
//---------------------------------------------------
    // 解綁FBO
    OpenGLTools.unbindFBO()
    // 恢復默認繪制窗口
    configDefViewport()
}

i. 綁定FBO

當調(diào)用了 OpenGLTools.bindFBO 之后,所有對于OpenGL的操作都將影響到我們自己創(chuàng)建的FBO饰及。也就是說蔗坯,在調(diào)用 OpenGLTools.unbindFBO() 解綁FBO之前,下面所有的操作燎含,都將作用在FBO上宾濒。

ii. 重新配置FBO窗口大小

將OpenGL窗口設置為視頻大小,并且將矩陣變化重置(畫面拉升到窗口大衅凉俊)绘梦,然后清屏。

至于為什么要重新設置窗口大小赴魁,前面設置紋理大小的時候已經(jīng)說過了卸奉。

還有一點要注意的是,這里將紋理坐標 mVertexCoors 做了上下顛倒(其實就是恢復為OpenGL默認的坐標)尚粘,這樣渲染到FBO綁定的紋理上后择卦,在片元著色器里面才能正常取色。

代碼如下:

private fun configFboViewport() {
    mDrawFbo = 1
    // 將變換矩陣恢復為單位矩陣
    //(將畫面拉升到整個窗口大小郎嫁,
    // 設置窗口寬高和FBO紋理寬高一致秉继,
    // 畫面剛好可以正常繪制到FBO綁定的紋理上)
    Matrix.setIdentityM(mMatrix, 0)
    // 設置顛倒的頂點坐標
    mVertexCoors = mReserveVertexCoors
    //重新初始化頂點坐標
    initPos()
    // 設置窗口大小
    GLES20.glViewport(0, 0, mVideoWidth, mVideoHeight)
    //設置一個顏色狀態(tài)
    GLES20.glClearColor(0.0f, 0.0f, 0.0f, 0.0f)
    //使能顏色狀態(tài)的值來清屏
    GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT)
}

iii. 激活和更新視頻原來的紋理

注意,這里是激活原來的渲染視頻的紋理

iv. 渲染繪制

也就是說泽铛,在綁定了FBO以后尚辑,按照正常的渲染流程,就可以將畫面渲染到FBO上了盔腔。

v. 解除FBO綁定杠茬,將窗口大小、紋理坐標弛随、矩陣都恢復回原來的配置瓢喉。

將渲染重新切換到原來的系統(tǒng)窗口上,畫面將重新顯示到系統(tǒng)窗口上舀透。

通過以上步驟栓票,就將畫面渲染到FBO綁定的紋理 mSoulTextureId 上面了。

4. 實現(xiàn)靈魂出竅效果

前面愕够,我們將一幀畫面渲染到了 mSoulTextureId 這個紋理上走贪, 接下來就要利用這個紋理佛猛,將畫面放大、透明漸變實現(xiàn)靈魂效果坠狡。

回到draw方法中继找,來到2.2步驟。

override fun draw() {
    if (mTextureId != -1) {
        //【步驟1: 創(chuàng)建逃沿、編譯并啟動OpenGL著色器】
        // -------【步驟2:新增FBO部分】-----
        //【步驟2.1: 更新靈魂紋理】
        //【步驟2.2: 激活靈魂紋理單元】
        activateSoulTexture()
        // ---------------------------
        
        //【步驟3: 激活并綁定紋理單元】
        activateDefTexture()
        //【步驟4: 綁定圖片到紋理單元】
        updateTexture()
        //【步驟5: 開始渲染繪制】
        doDraw()
    }
}

看下激活如何激活“靈魂”的紋理婴渡。

  • 傳遞多個紋理到著色器中
private fun activateSoulTexture() {
    activateTexture(GLES11.GL_TEXTURE_2D, mSoulTextureId, 1, mSoulTextureHandler)
}

private fun activateTexture(type: Int, textureId: Int, index: Int, textureHandler: Int) {
    //激活指定紋理單元
    GLES20.glActiveTexture(GLES20.GL_TEXTURE0 + index)
    //綁定紋理ID到紋理單元
    GLES20.glBindTexture(type, textureId)
    //將激活的紋理單元傳遞到著色器里面
    GLES20.glUniform1i(textureHandler, index)
    //配置邊緣過渡參數(shù)
    GLES20.glTexParameterf(type, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_LINEAR.toFloat())
    GLES20.glTexParameterf(type, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR.toFloat())
    GLES20.glTexParameteri(type, GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_CLAMP_TO_EDGE)
    GLES20.glTexParameteri(type, GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_CLAMP_TO_EDGE)
}

和之前文章稍微有點不同,以前參數(shù)都是直接寫死的感挥。這次改造了一下 activateTexture紋理類型 缩搅,紋理ID紋理單元索引 触幼,以及著色器對應的 紋理接收器 硼瓣,作為參數(shù)傳遞進來。

有2點要注意的:

  1. 關于紋理類型置谦。在 activateSoulTexture 中堂鲤,需要注意的是,紋理的類型為普通紋理類型 GLES11.GL_TEXTURE_2D 媒峡, 而非擴展紋理 GLES11Ext.GL_TEXTURE_EXTERNAL_OES 瘟栖,因為經(jīng)過之前的渲染以后,畫面已經(jīng)是普通紋理了谅阿。
  2. 關于紋理單元半哟。在OpenGL基礎知識中說過,OpenGL內(nèi)置了多個紋理單元签餐,并且可以同時使用寓涨。 所以這里, 正常畫面的紋理單元設置為默認的 GLES20.GL_TEXTURE0氯檐, “靈魂”的紋理單元為 GLES20.GL_TEXTURE1 = GLES20.GL_TEXTURE0 + 1 戒良。

接著,激活默認的正常畫面紋理 updateTexture() 冠摄,這樣就可以在片元著色器中糯崎,同時接收這兩個紋理單元。

private fun activateDefTexture() {
    activateTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, mTextureId, 0, mTextureHandler)
}
  • 渲染繪制

最后河泳,啟動渲染繪制沃呢,進入到著色器中。

  • “靈魂出竅”著色器

前面做了這么多的鋪墊拆挥,其實都是為了將一幀固定的視頻畫面?zhèn)鬟f到著色器中樟插。真正實現(xiàn)“靈魂出竅”的效果,也是在片元著色器中。

著色器代碼如下:

private fun getVertexShader(): String {
    return "attribute vec4 aPosition;" +
            "precision mediump float;" +
            "uniform mat4 uMatrix;" +
            "attribute vec2 aCoordinate;" +
            "varying vec2 vCoordinate;" +
            "attribute float alpha;" +
            "varying float inAlpha;" +
            "void main() {" +
            "    gl_Position = uMatrix*aPosition;" +
            "    vCoordinate = aCoordinate;" +
            "    inAlpha = alpha;" +
            "}"
}

private fun getFragmentShader(): String {
    //一定要加換行"\n"黄锤,否則會和下一行的precision混在一起,導致編譯出錯
    return "#extension GL_OES_EGL_image_external : require\n" +
            "precision mediump float;" +
            "varying vec2 vCoordinate;" +
            "varying float inAlpha;" +
            "uniform samplerExternalOES uTexture;" +
            "uniform float progress;" +
            "uniform int drawFbo;" +
            "uniform sampler2D uSoulTexture;" +
            "void main() {" +
                // 透明度[0,0.4]
                "float alpha = 0.6 * (1.0 - progress);" +
                // 縮放比例[1.0,1.5]
                "float scale = 1.0 + (1.5 - 1.0) * progress;" +

                // 放大紋理坐標
                "float soulX = 0.5 + (vCoordinate.x - 0.5) / scale;\n" +
                "float soulY = 0.5 + (vCoordinate.y - 0.5) / scale;\n" +
                "vec2 soulTextureCoords = vec2(soulX, soulY);" +
                // 獲取對應放大紋理坐標下的像素(顏色值rgba)
                "vec4 soulMask = texture2D(uSoulTexture, soulTextureCoords);" +

                "vec4 color = texture2D(uTexture, vCoordinate);" +

                "if (drawFbo == 0) {" +
                    // 顏色混合 默認顏色混合方程式 = mask * (1.0-alpha) + weakMask * alpha
                "    gl_FragColor = color * (1.0 - alpha) + soulMask * alpha;" +
                "} else {" +
                "   gl_FragColor = vec4(color.r, color.g, color.b, inAlpha);" +
                "}" +
            "}"
}

可以看到食拜,頂點著色器 的代碼和普通的渲染是一樣的鸵熟。

修改的都在 片元著色器中

簡單分析一下:

i. 除了正常畫面渲染需要的參數(shù)负甸,另外新增了3個參數(shù):

// 動畫進度
uniform float progress;
// 是否繪制到FBO
uniform int drawFbo;
// 一幀固定的紋理
uniform sampler2D uSoulTexture;

ii. 跳過中間關于“靈魂”動畫的部分流强,先看最后一個if/else

if (drawFbo == 0) {
    // 顏色混合 默認顏色混合方程式 = mask * (1.0-alpha) + weakMask * alpha
    gl_FragColor = color * (1.0 - alpha) + soulMask * alpha;" +
} else {
   gl_FragColor = vec4(color.r, color.g, color.b, inAlpha);
}

當一幀的時間超過500ms的時候,會重新獲取一幀新的視頻畫面呻待。

這里通過外部傳進來的標記 drawFbo 如果為 1 時打月,渲染普通的畫面,此時由于已經(jīng)綁定了FBO蚕捉,所以這一幀畫面會渲染到FBO的 mSoulTextureID 上奏篙。

在下一次渲染的時候,這一幀紋理將傳遞給片元著色器的 uSoulTexture迫淹。

iii. 中間的部分秘通,關于“靈魂出竅”的核心。

// 透明度[0,0.4]
float alpha = 0.6 * (1.0 - progress);
// 縮放比例[1.0,1.5]
float scale = 1.0 + (1.5 - 1.0) * progress;

// 放大紋理坐標
float soulX = 0.5 + (vCoordinate.x - 0.5) / scale;
float soulY = 0.5 + (vCoordinate.y - 0.5) / scale;
vec2 soulTextureCoords = vec2(soulX, soulY);

// 獲取對應放大紋理坐標下的像素(顏色值rgba)
vec4 soulMask = texture2D(uSoulTexture, soulTextureCoords);

首先敛熬,計算透明度肺稀。根據(jù)外面計算得到的 progress ,慢慢降低透明度应民,最大透明度為0.6话原。

然后,計算縮放后的坐標诲锹。隨著 progress 的增加繁仁,scale 越大。最大放大1.5倍辕狰。利用 scale 分別計算 X改备,Y 的縮放÷叮可以看到悬钳,scale 越大,soulX/soulY 反而更小偶翅。這是因為要達到放大的效果默勾,當前要渲染的點,應該取更小的坐標對應的顏色(像素)聚谁。

最后母剥,通過 soulX soulY ,到“靈魂”紋理 uSoulTexture 取到顏色。

iv. 混合底層正常畫面和上層“靈魂”畫面环疼,采用常用的混合算法习霹。

gl_FragColor = color * (1.0 - alpha) + soulMask * alpha;

5. 在頁面中接入繪制器

class SoulPlayerActivity: AppCompatActivity() {
    val path = Environment.getExternalStorageDirectory().absolutePath + "/mvtest.mp4"
    lateinit var drawer: IDrawer

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_opengl_player)
        initRender()
    }

    private fun initRender() {
        // 使用“靈魂出竅”渲染器
        drawer = SoulVideoDrawer()
        drawer.setVideoSize(1920, 1080)
        drawer.getSurfaceTexture {
            initPlayer(Surface(it))
        }
        gl_surface.setEGLContextClientVersion(2)
        val render = SimpleRender()
        render.addDrawer(drawer)
        gl_surface.setRenderer(render)
    }

    private fun initPlayer(sf: Surface) {
        val threadPool = Executors.newFixedThreadPool(10)

        val videoDecoder = VideoDecoder(path, null, sf)
        threadPool.execute(videoDecoder)

        val audioDecoder = AudioDecoder(path)
        threadPool.execute(audioDecoder)

        videoDecoder.goOn()
        audioDecoder.goOn()
    }
}

使用和普通的使用OpenGL渲染器一模一樣,不一樣的只是把 VideoDrawer 換成 SoulVideoDrawer 炫隶。

最終得到了文章開頭的效果:

靈魂出竅

四淋叶、總結(jié)

以上就是整個使用FBO的過程,使用也非常的簡單伪阶。當然了煞檩,只關注了顏色附著的部分,另外的深度附著和模板附著有興趣的可以自行探索學習栅贴。

可以看到斟湃,F(xiàn)BO為我們提供了一個實現(xiàn)視頻處理的好方法,許多酷炫的效果得以實現(xiàn)檐薯,更多有趣的效果凝赛,等著大家去實現(xiàn)。

  • 參考文章

幀緩沖區(qū)對象(FBO) 實現(xiàn)渲染到紋理(Render To Texture/RTT)

DEPTH_TEST(深度緩沖測試)

Stencil_TEST(模板緩沖測試)

OpenGL ES入門:濾鏡篇 - 縮放厨剪、靈魂出竅哄酝、抖動等

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
禁止轉(zhuǎn)載,如需轉(zhuǎn)載請通過簡信或評論聯(lián)系作者祷膳。
  • 序言:七十年代末陶衅,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子直晨,更是在濱河造成了極大的恐慌搀军,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,755評論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件勇皇,死亡現(xiàn)場離奇詭異罩句,居然都是意外死亡,警方通過查閱死者的電腦和手機敛摘,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,305評論 3 395
  • 文/潘曉璐 我一進店門门烂,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人兄淫,你說我怎么就攤上這事屯远。” “怎么了捕虽?”我有些...
    開封第一講書人閱讀 165,138評論 0 355
  • 文/不壞的土叔 我叫張陵慨丐,是天一觀的道長。 經(jīng)常有香客問我泄私,道長房揭,這世上最難降的妖魔是什么备闲? 我笑而不...
    開封第一講書人閱讀 58,791評論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮捅暴,結(jié)果婚禮上恬砂,老公的妹妹穿的比我還像新娘。我一直安慰自己伶唯,他們只是感情好觉既,可當我...
    茶點故事閱讀 67,794評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著乳幸,像睡著了一般。 火紅的嫁衣襯著肌膚如雪钧椰。 梳的紋絲不亂的頭發(fā)上粹断,一...
    開封第一講書人閱讀 51,631評論 1 305
  • 那天,我揣著相機與錄音嫡霞,去河邊找鬼瓶埋。 笑死,一個胖子當著我的面吹牛诊沪,可吹牛的內(nèi)容都是我干的养筒。 我是一名探鬼主播,決...
    沈念sama閱讀 40,362評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼端姚,長吁一口氣:“原來是場噩夢啊……” “哼晕粪!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起渐裸,我...
    開封第一講書人閱讀 39,264評論 0 276
  • 序言:老撾萬榮一對情侶失蹤巫湘,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后昏鹃,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體尚氛,經(jīng)...
    沈念sama閱讀 45,724評論 1 315
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,900評論 3 336
  • 正文 我和宋清朗相戀三年洞渤,在試婚紗的時候發(fā)現(xiàn)自己被綠了阅嘶。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,040評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡载迄,死狀恐怖讯柔,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情宪巨,我是刑警寧澤磷杏,帶...
    沈念sama閱讀 35,742評論 5 346
  • 正文 年R本政府宣布,位于F島的核電站捏卓,受9級特大地震影響极祸,放射性物質(zhì)發(fā)生泄漏慈格。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,364評論 3 330
  • 文/蒙蒙 一遥金、第九天 我趴在偏房一處隱蔽的房頂上張望浴捆。 院中可真熱鬧,春花似錦稿械、人聲如沸选泻。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,944評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽页眯。三九已至,卻和暖如春厢呵,著一層夾襖步出監(jiān)牢的瞬間窝撵,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,060評論 1 270
  • 我被黑心中介騙來泰國打工襟铭, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留碌奉,地道東北人。 一個月前我還...
    沈念sama閱讀 48,247評論 3 371
  • 正文 我出身青樓寒砖,卻偏偏與公主長得像赐劣,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子哩都,可洞房花燭夜當晚...
    茶點故事閱讀 44,979評論 2 355

推薦閱讀更多精彩內(nèi)容