反走樣技術相關文章

https://zhuanlan.zhihu.com/p/28800047

https://zhuanlan.zhihu.com/p/57503957

https://zhuanlan.zhihu.com/p/33444125

https://zhuanlan.zhihu.com/p/33444429

走樣的原因及其分類

說到走樣,首先要說的就是採樣。這也算是很多圖形學專著中提到反走樣相關技術時的一個慣例。許多專著中都會提到奈奎斯特採樣定理[1]。作爲一個數學渣我也不打算去嚴謹地證明,只說我對採樣定理的一個直觀理解:傅里葉變換[2]告訴我們,任何一個函數都可以通過不同週期的正餘弦函數線性組合而成,一個函數經過傅里葉變換後的函數的定義域表示分解後的正餘弦函數的頻率範圍,值則表示了這個線性組合的係數(當然對於連續的傅里葉變換而言線性組合係數這個解釋似乎不準確)。總之,如果一個函數它的傅里葉變換函數只在一個有限的區間內值不爲0,那就可以稱之爲有限帶寬函數。假設它的最大頻率不超過B,那麼我們通過2B的採樣頻率,就可以無損地恢復這個原始函數。因爲最大頻率的波可以通過一個週期內的兩個點唯一確定。

這個解釋可能也不好,畢竟我的數學是在機械班學的。。。回到圖形學的話題,所謂渲染,實際上就是對一個連續函數在空間內進行離散的採樣(這個函數應該包含的場景的幾何覆蓋關係,着色參數和着色方程等)。而這個函數不是有限帶寬函數,這意味着我們不論以多大的採樣頻率(反映在圖形上,就是圖像分辨率)去採樣這個函數,都不可能完美地恢復原始信號,也就都會造成走樣(aliasing),反應在圖像上就是鋸齒或者噪點。總結來說,就是在圖形渲染中,走樣是不可避免的,我們能夠做的,僅僅是利用各種技術去減輕這種現象

爲了去減輕走樣或者欠採樣,我們首先需要搞清楚有哪些因素會導致走樣,總體來說,造成走樣有兩個主要的原因[3]:

(1)對幾何覆蓋函數的採樣不足,也就是我們最常看到的邊緣鋸齒或者更學術化地稱之爲幾何走樣(Geometry Aliasing),一般發生在光柵化階段。

幾何走樣,注意立方體邊緣

(2)對渲染方程的採樣不足,因爲渲染方程也是一個連續函數,對某些部分(比如法線,高光等)在空間變化較快(高頻部分)採樣不足也會造成走樣,反映在視覺上一般是圖像閃爍或者噪點,這類稱之爲着色走樣(Shading Aliasing),一般發生在着色階段。

 

着色走樣,注意上圖中的高光,給人的感覺非常噪

接下來的技術將會按其解決的走樣問題類型進行一個簡單分類。相對來說人眼對幾何走樣的敏感度更高,所以我們一般提到的反走樣技術也大多數是幾何反走樣。但是隨着近年遊戲引擎對畫質要求越來越高,也有非常多算法是用於解決着色走樣。這兩類算法解決問題的思路也往往差別較大。

幾何反走樣:基於超採樣的方法

SSAA(Supersampling Anti-Aliasing)

SSAA可以說是圖形學中最簡單粗暴的反走樣方法,但同時也最有效,它唯一也是致命的缺點是性能太差。開篇已經說過,任何類型的走樣歸根結底都是因爲欠採樣,那麼我們只需要增加採樣數,就可以減輕走樣現象。這就是SSAA,所以SSAA簡單的來說可以分三步:

(1)在一個像素內取若干個子採樣點

(2)對子像素點進行顏色計算(採樣)

(3)根據子像素的顏色和位置,利用一個稱之爲resolve的合成階段,計算當前像素的最終顏色輸出

基本的SSAA框架

不同SSAA方式在子採樣位置的選取和最終resolve使用的濾波器上有所不同,可以使用不同的採樣模板(規則採樣,旋轉採樣,隨機採樣,抖動採樣等)或者不同的濾波函數(方波濾波器或者高斯濾波器)。

SSAA同時是幾何反走樣和着色反走樣方法,因爲它不但增加了當前幾何覆蓋函數(Coverage)的採樣率,也對渲染方程進行了更高頻率的採樣(單獨計算每個子像素的顏色)。

MSAA(Multisample Anti-Aliasing)

