阅读 1099

[ - OpenGLES3.0 - ] 第四集 视频接入OpenGLES3.0实现特效

零、前言

最近很久都在做 Flutter 相关的事,工作之外,闲余的时间大部分都奉献给了 FlutterUnit 以及在 Flutter 群里吹牛 ,OpenGLES3.0 的系列搁浅了很久。最近忙完一段时间,想捡起一下,更新一篇视频特效相关的内容。温馨提示: 本篇 gif 图片较多且比较大,注意流量


前面说过 OpenGLES 可以利用 片段着色器纹理贴图 进行特效处理。对应视频来说也是一样,比如下面的红色效果,通过 MediaPlayer 不断更新视频纹理,再由 OpenGLES 进行绘制,在此之间就可以通过 片段着色器 对纹理进行操作,从而达到各种各样的特效。

  • 比如通过控制片段着色器的输出颜色而产生颜色相关的特效
红色
  • 比如通过控制片段着色器纹理坐标实现特效
  • 比如通过入参实现动态效果

一、准备工作

项目 github 地址: github.com/toly1994328…

1. 准备资源

既然要实现视频特效,那么必然要有视频,本文只不想牵涉运行时权限,所以视频资源放在可访问的目录下。 大家可以酌情处理,只要能会获取到视频资源即可。

image-20201207155143067


2. 全屏横屏处理

MainActivity#onCreate 中进行 全屏横屏 操作。

public class MainActivity extends AppCompatActivity {
    private GLVideoView videoView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        fullScreenLandSpace();
				// TODO 设置 View
    }

    // 沉浸标题栏 且 横屏
    private void fullScreenLandSpace() {
        //判断SDK的版本是否>=21
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
            Window window = getWindow();
            // 全屏、隐藏状态栏
            getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN);
            window.getDecorView().setSystemUiVisibility(
                    View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION);
            window.setNavigationBarColor(Color.TRANSPARENT); //设置虚拟键为透明
        }
      
        //如果ActionBar非空,则隐藏
        ActionBar actionBar = getSupportActionBar();
        if (actionBar != null) {
            actionBar.hide();
        }

        // 如果非横屏,设置横屏
        if (getResources().getConfiguration().orientation == Configuration.ORIENTATION_PORTRAIT) {
            setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);
        }
    }
}
复制代码

3.检查 OpenGLES 版本,设置View

通过 checkSupportOpenGLES30 检测设备是否支持 OpenGLES30。如果支持的话,就创建 GLVideoView 然后设置到 setContentView 中进行展示。

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    fullScreenLandSpace();
    if (checkSupportOpenGLES30()) {
        videoView = new GLVideoView(this);
        setContentView(videoView);
    } else {
        Log.e("MainActivity", "当前设备不支持 OpenGL ES 3.0!");
        finish();
    }
}
private boolean checkSupportOpenGLES30() {
    ActivityManager am = (ActivityManager) getSystemService(Context.ACTIVITY_SERVICE);
    if (am != null) {
        ConfigurationInfo info = am.getDeviceConfigurationInfo();
        return (info.reqGlEsVersion >= 0x30000);
    }
    return false;
}
复制代码

二、接入视频播放 OpenGLES

1、视图 GLVideoView

GLVideoView 继承自 GLSurfaceView 它的本质是一个 View

public class GLVideoView extends GLSurfaceView{
    VideoRender render;

    public GLVideoView(Context context) {
        super(context);
        //设置OpenGL ES 3.0 context
        setEGLContextClientVersion(3);
      	
        // 视频路径设置
        String videoPath = "/data/data/com.toly1994.opengl_video/cache/sh.mp4";
        File video =  new File(videoPath);
        // 初始化 VideoRender
        render = new VideoRender(getContext(),video);
        //设置渲染器
        setRenderer(render);
    }
}
复制代码

2. 渲染器 VideoRender 类定义

VideoRender 实现 GLSurfaceView.Renderer 接口用于处理 OpenGL 渲染回调。 使用 MediaPlayer,视频尺寸监听需要 OnVideoSizeChangedListener,故VideoRender 实现之。 SurfaceTexture新的流帧可用时会触发通知,VideoRender 实现 OnFrameAvailableListener

