首先,這一系列文章均基於本身的理解和實踐,可能有不對的地方,歡迎你們指正。
其次,這是一個入門系列,涉及的知識也僅限於夠用,深刻的知識網上也有許許多多的博文供你們學習了。
最後,寫文章過程當中,會借鑑參考其餘人分享的文章,會在文章最後列出,感謝這些做者的分享。html
碼字不易,轉載請註明出處!java
教程代碼:【Github傳送門】 |
---|
本文將介紹如何使用
FBO
,FBO
能夠實現什麼效果,以及如何在着色器中使用多個紋理單元。git
先來看看利用FBO
實現的「靈魂出竅」效果:github
上一篇文章,講解了如何使用EGL,而且提到EGL能夠創建一個離屏渲染的緩衝區,這種離屏渲染的方式一般用於模擬整個渲染窗口,好比能夠用於FFmpeg軟編碼,將顯示在虛擬窗口中的畫面編碼成H264。算法
與此同時,OpenGL也提供另一種離屏渲染方式,即FBO。FBO不只能夠實現離屏渲染整個OpenGL窗口,也能夠用於處理碎片畫面,即窗口中的小畫面。緩存
關於EGL的離屏渲染,將會在後面關於FFmpeg的文章中使用到,這裏暫且不論。bash
而在視頻編輯當中,FBO離屏渲染扮演着很重要的角色,許多的視頻濾鏡都會用到,接下來就來看看FBO如何使用吧。app
OpenGL
在渲染到系統窗口以前,都會將數據送到FBO
上,也就是說,FBO
其實一直在默默的爲咱們服務。
因此,OpenGL
在一開始就建立了一個默認的FBO
。框架
FBO:Frame Buffer Object,幀緩存對象。ide
從名字上看,每每很容易讓人誤解這是一個緩存空間,但實際上,FBO很重要的在最後面的Object上。這是一個緩存對象,包含了多個緩衝索引
,分別爲顏色緩衝(Color buffers)
, 深度緩衝(Depth buffer)
, 模板緩衝(Stencil buffer)
。
之因此說是緩衝索引,是由於FBO並不包含這些緩衝數據,僅僅保存了緩衝數據的索引地址。
FBO和這些緩衝區則經過附着點進行鏈接。
能夠看到FBO中包含了:
1. 多個顏色附着點(GL_COLOR_ATTACHMENT0、GL_COLOR_ATTACHMENT1...)
2. 一個深度附着點(GL_DEPTH_ATTACHMENT)
3. 一個模板附着點(GL_STENCIL_ATTACHMENT)
複製代碼
能夠劃分爲兩類:
紋理附着(顏色附着):主要用於將顏色渲染到紋理中。
渲染緩衝對象RBO(Render Buffer Objecgt):主要用於渲染深度信息和模板信息。
在
2D
中,一般只用到了顏色附着,另外兩種附着一般在3D
渲染中使用。
上面說了,FBO可用於離屏渲染,下面就來看看如何經過FBO將畫面渲染到一個「後臺」的紋理中。
這裏的後臺,指不用於顯示到窗口的紋理。
fun createFBOTexture(width: Int, height: Int): IntArray {
// 新建紋理ID
val textures = IntArray(1)
GLES20.glGenTextures(1, textures, 0)
// 綁定紋理ID
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textures[0])
// 根據顏色參數,寬高等信息,爲上面的紋理ID,生成一個2D紋理
GLES20.glTexImage2D(GLES20.GL_TEXTURE_2D, 0, GLES20.GL_RGBA, width, height,
0, GLES20.GL_RGBA, GLES20.GL_UNSIGNED_BYTE, null)
// 設置紋理邊緣參數
GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_NEAREST.toFloat())
GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MAG_FILTER,GLES20.GL_LINEAR.toFloat())
GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_S,GLES20.GL_CLAMP_TO_EDGE.toFloat())
GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_T,GLES20.GL_CLAMP_TO_EDGE.toFloat())
// 解綁紋理ID
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D,0)
return textures
}
複製代碼
生成一個用於FBO的紋理和普通的紋理其實差很少。
首先,生成一個紋理ID,並綁定到OpenGL中。
其次,給這個紋理ID生成對應的紋理。
這裏使用的是
GLES20.glTexImage2D
,在渲染圖片紋理的時候,使用的是GLUtils.texImage2D
。
關於建立紋理的寬高問題,這裏說明一下:
FBO建立的是一個虛擬的窗口,因此,大小是能夠根據本身的需求設置的,能夠比實際系統窗口大。爲了視頻畫面比例正常,能夠把OpenGL的窗口寬高,以及紋理的寬高都設置爲視頻的寬高。所以,OpenGL在渲染的時候,咱們也把無需再經過矩陣變換來矯正比例,直接拉伸就能夠。
最後,設置紋理邊緣參數,而後解綁。
fun createFrameBuffer(): Int {
val fbs = IntArray(1)
GLES20.glGenFramebuffers(1, fbs, 0)
return fbs[0]
}
複製代碼
新建FrameBuffer相似新建紋理ID,最後返回FBO索引
fun bindFBO(fb: Int, textureId: Int) {
GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, fb)
GLES20.glFramebufferTexture2D(GLES20.GL_FRAMEBUFFER, GLES20.GL_COLOR_ATTACHMENT0,
GLES20.GL_TEXTURE_2D, textureId, 0)
}
複製代碼
先綁定上面建立的FBO,接着將FBO和上面建立的紋理經過顏色附着點 GLES20.GL_COLOR_ATTACHMENT0
綁定起來。
fun unbindFBO() {
GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, GLES20.GL_NONE)
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, 0)
}
複製代碼
解綁FBO比較簡單,其實就是將FBO綁定到默認的窗口上。
這裏的
GLES20.GL_NONE
其實就是0
,也就是系統默認的窗口的 FBO 。
fun deleteFBO(frame: IntArray, texture:IntArray) {
//刪除Frame Buffer
GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, GLES20.GL_NONE)
GLES20.glDeleteFramebuffers(1, frame, 0)
//刪除紋理
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, 0)
GLES20.glDeleteTextures(1, texture, 0)
}
複製代碼
以上,其實就是使用FBO的流程了:
除了第4步之外,其餘都是上面的封裝好的方法。
那麼接下就來看看,如何將畫面渲染到FBO鏈接的紋理上。
爲了更好的理解整個渲染的過程,下面經過一個很是經典的濾鏡來演示這個渲染的流程。
這個效果能夠拆分爲3個效果:
進而拆分爲2個組合:
根據靜態圖的靈魂出竅效果,能夠知道,上層的靈魂出竅效果是根據原圖而來的,就是說,靈魂的基礎圖片是不會變化的。
而視頻的每一幀都是在變化的。
因此,爲了使上層的「靈魂」達到比較平滑的放大效果,須要把一幀保持住一段時間,讓這一幀完成完整的放大過程。
這裏就遇到了一個問題:如何保存視頻的某一幀?
FBO
就是解決這個問題的關鍵。
爲了能夠方便的使用FBO相關的方法,咱們將上面的方法都封裝在一個靜態工具中 OpenGLTools
。
object OpenGLTools {
fun createFBOTexture(width: Int, height: Int): IntArray {
// 新建紋理ID
val textures = IntArray(1)
GLES20.glGenTextures(1, textures, 0)
// 綁定紋理ID
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textures[0])
// 根據顏色參數,寬高等信息,爲上面的紋理ID,生成一個2D紋理
GLES20.glTexImage2D(GLES20.GL_TEXTURE_2D, 0, GLES20.GL_RGBA, width, height,
0, GLES20.GL_RGBA, GLES20.GL_UNSIGNED_BYTE, null)
// 設置紋理邊緣參數
GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_NEAREST.toFloat())
GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MAG_FILTER,GLES20.GL_LINEAR.toFloat())
GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_S,GLES20.GL_CLAMP_TO_EDGE.toFloat())
GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_T,GLES20.GL_CLAMP_TO_EDGE.toFloat())
// 解綁紋理ID
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D,0)
return textures
}
fun createFrameBuffer(): Int {
val fbs = IntArray(1)
GLES20.glGenFramebuffers(1, fbs, 0)
return fbs[0]
}
fun bindFBO(fb: Int, textureId: Int) {
GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, fb)
GLES20.glFramebufferTexture2D(GLES20.GL_FRAMEBUFFER, GLES20.GL_COLOR_ATTACHMENT0,
GLES20.GL_TEXTURE_2D, textureId, 0)
}
fun unbindFBO() {
GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, GLES20.GL_NONE)
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, 0)
}
fun deleteFBO(frame: IntArray, texture:IntArray) {
//刪除Frame Buffer
GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, GLES20.GL_NONE)
GLES20.glDeleteFramebuffers(1, frame, 0)
//刪除紋理
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, 0)
GLES20.glDeleteTextures(1, texture, 0)
}
}
複製代碼
SoulVideoDrawer
這裏將以前的VideoDrawer直接複製過來,若是你們閱讀過以前的文章,相信對VideoDrawer應該不會陌生了。因此這裏就再也不貼完整代碼了。詳情請查看以前的文章,或者直接看源碼:VideoDrawer。
其實 SoulVideoDrawer
大部分代碼和 VideoDrawer
一致,這裏查看完整源碼:SoulVideoDrawer。
此次,再也不像以前那樣一次性貼出完整的代碼,一步步來看下如何使用 FBO 。
class SoulVideoDrawer : IDrawer {
// ......
// 省略和VideoDrawer同樣成員變量
// ......
//-------------靈魂出竅相關的變量--------------
/**上下顛倒的頂點矩陣*/
private val mReserveVertexCoors = floatArrayOf(
-1f, 1f,
1f, 1f,
-1f, -1f,
1f, -1f
)
private val mDefVertexCoors = floatArrayOf(
-1f, -1f,
1f, -1f,
-1f, 1f,
1f, 1f
)
// 頂點座標
private var mVertexCoors = mDefVertexCoors
// 靈魂幀緩衝
private var mSoulFrameBuffer: Int = -1
// 靈魂紋理ID
private var mSoulTextureId: Int = -1
// 靈魂紋理接收者
private var mSoulTextureHandler: Int = -1
// 靈魂縮放進度接收者
private var mProgressHandler: Int = -1
// 是否更新FBO紋理
private var mDrawFbo: Int = 1
// 更新FBO標記接收者
private var mDrawFobHandler: Int = -1
// 一幀靈魂的時間
private var mModifyTime: Long = -1
override fun draw() {
if (mTextureId != -1) {
initDefMatrix()
//【步驟1: 建立、編譯並啓動OpenGL着色器】
createGLPrg()
// -------【步驟2:新增FBO部分】-----
//【步驟2.1: 更新靈魂紋理】
updateFBO()
//【步驟2.2: 激活靈魂紋理單元】
activateSoulTexture()
// ---------------------------
//【步驟3: 激活並綁定紋理單元】
activateDefTexture()
//【步驟4: 綁定圖片到紋理單元】
updateTexture()
//【步驟5: 開始渲染繪製】
doDraw()
}
}
// ......
}
複製代碼
增長了和FBO、實現靈魂出竅效果相關的成員變量。
重點關注 draw
方法,有5個步驟,但真正增長的其實就是第2個步驟:
步驟2: 新增FBO部分
- 2.1: 更新靈魂紋理【updateFBO】
- 2.2: 激活靈魂紋理單元【activateSoulTexture】
複製代碼
先來看2.1。
class SoulVideoDrawer : IDrawer {
// ......
private fun updateFBO() {
//【1,建立FBO紋理】
if (mSoulTextureId == -1) {
mSoulTextureId = OpenGLTools.createFBOTexture(mVideoWidth, mVideoHeight)
}
// 【2,建立FBO】
if (mSoulFrameBuffer == -1) {
mSoulFrameBuffer = OpenGLTools.createFrameBuffer()
}
// 【3,渲染到FBO】
if (System.currentTimeMillis() - mModifyTime > 500) {
mModifyTime = System.currentTimeMillis()
// 綁定FBO
OpenGLTools.bindFBO(mSoulFrameBuffer, mSoulTextureId)
// 配置FBO窗口
configFboViewport()
//--------執行正常畫面渲染,畫面將渲染到FBO上--------------
// 激活默認的紋理
activateDefTexture()
// 更新紋理
updateTexture()
// 繪製到FBO
doDraw()
//---------------------------------------------------
// 解綁FBO
OpenGLTools.unbindFBO()
// 恢復默認繪製窗口
configDefViewport()
}
}
/** * 配置FBO窗口 */
private fun configFboViewport() {
mDrawFbo = 1
// 將變換矩陣回覆爲單位矩陣(將畫面拉昇到整個窗口大小,設置窗口比例和FBO紋理比例一致,畫面恰好能夠正常繪製到FBO紋理上)
Matrix.setIdentityM(mMatrix, 0)
// 設置顛倒的頂點座標
mVertexCoors = mReserveVertexCoors
//從新初始化頂點座標
initPos()
GLES20.glViewport(0, 0, mVideoWidth, mVideoHeight)
//設置一個顏色狀態
GLES20.glClearColor(0.0f, 0.0f, 0.0f, 0.0f)
//使能顏色狀態的值來清屏
GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT)
}
/** * 配置默認顯示的窗口 */
private fun configDefViewport() {
mDrawFbo = 0
mMatrix = null
// 恢復頂點座標
mVertexCoors = mDefVertexCoors
initPos()
initDefMatrix()
// 恢復窗口
GLES20.glViewport(0, 0, mWorldWidth, mWorldHeight)
}
private fun activateDefTexture() {
activateTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, mTextureId, 0, mTextureHandler)
}
private fun activateSoulTexture() {
activateTexture(GLES11.GL_TEXTURE_2D, mSoulTextureId, 1, mSoulTextureHandler)
}
private fun activateTexture(type: Int, textureId: Int, index: Int, textureHandler: Int) {
//激活指定紋理單元
GLES20.glActiveTexture(GLES20.GL_TEXTURE0 + index)
//綁定紋理ID到紋理單元
GLES20.glBindTexture(type, textureId)
//將激活的紋理單元傳遞到着色器裏面
GLES20.glUniform1i(textureHandler, index)
//配置邊緣過渡參數
GLES20.glTexParameterf(type, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_LINEAR.toFloat())
GLES20.glTexParameterf(type, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR.toFloat())
GLES20.glTexParameteri(type, GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_CLAMP_TO_EDGE)
GLES20.glTexParameteri(type, GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_CLAMP_TO_EDGE)
}
// ......
}
複製代碼
看 updateFBO
方法,3個步驟:
前面2個步驟,在以前已經介紹過,再也不贅述。
重點看第3步。
這裏讓一幀圖像保持500ms,咱們用一個變量 mModifyTime
來記錄當前這一幀渲染時候的時間,只要過了500ms,就刷新一次畫面。
來看渲染FBO的過程:
if (System.currentTimeMillis() - mModifyTime > 500) {
// 記錄時間
mModifyTime = System.currentTimeMillis()
// 綁定FBO
OpenGLTools.bindFBO(mSoulFrameBuffer, mSoulTextureId)
// 配置FBO窗口
configFboViewport()
//--------執行正常畫面渲染,畫面將渲染到FBO上--------------
// 激活默認的紋理
activateDefTexture()
// 更新紋理
updateTexture()
// 繪製到FBO
doDraw()
//---------------------------------------------------
// 解綁FBO
OpenGLTools.unbindFBO()
// 恢復默認繪製窗口
configDefViewport()
}
複製代碼
i. 綁定FBO
當調用了
OpenGLTools.bindFBO
以後,全部對於OpenGL的操做都將影響到咱們本身建立的FBO。也就是說,在調用OpenGLTools.unbindFBO()
解綁FBO以前,下面全部的操做,都將做用在FBO上。
ii. 從新配置FBO窗口大小
將OpenGL窗口設置爲視頻大小,而且將矩陣變化重置(畫面拉昇到窗口大小),而後清屏。
至於爲何要從新設置窗口大小,前面設置紋理大小的時候已經說過了。
還有一點要注意的是,這裏將紋理座標
mVertexCoors
作了上下顛倒(其實就是恢復爲OpenGL默認的座標),這樣渲染到FBO綁定的紋理上後,在片元着色器裏面才能正常取色。
代碼以下:
private fun configFboViewport() {
mDrawFbo = 1
// 將變換矩陣恢復爲單位矩陣
//(將畫面拉昇到整個窗口大小,
// 設置窗口寬高和FBO紋理寬高一致,
// 畫面恰好能夠正常繪製到FBO綁定的紋理上)
Matrix.setIdentityM(mMatrix, 0)
// 設置顛倒的頂點座標
mVertexCoors = mReserveVertexCoors
//從新初始化頂點座標
initPos()
// 設置窗口大小
GLES20.glViewport(0, 0, mVideoWidth, mVideoHeight)
//設置一個顏色狀態
GLES20.glClearColor(0.0f, 0.0f, 0.0f, 0.0f)
//使能顏色狀態的值來清屏
GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT)
}
複製代碼
iii. 激活和更新視頻原來的紋理
注意,這裏是激活原來的渲染視頻的紋理
iv. 渲染繪製
也就是說,在綁定了FBO之後,按照正常的渲染流程,就能夠將畫面渲染到FBO上了。
v. 解除FBO綁定,將窗口大小、紋理座標、矩陣都恢復回原來的配置。
將渲染從新切換到原來的系統窗口上,畫面將從新顯示到系統窗口上。
經過以上步驟,就將畫面渲染到FBO綁定的紋理 mSoulTextureId
上面了。
前面,咱們將一幀畫面渲染到了 mSoulTextureId
這個紋理上, 接下來就要利用這個紋理,將畫面放大、透明漸變實現靈魂效果。
回到draw方法中,來到2.2步驟。
override fun draw() {
if (mTextureId != -1) {
//【步驟1: 建立、編譯並啓動OpenGL着色器】
// -------【步驟2:新增FBO部分】-----
//【步驟2.1: 更新靈魂紋理】
//【步驟2.2: 激活靈魂紋理單元】
activateSoulTexture()
// ---------------------------
//【步驟3: 激活並綁定紋理單元】
activateDefTexture()
//【步驟4: 綁定圖片到紋理單元】
updateTexture()
//【步驟5: 開始渲染繪製】
doDraw()
}
}
複製代碼
看下激活如何激活「靈魂」的紋理。
private fun activateSoulTexture() {
activateTexture(GLES11.GL_TEXTURE_2D, mSoulTextureId, 1, mSoulTextureHandler)
}
private fun activateTexture(type: Int, textureId: Int, index: Int, textureHandler: Int) {
//激活指定紋理單元
GLES20.glActiveTexture(GLES20.GL_TEXTURE0 + index)
//綁定紋理ID到紋理單元
GLES20.glBindTexture(type, textureId)
//將激活的紋理單元傳遞到着色器裏面
GLES20.glUniform1i(textureHandler, index)
//配置邊緣過渡參數
GLES20.glTexParameterf(type, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_LINEAR.toFloat())
GLES20.glTexParameterf(type, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR.toFloat())
GLES20.glTexParameteri(type, GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_CLAMP_TO_EDGE)
GLES20.glTexParameteri(type, GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_CLAMP_TO_EDGE)
}
複製代碼
和以前文章稍微有點不一樣,之前參數都是直接寫死的。此次改造了一下 activateTexture
將 紋理類型
,紋理ID
, 紋理單元索引
,以及着色器對應的 紋理接收器
,做爲參數傳遞進來。
有2點要注意的:
activateSoulTexture
中,須要注意的是,紋理的類型爲普通紋理類型 GLES11.GL_TEXTURE_2D
, 而非擴展紋理 GLES11Ext.GL_TEXTURE_EXTERNAL_OES
,由於通過以前的渲染之後,畫面已是普通紋理了。GLES20.GL_TEXTURE0
, 「靈魂」的紋理單元爲 GLES20.GL_TEXTURE1 = GLES20.GL_TEXTURE0 + 1
。接着,激活默認的正常畫面紋理 updateTexture()
,這樣就能夠在片元着色器中,同時接收這兩個紋理單元。
private fun activateDefTexture() {
activateTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, mTextureId, 0, mTextureHandler)
}
複製代碼
最後,啓動渲染繪製,進入到着色器中。
前面作了這麼多的鋪墊,其實都是爲了將一幀固定的視頻畫面傳遞到着色器中。真正實現「靈魂出竅」的效果,也是在片元着色器中。
着色器代碼以下:
private fun getVertexShader(): String {
return "attribute vec4 aPosition;" +
"precision mediump float;" +
"uniform mat4 uMatrix;" +
"attribute vec2 aCoordinate;" +
"varying vec2 vCoordinate;" +
"attribute float alpha;" +
"varying float inAlpha;" +
"void main() {" +
" gl_Position = uMatrix*aPosition;" +
" vCoordinate = aCoordinate;" +
" inAlpha = alpha;" +
"}"
}
private fun getFragmentShader(): String {
//必定要加換行"\n",不然會和下一行的precision混在一塊兒,致使編譯出錯
return "#extension GL_OES_EGL_image_external : require\n" +
"precision mediump float;" +
"varying vec2 vCoordinate;" +
"varying float inAlpha;" +
"uniform samplerExternalOES uTexture;" +
"uniform float progress;" +
"uniform int drawFbo;" +
"uniform sampler2D uSoulTexture;" +
"void main() {" +
// 透明度[0,0.4]
"float alpha = 0.6 * (1.0 - progress);" +
// 縮放比例[1.0,1.5]
"float scale = 1.0 + (1.5 - 1.0) * progress;" +
// 放大紋理座標
"float soulX = 0.5 + (vCoordinate.x - 0.5) / scale;\n" +
"float soulY = 0.5 + (vCoordinate.y - 0.5) / scale;\n" +
"vec2 soulTextureCoords = vec2(soulX, soulY);" +
// 獲取對應放大紋理座標下的像素(顏色值rgba)
"vec4 soulMask = texture2D(uSoulTexture, soulTextureCoords);" +
"vec4 color = texture2D(uTexture, vCoordinate);" +
"if (drawFbo == 0) {" +
// 顏色混合 默認顏色混合方程式 = mask * (1.0-alpha) + weakMask * alpha
" gl_FragColor = color * (1.0 - alpha) + soulMask * alpha;" +
"} else {" +
" gl_FragColor = vec4(color.r, color.g, color.b, inAlpha);" +
"}" +
"}"
}
複製代碼
能夠看到,頂點着色器
的代碼和普通的渲染是同樣的。
修改的都在 片元着色器中
。
簡單分析一下:
i. 除了正常畫面渲染須要的參數,另外新增了3個參數:
// 動畫進度
uniform float progress;
// 是否繪製到FBO
uniform int drawFbo;
// 一幀固定的紋理
uniform sampler2D uSoulTexture;
複製代碼
ii. 跳過中間關於「靈魂」動畫的部分,先看最後一個if/else
if (drawFbo == 0) {
// 顏色混合 默認顏色混合方程式 = mask * (1.0-alpha) + weakMask * alpha
gl_FragColor = color * (1.0 - alpha) + soulMask * alpha;" +
} else {
gl_FragColor = vec4(color.r, color.g, color.b, inAlpha);
}
複製代碼
當一幀的時間超過500ms的時候,會從新獲取一幀新的視頻畫面。
這裏經過外部傳進來的標記 drawFbo
若是爲 1
時,渲染普通的畫面,此時因爲已經綁定了FBO,因此這一幀畫面會渲染到FBO的 mSoulTextureID
上。
在下一次渲染的時候,這一幀紋理將傳遞給片元着色器的 uSoulTexture
。
iii. 中間的部分,關於「靈魂出竅」的核心。
// 透明度[0,0.4]
float alpha = 0.6 * (1.0 - progress);
// 縮放比例[1.0,1.5]
float scale = 1.0 + (1.5 - 1.0) * progress;
// 放大紋理座標
float soulX = 0.5 + (vCoordinate.x - 0.5) / scale;
float soulY = 0.5 + (vCoordinate.y - 0.5) / scale;
vec2 soulTextureCoords = vec2(soulX, soulY);
// 獲取對應放大紋理座標下的像素(顏色值rgba)
vec4 soulMask = texture2D(uSoulTexture, soulTextureCoords);
複製代碼
首先,計算透明度。根據外面計算獲得的 progress
,慢慢下降透明度,最大透明度爲0.6。
而後,計算縮放後的座標。隨着 progress
的增長,scale
越大。最大放大1.5倍。利用 scale
分別計算 X,Y 的縮放。能夠看到,scale
越大,soulX/soulY
反而更小。這是由於要達到放大的效果,當前要渲染的點,應該取更小的座標對應的顏色(像素)。
最後,經過 soulX
soulY
,到「靈魂」紋理 uSoulTexture
取到顏色。
iv. 混合底層正常畫面和上層「靈魂」畫面,採用經常使用的混合算法。
gl_FragColor = color * (1.0 - alpha) + soulMask * alpha;
複製代碼
class SoulPlayerActivity: AppCompatActivity() {
val path = Environment.getExternalStorageDirectory().absolutePath + "/mvtest.mp4"
lateinit var drawer: IDrawer
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_opengl_player)
initRender()
}
private fun initRender() {
// 使用「靈魂出竅」渲染器
drawer = SoulVideoDrawer()
drawer.setVideoSize(1920, 1080)
drawer.getSurfaceTexture {
initPlayer(Surface(it))
}
gl_surface.setEGLContextClientVersion(2)
val render = SimpleRender()
render.addDrawer(drawer)
gl_surface.setRenderer(render)
}
private fun initPlayer(sf: Surface) {
val threadPool = Executors.newFixedThreadPool(10)
val videoDecoder = VideoDecoder(path, null, sf)
threadPool.execute(videoDecoder)
val audioDecoder = AudioDecoder(path)
threadPool.execute(audioDecoder)
videoDecoder.goOn()
audioDecoder.goOn()
}
}
複製代碼
使用和普通的使用OpenGL渲染器如出一轍,不同的只是把 VideoDrawer
換成 SoulVideoDrawer
。
最終獲得了文章開頭的效果:
以上就是整個使用FBO的過程,使用也很是的簡單。固然了,只關注了顏色附着的部分,另外的深度附着和模板附着有興趣的能夠自行探索學習。
能夠看到,FBO爲咱們提供了一個實現視頻處理的好方法,許多酷炫的效果得以實現,更多有趣的效果,等着你們去實現。
幀緩衝區對象(FBO) 實現渲染到紋理(Render To Texture/RTT)