Tabula Rasa中的延遲着色技術



Deferred Shading,看過《Gems2》 的應該都瞭解了。無論是Unreal3、Crysis還是星際2,都已經支持或者準備支持這個技術。

不過因爲國內這種環境,真正在項目中能用到的可能並不多,不知道這次星際2出來後,情況會不會有所變化。^_^

本文是對Gems2這篇文章的一個補充,小生在做此次外包的時候,由於需要,翻譯了這篇文章,不敢獨享,遂貼於此,望能拋磚引玉,願衆位前輩不吝賜教。 ^_^

 

 

 

Tabula Rasa中的延遲着色技術

作者:Rusty Koonce NCSoft

翻譯:noslopforever(天堂裏的死神)

本翻譯僅用於學術目的。

 

這篇文章是對GEMS2裏《Deferred Shading in S.T.A.L.K.E.R.》(中文譯名《S.T.A.L.K.E.R.中的延期着色》,原作者Oles Shishkovtsov)的一個補充。它是在我們耗時兩年時間、爲遊戲Tabula RasaRichard Garriott擔綱的MMORPG)完成的渲染引擎的基礎上形成的。GEMS2的這篇文章覆蓋了實現一個Deferred Shading引擎的基本原理,而我們將重點放在了基於Deferred Shading引擎的工作中時可能遇到的更高層面的問題、技術和解決方案上。

1 Introduction

在計算機圖形學的詞典裏,Shading表示“對受光物體的渲染”,這個渲染過程包括下面幾步:

1,  計算幾何多邊形(也就是Mesh)。

2,  決定表面材質特性,例如法線、雙向反射分佈函數(bidirectional reflectance distribution functionBRDF)等等。

3,  計算入射光照。

4,  計算光照對表面的影響,並最終顯示。

 

一般渲染引擎,渲染場景中的物體的時候,是將這四步一次執行完的。延遲着色則將前兩步和後兩步分開到渲染管道相互獨立的兩個部分來執行。

我們希望讀者在閱讀本文前,能先了解一下延遲着色的基本原理。以下的文章都不錯,可以讀讀:Shishkovtsov 2005Policarpo and Fonseca 2005Hargreaves and Harris 2004

在本文中:Forward Shading(前向渲染)是指4個步驟一齊處理的傳統着色方法。Effect就是Direct3DD3DX Effect,而TechniqueAnnotationPass,與它們在D3DX中的概念一樣。

材質着色(Material Shader)是指用來渲染幾何圖元的Effect(也就是前兩步),光着色則是指用來渲染可見光源的Effect。幾何體(Body)用來指代那些需要渲染的物體。

在這裏我們忽略了顯卡相關的優化或實現,所有的解決方案都是普遍適應於SM2SM3硬件的。我們希望能強調這個技術,而非實現。

 

2 Some Background

Tabula Rasa中,我們一開始的渲染引擎是基於最初的DX9而完成的傳統前向渲染技術的,使用了HLSLD3DX Effect。我們的Effect使用了Pass裏的Annotation來描述這個Pass所支持的光照。而在CPU這邊,引擎可以算出來每個幾何體被那些光源所影響——這個信息連同那些在PassAnnotation裏的信息一起,用於設置光源的參數、以及確定每個Pass該調用多少次。

這種前向着色有多種問題:

1,    計算每個幾何體受那些光影響耗費了CPU的時間,更壞的是,這是個O(n*m)的操作。

2,    Shader經常需要超過一次以上的Pass來渲染光照,渲染n個燈光,對於複雜的Shader,可能需要O(n)次運算。

3,    增加新的光照模型和新的光源類型,可能需要改變所有Effect的源文件。

4,    Shader很快就將達到或者超出SM2的指令限制。

MMO裏,我們對遊戲環境很少會有過於苛求的要求。我們無法控制同屏可見的玩家數量、無法控制同屏會有多少特效和光源。由於傳統前向渲染缺乏對環境的控制,且對於光源的複雜度難於估量,因此我們選擇了延期着色。這可以讓我們的畫面更接近於當今頂尖的遊戲引擎,並且讓光照所耗費的資源獨立於場景的幾何複雜度。

延期着色提供了下面的好處:

1,     光照所耗費的資源獨立於場景複雜度,這樣就不用再費盡心機去想着處理那些光源影響幾何體了。

2,     不必要再爲幾何體的受光提供附加的Pass了,這樣就節省了Draw Call和狀態切換的數量。

3,     在增加新的光源類型和光照模型時,材質的Shader不需要做出任何改變。

4,     材質Shader不產生光照,這樣就節省了計算額外的幾何體的指令數。

延期着色需要顯卡提供MRT的支持,且利用了不斷增加的存儲器的帶寬——這也就意味着我們可能得對玩家所使用的硬件提出更高的要求。因此我們既實現了前向着色,也實現了延期着色。我們優化了前向着色管道,並在此基礎上完成了延期着色管道。

有了一個完全基於前向着色的系統作爲後盾,我們就可以以更高的硬件標準來完成延期渲染系統了。我們使用了SM2的顯卡作爲前向着色系統的最低配置,而延期着色系統,則需要支持SM3的顯卡。這樣就更易於開發一個延期渲染管道,因爲我們不必要再顧慮指令數的限制,且能使用動態分支語句。

 

3前向着色支持

即便是工作在延期着色引擎下,對於半透明物體的渲染依舊需要前向渲染管道的支撐(詳見本文第8節)。我們的引擎裏保留了對整個前向着色管道的支持,這個管道用來處理半透明物體,以及用於在低端顯卡上替代延期着色引擎。

本節講述了我們是通過什麼方法來同時支持前向和延期渲染的。

3.1 受限的特性

我們限制了前向渲染管道的特性,只讓它實現延期渲染管道所有特性的一個很小的子集。有些特性因爲技術上的原因無法支持,有些是因爲工期太緊,但更多的,是爲了開發起來方便而被我們丟棄掉了。

我們的前向渲染管道支持球狀光源(hemispheric),方向光源和點光源,其中點光源是可選的,其他的所有類型光源都不支持(包括Spot LightBox Light,它們只由延期着色管道來支持)。在延期渲染管道里構建的陰影和其他特性,在前向渲染管道中都不支持。

