前言
上次我寫了一遍文章《Android 關(guān)于美顏/濾鏡 從OpenGl錄制視頻的一種方案》,里面利用ImageReader來從獲取Surface上獲取數(shù)據(jù),但是經(jīng)過@熊皮皮的提醒,我發(fā)現(xiàn)多PBO的確可以實(shí)現(xiàn)跟ImageReader一樣的效果,并且版本要求僅為Android4.3。
代碼已上傳至GitHub
濾鏡部分來源于《Android圖像處理之實(shí)時濾鏡》
提示:工程需要下載NDK和CMake
正文
1.原理
什么是PBO?PBO就是PixelBufferObject(像素緩存對象),它跟VBO很相似,只不過一個存像素數(shù)據(jù),一個存頂點(diǎn)數(shù)據(jù),你可以通過《OpenGL像素緩沖區(qū)對象(PBO)》了解。
其實(shí)上篇文章里我列舉的幾個方法里面已經(jīng)有PBO了,但是因?yàn)槲抑坝玫氖菃蝹€PBO,結(jié)果測試發(fā)現(xiàn)效率不行就放棄了。
單PBO獲取像素信息如下:
//綁定到PBO
GLES30.glBindBuffer(GLES30.GL_PIXEL_PACK_BUFFER, mPboIds.get(0));
//從FBO中讀取數(shù)據(jù)寫入到PBO中
GLES30.glReadPixels(0, 0, 480, 640, GLES30.GL_RGBA, GLES30.GL_UNSIGNED_BYTE,0);
//將OpenGL緩存區(qū)映射到客戶端內(nèi)存
ByteBuffer byteBuffer = (ByteBuffer) GLES30.glMapBufferRange(GLES30.GL_PIXEL_PACK_BUFFER, 0, 480 * 640 * 4, GLES30.GL_MAP_READ_BIT);
//取消內(nèi)存映射
GLES30.glUnmapBuffer(GLES30.GL_PIXEL_PACK_BUFFER);
//解除PBO綁定
GLES30.glBindBuffer(GLES30.GL_PIXEL_PACK_BUFFER, 0);
這上面代碼其實(shí)沒有什么問題,包括GLES30.glReadPixels()時間都已經(jīng)降為0,但就是在執(zhí)行函數(shù) GLES30.glMapBufferRange()映射內(nèi)存的時候非常慢。
后來經(jīng)過提醒后我重新翻看了《OpenGL像素緩沖區(qū)對象(PBO)》后發(fā)現(xiàn)我之前忽略了二點(diǎn)。
第一個問題是 GLES30.glMapBufferRange()這個函數(shù)實(shí)際會等待GPU完成了對相應(yīng)緩沖區(qū)對象的操作后才會返回,所以我使用單個PBO并不能顯著的提高傳輸效率,而PBO的主要優(yōu)點(diǎn)在于可以通過DMA(Direct Memory Access)進(jìn)行異步傳輸數(shù)據(jù),從而不影響CPU的時鐘周期,所以使用2個PBO, 一個PBO拷貝數(shù)據(jù)、一個PBO映射內(nèi)存,交替使用,效率將大大提高。
第二個問題就是字節(jié)對齊問題,OpenGLES默認(rèn)以4字節(jié)對齊,也就是說我取得的rowStride應(yīng)該是4的整數(shù)倍,計算公式如下:
int align = 4;//4字節(jié)對齊
int rowStride = (width * pixelStride + (align - 1)) & ~(align - 1);
而我在GLES30.glReadPixels()中使用的參數(shù)是GLES30.GL_RGBA,pixelStride應(yīng)該等于4,那么就有(width * 4 + (4 - 1)) & ~(4 - 1) == width * 4,從這個道理上來講,我的width無論取得什么應(yīng)該都是內(nèi)存對齊的,效率不應(yīng)該會降低,事實(shí)上大部分機(jī)子都沒有問題,但是在索尼Z2上效率下降了。
經(jīng)過我實(shí)驗(yàn)后發(fā)現(xiàn)如果我是128字節(jié)對齊,那么效率不會降低,代碼如下:
int align = 128;//128字節(jié)對齊
int rowStride = (width * mPixelStride + (align - 1)) & ~(align - 1);
事實(shí)上這里我很奇怪,理論上GLES20.glPixelStore()最大值應(yīng)該是8,怎么都不可能是128,我懷疑這個值應(yīng)該跟硬件和屏幕分辨率有關(guān),因?yàn)镮mageReader計算出來的rowStride和我計算出來的值不一樣,但是我沒有在網(wǎng)上找到相關(guān)的資料,如果有誰知道請留言告知我下,謝謝。
關(guān)于內(nèi)存對齊你可以通過《關(guān)于內(nèi)存對齊的那些事》了解。
修改后多PBO獲取像素信息如下:
//綁定到第一個PBO
GLES30.glBindBuffer(GLES30.GL_PIXEL_PACK_BUFFER, mPboIds.get(mPboIndex));
//從FBO中讀取數(shù)據(jù)寫入到PBO中
GLES30.glReadPixels(0, 0, 480, 640, GLES30.GL_RGBA, GLES30.GL_UNSIGNED_BYTE,0);
//綁定到第二個PBO
GLES30.glBindBuffer(GLES30.GL_PIXEL_PACK_BUFFER, mPboIds.get(mPboNewIndex));
//將OpenGL緩存區(qū)映射到客戶端內(nèi)存
ByteBuffer byteBuffer = (ByteBuffer) GLES30.glMapBufferRange(GLES30.GL_PIXEL_PACK_BUFFER, 0, 480 * 640 * 4, GLES30.GL_MAP_READ_BIT);
//取消內(nèi)存映射
GLES30.glUnmapBuffer(GLES30.GL_PIXEL_PACK_BUFFER);
//解除PBO綁定
GLES30.glBindBuffer(GLES30.GL_PIXEL_PACK_BUFFER, 0);
//交換索引
mPboIndex = (mPboIndex + 1) % 2;
mPboNewIndex = (mPboNewIndex + 1) % 2;
經(jīng)過修改后,2個PBO輪流交替使用,就完全可以滿足需求。
2.實(shí)現(xiàn)
實(shí)際上面講完,這篇文章就可以結(jié)束了,但是我怎么會滿足呢!所以我對MagicCamera進(jìn)行了一些修改。
1.去除grafika方法
在使用PBO之后,grafika方法就已經(jīng)失去作用了,并且在MagicCamera的寫法中過了2次濾鏡(繪制到本地窗口一次,繪制到Surface一次),所以開啟錄制后OpenGL的計算量將加倍。
這里直接刪除encoder文件夾。
2.修改原來的繪制方案
原來的繪制方案是先將攝像頭數(shù)據(jù)繪制到FBO,然后將返回的紋理經(jīng)過濾鏡后繪制到本地窗口。
但是因?yàn)橐褂肞BO,所以我先將攝像頭數(shù)據(jù)過濾鏡后繪制到FBO,然后以屏幕大小繪制到本地窗口,和以錄制大小繪制到另一個FBO在通過PBO獲取數(shù)據(jù)。
這樣做的好處就是3個大小,屏幕大小、攝像頭大小、錄制大小可以各不相同。