public class VideoRender implements
        GLSurfaceView.Renderer, // OpenGL 渲染回调
        SurfaceTexture.OnFrameAvailableListener,
        MediaPlayer.OnVideoSizeChangedListener
{

    private final Context context; // 上下文
    private final File video; // 视频文件
    private VideoDrawer videoDrawer; // 绘制器
    private int viewWidth, viewHeight, videoWidth, videoHeight; // 视频和屏幕尺寸  
          
    private MediaPlayer mediaPlayer; // 视频播放器
    private SurfaceTexture surfaceTexture; // 表面纹理
    private volatile boolean updateSurface; // 是否更新表面纹理
    private int textureId; // 纹理 id 

    public VideoRender(Context context, File video) {
        this.context = context;
        this.video = video;
    }
复制代码

3. 三个接口回调说明
  • GLSurfaceView.Renderer 中有三个回调,注意:它们都是在子线程GLThread中执行的。
  • onSurfaceCreated:surface 创建或重是回调 ,一般用于资源初始化;
  • onSurfaceChanged:surface的尺寸变化时回调 ,用于变换矩阵设置;
  • onDrawFrame:每帧绘制时回调,用于绘制。

image-20201207175852766

    @Override
    public void onSurfaceCreated(GL10 gl, EGLConfig config) {
        Log.e("VideoRender", "onSurfaceCreated: " + Thread.currentThread().getName());
        videoDrawer = new VideoDrawer(context); 
        initMediaPlayer(); // 初始化 MediaPlayer 
        mediaPlayer.start(); // 开始播放
    }

    @Override
    public void onSurfaceChanged(GL10 gl, int width, int height) {
        Log.e("VideoRender",
                "线程名: " + Thread.currentThread().getName() +
                        "-----onSurfaceChanged: (" + width + " , " + height + ")"
        );
    }

    @Override
    public void onDrawFrame(GL10 gl) {
        Log.e("VideoRender", "onDrawFrame: " + Thread.currentThread().getName());
    }
}
复制代码

  • OnVideoSizeChangedListener 中有一个回调 onVideoSizeChanged,在 mian 线程中进行,在此可以获得视频的尺寸。
  • OnFrameAvailableListener 中有一个回调 onFrameAvailable ,当新的流帧可用时会触发,在 mian 线程中进行,可以将更新纹理更新的 flag 标识为true
@Override
public void onVideoSizeChanged(MediaPlayer mp, int width, int height) {
    Log.e("VideoRender",
            "线程名: " + Thread.currentThread().getName() +
                    "-----onVideoSizeChanged: (" + width + " , " + height + ")"
    );
}

@Override
public void onFrameAvailable(SurfaceTexture surfaceTexture) {
    Log.e("VideoRender", "onFrameAvailable: "+Thread.currentThread().getName());
}
复制代码

4. 初始化 MediaPlayer 播放器

onSurfaceCreated 中进行 initMediaPlayer,主要是创建 MediaPlayer 对象,设置视频资源、音频流类型、音频流类型。比较重要的是绑定纹理创建 SurfaceTexture、Surface 对象 并为 MediaPlayer 设置 Surface

private void initMediaPlayer() {
    // 创建 MediaPlayer 对象
    mediaPlayer = new MediaPlayer();
    try {
        // 设置视频资源
        mediaPlayer.setDataSource(context, Uri.fromFile(video));
    } catch (IOException e) {
        e.printStackTrace();
    }
    // 设置音频流类型
    mediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);
  	// 设置循环播放
    mediaPlayer.setLooping(true);
    // 设置视频尺寸变化监听器
    mediaPlayer.setOnVideoSizeChangedListener(this);
  
    // 创建 surface 
    int[] textures = new int[1];
    GLES30.glGenTextures(1, textures, 0);
    textureId = textures[0];
    // 绑定纹理 id 
    GLES30.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, textureId);
    surfaceTexture = new SurfaceTexture(textureId);
    surfaceTexture.setOnFrameAvailableListener(this);
    Surface surface = new Surface(surfaceTexture);
    
    // 设置 surface
    mediaPlayer.setSurface(surface);
    surface.release();
    try {
        mediaPlayer.prepare();
    } catch (IOException t) {
        Log.e("Prepare ERROR", "onSurfaceCreated: ");
    }
}
复制代码

5.视频播放尺寸

这里通过 Matrix.orthoM 初始化正交投影矩阵,你可以通过 视频宽高比、视图宽高比 来设置投影的配置,关于投影这里就不展开了,你可以自己设置 Matrix.orthoM 的 3~7 参数来控制视频画面比例,这里 -1, 1, -1, 1, 表明填充整个视图。

private final float[] projectionMatrix = new float[16];

@Override
public void onSurfaceChanged(GL10 gl, int width, int height) {
    viewWidth = width;
    viewHeight = height;
    updateProjection();
    GLES30.glViewport(0, 0, viewWidth, viewHeight);
}

@Override
public void onVideoSizeChanged(MediaPlayer mp, int width, int height) {
    videoWidth = width;
    videoHeight = height;
}

private void updateProjection() {
    float screenRatio = (float) viewWidth / viewHeight;
    float videoRatio = (float) videoWidth / videoHeight;
    //正交投影矩阵
    Matrix.orthoM(projectionMatrix, 0,
            -1, 1, -1, 1,
            -1, 1);
}
复制代码

6. 绘制视频纹理

