1. 程式人生 > >Android OpenGLES2.0(十二)——FBO離屏渲染

Android OpenGLES2.0(十二)——FBO離屏渲染

之前的部落格我們所做的示例都是直接渲染到螢幕上的,如果我們並不需要渲染到螢幕上,也就是離屏渲染,該怎麼做呢?FBO離屏渲染是一個很好的選擇。在這篇部落格中,我們將以渲染攝像頭資料為例,使用FBO進行離屏渲染。

關於FBO離屏渲染

所謂的FBO就是Frame Buffer Object。之前我們使用OpenGLES渲染,都是直接渲染到螢幕上,FBO可以讓我們的渲染不渲染到螢幕上,而是渲染到離屏Buffer中。這樣的作用是什麼呢?比如我們需要處理一張圖片,在上傳時增加時間的水印,這個時候不需要顯示出來的。再比如我們需要對攝像頭採集的資料,一個彩色原大小的顯示出來,一個黑白的長寬各一半錄製成視訊。
像這些情況,我們就可以使用到FBO離屏渲染技術了,當然FBO並不是僅僅侷限於此。

FBO渲染步驟

如果這時候,你正在尋求關於離屏渲染的幫助,並且看到了這篇部落格,那麼我想你應該是能夠把影象直接渲染到螢幕上的。不能的話可以Google、百度或者直接看看前面的部落格。
影象直接渲染到螢幕上的步驟:

  1. 編寫Shader。(檢查支援、許可權什麼的就不再提了)
  2. 建立GL環境,直接使用GLSurfaceView,GLSurfaceView內部實現了建立GL環境。
  3. GL環境建立後,編譯Shader,建立GL Program。獲取可用Texture,設定渲染引數。(onSurfaceCreated中)
  4. 設定ViewPort。(onSurfaceChanged中)
  5. 清屏(onDrawFrame中)
  6. 啟用必要的屬性,useProgram,繫結紋理,傳入引數(頂點座標、紋理座標、變換矩陣等)。(onDrawFrame中)
  7. 繪製。(onDrawFrame中)
  8. 下一幀資料,requestRender,再一次從第5步開始執行。

FBO離屏渲染我們需要改動的地方為:

  • 獲取可用的Texture,不再只獲取一個,針對我們假設的需求可以獲取兩個。一個是作為資料來源的texture,另外一個是用來作為輸出影象的texture,這時候這個texture相當於是一塊還沒畫東西的畫布。獲取一個可用的FrameBuffer,方法名和獲取可用Texture類似,為glGenFrameBuffers。
  • 繪製前先繫結FrameBuffer、RenderBuffer、Texture,並將RenderBuffer和Texture掛載到FrameBuffer上。

這只是個大概的說法,並不太準確。具體程式碼可參看Demo

紋理(Texture)的使用

紋理在之前的圖片處理、Camera預覽中就使用到了,一直都是默默的用,沒詳細說明,在這裡補充一下。紋理的使用通常為:在GL執行緒建立成功後,在GL執行緒中生成紋理,並設定紋理引數,然後在渲染時啟用紋理,繫結紋理,並將紋理傳入Shader(告訴Shader,取樣器是哪個)。

生成紋理

OpenGL生成紋理,其實是從未被使用的“紋理堆”(姑且這樣理解吧)中獲取指定個數的紋理,這些紋理不一定是連續的。生成紋理的方法為GLES20.glGenTextures(int size,int[] textures,int start),其C函式為void glGenTextures(GLsizei n,GLuint * textures);。Android版的第一個引數為需要的紋理數,第二個引數為儲存獲得的紋理ID的陣列,第三個引數為陣列的起始位置。
gl的命名很有規律,類似於生成紋理的還有生成FrameBuffer的GLES20.glGenFrameBuffers、生成RenderBuffer的GLES20.glGenRenderBuffers等待。其他的方法基本也可以依次類推。

紋理過濾引數設定

glTexParameteri和glTexParameterf為紋理過濾設定函式,設定紋理引數前需要先繫結紋理,讓GPU明白需要設定的是哪個紋理,繫結紋理的方法為GLES20.glBindTexture(int target,int texture)使用示例如下:

GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, texture[0]);
//設定縮小過濾為使用紋理中座標最接近的一個畫素的顏色作為需要繪製的畫素顏色
GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MIN_FILTER,GLES20.GL_NEAREST);
//設定放大過濾為使用紋理中座標最接近的若干個顏色,通過加權平均演算法得到需要繪製的畫素顏色
GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MAG_FILTER,GLES20.GL_LINEAR);
//設定環繞方向S,擷取紋理座標到[1/2n,1-1/2n]。將導致永遠不會與border融合
GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_S,GLES20.GL_CLAMP_TO_EDGE);
//設定環繞方向T,擷取紋理座標到[1/2n,1-1/2n]。將導致永遠不會與border融合
GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_T,GLES20.GL_CLAMP_TO_EDGE);

以上為2D紋理GLES20.GL_TEXTURE_2D的繫結和引數設定,如果是相機,我們通常使用的是擴充套件紋理GLES11Ext.GL_TEXTURE_EXTERNAL_OES。詳細用法及使用區別參考Demo中的相機預覽和圖片處理。

使用紋理

我們在使用紋理時,一般需要用GLES20.glActiveTexture指明啟用的紋理單元,說啟用其實也不太合適,GLES20.glActiveTexture並不是啟用紋理單元,而是選擇當前活躍的紋理單元。預設情況下當前活躍的紋理單元為GLES20.GL_TEXTURE0。使用示例如下:

