一、概述
幀緩沖是OpenGL中非常重要的一部分,計算機圖形學中有很多效果的實作用到了幀緩沖(雖然所有的OpenGL程式都要使用幀緩沖,但是有些效果需要靈活的使用幀緩沖才行),比如螢幕後處理、延遲渲染、陰影等等。
到目前為止,我們已經使用了很多螢幕緩沖了:用于寫入顔色值的顔色緩沖、用于寫入深度資訊的深度緩沖和允許我們根據一些條件丢棄特定片段的模闆緩沖。這些緩沖結合起來叫做幀緩沖(Framebuffer),它被儲存在記憶體中。OpenGL允許我們定義我們自己的幀緩沖,也就是說我們能夠定義我們自己的顔色緩沖,甚至是深度緩沖和模闆緩沖。
我們目前所做的所有操作都是在預設幀緩沖的渲染緩沖上進行的。預設的幀緩沖是在你建立視窗的時候生成和配置的(GLFW幫我們做了這些)。有了我們自己的幀緩沖,我們就能夠有更多方式來渲染了。
你可能不能很快了解幀緩沖的應用,但渲染你的場景到不同的幀緩沖能夠讓我們在場景中加入類似鏡子的東西,或者做出很酷的後期處理效果。首先我們會讨論它是如何工作的,之後我們将來實作這些炫酷的後期處理效果。
相關學習資料:
- LearnOpenGL網站-進階OpenGL-幀緩沖:https://learnopengl-cn.github.io/
- OpenGL紅包書第四章第7節-幀緩沖對象和第8節-多重渲染緩存的同步寫入
- OpenGL藍寶書第八章-緩沖區對象:存儲盡在掌握
二、幀緩沖原理
1.建立一個幀緩沖
和OpenGL中的其它對象一樣,我們會使用一個叫做glGenFramebuffers的函數來建立一個幀緩沖對象(Framebuffer Object, FBO):
unsigned int fbo;
glGenFramebuffers(1, &fbo);
glBindFramebuffer(GL_FRAMEBUFFER, fbo);
這三步是OpengGL中非常經典的三個步驟, glGenFramebuffers隻是為幀緩沖區起個名字,隻有glBindFramebuffer才實際配置設定幀緩沖區,并且這個glBindFramebuffer隻有第一次調用才配置設定,後面調用glBindFramebuffer隻是将目前緩沖區啟用,最後取消這個緩沖區使用glBindFramebuffer(GL_FRAMEBUFFER, 0)。這些操作是所有緩沖區都有的,非常重要。
與其他緩沖區不同的是,我們現在還不能使用我們的幀緩沖,因為它還不完整(Complete),一個完整的幀緩沖需要滿足以下的條件:
1.附加至少一個緩沖(顔色、深度或模闆緩沖)。
2.至少有一個顔色附件(Attachment)。
3.所有的附件都必須是完整的(保留了記憶體)。
4.每個緩沖都應該有相同的樣本數。
之後所有的渲染操作将會渲染到目前綁定幀緩沖的附件中。由于我們的幀緩沖不是預設幀緩沖,渲染指令将不會對視窗的視覺輸出有任何影響。出于這個原因,渲染到一個不同的幀緩沖被叫做離屏渲染(Off-screen Rendering)。要保證所有的渲染操作在主視窗中有視覺效果,我們需要再次激活預設幀緩沖,将它綁定到0:
glBindFramebuffer(GL_FRAMEBUFFER, 0);
這裡綁定到0非常重要,與其他緩沖區綁定到0有點點差別,待補充。。。
在完成所有的幀緩沖操作之後,不要忘記删除這個幀緩沖對象:
glDeleteFramebuffers(1, &fbo);
在完整性檢查執行之前,我們需要給幀緩沖附加一個附件。附件是一個記憶體位置,它能夠作為幀緩沖的一個緩沖,可以将它想象為一個圖像。當建立一個附件的時候我們有兩個選項:紋理或渲染緩沖對象(Renderbuffer Object)。
2.幀緩沖的兩個附件: 紋理附件和渲染緩沖對象 2-1 紋理附件當把一個紋理附加到幀緩沖的時候,所有的渲染指令将會寫入到這個紋理中,就想它是一個普通的顔色/深度或模闆緩沖一樣。使用紋理的優點是,所有渲染操作的結果将會被儲存在一個紋理圖像中,我們之後可以在着色器中很友善地使用它。
為幀緩沖建立一個紋理和建立一個普通的紋理差不多:
unsigned int 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);
主要的差別就是,我們将次元設定為了螢幕大小(盡管這不是必須的),并且我們給紋理的data參數傳遞了NULL。對于這個紋理,我們僅僅配置設定了記憶體而沒有填充它。填充這個紋理将會在我們渲染到幀緩沖之後來進行。同樣注意我們并不關心環繞方式或多級漸遠紋理,我們在大多數情況下都不會需要它們。
如果你想将你的螢幕渲染到一個更小或更大的紋理上,你需要(在渲染到你的幀緩沖之前)再次調用glViewport,使用紋理的新次元作為參數,否則隻有一小部分的紋理或螢幕會被渲染到這個紋理上。
現在我們已經建立好一個紋理了,要做的最後一件事就是将它附加到幀緩沖上了:
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, texture, 0);
2-2 渲染緩沖對象附件渲染緩沖對象(Renderbuffer Object)是在紋理之後引入到OpenGL中,作為一個可用的幀緩沖附件類型的,是以在過去紋理是唯一可用的附件。和紋理圖像一樣,渲染緩沖對象是一個真正的緩沖,即一系列的位元組、整數、像素等。渲染緩沖對象附加的好處是,它會将資料儲存為OpenGL原生的渲染格式,它是為離屏渲染到幀緩沖優化過的。
渲染緩沖對象直接将所有的渲染資料儲存到它的緩沖中,不會做任何針對紋理格式的轉換,讓它變為一個更快的可寫儲存媒體。然而,渲染緩沖對象通常都是隻寫的,是以你不能讀取它們(比如使用紋理通路)。當然你仍然還是能夠使用glReadPixels來讀取它,這會從目前綁定的幀緩沖,而不是附件本身,中傳回特定區域的像素。
因為它的資料已經是原生的格式了,當寫入或者複制它的資料到其它緩沖中時是非常快的。是以,交換緩沖這樣的操作在使用渲染緩沖對象時會非常快。我們在每個渲染疊代最後使用的glfwSwapBuffers,也可以通過渲染緩沖對象實作:隻需要寫入一個渲染緩沖圖像,并在最後交換到另外一個渲染緩沖就可以了。渲染緩沖對象對這種操作非常完美。
建立一個渲染緩沖對象的代碼和幀緩沖的代碼很類似:
unsigned int rbo;
glGenRenderbuffers(1, &rbo);
glBindRenderbuffer(GL_RENDERBUFFER, rbo);
由于渲染緩沖對象通常都是隻寫的,它們會經常用于深度和模闆附件,因為大部分時間我們都不需要從深度和模闆緩沖中讀取值,隻關心深度和模闆測試。我們需要深度和模闆值用于測試,但不需要對它們進行采樣,是以渲染緩沖對象非常适合它們。當我們不需要從這些緩沖中采樣的時候,通常都會選擇渲染緩沖對象,因為它會更優化一點。
建立一個深度和模闆渲染緩沖對象可以通過調用glRenderbufferStorage函數來完成:
glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH24_STENCIL8, 800, 600);
建立一個渲染緩沖對象和紋理對象類似,不同的是這個對象是專門被設計作為圖像使用的,而不是紋理那樣的通用資料緩沖(General Purpose Data Buffer)。這裡我們選擇GL_DEPTH24_STENCIL8作為内部格式,它封裝了24位的深度和8位的模闆緩沖。
最後一件事就是附加這個渲染緩沖對象:
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_RENDERBUFFER, rbo);
三、總結與補充
本文一共涉及到兩個緩沖區對象和一個2D紋理:
- 幀緩沖區對象(GL_FRAMEBUFFER,FRO)
- 渲染緩沖區對象(GL_RENDERBUFFER,RBO)
- 2D紋理(GL_TEXTURE_2D),這裡不能叫做紋理緩沖區對象,因為OpenGL中還有一個GL_TEXTURE_BUFFER,這個才是紋理緩沖區對象(關于紋理,OpenGL搞了一套單獨的接口,應該特别注意)。
幀緩沖區對象是一個容器概念,它需要紋理緩沖區對象或渲染緩沖區對象填充它,一般來說紋理緩沖區對象用作紋理附件,渲染緩沖區對象則用作深度和模闆附件,原因是紋理緩沖區對象盡量用作讀資料,渲染緩沖區對象盡量用作寫資料,兩者底層優化不一樣。
glBindFramebuffer(GL_FRAMEBUFFER, 0);将目前幀緩沖區綁定到0有一個非常重要的意義,0是預設幀緩沖區,預設幀緩沖區就是我們在使用glfw或glut建立OpenGL上下文和視窗時自動為我們建立的一個幀緩沖區,這個幀緩沖區是要繪制到視窗(也就是螢幕)的,并且還用到glViewport()這個API控制幀緩沖的大小(glViewport也是一個非常重要的概念,控制幀緩沖區的大小,比如控制陰影深度貼圖大小等等)。