打开抖音,搜索蓝线挑战
特效,点击拍摄,就能够看到以下效果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中找到