为了不让 VideoRender 看起来太乱,这里穿件一个 VideoDrawer 类进行绘制相关的资源准备绘制流程。在构造函数中加载着色器代码并初始化程序初始化顶点缓冲和纹理坐标缓冲。一些比较固定的流程,我把它们简单地封装在 BufferUtilsLoadUtils 中,可自行查看源码。

public class VideoDrawer {

    private FloatBuffer vertexBuffer;
    private FloatBuffer textureVertexBuffer;

    private final float[] vertexData = {
            1f, -1f, 0f,
            -1f, -1f, 0f,
            1f, 1f, 0f,
            -1f, 1f, 0f
    };

    private final float[] textureVertexData = {
            1f, 0f,
            0f, 0f,
            1f, 1f,
            0f, 1f
    };

    private final int aPositionLocation = 0;
    private final int aTextureCoordLocation = 1;
    private final int uMatrixLocation = 2;
    private final int uSTMMatrixLocation = 3;
    private final int uSTextureLocation = 4;

    private int programId;

    public VideoDrawer(Context context) {
        vertexBuffer = BufferUtils.getFloatBuffer(vertexData);
        textureVertexBuffer = BufferUtils.getFloatBuffer(textureVertexData);
        programId = LoadUtils.initProgram(context, "video.vsh", "red_video.fsh");
    }

    public void draw(int textureId, float[] projectionMatrix, float[] sTMatrix) {
        GLES30.glClear(GLES30.GL_DEPTH_BUFFER_BIT | GLES30.GL_COLOR_BUFFER_BIT);

        GLES30.glUseProgram(programId);
        GLES30.glUniformMatrix4fv(uMatrixLocation, 1, false, projectionMatrix, 0);
        GLES30.glUniformMatrix4fv(uSTMMatrixLocation, 1, false, sTMatrix, 0);

        GLES30.glEnableVertexAttribArray(aPositionLocation);
        GLES30.glVertexAttribPointer(aPositionLocation, 3, GLES30.GL_FLOAT, false, 12, vertexBuffer);

        GLES30.glEnableVertexAttribArray(aTextureCoordLocation);
        GLES30.glVertexAttribPointer(aTextureCoordLocation, 2, GLES30.GL_FLOAT, false, 8, textureVertexBuffer);
        GLES30.glTexParameterf(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES30.GL_TEXTURE_MAG_FILTER, GLES30.GL_LINEAR);

        GLES30.glActiveTexture(GLES30.GL_TEXTURE0);
        GLES30.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, textureId);
        GLES30.glUniform1i(uSTextureLocation, 0);
        GLES30.glDrawArrays(GLES30.GL_TRIANGLE_STRIP, 0, 4);
    }
}
复制代码

在 OpenGLES2.0 中需要对变量的句柄进行获取,OpenGLES3.0 可以通过 layout (location = X) 指定位置,从而更方便使用。 下面是顶点着色器 video.vsh

#version 300 es
layout (location = 0) in vec4 vPosition;//顶点位置
layout (location = 1) in vec4 vTexCoord;//纹理坐标
layout (location = 2) uniform mat4 uMatrix; //顶点变换矩阵
layout (location = 3) uniform mat4 uSTMatrix; //纹理变换矩阵

out vec2 texCoo2Frag; 

void main() {
    texCoo2Frag = (uSTMatrix * vTexCoord).xy;
    gl_Position = uMatrix*vPosition;
}
复制代码

下面是片段着色器 video.fsh ,使用 samplerExternalOES 纹理 需要 #extension GL_OES_EGL_image_external_essl3,通过纹理和纹理坐标设置 outColor ,从而展现出来。

#version 300 es
#extension GL_OES_EGL_image_external_essl3 : require
precision highp float;

in vec2 texCoo2Frag;
out vec4 outColor;

layout (location = 4) uniform samplerExternalOES sTexture;

void main() {
    outColor = texture(sTexture, texCoo2Frag);
}
复制代码

7. 绘制与纹理更新

从前面的日志截图来看,onDrawFrameonFrameAvailable 并不是在同一个线程中运行的,当 onFrameAvailable 触发时表示新的流帧可用,此时可以执行纹理更新。两个线程需要修改同一共享变量会存在线程安全问题,这也是加 synchronized 的原因。这样就可以正常播放了。

@Override
public void onDrawFrame(GL10 gl) {
    synchronized (this) {
        if (updateSurface) {
            surfaceTexture.updateTexImage();
            surfaceTexture.getTransformMatrix(sTMatrix);
            updateSurface = false;
        }
    }
    videoDrawer.draw(textureId, projectionMatrix, sTMatrix);
}

@Override
public void onFrameAvailable(SurfaceTexture surfaceTexture) {
    updateSurface = true;
}
复制代码

三、片段着色器的颜色特效

虽然上面看似一大堆东西,其实流程还是比较固定的,下面的重点就是片段着色器 的使用了。在我们眼中,一切可视的东西都是颜色,而 片段着色器 就是对不同位置的颜色进行处理。

