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阅读 211,423评论 6 491
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,147评论 2 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,019评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,443评论 1 283
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,535评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,798评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,941评论 3 407
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,704评论 0 266
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,152评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,494评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,629评论 1 340
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,295评论 4 329
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,901评论 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,742评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,978评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,333评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,499评论 2 348

推荐阅读更多精彩内容