最後,前向渲染中的Shader是可以做逐頂點光照和逐像素光照的。在延期渲染管道中,所有的光都是逐像素的。

3.2 一個Effect,多個Technique

我們使用了在Effect中使用了不同的Technique來完成前向着色、延遲着色和Shadow Map,以及更多的東西。我們對每個Technique指定了Annotation來標明這個Technique使用了什麼樣的渲染方式。這就允許我們將所有的Shader代碼放到一個統一的Effect文件裏,來實現渲染引擎所需的所有Shader(見表19-1)。這包括前向着色中的靜態和骨骼模型,延期着色使用到的“材質着色”(Material Shading)的靜態和骨骼模型,以及Shadow Map

Effect所能用道德所有的Shader放到一個地方,我們就可以儘可能多地共享一些可以跨越不同渲染技術的代碼。當然,我們不會去做一個超長的文件來儲存這些代碼,而是將這些Shader建立了一個由多個文件組成的Shader庫,包含了各個Effect都能用到的共享的頂點和像素代碼,以及常用的函數。這減少了Shader代碼的複製,使得維護變得容易,減少了Bug,以及增強了各個Shader之間的穩定性(consistency)。

19-1:材質示例

                                                 Code View:

// These are defined in a common header, or definitions
// can be passed in to the effect compiler.
#define RM_FORWARD 1
#define RM_DEFERRED 2
#define TM_STATIC 1
#define TM_SKINNED 2
 
// Various techniques are defined, each using annotations to describe
// the render mode and the transform mode supported by the technique.
technique ExampleForwardStatic
  int render_mode = RM_FORWARD;
  int transform_mode = TM_STATIC;
{ . . . }
 
technique ExampleForwardSkinned
  int render_mode = RM_FORWARD;
  int transform_mode = TM_SKINNED;
{ . . . }
 
technique ExampleDeferredStatic
  int render_mode = RM_DEFERRED;
  int transform_mode = TM_STATIC;
{ . . . }
 
technique ExampleDeferredSkinned
  int render_mode = RM_DEFERRED;
  int transform_mode = TM_SKINNED;
{ . . . }
 
                                        

3.3      光照優先級

我們的前向渲染在對一個集合體使用多個光源時,很容易就需要增加額外的Pass了。增加Pass不僅會產生更多的Draw Call,也會造成更多的狀態切換和更多重繪(原文是Overdraw,我感覺這裏可能想表達重繪的意思)。我們發現在有很多光源的情況下,我們的前向渲染只繪製一小部分光源,就會比延期渲染慢。因此,爲了更好的性能,我們嚴格限制了前向渲染管道里,對一個集合體受光的最大數量。

延期渲染管道每幀可以處理30個、40個、50個、甚至更多的動態光源,它們消耗的資源與幾何體的數量,大小,以及受光程度毫無關係。然而,在前向渲染管道中,當有兩個光源影響了一大坨幾何體時,瞬間就卡了。由於兩個渲染管道存在如此顯著的性能差別,使用相同數量的光源幾乎是不可能的。

我們爲美術和策劃提供了對光源優先級的編輯操作,提供了光源用於前向渲染、還是用於延期渲染、還是都用的開關。光源的優先級在兩個管道中都有作用——當心能不足的時候,我們可以知道該關哪些光源;在延期渲染中,爲了性能、質量設置,光源可能需要依據優先級關掉因它產生的陰影。

地圖通常是按照延期渲染管道進行打光的。我們提供了一個很快的Pass來確認光源在前向渲染管道中是否是可接受的。一般地,在前向渲染管道下唯一的一個額外工作是增加Ambient Light的數量,來補償相對於延期渲染管道少得多的燈光。

4 高級光照特性

下面的這些技術在前向和延期渲染引擎中都有可能實現。在我們的延期渲染管道中,我們支持了所有這些技術。即便我們不用延期渲染,這些技術仍然可以使用(Even though deferred shading is not required, it made implementation much cleaner.)。在延期渲染中,我們將這些特性的實現與材質Shader分離開,這樣我們就可以增加新的光照模型和光源類型,而不必要修改材質屬性。這就正如我們可以添加新的、獨立於光照模型和光源類型材質。

 

4.1 Bidirectional Lighting各向異性光照

傳統的球面光照(hemispheric lighting),正如DX文檔裏所說的那樣,太普通了。這種光照模型使用了兩個顏色,一般標記爲TopBottom,然後基於表面法線對這兩個顏色進行線性插值。標準的球面光照,根據表面法線方向朝正上方和正下方(這也就是爲什麼叫TopBottom),來對顏色進行插值。在Tabula Rasa中,我們支持了這種傳統的球面光照,但我們也爲方向光源提供了背部顏色(Back Color)。

在延期渲染中,美術可以很簡單地增加多盞方向光源。我們發現他們經常使用一盞與另一盞光源恰巧相反的光源來模擬輻射度。他們很喜歡這種方法的結果,因此一個自然而然的優化就是:將這兩個光源統一成一個特殊的方向光源——一個正面顏色和一個背部顏色。這給了他們相同的控制,但少了一半工作量。

對於之後的優化,背部顏色只是一個N·L的運算,或者一個簡單的朗伯(Lambertian)光照模型。我們不必要爲背部實現Specular,陰影,遮擋,以及更多高級光照技術。這些背部顏色只是對整個場景環境光和輻射度一個簡單的近似。我們將正面顏色的N·L存了下來,將它的方向取反以用到背部顏色的計算上。

4.2 Globe Mapping

Globe Map是用來對光照添加顏色的,就像我們生活中的玻璃球(溜溜彈)那樣。光線從光源發射出來,穿過玻璃球,然後被玻璃球賦予顏色和遮擋。對於點光源,我們使用一個Cube Map來完成這個功能,而對於聚光燈,我們使用2D紋理。這可以用於高效地模擬彩色玻璃的效果,或者通過一個模板來對光線進行遮擋。我們也爲美術提供了旋轉和讓這些Globe Map動起來的效果。