1. 红色处理

通过 vec3 color = texture(sTexture, texCoo2Frag).rgb; 可以获取 rgb 三维颜色向量,某点颜色的 rgb 平均值大于 阈值 threshold 时 rgb都设为 1 ,即白色,否则gb为0 ,为红色,这样就可以实现红白效果,通过阈值的不同可以控制红色的通量,可以自己实验一下,另外阈值也可以作为参数通过外面进行传入。

红色

#version 300 es
#extension GL_OES_EGL_image_external_essl3 : require
precision highp float;

in vec2 texCoo2Frag;
out vec4 outColor;

layout (location = 4) uniform samplerExternalOES sTexture;

void main() {
    vec3 color = texture(sTexture, texCoo2Frag).rgb;
    float threshold = 0.7;//阈值
    float mean = (color.r + color.g + color.b) / 3.0;
    color.g = color.b = mean >= threshold ? 1.0 : 0.0;
    outColor = vec4(1,color.gb,1);
}
复制代码

同样通过固定蓝色通道也可以实现蓝色效果。这样你应该对着色器的作用有了简单的认识。

#version 300 es
#extension GL_OES_EGL_image_external_essl3 : require
precision highp float;

in vec2 texCoo2Frag;
out vec4 outColor;

layout (location = 4) uniform samplerExternalOES sTexture;

void main() {
    vec3 color = texture(sTexture, texCoo2Frag).rgb;
    float threshold = 0.7;//阈值
    float mean = (color.r + color.g + color.b) / 3.0;
    color.r = color.g = mean >= threshold ? 1.0 : 0.0;
    outColor = vec4(color.rg, 1, 1);
}
复制代码

2.负片效果

将 rgb 通道分别被 1 减就可以得到负片效果。
绘制器: view/VideoDrawer.java
顶点着色器 video.vsh
片段着色器: negative_video.fsh

#version 300 es
#extension GL_OES_EGL_image_external_essl3 : require
precision highp float;

in vec2 texCoo2Frag;
out vec4 outColor;

layout (location = 4) uniform samplerExternalOES sTexture;

void main() {
    vec4 color= texture(sTexture, texCoo2Frag);
    float r = 1.0 - color.r;
    float g = 1.0 - color.g;
    float b = 1.0 - color.b;
    outColor = vec4(r, g, b, 1.0);
}
复制代码

3.灰度效果

将 rgb 通道都置为 g 可得到灰度效果。
绘制器: view/VideoDrawer.java
顶点着色器 video.vsh
片段着色器: grey_video.fsh

#version 300 es
#extension GL_OES_EGL_image_external_essl3 : require
precision highp float;

in vec2 texCoo2Frag;
out vec4 outColor;

layout (location = 4) uniform samplerExternalOES sTexture;

void main() {
    vec4 color = texture(sTexture, texCoo2Frag);
    outColor = vec4(color.g, color.g, color.g, 1.0);
}
复制代码

4.怀旧效果

绘制器: view/VideoDrawer.java
顶点着色器 video.vsh
片段着色器: nostalgic_video.fsh

#version 300 es
#extension GL_OES_EGL_image_external_essl3 : require
precision highp float;

in vec2 texCoo2Frag;
out vec4 outColor;

layout (location = 4) uniform samplerExternalOES sTexture;

void main() {
    vec4 color = texture(sTexture, texCoo2Frag);
    float r = color.r;
    float g = color.g;
    float b = color.b;
    
    r = 0.393* r + 0.769 * g + 0.189* b;
    g = 0.349 * r + 0.686 * g + 0.168 * b;
    b = 0.272 * r + 0.534 * g + 0.131 * b;
    outColor = vec4(r, g, b, 1.0);
}
复制代码

5. 流年效果

绘制器: view/VideoDrawer.java
顶点着色器 video.vsh
片段着色器: year_video.fsh

#version 300 es
#extension GL_OES_EGL_image_external_essl3 : require
precision highp float;

in vec2 texCoo2Frag;
out vec4 outColor;

layout (location = 4) uniform samplerExternalOES sTexture;

void main(){
    float arg = 1.5;

    vec4 color= texture(sTexture, texCoo2Frag);
    float r = color.r;
    float g = color.g;
    float b = color.b;
    b = sqrt(b)*arg;
  
    if (b>1.0) b = 1.0;

    outColor = vec4(r, g, b, 1.0);
}
复制代码

其实说白了,这就是玩颜色。一些颜色的算法,无论是在什么平台、什么框架、什么系统,只要能获得图片的 rgb 通道值,就能按照这些算法进行图片特效处理。所以关于颜色的特效效果,重要是平时的积累和对颜色的认识,这些可以自己多找找,多试试。


四、片段着色器的位置特效

