從0開始的OpenGL學習(二十七)-抗鋸齒

本文主要解決兩個問題:

1、什么是抗鋸齒?
2、如何在OpenGL中使用抗鋸齒?

引言

抗鋸齒,英文名是anti-aliasing,直譯過來叫反走樣,但是由于抗鋸齒這個名字接受度更廣,所以筆者在這里也使用抗鋸齒來稱呼。“鋸齒”這種東西在3D渲染中十分常見,你之前肯定也注意到了,就是立方體邊緣那種像臺階一樣的東西:


鋸齒

抗鋸齒就是要消除這些臺階,讓邊緣看起來更加平滑,更真實。

鋸齒產生原因

鋸齒不可避免,因為我們顯示器顯示場景的時候要將某個具體的像素設置成一定的值。如果你仔細湊近顯示器看,你會發現,顯示器看上去很像一個個小方格的組合。這些小方格我們就稱之為像素。當我們要繪制3D物體的時候,計算物體的邊緣位置,如果這個位置把大半個像素包括進去了,那么顯示器會將這個像素設置成3D物體的顏色,如果沒有包一半以上,那么這個像素就會設置成背景色。我們無法把一半像素設置成物體顏色,另一半像素設置成背景色,這就是鋸齒產生的根本原因!

超級采樣抗鋸齒(SSAA, super sample anti-aliasing)

明白了鋸齒產生的原因之后,我們就要來想辦法抹平這個鋸齒了。最早想出的辦法是一種稱為超級采樣抗鋸齒的技術,簡稱SSAA。SSAA方法是把圖像映射到緩存并放大,再對放大后的圖像像素進行采樣,一般選取2個或4個鄰近像素,把這些采樣混合起來后生成最終像素,每個像素就擁有了鄰近像素的特特征,像素和像素之間的過度也就平緩了。最后將這個大圖還原到原來大小的圖像,輸出到顯示器上。

這種方法雖然也有抗鋸齒的效果,但是它對資源的消耗太大了,因為每一幀都需要進行放大、采樣、縮小的工作。所以,在經歷過一段輝煌時期之后就被掃進歷史的垃圾堆里了。

多重采樣抗鋸齒(MSAA,multisample anti-aliasing)

我們重點介紹的方法是多重采樣抗鋸齒,簡稱MSAA。要理解MSAA,我們要對OpenGL的光柵化原理了解地深入一些,對我們以后選方法、優化都有幫助。

光柵化在整個渲染流程中處于頂點處理完成與片元著色之間。它所做的工作就是將圖元的所有頂點都轉換成可以著色的片元(像素)。但是,頂點坐標可以是任意的而顯示器上的像素不是,它有大小,這個大小受限于硬件的技術。所以,絕大多數情況下,頂點和片元不會是完美的一一對應關系,所以光柵化的過程會確定圖元的邊界位置。

確定圖元包圍區域

上面的圖中,框框內紅色或者黑色的點被成為采樣點。注意:采樣點不是像素!!!默認情況下,采樣點只有一個(單點采樣),在片元中心。所以我們的邊界只有在包括一半以上的片元時才能將這個片元成物體的顏色。上圖中的三角形光柵化之后的結果是:
光柵化后的結果

可以很明顯地看到,在邊上有些像素被設置成紅色,有些沒有(即使它有一部分是在三角形內的),這樣就形成了鋸齒。

MSAA的原理是在一個片元內設置多個采樣點(2個、4個或者8個),根據被包含的采樣點數量來確定當前像素的顏色。也就是說,如果我設置了4個采樣點,其中有兩個采樣點被包含進去了,那么這個像素的顏色就是物體顏色的一半濃度。用一張圖來展示就是這樣:


MSAA

左邊的模式是單點采樣,采樣點沒被包括到三角形之內,那么對不起,這個像素就跟你三角形顏色沒關系。右邊的模式是MSAA(4點),有兩個采樣點被包含進去了,那這個點的顏色就是三角形顏色的一半濃度,成為淡藍色。但是,如果使用多重采樣,顏色緩存的大小也會變大,因為原本只要存1個采樣點的顏色,現在要存4個。

事實上,采樣點的數量是無限制的,你可以設置任何數量,只要你的機子跑的動!

事情開始變的有趣起來了。我們來猜一下OpenGL是如何實現將顏色平均效果的。是不是對每個采樣點運行一次片元著色器,然后將所有的顏色做平均?是的,你猜的沒錯。每個有效采樣點都要運行一次片元著色器,在最后輸出時才會取平均。

我們來看看用了MSAA之后的三角形原理圖:

MSAA結果

我們對每個像素設置了四個采樣點,在三角形邊緣的部分可能只有一兩個采樣點被包圍進來。對每個采樣點,我們都要進行一次片元著色計算出其顏色,最后輸出的時候要用。最終輸出的顏色取決于包含采樣點的個數,這三角形的渲染情況應當是像這樣的:

渲染結果

這樣看上去比前面的效果好多了。

開啟MSAA影響的不僅僅是顏色緩存。沒錯,深度緩存和模板緩存也是每份采樣一個。深度值會成為頂點深度到采樣點的內差值,模板值只是每個采樣點保留一份而已。當然,這也就意味著隨著采樣點數量的增加深度緩存和模板緩存占用的內存也會增大。

原理方面到這就差不多了,記住,OpenGL 內部實現比這里講的要復雜一點,但我們了解這么多就已經足夠了。

OpenGL中使用MSAA

要在OpenGL中使用MSAA,我們就必須使用能夠保存超過一個顏色值的顏色緩存。我們要一種新的緩存類型,稱為多重采樣緩存(multisample buffer)

