Android OpenGL ES 9.2 位置滤镜

课程介绍

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

《Android OpenGL ES 9.2 位置滤镜》 位置滤镜效果

实现讲解

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

《Android OpenGL ES 9.2 位置滤镜》 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)范围的就不绘制。否则位移滤镜就会出现下面的效果:

《Android OpenGL ES 9.2 位置滤镜》 位移动画-无限制

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

《Android OpenGL ES 9.2 位置滤镜》 纹理环绕方式 – 图源自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())
    }
}

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

再贴一次纹理坐标图:

《Android OpenGL ES 9.2 位置滤镜》 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)
    }
}

《Android OpenGL ES 9.2 位置滤镜》 完全克隆滤镜

这个滤镜的实现,是利用了纹理采样的环绕方式实现,如效果图中,将采样的范围改为(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)
    }
}

《Android OpenGL ES 9.2 位置滤镜》 部分克隆滤镜

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

《Android OpenGL ES 9.2 位置滤镜》 Jay

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

《Android OpenGL ES 9.2 位置滤镜》 部分克隆滤镜-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⭐,喜欢的请给个小星星。😃

    原文作者:Benhero
    原文地址: https://www.jianshu.com/p/87ccc9bfa362
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