SSAA中每個像素需要多次計算着色,這對實時渲染的開銷是巨大的(想想4K和1080P的性能差異),我們開始也說過,實際上人眼對幾何走樣更敏感,能否解耦幾何覆蓋函數的採樣率和着色方程的採樣率呢?答案是肯定的。MSAA的原理很簡單,它仍然把一個像素劃分爲若干個子採樣點,但是相較於SSAA,每個子採樣點的顏色值完全依賴於對應像素的顏色值進行簡單的複製(該子採樣點位於當前像素光柵化結果的覆蓋範圍內),不進行單獨計算。此外它的做法和SSAA相同,每個子像素會在光柵化階段分別計算自身的Z值和模板值,有完整的Z-Test和Stencil-Test並單獨保存在Z-Buffer和Stencil-Buffer裏,就是我們需要的幾何覆蓋信息。類似於SSAA,MSAA也需要一個resolve的過程,在早期(DX9/10?)這個過程是顯卡的一個固有單元在執行,執行的方式一般也就是簡單的Box Filter,隨着可編程管線的功能逐漸強大,現在可以通過Pixel Shader來訪問相應的MSAA Texture,並且定製resolve的算法[4]。由於MSAA擁有硬件支持,相對開銷比較小,又能很好地解決幾何走樣問題,在遊戲中應用非常廣泛(我們在遊戲畫質選項中常看到的4x/8x/16x抗鋸齒一般說的就是MSAA的子採樣點數量分別爲4/8/16個)。

MSAA在光柵化過程中的原理

CSAA(Coverage Sampling Anti-Aliasing)

MSAA相對於SSAA的好處是不用多次計算着色,但對每個子採樣點仍然需要單獨存儲其Z值和stencil值,並且實際上每個子採樣點還需要單獨存儲顏色(只是該顏色不是通過單獨計算得來),這仍然會造成非常巨大的存儲及讀寫開銷。能否把子像素的Z/Stencil/Color值和Coverage的值進一步解耦開呢?這就是CSAA[5]的思路:在MSAA已有的子像素的存儲結構基礎上,給每個像素再增加一個Coverage Info,也就是在光柵化階段進一步提高每個像素的幾何覆蓋函數的採樣率,這個採樣結果用一個N比特的數表示(每一個比特表示一個子採樣點的覆蓋信息),可以這麼理解:MSAA是一個像素有M個子像素,每個子像素有一份Z/Color/Stencil,而CSAA則是說,每個像素有M個子像素(每個子像素有一份Z/Color/Stencil),還有一個額外的N Bit的Coverage信息。乍一看似乎CSAA的開銷比MSAA更大,但實際上,CSAA可以使用少量的子像素加上更大的Coverage採樣率,來實現MSAA需要更多子像素才能達到的同等效果。例如可以通過16x CSAA(4個子像素,每個像素16Bit的Coverage)達到8x或者16x的MSAA的效果。當然缺點是CSAA的resolve過程不可控。但是可以通過在Shader裏輸出一個Custom Coverage的結果,然後將光柵器算出的Geometry Coverage和Custom Coverage通過位與(AND)操作合成爲最終用於resolve的Coverage。

CSAA子像素和覆蓋信息解耦的存儲結構

另:CSAA是我們廠(N)的叫法,友商(A)那邊應該管這個技術叫EQAA(Enhanced Quality Anti-Aliasing)[6]。

幾何反走樣:基於形態學的方法

前面提到的若干種基於超採樣的反走樣方法有一個基本特性:需要特定的硬件支持(當然SSAA除外),同時它的存儲和性能開銷也相當大,尤其是對一些性能瓶頸是帶寬的渲染架構(沒錯,說的就是延遲渲染)來說負擔更明顯甚至無法支持(當然MSAA可以應用於延遲渲染的架構,感興趣可見這裏[7][8][9],這裏不再展開)。現如今不支持延遲渲染你都不好意思說自己自己的引擎是次時代的,自然反走樣就需要更適合延遲渲染的方法。我們知道,延遲渲染框架帶來的一大便利是豐富的全屏後處理效果。那麼如果能在全屏後處理框架下完成反走樣無疑是最快最合適的方案。

形態學反走樣屬於Screen Space AA的一類,它的基本思路是:假設同一物體在某些信息上存在連續性,那麼可以通過檢測像素在這些信息(顏色,深度,法線)上的不連續找出一些邊緣,同時這些邊緣根據局部形狀不同會形成一些形態模式(pattern),我們通過總結出一些固有模式,然後通過這些模式反推(擬合)出採樣前幾何邊界的解析形式(直線方程),最後通過這些方程再來計算每個像素的覆蓋率,利用覆蓋率的結果重新混合原始顏色(也就是resolve過程),最終達到反走樣的目的。

