抖音的实现效果
打开抖音,搜索蓝线挑战
特效,点击拍摄,就可以看到如下效果
注意到,该特效有如下特点
- 预览界面有一根
蓝线
,均匀得在竖直方向
上运动 - 蓝线的
上方
,显示的是上一帧
的画面 - 蓝线的
下方
,显示的是正在预览
的画面 - 随着蓝线的
运动
,上一帧
不断被保留
,最终可以得到一副奇奇怪怪
的画面
这个特效虽然看着很普通,但结合使用者的创意,可以玩出各种各样的花样,下面就来看看如何实现
先看看笔者实现的效果
实现效果
注意到,实现的效果来看,和抖音的还是比较吻合,除了蓝线的颜色,笔者的蓝线是纯蓝色
的(#0000FF
),当然,颜色可以任意调整
特效分析
那么问题来了,这样的特效应该如何实现呢
当笔者第一次看到这个特效的时候,就在想应该如何使用OpenGLES
去实现,尝试了各种方式,首先遇到的几个问题
- 如何让画面能否
保留
下来,即保留上一帧
- 如何让画面随着时间的推移,
蓝线运动
,且不断的保留上一帧
注意到,上面问题都提到了的一个关键字保留上一帧
,其实保留上一帧
就是实现该特效的关键
笔者最先想到的实现方式是:
- 使用
glReadPixels
的方式,根据时间,不断的读取数据 - 将读取到的数据显示在一张
Bitmap
上,然后再渲染出来
方法有了,那么就开始实现,实现的过程中,越来越觉得不对劲,这样不断地读数据
,再渲染
,会不会太麻烦了,还有,这样的实现肯定会有内存功耗
问题,一定有其他简单
的实现方式
往往越简单的事情,在不了解其本质的时候就想得很复杂,把简单的事情复杂化,这样就算实现出来,也没什么意义,所以要观察其本质
,保留上一帧
就是其本质
笔者也是琢磨了很久,如何保留上一帧
,保留后要如何再显示出来,当笔者一筹莫展的时候,突然发现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
就是用来保留上一帧,那么它是如何保留住的呢(把不把握住,哈哈)
- 在创建的时候,调用
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
该特效相关代码,均可以在Github中找到
今天的文章Android OpenGLES 实现蓝线挑战特效分享到此就结束了,感谢您的阅读。
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
如需转载请保留出处:https://bianchenghao.cn/19005.html