令人討厭的“走樣”
? ? ? ? 我在日常工作中通過傳統(tǒng)的OpenGL繪制函數(shù)繪制線段時(shí),發(fā)現(xiàn)繪制出的線段邊緣充滿了“鋸齒”,而這種“鋸齒”在線段運(yùn)動(dòng)和旋轉(zhuǎn)時(shí)往往會(huì)更加明顯(圖 1)。這種我們不希望看到的“鋸齒”被成為“走樣”,而消除這種“鋸齒”的過程就是我們所說的“反走樣”。雖然OpenGL提供了諸如設(shè)置 GL_LINE_SMOOTH 屬性、多重采樣等線段反走樣的方法,但效果和質(zhì)量受到很多方面的限制,而且不同的硬件廠商使用不同的反走樣算法,所以使得反走樣的結(jié)果在不同的GPU上有 著不同的效果。因此我們需要一種更為高效和通用的線段反走樣技術(shù)。
為什么會(huì)“走樣”?
? ? ? 在介紹如何對(duì)線段反走樣之前,我們必須了解為什么我們繪制的線段會(huì)產(chǎn)生“走樣”。
? ? ? 我們都知道,在數(shù)學(xué)的定義中一條線段是由兩個(gè)端點(diǎn)確定的,而線段是沒有寬度和面積的。但在計(jì)算機(jī)圖形領(lǐng)域中,為了讓人的肉眼能夠看到,必須給線段一定的寬 度,所以我們的線段通常是由兩個(gè)端點(diǎn)和一個(gè)寬度參數(shù)確定的,而我們計(jì)算機(jī)中圖形的寬度通常都是以像素為單位的,因此我們的線段寬度有可能是1像素也有可能 是n像素。
??????? 如果需要在白色的背景下繪制一條寬度為1像素的黑色線段,從信號(hào)處理的觀點(diǎn)上來看,我們可以把這條線段看做一個(gè)值為1的信號(hào),而線段外部的區(qū)域信號(hào)值為0,如果不加任何處理,線段的邊界就是這樣一個(gè)不連續(xù)的階梯函數(shù)(圖2)。 因?yàn)閹彺婧惋@示器所能容納的像素點(diǎn)是有限的,所以我們需要對(duì)這個(gè)信號(hào)進(jìn)行采樣。
? ? ? ?我們可以看到:離散采樣(圖2 中用藍(lán)色虛線表示)的間隔無論多么的小都無法精確的表達(dá)它的不連續(xù)性,因此我們無論怎么提高分辨率,都無法徹底消除走樣。而根據(jù)耐奎斯特的信號(hào)采樣定理:要重構(gòu)一個(gè)不走樣的信號(hào),采樣率至少是信號(hào)最高頻率的兩倍。
???????????????????????????????????????????????? 即:C = B * log2 N ( bps )
?????? 因此,從理論上來說要繪制一條沒有走樣的線段,我們必須擁有足夠大的信號(hào)頻率,也就是我們需要無限放大我們的屏幕分辨率才能徹底消除走樣。
? ? ? ?從圖3我們可以看出,雖然我們提高了分辨率,但是走樣依然存在。因此,一味地提高分辨率是無法徹底解決掉線段走樣問題的,而且在時(shí)間、空間以及金錢有限的情況下是不允許我們這么做的。
解決之道
? ? ? 計(jì)算機(jī)圖形學(xué)領(lǐng)域中廣泛采用的一種方法是:限制信號(hào)的帶寬。也就是說既然無法提高分辨率,我們可以將信號(hào)中無法還原的高頻部分去掉以達(dá)到“反走樣”的目的。這樣線段就不再有明顯尖銳的邊界了,相反,線段的邊界處將變得模糊,這種將邊界模糊的過程我們稱之為“過濾”。我們可以讓信號(hào)通過一個(gè)低通過濾器,來過濾掉信號(hào)中的高頻部分,以達(dá)到過濾的效果,這樣的過濾器有很多,可以是簡(jiǎn)單的線性過濾器也可以是稍微復(fù)雜一點(diǎn)的盒狀過濾器或高斯過濾器。本文將以高斯過濾器為例,為大家介紹整個(gè)過濾的過程。 ? ? ??
? ? ? ?圖4演示了高斯過濾器對(duì)一個(gè)2D信號(hào)進(jìn)行過濾操作的整個(gè)過程,首先圖4(a)表示未處理的線段信號(hào),其中x和y軸表示線段所處平面坐標(biāo)系,z軸表示圖像信號(hào)的強(qiáng)度,可以認(rèn)為是RGBA顏色中的alpha值。其中左半部分z=1表示位于線段內(nèi)部,右半部份z=0表示位于線段外部,這里z=0和z=1邊界處是不連續(xù)的。圖4(b)所表示的是一個(gè)高斯地同過濾器,將它與圖4(a)中的某一段信號(hào)做卷積后就得到了圖4(c)中的效果。卷積在這里等效于求出過濾器與信號(hào)相交部分的體積,圖4(d)就是將所有信號(hào)與過濾器卷積后得到的最終過濾效果。
? ? ? ?從圖5可以看到:經(jīng)過過濾后的線段邊界將不再是一段不連續(xù)的階梯函數(shù),而是一段連續(xù)的平滑曲線。
預(yù)處理
? ? ? 如圖6所示,在預(yù)處理過程中,我們將半徑為R的過濾器和寬度為W的線段進(jìn)行卷積,所得到的強(qiáng)度值根據(jù)過濾器的位置變化而變化。當(dāng)過濾器剛好位于直線上(圖6a)時(shí),我們得到的強(qiáng)度值最大,因?yàn)榇藭r(shí)過濾器與直線重疊部分最多,(在圖4所示坐標(biāo)系中)重疊部分的體積也就越大。相反的,當(dāng)過濾器位于距離直線w/2+R的位置(圖6b)時(shí),卷積所得強(qiáng)度值最小,因?yàn)榇藭r(shí)過濾器與直線沒有重疊。而在過濾器從距離為0移動(dòng)到w/2+R處的過程中,強(qiáng)度值在慢慢變小。
? ? ? ?然而,我們并不希望計(jì)算量會(huì)隨線段寬度變化,我們希望我們的渲染過程的效率是穩(wěn)定的,因此,我們需要一張固定寬度的查找表。通過實(shí)踐發(fā)現(xiàn),一張32個(gè)強(qiáng)度值的查找表已經(jīng)足夠應(yīng)付任意寬度的直線了(圖7),如果覺得這樣不夠精細(xì),你還可以使用64個(gè)強(qiáng)度值的查找表,因?yàn)閷?duì)于GPU來說,處理一個(gè)32或64元素的1D紋理實(shí)在是小菜一碟。
? ? ? 如圖8中的代碼片段所示,生成這樣一個(gè)紋理只需按照設(shè)定的強(qiáng)度值數(shù)量利用過濾器計(jì)算出相應(yīng)數(shù)量的強(qiáng)度值就可以了。唯一需要注意的是這個(gè)紋理是關(guān)于直線中心對(duì)稱的,以及紋理參數(shù)中縮放過濾參數(shù)要設(shè)置為GL_NEAREST。
運(yùn)行時(shí)
? ? ? 預(yù)處理只需要在CPU中運(yùn)行一次,而當(dāng)我們將過濾后的紋理完成后,我們的預(yù)處理工作就算告一段落,接下來就可以進(jìn)行渲染了。渲染時(shí),我們需要進(jìn)行兩種計(jì)算,一種是在CPU中的線段相關(guān)參數(shù)的計(jì)算,另一種計(jì)算GPU的著色器中進(jìn)行的,主要是利用CPU提供的參數(shù)在頂點(diǎn)著色器和片段著色器中計(jì)算出真正的位置和顏色。
? ? ? 計(jì)算矩形頂點(diǎn)的坐標(biāo)看起來也不是一件很困難的事情,只需要將線段的兩個(gè)頂點(diǎn)向兩側(cè)分別平移w/2距離就可以得到(圖9),而線段的平移方向正好是xy平面上垂直于該線段的法向量方向,因此我們只需要計(jì)算這個(gè)法向量即可。
? ? ? 有了法向量后,頂點(diǎn)著色器中只需將頂點(diǎn)和法向量相乘,再乘上w/2就可以得到平移后的頂點(diǎn)位置,最后再與線段的模型視圖投影矩陣相乘,計(jì)算出最終的頂點(diǎn)位置(圖10)。
? ? ? 片段著色器只需對(duì)紋理進(jìn)行一次采樣得到強(qiáng)度值再與線段顏色進(jìn)行一次疊加就行了,這樣就能得到一條任意顏色的線段。
最終效果
? ? ? 并且對(duì)它進(jìn)行拉伸或者旋轉(zhuǎn)都不會(huì)產(chǎn)生新的走樣(圖13)。
? ? ? 通過圖13的對(duì)比我們可以清楚地看出,經(jīng)過預(yù)過濾反走樣處理的線段相比普通線段和硬件反走樣處理的線段鋸齒感明顯要弱了許多。這種處理方式所需的存儲(chǔ)空間代價(jià)僅僅是額外的兩個(gè)頂點(diǎn)和一個(gè)寬度64的一維紋理,而運(yùn)行時(shí)處理上也只是增加了一次法向量的計(jì)算,可以稱得上是簡(jiǎn)單高效。
從左至右依次為 普通線段 默認(rèn)硬件反走樣 盒狀過濾器 高斯過濾器
? ? ? 最后,希望這個(gè)方法能夠?qū)Υ蠹姨幚?D線段抗鋸齒問題能夠有所幫助,如果對(duì)有對(duì)這方面研究感興趣的朋友,歡迎加入我們進(jìn)行討論(QQ群:280689979)。