寫在前面
一直以來,我們在使用OpenGL渲染時,最終的目的地是默認的幀緩衝區,實際上OpenGL也允許我們創建自定義的幀緩衝區。使用自定義的幀緩衝區,可以實現鏡面,離屏渲染,以及很酷的後處理效果。本節將學習幀緩存的使用.。
本節內容整理自
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)
- 1
- 2
創建之後,我們需要將FBO綁定到目標對象:
void glBindFramebuffer(GLenum target, GLuint id)
- 1
這裏的target一般可以填寫GL_FRAMEBUFFER,這個緩衝區將會用來進行讀和寫操作;如果需要綁定到讀操作的緩衝區使用GL_READ_FRAMEBUFFER,支持 glReadPixels這類讀操作;如果需要綁定到寫操作的緩衝區使用GL_DRAW_FRAMEBUFFER,支持渲染、清除等操作。
OpenGL要求,一個完整的FBO需要滿足以下條件(來自FrameBufffer):
- 至少附加一個緩衝區(顏色、深度或者模板)
- 至少有一個顏色附加
- 所有的附加必須完整(預分配了內存)
- 每個緩衝區的採樣數需要一致
關於採樣,後面會學習,暫時不做討論。判斷一個FBO是否完整,可以如下:
if(glCheckFramebufferStatus(GL_FRAMEBUFFER) == GL_FRAMEBUFFER_COMPLETE)
- 1
如果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);
- 1
- 2
- 3
- 4
- 5
- 6
這裏需要注意的是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);
- 1
這裏我們附加到了顏色附加點。在繪製時,如果需要開啓深度測試還需要附加一個深度緩衝區,這裏我們也附加一個深度-模板到紋理中。將創建紋理的代碼封裝到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;
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
到此,我們的FBO就滿足了基本要求,可以使用了。在利用FBO作圖前,我們繼續介紹另一個附加圖像-RenderBuffer。
RenderBuffer Object
紋理圖像附加到FBO後,執行渲染後,我們可以在後期着色器處理中訪問到紋理,這給一些需要多遍處理的操作提供了很大方便。當我們不需要在後期讀取紋理時,我們可以使用Renderbuffer這種附加圖像,它主要用來存儲深度、模板這類沒有與之對應的紋理格式的緩衝區。創建和銷燬RenderBuffer也很簡單,如下:
void glGenRenderbuffers(GLsizei n, GLuint* ids)
void glDeleteRenderbuffers(GLsizei n, const Gluint* ids)
- 1
- 2
創建完畢後,仍然需要綁定道目標對象:
glBindRenderbuffer(GL_RENDERBUFFER, rbo);
- 1
需要注意的是,我們還需要爲RBO預分配內存空間:
void glRenderbufferStorage(GLenum target,
GLenum internalFormat,
GLsizei width,
GLsizei height)
- 1
- 2
- 3
- 4
這個函數爲指定內部格式的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;
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
到此,我們也利用RBO創建了一個完整的FBO。
繪製到紋理
上面利用紋理和RBO創建的FBO,我們在OpenGL中可以用來將場景繪製到紋理中。首先綁定自定義的FBO執行渲染,然後綁定到默認FBO,我們繪製一個矩形,矩形使用FBO中的紋理填充,得到效果如下圖所示:
採用線框模式繪製:
glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);
- 1
顯示的就是一個矩形:
從上面結果我們可以看到,利用FBO將場景繪製到紋理,在後期繪製矩形時使用這個紋理。這種方式可以製作鏡子等效果,十分有用。
使用後處理效果(postprocessing)
上面場景繪製到紋理後,我們可以通過操作這個紋理圖像,而得到很酷的後處理效果。例如在着色器中,將紋理的顏色進行反轉:
vec3 inversion() // 反色
{
return vec3(1.0 - texture(text, TextCoord));
}
- 1
- 2
- 3
- 4
得到的效果如下圖所示:
後處理也可以採取圖像處理的方式,例如使用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 底部右邊
);
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
然後使用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];
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
指定不同的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紋理,底層如何解釋爲採樣後的顏色值,還需要進一步學習和說明