MLAA(Morphological Antialiasing)→SMAA(Subpixel Morphological Antialiasing)

MLAA和SMAA在算法思路上並無區別,只是MLAA算法最初提出來是基於CPU的算法,而SMAA則結合GPU的特點進行了工程上的優化。看過我之前有關皮膚渲染的文章並且認真去看了相關文獻的讀者應該對SMAA的作者Jorge Jimenez不太陌生,因爲我們在那篇文章中介紹的大量有關角色渲染的方法都是他提出的。實際上他在Siggraph2011中有一個非常棒的course[10]專門闡述了SMAA算法,從原理到工程細節一清二楚,非常推薦仔細閱讀,這裏有關SMAA的算法概述基本也來自於這個course。

簡單來說SMAA(以及MLAA)包含三個步驟:

(1)找到圖像中不連續的像素(即邊緣像素)

(2)從每個不連續像素出發,找到經過它的直線的兩個端點,記錄端點的距離及整個線條的形狀(這個形狀模式最多有16種),並估算當前像素被這條線段切分後的兩個部分的面積

(3)每個像素最多被四個不同方向的線條切分,則該像素最多有四個面積權重,根據該權重,取周圍像素和當前像素進行顏色混合

整個SMAA的工作流程

在第一個步驟裏,我們確定哪些像素可能是邊緣,這些區域會被認爲是“可能出現走樣的像素集合”。這和我們一般意義上的邊緣檢測區別不大,關鍵在於你如何定義“不連續”,可選的依據包括顏色(亮度),深度,法線,PrimitiveID等信息,顏色作爲連續性的參考依據的優點是易於獲得(誰還沒個色彩信息),同時能一定程度上實現着色反走樣(因爲着色走樣反映出來就是色彩信息不連續)。但它也有可能造成不必要的模糊。而深度,法線,PrimitiveID這些信息在前向渲染的框架內往往不易獲取,但對延遲渲染來說天生就有(G-Buffer),使用它們能夠較爲準確地找到可能出現幾何走樣的像素集,缺點當然是邊緣檢測要更慢一些。此外,相較於由於CPU的版本可以一次完成上述三個步驟,而GPU必須分步執行,因此在第一步執行完畢後,我們可以使用Stencil Buffer把非邊緣像素mask掉來進一步提高之後的算法效率。

第二個步驟需要從當前像素出發,向兩端查找對應線段的端點,這個搜索過程單步來說很簡單:即沿某個方向檢查前一個像素和後一個像素是否都不爲0(假設0爲非邊緣,1爲邊緣)。但是對於GPU來說,爲了找到線段終點,每個像素需要執行大量的貼圖讀取操作。SMAA利用GPU固有的雙線性插值特性來將兩次貼圖讀取合併成一次,讀取位置位於兩個像素的中點,這樣,我們只需要判斷經過雙線性插值得到的值是否爲1即可,爲1表示當前位置不是線段終點,否則即是。

雙線性插值的步進示意圖,採樣數減少一半

找到終點實際上只是找到了線段的長度,線段是什麼形態的還需要確定端點的位置,SMAA進一步用雙線性插值加上一個小偏移量的做法,使得shader能夠用一次貼圖採樣即判斷出一個端點的位置。

端點形態的確定

此外,由於線段根據端點位置的不同可以有16中不同的形態,如果在shader裏一一判斷並計算,將造成大量的分支開銷(有關圖形渲染開銷的話題或許會在我今後的專欄文章裏詳細解釋)。爲了防止這種情況,SMAA把線段形態的確定以及進一步的面積覆蓋率計算全部預計算到了一張貼圖上,然後根據找到的端點位置和長度作爲索引去查找這張4D貼圖。

FXAA(Fast Approximate Anti-Aliasing)

FXAA和也是一種形態學反走樣方法,但相比於SMAA,它進一步簡化了整個算法步驟,將我們描述的三個步驟整合在了一個後處理的pass裏,當然它的基本算法也遵循以上步驟。實際算法的說明裏拆解出了更多的步驟,這裏還是對照SMAA來簡單解釋FXAA的步驟:

(1)邊緣像素集篩選,爲了更好的通用性,FXAA使用sRGB空間的顏色作爲輸入,並根據局部的亮度對比度來確定一個像素是否是邊緣像素。這也就是表示FXAA一般應該發生在Tone Mapping之後,或者也可以把Tone Mapping和FXAA整合成一個pass。

紅色像素表示找到的邊界像素,黃色表示水平的邊界,藍色的表示垂直的邊界

