Android OpenGL ES 9.1 基礎顏色濾鏡

課程介紹

本節介紹濾鏡基礎框架+基礎顏色濾鏡。

課程效果.gif

基礎框架

這節課我們開始講濾鏡的開發,為了便于展示各種濾鏡的效果,設計了一套簡易的框架,分兩部分。

1. 濾鏡的基類

主要的生命周期方法如下:

  • onCreated:創建的時候
  • onSizeChanged:濾鏡尺寸改變
  • onDraw:繪制每一幀
  • onDestroy:銷毀,用于回收無用資源
    而實現基礎濾鏡的時候,只需要復寫基類的構造方法即可。使用如下:
/**
 * 基礎濾鏡
 *
 * @author Benhero
 * @date   2018/11/28
 */
open class BaseFilter(val context: Context, val vertexShader: String = VERTEX_SHADER, val fragmentShader: String = FRAGMENT_SHADER) {
    companion object {
        val VERTEX_SHADER = """
                uniform mat4 u_Matrix;
                attribute vec4 a_Position;
                attribute vec2 a_TexCoord;
                varying vec2 v_TexCoord;
                void main() {
                    v_TexCoord = a_TexCoord;
                    gl_Position = u_Matrix * a_Position;
                }
                """
        val FRAGMENT_SHADER = """
                precision mediump float;
                varying vec2 v_TexCoord;
                uniform sampler2D u_TextureUnit;
                void main() {
                    gl_FragColor = texture2D(u_TextureUnit, v_TexCoord);
                }
                """

        private val POSITION_COMPONENT_COUNT = 2

        private val POINT_DATA = floatArrayOf(-1.0f, -1.0f, -1.0f, 1.0f, 1.0f, 1.0f, 1.0f, -1.0f)

        /**
         * 紋理坐標
         */
        private val TEX_VERTEX = floatArrayOf(0f, 1f, 0f, 0f, 1f, 0f, 1f, 1f)

        /**
         * 紋理坐標中每個點占的向量個數
         */
        private val TEX_VERTEX_COMPONENT_COUNT = 2
    }

    private val mVertexData: FloatBuffer

    private var uTextureUnitLocation: Int = 0
    private val mTexVertexBuffer: FloatBuffer
    /**
     * 紋理數據
     */
    var textureBean: TextureHelper.TextureBean? = null
    private var projectionMatrixHelper: ProjectionMatrixHelper? = null

    var program = 0

    init {
        mVertexData = BufferUtil.createFloatBuffer(POINT_DATA)
        mTexVertexBuffer = BufferUtil.createFloatBuffer(TEX_VERTEX)
    }


    public open fun onCreated() {
        makeProgram(vertexShader, fragmentShader)
        val aPositionLocation = getAttrib("a_Position")
        projectionMatrixHelper = ProjectionMatrixHelper(program, "u_Matrix")
        // 紋理坐標索引
        val aTexCoordLocation = getAttrib("a_TexCoord")
        uTextureUnitLocation = getUniform("u_TextureUnit")

        mVertexData.position(0)
        GLES20.glVertexAttribPointer(aPositionLocation, POSITION_COMPONENT_COUNT,
                GLES20.GL_FLOAT, false, 0, mVertexData)
        GLES20.glEnableVertexAttribArray(aPositionLocation)

        // 加載紋理坐標
        mTexVertexBuffer.position(0)
        GLES20.glVertexAttribPointer(aTexCoordLocation, TEX_VERTEX_COMPONENT_COUNT, GLES20.GL_FLOAT, false, 0, mTexVertexBuffer)
        GLES20.glEnableVertexAttribArray(aTexCoordLocation)

        GLES20.glClearColor(0f, 0f, 0f, 1f)
        // 開啟紋理透明混合,這樣才能繪制透明圖片
        GLES20.glEnable(GL10.GL_BLEND)
        GLES20.glBlendFunc(GL10.GL_ONE, GL10.GL_ONE_MINUS_SRC_ALPHA)
    }

    public open fun onSizeChanged(width: Int, height: Int) {
        GLES20.glViewport(0, 0, width, height)
        projectionMatrixHelper!!.enable(width, height)
    }

    public open fun onDraw() {
        GLES20.glClear(GL10.GL_COLOR_BUFFER_BIT)
        // 紋理單元:在OpenGL中,紋理不是直接繪制到片段著色器上,而是通過紋理單元去保存紋理

        // 設置當前活動的紋理單元為紋理單元0
        GLES20.glActiveTexture(GLES20.GL_TEXTURE0)

        // 將紋理ID綁定到當前活動的紋理單元上
        GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textureBean?.textureId ?: 0)

        // 將紋理單元傳遞片段著色器的u_TextureUnit
        GLES20.glUniform1i(uTextureUnitLocation, 0)

