1. 程式人生 > >OpenGL幀快取物件(FBO)

OpenGL幀快取物件(FBO)

幀快取(Frame buffer)

幀快取是螢幕所顯示畫面的一個直接映象,又稱為位對映圖 (Bit Map) 或光柵。幀快取的每一儲存單元對應螢幕上的一個畫素,整個幀快取對應一幀影象。

圖形程式一個重要的目標,就是在螢幕上繪製圖像(或者繪製到離屏的一處快取中)。幀快取(通常也就是螢幕)是由矩形的畫素陣列組成的,每個畫素都可以在影象對應的點上顯示一小塊方形的顏色值。經過光柵化階段,也就是執行片元著色器之後,得到的資料還不是真正的畫素,只是候選的片元。每個片元都包含與畫素位置對應的座標資料,以及顏色和深度的儲存值。通常來說,畫素(x,y)填充的區域是以x為左側,x+1為右側,y為底部,而y+1為頂部的一處矩形區域。

一個支援OpenGL渲染的視窗 (即幀快取) 可能包含以下的組合:
至多4個顏色快取,一個深度快取,一個模板快取,一個積累快取,一個多重取樣快取。

OpenGL給了我們自己定義幀快取的自由,我們可以選擇性的定義自己的顏色緩衝、深度和模板緩衝。我們目前所做的渲染操作都是是在預設的幀緩衝之上進行的。當你建立了你的視窗的時候預設幀緩衝就被建立和配置好了(GLFW為我們做了這件事)。通過建立我們自己的幀緩衝我們能夠獲得一種額外的渲染方式。

建立一個幀快取

我們可以使用一個叫做glGenFramebuffers的函式來建立一個幀緩衝物件(簡稱FBO):

GLuint fbo;
glGenFramebuffers(1
, &fbo)
;

這種物件的建立和使用的方式與之前見到的差不多。先建立一個幀緩衝物件,把它繫結到當前幀緩衝,做一些操作,然後解綁幀緩衝。我們使用glBindFramebuffer來繫結幀緩衝:

glBindFramebuffer(GL_FRAMEBUFFER, fbo);

繫結到GL_FRAMEBUFFER目標後,接下來所有的讀、寫幀緩衝的操作都會影響到當前繫結的幀緩衝。也可以使用GL_READ_FRAMEBUFFER或GL_DRAW_FRAMEBUFFER,把幀緩衝分開繫結到讀或寫目標上。

建構一個完整的幀緩衝必須滿足以下條件:

  • 我們必須往裡面加入至少一個附件(顏色、深度、模板緩衝)。
  • 其中至少有一個是顏色附件。
  • 所有的附件都應該是已經完全做好的(已經儲存在記憶體之中)。
  • 每個緩衝都應該有同樣數目的樣本。

我們需要為幀緩衝建立一些附件,還需要把這些附件附加到幀緩衝上。然後使用下面方法檢查是否完成:

if(glCheckFramebufferStatus(GL_FRAMEBUFFER) == GL_FRAMEBUFFER_COMPLETE)

後續所有渲染操作將渲染到當前繫結的幀快取的附加快取中,由於我們的幀緩衝不是預設的幀快取,渲染命令對視窗的視訊輸出不會產生任何影響。出於這個原因,它被稱為離屏渲染(off-screen rendering),就是渲染到一個另外的快取中。

如果要使渲染操作對視窗產生影響,要重新繫結0來使預設幀緩衝啟用:

glBindFramebuffer(GL_FRAMEBUFFER, 0);

當做完所有幀緩衝操作,要刪除幀緩衝物件:

glDeleteFramebuffers(1, &fbo);

在執行完成檢測前,我們先把一個或更多的附件附加到幀緩衝上。一個附件就是一個記憶體地址,這個記憶體地址裡面包含一個為幀緩衝準備的緩衝,它可以是個影象。當建立一個附件的時候我們有兩種方式可以採用:紋理或渲染緩衝(renderbuffer)物件。

紋理附件(Texture attachments)

當把一個紋理附件加到幀緩衝上的時候,所有渲染命令會寫入到紋理上,就像它是一個普通的顏色、深度或者模板緩衝一樣。使用紋理的好處是,所有渲染操作的結果都會被儲存為一個紋理影象,這樣我們就可以簡單的在著色器中使用了。