(2)線段的搜索,不同於SMAA,FXAA只查找一個方向的線段,這個線段我們認爲是主方向,它要麼是橫向要麼是縱向。在通過局部差分得到主方向後,FXAA沿着主方向搜索兩個端點到該點的距離(不同於SMAA,FXAA不需要確定兩個端點的位置,只確定距離即可),假設是橫向,則找到 [公式] 和 [公式] 。

(3)最後,FXAA根據 [公式] 和 [公式] 確定當前當前像素在找到的直線上的Coverage,進一步得出垂直於主方向的另一個方向上的偏移量,基於該偏移量用雙線性插值重採樣,得到的最終顏色就相當於根據Coverage進行了線性混合後的最終結果。

從當前像素的兩個方向查找長線段的端點

根據兩端線段及總長度的比值,得出當前像素的中心沿垂直方向的偏移量

更詳細的算法說明可以看NV的文檔[11],另外這篇文章[12]對FXAA算法也解釋的很清晰。FXAA主要的優勢體現在易於整合進現有架構,並且性能開銷極低,但比起之前的算法,效果當然也要差一些。

幾何反走樣:基於時間的方法

近年來遊戲引擎中最常用的反走樣方法是基於時間的反走樣方法,它的假設是:整個場景很少發生大幅度的鏡頭/物體運動,幀與幀之間具有比較明顯的連續性,上一幀某個物體的微小表面在下幾幀中仍會出現(只是位置發生了較小移動)。在文章開始時我們曾說過,走樣是因爲採樣不足,前面介紹的方法是把採樣點散佈在二維空間裏,這些可以統稱爲空間反走樣方法(Spatial Anti-Aliasing),而基於時間的反走樣則是把採樣點散佈在幀序列(時間)裏,這樣單幀渲染的壓力就明顯減小。理論上基於時間的反走樣在場景運動不大的情況下效果和性能都顯著好於上述各類方案。

TAA(Temporal Anti-Aliasing)

嚴格來說TAA並不能算一個具體的算法,而是更像一個統一的算法框架。和SSAA一樣,TAA也能夠同時減輕幾何走樣和着色走樣的問題。它的基本框架大概是這樣[13]:

Temporal AA的算法框架

總體來說TAA也分爲採樣(sampling)合成(resolve)兩個過程,不同的TAA的具體實現也是圍繞這兩個部分有所變化。

採樣

採樣的部分主要涉及兩個方面,一個是樣本的位置分佈,另一個是歷史樣本的獲取

因爲每個像素需要的樣本被分攤在了時間軸上,因此實際上每幀我們都只需渲染一個新的樣本,然後將它和其他歷史樣本混合即可。考慮這樣的情況:當整個場景完全不動的時候,每次我們獲取的子樣本位置都一樣,那不論經過多少次混合,最終混合後的像素仍然是走樣的,爲此,我們需要在光柵化G-Buffer的階段,在Projection Matrix之後再加上一個Jittered Matrix。這個Jitttered Matrix會根據一個樣本分佈的pattern對當前採樣位置進行一個微小的偏移,保證每幀樣本分佈的位置都略微有所不同。這樣經過混合即可產生反走樣的效果。通常來說規則的採樣點pattern效果不會太好,UE4使用的是Halton Sequence[14][15]。關於抖動採樣的一個具體實現,這裏有一篇介紹[16]。

爲了獲取歷史樣本,我們需要一個稱爲reproject的過程,這個過程從原理上是比較簡單的:首先根據當前像素的uv和depth以及當前相機的View Projection Matrix去反推出當像素的世界座標,然後根據上一幀的View Projection Matrix計算出當前像素點在上一幀圖像上的uv作爲採樣座標。但只有這些還不夠,原因是這裏我們只考慮了相機的移動。如果物體本身也發生了移動呢?這裏就需要另一個G-Buffer的輔助信息:Motion Vector Buffer。這裏Motion Vector Buffer的不做描述,它是一般是一張RG16F的貼圖,通常用於提供運動模糊計算需要的信息。綜上所述,對於靜態物體,我們使用反投影的方法找到它的歷史像素採樣座標,對於動態物體,我們使用反投影結合Motion Vector Buffer來獲取歷史像素座標[13][17]。

在實際的計算中,我們往往不會使用當前像素點位置對應的Motion Vector的值,而是在取該位置的領域中運動最劇烈的向量作爲實際的Motion Vector(比如3×3的領域裏的最大Motion Vector)。這樣做的原因是,如果只考慮當前像素自身的運動,那麼在運動物體(前景)和不運動物體(背景)交界處的背景像素就會因爲沒有運動而無法產生較好的反走樣效果。

