位置滤镜介绍
本节介绍如何改变改变片元着色器内的坐标位置参数,从而让渲染的内容动起来或者达到一些特殊的效果。git
位置滤镜效果
github
实现讲解
本节课的核心原理是修改采样的纹理坐标。这是以前课程中的纹理坐标图,纹理默认传入的读取范围是 (0,0) 到 (1,1) 的范围内读取颜色值。web
ST纹理坐标微信
若是对读取的位置进行调整修改,那么就能够作出各类各样的效果。好比缩放动画,让读取的范围改为 (-1, -1) 到 (2, 2)。app
1. 位移滤镜
/**
* 位移滤镜
*
* @author Benhero
* @date 2019-1-17
*/
class TranslateFilter(context: Context) : BaseFilter(context, VERTEX_SHADER, FRAGMENT_SHADER) {
companion object {
const val FRAGMENT_SHADER = """
precision mediump float;
varying vec2 v_TexCoord;
uniform sampler2D u_TextureUnit;
uniform float xV;
uniform float yV;
vec2 translate(vec2 srcCoord, float x, float y) {
return vec2(srcCoord.x + x, srcCoord.y + y);
}
void main() {
vec2 offsetTexCoord = translate(v_TexCoord, xV, yV);
if (offsetTexCoord.x >= 0.0 && offsetTexCoord.x <= 1.0 &&
offsetTexCoord.y >= 0.0 && offsetTexCoord.y <= 1.0) {
gl_FragColor = texture2D(u_TextureUnit, offsetTexCoord);
}
}
"""
}
private var xLocation: Int = 0
private var yLocation: Int = 0
private var startTime: Long = 0
override fun onCreated() {
super.onCreated()
startTime = System.currentTimeMillis()
xLocation = getUniform("xV")
yLocation = getUniform("yV")
}
override fun onDraw() {
super.onDraw()
val intensity = Math.sin((System.currentTimeMillis() - startTime) / 1000.0) * 0.5
GLES20.glUniform1f(xLocation, intensity.toFloat())
GLES20.glUniform1f(yLocation, 0.0f)
}
}
这个滤镜的核心有两个部分,一个是对纹理坐标的改变:编辑器
vec2 translate(vec2 srcCoord, float x, float y) {
return vec2(srcCoord.x + x, srcCoord.y + y);
}
另一个是限定纹理的采样范围:ide
if (offsetTexCoord.x >= 0.0 && offsetTexCoord.x <= 1.0 &&
offsetTexCoord.y >= 0.0 && offsetTexCoord.y <= 1.0) {}
这样子就能够控制超过了 (0, 0) 到( 1, 1) 范围的就不绘制。不然位移滤镜就会出现下面的效果:学习
位移动画-无限制flex
之因此会这样子的效果,是由于纹理采样的环绕方式问题。这里补充下《纹理绘制》章节没有讲解到的这个知识点。动画
纹理环绕方式 - 图源自LearnOpenGLCN
上图展现了超过 (0, 0) 到 (1, 1) 范围时,设置不一样环绕方式的效果。在默认状况下,系统会采用 GL_REPEAT 模式。
若是咱们想要位移滤镜运动只有1只皮卡丘,那么能够设置 GL_CLAMP_TO_BORDER 模式。
可是呢!
这个模式,在 Android OpenGL ES 2.0 版本是没有的,只有等到了 Android 24 版本,也就是 7.0 版本,Android OpenGL ES 3.2 的版本才引入的,详情能够参考API文档。
讲了一圈回来,要实现这个属性的效果,只能咱们自行判断纹理坐标采样范围的来控制绘制实现。
不过,如有更好的实现方式,请告诉我。
2. 缩放滤镜
/**
* 缩放滤镜
*
* @author Benhero
* @date 2019-1-16
*/
class ScaleFilter(context: Context) : BaseFilter(context, VERTEX_SHADER, FRAGMENT_SHADER) {
companion object {
const val FRAGMENT_SHADER = """
precision mediump float;
varying vec2 v_TexCoord;
uniform sampler2D u_TextureUnit;
uniform float intensity;
vec2 scale(vec2 srcCoord, float x, float y) {
return vec2((srcCoord.x - 0.5) / x + 0.5, (srcCoord.y - 0.5) / y + 0.5);
}
void main() {
vec2 offsetTexCoord = scale(v_TexCoord, intensity, intensity);
if (offsetTexCoord.x >= 0.0 && offsetTexCoord.x <= 1.0 &&
offsetTexCoord.y >= 0.0 && offsetTexCoord.y <= 1.0) {
gl_FragColor = texture2D(u_TextureUnit, offsetTexCoord);
}
}
"""
}
private var intensityLocation: Int = 0
private var startTime: Long = 0
override fun onCreated() {
super.onCreated()
startTime = System.currentTimeMillis()
intensityLocation = getUniform("intensity")
}
override fun onDraw() {
super.onDraw()
val intensity = Math.abs(Math.sin((System.currentTimeMillis() - startTime) / 1000.0)) + 0.5
GLES20.glUniform1f(intensityLocation, intensity.toFloat())
}
}
缩放滤镜和平移滤镜的思路差很少,也须要限制纹理采样的范围。那么讲解下缩放的计算。再贴一次纹理坐标图:
ST纹理坐标
vec2 scale(vec2 srcCoord, float x, float y) {
return vec2((srcCoord.x - 0.5) / x + 0.5, (srcCoord.y - 0.5) / y + 0.5);
}
代码中,参数 srcCoord 是本来的纹理坐标,也就是 (0, 0) 到 (1, 1) 范围内取值。参数x、y 分别是两个方向的缩放比例。
因此,要让图片变小成原来的二分之一,那么就须要让纹理的采样范围变大为原来的2倍。
因为这个缩放的中心点在图的中心,是 0.5 ,因此能够先计算当前片元距离中心点的距离,而后再进行拉伸指定的倍数,也就是 (srcCoord.x - 0.5) / x 的意义,最后再加上0.5,就是这个片元缩放后,距离中心点的距离。
3. 彻底克隆滤镜
/**
* 彻底分身克隆滤镜
*
* @author Benhero
* @date 2019/1/18
*/
class CloneFullFilter(context: Context) : BaseFilter(context, VERTEX_SHADER, FRAGMENT_SHADER) {
companion object {
const val FRAGMENT_SHADER = """
precision mediump float;
varying vec2 v_TexCoord;
uniform sampler2D u_TextureUnit;
uniform float cloneCount;
void main() {
gl_FragColor = texture2D(u_TextureUnit, v_TexCoord * cloneCount);
}
"""
}
override fun onCreated() {
super.onCreated()
GLES20.glUniform1f(getUniform("cloneCount"), 3.0f)
}
}
彻底克隆滤镜
这个滤镜的实现,是利用了纹理采样的环绕方式实现,如效果图中,将采样的范围改成(0, 0)到(3, 3)。
须要补充的是向量的计算方式的知识:
vec2(x, y) * z = vec(x * z, y * z);
4. 部分克隆滤镜
/**
* 部分克隆滤镜
*
* @author Benhero
* @date 2019/1/18
*/
class ClonePartFilter(context: Context) : BaseFilter(context, VERTEX_SHADER,
TextResourceReader.readTextFileFromResource(context, R.raw.filter_test)) {
companion object {
const val FRAGMENT_SHADER = """
precision mediump float;
varying vec2 v_TexCoord;
uniform sampler2D u_TextureUnit;
uniform float isVertical;
uniform float isHorizontal;
uniform float cloneCount;
void main() {
vec4 source = texture2D(u_TextureUnit, v_TexCoord);
float coordX = v_TexCoord.x;
float coordY = v_TexCoord.y;
if (isVertical == 1.0) {
float width = 1.0 / cloneCount;
float startX = (1.0 - width) / 2.0;
coordX = mod(v_TexCoord.x, width) + startX;
}
if (isHorizontal == 1.0) {
float height = 1.0 / cloneCount;
float startY = (1.0 - height) / 2.0;
coordY = mod(v_TexCoord.y, height) + startY;
}
gl_FragColor = texture2D(u_TextureUnit, vec2(coordX, coordY));
}
"""
}
override fun onCreated() {
super.onCreated()
GLES20.glUniform1f(getUniform("isVertical"), 1.0f)
GLES20.glUniform1f(getUniform("isHorizontal"), 1.0f)
GLES20.glUniform1f(getUniform("cloneCount"), 3.0f)
}
}
部分克隆滤镜
这个效果可能你会困惑这个滤镜这么丑,有什么用?嗯,是须要换个素材来解释下会比较好。这时候,须要来个小公举~
Jay
而经过使用部分克隆滤镜,就能够获得两个帅气的小公举。
部分克隆滤镜-Jay
周杰伦看到这个效果,都会说:“哎哟,不错哦!”
回归正题,讲解这个滤镜的逻辑:
需求:当X方向上须要克隆N个图片,那么在原图中心取原图大小的 N 分之一,粘贴复制。
计算:上图克隆 2 份的效果,那么须要在原图中心的裁处中心区域,原图大小为 1920*1440 ,也就是须要裁出 960×1440 的区域。
那么是从哪里开始裁才是中心点呢?应该是 (1920 - 960) / 2 = 480 的位置做为x方向上的起始点。
同理,须要裁 N 份,须要的参数以下:
显示区域大小:DisplayWidth = Width / N;
裁剪的起始点:startX = (Width - DisplayWidth) / 2;
GLSL 分析
在片断着色器中的,关键的代码以下:
float width = 1.0 / cloneCount;
float startX = (1.0 - width) / 2.0;
coordX = mod(v_TexCoord.x, width) + startX;
在上面咱们已经分析过前面两行的计算原理,那么第三行,须要先介绍 mod 这个方法,是取余的做用,在 GLSL 中不可使用 Kotlin、Java 中的百分号 % 来表明取余的意思,须要用 mod 这个方法。
mod(v_TexCoord.x, width) 计算出了当前每一个片断的坐标点,在重复的片断中的坐标,再加上 startX 就能够将原始坐标转换出克隆片断的坐标。
GLSL 日志
讲解到这里,读者应该多多少少写了一些 GLSL 代码了,不过在过程当中可能会遇到一些Bug,不明白怎么哪里就黑屏了,什么东西都没展现,因此这里讲解下如何看日志。
本身打 Log,是不可能的!这个搜索过,直接 Debug 的方式是没有的,由于GLSL 在 GPU 内跑,没有提供打日志的地方,因此,若是想要调试效果,能够经过本身改变画面的内容来验证本身的思路。好比符合某个条件,画面都是某个固定颜色。
若是 GLS 编译不经过,是有日志的:Adreno|GLConsumer,在 Logcat 上加上这个 Tag 就能够看到一些编译信息。
编译错误示范:
2019-01-19 15:07:43.979 20637-20672/com.benhero.glstudio I/Adreno: ERROR: 0:14: '%' : supported in pack/unpack shaders only
ERROR: 0:14: '%' : wrong operand types no operation '%' exists that takes a left-hand operand of type 'float' and a right operand of type 'float' (or there is no acceptable conversion)
ERROR: 2 compilation errors. No code generated.
这个表示了第1 4 行百分号 % 使用错误,致使了编译有问题。嗯,取余仍是用mod吧。
更多GLSL的方法,请看-GLSL 中文手册。
编译正确示范:
2019-01-19 15:21:15.817 21155-21187/? I/Adreno: QUALCOMM build : 8e3df98, Ie4790512f3
Build Date : 04/11/18
OpenGL ES Shader Compiler Version: EV031.22.00.01
Local Branch :
Remote Branch : quic/gfx-adreno.lnx.1.0.r36-rel
Remote Branch : NONE
Reconstruct Branch : NOTHING
GitHub工程
https://github.com/benhero/GLStudio
做者:Benhero
连接:https://www.jianshu.com/p/87ccc9bfa362
-- END --
进技术交流群,扫码添加个人微信:Byte-Flow
获取视频教程和源码
推荐:
FFmpeg + OpenGL ES 实现 3D 全景播放器
FFmpeg + OpenGLES 实现视频解码播放和视频滤镜
以为不错,点个在看呗~

本文分享自微信公众号 - 字节流动(google_developer)。
若有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一块儿分享。