打開抖音,搜索藍線挑戰
特效,點擊拍攝,就能夠看到以下效果java
注意到,該特效有以下特色
c++
藍線
,均勻得在豎直方向
上運動上方
,顯示的是上一幀
的畫面下方
,顯示的是正在預覽
的畫面運動
,上一幀
不斷被保留
,最終能夠獲得一副奇奇怪怪
的畫面這個特效雖然看着很普通,但結合使用者的創意,能夠玩出各類各樣的花樣,下面就來看看如何實現git
先看看筆者實現的效果github
注意到,實現的效果來看,和抖音的仍是比較吻合,除了藍線的顏色,筆者的藍線是純藍色
的(#0000FF
),固然,顏色能夠任意調整緩存
那麼問題來了,這樣的特效應該如何實現呢markdown
當筆者第一次看到這個特效的時候,就在想應該如何使用OpenGLES
去實現,嘗試了各類方式,首先遇到的幾個問題ide
保留
下來,即保留上一幀
藍線運動
,且不斷的保留上一幀
注意到,上面問題都提到了的一個關鍵字保留上一幀
,其實保留上一幀
就是實現該特效的關鍵
函數
筆者最早想到的實現方式是:oop
glReadPixels
的方式,根據時間,不斷的讀取數據Bitmap
上,而後再渲染出來方法有了,那麼就開始實現,實現的過程當中,愈來愈以爲不對勁,這樣不斷地讀數據
,再渲染
,會不會太麻煩了,還有,這樣的實現確定會有內存功耗
問題,必定有其餘簡單
的實現方式ui
每每越簡單的事情,在不瞭解其本質的時候就想得很複雜,把簡單的事情複雜化,這樣就算實現出來,也沒什麼意義,因此要觀察其本質
,保留上一幀
就是其本質
筆者也是琢磨了好久,如何保留上一幀
,保留後要如何再顯示出來,當筆者束手無策的時候,忽然發現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
就是用來保留上一幀,那麼它是如何保留住的呢(把不把握住,哈哈)
BaseRender
的setBindFbo
方法,讓其綁定Fbo
,以前筆者也說過,BaseRender
是筆者自定義一個基礎渲染類,包括渲染
、綁定Fbo
、綁定Vbo
之類的操做onDraw
中,將當前渲染後的Fbo
紋理傳入lastRender
的onDraw
方法中,此時,由於LaseRender
綁定了Fbo
,則對應的內容不渲染到屏幕,而是保留在幀緩存裏,接着獲取LaseRender
的Fbo
紋理,並賦值給LaseTextureId
LastRender
保留的上一幀紋理,也就分別對應着着色器裏的uSampler
和uSampler2
這樣,經過控制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.java和MoveLineVerticalRender.java結合起來而已
能夠看到內部會建立兩個Render
,一個是RetainFrameVerticalRender.java,另個就是MoveLineVerticalRender.java
而後在onDraw
中依次渲染便可
有細心的同窗,可能注意到Render
的命名,Render
中有一個Vertical
單詞,表示縱向
的藍線挑戰,若是想實現橫向
的,其實也比較簡單,把以前着色器裏面的判斷y
座標的地方都換成x
便可,具體能夠到Github
中查看BlueLineChallengeHFilter
看看最終實現的效果
該特效相關代碼,都可以在Github中找到