Motion Vector的選取,注意電線杆(前景)和天空(背景)的邊緣處發生的變化

合成

合成部分相對來說比較簡單,主要解決的問題是樣本的合成方法以及歷史樣本的合法性驗證

最直觀的樣本合成方法是把所有歷史像素和當前像素進行加權平均。

這個方法顯著的缺點是需要大量的存儲空間(每個歷史像素單獨存儲),並且難於跟蹤驗證每一個歷史樣本的有效性。所以一般用我們稱之爲Exponential Move Average(EMA)的方法,名字看起來很唬人,但是看一眼公式你就會發現非常熟悉:

實際上如果你實現過基於Ping-Pong Buffer的Motion Blur就會發現,兩者用的公式是一樣,這裏α一般設置成0.1。這個合成方法的優點是,不論我們有所少個採樣樣本,我們都只存一個像素的歷史顏色。

之前我們說過,TAA假設場景裏某一個微面元在連續的幀裏都能找到對應的像素樣本,但是有時候這樣的假設是錯誤的,比如某些時候因爲鏡頭或者物體的運動導致一些原本可見的像素變得不可見(或者相反),又或者場景某些物體的光照情況發生了劇烈的變化。這些都會導致我們在時間軸上累計的歷史樣本失效。如果將這些失效的像素混合進當前顏色裏,就會產生所謂的鬼影(Ghosting)

驗證像素有效性的方法都基於一個假設:當前像素樣本附近的顏色和它的顏色接近,並且它們的取值範圍形成一個凸包,我們認爲位於這個凸包內的歷史樣本的色彩取值都是有效的,可以採用,而這個凸包外的色彩取值則無效,需要通過進一步的處理才能夠採用

首先是凸包的構造,儘管我們能夠在某個色彩空間內(比如RGB空間)根據像素領域內的每個像素顏色精確構造一個凸包並判斷某個點是否位於凸包內,但這樣做計算成本很高,所以一般來說我們使用每個分量的最大/最小值構造出一個AABB,利用這個AABB去做歷史樣本的驗證。一般來說,基於YCoCg色彩空間的AABB的驗證效果會優於RGB空間(畢竟亮度的變化是比較敏感的,所以YCoCg構造出的更像是RGB空間裏的一個OBB包圍盒,它更能精確反應當前區域的色彩分佈)[14]。

另一種凸包的構造方案是基於當前區域的統計信息,我們稱之爲Variance Clipping。顧名思義,它使用當前像素領域內的所有像素顏色的一階矩和二階矩作爲AABB的center和extent

當歷史像素不在合法範圍內時,有兩種處理方法,分別是clampclip,clamp就是直接逐分量地把每個顏色值截取到合法範圍內,clip則更復雜一些,它將歷史樣本和當前AABB的中心值連線,並將連線和AABB的交點作爲修正後的值來使用。

YCoCg空間下的clamp和clip的區別

可以看出來,TAA從原理以及算法上並不複雜,但由於它將樣本分佈在時間上這樣一個特點,所以它的實現貫穿了整個引擎的渲染流水線(比如樣本生成是在G-Buffer的繪製階段和Lighting階段,resolve一般發生在後處理階段。相比之下全屏範走樣的方案則往往只發生在後處理階段)。所以它在引擎上實現的工程難度較大,需要針對具體引擎進行較爲深度的架構改造。此外,理想的情況是TAA結合MSAA一起使用,當然這樣會造成更大的開銷,因此很少真的有引擎這樣做。

實際上,這種將樣本從空間分佈到時間上的策略,對於一些需要隨機採樣點的圖形學算法(比如SSAO和SSR)來說,也能起到很好的效果(大幅增加了可用的採樣點)。今年HPG2017上的基於1SPP的光線追蹤反走樣方法[18]也利用了Temporal AA作爲基本的思路。

將隨機光線分攤到時間上的SSR的效果

總結

以上是一些常用反走樣方法的描述。在這個主題的下一篇文章裏我會繼續總結着色反走樣以及其他和反走樣有關的算法內容。整篇文章斷斷續續寫了一個月有餘,原本計劃一篇寫清楚的內容也因爲東拉西扯終於拆成兩篇,下一篇不出意外應該是明年的某個時候吧。。。寫完的時候恰好是平安夜,當然本來這也算不上什麼節日(至少對我而言),不過想想這種時候還在寫這種喪心病狂的文章突然覺得有點淒涼。。。好在南方並不太會下雪。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章