课程介绍
本节介绍滤镜基础框架+基础颜色滤镜。
基础框架
这节课我们开始讲滤镜的开发,为了便于展示各种滤镜的效果,设计了一套简易的框架,分两部分。
1. 滤镜的基类
主要的生命周期方法如下:
- onCreated:创建的时候
- onSizeChanged:滤镜尺寸改变
- onDraw:绘制每一帧
- onDestroy:销毁,用于回收无用资源
而实现基础滤镜的时候,只需要复写基类的构造方法即可。使用如下:
/**
* 基础滤镜
*
* @author Benhero
* @date 2018/11/28
*/
open class BaseFilter(val context: Context, val vertexShader: String = VERTEX_SHADER, val fragmentShader: String = FRAGMENT_SHADER) {
companion object {
val VERTEX_SHADER = """
uniform mat4 u_Matrix;
attribute vec4 a_Position;
attribute vec2 a_TexCoord;
varying vec2 v_TexCoord;
void main() {
v_TexCoord = a_TexCoord;
gl_Position = u_Matrix * a_Position;
}
"""
val FRAGMENT_SHADER = """
precision mediump float;
varying vec2 v_TexCoord;
uniform sampler2D u_TextureUnit;
void main() {
gl_FragColor = texture2D(u_TextureUnit, v_TexCoord);
}
"""
private val POSITION_COMPONENT_COUNT = 2
private val POINT_DATA = floatArrayOf(-1.0f, -1.0f, -1.0f, 1.0f, 1.0f, 1.0f, 1.0f, -1.0f)
/**
* 纹理坐标
*/
private val TEX_VERTEX = floatArrayOf(0f, 1f, 0f, 0f, 1f, 0f, 1f, 1f)
/**
* 纹理坐标中每个点占的向量个数
*/
private val TEX_VERTEX_COMPONENT_COUNT = 2
}
private val mVertexData: FloatBuffer
private var uTextureUnitLocation: Int = 0
private val mTexVertexBuffer: FloatBuffer
/**
* 纹理数据
*/
var textureBean: TextureHelper.TextureBean? = null
private var projectionMatrixHelper: ProjectionMatrixHelper? = null
var program = 0
init {
mVertexData = BufferUtil.createFloatBuffer(POINT_DATA)
mTexVertexBuffer = BufferUtil.createFloatBuffer(TEX_VERTEX)
}
public open fun onCreated() {
makeProgram(vertexShader, fragmentShader)
val aPositionLocation = getAttrib("a_Position")
projectionMatrixHelper = ProjectionMatrixHelper(program, "u_Matrix")
// 纹理坐标索引
val aTexCoordLocation = getAttrib("a_TexCoord")
uTextureUnitLocation = getUniform("u_TextureUnit")
mVertexData.position(0)
GLES20.glVertexAttribPointer(aPositionLocation, POSITION_COMPONENT_COUNT,
GLES20.GL_FLOAT, false, 0, mVertexData)
GLES20.glEnableVertexAttribArray(aPositionLocation)
// 加载纹理坐标
mTexVertexBuffer.position(0)
GLES20.glVertexAttribPointer(aTexCoordLocation, TEX_VERTEX_COMPONENT_COUNT, GLES20.GL_FLOAT, false, 0, mTexVertexBuffer)
GLES20.glEnableVertexAttribArray(aTexCoordLocation)
GLES20.glClearColor(0f, 0f, 0f, 1f)
// 开启纹理透明混合,这样才能绘制透明图片
GLES20.glEnable(GL10.GL_BLEND)
GLES20.glBlendFunc(GL10.GL_ONE, GL10.GL_ONE_MINUS_SRC_ALPHA)
}
public open fun onSizeChanged(width: Int, height: Int) {
GLES20.glViewport(0, 0, width, height)
projectionMatrixHelper!!.enable(width, height)
}
public open fun onDraw() {
GLES20.glClear(GL10.GL_COLOR_BUFFER_BIT)
// 纹理单元:在OpenGL中,纹理不是直接绘制到片段着色器上,而是通过纹理单元去保存纹理
// 设置当前活动的纹理单元为纹理单元0
GLES20.glActiveTexture(GLES20.GL_TEXTURE0)
// 将纹理ID绑定到当前活动的纹理单元上
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textureBean?.textureId ?: 0)
// 将纹理单元传递片段着色器的u_TextureUnit
GLES20.glUniform1i(uTextureUnitLocation, 0)
GLES20.glDrawArrays(GLES20.GL_TRIANGLE_FAN, 0, POINT_DATA.size / POSITION_COMPONENT_COUNT)
}
public open fun onDestroy() {
GLES20.glDeleteProgram(program)
program = 0
}
/**
* 创建OpenGL程序对象
*
* @param vertexShader 顶点着色器代码
* @param fragmentShader 片段着色器代码
*/
protected fun makeProgram(vertexShader: String, fragmentShader: String) {
// 步骤1:编译顶点着色器
val vertexShaderId = ShaderHelper.compileVertexShader(vertexShader)
// 步骤2:编译片段着色器
val fragmentShaderId = ShaderHelper.compileFragmentShader(fragmentShader)
// 步骤3:将顶点着色器、片段着色器进行链接,组装成一个OpenGL程序
program = ShaderHelper.linkProgram(vertexShaderId, fragmentShaderId)
if (LoggerConfig.ON) {
ShaderHelper.validateProgram(program)
}
// 步骤4:通知OpenGL开始使用该程序
GLES20.glUseProgram(program)
}
protected fun getUniform(name: String): Int {
return GLES20.glGetUniformLocation(program, name)
}
protected fun getAttrib(name: String): Int {
return GLES20.glGetAttribLocation(program, name)
}
}
2. 滤镜加载
/**
* 滤镜渲染
*
* @author Benhero
*/
class L8_1_FilterRenderer(context: Context) : BaseRenderer(context) {
val filterList = ArrayList()
var drawIndex = 0
var isChanged = false
var currentFilter: BaseFilter
var textureBean: TextureHelper.TextureBean? = null
init {
filterList.add(BaseFilter(context))
filterList.add(GrayFilter(context))
filterList.add(InverseFilter(context))
filterList.add(LightUpFilter(context))
currentFilter = filterList.get(0)
}
override fun onSurfaceCreated(glUnused: GL10, config: EGLConfig) {
currentFilter.onCreated()
textureBean = TextureHelper.loadTexture(context, R.drawable.pikachu)
}
override fun onSurfaceChanged(gl: GL10?, width: Int, height: Int) {
super.onSurfaceChanged(gl, width, height)
currentFilter.onSizeChanged(width, height)
currentFilter.textureBean = textureBean
}
override fun onDrawFrame(glUnused: GL10) {
if (isChanged) {
currentFilter = filterList.get(drawIndex)
filterList.forEach {
if (it != currentFilter) {
it.onDestroy()
}
}
currentFilter.onCreated()
currentFilter.onSizeChanged(outputWidth, outputHeight)
currentFilter.textureBean = textureBean
isChanged = false
}
currentFilter.onDraw()
}
override fun onClick() {
super.onClick()
drawIndex++
drawIndex = if (drawIndex >= filterList.size) 0 else drawIndex
isChanged = true
}
}
滤镜入门
完成了滤镜框架后,我们就可以开始真正地编写滤镜功能。本节课程的滤镜,基本都是集中在Fragment Shader上,而涉及到Vertex Shader的滤镜不在本次课程上讲解(其实也不难入门)。
1. 反色滤镜
/**
* 反色滤镜
*
* @author Benhero
* @date 2018/11/28
*/
class InverseFilter(context: Context) : BaseFilter(context, VERTEX_SHADER, INVERSE_FRAGMENT_SHADER) {
companion object {
val INVERSE_FRAGMENT_SHADER = """
precision mediump float;
varying vec2 v_TexCoord;
uniform sampler2D u_TextureUnit;
void main() {
vec4 src = texture2D(u_TextureUnit, v_TexCoord);
gl_FragColor = vec4(1.0 - src.r, 1.0 - src.g, 1.0 - src.b, 1.0);
}
"""
}
}
反色滤镜算是最简单的滤镜了,所以这边拿它来开讲,那么接下来会从最基础的内容开始讲起,可能会之前有重复。
- 滤镜实现思路:RGB三个通道的颜色都取反,而alpha通道不变。
- precision mediump float; 这行非常重要,它声明了接下来所有浮点型类型的默认精度(某些变量、常亮需要其他精度可以单独指定),若不声明,在有部分手机上会有黑屏、崩溃等莫名其妙的问题。
- 在GLSL中,float类型可以不带f结尾,但是不能不带点,正确的格式如1.0和1. 。
2. 灰色滤镜
/**
* 灰色滤镜
*
* @author Benhero
* @date 2018/11/28
*/
class GrayFilter(context: Context) : BaseFilter(context, VERTEX_SHADER, GRAY_FRAGMENT_SHADER) {
companion object {
val GRAY_FRAGMENT_SHADER = """
precision mediump float;
varying vec2 v_TexCoord;
uniform sampler2D u_TextureUnit;
void main() {
vec4 src = texture2D(u_TextureUnit, v_TexCoord);
float gray = (src.r + src.g + src.b) / 3.0;
gl_FragColor =vec4(gray, gray, gray, 1.0);
}
"""
}
}
- 滤镜实现思路:让RGB三个通道的颜色取均值
3. 发光滤镜
/**
* 发光滤镜
*
* @author Benhero
* @date 2018/11/28
*/
class LightUpFilter(context: Context) : BaseFilter(context, VERTEX_SHADER, INVERSE_FRAGMENT_SHADER) {
companion object {
val INVERSE_FRAGMENT_SHADER = """
precision mediump float;
varying vec2 v_TexCoord;
uniform sampler2D u_TextureUnit;
uniform float uTime;
void main() {
float lightUpValue = abs(sin(uTime / 1000.0)) / 4.0;
vec4 src = texture2D(u_TextureUnit, v_TexCoord);
vec4 addColor = vec4(lightUpValue, lightUpValue, lightUpValue, 1.0);
gl_FragColor = src + addColor;
}
"""
}
private var uTime: Int = 0
private var startTime: Long = 0
override public fun onCreated() {
super.onCreated()
startTime = System.currentTimeMillis()
uTime = getUniform("uTime")
}
override public fun onDraw() {
super.onDraw()
GLES20.glUniform1f(uTime, (System.currentTimeMillis() - startTime).toFloat())
}
}
这个滤镜时本节最难的滤镜,而且是一步引入了2个新的内容:新参数的传递方式、周期变化滤镜的实现。
- 新参数的传递方式:这个滤镜传入的是一个滤镜执行时间 的参数,需要实时更新,所以在onCreated的时候创建引用,在onDraw的时候不停地去更新参数值。
- 周期变换:要实现周期变换,这里使用的是sin正弦函数,y值会随着x的变换做周期变换,具体效果大家懂的。这里除以1000.0让x是个位数的变换,abs是为了让滤镜是变亮,而没有变暗的效果,除以4是为了减弱变亮的幅度,让增加的亮度值控制在0到0.25之间。
- 向量的计算:除了拆解成每个矢量上的相加减之外,还可以直接两个向量相加减 gl_FragColor = src + addColor;
性能优化
这个滤镜案例有个比较大的问题,就是性能相对较差,有优化空间。我们知道,每个Fragment Shader在每一帧执行次数是分解出来的片元数,那么也就是说,一帧会执行成千上万次。所以,我们应该避免将有些没必要的计算放在Fragment Shader里,而是放在CPU里一次计算好,再传进去,这样可以大大减少没必要的消耗。所以优化的代码如下:
/**
* 发光滤镜
*
* @author Benhero
* @date 2018/11/28
*/
class LightUpFilter(context: Context) : BaseFilter(context, VERTEX_SHADER, INVERSE_FRAGMENT_SHADER) {
companion object {
val INVERSE_FRAGMENT_SHADER = """
precision mediump float;
varying vec2 v_TexCoord;
uniform sampler2D u_TextureUnit;
uniform float intensity;
void main() {
vec4 src = texture2D(u_TextureUnit, v_TexCoord);
vec4 addColor = vec4(intensity, intensity, intensity, 1.0);
gl_FragColor = src + addColor;
}
"""
}
private var intensityLocation: Int = 0
private var startTime: Long = 0
override public fun onCreated() {
super.onCreated()
startTime = System.currentTimeMillis()
intensityLocation = getUniform("intensity")
}
override public fun onDraw() {
super.onDraw()
val intensity = Math.abs(Math.sin((System.currentTimeMillis() - startTime) / 1000.0)) / 4.0
GLES20.glUniform1f(intensityLocation, intensity.toFloat())
}
}
注意点
- gl_FragColor赋值的时候,一定要对alpha通道进行赋值,否则在一些机型上出现问题,默认设置1.0即可。(在Google Pixel上合成视频时,某个滤镜没有设置alpha通道,导致滤镜不生效)
- 若向GLSL中传递的值,但在方法内没有用到,则会报bindTextureImage: clearing GL error: 0x501
建议
- 推荐大家使用Kotlin来编写滤镜功能,因为使用本节课中三个引号的String拼接方式,会比Java中两个引号的方式方便太多了,不然每次换行都要写引号、加号,特别麻烦,容易出错。另外,还可以先把滤镜放在GLSL文件格式下,Android Studio有特定的编辑器渲染,更好看。
拓展资料
- OpenGL shader性能优化策略
参考
见Android OpenGL ES学习资料所列举的博客、资料。
GitHub代码工程
本系列课程所有相关代码请参考我的GitHub项目GLStudio。
课程目录
本系列课程目录详见 - Android OpenGL ES教程规划