        GLES20.glDrawArrays(GLES20.GL_TRIANGLE_FAN, 0, POINT_DATA.size / POSITION_COMPONENT_COUNT)
    }

    public open fun onDestroy() {
        GLES20.glDeleteProgram(program)
        program = 0
    }

    /**
     * 創建OpenGL程序對象
     *
     * @param vertexShader   頂點著色器代碼
     * @param fragmentShader 片段著色器代碼
     */
    protected fun makeProgram(vertexShader: String, fragmentShader: String) {
        // 步驟1:編譯頂點著色器
        val vertexShaderId = ShaderHelper.compileVertexShader(vertexShader)
        // 步驟2:編譯片段著色器
        val fragmentShaderId = ShaderHelper.compileFragmentShader(fragmentShader)
        // 步驟3:將頂點著色器、片段著色器進行鏈接,組裝成一個OpenGL程序
        program = ShaderHelper.linkProgram(vertexShaderId, fragmentShaderId)

        if (LoggerConfig.ON) {
            ShaderHelper.validateProgram(program)
        }

        // 步驟4:通知OpenGL開始使用該程序
        GLES20.glUseProgram(program)
    }

    protected fun getUniform(name: String): Int {
        return GLES20.glGetUniformLocation(program, name)
    }

    protected fun getAttrib(name: String): Int {
        return GLES20.glGetAttribLocation(program, name)
    }
}

2. 濾鏡加載

/**
 * 濾鏡渲染
 *
 * @author Benhero
 */
class L8_1_FilterRenderer(context: Context) : BaseRenderer(context) {
    val filterList = ArrayList<BaseFilter>()
    var drawIndex = 0
    var isChanged = false
    var currentFilter: BaseFilter
    var textureBean: TextureHelper.TextureBean? = null

    init {
        filterList.add(BaseFilter(context))
        filterList.add(GrayFilter(context))
        filterList.add(InverseFilter(context))
        filterList.add(LightUpFilter(context))
        currentFilter = filterList.get(0)
    }

    override fun onSurfaceCreated(glUnused: GL10, config: EGLConfig) {
        currentFilter.onCreated()
        textureBean = TextureHelper.loadTexture(context, R.drawable.pikachu)
    }

    override fun onSurfaceChanged(gl: GL10?, width: Int, height: Int) {
        super.onSurfaceChanged(gl, width, height)
        currentFilter.onSizeChanged(width, height)
        currentFilter.textureBean = textureBean
    }

    override fun onDrawFrame(glUnused: GL10) {
        if (isChanged) {
            currentFilter = filterList.get(drawIndex)

            filterList.forEach {
                if (it != currentFilter) {
                    it.onDestroy()
                }
            }

            currentFilter.onCreated()
            currentFilter.onSizeChanged(outputWidth, outputHeight)
            currentFilter.textureBean = textureBean
            isChanged = false
        }

        currentFilter.onDraw()
    }

    override fun onClick() {
        super.onClick()
        drawIndex++
        drawIndex = if (drawIndex >= filterList.size) 0 else drawIndex
        isChanged = true
    }
}

濾鏡入門

完成了濾鏡框架后,我們就可以開始真正地編寫濾鏡功能。本節課程的濾鏡,基本都是集中在Fragment Shader上,而涉及到Vertex Shader的濾鏡不在本次課程上講解(其實也不難入門)。

1. 反色濾鏡

/**
 * 反色濾鏡
 *
 * @author Benhero
 * @date   2018/11/28
 */
class InverseFilter(context: Context) : BaseFilter(context, VERTEX_SHADER, INVERSE_FRAGMENT_SHADER) {
    companion object {
        val INVERSE_FRAGMENT_SHADER = """
                precision mediump float;
                varying vec2 v_TexCoord;
                uniform sampler2D u_TextureUnit;
                void main() {
                    vec4 src = texture2D(u_TextureUnit, v_TexCoord);
                    gl_FragColor = vec4(1.0 - src.r, 1.0 - src.g, 1.0 - src.b, 1.0);
                }
                """
    }
}

反色濾鏡算是最簡單的濾鏡了,所以這邊拿它來開講,那么接下來會從最基礎的內容開始講起,可能會之前有重復。

  1. 濾鏡實現思路:RGB三個通道的顏色都取反,而alpha通道不變。
  2. precision mediump float; 這行非常重要,它聲明了接下來所有浮點型類型的默認精度(某些變量、常亮需要其他精度可以單獨指定),若不聲明,在有部分手機上會有黑屏、崩潰等莫名其妙的問題。
  3. 在GLSL中,float類型可以不帶f結尾,但是不能不帶點,正確的格式如1.0和1. 。

2. 灰色濾鏡

/**
 * 灰色濾鏡
 *
 * @author Benhero
 * @date   2018/11/28
 */
class GrayFilter(context: Context) : BaseFilter(context, VERTEX_SHADER, GRAY_FRAGMENT_SHADER) {
    companion object {
        val GRAY_FRAGMENT_SHADER = """
                precision mediump float;
                varying vec2 v_TexCoord;
                uniform sampler2D u_TextureUnit;
                void main() {
                    vec4 src = texture2D(u_TextureUnit, v_TexCoord);
                    float gray = (src.r + src.g + src.b) / 3.0;
                    gl_FragColor =vec4(gray, gray, gray, 1.0);
                }
                """
    }
}
  1. 濾鏡實現思路:讓RGB三個通道的顏色取均值