除了可以玩颜色,我们也可以通过纹理坐标的位置对视频传入的纹理进行特效处理,比如镜像、分镜、马赛克等。

1.镜像

绘制器: view/VideoDrawer.java
顶点着色器 video.vsh
片段着色器: mirror_video.fsh

先从一个简单的效果来看 纹理坐标 的位置,纹理左上角为(0,0),往右最大为 1 。下面处理逻辑为:当 x 大于 0.5 时 ,x 取 1-x 值,这样,在右侧的像素点就和左侧对称,从而达到下面的镜像效果。

#version 300 es
#extension GL_OES_EGL_image_external_essl3 : require
precision highp float;

in vec2 texCoo2Frag;
out vec4 outColor;

layout (location = 4) uniform samplerExternalOES sTexture;

void main() {
    vec2 pos = texCoo2Frag;
    if (pos.x > 0.5) {
        pos.x = 1.0 - pos.x;
    }
    outColor = texture(sTexture, pos);
}
复制代码

2. 分镜效果

利用缩放和坐标的偏移,可以实现四个视频贴图同屏的效果,当然你可以自己选择分两个、四个、六个、八个...... 算就完事了。
绘制器: view/VideoDrawer.java
顶点着色器 video.vsh
片段着色器: fenjing.fsh

#version 300 es
#extension GL_OES_EGL_image_external_essl3 : require
precision highp float;

in vec2 texCoo2Frag;
out vec4 outColor;

layout (location = 4) uniform samplerExternalOES sTexture;

void main(){
    vec2 pos = texCoo2Frag.xy;
    if (pos.x <= 0.5 && pos.y<= 0.5){ //左上
        pos.x = pos.x * 2.0;
        pos.y = pos.y * 2.0;
    } else if (pos.x > 0.5 && pos.y< 0.5){ //右上
        pos.x = (pos.x - 0.5) * 2.0;
        pos.y = (pos.y) * 2.0;
    } else if (pos.y> 0.5 && pos.x < 0.5) { //左下
        pos.y = (pos.y - 0.5) * 2.0;
        pos.x = pos.x * 2.0;
    } else if (pos.y> 0.5 && pos.x > 0.5){ //右下
        pos.y = (pos.y - 0.5) * 2.0;
        pos.x = (pos.x - 0.5) * 2.0;
    }
    outColor = texture(sTexture, pos);
}
复制代码

你也可以在每个分镜里进行不同的特效处理,比如将之前的颜色效果用在不同的分镜中。如你闲着无聊,可以在分镜中再加分镜...
绘制器: view/VideoDrawer.java
顶点着色器 video.vsh
片段着色器: splite.fsh

#version 300 es
#extension GL_OES_EGL_image_external_essl3 : require
precision highp float;

in vec2 texCoo2Frag;
out vec4 outColor;

layout (location = 4) uniform samplerExternalOES sTexture;

void main(){
    vec2 pos = texCoo2Frag.xy;
    vec4 result;

    if (pos.x <= 0.5 && pos.y<= 0.5){ //左上
        pos.x = pos.x * 2.0;
        pos.y = pos.y * 2.0;
        vec4 color = texture(sTexture, pos);
        result = vec4(color.g, color.g, color.g, 1.0);
    } else if (pos.x > 0.5 && pos.y< 0.5){ //右上
        pos.x = (pos.x - 0.5) * 2.0;
        pos.y = (pos.y) * 2.0;
        vec4 color= texture(sTexture, pos);
        float arg = 1.5;
        float r = color.r;
        float g = color.g;
        float b = color.b;
        b = sqrt(b)*arg;
        if (b>1.0) b = 1.0;
        result = vec4(r, g, b, 1.0);
    } else if (pos.y> 0.5 && pos.x < 0.5) { //左下
        pos.y = (pos.y - 0.5) * 2.0;
        pos.x = pos.x * 2.0;
        vec4 color= texture(sTexture, pos);
        float r = color.r;
        float g = color.g;
        float b = color.b;
        r = 0.393* r + 0.769 * g + 0.189* b;
        g = 0.349 * r + 0.686 * g + 0.168 * b;
        b = 0.272 * r + 0.534 * g + 0.131 * b;
        result = vec4(r, g, b, 1.0);
    } else if (pos.y> 0.5 && pos.x > 0.5){ //右下
        pos.y = (pos.y - 0.5) * 2.0;
        pos.x = (pos.x - 0.5) * 2.0;
        vec4 color= texture(sTexture, pos);
        float r = color.r;
        float g = color.g;
        float b = color.b;
        b = 0.393* r + 0.769 * g + 0.189* b;
        g = 0.349 * r + 0.686 * g + 0.168 * b;
        r = 0.272 * r + 0.534 * g + 0.131 * b;
        result = vec4(r, g, b, 1.0);
    }
    outColor = result;
}
复制代码

3. 马赛克效果

