Android OpenGLES 實現藍線挑戰特效

抖音的實現效果

打開抖音,搜索藍線挑戰特效,點擊拍攝,就能夠看到以下效果java

抖音實現.gif

注意到,該特效有以下特色c++

  • 預覽界面有一根藍線,均勻得在豎直方向上運動
  • 藍線的上方,顯示的是上一幀的畫面
  • 藍線的下方,顯示的是正在預覽的畫面
  • 隨着藍線的運動上一幀不斷被保留,最終能夠獲得一副奇奇怪怪的畫面

這個特效雖然看着很普通,但結合使用者的創意,能夠玩出各類各樣的花樣,下面就來看看如何實現git

先看看筆者實現的效果github

實現效果

自實現.gif

注意到,實現的效果來看,和抖音的仍是比較吻合,除了藍線的顏色,筆者的藍線是純藍色的(#0000FF),固然,顏色能夠任意調整緩存

特效分析

那麼問題來了,這樣的特效應該如何實現呢markdown

當筆者第一次看到這個特效的時候,就在想應該如何使用OpenGLES去實現,嘗試了各類方式,首先遇到的幾個問題ide

  • 如何讓畫面可否保留下來,即保留上一幀
  • 如何讓畫面隨着時間的推移,藍線運動,且不斷的保留上一幀

注意到,上面問題都提到了的一個關鍵字保留上一幀,其實保留上一幀就是實現該特效的關鍵函數

筆者最早想到的實現方式是:oop

  • 使用glReadPixels的方式,根據時間,不斷的讀取數據
  • 將讀取到的數據顯示在一張Bitmap上,而後再渲染出來

方法有了,那麼就開始實現,實現的過程當中,愈來愈以爲不對勁,這樣不斷地讀數據,再渲染,會不會太麻煩了,還有,這樣的實現確定會有內存功耗問題,必定有其餘簡單的實現方式ui

每每越簡單的事情,在不瞭解其本質的時候就想得很複雜,把簡單的事情複雜化,這樣就算實現出來,也沒什麼意義,因此要觀察其本質保留上一幀就是其本質

筆者也是琢磨了好久,如何保留上一幀,保留後要如何再顯示出來,當筆者束手無策的時候,忽然發現Fbo就有保留上一幀的功能,好了,本質找到了,那麼就着手實現

Fbo保留上一幀

首先,Fbo的概念性的東西,你們能夠上網查查,這裏就直接說說Fbo的做用

  • Oes紋理轉換2D紋理

    預覽相機、播放視頻等這些經過SurfaceTexture方式渲染的,通常都是使用Oes紋理,而當須要在相機預覽或者播放視頻中添加水印/貼紙,則須要先將Oes紋理轉化成2D紋理,由於Oes紋理和2D紋理是不能同時使用

  • 保留幀

    讓當前渲染的紋理保留在一個幀緩存裏,而不顯示在屏幕上

藍線挑戰這個特效,用到的就是Fbo保留幀功能

觀察上面的動圖,會發現,藍線上方顯示的是上一幀,而藍線下方顯示的是正在預覽的畫面,這也就意味着須要兩個紋理

  • lastTextureId

    上一幀渲染的紋理

  • textureId

    當前預覽的紋理

BaseRender這個類,是筆者封裝的一個基礎渲染類,裏面實現了基礎的渲染、綁定Fbo、綁定Vbo,若是須要,能夠到Github中拿來用

OpenGLES實現

接下來看看如何在着色器中實現

頂點着色器

attribute vec4 aPos;
attribute vec2 aCoordinate;
varying vec2 vCoordinate;
void main(){
    vCoordinate = aCoordinate;
    gl_Position = aPos;
}
複製代碼

注意到,頂點着色器沒有任何特殊處理

片元着色器

precision mediump float;
uniform sampler2D uSampler;
uniform sampler2D uSampler2;
varying vec2 vCoordinate;
uniform float uOffset;
void main(){
    if (vCoordinate.y < uOffset) {
        gl_FragColor = texture2D(uSampler2, vCoordinate);
    } else {
        gl_FragColor = texture2D(uSampler, vCoordinate);
    }
}
複製代碼

片元着色器的實現也比較簡單,簡單分析下

  • uSampler表示當前預覽的紋理

  • uSampler2表示上一幀的紋理

  • uOffset是外部傳入的一個float類型的值,用於控制顯示上一幀和顯示當前預覽畫面

  • main函數裏,只作了一個if判斷,若是當前y軸座標小於uOffset,則顯示上一幀,不然顯示當前預覽畫面

看到這裏,你可能會說,啊,不會吧,這樣就實現了?

固然不是,這裏只是着色器,接下來看看Java層那邊是如何作的

RetainFrameVerticalRender.java

public class RetainFrameVerticalRender extends BaseRender {
    private final BaseRender lastRender;

    private int uSampler2Location;
    private int uOffsetLocation;

    private int lastTextureId = -1;

    private float offset;

    public RetainFrameVerticalRender(Context context) {
        super(
                context,
                "render/other/retain_frame_vertical/vertex.frag",
                "render/other/retain_frame_vertical/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);
    }

    public void setOffset(float offset) {
        this.offset = offset;
    }
}
複製代碼

注意到,該Render內部建立了一個lastRender,這個lastRender就是用來保留上一幀,那麼它是如何保留住的呢(把不把握住,哈哈)

  • 在建立的時候,調用BaseRendersetBindFbo方法,讓其綁定Fbo,以前筆者也說過,BaseRender是筆者自定義一個基礎渲染類,包括渲染、綁定Fbo、綁定Vbo之類的操做
  • onDraw中,將當前渲染後的Fbo紋理傳入lastRenderonDraw方法中,此時,由於LaseRender綁定了Fbo,則對應的內容不渲染到屏幕,而是保留在幀緩存裏,接着獲取LaseRenderFbo紋理,並賦值給LaseTextureId
  • 因而,就獲得了兩個紋理,一個是當前相機紋理,一個是LastRender保留的上一幀紋理,也就分別對應着着色器裏的uSampleruSampler2

這樣,經過控制uOffset的值,就能夠達到對應的效果

到這裏,還差一點,就是藍線

那麼,接下來就來繪製下藍線

藍線繪製

藍線的繪製就比較簡單,在RetainFrameVerticalRender.java繪製完成後,再使用其Fbo紋理,則能夠拿來作藍線的渲染

頂點着色器

attribute vec4 aPos;
attribute vec2 aCoordinate;
varying vec2 vCoordinate;
void main(){
    vCoordinate = aCoordinate;
    gl_Position = aPos;
}
複製代碼

一樣未作特殊處理

片元着色器

precision mediump float;
uniform sampler2D uSampler;
varying vec2 vCoordinate;
uniform float uOffset;
const vec4 COLOR = vec4(0.0, 0.0, 1.0, 1.0);
const float SIZE = 0.005;
void main(){
    if (vCoordinate.y > uOffset - SIZE && vCoordinate.y < uOffset + SIZE) {
        gl_FragColor = COLOR;
    } else {
        gl_FragColor = texture2D(uSampler, vCoordinate);
    }
}
複製代碼

注意到,裏面定義了兩個常量

  • COLOR

    這個便是藍線的顏色,能夠根據需求,自定義對應的顏色

    這裏筆者定義爲「純」藍色

  • SIZE

    這個便是藍線的寬度,能夠根據屏幕的大小來定義

而後到main函數,這裏是一個判斷,若是當前y軸座標在以uOffset爲中心,寬度爲SIZE的範圍內的話,則讓當前的像素值設置爲定義的COLOR,否者使用texture2D函數獲取當前紋理的像素值

接下來看看Java層的實現

MoveLineVerticalRender.java

public class MoveLineVerticalRender extends BaseRender {
    private int uOffsetLocation;

    private float offset;

    public MoveLineVerticalRender(Context context) {
        super(
                context,
                "render/other/move_line_vertical/vertex.frag",
                "render/other/move_line_vertical/frag.frag"
        );
    }

    @Override
    public void onInitLocation() {
        super.onInitLocation();
        uOffsetLocation = GLES20.glGetUniformLocation(getProgram(), "uOffset");
    }

    @Override
    public void onSetOtherData() {
        super.onSetOtherData();
        GLES20.glUniform1f(uOffsetLocation, offset);
    }

    public void setOffset(float offset) {
        this.offset = offset;
    }
}
複製代碼

Java層的實現就比較簡單,只是傳入uOffset而已

那麼結合上面的RetainFrameVerticalRender.java,能夠建立一個類

BlueLineChallengeVFilter.java

public class BlueLineChallengeVFilter extends BaseFilter {
    private final RetainFrameVerticalRender inputRender;

    private final MoveLineVerticalRender outputRender;

    public BlueLineChallengeVFilter(Context context) {
        super(context);

        inputRender = new RetainFrameVerticalRender(context);
        inputRender.setBindFbo(true);

        outputRender = new MoveLineVerticalRender(context);
        outputRender.setBindFbo(true);

        timeStart(15000);
    }

    @Override
    public void onCreate() {
        inputRender.onCreate();
        outputRender.onCreate();
    }

    @Override
    public void onChange(int width, int height) {
        inputRender.onChange(width, height);
        outputRender.onChange(width, height);
    }

    @Override
    public void onDraw(int textureId) {
        float progress = getProgress();

        inputRender.setOffset(progress);
        outputRender.setOffset(progress);

        inputRender.onDraw(textureId);
        outputRender.onDraw(inputRender.getFboTextureId());
    }

    @Override
    public int getFboTextureId() {
        return outputRender.getFboTextureId();
    }

    @Override
    public void onRelease() {
        super.onRelease();
        inputRender.onRelease();
        outputRender.onRelease();
    }
}
複製代碼

該類並不是又作了什麼處理,只是將RetainFrameVerticalRender.javaMoveLineVerticalRender.java結合起來而已

能夠看到內部會建立兩個Render,一個是RetainFrameVerticalRender.java,另個就是MoveLineVerticalRender.java

而後在onDraw中依次渲染便可

有細心的同窗,可能注意到Render的命名,Render中有一個Vertical單詞,表示縱向的藍線挑戰,若是想實現橫向的,其實也比較簡單,把以前着色器裏面的判斷y座標的地方都換成x便可,具體能夠到Github中查看BlueLineChallengeHFilter

看看最終實現的效果

最終實現

自實現.gif

GitHub

該特效相關代碼,都可以在Github中找到

BlueLineChallengeVFilter

BlueLineChallengeHFilter

相關文章
相關標籤/搜索