主题列表:juejin, github, smartblue, cyanosis, channing-cyan, fancy, hydrogen, condensed-night-purple, greenwillow, v-green, vue-pro, healer-readable
贡献主题:https://github.com/xitu/juejin-markdown-themes
theme: juejin
highlight:
零、前言
最近很久都在做 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 地址: https://github.com/toly1994328/opengl_video
1. 准备资源
既然要实现视频特效,那么必然要有视频,
本文只不想牵涉运行时权限,所以视频资源放在可访问的目录下
。 大家可以酌情处理,只要能会获取到视频资源即可。
2. 全屏横屏处理
在
MainActivity#onCreate
中进行全屏横屏
操作。
```java 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
中进行展示。
dart @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
,
```java 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
。
```java 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:每帧绘制时回调
,用于绘制。
```java @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
;
```java @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
。
```java 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,
表明填充整个视图。
```java 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
类进行绘制相关的资源准备
和绘制流程
。在构造函数中加载着色器代码并初始化程序
、初始化顶点缓冲和纹理坐标缓冲
。一些比较固定的流程,我把它们简单地封装在BufferUtils
和LoadUtils
中,可自行查看源码。
```Java 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
```glsl
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
,从而展现出来。
```glsl
version 300 es
extension GLOESEGLimageexternal_essl3 : require
precision highp float;
in vec2 texCoo2Frag; out vec4 outColor;
layout (location = 4) uniform samplerExternalOES sTexture;
void main() { outColor = texture(sTexture, texCoo2Frag); } ```
7. 绘制与纹理更新
从前面的日志截图来看,
onDrawFrame
和onFrameAvailable
并不是在同一个线程中运行的,当onFrameAvailable
触发时表示新的流帧可用
,此时可以执行纹理更新。两个线程需要修改同一共享变量会存在线程安全问题,这也是加synchronized
的原因。这样就可以正常播放了。
```Java @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 ,为红色,这样就可以实现红白效果,通过阈值的不同可以控制红色的通量,可以自己实验一下,另外阈值也可以作为参数通过外面进行传入。
```glsl
version 300 es
extension GLOESEGLimageexternal_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); } ```
同样通过固定蓝色通道也可以实现蓝色效果。这样你应该对着色器的作用有了简单的认识。
```glsl
version 300 es
extension GLOESEGLimageexternal_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
```glsl
version 300 es
extension GLOESEGLimageexternal_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
```glsl
version 300 es
extension GLOESEGLimageexternal_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
```glsl
version 300 es
extension GLOESEGLimageexternal_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
```glsl
version 300 es
extension GLOESEGLimageexternal_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
值,这样,在右侧的像素点就和左侧对称,从而达到下面的镜像效果。
```glsl
version 300 es
extension GLOESEGLimageexternal_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
```glsl
version 300 es
extension GLOESEGLimageexternal_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
```glsl
version 300 es
extension GLOESEGLimageexternal_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 越大,马赛克越密集。
```glsl
version 300 es
extension GLOESEGLimageexternal_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
```glsl
version 300 es
extension GLOESEGLimageexternal_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
```glsl
version 300 es
extension GLOESEGLimageexternal_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
变量控制扩散的进度,现在只需要在绘制时动态改变进度即可。
```Java
version 300 es
extension GLOESEGLimageexternal_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
设置即可。
```java 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.GLDEPTHBUFFERBIT | GLES30.GLCOLORBUFFERBIT); GLES30.glUseProgram(programId); GLES30.glUniform1f(uProgressLocation, progress); // 略同 } ```
2. 毛刺效果
绘制器:
view/VideoDrawerPlus.java
顶点着色器video.vsh
片段着色器:video_ci.fsh
```glsl
version 300 es
extension GLOESEGLimageexternal_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
```glsl
version 300 es
extension GLOESEGLimageexternal_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
抖动是针对顶点着色器的变换矩阵进行不断地缩放操作产生的效果,片段着色器也可以同时进行特效,如下是抖动和色散的结合。
```glsl ---->[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
```glsl
version 300 es
extension GLOESEGLimageexternal_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. + rvec2(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 ~