當一個 Android 開發玩抖音玩瘋了之后(二)

上一篇文章中,我大概介紹了一下短視頻的拍攝,主要就是音視頻的加減速。這篇文章我將介紹下抖音視頻特效的實現,廢話不多說,進入正題。

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);
}

總結

抖音的特效大概就是這樣了,如果要對視頻進行后期處理的話,我們只需要記住每個特效開始的時間和結束的時間,然后在后臺對每一幀進行處理,最終保存到一個新的視頻文件里即可,這個其實跟錄制是差不多的,就是一個離屏渲染的操作。
小伙伴們覺得這篇文章對你們有幫助的話,歡迎點贊噢,覺得文章有不足之處的話,歡迎大佬們指出,謝謝啦!

Demo代碼寫的比較糙,有需要的同學自取吧!

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,646評論 6 533
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,595評論 3 418
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,560評論 0 376
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,035評論 1 314
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,814評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,224評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,301評論 3 442
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,444評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,988評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,804評論 3 355
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,998評論 1 370
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,544評論 5 360
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,237評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,665評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,927評論 1 287
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,706評論 3 393
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,993評論 2 374

推薦閱讀更多精彩內容