前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Android OpenGL ES 实现蓝线挑战特效

Android OpenGL ES 实现蓝线挑战特效

作者头像
字节流动
发布2021-08-12 11:17:24
1.2K0
发布2021-08-12 11:17:24
举报
文章被收录于专栏:字节流动

抖音的实现效果

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

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

  • 预览界面有一根蓝线,均匀得在竖直方向上运动
  • 蓝线的上方,显示的是上一帧的画面
  • 蓝线的下方,显示的是正在预览的画面
  • 随着蓝线的运动,上一帧不断被保留,最终可以得到一副奇奇怪怪的画面

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

先看看笔者实现的效果

实现效果

注意到,实现的效果来看,和抖音的还是比较吻合,除了蓝线的颜色,笔者的蓝线是纯蓝色的(#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实现」

接下来看看如何在着色器中实现

「顶点着色器」

代码语言:javascript
复制
attribute vec4 aPos;
attribute vec2 aCoordinate;
varying vec2 vCoordinate;
void main(){
    vCoordinate = aCoordinate;
    gl_Position = aPos;
}

注意到,顶点着色器没有任何特殊处理

「片元着色器」

代码语言:javascript
复制
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」

代码语言:javascript
复制
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纹理,则可以拿来做蓝线的渲染

「顶点着色器」

代码语言:javascript
复制
attribute vec4 aPos;
attribute vec2 aCoordinate;
varying vec2 vCoordinate;
void main(){
    vCoordinate = aCoordinate;
    gl_Position = aPos;
}

同样未做特殊处理

「片元着色器」

代码语言:javascript
复制
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」

代码语言:javascript
复制
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」

代码语言:javascript
复制
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 --

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2021-08-10,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 字节流动 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 抖音的实现效果
  • 实现效果
  • 特效分析
  • FBO 保留上一帧
  • 蓝线绘制
  • 最终实现
  • GitHub
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档