寫在前面
一直以來,我們在使用OpenGL渲染時,最終的目的地是默認的幀緩沖區,實際上OpenGL也允許我們創建自定義的幀緩沖區。使用自定義的幀緩沖區,可以實現鏡面,離屏渲染,以及很酷的後處理效果。本節將學習幀緩存的使用,文中示例代碼均可以在我的github下載。
本節內容整理自
1.OpenGL Frame Buffer Object (FBO)
2.www.learnopengl.com Framebuffers
FBO概念
在OpenGL中,渲染管線中的頂點、紋理等經過一系列處理後,最終顯示在2D屏幕設備上,渲染管線的最終目的地就是幀緩沖區。幀緩沖包括OpenGL使用的顏色緩沖區(color buffer)、深度緩沖區(depth buffer)、模板緩沖區(stencil buffer)等緩沖區。默認的幀緩沖區由窗口系統創建,例如我們一直使用的GLFW庫來完成這項任務。這個默認的幀緩沖區,就是目前我們一直使用的繪圖命令的作用對象,稱之為窗口系統提供的幀緩沖區(window-system-provided framebuffer)。
OpenGL也允許我們手動創建一個幀緩沖區,並將渲染結果重定向到這個緩沖區。在創建時允許我們自定義幀緩沖區的一些特性,這個自定義的幀緩沖區,稱之為應用程序幀緩沖區(application-created framebuffer object )。
同默認的幀緩沖區一樣,自定義的幀緩沖區也包含顏色緩沖區、深度和模板緩沖區,這些邏輯上的緩沖區(logical buffers)在FBO中稱之為可附加的圖像(framebuffer-attachable images),他們是可以附加到FBO的二維像素數組(2D arrays of pixels )。
FBO中包含兩種類型的附加圖像(framebuffer-attachable): 紋理圖像和RenderBuffer圖像(texture images and renderbuffer images)。附加紋理時OpenGL渲染到這個紋理圖像,在著色器中可以訪問到這個紋理對象;附加RenderBuffer時,OpenGL執行離屏渲染(offscreen rendering)。
之所以用附加這個詞,表達的是FBO可以附加多個緩沖區,而且可以靈活地在緩沖區中切換,一個重要的概念是附加點(attachment points)。FBO中包含一個以上的顏色附加點,但只有一個深度和模板附加點,如下圖所示(來自songho FBO):
一個FBO可以有
(GL_COLOR_ATTACHMENT0,…, GL_COLOR_ATTACHMENTn)
多個附加點,最多的附加點可以通過查詢GL_MAX_COLOR_ATTACHMENTS變量獲取。
值得註意的是:從上面的圖中我們可以看到,FBO本身並不包含任何緩沖對象,實際上是通過附加點指向實際的緩沖對象的。這樣FBO可以快速地切換緩沖對象。
創建FBO
同OpenGL中創建其他緩沖對象一樣,創建和銷毀FBO的步驟也很簡單:
void glGenFramebuffers(GLsizei n, GLuint* ids) void glDeleteFramebuffers(GLsizei n, const GLuint* ids)
創建之後,我們需要將FBO綁定到目標對象:
void glBindFramebuffer(GLenum target, GLuint id)
這裏的target一般可以填寫GL_FRAMEBUFFER,這個緩沖區將會用來進行讀和寫操作;如果需要綁定到讀操作的緩沖區使用GL_READ_FRAMEBUFFER,支持 glReadPixels這類讀操作;如果需要綁定到寫操作的緩沖區使用GL_DRAW_FRAMEBUFFER,支持渲染、清除等操作。
OpenGL要求,一個完整的FBO需要滿足以下條件(來自FrameBufffer):
- 至少附加一個緩沖區(顏色、深度或者模板)
- 至少有一個顏色附加
- 所有的附加必須完整(預分配了內存)
- 每個緩沖區的采樣數需要一致
關於采樣,後面會學習,暫時不做討論。判斷一個FBO是否完整,可以如下:
if(glCheckFramebufferStatus(GL_FRAMEBUFFER) == GL_FRAMEBUFFER_COMPLETE)
如果FBO不完整將不能正常工作。
那麽我們需要按照上述要求構建一個完整的FBO。
創建紋理附加圖像
創建FBO的附加紋理如同平常使用紋理一樣,不同的是,這裏只是為紋理預分配空間,而不需要真正的加載紋理,因為當使用FBO渲染時渲染結果將會寫入到我們創建的這個紋理上去。附加紋理使用函數glFramebufferTexture2D。
API void glFramebufferTexture2D( GLenum target,
GLenum attachment,
GLenum textarget,GLuint texture,GLint level);
1.target表示綁定目標,參數可選為GL_DRAW_FRAMEBUFFER, GL_READ_FRAMEBUFFER, or GL_FRAMEBUFFER。
2.attechment表示附加點,可選值為GL_COLOR_ATTACHMENTi, GL_DEPTH_ATTACHMENT, GL_STENCIL_ATTACHMENT or GL_DEPTH_STENCIL_ATTACHMMENT。
3. textTarget表示紋理的綁定目標,我們使用二維紋理填寫GL_TEXTURE_2D即可。
4. texture表示實際的紋理對象。
5. level表示 mipmap級別,我們填寫0即可。
這裏的texture是我們實際創建的紋理對象,在創建紋理對象時使用代碼:
GLuint texture; glGenTextures(1, &texture); glBindTexture(GL_TEXTURE_2D, texture); glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, 800, 600, 0, GL_RGB, GL_UNSIGNED_BYTE, NULL); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
這裏需要註意的是glTexImage2D函數,末尾的NULL表示我們只預分配空間,而不實際加載紋理。glTexImage2D函數也是一個OpenGL中相對復雜的一個函數。
API void glTexImage2D( GLenum target,
GLint level,
GLint internalFormat,
GLsizei width,
GLsizei height,
GLint border,
GLenum format,
GLenum type,
const GLvoid * data);
在前面二維紋理一節已經介紹過這個函數,這裏重點說下創建FBO紋理時需要註意的。函數中後面三個參數format、type、data表示的是內存中圖像像素的信息,包括格式,類型和指向內存的指針。而internalFormat表示的是OpenGL內存存儲紋理的格式,表示的是紋理中顏色成分的格式。從紋理圖片的內存轉移到OpenGL內存紋理存儲是一個像素轉移操作(Pixel Transfer ),關於這個部分的細節比較多,不在這裏展開,感興趣地可以參考OpenGL wiki-Pixel Transfer 。
上面填寫的紋理格式GL_RGB,以及GL_UNSIGNED_BYTE表示紋理包含紅綠藍三色,並且每個成分用無符號字節表示。600,800表示我們分配的紋理大小,註意這個紋理需要和我們渲染的屏幕大小保持一致,如果需要繪制與屏幕不一致的紋理,使用glViewport函數進行調節。
上面創建的紋理圖像,可以附加到FBO:
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, texture, 0);
這裏我們附加到了顏色附加點。在繪制時,如果需要開啟深度測試還需要附加一個深度緩沖區,這裏我們也附加一個深度-模板到紋理中。將創建紋理的代碼封裝到texture.h中,完整的用紋理圖像構建一個FBO的代碼如下:
/* * 附加紋理到Color, depth ,stencil Attachment */ bool prepareFBO1(GLuint& colorTextId, GLuint& depthStencilTextId, GLuint& fboId) { glGenFramebuffers(1, &fboId); glBindFramebuffer(GL_FRAMEBUFFER, fboId); // 附加 紋理 color attachment colorTextId = TextureHelper::makeAttachmentTexture(0, GL_RGB, WINDOW_WIDTH,WINDOW_HEIGHT, GL_RGB, GL_UNSIGNED_BYTE); glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, colorTextId, 0); // 附加 depth stencil texture attachment depthStencilTextId = TextureHelper::makeAttachmentTexture(0, GL_DEPTH24_STENCIL8,WINDOW_WIDTH, WINDOW_HEIGHT, GL_DEPTH_STENCIL, GL_UNSIGNED_INT_24_8); glFramebufferTexture2D(GL_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT,GL_TEXTURE_2D, depthStencilTextId, 0); // 檢測完整性 if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) { return false; } glBindFramebuffer(GL_FRAMEBUFFER, 0); return true; }
到此,我們的FBO就滿足了基本要求,可以使用了。在利用FBO作圖前,我們繼續介紹另一個附加圖像-RenderBuffer。
RenderBuffer Object
紋理圖像附加到FBO後,執行渲染後,我們可以在後期著色器處理中訪問到紋理,這給一些需要多遍處理的操作提供了很大方便。當我們不需要在後期讀取紋理時,我們可以使用Renderbuffer這種附加圖像,它主要用來存儲深度、模板這類沒有與之對應的紋理格式的緩沖區。創建和銷毀RenderBuffer也很簡單,如下:
void glGenRenderbuffers(GLsizei n, GLuint* ids) void glDeleteRenderbuffers(GLsizei n, const Gluint* ids)
創建完畢後,仍然需要綁定道目標對象:
glBindRenderbuffer(GL_RENDERBUFFER, rbo);
需要註意的是,我們還需要為RBO預分配內存空間:
void glRenderbufferStorage(GLenum target,
GLenum internalFormat,
GLsizei width,
GLsizei height)
這個函數為指定內部格式的RBO預分配空間。
當上述步驟完成後,我們可以將RBO綁定到FBO。
上面的紋理圖像中使用了紋理作為深度和模板緩沖區,這裏我們將深度模板緩沖區使用RBO代替:
/* * 附加紋理到Color Attachment * 同時附加RBO到depth stencil Attachment */ bool prepareFBO2(GLuint& textId, GLuint& fboId) { glGenFramebuffers(1, &fboId); glBindFramebuffer(GL_FRAMEBUFFER, fboId); // 附加紋理 color attachment textId = TextureHelper::makeAttachmentTexture(0, GL_RGB, WINDOW_WIDTH,WINDOW_HEIGHT, GL_RGB, GL_UNSIGNED_BYTE); glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, textId, 0); // 附加 depth stencil RBO attachment GLuint rboId; glGenRenderbuffers(1, &rboId); glBindRenderbuffer(GL_RENDERBUFFER, rboId); glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH24_STENCIL8, WINDOW_WIDTH, WINDOW_HEIGHT); // 預分配內存 glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_RENDERBUFFER, rboId); if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) { return false; } glBindFramebuffer(GL_FRAMEBUFFER, 0); return true; }
到此,我們也利用RBO創建了一個完整的FBO。
繪制到紋理
上面利用紋理和RBO創建的FBO,我們在OpenGL中可以用來將場景繪制到紋理中。首先綁定自定義的FBO執行渲染,然後綁定到默認FBO,我們繪制一個矩形,矩形使用FBO中的紋理填充,得到效果如下圖所示:
采用線框模式繪制:
glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);
顯示的就是一個矩形:
從上面結果我們可以看到,利用FBO將場景繪制到紋理,在後期繪制矩形時使用這個紋理。這種方式可以制作鏡子等效果,十分有用。
使用後處理效果(postprocessing)
上面場景繪制到紋理後,我們可以通過操作這個紋理圖像,而得到很酷的後處理效果。例如在著色器中,將紋理的顏色進行反轉:
vec3 inversion() // 反色 { return vec3(1.0 - texture(text, TextCoord)); }
得到的效果如下圖所示:
後處理也可以采取圖像處理的方式,例如使用kernel矩陣。kernel矩陣一般取為3x3矩陣,這個矩陣的和一般為1。通過kernel矩陣,將當前紋理坐標處的紋理擴展到周圍9個坐標處的紋理,然後通過權重計算出最終紋理的像素。例如產生浮雕效果的kernel矩陣如下所示:
在著色器中,我們定義當前紋理位置的9個周圍位置如下:
const float offset = 1.0 / 300; // 9個位置的紋理坐標偏移量 // 確定9個位置的偏移量 vec2 offsets[9] = vec2[]( vec2(-offset, offset), // top-left 左上方 vec2(0.0f, offset), // top-center 正上方 vec2(offset, offset), // top-right 右上方 vec2(-offset, 0.0f), // center-left 中間左邊 vec2(0.0f, 0.0f), // center-center 正中位置 vec2(offset, 0.0f), // center-right 中間右邊 vec2(-offset, -offset), // bottom-left 底部左邊 vec2(0.0f, -offset), // bottom-center 底部中間 vec2(offset, -offset) // bottom-right 底部右邊 );
然後使用kernel矩陣中的權系數,計算最終的紋理像素:
// 計算9個位置的紋理 vec3 sampleText[9]; for(int i=0; i < 9;++i) { sampleText[i] = vec3(texture(text, TextCoord.st + offsets[i])); } // 利用權值求最終紋理顏色 vec3 result = vec3(0.0); for(int i=0; i < 9;++i) { result += sampleText[i] * kernel[i]; }
指定不同的kernel將會得到不同的效果,例如指定模糊矩陣,得到模糊的效果如下圖所示:
指定edge-detection矩陣,得到效果如下圖所示:
當著色器中計算紋理坐標的偏移量offset不同時,效果會有所改變。想查看更多的kernel效果,可以訪問在線網站Image Kernels。
最後的說明
本節介紹了FBO的概念和使用,還有一些操作例如FBO的讀寫、復制操作沒有介紹到,同時glTextImage2D這個函數中紋理的內部格式以及內存中像素的格式和類型的說明將是一個比較繁瑣的工作,這些內容留到後續學習。關於選擇附加紋理還是附加RBO,可以參考Difference between Frame buffer object, Render buffer object and texture?。
在附加深度和模板的紋理時(即代碼中我們使用depthStencilTextId而不是colorTextId繪制最終的結果),如果我們使用深度和模板的紋理繪圖將會得到如下效果:
這個圖中主要呈現紅色,我分析是因為圖中離觀察者較遠的距離時深度值基本為1,那麽取得的紋理顏色基本上就是(1.0,0.0,0.0,1.0),因而呈現紅色;而離觀察者近一些的地方,深度值基本上為0,則取得的紋理顏色就是(0.0, 0.0, 0.0, 1.0),因而呈現出黑色。如果觀察者靠近場景中的立方體,那麽得到的圖像將主要呈現黑色:
附加的GL_DEPTH24_STENCIL8紋理,底層如何解釋為采樣後的顏色值,還需要進一步學習和說明。
參考資料
1.OpenGL wiki Image Format
2.OpenGL wiki Framebuffer Object
3.OpenGL wiki Pixel Transfer
4.Wiki Framebuffer object
Tags: 應用程序 Object images 緩沖區 color
文章來源: