當一個 Android 開發玩抖音玩瘋了之後(二)
上一篇文章中,我大概介紹了一下短視訊的拍攝,主要就是音視訊的加減速。這篇文章我將介紹下抖音視訊特效的實現,廢話不多說,進入正題。
1.特效概覽

texiao1.png

texiao2.png
抖音上目前有這九種視訊特效,本文將介紹前面六種的實現。有人可能會問了,為什麼最後三種特效被忽略了。
當然是因為我懶啦。

沒想到吧
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); }
總結
抖音的特效大概就是這樣了,如果要對視訊進行後期處理的話,我們只需要記住每個特效開始的時間和結束的時間,然後在後臺對每一幀進行處理,最終儲存到一個新的視訊檔案裡即可,這個其實跟錄製是差不多的,就是一個離屏渲染的操作。
小夥伴們覺得這篇文章對你們有幫助的話,歡迎點贊噢,覺得文章有不足之處的話,歡迎大佬們指出,謝謝啦!