大多數窗口系統能夠提供一個多重采樣緩存,包括GLFW。在GLFW中,我們若要使用多重采樣,操作非常簡單。使用glfw提供的函數glfwWindowHint就可以實現:

glfwWindowHint(GLFW_SAMPLES, 4);

當我們使用glfwCreateWindow函數創建窗口時,創建的窗口就會包含多重采樣緩存。GLFW也自動創建了深度緩存和模板緩存的多重采樣版。也就是說,所有的緩存大小都是原來的4倍。

現在,我們需要啟用多重采樣。啟用的方式是調用glEnable函數并傳入GL_MULTISAMPLE參數。大多數OpenGL驅動都會默認啟用多重采樣,不過我們要養成一個好的編程習慣,在每次需要啟用多重采樣的時候都調用一次glEnable函數:

glEnable(GL_MULTISAMPLE);  

啟用之后,編譯一下示例代碼,你就能看到邊緣明顯沒那么坑坑洼洼了:

啟用多重采樣之后的效果

離屏MSAA

MSAA對默認幀緩存來說非常簡單,它已經在背后偷偷地完成了所有工作。但要在自己創建的幀緩存中實現就沒那么簡單了(也不復雜)。

有兩種創建多重緩存附件到幀緩存的方式:紋理附件和渲染緩存附件,這點和之前幀緩存章節中討論的非常類似。

多重采樣紋理附件

創建用于多重采樣的紋理,我們只需要調用glTexImage2DMultisample函數替代glTexImage2D函數就行了。前者可以接受GL_TEXTURE_2D_MULTISAPLE參數,這就是我們要用到的重要參數。

glBindTexture(GL_TEXTURE_2D_MULTISAMPLE, tex);
glTexImage2DMultisample(GL_TEXTURE_2D_MULTISAMPLE, 4, GL_RGB, width, height, GL_TRUE);
glBindTexture(GL_TEXTURE_2D_MULTISAMPLE, 0);  

第二個參數指定我們想要的采樣數目。最后一個參數設置成GL_TRUE,OpenGL就會使用獨立的采樣位置,對每個紋素都用相同數目的采樣數。

將紋理附加到幀緩存上同樣是調用glFramebufferTexture2D函數,不同的是,我們要傳遞的參數是GL_TEXTURE_2D_MULTISAMPLE:

glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D_MULTISAMPLE, tex, 0); 

現在,幀緩存就有了多重采樣的顏色緩存了。

多重采樣渲染緩存對象

類似的,創建渲染緩存對象的方式是用glRenderbufferStorageMultisample代替glRenderbufferStorage:

glRenderbufferStorageMultisample(GL_RENDERBUFFER, 4, GL_DEPSTH24_STENCIL8, width, height);

同樣是第二個參數,設置成4表示啟用4個采樣點。

渲染到多重采樣的幀緩存

渲染到多重采樣的幀緩不需要我們管,它自己就搞定了。光柵化過程會自動掌管所有多重采樣的操作。我們得到的就是多重采樣后的顏色、深度、模板緩存。因為多重采樣緩存的特殊性,我們無法直接使用緩存圖片進行其他操作,比如在著色器中采樣。

因為多重采樣圖片中包含大量的數據,我們需要將它降級到普通圖片狀態。降級操作通常使用glBlitFramebuffer函數來實現。它在將一個幀緩存拷貝到另一個的時候自動進行了降級工作。

使用glBlitFramebuffer要指定源拷貝區域和目標拷貝區域,這些區域都是屏幕空間坐標。在幀緩存章節中,我們綁定幀緩存到GL_FRAMEBUFFER。這里我們用GL_READ_FRAMEBUFFER和GL_DRAW_FRAMEBUFFER函數來指定對應的源拷貝區域和目標拷貝區域。然后,glBlitFramebuffer函數就幫我們完成了剩余工作:

glBindFramebuffer(GL_READ_FRAMEBUFFER, multisampledFBO);
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0);
glBlitFramebuffer(0, 0, width, height, 0, 0, width, height, GL_COLOR_BUFFER_BIT, GL_NEAREST); 

使用這種方式繪制前面的綠色立方體,顯示的效果不會有啥區別,不過代碼很不一樣:


運行效果.png

下載這里的源碼進行比對。

自定義抗鋸齒算法

當然,將多重采樣的紋理圖直接傳遞給著色器(在降級之前)也是可以的。GLSL就讓我們自己去采樣每個紋理圖中的像素,所以,自定義抗鋸齒算法也是可行的,在大型圖形應用中經常使用。

當然,要使用多重采樣的紋理不能簡單的用sampler2D來采樣,我們需要sampler2DMS類型的采樣方式:

uniform sampler2DMS screenTextureMS;

使用texelFetch函數就可以從某個采樣中提取紋素:


vec4 colorSample = texelFetch(screenTextureMS, TexCoords, 3);  // 從第四個樣本中提取

自定義抗鋸齒算法超出了本文的討論范圍,詳細的內容就靠讀者自己去研究了。

總結

本章中,我們詳細地討論了鋸齒產生的原因以及MSAA算法的原理,對每個像素進行多次采樣來平滑鋸齒邊緣是一個普遍并且高效的方法。當然,MSAA只是其中一種方法,還有CSAA,FXAA等等很多算法可以抗鋸齒,這里就只是入個門而已,大家盡可以去研究這些方法,記得回來介紹給筆者哦!

下一篇
目錄
上一篇

參考資料

www.learnopengl.com(非常好的網站,建議學習)

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容