打开抖音,搜索 蓝线挑战
特效,点击拍摄,就可以看到如下效果
注意到,该特效有如下特点
这个特效虽然看着很普通,但结合使用者的创意,可以玩出各种各样的花样,下面就来看看如何实现
先看看笔者实现的效果
注意到,实现的效果来看,和抖音的还是比较吻合,除了蓝线的颜色,笔者的蓝线是纯蓝色的(#0000FF),当然,颜色可以任意调整
那么问题来了,这样的特效应该如何实现呢
当笔者第一次看到这个特效的时候,就在想应该如何使用 OpenGL ES 去实现,尝试了各种方式,首先遇到的几个问题
注意到,上面问题都提到了的一个关键字保留上一帧,其实保留上一帧就是实现该特效的关键
笔者最先想到的实现方式是:
方法有了,那么就开始实现,实现的过程中,越来越觉得不对劲,这样不断地读数据,再渲染,会不会太麻烦了,还有,这样的实现肯定会有内存功耗问题,一定有其他简单的实现方式
往往越简单的事情,在不了解其本质的时候就想得很复杂,把简单的事情复杂化,这样就算实现出来,也没什么意义,所以要观察其本质,保留上一帧就是其本质
笔者也是琢磨了很久,如何保留上一帧,保留后要如何再显示出来,当笔者一筹莫展的时候,突然发现Fbo就有保留上一帧的功能,好了,本质找到了,那么就着手实现
首先,Fbo 的概念性的东西,大家可以上网查查,这里就直接说说Fbo的作用
蓝线挑战这个特效,用到的就是Fbo的保留帧功能
观察上面的动图,会发现,蓝线上方显示的是上一帧,而蓝线下方显示的是正在预览的画面,这也就意味着需要两个纹理
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);
}
}
片元着色器的实现也比较简单,简单分析下
看到这里,你可能会说,啊,不会吧,这样就实现了?
当然不是,这里只是着色器,接下来看看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就是用来保留上一帧,那么它是如何保留住的呢(把不把握住,哈哈)
这样,通过控制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);
}
}
注意到,里面定义了两个常量
然后到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中找到
https://github.com/JYangkai/MediaDemo
作者:mirai 来源:https://juejin.cn/post/6978349424497917959
-- END --