Android 音影片開發【特效篇】【一】抖音傳送帶特效 | 8月更文挑戰

語言: CN / TW / HK

這是我參與8月更文挑戰的第6天,活動詳情檢視:8月更文挑戰

本章將介紹如何實現抖音傳送帶特效

一、實現效果

1.1 首先來看抖音的傳送帶特效

抖音實現效果.gif

從上圖可以看到,抖音的傳送帶特效有如下特點

  • 螢幕左半邊部分是正常預覽影片
  • 螢幕右半邊部分像傳送帶一般,將畫面不斷地像右邊運送

根據此特效的特點,我們可以製作出各種有趣的影片

1.2 筆者實現傳送帶特效

筆者實現效果.gif

從上圖來看,筆者實現的效果基本上和抖音實現的一致

那麼,對於該特效,我們應該如何去實現呢?

其實在介紹抖音藍線挑戰特效那一章已經將到一個核心知識點Fbo,對,沒錯,當時做藍線挑戰特效用到的就是Fbo,接下來傳送帶特效也需要使用Fbo的保留上一幀功能

接下來,我們就來進行特效分析和具體實現

二、特效分析

首先,根據上面的效果圖,我們可以簡單畫出示意圖,如下圖所示(小格子的數量越多,畫面越精細

特效分析1.png

我們以橫向進行分析

OpenGLES中,紋理座標水平方向的起始位置在左方(準確的說是在左上角,這裡只是分析橫向的效果,故圖上標點0.0隨意標在左方,便於分析)

根據上面的效果圖,瞭解到,該特效有兩個特點

  • 螢幕左半邊部分是正常預覽影片
  • 螢幕右半邊部分像傳送帶一般,將畫面不斷地像右邊運送

這裡,我用了運送一詞,那麼,我們得首先知道,它運送的是什麼

2.1 運送什麼?

通過分析特效圖,我們知道,影象右半部分是不斷地向右邊移動,而左半部分是正常預覽的,看起來就好像是從左半部分的邊緣處不斷移動到右邊,那麼從這裡可以得出一個小結論

運送的是左半部分的邊緣區域,根據上圖,準確的說是中線左邊0區域的畫面

那麼,知道了這點,我們就一目瞭然了

2.2 它是如何運送的?

前面,我們知道了它運送的是0區域的畫面,那麼接下來就來分析下,它是如何運送的

  • 在預覽時,相機畫面一般都是正常顯示,0區域的畫面當然也是正常一幀幀重新整理
  • 0區域顯示第一幀(簡稱f1,後面以f開後,數字為幀序)時,將其移動到1區域
  • 0區域顯示f2時,將1區域f1移動到2區域,將0區域f2移動到1區域
  • 依次類推,就可以將0區域的畫面源源不斷地運送到右邊

2.3 Fbo

其實,在知道了它是運送什麼,且如何運送後,我們還是無法得知如何實現這一特效

此刻,就該Fbo登場了,前面藍線挑戰特效的篇章已經對其做了詳細描述,現在簡單介紹下

  • 可以將Oes紋理轉換成2D紋理
  • 可以將紋理資料不顯示在螢幕上,並保留下來

這裡,我們要實現該特效,就要使用它的保留幀資料的功能

2.4 特效實現

在上面,我們已經知道了該特效是如何運送資料,那麼通過下圖,我們來了解如何使用Fbo實現

特效分析2.png

從上面的分析可知,該特效運送的是左半部分的邊緣區域,所有有如何下實現步驟:

  • 首先假設每個小格的步長為0.1,那麼左半部分的邊緣區域就是0.4 ~ 0.5這個區域

  • Fbo可以儲存上一幀,那麼在渲染時,我們將上一幀的資料儲存下來

  • 在渲染的時候,會有兩個紋理,一個是相機的正常預覽紋理,另一個是儲存的上一幀,此時,我們在著色器裡就要進行判斷

    • 當紋理座標x小於0.5時,顯示相機的正常預覽畫面
    • 當紋理座標x大於0.5時,顯示儲存的上一幀畫面,不過這裡要注意,並不是對應座標的上一幀資料,即,不是0.5 ~ 1.0區域的資料,而是0.4 ~ 0.9區域的資料,大家可以思考下這是為什麼,後面具體實現的時候會有解答
  • 這樣,當相機不斷產生預覽資料時,右半部分將不斷地將左半部分的邊緣區域向右邊運送

三、具體實現

前面我們分析了該特效的整個實現流程,接下來就是具體的實現

首先,先上大家最關心的著色器程式碼

3.1 著色器

頂點著色器

c++ attribute vec4 aPos; attribute vec2 aCoordinate; varying vec2 vCoordinate; void main(){    vCoordinate = aCoordinate;    gl_Position = aPos; }

關於頂點著色器,並沒有做任何特殊處理

片元著色器

c++ precision mediump float; uniform sampler2D uSampler; uniform sampler2D uSampler2; varying vec2 vCoordinate; uniform float uOffset; void main(){    if (vCoordinate.x < 0.5) {        gl_FragColor = texture2D(uSampler, vCoordinate);   } else {        gl_FragColor = texture2D(uSampler2, vCoordinate - vec2(uOffset, 0.0));   } }

對於片元著色器,關鍵就在於main()函式裡面的if判斷,前面也有提到,會對紋理座標進行一個判斷

  • x小於0.5時,顯示相機預覽畫面
  • x大於0.5時,顯示上一幀的資料,且取的是對應座標往左偏移的資料(uOffset是偏移量,可以理解成小格子的寬度)

那麼對於為什麼要偏移呢?

這是因為通過上面,我們可以知道,該特效是從左半部分的邊緣區域開始運送的,那麼如果我們從對應座標取,那麼不就得不到左半部分割槽域的座標了嗎,所有得偏移一個小格子的寬度,從而得到對應的資料

這樣,每幀渲染時,都取0.4 ~ 0.9區域資料顯示到0.5 ~ 1.0區域,從而就實現了該傳送帶特效

在知道了如何實現該特效後,我們還可以實現縱向的傳送帶特效,只需要將片元著色器裡的x改為y即可

c++ precision mediump float; uniform sampler2D uSampler; uniform sampler2D uSampler2; varying vec2 vCoordinate; uniform float uOffset; void main(){    if (vCoordinate.y < 0.5) {        gl_FragColor = texture2D(uSampler, vCoordinate);   } else {        gl_FragColor = texture2D(uSampler2, vCoordinate - vec2(0.0, uOffset));   } }

3.2 Java程式碼實現部分

下面是Java程式碼實現部分

這裡面使用了一個lastRender保留上一幀資料,從而在下一次渲染時能夠使用

java public class ConveyorBeltHFilter extends BaseFilter {    private final BaseRender lastRender; ​    private int uSampler2Location;    private int uOffsetLocation; ​    private int lastTextureId = -1; ​    private float offset = 0.01f; ​    public ConveyorBeltHFilter(Context context) {        super(                context,                "render/filter/conveyor_belt_h/vertex.frag",                "render/filter/conveyor_belt_h/frag.frag"       ); ​        lastRender = new BaseRender(context); ​        lastRender.setBindFbo(true);   } ​    @Override    public void onCreate() {        super.onCreate();        lastRender.onCreate();   } ​    @Override    public void onChange(int width, int height) {        super.onChange(width, height);        lastRender.onChange(width, height);   } ​    @Override    public void onDraw(int textureId) {        super.onDraw(textureId);        lastRender.onDraw(getFboTextureId());        lastTextureId = lastRender.getFboTextureId();   } ​    @Override    public void onInitLocation() {        super.onInitLocation();        uSampler2Location = GLES20.glGetUniformLocation(getProgram(), "uSampler2");        uOffsetLocation = GLES20.glGetUniformLocation(getProgram(), "uOffset");   } ​    @Override    public void onActiveTexture(int textureId) {        super.onActiveTexture(textureId);        GLES20.glActiveTexture(GLES20.GL_TEXTURE1);        GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, lastTextureId);        GLES20.glUniform1i(uSampler2Location, 1);   } ​    @Override    public void onSetOtherData() {        super.onSetOtherData();        GLES20.glUniform1f(uOffsetLocation, offset);   } }

以上就是抖音傳送帶特效的實現全過程,希望大家喜歡!!!

四、GitHub

ConveyorBeltHFilter.java

ConveyorBeltVFilter.java