天天看點

圖形管線之旅 Part5

原文:《A trip through the Graphics Pipeline 2011》

翻譯:往昔之劍

轉載請注明出處

在上一篇關于紋理采樣器之後,我們現在回到了3D前端。那執行完了頂點着色,現在就可以實際的渲染東西了,對嗎?可惜,還不行。你看,在我們實際開始光栅化圖元之前,仍然還有很多事要做。是以在本篇裡我們不會看到任何光栅化内容——還得等到下次講。

圖元組裝

當我們離開頂點管線時,我們從shader單元裡得到了一塊着色過的頂點,這塊頂點中包含了一些完整的圖元——我們不會讓三角形,直線或片被分割到多個塊裡。這很重要,因為這意味着我們可以真正的單獨處理每一個塊,并且不需要緩沖多個shader輸出塊——雖然可以緩沖,但沒必要這麼做。

下一步是組裝單個圖元的所有頂點。如果圖元碰巧是一個點,就隻需要讀取确切的頂點并傳遞它。如果是直線,要讀取兩個頂點。如果是三角形,要三個頂點。并以此類推大量控制點的片。

簡而言之,這裡做的工作是收集頂點。我們既可以通過讀取原始的索引緩沖來收集頂點并存一份頂點索引的拷貝——緩存周圍的位置映射,或者我們也可以存儲随同着色的頂點完全展開的圖元的索引。這将花費一些空間用于存儲輸出緩沖,但是在這裡我們就不必再讀取索引了。用哪種方式都可以。

現在我們已經展開了組成圖元的所有頂點。換言之,我們現在有完整的三角形了,而不僅僅是一堆頂點。那我們已經可以光栅化它們了嗎?還不行。

視口剔除與裁剪

我猜我們應該先執行這個,對不?這是管線中,你應該最感興趣的部分。我不打算在這裡解釋三角形裁剪,你可以在任何一本計算機圖形課本上查到,盡管通常都是一大堆内容看上去很可怕。如果你想詳細了解,就用Jim Blinn的(本書13章),雖然你可能會想通過傳[0,w]的裁剪空間來替代,如果沒别的方法就别搞混了。

裁剪簡而言之,即:在齊次裁剪空間裡,從vertex shader中傳回頂點的位置。選用裁剪空間是為了使方程描述視錐體盡可能的簡單;在D3D中,是 

以及 

;注意所有最終方程實際上排除了齊次點(0,0,0,0),這是一種退化情況。

我們首先需要找出三角形是部分的,還是完全的在裁剪平面之外。這可以用 Cohen-Sutherland直線裁剪算法高效執行。為每個頂點(例如,可以在頂點着色的時候計算,并随位置一起存儲)計算輸出碼out-code(或裁剪碼clip-code)。然後,對于每個圖元,裁剪碼clip-code的按位與運算将會告訴所有的視錐平面所有圖元頂點在錯誤的一側(意味着圖元完全的在視錐之外,就可以被抛棄了),并且裁剪碼clip-code的按位或運算将會告訴視錐平面需要再次裁剪圖元。裁剪碼隻是硬體部分的很簡單的東西。

另外,shader還可以生成一組“剔除距離(cull distances)”(如果所有頂點中任何一個剔除距離小于0,該三角形就會被丢棄),和一組“裁剪距離(clip distances)”(定義了額外的裁剪平面)。這些還用來參考圖元的rejection/clip testing。

在實際的裁剪過程中,可以采用兩種形式:我們既可以使用多邊形裁剪算法(會添加額外的頂點和三角形),也可以添加額外的裁剪邊方程到光栅器裡(如果聽不懂沒關系,等到下個部分講光栅化,就了解了)。後者方式更好,完全不需要實際的多邊形裁剪器,但是我們得需要能夠規範化32位浮點值來作為有效的頂點坐标;可能會有技巧建構快速的硬體光栅器來這樣做,但是似乎很困難。是以我認為有一個實際的裁剪器,包含了所有相關的東西(生成額外的三角形等)。這很麻煩,還很珍貴(比你想的要珍貴,我馬上就會講到),是以它不是個大問題。不确定是否特殊的硬體也是,或者執行實際的裁剪;專用的裁剪單元的大小和需要多少,取決于在這個階段派發一個新的頂點着色負載是否合适。我不知道這些問題的答案,但是至少在性能方面,它不是很重要:實際上不會頻繁地“真的”裁剪。因為我們會用到保護帶裁剪(guard-band clipping)。