為幀快取建立一個紋理和建立普通紋理差不多:

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);

主要的區別是我們把紋理的維度設定為螢幕大小(儘管不是必須的),我們還傳遞NULL作為紋理的data引數。對於這個紋理,我們只分配記憶體,而不去填充它。紋理填充會在渲染到幀緩衝的時候去做。同樣,要注意,我們不用關心環繞方式或者Mipmap,因為在大多數時候都不會需要它們的。

如果你打算把整個螢幕渲染到一個或大或小的紋理上,你需要用新的紋理的尺寸再次呼叫glViewport(在渲染到你的幀緩衝前),否則只有一小部分紋理或螢幕能夠繪製到紋理上。

現在我們已經建立了一個紋理,最後一件要做的事情是把它附加到幀緩衝上:

glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0,GL_TEXTURE_2D, texture, 0);

glFramebufferTexture2D 函式的引數:

  • target:我們所建立的幀緩衝型別的目標(繪製、讀取或兩者都有)。
  • attachment:我們所附加的附件的型別。現在我們附加的是一個顏色附件。需要注意,最後的那個0是暗示我們可以附加1個以上顏色的附件。
  • textarget:你希望附加的紋理型別。
  • texture:附加的實際紋理。
  • level:Mipmap level。我們設定為0。

除顏色附件以外,我們還可以附加一個深度和一個模板紋理到幀緩衝物件上。若要附加深度緩衝型別,使用GL_DEPTH_ATTACHMENT設定附件型別。若要附加模板緩衝,要使用 GL_STENCIL_ATTACHMENT設定附加型別,同時把glTexImage2D中紋理格式指定為 GL_STENCIL_INDEX。

也可以同時附加一個深度緩衝和一個模板緩衝為一個單獨的紋理。這樣紋理的每32位數值就包含了24位的深度資訊和8位的模板資訊。可以使用GL_DEPTH_STENCIL_ATTACHMENT型別設定,下面是一個附加了深度和模板緩衝為單一紋理的例子:

glTexImage2D( GL_TEXTURE_2D, 0, GL_DEPTH24_STENCIL8, 800, 600, 0, GL_DEPTH_STENCIL, GL_UNSIGNED_INT_24_8, NULL );
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_TEXTURE_2D, texture, 0);
渲染緩衝物件附件(Renderbuffer object attachments)

幀快取的附件方式除了紋理,還有渲染緩衝物件(Renderbuffer objects)。和紋理一樣,渲染緩衝物件也是一個緩衝,它可以是一堆位元組、整數、畫素或者其他東西。渲染緩衝物件的一大優點是,它以OpenGL原生渲染格式儲存它的資料,因此在離屏渲染到幀緩衝的時候,這些資料就相當於被優化過的了,在寫入或把它們的資料簡單地到其他緩衝的時候非常快。

渲染緩衝物件將所有渲染資料直接儲存到它們的緩衝裡,而不會進行鍼對特定紋理格式的任何轉換,這樣它們就成了一種快速可寫的儲存介質了。然而,渲染緩衝物件通常是隻寫的,不能修改它們(就像獲取紋理,不能寫入紋理一樣)。可以用glReadPixels函式去讀取,函式返回一個當前繫結的幀緩衝的特定畫素區域,而不是直接返回附件本身。

建立一個渲染緩衝物件和建立幀緩衝程式碼差不多:

GLuint rbo;
glGenRenderbuffers(1, &rbo);

相似地,把渲染緩衝物件繫結,這樣所有後續渲染緩衝操作都會影響到當前的渲染緩衝物件:

glBindRenderbuffer(GL_RENDERBUFFER, rbo);

由於渲染緩衝物件通常是隻寫的,它們經常作為深度和模板附件來使用。因為我們需要把深度值和模板值提供給測試,但不需要對這些值取樣,所以深度和模板緩衝物件是完全符合的。當我們不去從這些緩衝中取樣的時候,渲染緩衝物件通常很合適,因為它們等於是被優化過的。