可能的話,美術可以使用Globe Map來高效模擬Shadow Map,模擬彩色玻璃,迪斯科球(就是一般舞廳裏那個旋轉的,閃着曖昧和刺眼光芒的球球),以及更多。我們引擎裏所有的光源都支持這些。請參考圖19-1 19-3

19-1:基本的聚光燈

 

19-2:簡單的Globe Map

 

19-3:融合了Globe Map的聚光燈。

 

4.3 Box Lights

Tabula Rasa中,方向光是影響整個場景的全局光,且用於模擬太陽光和月亮光。我們發現,美術有時候想用方向光影響一小塊區域,而不是整個個場景。

我們的解決方案是Box Light。這些光也是方向光,但他們只在一個長方體中起作用。在這個長方體中,我們可以支持類如聚光燈那樣的衰減,這樣,他們的強度就會隨着距離邊界越近而衰減得越厲害。Box Light也支持Shadow MapGlobe Map,背面顏色,以及所有其他被我們引擎支持的光源特性。

4.4 Shadow Maps

Tabula Rasa中,沒有預計算的光照。我們只用到了Shadow Map,而沒有使用Stencil ShadowLight Map。美術們可以讓任何光源產生陰影(除了球面光照外)。對於Point Light,我們使用了Cube Map來產生Shadow Map,其它的情況下,我們都使用了2D紋理。

Tabula Rasa中的所有Shadow Map都使用了浮點紋理,且使用了抖動採樣(Jitter Sampling)來進行柔化。美術可以控制抖動的幅度,以控制軟陰影“軟”的程度。這個方法允許我們用一個固定的方法,在所有的硬件上實現相同的效果,當然,對於Shadow Map,我們肯定是要使用硬件相關的紋理格式的。硬件相關的紋理格式可以提供諸如更好的精度,更好的硬件過濾。

Global Shadow Maps