保護帶裁剪(guard-band clipping)

這個名字不是很恰當;這不是一個神奇的裁剪方法。事實上,恰恰相反:直截了當的不做裁剪:)

底層思路非常簡單:在左,右,上和下裁剪面之外部分的大多數圖元完全不需要裁剪。靠GPU來光栅化三角形,實際上的做法是掃描全屏區域(更準确的說,是裁剪區域scissor rect)并詢問每個像素:“這個像素被目前三角形覆寫了嗎?”(實際上這有點複雜,并且有更高效的方式,但這是正常思路)。并且這同樣适用于三角形完全在視口内的情況。隻要我們的三角形覆寫測試(coverage test)是可靠的,我們就完全不需要裁剪靠近左,右,上和下平面的部分。

這個測試通常都是用固定精度的整數運算。最後,一步步的得到一個三角形頂點,将會整數溢出并且得到錯誤的結果。我覺得由光栅器生成像素而不是在三角形中生成,這點讓人感覺很不爽非常,這應該是不合理的。硬體實際上是違反了規範的。

針對這個問題有兩個解決辦法:首先是確定絕對不會進行三角形測試。如果真正做到了這點,那麼就不用裁剪四個平面了。這就是所謂的“無限保護帶”,保護帶實際上是無限的。解決方案二是最後裁剪三角形,僅當他們在安全區域(光栅器計算不會溢出的區域)之外時。例如,光栅器有足夠的内部位來處理整數三角形坐标:

, 

(注意我這裡都用大寫的X和Y來表示螢幕空間的位置)。仍然用正常的視平面做視口裁剪測試,但實際上在投影和視口變換之後隻是裁剪了指定的保護帶裁剪平面,結果的坐标都在安全區域裡。如圖所示:

中間的小塊藍邊白色矩形表示我們的視口,而大塊的橙色區域就是保護帶(guard band)。圖中的視口看起來貌似很小,但其實我還畫大了呢,好讓你可以看到所有東西!在保護帶裁剪範圍-32768~32768裡,視口大約是5500個像素寬度,這裡可以容納下一些很大的三角形。這些三角形表示了某些重要情況。黃色三角形是最常見的——延伸到視口之外但沒出保護帶。這種可以通過測試,沒必要進一步處理。綠色三角形在保護帶以内視口區域以外,是以它會被視口裁剪掉不會通過測試。藍色三角形延伸到了保護帶裁剪區域之外,需要被裁剪,但它完全在視口區域之外,會被視口裁剪拒絕。最後的紫色三角形既延伸到了視口區域之内又延伸到了保護帶之外,就需要被裁剪了。

如你所見,這幾類三角形需要被四個側面裁剪都是比較極端的情況。正如所說的,不要擔心,這都是很罕見的情況。

題外話:正确的裁剪

如果你熟悉算法,這塊就不是很難。但其中細節總是很神奇。三角形裁剪器實際上不得不遵守一些潛規則。如果破壞了這些規則,共用一個邊的鄰接三角形就會産生裂縫。這是不允許的。

  • 視錐内的頂點位置必須被裁剪器儲存為比特率(bit-exact)。
  • 裁剪一個平面中的邊AB必須與裁剪邊BA(方向相反)産生相同的結果(這可以保證數學上完全對稱,或者可以保證總是裁剪相同方向上的邊)。
  • 對多個平面裁剪的圖元必須按相同的順序對平面裁剪(或者一次對所有平面裁剪)。
  • 如果用到了保護帶,必須對保護帶平面裁剪。如果真的需要剔除,就不能用保護帶了,得對原始的視口平面裁剪。不這麼做的話會産生裂縫。

