零、前言
最近很久都在做 Flutter 相关的事,工作之外,闲余的时间大部分都奉献给了 FlutterUnit 以及在 Flutter 群里吹牛 ,OpenGLES3.0 的系列搁浅了很久。最近忙完一段时间,想捡起一下,更新一篇视频特效相关的内容。温馨提示:
本篇 gif 图片较多且比较大,注意流量
。
- [- 多媒体 -] OpenGLES3.0 接入视频实现特效 - 引言
- [ - OpenGLES3.0 - ] 第一集 主线 - 打开新世界的大门
- [ - OpenGLES3.0 - ] 第二集 主线 - 绘制面与图片贴图
- [ - OpenGLES3.0 - ] 第三集 主线 - shader着色器与图片特效
- - OpenGLES3.0 - 第四集 支线1 - 视频接入OpenGLES3.0实现特效 - this
前面说过 OpenGLES 可以利用
片段着色器
对纹理贴图
进行特效处理。对应视频来说也是一样,比如下面的红色效果,通过MediaPlayer
不断更新视频纹理,再由OpenGLES
进行绘制,在此之间就可以通过片段着色器
对纹理进行操作,从而达到各种各样的特效。
比如通过控制片段着色器的输出颜色而产生颜色相关的特效
| |
---|---|
| |
| |
比如通过控制片段着色器纹理坐标实现特效
| |
---|---|
| |
| |
比如通过入参实现动态效果
| |
---|---|
| |
| |
一、准备工作
项目 github 地址: github.com/toly1994328…
1. 准备资源
既然要实现视频特效,那么必然要有视频,
本文只不想牵涉运行时权限,所以视频资源放在可访问的目录下
。 大家可以酌情处理,只要能会获取到视频资源即可。
2. 全屏横屏处理
代码语言:javascript复制在
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
代码语言:javascript复制通过
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
代码语言:javascript复制
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
类定义
代码语言:javascript复制
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:每帧绘制时回调
,用于绘制。
@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
播放器
代码语言:javascript复制在
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.视频播放尺寸
代码语言:javascript复制这里通过
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. 绘制视频纹理
代码语言:javascript复制为了不让
VideoRender
看起来太乱,这里穿件一个VideoDrawer
类进行绘制相关的资源准备
和绘制流程
。在构造函数中加载着色器代码并初始化程序
、初始化顶点缓冲和纹理坐标缓冲
。一些比较固定的流程,我把它们简单地封装在BufferUtils
和LoadUtils
中,可自行查看源码。
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);
}
}
复制代码
代码语言:javascript复制在 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;
}
代码语言:javascript复制下面是片段着色器
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. 绘制与纹理更新
代码语言:javascript复制从前面的日志截图来看,
onDrawFrame
和onFrameAvailable
并不是在同一个线程中运行的,当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. 红色处理
代码语言:javascript复制通过
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);
}
代码语言:javascript复制同样通过固定蓝色通道也可以实现蓝色效果。这样你应该对着色器的作用有了简单的认识。
#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.负片效果
代码语言:javascript复制将 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.灰度效果
代码语言:javascript复制将 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.怀旧效果
代码语言:javascript复制绘制器:
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. 流年效果
代码语言:javascript复制绘制器:
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.镜像
代码语言:javascript复制绘制器:
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. 分镜效果
代码语言:javascript复制利用缩放和坐标的偏移,可以实现四个视频贴图同屏的效果,当然你可以自己选择分两个、四个、六个、八个...... 算就完事了。 绘制器:
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);
}
代码语言:javascript复制你也可以在每个分镜里进行不同的特效处理,比如将之前的颜色效果用在不同的分镜中。如你闲着无聊,可以在分镜中再加分镜... 绘制器:
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. 马赛克效果
代码语言:javascript复制绘制器:
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);
}
代码语言:javascript复制除了方形,还可以圆形马赛克。 绘制器:
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;
}
代码语言:javascript复制六边形马赛克效果 绘制器:
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. 灵魂出窍
代码语言:javascript复制绘制器:
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;
}
复制代码
代码语言:javascript复制为避免混乱,这里新建了一个类
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. 毛刺效果
代码语言:javascript复制绘制器:
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.色散效果
代码语言:javascript复制绘制器:
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.抖动效果
代码语言:javascript复制绘制器:
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. 扭曲效果
代码语言:javascript复制绘制器:
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 -- 微信:
~ END ~