美文网首页Android开发Android开发经验谈Android知识
当一个 Android 开发玩抖音玩疯了之后(二)

当一个 Android 开发玩抖音玩疯了之后(二)

作者: Mr_villain | 来源:发表于2018-09-11 11:22 被阅读214次

    上一篇文章中,我大概介绍了一下短视频的拍摄,主要就是音视频的加减速。这篇文章我将介绍下抖音视频特效的实现,废话不多说,进入正题。

    1.特效概览

    特效列表 特效列表

    抖音上目前有这九种视频特效,本文将介绍前面六种的实现。有人可能会问了,为什么最后三种特效被忽略了。

    当然是因为我懒啦。

    没想到吧

    2.『灵魂出窍』

    抖音的实现效果如下:

    灵魂出窍

    我的实现效果如下:

    ezgif.com-rotate.gif

    代码实现

    通过观察抖音的效果,可以看到,共有两个图层,一个是视频原图,还有一个是从中心放大并且透明度逐渐减小的图层,关键代码如下。

    2.1 顶点着色器
    uniform mat4 uTexMatrix;
    attribute vec2 aPosition;
    attribute vec4 aTextureCoord;
    varying vec2 vTextureCoord;
    uniform mat4 uMvpMatrix;
    void main(){
        gl_Position = uMvpMatrix * vec4(aPosition,0.1,1.0);
        vTextureCoord = (uTexMatrix * aTextureCoord).xy;
    }
    
    2.2 片元着色器
    #extension GL_OES_EGL_image_external : require
    precision mediump float;
    varying vec2 vTextureCoord;
    uniform samplerExternalOES uTexture;
    uniform float uAlpha;
    void main(){
        gl_FragColor = vec4(texture2D(uTexture,vTextureCoord).rgb,uAlpha);
    }
    

    这两部分代码比较简单,没有什么特殊的操作,就是单纯地把纹理渲染到内存中

    2.3动画代码
    //当前动画进度
    private float mProgress = 0.0f;
    //当前地帧数
    private int mFrames = 0;
    //动画最大帧数
    private static final int mMaxFrames = 15;
    //动画完成后跳过的帧数
    private static final int mSkipFrames = 8;
    //放大矩阵
    private float[] mMvpMatrix = new float[16];
    //opengl 参数位置
    private int mMvpMatrixLocation;
    private int mAlphaLocation;
    public void onDraw(int textureId,float[] texMatrix){
            //因为这里是两个图层,所以开启混合模式
            glEnable(GL_BLEND);
            glBlendFunc(GL_SRC_ALPHA, GL_DST_ALPHA);
            mProgress = (float) mFrames / mMaxFrames;
            if (mProgress > 1f) {
                mProgress = 0f;
            }
            mFrames++;
            if (mFrames > mMaxFrames + mSkipFrames) {
                mFrames = 0;
            }
            Matrix.setIdentityM(mMvpMatrix, 0);//初始化矩阵
            //第一帧是没有放大的,所以这里直接赋值一个单位矩阵
            glUniformMatrix4fv(mMvpMatrixLocation, 1, false, mMvpMatrix, 0);
            //底层图层的透明度
            float backAlpha = 1f;
            //放大图层的透明度
            float alpha = 0f;
            if (mProgress > 0f) {
                alpha = 0.2f - mProgress * 0.2f;
                backAlpha = 1 - alpha;
            }
            glUniform1f(mAlphaLocation, backAlpha);
            glUniformMatrix4fv(mUniformTexMatrixLocation, 1, false, texMatrix, 0);
            //初始化顶点着色器数据,包括纹理坐标以及顶点坐标
            mRendererInfo.getVertexBuffer().position(0);
            glVertexAttribPointer(mAttrPositionLocation, 2,
                    GL_FLOAT, false, 0, mRendererInfo.getVertexBuffer());
            mRendererInfo.getTextureBuffer().position(0);
            glVertexAttribPointer(mAttrTexCoordLocation, 2,
                    GL_FLOAT, false, 0, mRendererInfo.getTextureBuffer());
            GLES20.glActiveTexture(GLES20.GL_TEXTURE0);
            GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, textureId);
            //绘制底部原图
            GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4);
            if (mProgress > 0f) {
                //这里绘制放大图层
                glUniform1f(mAlphaLocation, alpha);
                float scale = 1.0f + 1f * mProgress;
                Matrix.scaleM(mMvpMatrix, 0, scale, scale, scale);
                glUniformMatrix4fv(mMvpMatrixLocation, 1, false, mMvpMatrix, 0);
                GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4);
            }
    
            GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, 0);
            GLES20.glUseProgram(0);
            glDisable(GL_BLEND);
    }
    

    以上代码最终绘制出来的就是 『灵魂出窍』的效果

    3.『抖动』

    抖音的实现效果如下:

    shake

    我的实现效果如下:

    ezgif-4-d0c993e10f.gif

    代码实现

    要做这个效果前,我们先分析下抖音的效果。这个特效总共包含两个部分的内容:

    • 中心放大
    • 颜色偏移

    我们把视频暂停截图之后,可以看到如下的图:

    WX20180910-191159.png

    从图上我们可以看到,键盘里的原文字变成了蓝色,而左上角和右下角分别多了绿色和红色的字,那么这个颜色分离就是将一个像素的RGB值分别分离出去。

    2.1 顶点着色器
    uniform mat4 uTexMatrix;
    attribute vec2 aPosition;
    attribute vec4 aTextureCoord;
    varying vec2 vTextureCoord;
    uniform mat4 uMvpMatrix;
    void main(){
        gl_Position = uMvpMatrix * vec4(aPosition,0.1,1.0);
        vTextureCoord = (uTexMatrix * aTextureCoord).xy;
    }
    
    2.2 片元着色器
    #extension GL_OES_EGL_image_external : require
    precision mediump float;
    varying vec2 vTextureCoord;
    uniform samplerExternalOES uTexture;
    //颜色的偏移距离
    uniform float uTextureCoordOffset;
    void main(){
        vec4 blue = texture2D(uTexture,vTextureCoord);
        vec4 green = texture2D(uTexture,vec2(vTextureCoord.x + uTextureCoordOffset,vTextureCoord.y + uTextureCoordOffset));
        vec4 red = texture2D(uTexture,vec2(vTextureCoord.x - uTextureCoordOffset,vTextureCoord.y - uTextureCoordOffset));
        gl_FragColor = vec4(red.x,green.y,blue.z,blue.w);
    }
    

    这里分析下片元着色器的代码,要实现像素偏移,首先我们要明白的一点是,片元着色器是针对每个像素生效的,代码中的vTextureCoord包含了当前像素的坐标(x,y),x和y分别都是从0到1。如果要将像素的颜色分离,那么我们只需要将texture2D函数中的坐标进行转换就行了。举个栗子,(0.1,0.1)的点上有个白色像素,当前像素的坐标是(0.0,0.0),我们要让白色像素的绿色分量显示在当前像素的位置上,那么我们可以将当前像素的x、y坐标全部加上0.1,那么实际产生的效果就是那个白色像素向左上角偏移了。红色值偏移也是类似的意思,拿到左上角和右下角的像素的红绿色值之后,跟当前的像素的蓝色色值进行组合,就形成了图片中的效果。

    2.3 动画关键代码
        private float[] mMvpMatrix = new float[16];
        private float mProgress = 0.0f;
        private int mFrames = 0;
        private static final int mMaxFrames = 8;
        private static final int mSkipFrames = 4;
        @Override
        protected void onDraw(int textureId, float[] texMatrix) {
            mProgress = (float) mFrames / mMaxFrames;
            if (mProgress > 1f) {
                mProgress = 0f;
            }
            mFrames++;
            if (mFrames > mMaxFrames + mSkipFrames) {
                mFrames = 0;
            }
            float scale = 1.0f + 0.2f * mProgress;
            Matrix.setIdentityM(mMvpMatrix, 0);
            //设置放大的百分比
            Matrix.scaleM(mMvpMatrix, 0, scale, scale, 1.0f);
            glUniformMatrix4fv(mMvpMatrixLocation, 1, false, mMvpMatrix, 0);
            //设置色值偏移的量
            float textureCoordOffset = 0.01f * mProgress;
            glUniform1f(mTextureCoordOffsetLocation, textureCoordOffset);
            super.onDraw(textureId, texMatrix);
        }
    

    4.『毛刺』

    抖音效果图:

    毛刺

    我的实现效果图:

    毛刺

    『毛刺』的效果还原的不是很完整,动画的参数没有调整好。

    代码实现

    看到这个效果,我们先分析一下,将视频逐帧分析,可以看到以下的截图:

    毛刺截图

    仔细观察这个图片,我们可以发现,其实毛刺效果就是某一行像素值偏移了一段距离,看着就像是图片被撕裂了,并且这个偏移是随着y轴随机变化的,这样看起来效果更自然,并且观察gif图可以看到,除了撕裂,还有个色值偏移的效果。色值偏移在介绍 "抖动" 效果时已经讲过了,那么这里只要解决撕裂效果就可以了。

    4.1 顶点着色器
    uniform mat4 uTexMatrix;
    attribute vec2 aPosition;
    attribute vec4 aTextureCoord;
    varying vec2 vTextureCoord;
    uniform mat4 uMvpMatrix;
    void main(){
        gl_Position = uMvpMatrix * vec4(aPosition,0.1,1.0);
        vTextureCoord = (uTexMatrix * aTextureCoord).xy;
    }
    
    4.2 片元着色器
    #extension GL_OES_EGL_image_external : require
    precision highp float;
    varying vec2 vTextureCoord;
    uniform samplerExternalOES uTexture;
    //这是个二阶向量,x是横向偏移的值,y是阈值
    uniform vec2 uScanLineJitter;
    //颜色偏移的值
    uniform float uColorDrift;
    //随机函数
    float nrand(in float x, in float y){
        return fract(sin(dot(vec2(x, y), vec2(12.9898, 78.233))) * 43758.5453);
    }
    
    void main(){
        float u = vTextureCoord.x;
        float v = vTextureCoord.y;
        float jitter = nrand(v,0.0) * 2.0 - 1.0;
        float drift = uColorDrift;
        float offsetParam = step(uScanLineJitter.y,abs(jitter));
        jitter = jitter * offsetParam * uScanLineJitter.x;
        vec4 color1 = texture2D(uTexture,fract(vec2( u + jitter,v)));
        vec4 color2 = texture2D(uTexture,fract(vec2(u + jitter + v*drift ,v)));
        gl_FragColor = vec4(color1.r,color2.g,color1.b,1.0);
    }
    

    这里重点讲解下片元着色器的代码,随机函数就是代码中的nrand函数

    fract、dot和sin是opengl自带的函数,意思是取某个数的小数部分,即fract(x) = x - floor(x);
    dot是向量点乘,sin就是正弦函数

    如上代码所示,我们首先取出当前像素的x、y的值,然后用y去计算随机数

    float jitter = nrand(v,0.0) * 2.0 - 1.0;//这里得到一个-1到1的数
    

    然后接下来,我们计算当前这一行的像素要往左偏,还是往右偏

    float offsetParam = step(uScanLineJitter.y,abs(jitter));//step是gl自带函数,意思是,如果第一个参数大于第二个参数,那么返回0,否则返回1
    

    所以这句话的意思就是,判断当前的随机数是否大于某个阈值,如果大于这个阈值,那么就偏移,否则就不偏移。通过控制这个阈值,我们可以改变当前视频的混乱度(越混乱,撕裂的像素就越多)

    接着是计算某行像素的偏移值

    jitter = jitter * offsetParam * uScanLineJitter.x;//offsetParam如果是0,就不便宜了,如果是1,就偏移jitter*uScanLineJitter.x的距离,其中uScanLineJitter.x是最大偏移值
    //这里计算最终的像素值,纹理坐标是0到1之间的数,如果小于0,那么图像就捅到屏幕右边去,如果超过1,那么就捅到屏幕左边去。
    vec4 color1 = texture2D(uTexture,fract(vec2( u + jitter,v)));
    vec4 color2 = texture2D(uTexture,fract(vec2(u + jitter + v*drift ,v)));
    
    4.3 动画代码

    动画代码这里就不贴了,大概就是根据当前帧数控制

    //这是个二阶向量,x是横向偏移的值,y是阈值
    uniform vec2 uScanLineJitter;
    //颜色偏移的值
    uniform float uColorDrift;
    

    这两个参数的值,uScanLineJitter.x越大,横向撕裂的距离就越大;uScanLineJitter.y越大,屏幕上被撕裂的像素就越多

    5.『缩放』

    抖音效果图:

    缩放

    我的实现效果图:

    缩放

    代码实现

    这个效果比较简单,就是放大然后缩小 不停地循环

    5.1顶点着色器
    uniform mat4 uTexMatrix;
    attribute vec2 aPosition;
    attribute vec4 aTextureCoord;
    varying vec2 vTextureCoord;
    //缩放矩阵
    uniform mat4 uMvpMatrix;
    void main(){
        gl_Position = uMvpMatrix * vec4(aPosition,0.1,1.0);
        vTextureCoord = (uTexMatrix * aTextureCoord).xy;
    }
    
    5.2片元着色器
    #extension GL_OES_EGL_image_external : require
    precision mediump float;
    varying vec2 vTextureCoord;
    uniform samplerExternalOES uTexture;
    void main(){
        gl_FragColor = texture2D(uTexture,vTextureCoord);
    }
    
    5.3动画代码

    动画代码比较简单,就是控制缩放矩阵来放大缩小,关键代码如下:

        private int mScaleMatrixLocation;
        //最大缩放是1.3倍
        private static final float mScale = 0.3f;
        private int mFrames;
        //最大帧数是14帧,通过这个控制动画速度
        private int mMaxFrames = 14;
        private int mMiddleFrames = mMaxFrames / 2;
        private float[] mScaleMatrix = new float[16];
        public void onDraw(int textureId,float texMatrix[]){
            //初始化矩阵
            Matrix.setIdentityM(mScaleMatrix, 0);
            float progress;
            if (mFrames <= mMiddleFrames) {
                progress = mFrames * 1.0f / mMiddleFrames;
            } else {
                progress = 2f - mFrames * 1.0f / mMiddleFrames;
            }
            float scale = 1f + mScale * progress;
            Matrix.scaleM(mScaleMatrix, 0, scale, scale, scale);
            glUniformMatrix4fv(mScaleMatrixLocation, 1, false, mScaleMatrix, 0);
            mFrames++;
            if (mFrames > mMaxFrames) {
                mFrames = 0;
            }
            ...
        }
    

    6.『闪白』

    抖音实现效果图:

    闪白

    我的实现效果图:

    闪白

    代码实现

    这个效果比较简单,就是个相机过度曝光的感觉,具体实现就是给RGB的每个分量增加一个固定的值。

    6.1顶点着色器
    uniform mat4 uTexMatrix;
    attribute vec2 aPosition;
    attribute vec4 aTextureCoord;
    varying vec2 vTextureCoord;
    void main(){
        gl_Position = vec4(aPosition,0.1,1.0);
        vTextureCoord = (uTexMatrix * aTextureCoord).xy;
    }
    
    6.2片元着色器
    #extension GL_OES_EGL_image_external : require
    precision mediump float;
    varying vec2 vTextureCoord;
    uniform samplerExternalOES uTexture;
    //修改这个值,可以控制曝光的程度
    uniform float uAdditionalColor;
    void main(){
        vec4 color = texture2D(uTexture,vTextureCoord);
        gl_FragColor = vec4(color.r + uAdditionalColor,color.g + uAdditionalColor,color.b + uAdditionalColor,color.a);
    }
    
    6.3动画代码
    public void onDraw(int textureId,float[] texMatrix){        
            float progress;
            if (mFrames <= mHalfFrames) {
                progress = mFrames * 1.0f / mHalfFrames;
            } else {
                progress = 2.0f - mFrames * 1.0f / mHalfFrames;
            }
            mFrames++;
            if (mFrames > mMaxFrames) {
                mFrames = 0;
            }
            glUniform1f(mAdditionColorLocation, progress);
            ...绘制
    }
    

    7.『幻觉』

    抖音实现效果:

    huanjue.gif

    我的实现效果:

    huanjue1.gif

    代码实现

    第一次看到这个效果的时候,我是有点懵逼的,因为一点头绪都没有,当时只想把电脑扔了。

    throw-away-your-laptop

    后来逐帧分析的时候,还是发现了一丝端倪。这个特效大概可以总结为三个部分:

    • 滤镜
    • 残影
    • 残影颜色分离
    7.1 滤镜

    用两张图来对比一下,大家大概就知道了

    滤镜前

    滤镜前

    滤镜后

    751536631171_.pic.jpg

    可以看到,在使用了幻觉特效之后,图片有种偏暗蓝的感觉。这种情况下咋整?一般有两种选择,找视觉同学帮你还原,或者是,反编译apk包搜代码。我选择了后者。在将抖音apk解压之后,搜索资源文件,发现了一张图——lookup_vertigo.png,就是这个东东

    lut

    这个是啥呢?就是一个颜色查找表,滤镜可以通过代码手动转换颜色或者把颜色转换信息写在一个lut文件里,然后要用的时候直接从图片里查找即可。
    LUT文件使用代码如下:

    //这个是LUT文件的纹理
    uniform sampler2D uTexture2;
    vec4 lookup(in vec4 textureColor){
        mediump float blueColor = textureColor.b * 63.0;
        mediump vec2 quad1;
        quad1.y = floor(floor(blueColor) / 8.0);
        quad1.x = floor(blueColor) - (quad1.y * 8.0);
        mediump vec2 quad2;
        quad2.y = floor(ceil(blueColor) / 8.0);
        quad2.x = ceil(blueColor) - (quad2.y * 8.0);
        highp vec2 texPos1;
        texPos1.x = (quad1.x * 0.125) + 0.5/512.0 + ((0.125 - 1.0/512.0) * textureColor.r);
        texPos1.y = (quad1.y * 0.125) + 0.5/512.0 + ((0.125 - 1.0/512.0) * textureColor.g);
        texPos1.y = 1.0-texPos1.y;
        highp vec2 texPos2;
        texPos2.x = (quad2.x * 0.125) + 0.5/512.0 + ((0.125 - 1.0/512.0) * textureColor.r);
        texPos2.y = (quad2.y * 0.125) + 0.5/512.0 + ((0.125 - 1.0/512.0) * textureColor.g);
        texPos2.y = 1.0-texPos2.y;
        lowp vec4 newColor1 = texture2D(uTexture2, texPos1);
        lowp vec4 newColor2 = texture2D(uTexture2, texPos2);
        lowp vec4 newColor = mix(newColor1, newColor2, fract(blueColor));
        return newColor;
    }
    

    将我们的视频帧通过这个lut文件转换之后,就是『幻觉』滤镜的效果了。
    在做滤镜的时候碰到了一个问题,就是普通的sampler2D纹理无法和samplerExternalOES纹理共用,具体情况就是,当在glsl代码中同时存在这两种纹理时,代码是无法正常运行的。那么怎么解决呢?如果只是视频预览,解决的方法比较多,比如使用Camera类的previewCallback,拿到每一帧的byte数组(yuv数据)之后,将yuv数据转成rgb,再将rgb转成纹理来显示就可以了。这种方法虽然可行,但是因为需要数据转换,效率比较差。那有没有比较优雅并且高效的解决办法呢?答案是——FBO。

    在OpenGL渲染管线中,几何数据和纹理经过多次转化和多次测试,最后以二维像素的形式显示在屏幕上。OpenGL管线的最终渲染目的地被称作帧缓存(framebuffer)。帧缓冲是一些二维数组和OpenG所使用的存储区的集合:颜色缓存、深度缓存、模板缓存和累计缓存。一般情况下,帧缓存完全由window系统生成和管理,由OpenGL使用。这个默认的帧缓存被称作“window系统生成”(window-system-provided)的帧缓存。
    在OpenGL扩展中,GL_EXT_framebuffer_object提供了一种创建额外的不能显示的帧缓存对象的接口。为了和默认的“window系统生成”的帧缓存区别,这种帧缓冲成为应用程序帧缓存(application-createdframebuffer)。通过使用帧缓存对象(FBO),OpenGL可以将显示输出到引用程序帧缓存对象,而不是传统的“window系统生成”帧缓存。而且,它完全受OpenGL控制。

    总结来说就是,FBO相当于在内存中创建了一个Canvas,我们可以将这块画布和一个纹理绑定,然后先将内容画到画布上,之后就可以通过纹理对这块画布里的内容为所欲为了。

    FBO的使用下文会继续说明。

    7.2残影

    『幻觉』特效最明显的一个效果就是,画面中的物体移动时会有残影,这个如何解决呢?仔细思考一下我们就可以得到答案——保留上一帧的内容,将其透明化,然后和当前帧的内容混合。不断重复这个过程,就会得到残影的效果。那么如何保留上一帧的内容呢?答案还是——FBO。

    7.3残影颜色分离

    这个可能不好理解,看个截图大家应该就懂了。

    残影颜色分离

    可以看到,截图中的那支笔的残影是七彩的。

    这个如何解决呢?我们在将当前帧和上一帧内容混合时,肯定是操作每一个像素点的RGB分量的,那么这个七彩色应该就是从这里入手,肯定有一个混合公式

    vec4 currentFrame;
    vec4 lastFrame;
    gl_FragColor = vec4(a1 * currentFrame.r + a2 * lastFrame.r,b1 * currentFrame.g + b2 * lastFrame.g,c1 * currentFrame.b + c2 * lastFrame.b,1.0);
    

    我们要做的就是把这个公式里的a,b,c值给算出来。那么如何计算呢?这里有个小窍门,我们假定currentFrame的rgb值都是0,lastFrame的rgb都是1。你可能会问,这是什么马叉虫操作呢?我们让上一帧是黑色的,这一帧是白色的就可以啦。废话不多说,看图。

    我们找个黑色的背景,白色的物体——黑色鼠标垫和纸巾,效果大概如下图所示:

    颜色分离效果图

    我们逐帧分析,很快就能算出我们想要的结果。

    首先我们看前面三帧

    逐帧分析1

    可以看到,当纸巾向下移动时,露出来的部分是蓝色的(当前帧是白色,上一帧是黑色),而上面的部分是橙色的(此时上一帧是白色的,当前帧是黑色的),那么从这里我们得出一个结论就是,c1=1,c2 = 0,因为橙色的部分蓝色色值是0。

    再看后面几帧

    逐帧分析1

    可以看到,最顶上的那个残影,最终变得特别的红,那么我们可以知道,a1是一个接近0的数,而a2是一个十分接近1的数,为什么不能是1呢?因为如果是1,那么lastFrame的色值就会一直保留了,并不会随着帧数增加逐渐变淡消失。

    得出a和c的值以后,b的值我们大概猜测一下,试几个数字之后就能得到我们的结果了。最终得出的公式如下:

    gl_FragColor = vec4(0.95 * lastFrame.r  +  0.05* currentFrame.r,currentFrame.g * 0.2 + lastFrame.g * 0.8, currentFrame.b,1.0);
    

    这个公式的效果已经十分接近了。

    7.4关键代码
        private RenderBuffer mRenderBuffer;
    
        private RenderBuffer mRenderBuffer2;
    
        private RenderBuffer mRenderBuffer3;
    
        private int mLutTexture;
        //当前帧
        private int mCurrentFrameProgram;
        //上一帧
        private int mLastFrameProgram;
    
        private boolean mFirst = true;
        @Override
        public void draw(int textureId, float[] texMatrix, int canvasWidth, int canvasHeight) {
            if (mRenderBuffer == null) {
                mRenderBuffer = new RenderBuffer(GL_TEXTURE8, canvasWidth, canvasHeight);
                mRenderBuffer2 = new RenderBuffer(GL_TEXTURE9, canvasWidth, canvasHeight);
                mRenderBuffer3 = new RenderBuffer(GL_TEXTURE10, canvasWidth, canvasHeight);
                mLastFrameProgram = GLUtils.buildProgram(FileUtils.readFromRaw(R.raw.vertex_common), FileUtils.readFromRaw(R.raw.fragment_common));
                mCurrentFrameProgram = GLUtils.buildProgram(FileUtils.readFromRaw(R.raw.vertex_common), FileUtils.readFromRaw(R.raw.fragment_current_frame));
                mLutTexture = GLUtils.genLutTexture();
                android.opengl.GLUtils.texImage2D(GL_TEXTURE_2D, 0, BitmapFactory.decodeResource(AppProfile.getContext().getResources(), R.raw.lookup_vertigo), 0);
            }
            mRenderBuffer.bind();
            //这里使用samplerExternalOES纹理将当前的视频内容绘制到缓存中
            super.draw(textureId, texMatrix, canvasWidth, canvasHeight);
            mRenderBuffer.unbind();
            //绘制当前帧
            GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT);
            drawCurrentFrame();
            //将当前帧的内容保存到缓存中
            mRenderBuffer3.bind();
            drawCurrentFrame();
            mRenderBuffer3.unbind();
            //只用两个buffer的话,屏幕中会有黑格子
            //把缓存3中的内容画到缓存2中,缓存2中的内容在下一帧会用到
            mRenderBuffer2.bind();
            drawToBuffer();
            mRenderBuffer2.unbind();
            mFrames++;
            mFirst = false;
        }
        private void drawCurrentFrame() {
            glUseProgram(mCurrentFrameProgram);
            int textureId = mRenderBuffer.getTextureId();
            setup(mCurrentFrameProgram, new int[]{textureId, mFirst ? textureId : mRenderBuffer2.getTextureId(), mLutTexture});
            GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT);
            GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4);
        }
        private void drawToBuffer() {
            glUseProgram(mLastFrameProgram);
            setup(mLastFrameProgram, new int[]{mRenderBuffer3.getTextureId()});
            GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT);
            GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4);
    
        }
        private void setup(int programId, int[] textureId) {
            glUseProgram(programId);
            int aPositionLocation = glGetAttribLocation(programId, "aPosition");
            int aTexCoordLocation = glGetAttribLocation(programId, "aTextureCoord");
            mRendererInfo.getVertexBuffer().position(0);
            glEnableVertexAttribArray(aPositionLocation);
            glVertexAttribPointer(aPositionLocation, 2,
                    GL_FLOAT, false, 0, mRendererInfo.getVertexBuffer());
            mRendererInfo.getTextureBuffer().position(0);
            glEnableVertexAttribArray(aTexCoordLocation);
            glVertexAttribPointer(aTexCoordLocation, 2,
                    GL_FLOAT, false, 0, mRendererInfo.getTextureBuffer());
            for (int i = 0; i < textureId.length; i++) {
                int textureLocation = glGetUniformLocation(programId, "uTexture" + i);
                glActiveTexture(GL_TEXTURE0 + i);
                glBindTexture(GLES20.GL_TEXTURE_2D, textureId[i]);
                glUniform1i(textureLocation, i);
            }
        }
    

    帧缓存代码

    public class RenderBuffer {
        private int mTextureId;
    
        private int mActiveTextureUnit;
    
        private int mRenderBufferId;
    
        private int mFrameBufferId;
    
        private int mWidth, mHeight;
    
        public RenderBuffer(int activeTextureUnit, int width, int height) {
            this.mActiveTextureUnit = activeTextureUnit;
            this.mWidth = width;
            this.mHeight = height;
            int[] buffer = new int[1];
            GLES20.glActiveTexture(activeTextureUnit);
            mTextureId = GLUtils.genTexture();
            IntBuffer texBuffer =
                    ByteBuffer.allocateDirect(width * height * 4).order(ByteOrder.nativeOrder()).asIntBuffer();
            GLES20.glTexImage2D(GLES20.GL_TEXTURE_2D, 0, GLES20.GL_RGBA, width, height, 0, GLES20.GL_RGBA, GLES20.GL_UNSIGNED_BYTE, texBuffer);
    
            GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D, GL10.GL_TEXTURE_MIN_FILTER, GL10.GL_LINEAR);
            GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D, GL10.GL_TEXTURE_MAG_FILTER, GL10.GL_LINEAR);
            GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GL10.GL_TEXTURE_WRAP_S, GL10.GL_CLAMP_TO_EDGE);
            GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GL10.GL_TEXTURE_WRAP_T, GL10.GL_CLAMP_TO_EDGE);
    
            // Generate frame buffer
            GLES20.glGenFramebuffers(1, buffer, 0);
            mFrameBufferId = buffer[0];
            // Bind frame buffer
            GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, mFrameBufferId);
            // Generate render buffer
            GLES20.glGenRenderbuffers(1, buffer, 0);
            mRenderBufferId = buffer[0];
            // Bind render buffer
            GLES20.glBindRenderbuffer(GLES20.GL_RENDERBUFFER, mRenderBufferId);
            GLES20.glRenderbufferStorage(GLES20.GL_RENDERBUFFER, GLES20.GL_DEPTH_COMPONENT16, width, height);
        }
    
        public void bind() {
            GLES20.glViewport(0, 0, mWidth, mHeight);
            checkGlError("glViewport");
            GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, mFrameBufferId);
            checkGlError("glBindFramebuffer");
            GLES20.glFramebufferTexture2D(GLES20.GL_FRAMEBUFFER, GLES20.GL_COLOR_ATTACHMENT0,
                    GLES20.GL_TEXTURE_2D, mTextureId, 0);
            checkGlError("glFramebufferTexture2D");
            GLES20.glFramebufferRenderbuffer(GLES20.GL_FRAMEBUFFER, GLES20.GL_DEPTH_ATTACHMENT,
                    GLES20.GL_RENDERBUFFER, mRenderBufferId);
            checkGlError("glFramebufferRenderbuffer");
        }
    
    
        public void unbind() {
            GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, 0);
        }
    
        public int getTextureId(){
            return mTextureId;
        }
    }
    

    着色器代码

    precision mediump float;
    varying vec2 vTextureCoord;
    uniform sampler2D uTexture0;
    uniform sampler2D uTexture1;
    uniform sampler2D uTexture2;
    
    vec4 lookup(in vec4 textureColor){
        mediump float blueColor = textureColor.b * 63.0;
        mediump vec2 quad1;
        quad1.y = floor(floor(blueColor) / 8.0);
        quad1.x = floor(blueColor) - (quad1.y * 8.0);
        mediump vec2 quad2;
        quad2.y = floor(ceil(blueColor) / 8.0);
        quad2.x = ceil(blueColor) - (quad2.y * 8.0);
        highp vec2 texPos1;
        texPos1.x = (quad1.x * 0.125) + 0.5/512.0 + ((0.125 - 1.0/512.0) * textureColor.r);
        texPos1.y = (quad1.y * 0.125) + 0.5/512.0 + ((0.125 - 1.0/512.0) * textureColor.g);
        texPos1.y = 1.0-texPos1.y;
        highp vec2 texPos2;
        texPos2.x = (quad2.x * 0.125) + 0.5/512.0 + ((0.125 - 1.0/512.0) * textureColor.r);
        texPos2.y = (quad2.y * 0.125) + 0.5/512.0 + ((0.125 - 1.0/512.0) * textureColor.g);
        texPos2.y = 1.0-texPos2.y;
        lowp vec4 newColor1 = texture2D(uTexture2, texPos1);
        lowp vec4 newColor2 = texture2D(uTexture2, texPos2);
        lowp vec4 newColor = mix(newColor1, newColor2, fract(blueColor));
        return newColor;
    }
    void main(){
        vec4 lastFrame = texture2D(uTexture1,vTextureCoord);
        vec4 currentFrame = lookup(texture2D(uTexture0,vTextureCoord));
    
        gl_FragColor = vec4(0.95 * lastFrame.r  +  0.05* currentFrame.r,currentFrame.g * 0.2 + lastFrame.g * 0.8, currentFrame.b,1.0);
    }
    

    总结

    抖音的特效大概就是这样了,如果要对视频进行后期处理的话,我们只需要记住每个特效开始的时间和结束的时间,然后在后台对每一帧进行处理,最终保存到一个新的视频文件里即可,这个其实跟录制是差不多的,就是一个离屏渲染的操作。
    小伙伴们觉得这篇文章对你们有帮助的话,欢迎点赞噢,觉得文章有不足之处的话,欢迎大佬们指出,谢谢啦!

    相关文章

      网友评论

      • meStronger:老哥 6666
      • hackest:老哥 6的一批
      • 你饿不饿呀:6啊!收藏退出一气呵成
      • 筱宓dawnLing:凑够100个喜欢:smile:
      • 林右耳:大佬
      • don94:请收下我的膝盖,大佬
      • superroshan:厉害了,我的哥
      • 风飞燕:大佬玩抖音都玩的画风如此新奇,佩服佩服
      • 清风尤灵:大神,你的世界我不懂:+1:
      • iwuq:大神,请收下我的膝盖!!!
      • l1zheng:大佬! 猛男!
      • Benhero:我刚想写一篇标题一模一样,内容一模一样的,就看到你发了。。。:joy:
        还是老哥你手速快啊!:+1:
      • 路痴欧巴:还是默默的收藏吧
      • 夨落旳尐孩:请收下在下的膝盖
      • 曾大稳丶:老哥,很棒
      • 11amok:牛逼啊
      • 26d7895fa345:点击特效按钮 没反应 然后就崩溃了
        26d7895fa345:@知心猛男 09-17 16:24:26.112 1129-1177/com.xue.douyin.douyin D/mali_winsys: [MALI] win=0xdc6e4008, native_buffer=0xdd0aefc8, fd=-1
        09-17 16:24:26.112 1129-1177/com.xue.douyin.douyin D/GraphicBuffer: unregister, handle(0xdd0af060) (w:342 h:132 s:352 f:0x1 u:0x000b00)
        09-17 16:24:26.113 1129-1177/com.xue.douyin.douyin I/[MALI][Gralloc]: [-]r_hnd(0xdd0af060), client(53), share_fd(50)
        09-17 16:24:26.113 1129-1177/com.xue.douyin.douyin D/Surface: Surface::disconnect(this=0xdc6e4000,api=1)
        09-17 16:24:26.121 1129-1154/com.xue.douyin.douyin D/WindowClient: Remove from mViews: android.widget.LinearLayout{de497a4 V.E...... ........ 0,0-342,132}, this = android.view.WindowManagerGlobal@66dc941
        09-17 16:24:34.382 1129-1150/com.xue.douyin.douyin I/ViewRootImpl: ANR Key Analyze: No Key event currently.
        ANR Key Analyze: Previeous Event null,finish at 1970-01-01 08:00:00.000
        09-17 16:24:34.383 1129-1150/com.xue.douyin.douyin I/ViewRootImpl: ANR Motion Analyze: No motion event currently.
        09-17 16:24:34.385 1129-1150/com.xue.douyin.douyin I/ViewRootImpl: ANR Motion Analyze: Previeous Event null,finish at 1970-01-01 08:00:00.000
        09-17 16:24:34.551 1129-1142/com.xue.douyin.douyin I/art: Thread[2,tid=1142,WaitingInMainSignalCatcherLoop,Thread*=0xeeee5000,peer=0x12c760a0,"Signal Catcher"]: reacting to signal 3
        09-17 16:24:35.133 1129-1142/com.xue.douyin.douyin I/art: Wrote stack traces to '/data/anr/traces.txt'
        Mr_villain:@cv小法师 特效切换的话需要在代码里改哈,界面里切换特效的功能没做。 crash的stack-trace能麻烦贴一下嘛?
      • 浮华染流年:第一次看到这个效果的时候,我是有点懵逼的,因为一点头绪都没有,当时只想把电脑扔了。

        我只能说2个来表达我的内心想法: 大佬
      • 沐小晨曦:太厉害了吧,大佬!
      • Lyhvin:厉害了 大佬
      • 冰冰的冻结:有没有完整代码啊
        冰冰的冻结:@知心猛男 厉害,问一下大佬,我最近搞ffmpeg把所有架构都编译出来了,可是不知道怎么把所有架构so,整合到studio 里
        Mr_villain:@冰冰的冻结 https://github.com/xue5455/VideoAudioDemo 代码写的比较糙,你凑合看一下吧。
      • glumes:现在主流是用 OpenGL ES 3.0 还是 2.0 啊。。
      • XGHeaven:着色器看的我一脸懵逼,当初学过,不过都忘了。

      本文标题:当一个 Android 开发玩抖音玩疯了之后(二)

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