绘制器: view/VideoDrawer.java
顶点着色器 video.vsh
片段着色器: mask_rect.fsh

先从简单的方形马赛克看起,这里2264.0 / 1080.0 是画面的宽高比,这里写死了,可以通过外面传入进来。cellX 和 cellY 控制小矩形的宽高,count 用来控制多少,count 越大,马赛克越密集。

#version 300 es
#extension GL_OES_EGL_image_external_essl3 : require
precision highp float;

in vec2 texCoo2Frag;
out vec4 outColor;

layout (location = 4) uniform samplerExternalOES sTexture;

void main(){
    float rate= 2264.0 / 1080.0;
    float cellX= 1.0;
    float cellY= 1.0;
    float count = 80.0;

    vec2 pos = texCoo2Frag;
    pos.x = pos.x*count;
    pos.y = pos.y*count/rate;

    pos = vec2(floor(pos.x/cellX)*cellX/count, floor(pos.y/cellY)*cellY/(count/rate))+ 0.5/count*vec2(cellX, cellY);
    outColor = texture(sTexture, pos);
}
复制代码

除了方形,还可以圆形马赛克。
绘制器: view/VideoDrawer.java
顶点着色器 video.vsh
片段着色器: ball_mask.fsh

#version 300 es
#extension GL_OES_EGL_image_external_essl3 : require
precision highp float;

in vec2 texCoo2Frag;
out vec4 outColor;

layout (location = 4) uniform samplerExternalOES sTexture;

void main(){
    float rate= 2264.0 / 1080.0;
    float cellX= 3.0;
    float cellY= 3.0;
    float rowCount=300.0;

    vec2 sizeFmt=vec2(rowCount, rowCount/rate);
    vec2 sizeMsk=vec2(cellX, cellY);
    vec2 posFmt = vec2(texCoo2Frag.x*sizeFmt.x, texCoo2Frag.y*sizeFmt.y);
    vec2 posMsk = vec2(floor(posFmt.x/sizeMsk.x)*sizeMsk.x, floor(posFmt.y/sizeMsk.y)*sizeMsk.y)+ 0.5*sizeMsk;
    float del = length(posMsk - posFmt);
    vec2 UVMosaic = vec2(posMsk.x/sizeFmt.x, posMsk.y/sizeFmt.y);

    vec4 result;
    if (del< cellX/2.0)
    result = texture(sTexture, UVMosaic);
    else
    result = vec4(1.0,1.0,1.0,0.0);
    outColor = result;
}
复制代码

六边形马赛克效果
绘制器: view/VideoDrawer.java
顶点着色器 video.vsh
片段着色器: video_mask.fsh

#version 300 es
#extension GL_OES_EGL_image_external_essl3 : require
precision highp float;

in vec2 texCoo2Frag;
out vec4 outColor;

layout (location = 4) uniform samplerExternalOES sTexture;

//六边型的增量
const float mosaicSize = 0.01;

void main (void)
{
  float rate= 2264.0 / 1080.0;
  float length = mosaicSize;
  float TR = 0.866025;

  //纹理坐标值
  float x = texCoo2Frag.x;
  float y = texCoo2Frag.y;

  //转化为矩阵中的坐标
  int wx = int(x / 1.5 / length);
  int wy = int(y / TR / length);
  vec2 v1, v2, vn;

  //分析矩阵中的坐标是在奇数还是在偶数行,根据奇数偶数值来确定我们的矩阵的角标坐标值
  if (wx/2 * 2 == wx) {
    if (wy/2 * 2 == wy) {
      //(0,0),(1,1)
      v1 = vec2(length * 1.5 * float(wx), length * TR * float(wy));
      v2 = vec2(length * 1.5 * float(wx + 1), length * TR * float(wy + 1));
    } else {
      //(0,1),(1,0)
      v1 = vec2(length * 1.5 * float(wx), length * TR * float(wy + 1));
      v2 = vec2(length * 1.5 * float(wx + 1), length * TR * float(wy));
    }
  }else {
    if (wy/2 * 2 == wy) {
      //(0,1),(1,0)
      v1 = vec2(length * 1.5 * float(wx), length * TR * float(wy + 1));
      v2 = vec2(length * 1.5 * float(wx + 1), length * TR * float(wy));
    } else {
      //(0,0),(1,1)
      v1 = vec2(length * 1.5 * float(wx), length * TR * float(wy));
      v2 = vec2(length * 1.5 * float(wx + 1), length * TR * float(wy + 1));
    }
  }

  //获取距离
  float s1 = sqrt(pow(v1.x - x, 2.0) + pow(v1.y - y, 2.0));
  float s2 = sqrt(pow(v2.x - x, 2.0) + pow(v2.y - y, 2.0));

  //设置具体的纹理坐标
  if (s1 < s2) {
    vn = v1;
  } else {
    vn = v2;
  }
  vec4 color = texture(sTexture, vn);
  outColor = color;
}
复制代码

