首先,这一系列文章均基于本身的理解和实践,可能有不对的地方,欢迎你们指正。
其次,这是一个入门系列,涉及的知识也仅限于够用,深刻的知识网上也有许许多多的博文供你们学习了。
最后,写文章过程当中,会借鉴参考其余人分享的文章,会在文章最后列出,感谢这些做者的分享。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)