呼叫glRenderbufferStorage函式可以建立一個深度和模板渲染緩衝物件,我們選擇GL_DEPTH24_STENCIL8作為內部格式:

glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH24_STENCIL8, 800, 600);

最後一件還要做的事情是把幀緩衝物件附加上:

glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_RENDERBUFFER, rbo);
  • 比較
    在幀緩衝專案中,渲染緩衝物件可以提供一些優化,但更重要的是知道何時使用渲染緩衝物件,何時使用紋理。通常的規則是,如果你永遠都不需要從特定的緩衝中進行取樣,渲染緩衝物件對特定緩衝是更明智的選擇。如果哪天需要從比如顏色或深度值這樣的特定緩衝取樣資料的話,你最好還是使用紋理附件。從執行效率角度考慮,它不會對效率有太大影響。
渲染到紋理

現在我們知道了一些幀緩衝工作原理,開始嘗試使用它們。我們把場景渲染到一個顏色紋理上,這個紋理附加到一個我們建立的幀緩衝上,然後把這個紋理繪製到一個鋪滿螢幕的四邊形上。輸出的影象看似和沒用幀緩衝一樣,但其實是直接列印到了一個單獨的四邊形上面。為什麼這很有用呢?下一部分我們會看到原因。

我們先來建立幀快取,具體思路是:
先建立幀快取,然後建立一個顏色紋理附件用於繪製,再建立一個深度和模板 渲染快取物件用於深度測試(本例先不使用模板測試),還要把它們都附加到幀快取上。

第一件要做的事情是建立一個幀緩衝物件,並繫結它,這比較明瞭:

GLuint framebuffer;
glGenFramebuffers(1, &framebuffer);
glBindFramebuffer(GL_FRAMEBUFFER, framebuffer);

下一步我們建立一個紋理影象,這是我們將要附加到幀緩衝的顏色附件。我們把紋理的尺寸設定為視窗的寬度和高度,並保持資料未初始化:

// Generate texture
GLuint texColorBuffer;
glGenTextures(1, &texColorBuffer);
glBindTexture(GL_TEXTURE_2D, texColorBuffer);
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);
glBindTexture(GL_TEXTURE_2D, 0);

// 把顏色紋理附加到幀快取上
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, texColorBuffer, 0);

接下來建立一個渲染緩衝物件來進行深度測試和模板測試。記住,當你不打算從指定緩衝取樣的的時候,渲染緩衝物件是不錯的選擇。我們把它設定為GL_DEPTH24_STENCIL8,對於我們的目的來說這個精確度已經足夠了。

GLuint rbo;
glGenRenderbuffers(1, &rbo);
glBindRenderbuffer(GL_RENDERBUFFER, rbo);
glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH24_STENCIL8, 800, 600);  
glBindRenderbuffer(GL_RENDERBUFFER, 0);

// 把渲染緩衝物件附加到 幀緩衝
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_RENDERBUFFER, rbo);

然後我們檢查幀緩衝是否完成了:

if(glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE){
    cout << "ERROR::FRAMEBUFFER:: Framebuffer is not complete!" << endl;
}
// 解綁幀緩衝,確保不會意外渲染到錯誤的幀緩衝上。
glBindFramebuffer(GL_FRAMEBUFFER, 0);

現在完成幀快取了,要做的就是渲染到幀快取上。具體流程如下:

  1. 繫結我們建立的幀快取來啟用它,開啟深度測試。
  2. 繪製場景內容。此時都繪製到了幀快取的紋理上。
  3. 再繫結預設的幀快取來啟用它。
  4. 使用上面的紋理來繪製四邊形,同時關閉深度測試(因為繪製一個四邊形不需要深度測試)。

最後,我們能看到場景內容,結果和之前不使用自定義的幀快取是一樣的。然而這有什麼好處呢?那就是場景中的任何畫素已經被當作一個紋理影象了,我們可以在片段著色器中對其建立一些有意思的效果。所有這些有意思的效果統稱為後處理特效。



文/fan2b(簡書作者)
原文連結:http://www.jianshu.com/p/7a4c8cd4587e
著作權歸作者所有,轉載請聯絡作者獲得授權,並標註“簡書作者”。