//啟用紋理單元1
GLES20.glActiveTexture(GLES20.GL_TEXTURE1);
//繫結2D紋理
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D,getTextureId());
//將紋理設定給Shader
GLES20.glUniform1i(mHTexture,1);

Frame Buffer和Render Buffer

Frame Buffer Object(FBO)即為幀緩衝物件,用於離屏渲染緩衝。相對於其它同類技術,如資料拷貝或交換緩衝區等,使用FBO技術會更高效並且更容易實現。而且FBO不受視窗大小限制。FBO可以包含許多顏色緩衝區,可以同時從一個片元著色器寫入。FBO是一個容器,自身不能用於渲染,需要與一些可渲染的緩衝區繫結在一起,像紋理或者渲染緩衝區
Render Buffer Object(RBO)即為渲染緩衝物件,分為color buffer(顏色)、depth buffer(深度)、stencil buffer(模板)。
在使用FBO做離屏渲染時,可以只繫結紋理,也可以只繫結Render Buffer,也可以都繫結或者繫結多個,視使用場景而定。如只是對一個影象做變色處理等,只繫結紋理即可。如果需要往一個影象上增加3D的模型和貼紙,則一定還要繫結depth Render Buffer。
同Texture使用一樣,FrameBuffer使用也需要呼叫GLES20.glGenFrameBuffers生成FrameBuffer,然後在需要使用的時候呼叫GLES20.glBindFrameBuffer

Frame Buffer只與Texture繫結

FrameBuffer的建立,Texture的建立及引數設定,程式碼如下:

GLES20.glGenFramebuffers(1, fFrame, 0);
GLES20.glGenTextures(2, textures, start);
for (int i = 0; i < 2; i++) {
    GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textures[i]);
    GLES20.glTexImage2D(GLES20.GL_TEXTURE_2D, 0,gl_format, width, height,
        0, gl_format, GLES20.GL_UNSIGNED_BYTE, null);
    GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MIN_FILTER,GLES20.GL_NEAREST);
    GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MAG_FILTER,GLES20.GL_LINEAR);
    GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_S,GLES20.GL_CLAMP_TO_EDGE);
    GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_T,GLES20.GL_CLAMP_TO_EDGE);
}
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D,0);

建立完畢後,在渲染時使用:

//繫結FrameBuffer
GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, fFrame[0]);
//為FrameBuffer掛載Texture[1]來儲存顏色
GLES20.glFramebufferTexture2D(GLES20.GL_FRAMEBUFFER, GLES20.GL_COLOR_ATTACHMENT0,
    GLES20.GL_TEXTURE_2D, textures[1], 0);
//繫結FrameBuffer後的繪製會繪製到textures[1]上了
GLES20.glViewport(0,0,w,h);
mFilter.draw();
//解綁FrameBuffer
GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER,0);

Frame Buffer與Texture、Render Buffer繫結

有時候只繫結紋理並不能滿足我們的要求,當我們需要深度的時候,還需要繫結Render Buffer。在繫結紋理的基礎上再繫結RenderBuffer其實也就比較簡單了。首先使用前還是先生成RenderBuffer,併為RenderBuffer建立資料儲存的格式和渲染物件的尺寸:

//生成Render Buffer
GLES20.glGenRenderbuffers(1,fRender,0);
//繫結Render Buffer
GLES20.glBindRenderbuffer(GLES20.GL_RENDERBUFFER,fRender[0]);
//設定為深度的Render Buffer,並傳入大小
GLES20.glRenderbufferStorage(GLES20.GL_RENDERBUFFER,GLES20.GL_DEPTH_COMPONENT16,
    width, height);
//為FrameBuffer掛載fRender[0]來儲存深度
GLES20.glFramebufferRenderbuffer(GLES20.GL_FRAMEBUFFER, GLES20.GL_DEPTH_ATTACHMENT,
    GLES20.GL_RENDERBUFFER, fRender[0]);
//解綁Render Buffer
GLES20.glBindRenderbuffer(GLES20.GL_RENDERBUFFER,0);

然後渲染時,增加深度繫結和解綁:

GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, frameBufferId);
GLES20.glFramebufferTexture2D(GLES20.GL_FRAMEBUFFER, GLES20.GL_COLOR_ATTACHMENT0,
    GLES20.GL_TEXTURE_2D, textureId, 0);
//為FrameBuffer掛載fRender[0]來儲存深度
GLES20.glFramebufferRenderbuffer(GLES20.GL_FRAMEBUFFER, GLES20.GL_DEPTH_ATTACHMENT,
            GLES20.GL_RENDERBUFFER, fRender[0]);
GLES20.glViewport(0,0,width,height);
mFilter.draw();
GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER,0);
//解綁Render Buffer
GLES20.glBindRenderbuffer(GLES20.GL_RENDERBUFFER,0);

最後的處理

不管是FrameBuffer、RenderBuffer還是Texture不再使用時,都應該刪除掉,刪除的方法類似:

//刪除Render Buffer
GLES20.glDeleteRenderbuffers(1, fRender, 0);
//刪除Frame Buffer
GLES20.glDeleteFramebuffers(1, fFrame, 0);
//刪除紋理
GLES20.glDeleteTextures(1, fTexture, 0);

原始碼

所有的程式碼全部在一個專案中,託管在Github上——Android OpenGLES 2.0系列部落格的Demo
工程內容是將一張圖片,使用FBO轉為黑白圖片並儲存的例子。為了方便,直接使用GLSurfaceView來提供GL環境。