Android OpenGL ES 位置滤镜

位置滤镜介绍


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


位置滤镜效果
github


实现讲解


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

ST纹理坐标微信


若是对读取的位置进行调整修改,那么就能够作出各类各样的效果。好比缩放动画,让读取的范围改为 (-1, -1) 到 (2, 2)。app


1. 位移滤镜


/**
 * 位移滤镜
 *
 * @author Benhero
 * @date   2019-1-17
 */

class TranslateFilter(contextContext) : BaseFilter(contextVERTEX_SHADERFRAGMENT_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);
}


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


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


这样子就能够控制超过了 (0, 0) 到( 1, 1) 范围的就不绘制。不然位移滤镜就会出现下面的效果:学习

位移动画-无限制flex


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


纹理环绕方式 - 图源自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(contextContext) : BaseFilter(contextVERTEX_SHADERFRAGMENT_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(contextContext) : BaseFilter(contextVERTEX_SHADERFRAGMENT_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(contextContext) : BaseFilter(contextVERTEX_SHADER,
        TextResourceReader.readTextFileFromResource(contextR.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,不明白怎么哪里就黑屏了,什么东西都没展现,因此这里讲解下如何看日志。


  • 本身打 Log,是不可能的!这个搜索过,直接 Debug 的方式是没有的,由于GLSL 在 GPU 内跑,没有提供打日志的地方,因此,若是想要调试效果,能够经过本身改变画面的内容来验证本身的思路。好比符合某个条件,画面都是某个固定颜色。

  • 若是 GLS 编译不经过,是有日志的: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.


这个表示了第1 4 行百分号 % 使用错误,致使了编译有问题。嗯,取余仍是用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工程

https://github.com/benhero/GLStudio


做者:Benhero
连接:https://www.jianshu.com/p/87ccc9bfa362


-- END --


进技术交流群,扫码添加个人微信:Byte-Flow



获取视频教程和源码



推荐:

字节流动 OpenGL ES 技术交流群来啦

Android OpenGL 渲染图像读取哪家强?

FFmpeg + OpenGL ES 实现 3D 全景播放器

FFmpeg + OpenGLES 实现视频解码播放和视频滤镜

一文掌握 YUV 图像的基本处理

Android OpenGL ES 从入门到精通系统性学习教程

OpenGL ES 实现动态(水波纹)涟漪效果


以为不错,点个在看呗~

本文分享自微信公众号 - 字节流动(google_developer)。
若有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一块儿分享。

相关文章
相关标签/搜索