歷史上,硬件圖加速器出現於管線的末端,首先是運行三角面掃描線的光珊化。緊接着的下一代硬件沿管線上溯,到一些更上級的層次,一些應用程序階段的算法亦被囊括在硬件加速器的范圍內。致力於使用硬件唯一的好處是其超過軟件實現的速度,速度是關鍵的。
在過去的十年,圖形硬件經歷了一個不可想象變革。第一塊包括硬件頂點處理的消費性圖形芯片(NIDIA的Geforce256)在1999年問世。NVIDIA杜撰了圖形處理單元(GPU)這一術語去區分Geforce256和之前其它只處理光珊化的芯片,(and it stuck)。在此后的幾年,GPU從原來復雜的可配置固定功能管線到高度可編程的實現,就像白紙一樣,開發者可在其上實現他們自己算法。各種可編程着色器是GPU控制的主要手段。頂點着色器能實現多種運行在每個頂點上的運算(包括坐標變換和變形)。相似的,像素着色器處理每個像素個體,允許在逐素上使用復雜的方程求值。幾何着色器允許GPU在工作的間隙創建和銷毀幾何圖元(點,線,三角面)。計算得出的數值可寫入高數度緩存中作為頂點紋理數據重用。基於效率,管線的一部分仍然是可配置,而非可編程,但趨勢是朝向可編程性和靈活性發展。
圖3.1 GPU實現的渲染管線。圖根據使用者可控制的程度使用不同顏色標識。綠色的階段為完全可編程。黃色的階段為可配置而非可編程,例如裁剪階段是可選擇運行的裁剪和添加用戶制定的裁剪平面。藍色的階段的功能是完全固定。
3.1 GPU管線概述
GPU實現了第二章所描述的幾何和光珊化的概念管線。這些被分割成數個硬件實現的階段,這些階段支持不同程度的可配置性或可編程性的。圖3-1通過不同的顏色展示了其可編程性和可配置性。需要注意的是這些物理的階段划分相對第二章中的功能性階段有輕微的不同。
頂點着色器是一完全可編程的階段,通常用於實現“模型和視圖變換”,“頂點着色”和“投影”等功能性階段。幾何着色器則是可選的,完全可編程的階段,對圖元(點、線、三角面)的頂點進行操作。它可以用於每個圖元的着色操作,銷毀或創建新圖元。而裁剪,屏幕映射,三角形建立和三角形遍歷階段是固定功能階段,實現其同名的功能階段。像頂點和幾何着色器,像素着色器是一完全可編程且運行像素着色的功能階段。最后,融合階段處於完全可編程的着色器階段和其它階段的固定操作之間。雖然它不是可編程的,但它是高度可配置並且可在之上運行多種操作。當然,它實現了融合這一功能階段,負責修改顏色、深度、混合、模板及其它相關緩存。
隨着時間的過去,GPU管線由硬編碼的操作向越來越靈活和可編程操控的方向發展。可編程着色器階段的引入是管線進化的重要一步。下節將描述各種可編程階段共有的特點。
3.2 可編程着色器階段
現代的着色器階段(例如在Vista上支持Shaker Model 4.0, DirectX10及后面出現的)使用一個共同的着色器內核。這意味着頂點、像素和幾何着色共享同一個編程模型。我們在書里需要區分着色器公共核心(一個為應用程序員所見的功能說明)和統一着色器(一個與這個核心相吻合的GPU架構)。見18.4節。着色器公共核心是API;統一着色器是GPU的一特征。早期的GPU中的頂點和像素着色器並無太多共同的地方,且其沒有幾何着色器。但這個模型大多數的設計元素是共享使用了舊的硬件;對於大部分的修改,舊版本的設計不是太簡單就是缺少這個功能,而非根本的不同。所以現在我們反焦點放在Shader Model 4.0而將舊GPU着色器模型放在后面討論。
描述整個編程模型是已經超出本書的范圍,且有很多的文件、書籍和網頁已經做了這方面的工作。但很少關於這些文獻的良好的評論。着色器使用與C語法相似的着色語言如HLSL,Cg和GLSL等進行編程。這些代碼被編譯成獨立於計算機硬件的匯編語言,亦叫作中間語言。之前的着色器模型允許直接使用匯編編程,但DirectX10,這種語言的編程僅作為調試版本的輸出。這些匯編語言會在一單獨的步驟被轉換成機器碼,這通常在驅動程序中。這種設計能兼容不同的硬件實現。這種匯編語言可被視為定義了一以着色語言編譯器為目標的虛擬機。
這虛擬機是一個帶有各種寄存器和數據源的處理器,它使用一套指令進行編程。因為很多的圖形操作是使用短整形的向量(長度為4),處理器有四道的SIMD(單指令多數據)能力。每個寄存器包含四個獨立的數值。32位單精度浮點標量和向量是基本的數據類型;最近加入了對32位整形數的支持。浮點數何量通常包含了位置(XYZW),法線,矩陣行,顏色(rgba)和紋理坐標(uvwq)等數據。整形數通常用於計數器,索引或位掩碼。集合數據類型例如結構件,數組和矩陣等亦被支持。為了方便向量的使用,拌和技術(swizzling)——向量成員的復現,亦被支持。這是何量的單元可以被任意地重排或重復。相似地,掩蔽技術(masking),即使用所指定的向量單元,亦被支持。
繪制調用到圖形API去繪制一組圖元,因此引起圖形管線的執行。每個可編程着色器階段有兩種輸入:一致輸入(uniform input),其數值在整個繪制調用中保持不變(但在不同的繪制調用間是可變的);而可變輸入,對於每個頂點或像素着色器是不同的。紋理貼圖是一種特殊的一致輸入,在以前經常被認為是應用於表面的一幅彩色圖片,但現在也可以認為是一任意大小的數據。需要注意的是雖然着色器可以不同方式片理各種輸入,但其輸出是極度固定的。這是着色器與運行在通用處理器上的程序最顯注的區別。虛擬機的底層提供了用於不同輸入輸出的寄存器。一致輸入能被常量寄存器或常量緩存訪問,如此稱謂是因為它們的內容在一次繪制調用中保持不變。常量寄存器的數目遠遠大於可變的輸入輸出寄存器的數目。這是因為不同的頂點和像素輸入和輸出需要分別存儲,而一致輸入存儲一次然后在繪圖調用的所有頂點或像素中重用。虛擬機亦具有通用的臨時變量,是用於暫存空間。所有類型的寄存器可使用臨時寄存器中的整數值進行數組索引。着色器虛擬機的輸入輸出如圖3.2所示。
圖3.2 在Directx10下的公共着色器核心虛擬機的架構和寄存器的布置。每個資源的最大的數值在其旁邊顯示。三個使用斜杠分隔的數值分別指代其頂點上,幾何,像素着色器的最大值(從左到右)。
在圖形計算中常用的操作在當代的GPU上是高效地執行的。例如,最快的操作是標量和向量的乘法、加法和它們的混合,例如乘加和點積。另一些操作,例如倒數,平方根,正弦,余弦,指數和對數,相較而言會消耗更多,但仍然是相當快速的。紋理操作(見第六章)也是高效率的,但它們的運行表現受限於一些因素,例如花費時間在等待訪問資源的結果。着色語言通過操作符提供了這些普遍的操作(例如通過操作符*和+提供加法和乘法)。其余則通過內置函數提供,例如atan(),dot()log()及其它。內置函數亦處理更復雜情況,例如向量的單位化和反射,叉積,矩陣轉置和行列式等。
流程控制這個術語指的是使用分支指令改變代碼執行的流程。這些指令用於實現高層語言的“if”和“case”聲明語句,以及各種循環。着色器支持的兩種流程控制。靜態流程控制分支是基於一致輸入的值。這意味着流程控制在繪制調用中保持不變。靜態流程控制的主要好處在於允許同一個着色器應用在各種不同的情況(例如,光線數目的變化)。動態流程控制則是基於可變輸入的值。這比起靜態流程、控制的功能更為強大但亦耗費更多,特別是在着色器調用之間不規律地改變。在18.4.2所論及的,着色器每次都對若干的頂點或像素進行賦值。雖然在流程選擇中的if分支用於某些單元,而else分支用於其它,但對於所有單元所有的分支都會被賦值(而在每個單元中沒有使用的分支將被丟棄)。
着色器程序可在程序被裝載前離線編譯或可以運行時編譯。像其它任何的編譯器一樣,這有關於不同輸出文件和不同代碼優化水平的設置。編譯好的着色器被以文字字符串形式保存,通過驅動程序傳遞到GPU。
3.3 可編程着色器的衍化歷程
關於可編程着器框架的想法可追溯到1984所Cook的着色樹。一簡單的着色器和他對應的着色樹如圖3.3所示。Render Man着色語言就是建基於這種想法,它產生於80年代末而時至今天仍廣泛用於電影場景的渲染。在GPU原生地支持可編程語言之前,已有一些以多渲染路徑實現實時可編程着色操作的嘗試。1999年的《雷神之錘III:競技場》的腳本語言是第一個在這方面取得普遍的商業化成功的例子。在2000年,Peercy等人描述一個將Render Man着色器翻譯並運行在圖形硬件的多個渲染路徑的系統。他們發現GPU缺少兩項能使這個方法變得非常普遍的特性:像紋理座標那樣使用計算結果(附屬紋理貼圖讀取),以及支持擴展和精度的紋理和顏色緩沖數據類型。所提出的新型數據(在當時)是16位的浮點數。在那個時候,沒有任何商業化的GPU支持可編程着色,雖然大部分已經有高度可配置的管線。
圖3.3銅材質的着色樹,和它相應的着色語言程序
在2001年的上半年,NVIDIA的GeForce3是第一顆支持可編程頂點着色器的GPU,可通過DirectX8.0和Open GL擴展去使用。這些着色器使用類似匯編的語言進行編程並由驅動程序轉換為機器碼。像素着色器也包含在DirectX8.0中,但SM1.1中的像素着色器並未達到真正意義上的可編程性——十分有限的“編程”的支持,即通過驅動程序,與硬件的“寄存器組合器”連接,去轉換紋理貼圖的混合狀態。這些程序不僅在長度上有限制(小於等於12個指令),而且缺少兩個Peercy等人指出的兩個關於可編程性極為關鍵的元素(附屬紋理讀取和浮點數據)。
這個時期的着色器並不允許流程控制(分支),因此條件句可以以通過計算所有的條件然后選擇或對結果進行插值的方式進行模擬。DirectX定義了着色器模型這樣的一個概念去區分擁有不同着色器編程能力的硬件。GeForce3支持頂點着色器模型1.1和像素着色器模型1.1(着色器模型1.0針對的是硬件故並未公布出來)。在2001年,GPU在通用的像素着色器模型上取得進步。DirectX8.1增加了像素着色器模型1.2和1.4(兩者意味着不同的硬件),它們擴展像素着色器的能力,增加了額外的指令和對附屬紋理讀取更綜合的支持。
2002年,DirectX9.0對外公布,其包含着色器模型2.0(並擴展到2.X版本)。這個模型是真正的可編程頂點着色器及像素着色器。相同的功能出現在OpenGL的各種擴展庫中。支持任意的附屬紋理讀取和16位浮點數儲存,最終完成了所有Peercy等人在2000年提出的一系列需求。有限的着色器資源例如指令,紋理和寄存器有所增加,使得着色器有能力處理更復雜的效果。增加了流程控制。增長中的着色器代碼長度和復雜度使得匯編編程模型變復越來越笨拙和難以處理。幸好,DirectX9.0亦包括了一種新的着色器編程語言,叫HLSL(變級着色語言),HLSL由Microsoft與Nvidia合作研發的,Nvidia亦公開發布了其另一種跨平台的變體,叫Cg。與此同時,OpenGL架構檢查委員會(Architecture Review Board)公布與之類似OpenGL版本的語言,叫GLSL(亦被稱為GLslang)。這些語言受到了C編程語言的語法和設計理念以及Render Man着色語言的重大影響。
Shader Model 3.0在2004年被引入並逐步改進,將可選的特性轉化為模型的需求,更進一步的是增加資源的限值和對紋理讀取和頂點着色的有限支持。當新的一代游戲主機在2005年下半年(微軟的Xbox360)和2006年(索尼的PS3)被引入時,它們都配置了支持Shader Model 3.0的GPU。固定功能管線並非完全廢除:2006年問世的任天堂的Wii游戲主機則使用了固定功能GPU,但幾乎可以肯定的是這是最后一款這種類型的主機了,因為甚至移動設備,例如手機亦可使用可編程的着色器了。
其它語言和着色器開發環境也出現了。例如Sh語言允許通過C++庫生成和混合GPU着色器。這開源項目可在多個平台上運行。在另一方面,一些可視化編程工具被引入,使得藝術家們(其中大部分的人對類C語言編程不熟悉)可以設計着色器。這樣工具包含用於聯接預定義着色器代碼塊的可視圖形編輯器,以及將結果圖形轉換為着色語言(如HISL)的編譯器。圖3.4展示了一個這類型工具的截圖(melatal mill,包含在NVIDIA的FX Cowposer2中)。Mc Guire等人調研了可視着色器編程系統並提出了關於高層次,抽像可擴展性的概念。
圖3.4 一用於着色器設計的可視圖形系統。各種操作被封裝進左邊可選的功能盒中。當選中時,功能盒的可調整參數將展示在右邊。每個功能盒的輸入和輸出將其它聯接在一起,共同形成最終結果,即中間窗體右下角的那幅圖。(截圖來自mental image公司的“mental mill”)
2007年,業界又向可編程方向邁出又一大步。着色器模型4.0(包括DirectX10.0和通過擴展的OpenGL)引入了一些主要特征,例如幾何着色器和流輸出。
着色器模型4.0包括了一個針對所有着色器(頂點、像素和幾何)的統一的編程模型,其公共着色器核心在關面已經描述過了。資源上限進一步增加,並加入了對整形數據類型的支持(包括位運算)。值得注意的是着色器模型4.0僅支持高層語言着色器(DirectX的HLSL和OpenGL的GLSL)——沒有像之前的版本那樣,提供用戶可寫的匯編語言接口。
GPU制造商,微軟和OpenGL架構委員會一直不斷在提升和擴展可編程着色的能力。除了新版本的擴展API,新的編程模型,例如NVIDIA的CUDA和AMD的CTM以非圖形應用程序為目標。關於這個運行在GPU上的通用計算,在18.3.1節中有概括的介紹。
3.3.1 着色模型的比較
雖然這章的內容關注在着色模型4.0(寫作時的最新版本),開發者經常需要支持使用舊着色模型的硬件。因此,我們對一些最后的着色模型的能力進行一個簡單的比較:2.0(和它的擴展版本2.X),3.0和4.0。列出所有的不同點則超越本書的范圍;具體的信息可以MSDN和DirectX SDK中獲行。
我們在這里集中關注DirectX,因為其清楚明了的版本發布,相比這下OpenGL擴展庫的演化,一些是由OpenGL架構檢查委員會批准通過的,而另一些則是生產商指定的。這樣的擴展系統有一個優勢,就是來自指定的獨立硬件廠商(Independent Hardware Vendor)的最尖端的特性可以立刻使用。DirectX9以及更早的支持獨立硬件廠商的變體版本通過檢查“能力比特”去判斷GPU是否支持該特性。到了DirectX10,微軟急速地從這些做法轉向所有獨立硬件廠商都必須支持的標准模型。雖然這里集中關注DirectX,但接下來的討論亦適用於OpenGL,因為相關聯的底層GPU在同一時期都具有相同特性。
表3.1比較了各種着色模型的能力。在這個表格中,“VS”代表頂點着色器和“PS”代表像素着色器(着色模型4.0 引入了幾何着色器,其能力與頂點着色相似)。如果沒有出現“VS”和“PS”,那么該行適用於頂點和像素着色器。因為虛擬機是四路SIMD,每個寄存器可以儲存一到四個獨立的值。“指令槽位”指代着色器可包含的最大指令數目。“最大執行步長”指代執行指令的最大數目,考慮上分支和循環語句。“臨時寄存器”展示了用於儲存中間結果的通用寄存器的數目。“常量寄存器”指示了可輸入着色器的常量數目。“流程控制,判斷”涉及通過分枝指令和判斷(條件執行或跳過命令等的能力)去計算條件表達式和執行循環。“紋理貼圖”展示了可被着色器訪問(每張紋理貼圖勻可被多次訪問)的distinct紋理(見第六章)。“整數支持”指代其使用位運算操作符和整數算術進行整數類型運算的能力。“VS輸入寄存器”展示可被頂點着色器訪問的各種輸入寄存器的數目。“插值寄存器”是頂點着色器的輸出寄存器和像素着色器的輸入寄存器。他們被如此稱謂因為在傳送到像素着色器前頂點着色器的輸出值在整個三角形片上進行插值。最后,“PS輸出寄存器”展示了可輸出像素着色器的寄存器數目——每個均與不同的緩沖區或繪制目標相關聯。
SM 2.0/2.X |
SM 3.0 |
SM 4.0 |
備注 |
|
引入版本 |
DX 9.0, 2002 |
DX 9.0c, 2004 |
DX 10, 2007 |
a. 最小需求量(或許可使用更多); b. 最少32紋理和64算術指令; c. 14個常量緩存可供使用(+2私有,預留給微軟或硬件廠商),每個都可以包含最大4096個常量; d. 頂點着色器須支持靜態流程控制(基於常量); e. 對於頂點紋理,SM3.0硬件通常的格式種類非常有限並且沒有過濾器; f. 高達128個紋理隊列,每個都能包含最多512個紋理; g. 不包括兩種顏色在有限精度和范圍的差值; h. 頂點着色器輸出16個插值器,而幾何着色器可以擴展至32個 |
VS指令槽位 |
256 |
≥512 a |
4096 |
|
VS最大執行步長 |
65536 |
65536 |
∞ |
|
PS指令槽位 |
≥96 b |
≥512 a |
≥65536 a |
|
PS最大執行步長 |
≥96 b |
65536 |
∞ |
|
臨時寄存器 |
≥12 a |
32 |
4096 |
|
VS常量寄存器 |
≥256 a |
≥256 a |
14×4096 c |
|
PS常量寄存器 |
32 |
224 |
14×4096 c |
|
流程控制,判斷 |
Optional d |
Yes |
Yes |
|
VS紋理貼圖 |
None |
4 e |
128×512 f |
|
PS紋理貼圖 |
16 |
16 |
128×512 f |
|
整數支持 |
No |
No |
Yes |
|
VS輸入寄存器 |
16 |
16 |
16 |
|
插值寄存器 |
8 g |
10 |
16 / 32 h |
|
PS輸出寄存器 |
4 |
4 |
8 |
表3.1 着色器能力,按DirectX着色模型版本列出。
3.4 頂點着色器
頂點着色器是圖3.2中展示的功能管線的第一階段。因為這是所有圖形處理的第一個階段,在此階段前的數據處理是毫無意義的。這在DirectX中稱為輸入裝配,許多的數據流組合在一起形成一個頂點和圖元組成的集合,發送到管線中。例如,一對像可由一位置坐標數組和一個顏色數組所代表。輸入裝配會基於位置和顏色生成該對像的三角形面(或點、線)。另一個對像可以由同一個位置數組(隨不同的模型轉換矩陣)和不同的顏色數組去代表。數據表現的細節將在12.4.5節討論。在輸入裝配中亦支持實例化。這允許通過一個單獨的繪制調用,對一個對像的不同實例使用不同的數據進行多次繪制。關於實例化的使用參見15.4.2節。DirectX10的輸入裝配亦使用標識數標記了每個實例、圖元和頂點,使得后續的任何着色器階段都可以訪問。對於較早期的着色器模型,這種數據需要明確地加入模型。
一個三角片網格是由一個頂點集合和描述頂點所屬三角形的付加信息所組成。頂點着色器是處理三角形網格的第一個階段。頂點着色器未能得到關於三角形所組成什么的信息;像名字所暗示的,它只處理輸入進來的頂點。從總體上說,頂點着色器提供一個修改、創建或忽略那些與每個多邊形頂點的值(例如其顏色,法向量,紋理坐標和位置)的方法。通常頂點着色程序一般將頂點從模型空間轉換到齊次裁空間;一個頂點着色器至少也必須輸出它的位置。
這個功能第一次在2001年的DirectX8中引入。因為它是管線的第一個階段而且調用的頻率不高,它可以實現在GPU或CPU上,然后將數據發送到GPU做光柵處理。這使得由舊硬件向新硬件的轉換僅是速度的問題,而非功能性的問題。現在所有的GPU產品都支持頂點着色器。
頂點着色器本身與3.2節中描述的公共核心虛擬機十分相似。每個傳入的頂點會被頂點着色器程序處理,然后輸出許多在三角形或線上插值得出的數值。頂點着色器既不能創造出不能消毀頂點。因為每個頂點是被獨立處理的,任何數目的着色器處理器在GPU上均可並行處理輸入的頂點流。
下面列出解釋頂點着色器效果的章節,例如陰影體的創建,用於動畫連接的頂點混合和輪廓的渲染。其它關於頂點着色器的用法包括:
·棱鏡效果,使得屏幕出現魚眼,水下或其它扭曲效果。
·通過僅一次的網格創建並由頂點着色器變形定義對象。
·對像的扭曲,彎曲和錐化操作。
·程序化的變形,例如旗幟、服裝或水的移動。
·圖元創建,by sending degenerate meshes down the pipeline and having these be given an area as needed. .這個功能在新的GPU中由幾何着色器所到代。
·頁的卷曲,熱天的霧氣,水的漣漪以及其特效可以通過將整個幀緩存的內容當做屏幕的紋理與正在進行程序化變形的網對齊。
·頂點紋理獲取(在SM3.0之后可獲取)可用於將紋理貼圖應用在頂點網格,使得海洋表面和地形高度場的應用開銷不再昂貴。
圖3.5展示了一些使用頂點着色器的變形效果。
圖3.5在左邊,是一個正常的茶壺。頂點着色器運行一簡單的剪切操作產生了中間的圖片。在右邊,噪聲函數產生一場扭曲了模型。(圖片由FX Composer 2產生)
頂點着色器的輸出數據可以有多種不同的使用方式。對於每個實例的三角形通常的路徑是生成和光珊化,並將每個獨立的像素片段發送到像素着色程序去繼續處理。在着色模型4.0引入后,數據亦可發送到幾何着色器中,以流輸出,或兩種楷可。這些選項是下一節的內容。
3.5 幾何着色器
2006年末發布的DirectX10將幾何着色器加入了硬件加速圖形管線。在管線中,它緊隨頂點着色器之后,並且是可選。它是着色模型4.0的一部分,但在之前的着色模型並不存在。
幾何着色器的輸入是一單獨的對像和對像所關聯的頂點。對像通常是一網格中的三角形,一直線的線段或簡單的一個點。此外,擴展的圖元可以被幾何着色器定義和處理。特別的是,三個額外的在三角形外面的頂被傳入,並且兩個相鄰的在多邊形上的頂亦被使用。見圖3.6。
圖3.6 幾何着色器程序的輸入是一個單獨的類型:點,線段,三角形。兩個最右邊的圖元,包括與線和三角形對象相鄰的頂點也可被使用。
幾何着色器處理這些圖元並輸出零個或更多的圖元。輸出的形式為點,多邊形和三角形條帶。例如一次單獨調用幾何着色程序的輸出可以輸出多個三角形條帶。同樣重要的,幾何着色器亦可生成沒有輸出的結果。通過這個方法,網格可以被選擇性地通過編輯頂點,增加新圖元和移除其它單元等方法修改。
幾何着色器程序輸入一類對象然后輸出另一類對像,這兩類對像並不需要相匹配。例如,輸入可以是三角形而它們的形心可以以點的形式,對應每一個三角形地輸出。即便輸入和輸出的對像類型是相匹配的,其每個頂點所攜帶的數據亦有可能被忽略或擴展。例如,三角形平面的法向量可以被計算,並加入到每個頂點數據中。與頂點着色器相似的是,幾何着色器必須將每一個頂點輸出到齊次裁剪空間的位置上。
幾何着色器保證其輸出的結果圖元的順序與輸入的是一樣的。這影響到其運行效率,因為許多的着色器單元是並行運行的,其結果必須保存並排序。為了兼顧功能和效率,着色模型4.0中存在一個每次執行只能生成1024個32位數值的限制。所以以單個葉子作為輸入去生成成千上萬的灌木叢葉子是不可行,也不推薦的幾何着色器使用方式。使用曲面細分將簡單的平面細分為更精細的三角形網格在這里也是不被推薦的。這個階段更多是關於以程序方式修改輸入數據或進行數量有限的復制,而非大量地復制或放大細化它。例如,一個使用是生成六個經座標變換的數據副本似便同時渲染六個面的立方體貼圖,詳見8.4.3節。附加的算法可利用幾何着色器實現包括從點數據生成各種粒子,沿輪廓擠壓出鰭用於毛發渲染,以及用於陰影算法的對象邊界查找。圖3.7有更多這方面的應用。這些和其它的應用將在書本剩余部分討論
圖3.7 一些幾何着色器的應用。在左邊,元球的等值面曲面細分運行在幾何着色器上。在中間,線段細分的分形使用了幾何着色器和流輸出,並且幾何着色器生成用於展示的廣告板。在右邊,布料模擬使用了頂點和幾何着色器的流輸出。(圖片來自NVIDIA SDK 10的例子)
3.5.1 流輸出
GPU管線的標准使用方式是發送數據到頂點着色器,然后對結果的三角形進行光柵化並在像素着色器中處理它們。數據總是穿過整個管線而中間結果不能被訪問。流輸出是在着色模型4.0中引入。在頂點經過頂點着色器的處理后(及選擇的幾何着色器),除發送光柵階段外,可以輸出到流,也就是一個有序數組。實際上,光柵化可以整個被關閉,而管線可被當作非圖形的流處理器被應用,數據以這種方式處理可以通過管道回傳,那么可以迭代地處理。這種操作對模擬流動的水體或粒子效果特別有用,將在10.7節中討論。
3.6 像素着色器
在頂點和幾何着色器運行完他們的操作后,圖元像上一章所說明的那樣被裁剪並設置(set up)為光柵化准備。管線這部分的處理步驟是相對固定的,不可編程的。每個三角形遍歷而頂點上的數值將在三角形上插着。像素着色器是另外一個可編程階段。在OpenGL,這個階段稱為片段着色器(fragment shader),一個某程度上更好的名字。其思想是一個三角形完全或部分覆蓋每個像素單元,而材質繪制成透明或不透明的。光柵器生成一些數據(數據在一定程度上描述三角形如何覆蓋像素單元),而不會直接影響像素所儲存的顏色。在接下來的融合階段,這些片段數據用於修改像素里面儲存的數據。
頂點着色程序的輸出實際上變成了像素着色程序的輸入。在Shader Model 4.0中,總計16個向量(每個有四個值)可以由頂點着色器傳送到像素着色器。當使用幾何着色器后,他可以向像素着色器輸出32個向量。
像素着色器的追加輸入是在Shader Model 3.0中引入的。例如,三角形的那一面是可視的是通過增加輸入標志。這個知識對於在一條獨立路徑中對三角形的前后面渲染不同材質是十分重要。像素着色器也可以獲得片段的屏幕位置。
像素着色器的局限是它只能影響它所處理的片段。就是說,當像素着色器運行時,它不能將結果直接發送給相鄰的像素。恰恰相反,它使用經頂點插着而得的數據,以及任何儲存的常量和紋理數據,計算得出的結果僅會影響一個單獨的像素。然和,這個限制並不像它聽起來那么嚴重。鄰近的像素可以通過圖像處理技術最終被影響,在10.9節有相應描述。
一個像素着色器可以訪問鄰近像素(雖然不是直接)的例子是計算梯度或派生信息。像素着色器可以取任何計算值並計算其數值,該數值可以根據每個像素在屏幕上的x和y坐標變化。這對各種計算和紋理尋址都十分有用。這些梯度是十分重要的操作,例如過濾(詳見6.2.2)。大多數GPU通過一2×2或更多的分組處理像素實現這個功能。當像素着色器請求一個梯度值,相鄰像素之間的差值被返回。這樣實現的一個結果是梯度信息不能訪問,因為着色器被動態流程控制影響——所有在同一組的像素必須被同樣的指令處理。這是存在於離線渲染系統的基礎性的局限。訪問梯度信息是像素着色器獨有的能力,是其他着色器所不具備的。
像素着色程序通常設置用於最后融合階段的片段顏色。由光柵階段生成的深度值同樣可以被像素着色器修改。模板緩存的值是不可修改,而是直接送到融合階段。在SM2.0以及后續版本,像素着色器可以丟棄輸入的片段數據,也就是沒有輸出。這樣的操作會花費性能,因為通常由GPU運行的優化不能夠使用。詳見18.3.7節。一些操作,例如霧計算和α測試,在SM4.0中已經由屬於融合的操作轉到屬於像素着色器的計算。
現在的像素着色器可以做大量的處理。計算在一條渲染路徑上的多個數值的能力引出了多渲染目標(Multiple Render Targets)的概念。不是將像素着色器程序的計算結果保存到單獨的顏色緩存,而是可以為每個片段生成多個向量並保存到不同的緩存中。這些緩存必須是維度相同,並且一些架構還要求它們每一個都有相同的位深度(雖然根據需要為不同格式)。在表3.1中的PS輸出寄存器數目指的是單獨緩存訪問數量,即4或8。與可顯示的顏色緩存不同,對於任何附加目標都有一定的限制。例如,普遍不能運行反鋸齒。即使存在這些限制,MRT功能仍舊是為高效運行渲染算法提供強大幫助。如果若干中間結果圖像從同一數據集合中計算出來,只需要一個單獨的渲染路徑,而不是一個路徑對應每個輸出緩存。另一項與MRT相關的關鍵能力是以紋理的方式讀取結果圖片的能力。
3.7 融合階段
像2.4.4節討論的,融合階段是個體片段的深度和顏色(在像素着色器中生成)與幀緩存結合。這個階段發生模板緩存與深度緩存運算。在這個階段發生的另一個操作是顏色的混合,通常用於透明和影像合成(見5.7節)。
融合階段處於固定功能階段,例如裁剪,和完全可編程的着色器階段之間一個非常有趣的中間點。雖然它不是可編程的,但他的操作是高度可配置的。尤其是顏色混合可以運行大量不同的操作。最普遍的是乘法,加法和減法的結合,並涉及顏色值和α值,但其他操作也是可能的,例如最小和最大值,以及位的邏輯運算。DirectX 10增加了將兩種來自像素着色器的顏色與幀緩存的顏色混合的功能——叫做二重顏色混合(dual-color blending)。
如果使用了MRT功能,那么混合可以運行在多個緩存上。DirectX 10.1引入了在每個MRT緩存上運行不同混合操作的機制。在之前的版本,所有的緩存總是運行相同的混合操作(注意的是二重顏色混合與MRT不相容)。
3.8 效果
這趟管線之旅到現在為止主要關注各種可編程階段。頂點、幾何和像素着色器控制着這些階段,他們並不是存在於真空中。首先,一個單獨的着色器程序在孤立狀態下作用不大:頂點着色器程序將它的結果傳送給像素着色器。任何工作的完成都需要加載所有的程序。程序員必須確保頂點着色器的輸出與像素着色器的輸入相吻合。一個特定的渲染效果可能由多個着色器在數個渲染路徑上執行而產生。在着色器本身之外,狀態變量有時在一些特殊的配置也必須設置,以使得這些着色器能夠正常工作。例如,渲染器的狀態包括每次渲染Z緩存和模板緩存是否和如何使用,一片斷如何影響已存在的像素的值(例如替換,增加或混合)。
因為這些原因,不同的團體已經研發出不同的效果語言,例如HLSL FX,CgFX,以及COLLADA FX。一個效果文件通常試圖包含所有執行一種特定圖形算法的所有相關信息。它通常定義一些可被應用程序賦值的全局參數。例如,一個獨立的效果文件可以定義用於渲染擬真塑料材質的頂點着色器和像素着色器。它可以提供一些參數,例如塑料的顏色和粗糙程度,使得雖然使用同一個效果文件,但這些參數在每個模型渲染時都可以變化。
為了展示效果文件的受歡迎程度,我們將會由頭到尾看一遍一個來自NVIDIA FX Composer 2效果系統的簡潔例子。這個DirectX 9 HLSL效果文件實現了一個非常簡單的Gooch着色。Gooch着色中,表面的法線與光源位置被使用。如果法向量指向光源,表面被塗上暖和的色調;如果它指向背離光源,冷色調將被使用。兩者間的角度在這兩種用戶自定義顏色間插值。這種着色技術是一種非照片級真實的渲染方式,這是第11章的內容。圖3.8展示了這個效果運行的例子。
圖3.8 Gooch着色,從暖和的橙色變化為冷的藍色。
效果變量在效果文件的開始處定義。最初幾個變量是不能寧在一起的,這些參數都與因為效果而被自動跟蹤的攝像機位置有關:
float4x4 WorldXf : World;
float4x4 WorldITXf : WorldInverseTranspose;
float4x4 WvpXf : WorldViewProjection;
其語法是“類型 變量標示符 : 語意”。類型floa4x4是用於矩陣,名稱是用戶定義的,而語意是一個內置的名稱。正如名字所暗示的,WorldXf是模型到世界的轉換矩陣,WorldITXf是逆轉置矩陣,而WvpXf是模型到攝像機裁剪空間的轉換矩陣。這些帶有公認語意的數值將由應用程序提供,而不是顯示在用戶接口。
接下來,指定用戶定義變量:
float3 Lamp0Pos : Position < string Object = “PointLight0”; string UIName = “Lamp 0 Position”; string Space = “World”; > = {-0.5f, 2.0f, 0.15f}; float3 WarmColor < string UIName = “Gooch Warm Tone”; string UIWidget = “Color”; > = {-1.3f, 0.9f, 0.15f}; float3 CoolColor < string UIName = “Gooch Cool Tone”; string UIWidget = “Color”; > = {0.05f, 0.05f, 0.6f};
在尖括號“<>”里面還提供了一些附加的注釋,然后賦予默認值。注釋是特定於應用程序的,對效果或着色器編譯器沒有任何意義。這些注釋可以被應用程序查詢。在此情況下,注釋描述如何在用戶接口中顯示這些變量。
接下來定義着色器輸入輸出的數據結構:
struct appdata { float3 Position : POSITION; float3 Normal : NORMAL; }; struct vertexOutput { float4 HPosition : POSITION; float3 LightVec : TEXCOORD1; float3 WorldNormal : TEXCOORD2; };
appdata定義了在模型中每個頂點的數據並由此定義了頂點着色器程序的輸入數據。vertexOutput則是頂點着色器的產物和像素着色器所需的消耗。將“TEXCOORD*”用作輸出名字是工件進化的管道(The use of TEXCOORD* as the output names is an artifact of the evolution of the pipeline.)首先,多重紋理可以被附加到表面上,因此這些附加的數據字段被叫做紋理坐標。在現在中,這些字段持有任何從頂點着色器傳遞到像素着色器的數據。
接下來將定義各種着色器程序代碼的元素。我們只有一個頂點着色器程序:
vertexOutput std_VS(appdata IN){ vertexOutput OUT; float4 No = float4(IN.Normal, 0); OUT.WorldNormal = mul(No, WorldITXf).xyz; float4 Po = float4(IN.Position, 1); float4 Pw = mul(Po, WorldXf); OUT.LightVec = (Lamp0Pos - Pw.xyz); OUT.HPosition = mul(Po, WvpXf); return OUT; };
這個程序首先使用矩陣相乘計算了世界空間下表面的法線。轉換是下一章的主題,所以我們不會解析為什么這里使用了逆轉置矩陣。世界空間下的位置也是通過離屏轉換計算的。這個定位是從光源位置中減去從表面到光源的方向向量而得。最后,對象的位置被轉換到裁剪空間,用於光柵器。這是一個任何頂點着色器程序所必須的輸出。
在世界空間上給出光的方向和表面法線,像素着色器程序計算表面顏色:
float4 gooch_PS(vertexOutput IN) : COLOR { float3 Ln = normalize(IN.LightVec); float3 Nn = normalize(IN.WorldNormal); float ldn = dot(Ln, Nn); float mixer = 0.5 * (ldn + 1.0); float4 result = lerp(CoolColor, WarmColor, mixer); Return result; }
Ln向量是單位化的光照方向,而Nn是單位化的表面法向量。通過單位化,這兩個向量的點積ldn表示它們之間夾角的余弦。我們希望在冷和暖色調之間的差值中使用這個數值。函數lerp()產生一個介於0和1之間的值,0代表使用CoolColor,1代表WarmColor,而之間的數值由它們倆混合而成。因為角度余弦的取值范圍是[-1,1],其混合數值轉換至[0,1]的范圍。這個數值用於混合色調並產生帶有合適顏色的片段。這些着色器都是函數。一個效果文件可以由任意函數組成並且可以包含來自其他效果文件的公共函數。
一個路徑(pass)通常由定點和像素(和幾何)着色器組成,以及任何路徑需要的狀態設置。一個手法(technique)是一個包含一個或多個路徑,用於產生預期效果的集合。這個簡單的效果文件擁有一個有一條路徑的手法:
technique Gooch< string Script = “Pass=p0;”; > { pass p0 < string Script = “Draw=geometry;”; > { VertexShader = compile vs_2_0 std_VS(); PixelShader = compile ps_2_a gooch_PS(); ZEnable = true; ZWriteEnable = true; ZFunc = LessEqual; AlphaBlendEnable = false; } }
這些狀態設置使得Z緩存能夠以正常的方式(可讀可寫)被使用,並且通過深度小於或等於所存儲深度值的片段。Alpha混合則是關閉的,因為使用這種手法的模型是假定不透明的。這些規定意味着如果片段深度是與Z緩存所存數值相等或者接近,那么計算后的片段顏色將用於替換相應像素的顏色。用另一個說法,使用的是Z緩存的標准用法。
同一份效果文件可以存儲若干個手法。這些手法通常是同一個效果的變體,每個都以不同的着色器模型為目標(例如SM2.0與SM3.0)。效果可以是范圍極廣。圖3.9給出了現代可編程着色器管線強大功能的體驗。一個效果通常封裝了相關的手法。多種管理着色器集合的方法亦以被開發出來。
圖3.9 可編程着色器使各種各樣的材料和后處理效果變得可能。
我們已經在GPU旅程的終點。GPU還可以做的其他很多的,並且有很多使用和結合它功能的方法。相應的利用這些能力的理論和算法是本書的中心主題。有了這些基礎知識,關注點將轉移到給出對轉換和視覺呈現,這些管線關鍵單元的,深入的理解上。
進一步閱讀及資源
David Blythe的關於DirectX 10的論文對現代GPU管線和其設計背后原理有一個非常好的概述,以及相應的參考文獻。
單獨關於頂點和像素着色器的信息就可以很容易地寫出一本書。我們的最佳建議是投入其中:訪問ATI和NVIDIA開發者的網站獲取最新技術的信息。它們免費的FX Composer 2和RenderMonkey互動的着色器設計工具套件提供了一個優秀的方法去嘗試、修改着色器,去看是什么讓它們改變。Sander提供了一個固定功能管線在HLSL(用於具有SM2.0能力硬件)上的實現。
正式地學習着色器編程的方方面面需要做更多工作。《OpenGL Shading Language》將紅寶書遺漏的部分補上,描述了GLSL——OpenGL的可編程着色語言。為了適應HLSL,DirectX API的每個新版本都不斷進化;在它們SDK之上的相關的鏈接和書籍可見於本書的網站(http://www.realtimerendering.com)。O’Rorke的文章提供了關於效果的易讀的介紹和管理着色器的有效辦法。Gg語言提供了一層抽象,輸出到很多主要的API和平台,並且也為主要的建模和動畫程序提供了插件工具。Sh元程序語言是更為抽象,本質上作為一個C++庫那樣工作,去將相應的圖形代碼映射到GPU上。
對於更高級的着色器技術,可以以閱讀《GPU精粹》和《ShaderX》系列書籍作為開始。《游戲編程精粹》上也有一些相關的文章。DirectX SDK也有很多重要的着色器和算法例子。