課程介紹
本節介紹濾鏡基礎框架+基礎顏色濾鏡。
課程效果.gif
基礎框架
這節課我們開始講濾鏡的開發,為了便于展示各種濾鏡的效果,設計了一套簡易的框架,分兩部分。
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<BaseFilter>()
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有特定的編輯器渲染,更好看。
拓展資料
參考
見Android OpenGL ES學習資料所列舉的博客、資料。
GitHub代碼工程
本系列課程所有相關代碼請參考我的GitHub項目GLStudio。
課程目錄
本系列課程目錄詳見 簡書 - Android OpenGL ES教程規劃