文章目錄
- 一、說明
- 二、幀緩沖區
- 三、創建新的幀緩沖區
- 四、附屬裝飾
- 4.1 紋理圖像
- 4.2 渲染緩沖區對象圖像
- 五、使用幀緩沖區
- 5.1 后期處理
- 5.2 更改代碼
- 六、后期處理效果
- 6.1 色彩處理
- 6.2 模糊
- 6.3 Sobel算子
- 七、結論
- 練習
一、說明
關于FrameBuffer的使用,是OpenGL的高級使用方式,而這種新誕生的功能有些未知的技術需要逐步熟悉,本篇就是針對它的圖像處理功能展開陳述。
二、幀緩沖區
在前面的章節中,我們了解了 OpenGL 提供的不同類型的緩沖區:顏色、深度和模板緩沖區。這些緩沖區像任何其他 OpenGL 對象一樣占用視頻內存,但到目前為止,除了在創建 OpenGL 上下文時指定像素格式之外,我們幾乎無法控制它們。這種緩沖區組合稱為默認幀緩沖區,正如您所見,幀緩沖區是內存中可以渲染的區域。如果您想獲取渲染結果并對其進行一些附加操作(例如許多現代游戲中的后處理),該怎么辦?
在本章中,我們將討論幀緩沖區對象,這是創建額外的幀緩沖區以進行渲染的一種方法。幀緩沖區的偉大之處在于它們允許您將場景直接渲染到紋理,然后可以在其他渲染操作中使用該紋理。在討論了幀緩沖區對象的工作原理之后,我將向您展示如何使用它們對上一章的場景進行后處理。
三、創建新的幀緩沖區
您需要的第一件事是一個幀緩沖區對象來管理新的幀緩沖區。
GLuint frameBuffer;
glGenFramebuffers(1, &frameBuffer);
此時您還不能使用此幀緩沖區,因為它還不完整。在以下情況下,幀緩沖區通常是完整的:
至少已附加一個緩沖區(例如顏色、深度、模板)
必須至少有一種顏色附件(OpenGL 4.1 及更早版本)
所有附件均已完成(例如,紋理附件需要保留內存)
所有附件必須具有相同數量的多重樣本
您可以隨時通過調用glCheckFramebufferStatus并檢查它是否返回來檢查幀緩沖區是否完整GL_FRAMEBUFFER_COMPLETE。其他返回值請參閱參考資料。您不必執行此檢查,但驗證通常是一件好事,就像檢查著色器是否成功編譯一樣。
現在,讓我們綁定幀緩沖區來使用它。
glBindFramebuffer(GL_FRAMEBUFFER, frameBuffer);
第一個參數指定幀緩沖區應附加到的目標。OpenGL 在這里區分了GL_DRAW_FRAMEBUFFER和GL_READ_FRAMEBUFFER。綁定到 read 的幀緩沖區用于 的調用glReadPixels,但由于這種區別在普通應用程序中相當罕見,因此您可以使用 將您的操作應用于兩者GL_FRAMEBUFFER。
glDeleteFramebuffers(1, &frameBuffer);
完成后別忘了清理。
四、附屬裝飾
僅當已分配內存來存儲結果時,您的幀緩沖區才能用作渲染目標。這是通過為每個緩沖區附加圖像(顏色、深度、模板或深度和模板的組合)來完成的。有兩種對象可以用作圖像:紋理對象和渲染緩沖區對象。前者的優點是它們可以直接在著色器中使用,如前面的章節所示,但根據您的實現,渲染緩沖區對象可能會作為渲染目標進行更優化。
4.1 紋理圖像
我們希望能夠渲染一個場景,然后在另一個渲染操作中使用顏色緩沖區中的結果,因此紋理在這種情況下是理想的選擇。創建紋理作為新幀緩沖區顏色緩沖區的圖像與創建任何紋理一樣簡單。
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);
該紋理與您之前見過的紋理之間的區別在于NULLdata 參數的值。這是有道理的,因為這次將通過渲染操作動態創建數據。由于這是顏色緩沖區的圖像,因此format和internalformat參數受到更多限制。該format參數通常僅限于 或GL_RGB以及GL_RGBA顏色internalformat格式。
我在這里選擇了默認的 RGB 內部格式,但您可以嘗試更奇特的格式,例如GL_RGB10如果您想要 10 位顏色精度。我的應用程序的分辨率為 800 x 600 像素,因此我使這個新的顏色緩沖區與該分辨率相匹配。分辨率不必與默認幀緩沖區相匹配,但glViewport如果您決定改變,請不要忘記調用。
剩下的一件事是將圖像附加到幀緩沖區。
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, texColorBuffer, 0 );
第二個參數意味著您可以有多個顏色附件。片段著色器可以通過使用我們之前使用的函數將out變量鏈接到附件來向其中任何一個輸出不同的數據。glBindFragDataLocation我們現在將堅持使用一種輸出。最后一個參數指定圖像應附加到的 mipmap 級別。 Mipmapping 沒有任何用處,因為在使用顏色緩沖區圖像進行后期處理時,它將以其原始大小進行渲染。
4.2 渲染緩沖區對象圖像
由于我們使用深度和模板緩沖區來渲染可愛的旋轉立方體,因此我們也必須創建它們。 OpenGL 允許您將它們組合成一張圖像,因此我們必須再創建一張圖像才能使用幀緩沖區。盡管我們可以通過創建另一個紋理來做到這一點,但將這些緩沖區存儲在渲染緩沖區對象中會更有效,因為我們只對讀取著色器中的顏色緩沖區感興趣。
GLuint rboDepthStencil;
glGenRenderbuffers(1, &rboDepthStencil);
glBindRenderbuffer(GL_RENDERBUFFER, rboDepthStencil);
glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH24_STENCIL8, 800, 600);
創建渲染緩沖區對象與創建紋理非常相似,不同之處在于該對象被設計用作圖像而不是像紋理那樣的通用數據緩沖區。我在這里選擇了GL_DEPTH24_STENCIL8內部格式,它適合分別保存 24 位和 8 位精度的深度緩沖區和模板緩沖區。
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_RENDERBUFFER, rboDepthStencil );
連接起來也很容易。您可以稍后通過調用刪除該對象,就像刪除任何其他對象一樣glDeleteRenderbuffers。
五、使用幀緩沖區
選擇幀緩沖區作為渲染目標非常容易,實際上只需一次調用即可完成。
glBindFramebuffer(GL_FRAMEBUFFER, frameBuffer);
在此調用之后,所有渲染操作都將其結果存儲在新創建的幀緩沖區的附件中。要切換回屏幕上可見的默認幀緩沖區,只需傳遞0.
glBindFramebuffer(GL_FRAMEBUFFER, 0);
請注意,雖然只有默認的幀緩沖區在屏幕上可見,但您可以讀取當前與調用綁定的任何幀緩沖區,glReadPixels只要它不僅綁定到GL_DRAW_FRAMEBUFFER.
5.1 后期處理
在當今的游戲中,后處理效果似乎幾乎與屏幕上渲染的實際場景一樣重要,并且實際上可以使用不同的技術來實現一些令人驚嘆的結果。實時圖形中的后處理效果通常在片段著色器中實現,并將渲染的場景作為紋理形式的輸入。幀緩沖區對象允許我們使用紋理來包含顏色緩沖區,因此我們可以使用它們為后處理效果準備輸入。
要使用著色器為先前渲染到紋理的場景創建后處理效果,通常將其渲染為屏幕填充的 2D 矩形。這樣,應用了效果的原始場景就會以其原始大小填充屏幕,就好像它首先被渲染到默認幀緩沖區一樣。
當然,您可以利用幀緩沖區發揮創意,通過從不同角度多次渲染場景并將其顯示在監視器或最終圖像中的其他對象上,使用它們在游戲世界中執行從門戶到攝像機的任何操作。這些用途更加具體,因此我將它們作為練習留給您。
5.2 更改代碼
不幸的是,在這里逐步介紹對代碼的更改有點困難,特別是如果您偏離了此處的示例代碼。現在您已經了解了幀緩沖區是如何創建和綁定的,并且只要小心一點,您應該能夠做到這一點。讓我們全局地完成這里的步驟。
首先嘗試創建幀緩沖區并檢查它是否完整。嘗試將其綁定為渲染目標,您會看到屏幕變黑,因為場景不再渲染到默認幀緩沖區。嘗試更改場景的清晰顏色并讀取它以glReadPixels檢查場景是否正確渲染到新的幀緩沖區。
接下來,嘗試創建一個新的著色器程序、頂點數組對象和頂點緩沖區對象,以 2D(而不是 3D)渲染事物。切換回默認幀緩沖區非常有用,這樣可以輕松查看結果。您的 2D 著色器不需要變換矩陣。嘗試以這種方式在 3D 旋轉立方體場景前面渲染一個矩形。
最后,嘗試將 3D 場景渲染到您創建的幀緩沖區,并將矩形渲染到默認幀緩沖區。現在嘗試使用矩形中幀緩沖區的紋理來渲染場景。
我選擇僅使用 2 個位置坐標和 2 個紋理坐標進行 2D 渲染。我的 2D 著色器如下所示:
#version 150 core
in vec2 position;
in vec2 texcoord;
out vec2 Texcoord;
void main()
{Texcoord = texcoord;gl_Position = vec4(position, 0.0, 1.0);
}
#version 150 core
in vec2 Texcoord;
out vec4 outColor;
uniform sampler2D texFramebuffer;
void main()
{outColor = texture(texFramebuffer, Texcoord);
}
使用此著色器,程序的輸出應該與您了解幀緩沖區之前相同。渲染一幀大致如下所示:
// Bind our framebuffer and draw 3D scene (spinning cube)
glBindFramebuffer(GL_FRAMEBUFFER, frameBuffer);
glBindVertexArray(vaoCube);
glEnable(GL_DEPTH_TEST);
glUseProgram(sceneShaderProgram);glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, texKitten);
glActiveTexture(GL_TEXTURE1);
glBindTexture(GL_TEXTURE_2D, texPuppy);// Draw cube scene here// Bind default framebuffer and draw contents of our framebuffer
glBindFramebuffer(GL_FRAMEBUFFER, 0);
glBindVertexArray(vaoQuad);
glDisable(GL_DEPTH_TEST);
glUseProgram(screenShaderProgram);glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, texColorBuffer);glDrawArrays(GL_TRIANGLES, 0, 6);
3D 和 2D 繪圖操作都有自己的頂點數組(立方體與四邊形)、著色器程序(3D 與 2D 后處理)和紋理。您可以看到,綁定顏色緩沖區紋理與綁定常規紋理一樣簡單。請注意,glBindTexture更改 OpenGL 狀態之類的調用相對昂貴,因此請嘗試將它們保持在最低限度。
我認為,無論我在這里解釋程序的總體結構有多好,你們中的一些人只是喜歡看一些新的示例代碼,也許還可以運行diff它以及上一章中的代碼。
六、后期處理效果
我現在將討論各種有趣的后處理效果、它們的工作原理以及它們的外觀。
6.1 色彩處理
反轉顏色是圖像處理程序中常見的一個選項,但您也可以使用著色器自己完成!
由于顏色值是從0.0到 1.0的浮點值,因此反轉通道就像計算一樣簡單1.0 - channel。如果對每個通道(紅色、綠色、藍色)執行此操作,您將得到反轉的顏色。在片段著色器中,可以這樣完成。
outColor = vec4(1.0, 1.0, 1.0, 1.0) - texture(texFramebuffer, Texcoord);
這也會影響 alpha 通道,但這并不重要,因為 alpha 混合默認情況下是禁用的。
通過計算每個通道的平均強度可以簡單地實現顏色灰度化。
outColor = texture(texFramebuffer, Texcoord);
float avg = (outColor.r + outColor.g + outColor.b) / 3.0;
outColor = vec4(avg, avg, avg, 1.0);
這工作得很好,但人類對綠色最敏感,對藍色最不敏感,因此更好的轉換可以使用加權通道。
outColor = texture(texFramebuffer, Texcoord);
float avg = 0.2126 * outColor.r + 0.7152 * outColor.g + 0.0722 * outColor.b;
outColor = vec4(avg, avg, avg, 1.0);
6.2 模糊
有兩種眾所周知的模糊技術:框模糊和高斯模糊。后者會產生更高質量的結果,但前者更容易實現,并且仍然相當接近高斯模糊。
模糊是通過對像素周圍的像素進行采樣并計算平均顏色來完成的。
const float blurSizeH = 1.0 / 300.0;
const float blurSizeV = 1.0 / 200.0;
void main()
{vec4 sum = vec4(0.0);for (int x = -4; x <= 4; x++)for (int y = -4; y <= 4; y++)sum += texture(texFramebuffer,vec2(Texcoord.x + x * blurSizeH, Texcoord.y + y * blurSizeV)) / 81.0;outColor = sum;
}
您可以看到總共采集了 81 個樣本。您可以更改 X 軸和 Y 軸上的樣本量來控制模糊量。這些blurSize變量用于確定每個樣本之間的距離。較高的樣本數和較低的樣本距離會產生更好的近似值,但也會迅速降低性能,因此請嘗試找到一個良好的平衡點。
6.3 Sobel算子
Sobel算子常用于邊緣檢測算法中,我們來看看它長什么樣。
片段著色器如下所示:
vec4 top = texture(texFramebuffer, vec2(Texcoord.x, Texcoord.y + 1.0 / 200.0));
vec4 bottom = texture(texFramebuffer, vec2(Texcoord.x, Texcoord.y - 1.0 / 200.0));
vec4 left = texture(texFramebuffer, vec2(Texcoord.x - 1.0 / 300.0, Texcoord.y));
vec4 right = texture(texFramebuffer, vec2(Texcoord.x + 1.0 / 300.0, Texcoord.y));
vec4 topLeft = texture(texFramebuffer, vec2(Texcoord.x - 1.0 / 300.0, Texcoord.y + 1.0 / 200.0));
vec4 topRight = texture(texFramebuffer, vec2(Texcoord.x + 1.0 / 300.0, Texcoord.y + 1.0 / 200.0));
vec4 bottomLeft = texture(texFramebuffer, vec2(Texcoord.x - 1.0 / 300.0, Texcoord.y - 1.0 / 200.0));
vec4 bottomRight = texture(texFramebuffer, vec2(Texcoord.x + 1.0 / 300.0, Texcoord.y - 1.0 / 200.0));
vec4 sx = -topLeft - 2 * left - bottomLeft + topRight + 2 * right + bottomRight;
vec4 sy = -topLeft - 2 * top - topRight + bottomLeft + 2 * bottom + bottomRight;
vec4 sobel = sqrt(sx * sx + sy * sy);
outColor = sobel;
就像模糊著色器一樣,以有趣的方式采集并組合一些樣本。您可以在其他地方閱讀有關技術細節的更多信息。
七、結論
著色器的一個很酷的事情是,由于顯卡具有巨大的并行處理能力,您可以實時地按像素操作圖像。毫不奇怪,像 Photoshop 這樣的新版本軟件使用顯卡來加速圖像處理操作!還有許多更復雜的效果,例如 HDR、運動模糊和 SSAO(屏幕空間環境光遮擋),但這些效果比單個著色器涉及的工作量要多一些,因此它們超出了本章的范圍。
練習
嘗試通過添加另一個幀緩沖區來實現兩次通過的高斯模糊效果。 (解決方案)
嘗試在 3D 場景中添加一個面板,從不同角度顯示該場景。 (解決方案)