首页 文章详情

Android OpenGL ES 实现蓝线挑战特效

字节流动 | 600 2021-08-13 21:56 0 0 0
UniSMS (合一短信)

抖音的实现效果

打开抖音,搜索 蓝线挑战 特效,点击拍摄,就可以看到如下效果


注意到,该特效有如下特点

  • 预览界面有一根蓝线,均匀得在竖直方向上运动

  • 蓝线的上方,显示的是上一帧的画面

  • 蓝线的下方,显示的是正在预览的画面

  • 随着蓝线的运动,上一帧不断被保留,最终可以得到一副奇奇怪怪的画面

这个特效虽然看着很普通,但结合使用者的创意,可以玩出各种各样的花样,下面就来看看如何实现

先看看笔者实现的效果

实现效果

注意到,实现的效果来看,和抖音的还是比较吻合,除了蓝线的颜色,笔者的蓝线是纯蓝色的(#0000FF),当然,颜色可以任意调整

特效分析

那么问题来了,这样的特效应该如何实现呢

当笔者第一次看到这个特效的时候,就在想应该如何使用 OpenGL ES 去实现,尝试了各种方式,首先遇到的几个问题

  • 如何让画面能否保留下来,即保留上一帧

  • 如何让画面随着时间的推移,蓝线运动,且不断的保留上一帧

注意到,上面问题都提到了的一个关键字保留上一帧,其实保留上一帧就是实现该特效的关键

笔者最先想到的实现方式是:

  • 使用 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中找到

https://github.com/JYangkai/MediaDemo

作者:mirai

来源:https://juejin.cn/post/6978349424497917959


-- END --


进技术交流群,扫码添加我的微信:Byte-Flow



获取视频教程和源码



推荐:

Android FFmpeg 实现带滤镜的微信小视频录制功能

全网最全的 Android 音视频和 OpenGL ES 干货,都在这了

Android OpenGL ES 从入门到精通系统性学习教程

good-icon 0
favorite-icon 0
收藏
回复数量: 0
    暂无评论~~
    Ctrl+Enter