首先,這一系列文章均基於本身的理解和實踐,可能有不對的地方,歡迎你們指正。
其次,這是一個入門系列,涉及的知識也僅限於夠用,深刻的知識網上也有許許多多的博文供你們學習了。
最後,寫文章過程當中,會借鑑參考其餘人分享的文章,會在文章最後列出,感謝這些做者的分享。php
碼字不易,轉載請註明出處!java
教程代碼:【Github傳送門】 |
---|
渲染多視頻畫面,是實現音視頻編輯的基礎,本文將介紹如何將多個視頻畫面渲染到OpenGL中,以及如何對畫面進行混合、縮放、移動等。android
距離上次更新已經有兩個星期,因爲這段時間事情比較多,還請各位關注本系列文章的小夥伴見諒,一有時間我會加緊碼字,感謝你們的關注和督促。git
下面就來看看如何在OpenGL中渲染多視頻畫面。github
在上篇文章中,詳細的講解了如何經過OpenGL渲染視頻畫面,以及對視頻畫面進行比例矯正,基於前面系列文章中封裝好的工具,能夠很是容易地實如今OpenGL中渲染多個視頻畫面。算法
上文的OpenGL Render很是簡單以下:bash
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修改成列表,以支持多個繪製器。app
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)
}
}
複製代碼
一樣很是簡單,框架
接着,新建一個新頁面,生成多個解碼器和繪製器。ide
<?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中渲染。
而且,你應該發現了,渲染多個視頻,其實就是生成多個紋理ID,利用這個ID生成一個Surface渲染表面,最後把這個Surface給到解碼器MediaCodec渲染便可。
因爲我這裏使用的兩個視頻都是1920*1080的寬高,因此會發現,兩個視頻只顯示了一個,由於重疊在一塊兒了。
兩個畫面以下:
如今,兩個視頻疊加在一塊兒,看不到底下的視頻,那麼,咱們來改變一下上面這個視頻的alpha值,讓它變成半透明,不就能夠看到下面的視頻了嗎?
首先,爲了統一,在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()
//新增調節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())
//將座標數據轉換爲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 = worldRatio * originRatio
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 = originRatio / worldRatio
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: 建立、編譯並啓動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())
//建立OpenGL ES程序,注意:須要在OpenGL渲染線程中建立,不然沒法渲染
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)
//配置邊緣過渡參數
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)
//設置着色器參數, 第二個參數表示一個頂點包含的數據數量,這裏爲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 {
//根據type建立頂點着色器或者片元着色器
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,最後給到了片元着色器。
簡單講一下如何傳遞參數到片元着色器。
要把Java中的值傳遞到片元着色器中,直接傳值是不行的,須要經過頂點着色器,間接傳遞。
build-in變量,此類變量爲opengl內建參數,能夠當作是opengl的繪製上下文信息
uniform變量:通常用於Java程序傳入變換矩陣,材質,光照參數和顏色等信息。如:uniform mat4 uMatrix;
attribute變量:通常用來傳入一些頂點的數據,如:頂點座標,法線,紋理座標,頂點顏色等。
build-in變量:即glsl的內建變量,如:gl_Position。
varying變量:用於頂點着色器向片元着色器傳遞數據。須要注意的是:這種變量必須在頂點着色器和片元着色器中,聲明必須一致。好比上面的inAlpha。
build-in變量:同頂點着色器。
varying變量:用於做爲頂點着色器數據的輸入,與頂點着色器聲明一致
build-in變量:即glsl的內建變量,如: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)
}
//...
}
複製代碼
當你覺得能夠完美的輸出一個半透明的畫面時,會發現畫面依然不是透明的。爲啥?
由於沒有開啓OpenGL混合模式,回到SimpleRender中。
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)
}
}
複製代碼
這樣,就能夠看到一個半透明的視頻,疊加在另外一個視頻上面啦。
怎麼樣,是否是嗅到一股視頻編輯的騷味?
這其實就是最基礎的視頻編輯原理了,基本上全部的視頻編輯都是基於着色器,去作畫面的變換。
接下來再來看下兩個基本的變換:移動和縮放。
接下來,來看看如何經過觸摸拖動,來改變視頻的位置。
前面文章講過,圖片或視頻的移位和縮放,基本都是經過矩陣變換完成的。
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方法進行移動變換便可。
可是爲了矯正畫面的比例,上篇文章詳細的介紹過,視頻畫面是通過縮放的,所以當前畫面的矩陣並不是單位矩陣。
爲此,要平移畫面,就須要對x,y,z進行相應的縮放處理(不然移動的距離將被原矩陣中的縮放因子改變)。
那麼,有兩種辦法可使畫面按照正常的距離移動:
不少人都是使用第一種,這裏使用第二種。
在上一篇文章中,介紹瞭如何計算縮放係數:
ratio = videoRatio * worldRatio
或
ratio = videoRatio / worldRatio
複製代碼
分別對應寬或者高的縮放係數。在VideoDrawer中,分別把寬高的縮放係數記錄下來。
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) {
mWidthRatio = worldRatio * originRatio
Matrix.orthoM(
prjMatrix, 0,
-mWidthRatio, mWidthRatio,
-mHeightRatio, mHeightRatio,
3f, 5f
)
} else {// 原始比例小於窗口比例,縮放寬度會致使寬度度超出,所以,寬度以窗口爲準,縮放高度
mHeightRatio = 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 = originRatio / worldRatio
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)
}
// ......
}
複製代碼
代碼中,根據縮放寬或高,分別記錄對應的寬高縮放比。
接着,在translate方法中,對dx和dy分別作了縮放。那麼縮放是如何得出的呢?
首先,來看下普通矩陣平移是如何計算縮放的。
能夠看到,一個單位矩陣,在Y方向上放大了2倍之後,通過Matrix.translateM變換,實際平移的距離是原來的2倍。
那麼爲了將移動的距離還原回來,須要把這個倍數除去。
最終獲得:
sx = dx / w_ratio
sy = dy / h_ratio
複製代碼
接下來看看,如何計算OpenGL視頻畫面的移動縮放係數。
第一個是矩陣是OpenGL正交投影矩陣,咱們已經知道left和right,top和bottom互爲反數,而且等於視頻畫面的縮放比w_ratio,h_ratio(不清楚的,請看上一篇文章),所以能夠簡化成爲右邊的矩陣。
通過Matrix.translateM進行轉換之後,獲得的平移分別爲:
x方向:1/w_ratio * dx
y方向:1/h_ratio * dy
複製代碼
所以,能夠得出正確的平移量爲:
sx = dx * w_ratio
sy = dy * h_ratio
複製代碼
可是,爲什麼代碼中的平移係數都乘以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)
}
複製代碼
這樣,就能夠隨便移動畫面啦。
相對於移動縮放顯得要簡單的多。
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對應的矩陣縮放的位置乘以縮放倍數。
在VideoDrawer中添加一個縮放的方法scale:
class VideoDrawer : IDrawer {
// 省略無關代碼.......
fun scale(sx: Float, sy: Float) {
Matrix.scaleM(mMatrix, 0, sx, sy, 1f)
mWidthRatio /= sx
mHeightRatio /= sy
}
// ......
}
複製代碼
這裏要注意的一點是,設置完縮放係數的時候,要把該縮放係數累計到原來的投影矩陣的縮放係數中,這樣在平移的時候才能正確縮放移動距離。
注意:這裏是 (原來的縮放係數 / 正要縮放的係數),而非「乘」。由於縮放投影矩陣的縮放比例是「越大,縮的越小」(能夠再去看下正交投影的矩陣,left、right、top、bottom是分母)
最後給畫面設置一個縮放係數,好比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)
// 設置縮放係數
Handler().postDelayed({
drawer.scale(0.5f, 0.5f)
}, 1000)
}
複製代碼
效果以下:
以上就是在音視頻開發中使用到的最基礎的知識,但千萬不要小瞧這些知識,許多酷炫的效果其實都是基於這些最簡單的變換去實現的,但願你們有所收穫。
我們下篇見!