四、片段着色器动态效果

前面的效果是写死的变量,其实很多量可以从外界传入,而达到动态的特效,比如 灵魂出窍杂色抖动 等。借此也可以说明一下如何在外界将参数传入着色器。

1. 灵魂出窍

绘制器: view/VideoDrawerPlus.java
顶点着色器 video.vsh
片段着色器: gost.fsh

通过 uProgress 变量控制扩散的进度,现在只需要在绘制时动态改变进度即可。

#version 300 es
#extension GL_OES_EGL_image_external_essl3 : require
precision highp float;

in vec2 texCoo2Frag;
out vec4 outColor;

layout (location = 4) uniform samplerExternalOES sTexture;

// 进度值
layout (location = 5) uniform float uProgress;

void main (void) {
  //周期
  float duration = 0.7;
  //生成的第二个图层的最大透明度
  float maxAlpha = 0.4;
  //第二个图层放大的最大比率
  float maxScale = 1.8;

  //进度
  float progress = mod(uProgress, duration) / duration; // 0~1
  //当前的透明度
  float alpha = maxAlpha * (1.0 - progress);
  //当前的放大比例
  float scale = 1.0 + (maxScale - 1.0) * progress;

  //根据放大比例获取对应的x、y值坐标
  float weakX = 0.5 + (texCoo2Frag.x - 0.5) / scale;
  float weakY = 0.5 + (texCoo2Frag.y - 0.5) / scale;
  //新的图层纹理坐标
  vec2 weakTextureCoords = vec2(weakX, weakY);

  //新图层纹理坐标对应的纹理像素值
  vec4 weakMask = texture(sTexture, weakTextureCoords);

  vec4 mask = texture(sTexture, texCoo2Frag);

  //纹理像素值的混合公式,获得混合后的实际颜色
  outColor = mask * (1.0 - alpha) + weakMask * alpha;
}
复制代码

为避免混乱,这里新建了一个类 com/toly1994/opengl_video/view/VideoDrawerPlus.java ,需要做的只是定义 uProgressLocation,在 draw 中更新 progress 并通过 glUniform1f 设置即可。

private final int uProgressLocation = 5;
private float progress = 0.0f;

public void draw(int textureId, float[] projectionMatrix, float[] sTMatrix) {
    progress += 0.02;
    GLES30.glClear(GLES30.GL_DEPTH_BUFFER_BIT | GLES30.GL_COLOR_BUFFER_BIT);
    GLES30.glUseProgram(programId);
    GLES30.glUniform1f(uProgressLocation, progress);
    // 略同
}
复制代码

2. 毛刺效果

绘制器: view/VideoDrawerPlus.java
顶点着色器 video.vsh
片段着色器: video_ci.fsh

#version 300 es
#extension GL_OES_EGL_image_external_essl3 : require
precision highp float;

in vec2 texCoo2Frag;
out vec4 outColor;

layout (location = 4) uniform samplerExternalOES sTexture;

// 进度值
layout (location = 5) uniform float uProgress;

const float PI = 3.14159265;
const float uD = 80.0;
const float uR = 0.5;

//这个函数式c中获取随机数的
float rand(float n) {
  //返回fract(x)的x小数部分
  return fract(sin(n) * 43758.5453123);
}

void main (void) {
  //最大抖动
  float maxJitter = 0.2;
  float duration = 0.4;
  //红色颜色偏移量
  float colorROffset = 0.01;
  //蓝色颜色偏移量
  float colorBOffset = -0.025;

  //当前周期的时间
  float time = mod(uProgress, duration * 2.0);
  //当前振幅0.0 ~ 1.0
  float amplitude = max(sin(uProgress * (PI / duration)), 0.0);

  // 当前坐标的y值获取随机偏移值  -1~1
  float jitter = rand(texCoo2Frag.y) * 2.0 - 1.0;
  //判断当前的坐标是否需要偏移
  bool needOffset = abs(jitter) < maxJitter * amplitude;

  //获取纹理x值,根据是否大于某一个阀值来判断到底在x方向应该偏移多少
  float textureX = texCoo2Frag.x + (needOffset ? jitter : (jitter * amplitude * 0.006));
  //x轴方向进行撕裂之后的纹理坐标
  vec2 textureCoords = vec2(textureX, texCoo2Frag.y);

  //颜色偏移3组颜色
  vec4 mask = texture(sTexture, textureCoords);
  vec4 maskR = texture(sTexture, textureCoords + vec2(colorROffset * amplitude, 0.0));
  vec4 maskB = texture(sTexture, textureCoords + vec2(colorBOffset * amplitude, 0.0));

  //最终根据三组不同的纹理坐标值来获取最终的颜色
  outColor = vec4(maskR.r, mask.g, maskB.b, mask.a);
}
复制代码

3.色散效果