讨厭的遠近平面

好吧,雖然對于4側平面有很好解決方案,但是對于近和遠平面呢?尤其近平面是很麻煩的,因為所有。那我們該怎麼做呢?用z保護帶嗎?但是要怎麼工作呢——我們實際上并沒有按z軸來光栅化!事實上隻是在三角形上做插值!

另外,這隻是三角形的插值。實際上插值Z的話,z-near test(Z<0)是很簡單的——隻是 符号位。而z-far(Z>1)要額外比較(這裡我使用Z,而不是z,表示螢幕坐标或投影之後的坐标)。但是我們還要進行逐像素的Z比較(Z test),是以這不是很大的開銷。視情況而定,但這樣執行z裁剪是一個可選項。如果你想要支援像NVidias的“depth clamp”OpenGL擴充的話,就需要跳過z-near/z-far裁剪。實際上,這個擴充很好的暗示了他們是這樣做的,至少用過一段時間。

對于w>0的裁剪。也能擺脫它嗎?答案是當然,比如齊次坐标的光栅化算法( http://www.cs.unc.edu/~olano/papers/2dh-tri/)。 我不确定硬體是是否這樣用的。這個方法不錯,不過很難符合D3D11的光栅化規則。也可能用一些我不了解的技巧。以上就是裁剪相關内容。

投影和視口變換

投影隻需要将x,y和z坐标除以w(除非你使用了齊次的光栅器,否則實際上是并不投影,下面将忽略這種可能性),就得到了在-1到1之間的NDC(規範化的裝置坐标Normalized device coordinates)。然後用視口變換将投影的x和y映射到像素坐标(将稱為X和Y)以及投影的z映射到[0,1](将稱為Z),這樣在z-near平面Z=0并且在z-far平面Z=1。

我們還要對齊像素到子像素格上的小數坐标。從D3D11開始,硬體需要精确的8位三角形坐标的子像素精度。這個對齊會把一些非常窄的碎片(這些碎片會導緻問題)變成退化三角形(不需要被渲染)。

背面和其它三角形剔除

當我們擁有了所有頂點的X和Y,我們就可以叉乘邊向量來計算标記的三角形面積。如果面積是負值,三角形就是逆時針的(在這裡負面積對應逆時針,因為我們正處于像素坐标空間,在D3D的像素空間中y向下增加而不是向上增加,是以符号是相反的)。如果面積是正值,就是順時針。如果是0,就是退化三角形,不覆寫任何像素,那麼它就可以被安全的剔除了。我們知道了三角形朝向就可以進行背面裁剪了(開啟的情況下)。

我們現在快準備好光栅化了。實際上我們還得先設定好三角形。但這塊還需要光栅化如何執行的知識,是以我會把放到下一篇再講。

結束語

我跳過并簡化了一部分内容,實際情況要更複雜:比如,我假設你隻是使用正常的齊次裁剪算法。通常是這樣——但你可以用一些vertex shader屬性标記作為使用螢幕空間線性插值來替代透視矯正插值。目前,正常齊次裁剪都是透視矯正插值;在使用螢幕空間線性屬性的時,你實際上需要執行一些額外的工作來不進行透視矯正:)

有很多光栅化算法(比如我提過的Olanos 2DH方法)可以讓你跳過幾乎所有的裁剪,但如前所述,D3D11對于三角形光栅器需求很嚴格,所有沒有很多硬體實作的餘地;我不确定那些方法是否符合規範(有很多細節下次會介紹)。我用的方法不是很先進,在光栅器中逐像素處理上用到少量的數學運算。如果你知道更好的解決方案,請在評論中告之。

最後,三角形剔除我這裡描述的是最基本情況;例如,一類三角形在光栅化時會生成零個像素遠大于零面積的三角形,如果你可以足夠快的查找到它,你就可以立即丢棄掉這個三角形并且不需要經過三角形設定。最後說一點,在三角形設定之前以最低限度的光栅化進行剔除——找到其它方法來早期拒絕(early-reject)三角形是相當值得的。