Android OpenGL ES 9.2 位置滤镜
课程介绍
本节介绍如何改变改变片元着色器内的坐标位置参数,从而让渲染的内容动起来或者达到一些特殊的效果。
位置滤镜效果实现讲解
本节课的核心原理是修改采样的纹理坐标。
这是之前课程中的纹理坐标图,纹理默认传入的读取范围是(0,0)到(1,1)的范围内读取颜色值。
如果对读取的位置进行调整修改,那么就可以做出各种各样的效果。比如缩放动画,让读取的范围改成(-1, -1)到(2, 2)。
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);
}
另外一个是限定纹理的采样范围:
if (offsetTexCoord.x >= 0.0 && offsetTexCoord.x <= 1.0 &&
offsetTexCoord.y >= 0.0 && offsetTexCoord.y <= 1.0) {}
这样子就可以控制超过了(0, 0)到(1, 1)范围的就不绘制。否则位移滤镜就会出现下面的效果:
位移动画-无限制之所以会这样子的效果,是因为纹理采样的环绕方式问题。这里补充下《纹理绘制》章节没有讲解到的这个知识点。
纹理环绕方式 - 图源自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内跑,没有提供打日志的地方,所以,如果想要调试效果,可以通过自己改变画面的内容来验证自己的思路。比如符合某个条件,画面都是某个固定颜色。
- 如果GLSL编译不通过,是有日志的: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.
这个表示了第14行百分号%使用错误,导致了编译有问题。嗯,取余还是用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
其他
- 本系列课程目录详见 简书 - Android OpenGL ES教程规划
- 参考资料见Android OpenGL ES学习资料所列举的博客、资料。
⭐GitHub工程⭐
本系列课程所有相关代码请参考我的GitHub项目⭐GLStudio⭐,喜欢的请给个小星星。😃