绘制器: view/VideoDrawerPlus.java
顶点着色器 video.vsh
片段着色器: video_offset.fsh

#version 300 es
#extension GL_OES_EGL_image_external_essl3 : require
precision highp float;

in vec2 texCoo2Frag;
out vec4 outColor;

layout (location = 4) uniform samplerExternalOES sTexture;

// 进度值
layout (location = 5) uniform float uProgress;

void main (void) {
  //周期
  float duration = 0.7;
  //生成的第二个图层的最大透明度
  float maxAlpha = 0.4;
  //第二个图层放大的最大比率
  float maxScale = 1.8;

  //进度
  float progress = mod(uProgress, duration) / duration; // 0~1
  //当前的透明度
  float alpha = maxAlpha * (1.0 - progress);
  //当前的放大比例
  float scale = 1.0 + (maxScale - 1.0) * progress;

  //根据放大比例获取对应的x、y值坐标
  float weakX = 0.5 + (texCoo2Frag.x - 0.5) / scale;
  float weakY = 0.5 + (texCoo2Frag.y - 0.5) / scale;
  //新的图层纹理坐标
  vec2 weakTextureCoords = vec2(weakX, weakY);

  //新图层纹理坐标对应的纹理像素值
  vec4 weakMask = texture(sTexture, weakTextureCoords);

  vec4 mask = texture(sTexture, texCoo2Frag);

  //纹理像素值的混合公式,获得混合后的实际颜色
  outColor = mask * (1.0 - alpha) + weakMask * alpha;
}
复制代码

4.抖动效果

绘制器: view/VideoDrawerPlus.java
顶点着色器 video_scale.vsh
片段着色器: video_offset.fsh

抖动是针对顶点着色器的变换矩阵进行不断地缩放操作产生的效果,片段着色器也可以同时进行特效,如下是抖动和色散的结合。

---->[video_scale.vsh]----
#version 300 es
layout (location = 0) in vec4 vPosition;//顶点位置
layout (location = 1) in vec4 vTexCoord;//纹理坐标
layout (location = 2) uniform mat4 uMatrix;
layout (location = 3) uniform mat4 uSTMatrix;
//当前的时间
layout (location = 5) uniform float uProgress;

out vec2 texCoo2Frag;
const float PI = 3.1415926;
void main() {
    
    //周期
    float duration = 0.6;
    //缩放的最大值
    float maxAmplitude = 0.3;

    //类似取余,表示当前周期中的时间值
    float time = mod(uProgress, duration);
    //根据周期中的位置,获取当前的放大值
    float amplitude = 1.0 + maxAmplitude * abs(sin(time * (PI / duration)));
    //当前顶点转化到屏幕坐标的位置
    gl_Position = uMatrix*vec4(vPosition.x * amplitude, vPosition.y * amplitude, vPosition.zw);
    texCoo2Frag = (uSTMatrix * vTexCoord).xy;
}
复制代码

5. 扭曲效果

绘制器: view/VideoDrawerPlus.java
顶点着色器 video.vsh
片段着色器: video_rotate.fsh

#version 300 es
#extension GL_OES_EGL_image_external_essl3 : require
precision highp float;

in vec2 texCoo2Frag;
out vec4 outColor;

layout (location = 4) uniform samplerExternalOES sTexture;
layout (location = 5) uniform float uProgress;

const float PI = 3.14159265;
const float uD = 80.0;
const float uR = 1.0;

void main()
{
  float rate= 2264.0 / 1080.0;
  ivec2 ires = ivec2(128, 128);
  float res = float(ires.s);
  //周期
  float duration = 3.0;
  vec2 st = texCoo2Frag;
  float radius = res * uR;
  //进度
  float progress = mod(uProgress, duration) / duration; // 0~1
  vec2 xy = res * st;

  vec2 dxy = xy - vec2(res/2., res/2.);
  float r = length(dxy);

  //(1.0 - r/Radius);
  float beta = atan(dxy.y, dxy.x) + radians(uD) * 2.0 * (-(r/radius)*(r/radius) + 1.0);

  vec2 xy1 = xy;
  if(r<=radius) {
    xy1 = res/2. + r*vec2(cos(beta), sin(beta))*progress;
  }
  st = xy1/res;

  vec3 irgb = texture(sTexture, st).rgb;

  outColor = vec4( irgb, 1.0 );
}
复制代码

总的来说,关于特效,就是对纹理位置顶点位置片段颜色进行操作。很多着色器都是我平时收集的,一些很长的着色器代码我也不大看得懂,后面会好好研究它们。这些着色器集聚着先驱者们对世界变换的思考、对于视觉呈现的执著、在此致敬。也希望后来者也可以设计出有意思的着色器。

@张风捷特烈 2020.12.08 未允禁转
我的公众号:编程之王
联系我--邮箱:1981462002@qq.com -- 微信:zdl1994328
~ END ~