【聲 明】
首先涨冀,這一系列文章均基于自己的理解和實踐,可能有不對的地方麦萤,歡迎大家指正鹿鳖。
其次,這是一個入門系列频鉴,涉及的知識也僅限于夠用栓辜,深入的知識網(wǎng)上也有許許多多的博文供大家學習了。
最后垛孔,寫文章過程中藕甩,會借鑒參考其他人分享的文章,會在文章最后列出,感謝這些作者的分享狭莱。
碼字不易僵娃,轉載請注明出處!
教程代碼:【Github傳送門】 |
---|
目錄
一腋妙、Android音視頻硬解碼篇:
二、使用OpenGL渲染視頻畫面篇
- 1济竹,初步了解OpenGL ES
- 2痕檬,使用OpenGL渲染視頻畫面
- 3,OpenGL渲染多視頻送浊,實現(xiàn)畫中畫
- 4梦谜,深入了解OpenGL之EGL
- 5,OpenGL FBO數(shù)據(jù)緩沖區(qū)
- 6袭景,Android音視頻硬編碼:生成一個MP4
三唁桩、Android FFmpeg音視頻解碼篇
- 1,F(xiàn)Fmpeg so庫編譯
- 2耸棒,Android 引入FFmpeg
- 3荒澡,Android FFmpeg視頻解碼播放
- 4,Android FFmpeg+OpenSL ES音頻解碼播放
- 5榆纽,Android FFmpeg+OpenGL ES播放視頻
- 6仰猖,Android FFmpeg簡單合成MP4:視屏解封與重新封裝
- 7捏肢,Android FFmpeg視頻編碼
本文你可以了解到
渲染多視頻畫面奈籽,是實現(xiàn)音視頻編輯的基礎,本文將介紹如何將多個視頻畫面渲染到OpenGL中鸵赫,以及如何對畫面進行混合衣屏、縮放、移動等辩棒。
寫在前面
距離上次更新已經(jīng)有兩個星期狼忱,由于這段時間事情比較多,還請各位關注本系列文章的小伙伴見諒一睁,一有時間我會加緊碼字钻弄,感謝大家的關注和督促。
下面就來看看如何在OpenGL中渲染多視頻畫面者吁。
一窘俺、渲染多畫面
在上篇文章中,詳細的講解了如何通過OpenGL渲染視頻畫面复凳,以及對視頻畫面進行比例矯正瘤泪,基于前面系列文章中封裝好的工具灶泵,可以非常容易地實現(xiàn)在OpenGL中渲染多個視頻畫面。
上文的OpenGL Render非常簡單如下:
class SimpleRender(private val mDrawer: IDrawer): GLSurfaceView.Renderer {
override fun onSurfaceCreated(gl: GL10?, config: EGLConfig?) {
GLES20.glClearColor(0f, 0f, 0f, 0f)
GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT)
mDrawer.setTextureID(OpenGLTools.createTextureIds(1)[0])
}
override fun onSurfaceChanged(gl: GL10?, width: Int, height: Int) {
GLES20.glViewport(0, 0, width, height)
mDrawer.setWorldSize(width, height)
}
override fun onDrawFrame(gl: GL10?) {
mDrawer.draw()
}
}
只支持一個Drawer对途,這里改造一下赦邻,把Drawer修改為列表,以支持多個繪制器实檀。
class SimpleRender: GLSurfaceView.Renderer {
private val drawers = mutableListOf<IDrawer>()
override fun onSurfaceCreated(gl: GL10?, config: EGLConfig?) {
GLES20.glClearColor(0f, 0f, 0f, 0f)
val textureIds = OpenGLTools.createTextureIds(drawers.size)
for ((idx, drawer) in drawers.withIndex()) {
drawer.setTextureID(textureIds[idx])
}
}
override fun onSurfaceChanged(gl: GL10?, width: Int, height: Int) {
GLES20.glViewport(0, 0, width, height)
for (drawer in drawers) {
drawer.setWorldSize(width, height)
}
}
override fun onDrawFrame(gl: GL10?) {
GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT or GLES20.GL_DEPTH_BUFFER_BIT)
drawers.forEach {
it.draw()
}
}
fun addDrawer(drawer: IDrawer) {
drawers.add(drawer)
}
}
同樣非常簡單惶洲,
- 增加一個addDrawer方法,用來添加多個繪制器膳犹。
- 在onSurfaceCreated中為每個繪制器設置一個紋理ID湃鹊。
- 在onSurfaceChanged中為每個繪制器設置顯示區(qū)域寬高。
- 在onDrawFrame中镣奋,遍歷所有繪制器币呵,啟動繪制。
接著侨颈,新建一個新頁面余赢,生成多個解碼器和繪制器。
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent" xmlns:app="http://schemas.android.com/apk/res-auto">
<android.opengl.GLSurfaceView
android:id="@+id/gl_surface"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
</android.support.constraint.ConstraintLayout>
class MultiOpenGLPlayerActivity: AppCompatActivity() {
private val path = Environment.getExternalStorageDirectory().absolutePath + "/mvtest.mp4"
private val path2 = Environment.getExternalStorageDirectory().absolutePath + "/mvtest_2.mp4"
private val render = SimpleRender()
private val threadPool = Executors.newFixedThreadPool(10)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_opengl_player)
initFirstVideo()
initSecondVideo()
initRender()
}
private fun initFirstVideo() {
val drawer = VideoDrawer()
drawer.setVideoSize(1920, 1080)
drawer.getSurfaceTexture {
initPlayer(path, Surface(it), true)
}
render.addDrawer(drawer)
}
private fun initSecondVideo() {
val drawer = VideoDrawer()
drawer.setVideoSize(1920, 1080)
drawer.getSurfaceTexture {
initPlayer(path2, Surface(it), false)
}
render.addDrawer(drawer)
}
private fun initPlayer(path: String, sf: Surface, withSound: Boolean) {
val videoDecoder = VideoDecoder(path, null, sf)
threadPool.execute(videoDecoder)
videoDecoder.goOn()
if (withSound) {
val audioDecoder = AudioDecoder(path)
threadPool.execute(audioDecoder)
audioDecoder.goOn()
}
}
private fun initRender() {
gl_surface.setEGLContextClientVersion(2)
gl_surface.setRenderer(render)
}
}
代碼比較簡單哈垢,通過之前封裝好的解碼工具和繪制工具妻柒,添加了兩個視頻畫面的渲染。
當然了耘分,你可以添加更多的畫面到OpenGL中渲染举塔。
并且,你應該發(fā)現(xiàn)了求泰,渲染多個視頻央渣,其實就是生成多個紋理ID,利用這個ID生成一個Surface渲染表面渴频,最后把這個Surface給到解碼器MediaCodec渲染即可芽丹。
由于我這里使用的兩個視頻都是1920*1080的寬高,所以會發(fā)現(xiàn)卜朗,兩個視頻只顯示了一個拔第,因為重疊在一起了。
兩個畫面如下:
二场钉、嘗一下視頻編輯的味道
現(xiàn)在蚊俺,兩個視頻疊加在一起,看不到底下的視頻逛万,那么泳猬,我們來改變一下上面這個視頻的alpha值,讓它變成半透明,不就可以看到下面的視頻了嗎暂殖?
1)實現(xiàn)半透
首先价匠,為了統(tǒng)一,在IDrawer中新加一個接口:
interface IDrawer {
fun setVideoSize(videoW: Int, videoH: Int)
fun setWorldSize(worldW: Int, worldH: Int)
fun draw()
fun setTextureID(id: Int)
fun getSurfaceTexture(cb: (st: SurfaceTexture)->Unit) {}
fun release()
//新增調節(jié)alpha接口
fun setAlpha(alpha: Float)
}
在VideoDrawer中呛每,保存該值踩窖。
為了方便查看,這里將整個VideoDrawer都貼出來(不想看的可跳過看下面增加的部分):
class VideoDrawer : IDrawer {
// 頂點坐標
private val mVertexCoors = floatArrayOf(
-1f, -1f,
1f, -1f,
-1f, 1f,
1f, 1f
)
// 紋理坐標
private val mTextureCoors = floatArrayOf(
0f, 1f,
1f, 1f,
0f, 0f,
1f, 0f
)
private var mWorldWidth: Int = -1
private var mWorldHeight: Int = -1
private var mVideoWidth: Int = -1
private var mVideoHeight: Int = -1
private var mTextureId: Int = -1
private var mSurfaceTexture: SurfaceTexture? = null
private var mSftCb: ((SurfaceTexture) -> Unit)? = null
//OpenGL程序ID
private var mProgram: Int = -1
//矩陣變換接收者
private var mVertexMatrixHandler: Int = -1
// 頂點坐標接收者
private var mVertexPosHandler: Int = -1
// 紋理坐標接收者
private var mTexturePosHandler: Int = -1
// 紋理接收者
private var mTextureHandler: Int = -1
// 半透值接收者
private var mAlphaHandler: Int = -1
private lateinit var mVertexBuffer: FloatBuffer
private lateinit var mTextureBuffer: FloatBuffer
private var mMatrix: FloatArray? = null
private var mAlpha = 1f
init {
//【步驟1: 初始化頂點坐標】
initPos()
}
private fun initPos() {
val bb = ByteBuffer.allocateDirect(mVertexCoors.size * 4)
bb.order(ByteOrder.nativeOrder())
//將坐標數(shù)據(jù)轉換為FloatBuffer晨横,用以傳入給OpenGL ES程序
mVertexBuffer = bb.asFloatBuffer()
mVertexBuffer.put(mVertexCoors)
mVertexBuffer.position(0)
val cc = ByteBuffer.allocateDirect(mTextureCoors.size * 4)
cc.order(ByteOrder.nativeOrder())
mTextureBuffer = cc.asFloatBuffer()
mTextureBuffer.put(mTextureCoors)
mTextureBuffer.position(0)
}
private fun initDefMatrix() {
if (mMatrix != null) return
if (mVideoWidth != -1 && mVideoHeight != -1 &&
mWorldWidth != -1 && mWorldHeight != -1) {
mMatrix = FloatArray(16)
var prjMatrix = FloatArray(16)
val originRatio = mVideoWidth / mVideoHeight.toFloat()
val worldRatio = mWorldWidth / mWorldHeight.toFloat()
if (mWorldWidth > mWorldHeight) {
if (originRatio > worldRatio) {
val actualRatio = originRatio / worldRatio
Matrix.orthoM(
prjMatrix, 0,
-actualRatio, actualRatio,
-1f, 1f,
3f, 5f
)
} else {// 原始比例小于窗口比例洋腮,縮放寬度會導致寬度度超出,因此手形,寬度以窗口為準啥供,縮放高度
val actualRatio = worldRatio / originRatio
Matrix.orthoM(
prjMatrix, 0,
-1f, 1f,
-actualRatio, actualRatio,
3f, 5f
)
}
} else {
if (originRatio > worldRatio) {
val actualRatio = originRatio / worldRatio
Matrix.orthoM(
prjMatrix, 0,
-1f, 1f,
-actualRatio, actualRatio,
3f, 5f
)
} else {// 原始比例小于窗口比例,縮放高度會導致高度超出库糠,因此伙狐,高度以窗口為準,縮放寬度
val actualRatio = worldRatio / originRatio
Matrix.orthoM(
prjMatrix, 0,
-actualRatio, actualRatio,
-1f, 1f,
3f, 5f
)
}
}
//設置相機位置
val viewMatrix = FloatArray(16)
Matrix.setLookAtM(
viewMatrix, 0,
0f, 0f, 5.0f,
0f, 0f, 0f,
0f, 1.0f, 0f
)
//計算變換矩陣
Matrix.multiplyMM(mMatrix, 0, prjMatrix, 0, viewMatrix, 0)
}
}
override fun setVideoSize(videoW: Int, videoH: Int) {
mVideoWidth = videoW
mVideoHeight = videoH
}
override fun setWorldSize(worldW: Int, worldH: Int) {
mWorldWidth = worldW
mWorldHeight = worldH
}
override fun setAlpha(alpha: Float) {
mAlpha = alpha
}
override fun setTextureID(id: Int) {
mTextureId = id
mSurfaceTexture = SurfaceTexture(id)
mSftCb?.invoke(mSurfaceTexture!!)
}
override fun getSurfaceTexture(cb: (st: SurfaceTexture) -> Unit) {
mSftCb = cb
}
override fun draw() {
if (mTextureId != -1) {
initDefMatrix()
//【步驟2: 創(chuàng)建瞬欧、編譯并啟動OpenGL著色器】
createGLPrg()
//【步驟3: 激活并綁定紋理單元】
activateTexture()
//【步驟4: 綁定圖片到紋理單元】
updateTexture()
//【步驟5: 開始渲染繪制】
doDraw()
}
}
private fun createGLPrg() {
if (mProgram == -1) {
val vertexShader = loadShader(GLES20.GL_VERTEX_SHADER, getVertexShader())
val fragmentShader = loadShader(GLES20.GL_FRAGMENT_SHADER, getFragmentShader())
//創(chuàng)建OpenGL ES程序贷屎,注意:需要在OpenGL渲染線程中創(chuàng)建,否則無法渲染
mProgram = GLES20.glCreateProgram()
//將頂點著色器加入到程序
GLES20.glAttachShader(mProgram, vertexShader)
//將片元著色器加入到程序中
GLES20.glAttachShader(mProgram, fragmentShader)
//連接到著色器程序
GLES20.glLinkProgram(mProgram)
mVertexMatrixHandler = GLES20.glGetUniformLocation(mProgram, "uMatrix")
mVertexPosHandler = GLES20.glGetAttribLocation(mProgram, "aPosition")
mTextureHandler = GLES20.glGetUniformLocation(mProgram, "uTexture")
mTexturePosHandler = GLES20.glGetAttribLocation(mProgram, "aCoordinate")
mAlphaHandler = GLES20.glGetAttribLocation(mProgram, "alpha")
}
//使用OpenGL程序
GLES20.glUseProgram(mProgram)
}
private fun activateTexture() {
//激活指定紋理單元
GLES20.glActiveTexture(GLES20.GL_TEXTURE0)
//綁定紋理ID到紋理單元
GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, mTextureId)
//將激活的紋理單元傳遞到著色器里面
GLES20.glUniform1i(mTextureHandler, 0)
//配置邊緣過渡參數(shù)
GLES20.glTexParameterf(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_LINEAR.toFloat())
GLES20.glTexParameterf(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR.toFloat())
GLES20.glTexParameteri(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_CLAMP_TO_EDGE)
GLES20.glTexParameteri(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_CLAMP_TO_EDGE)
}
private fun updateTexture() {
mSurfaceTexture?.updateTexImage()
}
private fun doDraw() {
//啟用頂點的句柄
GLES20.glEnableVertexAttribArray(mVertexPosHandler)
GLES20.glEnableVertexAttribArray(mTexturePosHandler)
GLES20.glUniformMatrix4fv(mVertexMatrixHandler, 1, false, mMatrix, 0)
//設置著色器參數(shù)艘虎, 第二個參數(shù)表示一個頂點包含的數(shù)據(jù)數(shù)量唉侄,這里為xy,所以為2
GLES20.glVertexAttribPointer(mVertexPosHandler, 2, GLES20.GL_FLOAT, false, 0, mVertexBuffer)
GLES20.glVertexAttribPointer(mTexturePosHandler, 2, GLES20.GL_FLOAT, false, 0, mTextureBuffer)
GLES20.glVertexAttrib1f(mAlphaHandler, mAlpha)
//開始繪制
GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4)
}
override fun release() {
GLES20.glDisableVertexAttribArray(mVertexPosHandler)
GLES20.glDisableVertexAttribArray(mTexturePosHandler)
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, 0)
GLES20.glDeleteTextures(1, intArrayOf(mTextureId), 0)
GLES20.glDeleteProgram(mProgram)
}
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;" +
"void main() {" +
" vec4 color = texture2D(uTexture, vCoordinate);" +
" gl_FragColor = vec4(color.r, color.g, color.b, inAlpha);" +
"}"
}
private fun loadShader(type: Int, shaderCode: String): Int {
//根據(jù)type創(chuàng)建頂點著色器或者片元著色器
val shader = GLES20.glCreateShader(type)
//將資源加入到著色器中,并編譯
GLES20.glShaderSource(shader, shaderCode)
GLES20.glCompileShader(shader)
return shader
}
}
實際上候生,相比較之前的繪制器同眯,改變的地方很少:
class VideoDrawer : IDrawer {
// 省略無關代碼......
// 半透值接收者
private var mAlphaHandler: Int = -1
// 半透明值
private var mAlpha = 1f
override fun setAlpha(alpha: Float) {
mAlpha = alpha
}
private fun createGLPrg() {
if (mProgram == -1) {
// 省略無關代碼......
mAlphaHandler = GLES20.glGetAttribLocation(mProgram, "alpha")
//......
}
//使用OpenGL程序
GLES20.glUseProgram(mProgram)
}
private fun doDraw() {
// 省略無關代碼......
GLES20.glVertexAttrib1f(mAlphaHandler, mAlpha)
//......
}
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;" +
"void main() {" +
" vec4 color = texture2D(uTexture, vCoordinate);" +
" gl_FragColor = vec4(color.r, color.g, color.b, inAlpha);" +
"}"
}
}
重點關注兩個著色器的代碼:
在頂點著色器中嗽测,傳入了一個alpha變量绪励,該值由java代碼傳入肿孵,然后頂點著色器將該值賦值給了inAlpha,最后給到了片元著色器疏魏。
簡單講一下如何傳遞參數(shù)到片元著色器停做。
要把Java中的值傳遞到片元著色器中,直接傳值是不行的大莫,需要通過頂點著色器蛉腌,間接傳遞。
頂點著色器輸入與輸出
- 輸入
build-in變量,此類變量為opengl內建參數(shù)烙丛,可以看成是opengl的繪制上下文信息
uniform變量:一般用于Java程序傳入變換矩陣舅巷,材質,光照參數(shù)和顏色等信息河咽。如:uniform mat4 uMatrix;
attribute變量:一般用來傳入一些頂點的數(shù)據(jù)钠右,如:頂點坐標,法線忘蟹,紋理坐標飒房,頂點顏色等。
- 輸出
build-in變量:即glsl的內建變量媚值,如:gl_Position狠毯。
varying變量:用于頂點著色器向片元著色器傳遞數(shù)據(jù)。需要注意的是:這種變量必須在頂點著色器和片元著色器中褥芒,聲明必須一致嚼松。比如上面的inAlpha。
片元著色器輸入與輸出
- 輸入
build-in變量:同頂點著色器锰扶。
varying變量:用于作為頂點著色器數(shù)據(jù)的輸入惜颇,與頂點著色器聲明一致
- 輸出
build-in變量:即glsl的內建變量,如:gl_FragColor少辣。
知道了如何傳值凌摄,其他的就一目了然了。
- 獲取頂點著色器的alpha漓帅,然后在繪制前把值傳遞進入锨亏。
- 在片元著色器中,修改從紋理中取出的顏色值的alpha忙干。最后賦值給gl_FragColor進行輸出器予。
接著,在MultiOpenGLPlayerAcitivity中捐迫,改變上層畫面的半透值
class MultiOpenGLPlayerActivity: AppCompatActivity() {
// 省略無關代碼...
private fun initSecondVideo() {
val drawer = VideoDrawer()
// 設置半透值
drawer.setAlpha(0.5f)
drawer.setVideoSize(1920, 1080)
drawer.getSurfaceTexture {
initPlayer(path2, Surface(it), false)
}
render.addDrawer(drawer)
}
//...
}
當你以為可以完美的輸出一個半透明的畫面時乾翔,會發(fā)現(xiàn)畫面依然不是透明的。為啥施戴?
因為沒有開啟OpenGL混合模式反浓,回到SimpleRender中。
- 在onSurfaceCreated中開啟混合模式赞哗;
- 在onDrawFrame中開始繪制每一幀之前雷则,清除屏幕,否則會有畫面殘留肪笋。
class SimpleRender: GLSurfaceView.Renderer {
private val drawers = mutableListOf<IDrawer>()
override fun onSurfaceCreated(gl: GL10?, config: EGLConfig?) {
GLES20.glClearColor(0f, 0f, 0f, 0f)
//------開啟混合月劈,即半透明---------
// 開啟很混合模式
GLES20.glEnable(GLES20.GL_BLEND)
// 配置混合算法
GLES20.glBlendFunc(GLES20.GL_SRC_ALPHA, GLES20.GL_ONE_MINUS_SRC_ALPHA)
//------------------------------
val textureIds = OpenGLTools.createTextureIds(drawers.size)
for ((idx, drawer) in drawers.withIndex()) {
drawer.setTextureID(textureIds[idx])
}
}
override fun onSurfaceChanged(gl: GL10?, width: Int, height: Int) {
GLES20.glViewport(0, 0, width, height)
for (drawer in drawers) {
drawer.setWorldSize(width, height)
}
}
override fun onDrawFrame(gl: GL10?) {
// 清屏度迂,否則會有畫面殘留
GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT or GLES20.GL_DEPTH_BUFFER_BIT)
drawers.forEach {
it.draw()
}
}
fun addDrawer(drawer: IDrawer) {
drawers.add(drawer)
}
}
這樣,就可以看到一個半透明的視頻猜揪,疊加在另一個視頻上面啦惭墓。
怎么樣,是不是嗅到一股視頻編輯的騷味而姐?
這其實就是最基礎的視頻編輯原理了诅妹,基本上所有的視頻編輯都是基于著色器,去做畫面的變換毅人。
接下來再來看下兩個基本的變換:移動和縮放吭狡。
2) 移動
接下來,來看看如何通過觸摸拖動丈莺,來改變視頻的位置划煮。
前面文章講過,圖片或視頻的移位和縮放缔俄,基本都是通過矩陣變換完成的弛秋。
Android在Matrix中提供了一個方法用于矩陣的平移:
/**
* Translates matrix m by x, y, and z in place.
*
* @param m matrix
* @param mOffset index into m where the matrix starts
* @param x translation factor x
* @param y translation factor y
* @param z translation factor z
*/
public static void translateM(
float[] m, int mOffset,
float x, float y, float z) {
for (int i=0 ; i<4 ; i++) {
int mi = mOffset + i;
m[12 + mi] += m[mi] * x + m[4 + mi] * y + m[8 + mi] * z;
}
}
其實就是改變了4x4矩陣的最后一行的值。
其中俐载,x蟹略,y,z分別是相對于當前位置移動的距離遏佣。
這里需要注意的是:平移的變化值挖炬,被乘上了縮放的比例。具體大家可以用筆在紙上算一下就知道了状婶。
如果原始矩陣是單位矩陣意敛,直接使用以上translateM方法進行移動變換即可。
但是為了矯正畫面的比例膛虫,上篇文章詳細的介紹過草姻,視頻畫面是經(jīng)過縮放的,因此當前畫面的矩陣并非單位矩陣稍刀。
為此撩独,要平移畫面,就需要對x账月,y综膀,z進行相應的縮放處理(否則移動的距離將被原矩陣中的縮放因子改變)。
那么捶障,有兩種辦法可以使畫面按照正常的距離移動:
- 將矩陣還原為單位矩陣->移動->再縮放
- 使用當前矩陣->縮放移動距離->移動
很多人都是使用第一種僧须,這里使用第二種。
- 記錄縮放比例
在上一篇文章中项炼,介紹了如何計算縮放系數(shù):
ratio = videoRatio * worldRatio
或
ratio = videoRatio / worldRatio
分別對應寬或者高的縮放系數(shù)担平。在VideoDrawer中,分別把寬高的縮放系數(shù)記錄下來锭部。
class VideoDrawer : IDrawer {
// 省略無關代碼......
private var mWidthRatio = 1f
private var mHeightRatio = 1f
private fun initDefMatrix() {
if (mMatrix != null) return
if (mVideoWidth != -1 && mVideoHeight != -1 &&
mWorldWidth != -1 && mWorldHeight != -1) {
mMatrix = FloatArray(16)
var prjMatrix = FloatArray(16)
val originRatio = mVideoWidth / mVideoHeight.toFloat()
val worldRatio = mWorldWidth / mWorldHeight.toFloat()
if (mWorldWidth > mWorldHeight) {
if (originRatio > worldRatio) {
mHeightRatio = originRatio / worldRatio
Matrix.orthoM(
prjMatrix, 0,
-mWidthRatio, mWidthRatio,
-mHeightRatio, mHeightRatio,
3f, 5f
)
} else {// 原始比例小于窗口比例暂论,縮放高度度會導致高度超出,因此拌禾,高度以窗口為準取胎,縮放寬度
mWidthRatio = worldRatio / originRatio
Matrix.orthoM(
prjMatrix, 0,
-mWidthRatio, mWidthRatio,
-mHeightRatio, mHeightRatio,
3f, 5f
)
}
} else {
if (originRatio > worldRatio) {
mHeightRatio = originRatio / worldRatio
Matrix.orthoM(
prjMatrix, 0,
-mWidthRatio, mWidthRatio,
-mHeightRatio, mHeightRatio,
3f, 5f
)
} else {// 原始比例小于窗口比例,縮放高度會導致高度超出湃窍,因此闻蛀,高度以窗口為準,縮放寬度
mWidthRatio = worldRatio / originRatio
Matrix.orthoM(
prjMatrix, 0,
-mWidthRatio, mWidthRatio,
-mHeightRatio, mHeightRatio,
3f, 5f
)
}
}
//設置相機位置
val viewMatrix = FloatArray(16)
Matrix.setLookAtM(
viewMatrix, 0,
0f, 0f, 5.0f,
0f, 0f, 0f,
0f, 1.0f, 0f
)
//計算變換矩陣
Matrix.multiplyMM(mMatrix, 0, prjMatrix, 0, viewMatrix, 0)
}
}
// 平移
fun translate(dx: Float, dy: Float) {
Matrix.translateM(mMatrix, 0, dx*mWidthRatio*2, -dy*mHeightRatio*2, 0f)
}
// ......
}
代碼中您市,根據(jù)縮放寬或高觉痛,分別記錄對應的寬高縮放比。
接著茵休,在translate方法中,對dx和dy分別做了縮放。那么縮放是如何得出的呢散怖?
- 計算移動縮放比
首先晒屎,來看下普通矩陣平移是如何計算縮放的。
可以看到钉鸯,一個單位矩陣吧史,在Y方向上放大了2倍以后,經(jīng)過Matrix.translateM變換唠雕,實際平移的距離是原來的2倍扣蜻。
那么為了將移動的距離還原回來,需要把這個倍數(shù)除去及塘。
最終得到:
sx = dx / w_ratio
sy = dy / h_ratio
接下來看看莽使,如何計算OpenGL視頻畫面的移動縮放系數(shù)。
第一個是矩陣是OpenGL正交投影矩陣笙僚,我們已經(jīng)知道left和right芳肌,top和bottom互為反數(shù),并且等于視頻畫面的縮放比w_ratio肋层,h_ratio(不清楚的亿笤,請看上一篇文章),因此可以簡化成為右邊的矩陣栋猖。
經(jīng)過Matrix.translateM進行轉換以后净薛,得到的平移分別為:
x方向:1/w_ratio * dx
y方向:1/h_ratio * dy
因此,可以得出正確的平移量為:
sx = dx * w_ratio
sy = dy * h_ratio
但是蒲拉,為何代碼中的平移系數(shù)都乘以2呢肃拜?即
fun translate(dx: Float, dy: Float) {
Matrix.translateM(mMatrix, 0, dx*mWidthRatio*2, -dy*mHeightRatio*2, 0f)
}
首先理解一下痴腌,這里的dx和dy指的是什么呢?
dx = (curX - prevX) / GLSurfaceView_Width
dy = (curY - prevY) / GLSurfaceView_Height
其中燃领,
curX/curY:為當前手指觸摸點的x/y坐標
pervX/prevY:為上一個手指觸摸點的x/y坐標
即dx士聪,dy是歸一化的距離,范圍(0~1)猛蔽。
對應了OpenGL的世界坐標:
x方向為 (left, right) -> (-w_ratio, w_ratio)
y方向為 (top, bottom) ->(-h_ratio, h_ratio)
實際上整個OpenGL的世界坐標寬為:2倍的w_ratio剥悟;高為2倍的h_ratio。所以要把實際(0~1)換算為對應的世界坐標中的距離曼库,需要乘以2区岗,才能得到正確的移動距離。
最后毁枯,還有一點要注意的是慈缔,y方向的平移前面加了一個負號,這是因為Android屏幕Y軸的正方向是向下后众,而OpenGL世界坐標Y軸方向是向上的胀糜,正好相反。
- 獲取觸摸距離蒂誉,并平移畫面
為了獲取手指的觸摸點教藻,需要自定義一個GLSurfaceView。
class DefGLSurfaceView : GLSurfaceView {
constructor(context: Context): super(context)
constructor(context: Context, attrs: AttributeSet): super(context, attrs)
private var mPrePoint = PointF()
private var mDrawer: VideoDrawer? = null
override fun onTouchEvent(event: MotionEvent): Boolean {
when (event.action) {
MotionEvent.ACTION_DOWN -> {
mPrePoint.x = event.x
mPrePoint.y = event.y
}
MotionEvent.ACTION_MOVE -> {
val dx = (event.x - mPrePoint.x) / width
val dy = (event.y - mPrePoint.y) / height
mDrawer?.translate(dx, dy)
mPrePoint.x = event.x
mPrePoint.y = event.y
}
}
return true
}
fun addDrawer(drawer: VideoDrawer) {
mDrawer = drawer
}
}
代碼很簡單右锨,為了方便演示括堤,只添加了一個繪制器,也沒有去判斷手指是否觸摸到實際畫面的位置绍移,只要有觸摸移動悄窃,就平移畫面。
然后把它放到頁面中使用
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.cxp.learningvideo.opengl.DefGLSurfaceView
android:id="@+id/gl_surface"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
</android.support.constraint.ConstraintLayout>
最后蹂窖,在Activity中調用addDrawer轧抗,把上面那個畫面的繪制器設置給DefGLSurfaceView。
private fun initSecondVideo() {
val drawer = VideoDrawer()
drawer.setVideoSize(1920, 1080)
drawer.getSurfaceTexture {
initPlayer(path2, Surface(it), false)
}
render.addDrawer(drawer)
//設置繪制器瞬测,用于觸摸移動
gl_surface.addDrawer(drawer)
}
這樣横媚,就可以隨便移動畫面啦。
3)縮放
相對于移動縮放顯得要簡單的多月趟。
Android的Matrix提供一個矩陣縮放方法:
/**
* Scales matrix m in place by sx, sy, and sz.
*
* @param m matrix to scale
* @param mOffset index into m where the matrix starts
* @param x scale factor x
* @param y scale factor y
* @param z scale factor z
*/
public static void scaleM(float[] m, int mOffset,
float x, float y, float z) {
for (int i=0 ; i<4 ; i++) {
int mi = mOffset + i;
m[ mi] *= x;
m[ 4 + mi] *= y;
m[ 8 + mi] *= z;
}
}
這個方法也非常簡單灯蝴,就是將x,y孝宗,z對應的矩陣縮放的位置乘以縮放倍數(shù)穷躁。
在VideoDrawer中添加一個縮放的方法scale:
class VideoDrawer : IDrawer {
// 省略無關代碼.......
fun scale(sx: Float, sy: Float) {
Matrix.scaleM(mMatrix, 0, sx, sy, 1f)
mWidthRatio /= sx
mHeightRatio /= sy
}
// ......
}
這里要注意的一點是,設置完縮放系數(shù)的時候因妇,要把該縮放系數(shù)累計到原來的投影矩陣的縮放系數(shù)中问潭,這樣在平移的時候才能正確縮放移動距離猿诸。
注意:這里是 (原來的縮放系數(shù) / 正要縮放的系數(shù)),而非“乘”睦授。因為縮放投影矩陣的縮放比例是“越大两芳,縮的越小”(可以再去看下正交投影的矩陣摔寨,left去枷、right、top是复、bottom是分母)
最后給畫面設置一個縮放系數(shù)删顶,比如0.5f。
private fun initSecondVideo() {
val drawer = VideoDrawer()
drawer.setAlpha(0.5f)
drawer.setVideoSize(1920, 1080)
drawer.getSurfaceTexture {
initPlayer(path2, Surface(it), false)
}
render.addDrawer(drawer)
gl_surface.addDrawer(drawer)
// 設置縮放系數(shù)
Handler().postDelayed({
drawer.scale(0.5f, 0.5f)
}, 1000)
}
效果如下:
三淑廊、后話
以上就是在音視頻開發(fā)中使用到的最基礎的知識逗余,但千萬不要小瞧這些知識,許多酷炫的效果其實都是基于這些最簡單的變換去實現(xiàn)的季惩,希望大家有所收獲录粱。
咱們下篇見!