這樣需要注意一點(diǎn)因?yàn)槠聊淮笮『弯浿拼笮〔幌嗤?,所以它們的頂點(diǎn)坐標(biāo)和紋理坐標(biāo)也不相同,需要重新計算屏幕坐標(biāo)和錄制坐標(biāo)。
修改后代碼請看CameraGlSurfaceView。
3.開始繪制
接下來就可以開始繪制了,首先將攝像頭數(shù)據(jù)經(jīng)過濾鏡后繪制到FBO。
1.初始化FBO,完整代碼請看GPUImageFilter
//生成FBO
GLES20.glGenFramebuffers(1, mFrameBuffers, 0);
//生成紋理
GLES20.glGenTextures(1, mFrameBufferTextures, 0);
//綁定到紋理
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, mFrameBufferTextures[0]);
//...省略設(shè)置紋理參數(shù)
//將紋理關(guān)聯(lián)到FBO
GLES20.glFramebufferTexture2D(GLES20.GL_FRAMEBUFFER, GLES20.GL_COLOR_ATTACHMENT0, GLES20.GL_TEXTURE_2D, mFrameBufferTextures[0], 0);
//解除綁定紋理
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, 0);
//解除綁定FBO
GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, 0);
上面將紋理關(guān)聯(lián)到FBO,這樣就可以直接繪制到紋理上。
2.將攝像頭數(shù)據(jù)經(jīng)過濾鏡后繪制到FBO,完整代碼請看GPUImageFilter
//設(shè)定為攝像頭大小
GLES20.glViewport(0, 0, 480, 640);
//綁定到FBO
GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, mFrameBuffers[0]);
//...省略其他代碼
//設(shè)置矩陣,該矩陣從攝像頭獲得
GLES20.glUniformMatrix4fv(mTextureTransformMatrixLocation, 1, false, mTextureTransformMatrix, 0);
//選擇活躍紋理
GLES20.glActiveTexture(GLES20.GL_TEXTURE0);
//綁定到紋理,這里需要注意GL_TEXTURE_EXTERNAL_OES是特殊的
GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, textureId);
GLES20.glUniform1i(mGLUniformTexture, 0);
//...省略其他代碼
//解除綁定紋理
GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, 0);
//解除綁定FBO
GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, 0);
//設(shè)定為屏幕大小
GLES20.glViewport(0, 0, 1080, 1920);
上面的矩陣通過mSurfaceTexture.getTransformMatrix(mtx)獲得,頂點(diǎn)著色器需要添加參數(shù)。
attribute vec4 position;
attribute vec4 inputTextureCoordinate;
varying vec2 textureCoordinate;
uniform mat4 textureTransform;
void main() {
textureCoordinate = (textureTransform * inputTextureCoordinate).xy;
gl_Position = position;
}
這里的GL_TEXTURE_EXTERNAL_OES必須要注意,當(dāng)我們使用mSurfaceTexture.updateTexImage()時,圖像會被隱式的綁定到GL_TEXTURE_EXTERNAL_OES,所以這里跟我們一般使用的紋理GL_TEXTURE_2D不同。
所以片段著色器也必須要修改,下面是沒有濾鏡的實(shí)現(xiàn),其他的看Raw。
#extension GL_OES_EGL_image_external : require
varying highp vec2 textureCoordinate;
uniform samplerExternalOES inputImageTexture;
void main(){
gl_FragColor = texture2D(inputImageTexture, textureCoordinate);
}`
3.將返回的紋理繪制到本地窗口,完整代碼請看GPUImageFilter
//...省略其他代碼
GLES20.glActiveTexture(GLES20.GL_TEXTURE0);
//綁定紋理,這里的紋理是GL_TEXTURE_2D
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textureId);
GLES20.glUniform1i(mGLUniformTexture, 0);
//...省略其他代碼
//解除綁定紋理
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, 0);
這里的頂點(diǎn)著色器和片段著色器需要去除矩陣和OES參數(shù)。
4.如果開始錄制將返回的紋理繪制到FBO然后通過PBO獲得數(shù)據(jù),完整代碼請看MagicRecordFilter
1.初始化PBO,完整代碼請看MagicRecordFilter
final int align = 128;//128字節(jié)對齊
mRowStride = (width * mPixelStride + (align - 1)) & ~(align - 1);
mPboIds = IntBuffer.allocate(2);
//生成2個PBO
GLES30.glGenBuffers(2, mPboIds);
//綁定到第一個PBO
GLES30.glBindBuffer(GLES30.GL_PIXEL_PACK_BUFFER, mPboIds.get(0));
//設(shè)置內(nèi)存大小
GLES30.glBufferData(GLES30.GL_PIXEL_PACK_BUFFER, mPboSize, null,GLES30.GL_STATIC_READ);
//綁定到第而個PBO
GLES30.glBindBuffer(GLES30.GL_PIXEL_PACK_BUFFER, mPboIds.get(1));
//設(shè)置內(nèi)存大小
GLES30.glBufferData(GLES30.GL_PIXEL_PACK_BUFFER, mPboSize, null,GLES30.GL_STATIC_READ);
//解除綁定PBO
GLES30.glBindBuffer(GLES30.GL_PIXEL_PACK_BUFFER, 0);
2.繪制2D紋理到FBO,完整代碼請看MagicRecordFilter
//設(shè)定為錄制大小
GLES20.glViewport(0, 0, 240, 320);
//綁定到FBO
GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, mFrameBuffers[0]);
//...省略其他代碼
//設(shè)置矩陣
GLES20.glUniformMatrix4fv(mTextureTransformMatrixLocation, 1, false, mTextureTransformMatrix, 0);
//選擇活躍紋理
GLES20.glActiveTexture(GLES20.GL_TEXTURE0);
//綁定到紋理
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textureId);
GLES20.glUniform1i(mGLUniformTexture, 0);
//...省略其他代碼
//解除綁定紋理
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, 0);
//解除綁定FBO
GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, 0);
//設(shè)定為屏幕大小
GLES20.glViewport(0, 0, 1080, 1920);
這里也需要設(shè)置矩陣,但是這個矩陣不是從攝像頭獲取的,而是我自己把它垂直翻轉(zhuǎn)了下。
mTextureTransformMatrix = new float[]{
-1f, 0f, 0f, 0f,
0f, 1f, 0f, 0f,
0f, 0f, 1f, 0f,
1f, 0f, 0f, 1f});
為什么我要垂直翻轉(zhuǎn)呢,因?yàn)镽GB圖像在內(nèi)存中存儲的時候是從下到上的,如果你直接把數(shù)據(jù)賦值給Bitmap,那么你將得到一張倒置的并且顏色為BGRA的圖像,這也可以解釋為什么我們最終要將BGRA轉(zhuǎn)換為ARGB,因?yàn)锽itmap需要的是Bitmap.Config.ARGB_8888。
這里你可以通過 《Image Stride(內(nèi)存圖像行跨度)》了解。
3.PBO獲取數(shù)據(jù),完整代碼請看MagicRecordFilter
private void bindPixelBuffer() {
//綁定到第一個PBO
GLES30.glBindBuffer(GLES30.GL_PIXEL_PACK_BUFFER, mPboIds.get(mPboIndex));
//調(diào)用glReadPixels獲取數(shù)據(jù),這里需要注意原生的Java里面沒有與PBO配合的glReadPixels方法
MagicJni.glReadPixels(0, 0, mRowStride, mInputHeight, GLES30.GL_RGBA, GLES30.GL_UNSIGNED_BYTE);
//第一幀沒有數(shù)據(jù)跳出
if (mInitRecord) {
unbindPixelBuffer();
mInitRecord = false;
return;
}
//綁定到第二個PBO
GLES30.glBindBuffer(GLES30.GL_PIXEL_PACK_BUFFER, mPboIds.get(mPboNewIndex));
//glMapBufferRange會等待DMA傳輸完成,所以需要交替使用pbo
//映射內(nèi)存
ByteBuffer byteBuffer = (ByteBuffer) GLES30.glMapBufferRange(GLES30.GL_PIXEL_PACK_BUFFER, 0, mPboSize, GLES30.GL_MAP_READ_BIT);
//解除映射
GLES30.glUnmapBuffer(GLES30.GL_PIXEL_PACK_BUFFER);
unbindPixelBuffer();
//交給mRecordHelper錄制
mRecordHelper.onRecord(byteBuffer, mInputWidth, mInputHeight, mRowStride, mLastTimestamp);
}
//解綁pbo
private void unbindPixelBuffer() {
//解除綁定PBO
GLES30.glBindBuffer(GLES30.GL_PIXEL_PACK_BUFFER, 0);
//交換索引
mPboIndex = (mPboIndex + 1) % 2;
mPboNewIndex = (mPboNewIndex + 1) % 2;
}
這里必須要注意,要與PBO配合使用glReadPixels()最后一個參數(shù)必須為0,但是原生Java層的glReadPixels()最后一個參數(shù)是Buffer,而最后參數(shù)為int的glReadPixels()24版本才有,所以這里需要使用jni去調(diào)用原生的glReadPixels()方法,代碼在MagicJni。
關(guān)于RecordHelper我就不講了,跟上篇一樣,這里可以用libyuv代替,我這只是作為測試瀏覽用。
我這里JNI采用CMake編譯,編譯指令在CMakeLists.txt,更多可以參考谷歌官方文檔《向您的項目添加 C 和 C++ 代碼》。
結(jié)尾
其實(shí)在篇文章我早就寫完了,但是一直搞不清楚rowStride的計算方式,最終我決定還是不拖了,直接發(fā)布希望有誰知道的能指點(diǎn)下,謝謝。
最后,如果它有解決你的問題的話,請下點(diǎn)個贊,謝謝。
這是我個人的第四篇文章,發(fā)布于2017年5月15日。