Android OpenGL ES 9.2 位置滤镜

作者: Benhero | 来源:发表于2019-01-21 16:04 被阅读2次

    课程介绍

    本节介绍如何改变改变片元着色器内的坐标位置参数,从而让渲染的内容动起来或者达到一些特殊的效果。

    位置滤镜效果

    实现讲解

    本节课的核心原理是修改采样的纹理坐标。
    这是之前课程中的纹理坐标图,纹理默认传入的读取范围是(0,0)到(1,1)的范围内读取颜色值。

    ST纹理坐标

    如果对读取的位置进行调整修改,那么就可以做出各种各样的效果。比如缩放动画,让读取的范围改成(-1, -1)到(2, 2)。

    1. 位移滤镜

    /**
     * 位移滤镜
     *
     * @author Benhero
     * @date   2019-1-17
     */
    class TranslateFilter(context: Context) : BaseFilter(context, VERTEX_SHADER, FRAGMENT_SHADER) {
        companion object {
            const val FRAGMENT_SHADER = """
                    precision mediump float;
                    varying vec2 v_TexCoord;
                    uniform sampler2D u_TextureUnit;
                    uniform float xV;
                    uniform float yV;
    
                    vec2 translate(vec2 srcCoord, float x, float y) {
                        return vec2(srcCoord.x + x, srcCoord.y + y);
                    }
    
                    void main() {
                        vec2 offsetTexCoord = translate(v_TexCoord, xV, yV);
                        if (offsetTexCoord.x >= 0.0 && offsetTexCoord.x <= 1.0 &&
                            offsetTexCoord.y >= 0.0 && offsetTexCoord.y <= 1.0) {
                            gl_FragColor = texture2D(u_TextureUnit, offsetTexCoord);
                        }
                    }
                    """
        }
    
        private var xLocation: Int = 0
        private var yLocation: Int = 0
        private var startTime: Long = 0
    
        override fun onCreated() {
            super.onCreated()
            startTime = System.currentTimeMillis()
            xLocation = getUniform("xV")
            yLocation = getUniform("yV")
        }
    
        override fun onDraw() {
            super.onDraw()
            val intensity = Math.sin((System.currentTimeMillis() - startTime) / 1000.0) * 0.5
            GLES20.glUniform1f(xLocation, intensity.toFloat())
            GLES20.glUniform1f(yLocation, 0.0f)
        }
    }
    

    这个滤镜的核心有两个部分,一个是对纹理坐标的改变:

    vec2 translate(vec2 srcCoord, float x, float y) {
        return vec2(srcCoord.x + x, srcCoord.y + y);
    }
    

    另外一个是限定纹理的采样范围:

    if (offsetTexCoord.x >= 0.0 && offsetTexCoord.x <= 1.0 &&
        offsetTexCoord.y >= 0.0 && offsetTexCoord.y <= 1.0) {}
    

    这样子就可以控制超过了(0, 0)到(1, 1)范围的就不绘制。否则位移滤镜就会出现下面的效果:

    位移动画-无限制

    之所以会这样子的效果,是因为纹理采样的环绕方式问题。这里补充下《纹理绘制》章节没有讲解到的这个知识点。

    纹理环绕方式 - 图源自LearnOpenGLCN

    上图展示了超过(0, 0)到(1, 1)范围时,设置不同环绕方式的效果。在默认情况下,系统会采用GL_REPEAT模式。

    如果我们想要位移滤镜运动只有1只皮卡丘,那么可以设置GL_CLAMP_TO_BORDER模式。

    但是呢!

    这个模式,在Android OpenGL ES 2.0版本是没有的,只有等到了Android 24版本,也就是7.0版本,Android OpenGL ES 3.2的版本才引入的,详情可以参考API文档

    讲了一圈回来,要实现这个属性的效果,只能我们自行判断纹理坐标采样范围的来控制绘制实现。

    不过,若有更好的实现方式,请告诉我。

    2. 缩放滤镜

    /**
     * 缩放滤镜
     *
     * @author Benhero
     * @date   2019-1-16
     */
    class ScaleFilter(context: Context) : BaseFilter(context, VERTEX_SHADER, FRAGMENT_SHADER) {
        companion object {
            const val FRAGMENT_SHADER = """
                    precision mediump float;
                    varying vec2 v_TexCoord;
                    uniform sampler2D u_TextureUnit;
                    uniform float intensity;
    
                    vec2 scale(vec2 srcCoord, float x, float y) {
                        return vec2((srcCoord.x - 0.5) / x + 0.5, (srcCoord.y - 0.5) / y + 0.5);
                    }
    
                    void main() {
                        vec2 offsetTexCoord = scale(v_TexCoord, intensity, intensity);
                        if (offsetTexCoord.x >= 0.0 && offsetTexCoord.x <= 1.0 &&
                            offsetTexCoord.y >= 0.0 && offsetTexCoord.y <= 1.0) {
                            gl_FragColor = texture2D(u_TextureUnit, offsetTexCoord);
                        }
                    }
                    """
        }
    
        private var intensityLocation: Int = 0
        private var startTime: Long = 0
    
        override fun onCreated() {
            super.onCreated()
            startTime = System.currentTimeMillis()
            intensityLocation = getUniform("intensity")
        }
    
        override fun onDraw() {
            super.onDraw()
            val intensity = Math.abs(Math.sin((System.currentTimeMillis() - startTime) / 1000.0)) + 0.5
            GLES20.glUniform1f(intensityLocation, intensity.toFloat())
        }
    }
    

    缩放滤镜和平移滤镜的思路差不多,也需要限制纹理采样的范围。那么讲解下缩放的计算。

    再贴一次纹理坐标图:

    ST纹理坐标
    vec2 scale(vec2 srcCoord, float x, float y) {
        return vec2((srcCoord.x - 0.5) / x + 0.5, (srcCoord.y - 0.5) / y + 0.5);
    }
    

    代码中,参数srcCoord是原本的纹理坐标,也就是(0, 0)到(1, 1)范围内取值。参数x、y分别是两个方向的缩放比例。所以,要让图片变小成原来的二分之一,那么就需要让纹理的采样范围变大为原来的2倍。(对于这句话理解很重要,如果想不通的,可以回顾下第七节课关于纹理坐标和顶点坐标的映射关系)

    由于这个缩放的中心点在图的中心,是0.5,所以可以先计算当前片元距离中心点的距离,然后再进行拉伸指定的倍数,也就是(srcCoord.x - 0.5) / x的意义,最后再加上0.5,就是这个片元缩放后,距离中心点的距离。

    3. 完全克隆滤镜

    /**
     * 完全分身克隆滤镜
     *
     * @author Benhero
     * @date   2019/1/18
     */
    class CloneFullFilter(context: Context) : BaseFilter(context, VERTEX_SHADER, FRAGMENT_SHADER) {
        companion object {
            const val FRAGMENT_SHADER = """
                precision mediump float;
                varying vec2 v_TexCoord;
                uniform sampler2D u_TextureUnit;
                uniform float cloneCount;
                void main() {
                    gl_FragColor = texture2D(u_TextureUnit, v_TexCoord * cloneCount);
                }
            """
        }
    
        override fun onCreated() {
            super.onCreated()
            GLES20.glUniform1f(getUniform("cloneCount"), 3.0f)
        }
    }
    
    完全克隆滤镜

    这个滤镜的实现,是利用了纹理采样的环绕方式实现,如效果图中,将采样的范围改为(0, 0)到(3, 3)。

    需要补充的是向量的计算方式的知识:
    vec2(x, y) * z = vec(x * z, y * z);

    4. 部分克隆滤镜

    /**
     * 部分克隆滤镜
     *
     * @author Benhero
     * @date   2019/1/18
     */
    class ClonePartFilter(context: Context) : BaseFilter(context, VERTEX_SHADER,
            TextResourceReader.readTextFileFromResource(context, R.raw.filter_test)) {
        companion object {
            const val FRAGMENT_SHADER = """
                precision mediump float;
                varying vec2 v_TexCoord;
                uniform sampler2D u_TextureUnit;
                uniform float isVertical;
                uniform float isHorizontal;
                uniform float cloneCount;
                void main() {
                    vec4 source = texture2D(u_TextureUnit, v_TexCoord);
                    float coordX = v_TexCoord.x;
                    float coordY = v_TexCoord.y;
                    if (isVertical == 1.0) {
                        float width = 1.0 / cloneCount;
                        float startX = (1.0 - width) / 2.0;
                        coordX = mod(v_TexCoord.x, width) + startX;
                    }
                    if (isHorizontal == 1.0) {
                        float height = 1.0 / cloneCount;
                        float startY = (1.0 - height) / 2.0;
                        coordY = mod(v_TexCoord.y, height) + startY;
                    }
                    gl_FragColor = texture2D(u_TextureUnit, vec2(coordX, coordY));
                }
            """
        }
    
        override fun onCreated() {
            super.onCreated()
            GLES20.glUniform1f(getUniform("isVertical"), 1.0f)
            GLES20.glUniform1f(getUniform("isHorizontal"), 1.0f)
            GLES20.glUniform1f(getUniform("cloneCount"), 3.0f)
        }
    }
    
    部分克隆滤镜

    这个效果可能你会困惑这个滤镜这么丑,有什么用?嗯,是需要换个素材来解释下会比较好。这时候,需要来个小公举~

    Jay

    而通过使用部分克隆滤镜,就可以得到两个帅气的小公举。

    部分克隆滤镜-Jay

    周杰伦看到这个效果,都会说:“哎哟,不错哦!”

    回归正题,讲解这个滤镜的逻辑:

    效果实现

    需求:当X方向上需要克隆N个图片,那么在原图中心取原图大小的N分之一,粘贴复制。

    计算:上图克隆2份的效果,那么需要在原图中心的裁处中心区域,原图大小为1920*1440,也就是需要裁出960×1440的区域。那么是从哪里开始裁才是中心点呢?应该是(1920 - 960) / 2 = 480的位置作为x方向上的起始点。

    同理,需要裁N份,需要的参数如下:

    • 显示区域大小:DisplayWidth = Width / N;
    • 裁剪的起始点:startX = (Width - DisplayWidth) / 2;

    GLSL分析

    在片段着色器中的,关键的代码如下:

    float width = 1.0 / cloneCount;
    float startX = (1.0 - width) / 2.0;
    coordX = mod(v_TexCoord.x, width) + startX;
    

    在上面我们已经分析过前面两行的计算原理,那么第三行,需要先介绍mod这个方法,是取余的作用,在GLSL中不可以使用Kotlin、Java中的百分号%来代表取余的意思,需要用mod这个方法。

    mod(v_TexCoord.x, width)计算出了当前每个片段的坐标点,在重复的片段中的坐标,再加上startX就可以将原始坐标转换出克隆片段的坐标。(示意图就不放上来了,解释到这里,读者可自己画一下图就明白这整个滤镜的计算方式了)

    GLSL日志

    讲解到这里,读者应该多多少少写了一些GLSL代码了,不过在过程中可能会遇到一些Bug,不明白怎么哪里就黑屏了,什么东西都没展示,所以这里讲解下如何看日志。

    1. 自己打Log,是不可能的!这个搜索过,直接Debug的方式是没有的,因为GLSL在GPU内跑,没有提供打日志的地方,所以,如果想要调试效果,可以通过自己改变画面的内容来验证自己的思路。比如符合某个条件,画面都是某个固定颜色。
    2. 如果GLSL编译不通过,是有日志的:Adreno|GLConsumer,在Logcat上加上这个Tag就可以看到一些编译信息。
    编译错误示范:
    2019-01-19 15:07:43.979 20637-20672/com.benhero.glstudio I/Adreno: ERROR: 0:14: '%' :  supported in pack/unpack shaders only  
        ERROR: 0:14: '%' :  wrong operand types  no operation '%' exists that takes a left-hand operand of type 'float' and a right operand of type 'float' (or there is no acceptable conversion)
        ERROR: 2 compilation errors.  No code generated.
    

    这个表示了第14行百分号%使用错误,导致了编译有问题。嗯,取余还是用mod吧。

    更多GLSL的方法,请看-GLSL 中文手册

    编译正确示范:
    2019-01-19 15:21:15.817 21155-21187/? I/Adreno: QUALCOMM build                   : 8e3df98, Ie4790512f3
        Build Date                       : 04/11/18
        OpenGL ES Shader Compiler Version: EV031.22.00.01
        Local Branch                     : 
        Remote Branch                    : quic/gfx-adreno.lnx.1.0.r36-rel
        Remote Branch                    : NONE
        Reconstruct Branch               : NOTHING
    

    其他

    GitHub工程

    本系列课程所有相关代码请参考我的GitHub项目⭐GLStudio⭐,喜欢的请给个小星星。😃

    相关文章

      网友评论

        本文标题:Android OpenGL ES 9.2 位置滤镜

        本文链接:https://www.haomeiwen.com/subject/rhicjqtx.html