很多論文討論了全局陰影圖,或者由一盞方向光的平截臺體所產生的單獨的陰影圖。我們花了兩個星期的時間來研究透視陰影圖(Perspective shadow Maps[Stamminger and Drettakis 2002]和梯形陰影圖(trapezoidal shadow maps[Martin and Tan 2004]。這兩個方法最大的問題是最後的結果取決於光源方向和眼睛方向。只要攝像機一變化,陰影的質量就會發生改變,最壞的情況下,變成了標準的正交投影。

Tabula Rasa裏面是有白天和夜晚的循環的,太陽和月亮持續的在天上劃過。在黃昏和拂曉的時候,光源方向與水平面幾乎平行,這就增加了攝像機方向與光源方向平行的機率。這是前述兩種方法面對的最糟糕的情況。

由於攝像機和光源方向不斷移動,陰影質量變得很難把控,我們最終(end up,這裏不知道作者是想說最終,還是說不再)使用一張大的2048 X 2048Shadow Map進行正交投影。這使得最後的結果很統一,而且與光源和攝像機的夾角無關。當然,肯定會有比我們這種方法好得多的方法,例如Cascaded Shadow Map

我們使用了抖動採樣來柔滑陰影邊緣,我們對光源的位移進行了離散化,因此他總是指向Shadow Map中固定的位置,我們也對光源的方向離散化了,這樣Shadow Map計算時的值不需要每幀都發生變化。最終的結果是,我們獲得了一個穩定的陰影,無論攝像機如何移動。

請看錶19-2

 

19-2 離散化光源位置以計算Shadow Map投影矩陣。

Code View:

// Assumes a square shadow map and square shadow view volume.
// Compute how "wide" a pixel in the shadow map is in world space.
const float pixelSize = viewSize / shadowMapWidth;
 
// How much has our light position changed since last frame?
vector3 delta(lightPos - lastLightPos);
 
// Project the delta onto the basis vectors of the light matrix.
float xProj = dot(delta, lightRight);
float yProj = dot(delta, lightUp);
float zProj = dot(delta, lightDir);
 
// Quantize the projection to the nearest integral value.
// (How many "pixels" across and up has the light moved?)
const int numStepsX = static_cast<int>(xProj / pixelSize);
const int numStepsY = static_cast<int>(yProj / pixelSize);
 
// Go ahead and quantize "z" or the light direction.
// This value affects the depth components going into the shadowmap.
// This will stabilize the shadow depth values and minimize
// shadow bias aliasing.
const float zQuantization = 0.5f;
const int numStepsZ = static_cast<int>(zProj / zQuantization);
 
// Compute the new light position that retains the same subpixel
// location as the last light position.
lightPos = lastLightPos + (pixelSize * numStepsX) * lgtRight +
                          (pixelSize * numStepsY) * lgtUp +
                          (zQuantization * numStepsZ) * lgtDir;
 
                                        

Local Shadow Maps

在我們的引擎中,所有光源都可能產生陰影,而整個地圖有上百盞燈。引擎必須提供管理和使用Shadow Map的方法。所有的Shadow Map知道他們需要時纔會被創建出來,並且,大多數Shadow Map是靜態的,不需要每幀都重新創建。我們爲美術提供了控制每個產影燈是使用靜態Shadow Map還是動態的Shadow Map。靜態的Shadow Map只生成一次,之後就一直使用,而動態的則每幀都會被刷新。

我們同樣標定了幾何體是靜態的還是動態的,也就是運行時是否可動。我們可以根據這個標誌來在計算中裁減掉部分幾何體。當創建靜態Shadow Map時,我們排除了動態幾何體部分。這可以防止類如Avatar這樣的動態物體產生的動態影“getting ‘baked‘ into a static shadow map”(這句話不是特別明白,可能想表達的意思就是,靜態Shadow Map產生時僅考慮靜態物體,而不考慮場景中當前的動態物體吧?這不廢話麼?!拍靜態物體的Shadow Map當然不應該考慮動態物體了,要不幹嘛弄這一套靜態Shadow Map?!)。動態物體如同其它靜態物體那樣,使用靜態Shadow Map來對自己打影。例如,沿着樓梯走的Avatar,將會被樓梯投到他身上的影子所影響。(個人感覺這裏作者可能想表述的就是他們把靜態物體和動態物體的產影分開了,互相獨立,不過業界的應該都是這麼做的吧?需要這麼特別說明一下麼?搞不明白!當然,也可能使我理解錯了,歡迎大家批評指正!

這裏有很多種自動化和優化的方法。我們並不一開始就生成所有的靜態Shadow Map,而是在需要用到的時候再去創建。這就意味着我們並不需要發佈這些Shadow Map文件,且減少了Loading時和運行時從磁盤讀取數據的數量。爲了節省顯存和節省紋理創建的開銷,我們使用了Shadow Map池。關於這些本文後面會有更多地描述。

動態產影光源是最耗費的,他們需要常時重新生成他們的Shadow Map。如果動態產影光源不移動,或者移動得不那麼劇烈,則就有一些方法可以提升一些性能了。最簡單的是,除非有動態幾何體在光源的影響範圍內,否則就不要重新生成這些Shadow Map。另一個選擇是將靜態模型渲到各自獨立的靜態Shadow Map上,這樣這些Shadow Map就只用創建一次了。每幀都需要將動態物體渲染到獨立的動態Shadow Map上,在最後,只需要判斷兩個Shadow Map中最小的,或者最近的值就可以了。最後的結果就類似於整個場景的所有物體都產生了Shadow Map——其實我們生成的只有動態物體。

4.5 將來的擴展

由於基於延期着色的引擎已經將光照和幾何渲染完全分開, 因此我們就可以很方便的修改或增加光照的特性了。事實上,前面說的Box Light,從會議上的提議到最後編輯器裏的完整功能,我們只花了三天時間。

HDRBloom,以及其它特效,添加到延期渲染引擎裏、與添加到傳統渲染引擎的難度相當。延期渲染引擎的架構,使得他更易於擴展。一般的,在延期渲染引擎裏增加一個特性,比在前向渲染引擎裏增加一個特性顯得簡單,或者起碼不會難太多。限制延期渲染引擎特性的最大問題是能添加到每個象素中的材質屬性,可用的顯存,以及顯存帶寬。

5 可讀的DepthNormal Buffer的優勢

延期着色的一個前提是,需要創建儲存深度和法線信息的紋理。這些信息將被用到光照計算中。然而,他們也可以超越光照的範疇,用於計算霧,深度Blur應該是指DOF),體積粒子,以及消除半透明物體穿入不透明物體時的硬邊。

5.1 高級水和折射

Tabula Rasa的延期渲染中,我們的水面Shader充分考慮了水的深度信息(視空間下)。當水的被渲染時,我們將拿它的每個象素和我們延期着色中已有的深度進行比較。這就使我們的水面可以具備自動的海岸線,而且,水可以根據視空間的深度來改變顏色和半透明,同時,在水下的物體可以做折射,而水上的物體則不用。我們可以在一個Pass裏面做完所有這些工作,而不像傳統渲染引擎那樣。(譯者:但是,我個人認爲,這種海岸線的效果真得不怎麼樣……!除了過度柔和的邊緣之外,對於海浪之類的模擬較差。相比而言,還是CrysisFarCry這類引擎的海面做得好啊

我們的前向渲染引擎只支持基本的折射特性,它需要一條獨立的Pass來初始化折射紋理的Alpha信息,以分辨那些在水面之上的部分,這些部分不能計算折射。[Sousa 2005]給出了這個算法。

在我們的延期渲染引擎中,我們可以採樣到當前像素的視空間深度和被折射像素的視空間深度。通過比較這兩個深度,我們可以知道究竟被折射像素是高於水面還是低於水面,低於水面的,發生折射,高於水面的,就不再處理了。見圖19-419-5

 

19-4 前向渲染的水。

折射只在低於水面的地方產生,這裏沒有可訪問的深度信息,只能用多個Pass來處理,不能用視空間深度。

 

19-5 使用了前向渲染,但通過延期渲染的Depth Buffer來獲取深度。

注意顏色和半透明隨着視空間深度變化而變化,沒有了水體的硬邊,只需要一個Pass

 

爲了方便美術控制隨深度變化的顏色和透明度,我們提供了一個Volume紋理,而非一個1維紋理。1維紋理只是一個從歸一化的深度查詢到透明度的速查表。而Volume紋理則允許美術模擬水深對半透明的非線性的變化。Volume紋理也用於影響水面的顏色。這可以使一個平板Volume紋理(也就是一張標準的2D紋理),也可以是有2或者4W分量的Volume紋理。歸一化的深度用於對W進行採樣,UV則由美術來指定。水面的表面法線有兩個相互獨立的、UV動畫的發現圖構成。

5.2 分辨率無關的邊緣檢測

[Shishkovtsov 2005]GEMS2裏的那篇文章)提出了一個邊緣檢測方法,用於在幀緩存上模擬反鋸齒。這種方法需要一些與分辨率相關的魔數。我們也需要反鋸齒,我們修改了一下這個方法,使之可以與分辨率無關。

我們對一個像素鄰近的8個像素,進行深度梯度和法線角度的採樣,這一點是與Gems2一致的。我們在這個點上判斷深度上最大的和最小的變動,來確定邊緣有多強。像素之間深度的梯度是與分辨率無關的。通過比較梯度變化率之間的關係,而不是梯度,就可以做到分辨率無關了。

我們的法線處理類似於GEMS2的方法。我們比較了中央像素和其周圍、沿與我們檢測梯度相同的邊緣、的像素角度的餘弦的變化(譯者:我也沒弄明白啥意思,具體就看代碼吧……)。這裏我們使用了我們自己的常數。無論如何,法線的變化率也是分辨率無關的,這就達到了我們的要求。

在這個算法中,我們沒有做對“右上”或“前”邊緣的選擇的限制,因此很多邊緣會有兩個像素寬,不過,當使用了Filter來平滑這些邊緣後,看起來也不錯。

邊緣檢測的結果是生成了逐像素的邊緣權重,這個值在0~1之間。這個權重反映了會有多少像素在它上面。在最後的渲染前,我們會把這個權重進行四個Bilinear採樣。這四個採樣是中心像素權重0,四周權重爲1的採樣。這樣的結果就是目標像素的權重是它8個鄰居權重的平均值。像素越是一個邊緣像素,就會越多與它的鄰居混合。請參考表19-3

 

19-3:邊緣檢測的Shader代碼。

Code View:

////////////////////////////
// Neighbor offset table
////////////////////////////
const static float2 offsets[9] = {
  float2( 0.0,  0.0), //Center       0
  float2(-1.0, -1.0), //Top Left     1
  float2( 0.0, -1.0), //Top          2
  float2( 1.0, -1.0), //Top Right    3
  float2( 1.0,  0.0), //Right        4
  float2( 1.0,  1.0), //Bottom Right 5
  float2( 0.0,  1.0), //Bottom       6
  float2(-1.0,  1.0), //Bottom Left  7
  float2(-1.0,  0.0)  //Left         8
};
 
float DL_GetEdgeWeight(in float2 screenPos)
{
  float Depth[9];
  float3 Normal[9];
 
  //Retrieve normal and depth data for all neighbors.
  for (int i=0; i<9; ++i)
  {
    float2 uv = screenPos + offsets[i] * PixelSize;
    Depth[i] = DL_GetDepth(uv);  //Retrieves depth from MRTs
    Normal[i]= DL_GetNormal(uv); //Retrieves normal from MRTs
  }
 
  //Compute Deltas in Depth.
  float4 Deltas1;
  float4 Deltas2;
  Deltas1.x = Depth[1];
  Deltas1.y = Depth[2];
  Deltas1.z = Depth[3];
  Deltas1.w = Depth[4];
 
  Deltas2.x = Depth[5];
  Deltas2.y = Depth[6];
  Deltas2.z = Depth[7];
  Deltas2.w = Depth[8];
  //Compute absolute gradients from center.
  Deltas1 = abs(Deltas1 - Depth[0]);
  Deltas2 = abs(Depth[0] - Deltas2);
 
  //Find min and max gradient, ensuring min != 0
  float4 maxDeltas = max(Deltas1, Deltas2);
  float4 minDeltas = max(min(Deltas1, Deltas2), 0.00001);
 
  // Compare change in gradients, flagging ones that change
  // significantly.
  // How severe the change must be to get flagged is a function of the
  // minimum gradient. It is not resolution dependent. The constant
  // number here would change based on how the depth values are stored
  // and how sensitive the edge detection should be.
  float4 depthResults = step(minDeltas * 25.0, maxDeltas);
 
  //Compute change in the cosine of the angle between normals.
  Deltas1.x = dot(Normal[1], Normal[0]);
  Deltas1.y = dot(Normal[2], Normal[0]);
  Deltas1.z = dot(Normal[3], Normal[0]);
  Deltas1.w = dot(Normal[4], Normal[0]);
 
  Deltas2.x = dot(Normal[5], Normal[0]);
  Deltas2.y = dot(Normal[6], Normal[0]);
  Deltas2.z = dot(Normal[7], Normal[0]);
  Deltas2.w = dot(Normal[8], Normal[0]);
 
  Deltas1 = abs(Deltas1 - Deltas2);
 
  // Compare change in the cosine of the angles, flagging changes
  // above some constant threshold. The cosine of the angle is not a
  // linear function of the angle, so to have the flagging be
  // independent of the angles involved, an arccos function would be
  // required.
  float4 normalResults = step(0.4, Deltas1);
 
  normalResults = max(normalResults, depthResults);
  return (normalResults.x + normalResults.y +
          normalResults.z + normalResults.w) * 0.25;
}
 
                                        

6 警告

6.1 材質屬性

小心選擇屬性

Tabula Rasa的延期渲染瞄準的是DX9 平臺,SM3的硬件環境。這個階段的配置有大量的用戶羣,然而同時,DX10SM4可以減少許多限制。首要的一點是,SM3最多隻支持4Render Target,且不支持獨立的Render Target位深度(也就是,4RT必須具備同樣的Bit數,如果你一個用的是R8G8B8A832bit,那麼另一個你就不能用FP16,只能用FP32,因爲FP1616Bit的,而FP3232Bit)。這就限制了我們可以使用的、用於儲存材質信息的數據通道數量。

一般的4DX9 32Bit MRT紋理,除了深度緩衝(DX本身的DepthStencil)之外,剩下還有13個數據通道來儲存屬性信息:34通道的RGBA紋理,和一個32Bit的高精度深度紋理。即便我們使用的是64Bit,而非32Bit,除了能提供更高的精度外,其實並不能增加數據通道的數量。

即便所有的數據通道都是按照順序來儲存信息的,但在SM3下,所有對數據的訪問都通過浮點寄存器。這就意味着使用Bit Mask或者類似手段來做壓縮或者將更多信息存儲到一個通道里是不切實際的。到了SM4,才支持真正的整數運算。

必須指出的是,這些通道里存儲的信息,直接決定了引擎能支持怎樣的光源類型。我們只能儘可能避免存儲某一個具體光源類型獨特的數據。在通道受限的情況下,每個通道都必須最大程度地利用來存儲那些最重要的數據。

這裏有一些輔助壓縮或者減少通道使用量的方法。存儲視矩陣的法線時,可以存儲在兩個Channel裏,而不是三個。在視矩陣裏,法線的Z分量只可能具有統一的符號(正負號),因爲所有可視的像素都面對攝像機。利用這個信息,同時,利用所有的法線都是單位向量,我們可以通過XY分量構建出Z分量來。另一個方法,是把材質屬性存儲到一個紋理速查表中,然後把必要的紋理座標(也就是這個速查表的索引)存儲到MRT的數據通道里。

這些材質的屬性,就是維繫材質和光源之間的膠水。它們是材質Shader的輸出,同時是光照Shader的輸入。

同時,他們也是材質和光照之間唯一的關聯。這樣,改變材質的屬性數據(應該是指改變數據通道的組織,服了這爲大哥了……到處寫這種語焉不詳模棱兩可的話,官腔!……改變個數據至於要修改所有Shader嗎?那你延期渲染的優勢還能體現到哪裏呢?!),同時必然需要改變所有的Shader,包括材質和光照。

封裝和隱藏MRT數據

我們並不直接把材質屬性的數據通道或者數據格式暴露給光照Shader,而是通過一些函數來設置和獲取這些信息(拜託,求你了,大哥,我們懂基本的封裝,你不是寫給小學生看的,OK?!快說重點!)。這樣,數據的位置和格式就可以隨意改變,而材質和光源則只需要重新編譯,而不必修改。

我們也提供了一個在材質裏專門初始化所有MRT數據的Shader。這可能增加了不必要的指令開銷,但爲我們未來擴展新的數據通道提供了便利,也不必要再去修改已經存在的材質Shader了。材質Shader只有在默認值需要發生改變時,纔會去修改。請見表19-4

 

19-4:封裝和隱藏MRT數據

Code View:

// Put all of the material attribute layout information in its own
// header file and include this header from material and light
// shaders. Provide accessor and mutator functions for each
// material attribute and use those functions exclusively for
// accessing the material attribute data in the MRTs.
 
// Deferred lighting material shader output
struct DL_PixelOutput
{
  float4 mrt_0 : COLOR0;
  float4 mrt_1 : COLOR1;
  float4 mrt_2 : COLOR2;
  float4 mrt_3 : COLOR3;
};
 
// Function to initialize material output to default values
void DL_Reset(out DL_PixelOutput frag)
{
  // Set all material attributes to suitable default values
  frag.mrt_0 = 0;
  frag.mrt_1 = 0;
  frag.mrt_2 = 0;
  frag.mrt_3 = 0;
}
// Mutator/Accessor – Any data conversion/compression should be done
// here to keep it and the exact storage specifics abstracted and
// hidden from shaders
void DL_SetDiffuse(inout DL_PixelOutput frag, in float3 diffuse)
{
  frag.mrt_0.rgb = diffuse;
}
float3 DL_GetDiffuse(in float2 coord)
{
  return tex2D(MRT_Sampler_0, coord).rgb;
}
 
. . .
 
// Example material shader
DL_PixelOutput psProgram(INPUT input)
{
  DL_PixelOutput output;
 
  // Initialize output with default values
  DL_Reset(output);
 
  // Override default values with properties
  // specific to this shader.
  DL_SetDiffuse(output, input.diffuse);
  DL_SetDepth(output, input.pos);
  DL_SetNormal(output, input.normal);
 
  return output;
}
 
                                        

 

6.2 精度

延期着色很容易由於喪失了數據的精度而引發問題。最明顯的丟失,是由於材質數據被存儲到了MRT數據通道里。在Tabula Rasa中,絕大多數數據通道是8Bit或者16Bit的,取決於我們使用了32BitRender Target還是64Bit的(一個Render Target4通道這一點並沒有改變)。硬件內部的寄存器與Render Target的內部格式精度並不一致,再讀和寫的時候均需要數據的轉換。例如:我們的法線分量是通過硬件最高精度的運算得出的,但卻要被存儲到8Bit或者16Bit精度的通道里。在8Bit的情況下,高光看起來很不平滑,而且還會有破碎的情況出現。

7 優化

在延期着色下,光照系統的性能直接取決於光源需要處理的像素的數量。我們用了下面的技術來減少光照需要計算的象素數量,以提升性能。

早期Z剔除(Early z-rejection),模板緩衝,以及動態分支,它們具備相同的特徵:取決於數據的位置。這需要硬件體系結構的支持,不過現在絕大多數硬件都支持了。一般的,如果我們儘可能地使用了早期Z剔除,模板緩衝和動態分支,那麼在屏幕上的一個局部區域內,所有的像素的行爲都是均勻的。也就是說,他們都經過了Z剔除,模板,或者走入了同一個分支中。

7.1 有效的Light Volume

我們使用了緊密包圍着光源影響區域的Light Volume來計算光照。理論上說,如果我們對整個屏幕所有的像素全都用光照計算,那麼最後的結果也是一樣的,但是,性能就會變得很差(每個燈光對整個GBuffer進行一次全採樣,OMG……)。Light Volume覆蓋的屏幕空間的像素越少,光照Pixel Shader需要處理的數據就越少。我們適用錐體來描述聚光燈,球體來描述點光源,長方體來描述Box Light,而對於方向光這樣的全局光照,我們適用了整個屏幕空間。

另一個延期渲染的論文都會描述的方法,是通過基於Light Volume和攝像機位置的深度測試和Cull Mode順時針逆時針那個),來減少計算量。這種調整最大程度的進行了早期Z剔除。這種方法需要CPU來判斷用哪種深度測試和Cull Mode的組合可以最大程度的進行早期Z剔除。

在我們所有的情況下(我們的Light Volume不會被遠面剔除掉),我們都使用“Greater”的深度測試和順時針的繞法(也就是反着繞)。可以通過一些推測,來選出對自己最有效的深度測試和Cull Mode。然而我們遇到的瓶頸在其他地方,因此我們決定不再用這種方法,通過浪費CPU資源來優化性能。

感覺本段的技術並無實用性,剔除肯定是必須做的,硬件Occlusion,其他的,各種方法其實都很簡單,而且實用性較強。不過也有可能是我沒理解了作者的意思。

7.2 模板緩衝

在延期渲染系統中,使用Stencil來屏蔽一些像素,是另一個常用的手段。基本上,就是用Stencil Buffer來指定哪些像素不必要進行光照。當渲染Light Volume的幾何體時,可以通過簡單的模板測試來取消對這些標定像素的處理。

我們試了一些這個技術的變種。我們發現這個方法所帶來的性能提升,還不如增加了Draw Call導致的性能下降。我們試圖使用一個“便宜的”Pass來標定所有的像素是否面向光源或者是否在光源之外。這個確實是減少了需要處理光源的像素的數量。在DX9 一般的下,“便宜的”Pass增加的Draw Call抵消掉、甚至遠超過了最後光源Pass時提升的性能。

我們利用了Stencil來標定那些之後延期渲染需要處理的場景中的不透明物體。這個方法把那些天空盒和其他正面不需要進行光照的物體排除掉了(主要是那些只有Emissive的物體)。這個方法不需要任何多餘的Draw Call,因此會非常“便宜”。光照Pass之需要簡單的把這些標定的像素丟棄掉就可以了。當天空盒佔了整個屏幕絕大部分的時候,這種方法會帶來相當程度的性能提升,而即便不是這樣,這種方法起碼也不會帶來任何損失。

DX10減少了Draw Call的開銷。對於那些瞄準了DX10平臺的讀者,製作一個“便宜的Pass”(第二段描述的方式)應該是個不錯的嘗試。然而在SM3下使用動態分支,比增加新的Pass要好一些。

7.3 動態分支

SM3一個很關鍵的特性就是支持動態分支。動態分支不僅增加了GPU的可編程性,在合適的情況下,他也可以用來進行優化。

使用動態分支進行優化需要注意兩個原則:

1,              製造一個或者兩個動態分支,以確保能最大程度的跳過更多數量的代碼和那些頻率較高的代碼。

2,              注意數據的位置。如果一個像素走了分支A,那麼它鄰近的像素最好也能儘可能走分支A

光照中最好的時機是根據像素距離光源的遠近和表面法線來進行分支。如果使用了法線圖,則表面法線就會變得不再均勻,優化就會變得比較麻煩。

8 一些問題

在使用延期着色的過程中並非都是一帆風順的。由於顯存帶寬和數據通道的限制,延期着色也有它本身不可調和的問題。

8.1 半透明幾何體

延期着色最大的問題是在處理半透明物體的時候顯得無能爲力。不支持半透明不僅僅是硬件的限制問題,同時也是這個技術本身的硬傷:我們所有的工作均受限於“只能知道臨近像素的材質信息”。在Tabula Rasa中,我們使用了大家都在用的方法:在延期着色渲染勒索由的不透明物體之後,使用前向着色來渲染所有的半透明物體。

要在延期着色中支持真正的半透明,則可能需要一些更多的幀緩衝來存儲一個片斷是否被遮擋的信息。這也是解決不排序半透明的一種方式。這種緩衝現在並不被我們的圖形顯卡支持。

然而,開啓MRT時,只要Render Target允許,我們可以支持基於Alpha TestAdditive式的混合(也是一種Alpha混合)。(看了看後面,感覺這個意思可能是想說,由於延期着色中,Alpha是不會被存下來的,因此只能用來做Additive這樣的混合,而不能做基於Alpha的混合。)當MRTAlpha Test開啓時,如果COLOR0(也就是第一張Render Target)的Alpha爲零,當前片斷的Test失敗,則不會有任何Render Target被更新(也就是說,存儲Alpha是無意義的,因爲那些被裁減掉的像素根本就不會寫到幀緩存裏)。因此這裏我們不能使用Alpha Test,而是應該使用clip指令來裁掉一個像素。因爲Render Target 0並沒有用來存儲Diffuse,而是用於存儲其它材質信息的。延期渲染管道渲染的東西都應該是完全不透明的,因此,我們不再使用這些通道來存儲無意義的Alpha信息。

使用前向渲染來處理半透明幾何體可以解決一些問題。我們使用了我們的前向渲染管道來處理水面和其他半透明幾何體。水的Shader使用了在延期着色中生成的深度紋理。水的光照計算用的是傳統的前向着色技術。這種方案也有一點問題——讓半透明幾何體和不透明幾何體之間光照統一是比較困難的。而且,我們延期着色支持的很多光照特性,前向渲染管道是不支持的。這就使得兩者的結合變得不太現實。

Tabula Rasa中,在兩個方面,兩種光照系統的不一致成爲一個巨大的問題:頭髮和植被。頭髮和植被在半透明時看這會比較舒服。然而,當一個角色進入陰影的時候,他的頭髮沒有變色,這點是不可接受的。同樣的,當週圍所有的東西都被投影的時候,僅有草沒有被投影,也是不可接受的。

我們最終決定使用Alpha Test而不是半透明。這樣,頭髮和植被就可以利用延期渲染來處理了。光源的效果在頭髮和植被上也比較統一。爲了減少植被邊緣的粗糙,我們嘗試使用過一些小技巧。例如進行屏幕空間的半透明排序,或者使用半透明從前向着色過渡到延期着色。但沒有一個方案是真正可用的。我們現在的做法是通過讓植被變大變小來處理淡入和淡出。

8.2 帶寬

由於硬件帶寬的增加,延期着色才成爲可能。延期着色需要寫入到4Render Target中,而不是1個,也就是寫入量是原來的4倍。在光照Pass裏,我們也需要從這所有的緩衝中讀取信息,讀入量也超過了過去。帶寬和填充率,是延期着色最大的性能影響因素。

最大的減少帶寬開銷的因素是屏幕分辨率。帶寬與渲染像素的數量直接相關,1280x1024只有1024x76866%速度。延期渲染的引擎性能嚴重受限於分辨率的大小。

進行獨立的位深度存取,在捨棄一定精度的前提下,應該可以減少帶寬的損耗量。但是這種方法對我們並沒有用,因爲現在的硬件並不支持這種特性。我們的做法是儘可能減少材質數據的存儲量、儘可能減少這些緩衝的使用率。

當渲染光照的時候,我們也使用了MRT。我們使用了兩個Render Target,並進行Additive的混合。這些Render Target分別屬於DiffuseSpecular的積累緩衝區。乍一眼看,這好像對於節省帶寬而言是多餘的,因爲我們將信息寫到了兩個Render Target上。然而,這個選擇確實可以提高效率。

DiffuseSpecular加到一起的一辦法可能如下面這樣:

Fraglit = Fragunlit x Lightdiffuse + Lightspecular·

這個公式是可以分爲DiffuseSpecular兩部分的。將這兩部分分別放到兩個Render Target的話,在光照Shader裏,我們不用再去取出Unlit的片段(Frag Unlit)。Shader只是產生Light DiffuseLight Specular項,它們除了表面與光源的關係外,不用承擔任何其他的計算量。

如果我們不把DiffuseSpecular分開,那麼Light Shader則必須計算出最終的片段顏色。這個計算必須獲取Unlit的片段顏色(紋理本身的顏色),以及其他可能影響最終顏色的材質屬性(例如自發光)。把這些最終顏色放到光照Shader中,就意味着我們將真正丟掉DiffuseSpecular分量。也就是說,我們無法從Shader的結果中分解出來光源的原始信息了。將DiffuseSpecular分量存儲到Render Target中,對於進行HDR和其他需要影響光源的Post Process運算都很有利。

在所有的光照Shader都運行完畢後,我們進行最後一個全屏的Pass,來計算最終的片段顏色。這個最終的Post Process Pass裏,我們計算霧、邊緣檢測和平滑、以及最終的片段顏色。這個方法確保了這些方法對每個象素僅計算一次,減少了繞路的數量,最大化了從MRT裏讀取信息時紋理Cache的命中率。從MRT中反解材質數據是耗費很高的運算,特別是當大量使用時,導致的紋理Cache的顛簸,會讓這個情況變得更加糟糕。

使用這些光照的積累緩衝之後,我們可以很方便的在需要的時候關閉Specular光照,以避免帶寬的浪費。這些光照的積累緩衝也可以在光照相關的Post Process中也很有用,例如增加對比度,計算HDR,以及其它類似的特效。

8.3 內存管理

Tabula Rasa中,即便在最普通的1024x768分辨率下,我們也要爲延期渲染和反射這所有的Render Target花掉50MB的顯存。這還不包括主緩衝,頂點和索引,以及紋理。而這些Render Target1600x1200的分辨率下則需要100MB的顯存。

我們使用了4個、屏幕大小的Render Target來存儲幾何體的材質數據。我們的光照Shader使用了兩個、屏幕大小的Render Target。這些Render Target可以是32 Bit的或者64Bit的,取決於顯卡和顯示質量設置。然後,爲了全局方向光,還有一個2048x2048Shadow Map,以及爲了其他光源產生的各種附加Shadow Map

使用Render Target的一個可能的提議是:減少分辨率,只在最後渲染的時候把它們縮放上去。這有很多好處,但我們發現圖像質量變差了,因此就沒有繼續接下去研究,不過這種方法有可能在一些特殊的應用中是可行的。

Render Target使用的顯存只是一個問題。他們的生存週期和位置對整個性能有更爲關鍵的影響。即使這些紋理在顯存中,超出了我們的控制範疇,我們仍然可以做一些事情來挽回一些事情。

我們使我們主要的MRT早於其他任何紋理分配,這種分配可以幫助驅動,將他們放到最完整、連續的顯存中。我們仍然受制於驅動的實現,但是我們起碼可以幫助驅動去讓它實現我們希望的結果。

我們使用了Shadow Map池,並允許光源共享這些Shadow Map。在引擎裏,我們限制了Shadow Map的最大數量。基於光源的優先級,位置和所需的Shadow Map大小,爲這些光源分配少量的Shadow Map。這些Shadow Map永遠不會釋放,只是不斷地重用。這個減少了顯存碎片,並且減少了因爲創建和銷燬資源而帶來的性能損失。

基於這一點,我們也限制了每幀渲染(或重生)的Shadow Map的數量。如果有好幾盞光同時需要生成他們的Shadow Map,引擎每幀只會創建一到兩個,這就將花銷平攤到了幾幀中。

9 結果

Tabula Rasa中,使用延期渲染,使我們達到了預定的目標。我們找到了一條高性能、可度量的方法來實現延期渲染。在一些早期的SM3顯卡——如NV6800Ultra——在基本的設置和中端分辨率上可以達到30幀。而在最新的DX10顯卡,諸如NV8800ATI2900上,可以在全效果下跑的很好。

Figure 19-6. An Outdoor Scene with a Global Shadow Map

 

 

Figure 19-7. An Indoor Scene with Numerous Static Shadow-Casting Lights

Shown are box, spot, and point lights.

 

 

Figure 19-8. Fragment Colors and Normals

Left: Unlit diffuse fragment color. Right: Normals

 

 

Figure 19-9. Depth and Edge Weight Visualization

Left: The depth of pixels. Right: Edge detection.

 

 

Figure 19-10. Light Accumulation

 

 

 

10 討論

延期着色正在從理論走向現實。很多時候,很多新的技術需要耗費高昂代價,過於抽象,或者無法真正應用於商業。而延期着色則被證明是真實感遊戲設計領域一個通用的、強大的、可控的技術。

延期着色還需要克服的主要障礙包括:

較高的顯存帶寬佔用

無硬件反鋸齒的支持

Alpha Blend支持較差

 

我們發現當前駐留的顯卡已經可以在稍低的分辨率下解決貸款問題了,而在當今最高端的機器上,可以在開啓全部特性的前提下,適應更高的分辨率。在DX10 即便顯卡上,ATINVIDIA都增強了MRT的性能。DX10SM4都提供了GPU支持的整數處理,以及從深度緩衝中讀取數據。所有這些都可以減少顯存帶寬。當提供了新的硬件和特性時,性能自然就會提升。

在合適的Filter作用下,精確的邊緣檢測可以減少幾何體邊緣的鋸齒。雖然這些方法並不像硬件全場景反鋸齒那樣精確,但是仍然可以以假亂真。

延期着色最顯著的問題是對半透明的支持。我們自覺犧牲了一些半透明方面的圖形質量,然而,我們覺得延期着色所帶來的優點遠遠超過了這些問題。

延期着色主要的好處包括:

光照的開銷與場景複雜度無關。

Shader可以訪問深度和其他像素信息。

每個象素對每個光源僅運行一次。也就是說,那些被遮擋的像素是不會被光照計算到的。

材質和光照的Shader完全分開。

每天都有新的技術和新的硬件出來,由於他們的存在,延期渲染的地位也可能會有浮沉。未來是很難預料的,但我們很高興當時做出了在當今的顯卡上使用延期着色的決定。

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