3. 發光濾鏡

/**
 * 發光濾鏡
 *
 * @author Benhero
 * @date   2018/11/28
 */
class LightUpFilter(context: Context) : BaseFilter(context, VERTEX_SHADER, INVERSE_FRAGMENT_SHADER) {
    companion object {
        val INVERSE_FRAGMENT_SHADER = """
                precision mediump float;
                varying vec2 v_TexCoord;
                uniform sampler2D u_TextureUnit;
                uniform float uTime;
                void main() {
                    float lightUpValue = abs(sin(uTime / 1000.0)) / 4.0;
                    vec4 src = texture2D(u_TextureUnit, v_TexCoord);
                    vec4 addColor = vec4(lightUpValue, lightUpValue, lightUpValue, 1.0);
                    gl_FragColor = src + addColor;
                }
                """
    }

    private var uTime: Int = 0
    private var startTime: Long = 0

    override public fun onCreated() {
        super.onCreated()
        startTime = System.currentTimeMillis()
        uTime = getUniform("uTime")
    }

    override public fun onDraw() {
        super.onDraw()
        GLES20.glUniform1f(uTime, (System.currentTimeMillis() - startTime).toFloat())
    }
}

這個濾鏡時本節最難的濾鏡,而且是一步引入了2個新的內容:新參數的傳遞方式、周期變化濾鏡的實現。

  1. 新參數的傳遞方式:這個濾鏡傳入的是一個濾鏡執行時間 的參數,需要實時更新,所以在onCreated的時候創建引用,在onDraw的時候不停地去更新參數值。
  2. 周期變換:要實現周期變換,這里使用的是sin正弦函數,y值會隨著x的變換做周期變換,具體效果大家懂的。這里除以1000.0讓x是個位數的變換,abs是為了讓濾鏡是變亮,而沒有變暗的效果,除以4是為了減弱變亮的幅度,讓增加的亮度值控制在0到0.25之間。
  3. 向量的計算:除了拆解成每個矢量上的相加減之外,還可以直接兩個向量相加減 gl_FragColor = src + addColor;
性能優化

這個濾鏡案例有個比較大的問題,就是性能相對較差,有優化空間。我們知道,每個Fragment Shader在每一幀執行次數是分解出來的片元數,那么也就是說,一幀會執行成千上萬次。所以,我們應該避免將有些沒必要的計算放在Fragment Shader里,而是放在CPU里一次計算好,再傳進去,這樣可以大大減少沒必要的消耗。所以優化的代碼如下:

/**
 * 發光濾鏡
 *
 * @author Benhero
 * @date   2018/11/28
 */
class LightUpFilter(context: Context) : BaseFilter(context, VERTEX_SHADER, INVERSE_FRAGMENT_SHADER) {
    companion object {
        val INVERSE_FRAGMENT_SHADER = """
                precision mediump float;
                varying vec2 v_TexCoord;
                uniform sampler2D u_TextureUnit;
                uniform float intensity;
                void main() {
                    vec4 src = texture2D(u_TextureUnit, v_TexCoord);
                    vec4 addColor = vec4(intensity, intensity, intensity, 1.0);
                    gl_FragColor = src + addColor;
                }
                """
    }

    private var intensityLocation: Int = 0
    private var startTime: Long = 0

    override public fun onCreated() {
        super.onCreated()
        startTime = System.currentTimeMillis()
        intensityLocation = getUniform("intensity")
    }

    override public fun onDraw() {
        super.onDraw()
        val intensity = Math.abs(Math.sin((System.currentTimeMillis() - startTime) / 1000.0)) / 4.0
        GLES20.glUniform1f(intensityLocation, intensity.toFloat())
    }
}

注意點

  1. gl_FragColor賦值的時候,一定要對alpha通道進行賦值,否則在一些機型上出現問題,默認設置1.0即可。(在Google Pixel上合成視頻時,某個濾鏡沒有設置alpha通道,導致濾鏡不生效)
  2. 若向GLSL中傳遞的值,但在方法內沒有用到,則會報bindTextureImage: clearing GL error: 0x501

建議

  1. 推薦大家使用Kotlin來編寫濾鏡功能,因為使用本節課中三個引號的String拼接方式,會比Java中兩個引號的方式方便太多了,不然每次換行都要寫引號、加號,特別麻煩,容易出錯。另外,還可以先把濾鏡放在GLSL文件格式下,Android Studio有特定的編輯器渲染,更好看。

拓展資料

參考

Android OpenGL ES學習資料所列舉的博客、資料。

GitHub代碼工程

本系列課程所有相關代碼請參考我的GitHub項目GLStudio

課程目錄

本系列課程目錄詳見 簡書 - Android OpenGL ES教程規劃

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,345評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,494評論 3 416
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,283評論 0 374
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,953評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,714評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,186評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,255評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,410評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,940評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,776評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,976評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,518評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,210評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,642評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,878評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,654評論 3 391
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,958評論 2 373

推薦閱讀更多精彩內容