寫在前面的話:因爲英語不好,所以看得慢,所以還不如索性按自己的理解簡單粗糙翻譯一遍,就當是自己的讀書筆記了。不對之處甚多,以後理解深刻了,英語好了再回來修改。相信花在本書上的時間和精力是值得的。
———————————————————————————————
”你需要做的是渲染出一張好圖片“
當你需要渲染一個三維對象時,模型不僅需要合適的幾何形狀,還需要有合適的視覺外觀。根據應用程序的不同,其範圍可以從照片寫實(外觀幾乎和真實物體的照片一樣)到出於創造而選擇的各種風格化外觀。如圖5.1所示兩種例子。
圖 5.1 上圖是用虛幻引擎渲染的寫實山水場景。而下圖的渲染用的是Campo Santo,這是專門用來渲染插圖風格類型的。
5.1 着色模型(Shading Models)
5.2 光源(Light Sources)
5.2.1 平行光(Directional Lights)
5.2.2 精準光源(Punctual Lights )
點光源/泛光源 (Point/Omni Lights)
聚光燈(Spotlights)
5.2.3 其他光源類型
5.3 實現着色模型(Implementing Shading Models)
5.3.1 求值頻率(Frequency of Evaluation)
在設計一個着色的實現時,計算需要根據求值頻率來劃分。首先,確定給定的計算結果是否在整個DrawCall過程中保持不變。在這種情況下,計算通常在CPU上執行,因爲GPU的計算着色器通常用來做一些昂貴的計算。計算結果由uniform着色器輸入傳給圖形API。
即使在這一類別中,也存在各種可能的求值頻率。最簡單的情況是就是着色方程中的常量子表達式,但這隻能應用到基於很少更改因素的計算中,例如硬件配置和安裝選項。編譯着色器時可以解決這種着色計算,在這種情況下,甚至不需要設置uniform着色器輸入。 或者,可以在安裝時或加載應用程序時在脫機的預計算過程中執行計算。
另外一種情況是,當着色計算結果在應用程序運行過程中發生變化,但速度很慢,可以不必每幀都進行更新。例如,光照因素取決於虛擬世界中一天的時間。如果計算很昂貴,可以嘗試在多幀執行一次。
其他的情況,包括每幀執行一次的計算,例如組合觀察矩陣和透視矩陣;也包括每個模型執行一次的計算,例如,根據模型位置更新模型的光照參數;或者每一次Draw Call執行一次的計算,例如更新模型中每種材質球的參數。利用求值頻率將uniform着色器輸入分組有助於提高應用程序的效率,並且通過最小化更新頻率來提高GPU性能。
如果着色計算結果需一次DrawCall改變一次,則不能通過uniform着色器輸入傳輸給着色器,取而代之,它必須由第三章描述的可編程階段之一進行計算,並且如果需要,可由varying 着色器輸入傳遞給其他階段。理論上講,可編程階段都可以進行着色計算,每個階段對應不同的求值頻率:
頂點着色(Vertex Shader)— 每個細分前的頂點(Evaluation per pre-tessellation vertex)求值。
殼着色(Hull Shader)— 每個表面Patch點求值。
域着色(Domain Shader)— 每個細分後的頂點求值。
幾何着色(Geometry Shader) — 每個圖元求值。
像素着色(Pixel Shader) — 每個像素求值。
實際上,大部分着色計算都是針對每個像素執行的。儘管這些通常在像素着色器中實現,但如今計算着色器已經越來越普遍了。其他階段主要用的是幾何運算,例如變換和變形。
圖5.9展示了具有廣泛頂點密度的模型的逐頂點和逐像素的着色結果。對龍來說,模型網格很緻密,兩者的差異很小。對茶壺而言,頂點着色求值會導致可見的錯誤,例如棱角分明的高光,而且在兩個三角形平面上的頂點着色效果明顯不對。這些錯誤的原因是,着色方程中某些部分的值在網格表面呈非線性變化,尤其是高光部分。這使得它們不適合頂點着色,因爲頂點着色的結果在傳遞給像素着色之前需要在三角形上線性插值。
圖 5.9 左列顯示的是逐像素的評估結果,中間列顯示的是逐頂點評估結果,右側列顯示的是每個模型的線框渲染,以展示頂點密度。
原則上講,可以在像素着色中僅計算鏡面高光部分,其餘部分在頂點着色中進行。這可能不會造成視覺瑕疵,並且理論上還可以節省一些計算。實際上,這種混合實施方案並不是最佳。着色模型中線性變化部分的計算通常花費最少,並且以這種方式拆分開來計算往往會增加足夠的開銷,例如重複的計算和額外輸入,弊大於利。
正如前面所說,在大多數頂點着色的實現中,頂點着色負責着非上色操作,例如幾何變換和變形。最終生成的幾何表面屬性,轉換到合適的座標系中,有頂點着色寫出,在三角形上線性插值,最終作爲varying着色器輸入傳遞給像素着色階段。這些屬性包括表面位置、表面法線,如果需要法線貼圖,還可以選擇表面切線向量。
注意,儘管頂點着色一直生成單位長度的表面法線,但是插值會改變其長度。如圖5.10左圖所示。因爲這個原因,在像素着色階段法線需要進行歸一化操作。但是頂點着色生成的法線長度還是很重要,如果各頂點之間的法線長度差異很大,例如,作爲頂點混合的副作用,會導致插值傾斜,如圖5.10右圖所示。由於這兩種情況,在頂點着色和像素着色中,在實現插值之前和實現插值之後都會對插值向量進行歸一化操作。
圖 5.10 左圖可以看到單位法線經過表面插值後會生成長度小於1的插值向量。右圖可以看到兩個明顯長度不一的表面法線經過插值後會發現插值方向偏向於較長法線的方向。
和表面法線不同,一些指向特定位置的向量,例如觀察方向和精準光源的光方向,通常不會進行插值。取而代之的是,在像素着色中用插值表面位置來計算這些向量。在像素着色中除了在任何情況下都要進行歸一化操作外,這些向量都是通過向量減法得到,因爲快。如果出於某個原因,需要插值這些向量,請不要事先對其進行歸一化,會導致不正確的結果,如圖5.11所示。
圖 5.11 對兩個光向量進行插值。左圖,對插值之前進行了歸一化操作,插值後會不正確。右圖,插值之前沒進行歸一化操作,結果正確。
前面提到頂點着色會把表面幾何轉換到“適當的座標系”內,通過uniform變量把攝像機和光源的位置傳遞給像素着色中,就是常見由應用程序轉換到相同座標系的例子。這樣會最大化減少像素着色中把所有着色模型向量帶入同一座標空間中。但是哪個座標系是合適的座標系?可能是全局世界座標系,或攝像機的局部座標系,或更爲罕見的當前渲染模型的局部座標系。通常由渲染系統作出選擇,基於系統性能表現,例如靈活性和簡單性。例如,如果需要渲染的場景中有大量的光源,那麼或許選擇世界空間是個很好的選擇,這樣可以避免大量光源位置的變換。或者,最好的選擇是選擇攝像機空間,爲了更好的優化和觀察向量有關的像素着色操作,並儘可能提高精度。
儘管大部分着色實現包括即將要討論的示例的實現,都遵循上述概述。當然也有些例外,一些應用出於風格化原因,選擇了多面外觀的逐圖元的着色求值,這種風格通常被稱爲平面着色(Flat Shading)。如圖5.12所示兩個例子。
圖 5.12 選擇了平面着色(Flat Shading)作爲風格的兩款遊戲:Kentucky Route Zero,上圖, That Dragon, Cancer 下圖。
原則上,平面着色可以在幾何着色中執行,但目前都是在頂點着色中實現。這和關聯每個圖元的屬性及其第一個頂點,並禁用頂點值插值來完成。禁用插值(可由每個頂點值分別完成)會使第一個頂點的值傳遞給圖元中所有的像素。
5.3.2 實施案例
in vec3 vPos;
in vec3 vNormal;
out vec4 outColor;
struct Light {
vec4 position;
vec4 color;
};
uniform LightUBlock {
Light uLights[MAXLIGHTS];
};
uniform uint uLightCount;
因爲這裏都是點光源,所以定義光源的結構體裏有一個position和一個color。這裏用的是vec4而不是vec3是爲了符合GLSL std140數據分佈標準。在這個例子中,儘管std140分佈會導致一些空間浪費,它簡化了確保CPU和GPU之間數據分佈需一致的任務,這就是爲什麼這裏採用std140的原因。Light 結構體的數組被定義成一個uniform塊,這是GLSL的特色,因爲把一組uniform變量綁定到一個緩衝對象中會讓數據傳輸很快。數組的長度就是應用程序允許一次drawcall中最大的光源數量。後面也會看到,shader源代碼在編譯之前,會用MAXLIGHTS宏來表示這一數量,本例中是10。uniform整數uLightCount表示的是實際在一次drawcall中激活的光源數。
vec3 lit(vec3 l, vec3 n, vec3 v) {
vec3 r_l = reflect(-l, n);
float s = clamp(100.0 * dot(r_l, v) - 97.0, 0.0, 1.0);
vec3 highlightColor = vec3(2,2,2);
return mix(uWarmColor , highlightColor , s);
}
void main () {
vec3 n = normalize(vNormal);
vec3 v = normalize(uEyePosition.xyz - vPos);
outColor = vec4(uFUnlit , 1.0);
for (uint i = 0u; i < uLightCount; i++) {
vec3 l = normalize(uLights[i].position.xyz - vPos);
float NdL = clamp(dot(n, l), 0.0, 1.0);
outColor.rgb += NdL * uLights[i].color.rgb * lit(l,n,v);
}
}
我們定義了一個lit()函數,被main()函數調用。總體來說,這就是公式5.20和5.21以GLSL來實現的。注意Funlit()的值和Cwarm值都是以uniform變量的形式傳入的。因爲這些值都是在整個drawcall中保持不變的,可以由應用程序計算這些值,以來節省一些GPU週期。
layout(location=0) in vec4 position;
layout(location=1) in vec4 normal;
out vec3 vPos;
out vec3 vNormal;
接下來看下頂點着色代碼,將不會展示任何它的uniform輸入了,但是會展示varying輸入和varying輸出的定義:
void main(){
vec4 worldPosition = uModel * position;
vPos = worldPosition.xyz;
vNormal = (uModel * normal).xyz;
gl_Position = viewProj * worldPosition;
}
這些都是常規操作,把表面位置和表面法線轉換到世界空間,然後傳遞給像素着色用,最後把表面位置轉換到裁剪空間,賦值給gl_Position,這是一個系統定義的變量,是任何頂點着色都需要輸出的一個變量,光柵化會用到。
var fSource = document.getElementById("fragment").text.trim();
var maxLights = 10;
fSource = fSource.replace(/MAXLIGHTS/g, maxLights.toString());
var fragmentShader = gl.createShader(gl.FRAGMENT_SHADER);
gl.shaderSource(fragmentShader , fSource);
gl.compileShader(fragmentShader);
注意“片元着色,fragment shader”對應着像素着色。MAXLIGHTS的賦值也在這,大部分渲染框架都執行類似的預編譯着色操作。
5.3.3 材質系統(Material System)
很少渲染框架只是簡單一個着色器,通常,需要一個專門的系統來處理各種材質,着色模型及着色器。
前面章節有說到,一個着色器可以認爲是一段程序,在GPU可編程着色階段觸發。因此,它是低級圖形API資源,而不是藝術家直接交互的對象。相反,材質是面向藝術家的表面視覺外觀的封裝。材質有時候也描述非視覺外觀方面,例如碰撞屬性,不在本書討論範圍內。
雖然材質是通過着色器來實現的,但不是簡單的一對一對應關係。在不同的渲染情況下,相同的材質可能使用不同的着色器。一個着色器可被多個材質使用。最常見的例子是參數化材質,以最簡單的形式,材質參數化要求兩種類型的材質實體:材質模板和材質實例。每個材質模板都是描述了一個類型的材質,具有一組參數,根據參數類型可以分配數字,顏色或紋理貼圖。每個材質實例對應一個材質模板和一組特定的參數值。一些渲染框架,像虛幻引擎,允許更復雜,層次結構,材質模板可以從其他模板派生。
參數可在應用程序運行時解析到,通過uniform輸入傳遞給着色程序,或者在着色程序編譯時通過某些宏替換得到參數。一種常見的編譯時參數是一種布爾開關,它控制着是否激活材質的特定特徵。
儘管材質參數可能和着色模型的參數是一一對應關係,但是並非總是如此。材質可能將某個着色模型參數的值,例如表面顏色,固定爲恆定值。或者,着色模型參數也可以由一系列複雜操作的結果計算得到。在某些情況下,像表面位置,表面朝向甚至時間之類的參數都可能會影響參數的計算。着色基於表面位置和朝向在地形材質很常見。例如,高度和表面法線可以被用來控制降雪效果。着色基於時間在動畫材質中很常見,例如閃爍的霓虹燈。
材質系統最重要的任務之一就是將各種着色器功能劃分爲單獨的元素,並控制這些元素的組合方式。在許多情況下,這種類型的組合很有用,包括以下幾種:
·使用幾何處理(例如剛體變換,頂點混合,變形,曲面細分,實例化及裁剪)來構成表面着色。這些功能變化獨立:表面着色基於材質,幾何處理基於網格,因此,很方便按需進行組合。
·用類似於像素丟棄和像素混合的合成操作來構成表面着色。這和移動GPU尤其相關,在移動GPU中,混合是像素着色中常見操作。
·將計算着色模型參數的操作和着色模型自身的計算組合在一起。這可以只需實現一次着色模型,然後計算結果就可用於計算各種着色模型參數。
·按材質特徵選擇。這樣可分別編寫每個功能的實現。
·將着色模型和模型在光源評估中的參數計算(計算Clight和光向量l)進行組合。技術,像延遲渲染,會改變組合的結構。支持多種這類技術的渲染框架又多了一層額外的複雜性。
如果圖形API提供來這類着色代碼模塊化的話,將會很方便。不幸的是,不像CPU代碼,GPU代碼不能對代碼塊進行編譯後鏈接。每個着色階段的程序都作爲一個單元進行編譯。着色階段之間的分隔有着有限的模塊性,在某種程度上適合列表中的第一項:使用幾何處理來構成表面着色。但擬合效果並不理想,因爲每個着色器同時也需要處理其他的操作,並且其他類型的合成仍需處理。考慮到這些限制,唯一讓材質系統能夠實現全部類型的合成的方法就是源碼級別(source-code level)。這主要涉及到連接和替換,通常用C風格的預處理指令(例如#include,#if,#define)執行。
早期的渲染系統具有相對較少的着色變動,並且通常都是手動編寫的。這有一些好處,例如,可以方便優化變動。但是,隨着變動數量的增多,這種方法變得不切實際了。當考慮到所有不同的部分和選項時,變動數量會很龐大。這也就是爲什麼模塊化和可組合性如此重要的原因。
設計處理着色器變動系統需要解決的第一個問題是,爲了表現不同的效果,是選擇在運行時使用動態分支還是選擇在編譯時使用條件預處理。在較老的硬件是不支持動態分支或非常緩慢,因此運行時選擇不是一個好選擇。所有的變動都會在編譯時進行處理,包括不同類型光源的所有可能組合。
相反,現代GPU可以很好地處理動態分支,尤其在一次DrawCall中所有的像素分支相同時。如今,許多功能變化(例如光源數量)都是運行時處理。然而,給着色器添加大量功能變化會帶來不同的成本:寄存器數量的增加和佔用率的相對減少。因此編譯時的處理變化仍然很重要,它會避免一些複雜邏輯。
現代材質系統同時使用了實時運行和編譯時的着色變動。即使不在編譯時處理全部工作,但隨着總體複雜性和變動數量在增加,因此仍然需要在編譯時處理着色變動。
材質系統設計時有一些策略:
·代碼複用(Code reuse ) 在共享文件中實現功能,用#include指令來訪問這些功能。
·減法(Subtractive ) 利用組合編譯時預處理條件和動態分支來刪除不需要的部分以及對互斥的備選方案間進行切換。
·加法(Additive ) 各種功能都被定義成擁有輸入連機器和輸出連接器的節點,並且可以組合在一起。這和代碼重用類似,但結構更清晰。節點之間的組合可以通過文本或可視化圖形編輯器來完成。後者是爲了讓非技術人員更容易創造新的材質模版。通常可視化圖形編輯只能訪問着色器的一部分,例如虛幻引擎的圖形編輯器智能影響着色模型輸入的計算,如圖5.13所示。
·基於模版(Template-based ) 接口被定義爲只需要符合接口條就可以插入到不同的實現中。這比加法策略更加正式,通常用於較大的功能模塊。常見的例子是分離着色模型參數的計算和着色模型自身的計算。
圖 5.13 虛幻引擎的材質編輯器。
除了組合,還有一些其他的重要設計策略對現代材質系統需要考慮,例如需要在支持多平臺的情況下最小複製着色代碼。考慮到平臺不同,着色語言不同,API不同,需要考慮的東西很多。迪斯尼的着色系統是一個很好的代表來解決這個問題,它使用專門的預處理層來處理用自定義着色語言寫的着色程序。這就使得材質的編寫和平臺無關,可以自動翻譯成對應平臺的着色語言及實現。
材質系統還需要有良好的性能。除了對着色變動進行專門的編譯外,材質系統還有一些其他常見的優化。迪斯尼着色系統和虛幻引擎會自動檢測在一次DrawCall中保持不變的計算(例如前面提到的冷色值計算和曖色值計算),並將其移到着色器之外。另外一個示例是迪斯尼中使用的範圍界定系統,用以區分不同頻率更新的常量(例如,逐幀,逐光照,逐對象),在適當的時間更新每組常量,可以減少API的開銷。
5.4 鋸齒和抗鋸齒(Aliasing and AntiAliasing)
想象一下,一個大的黑色三角形在白色背景上緩慢移動,由於屏幕網格單元被三角形覆蓋,代表該單元的像素值的強度應該平穩下降。在大部分基本着色器中,一旦網格單元中心被覆蓋,則像素會立馬從白色變成黑色。標準的GPU渲染也不例外。可參閱圖5.14最左列。
用像素呈現三角形,邊緣呈現鋸齒狀,因此這種視覺瑕疵會被稱爲“鋸齒”,更正式講,應該稱爲走樣(aliasing),而爲避免這個問題所用技術稱爲反走樣技術(antialiasing)。
圖5.14 上面一行的三幅圖展示了三角形、直線和點的不同等級的反走樣。下面一排是上面一排的放大版。最左列的每次採樣使用的都是一個像素,這意味着沒有使用反走樣。中間列每個像素有四次採樣,而右側列每個像素有八次採樣。
5.4.1 採樣和濾波理論
渲染圖像的過程本質上是一個採樣任務。圖像的生成色對三維場景進行採樣的過程以給圖中每個像素附上顏色。爲了使用紋理映射,紋素需要重新採樣才能在不同條件下有良好的效果。爲了在動畫中生成一組圖像,通常以恆定的時間間隔對動畫進行採樣。
圖5.15展示瞭如何對連續信號以均勻間隔進行採樣,即離散化。採樣的目的是爲了讓信息數字化。這麼做可以減少信息量,但是採樣後的信號需要重構來恢復原始信號,這是採樣對信號進行濾波完成的。
圖5.15 左圖是連續信號,中間圖是採樣後信號,通過對採樣信號重構後可恢復成原始信號,見右圖。
當採樣完成後,走樣就可能出現。這是一個並不想要的瑕疵,需要對抗走樣的發生。在古老的西方人看來,一個經典的例子就是用電影攝像機拍攝旋轉木馬的車輪。輻條移動速度如果比攝像機拍攝圖像的速度快得多,輪子看起來似乎在緩慢旋轉,甚至看起來沒有旋轉。如圖5.16所示。之所以會出現這種情況,是因爲輪子的旋轉被拍攝進圖像中需要花費一定的時間,被稱爲時間域走樣(temporal aliasing)。
圖5.16 最上面一排是車輪的輻條(或者說是原始信號)。第二排的採樣不足,看起來車輪上朝相反的方向移動,這是採樣頻率太低的原因導致的走樣。第三排中,採樣率正好是每轉一輪採樣兩次,因此無法確定輪子是朝哪個方向旋轉,這就是Nyquist極限。在第四排中,採樣率每轉一輪要多兩次採樣(是Nyquis極限的兩倍,4次採樣),就可以看到輪子轉動的正確方向。
計算機圖像中經常出現的走樣是光柵化一個線或三角形時出現的鋸齒。當一個信號被採樣的頻率太低時會發生走樣,如圖5.17所示。爲了正確的採樣,採樣率必須要大於需要採樣的信號的最大頻率的兩倍。這被稱爲採樣定理,採樣率被稱爲Nyquist速率或Nyquist極限。Nyquist極限如圖5.16所示。該定理使用的術語“最大頻率”意味着該信號受到頻帶限制。換句話說,信號必須要相對於相鄰兩個樣本之間的間隔足夠平滑。
圖 5.17 藍色實線表示的是原始信號,紅色圓點表示的是均勻間隔的採樣點,綠色虛線表示的是重構後的信號。上圖展示了採樣率太低,導致重構後的信號的頻率較原始信號的頻率低太多,這時候出現了走樣。而下圖展示了採樣率正好是原始信號的頻率的兩倍,重構後的信號就是一條水平線,證明了如果採樣率再稍微增加點,則完美重構出原始信號成爲可能。
使用點採樣三維場景時,是不會限制帶寬的。三角形邊緣,陰影的邊界和其他現象會產生不連續的信號,則會有趨於無限的頻率出現。無論對物體進行多緊密的採樣,都會有小到無法採樣的地方。因此,使用點採樣來渲染場景是無法避免走樣問題,但是我們幾乎總是使用點採樣。但是,有時會知道信號是受到帶寬限制。例如,將紋理應用於表面時,可以通過像素的採樣率計算出紋理採樣的頻率,如果此頻率低於Nyquist極限,則無需採取任何特殊措施就可以得到正確的紋理採樣。如果頻率過高,則可使用多種算法對紋理進行寬帶限制。
重構(Reconstruction)
重採樣(Resampling)
重採樣主要是放大或縮小採樣信號。假設原始採樣點的座標都是整數(0、1、2,…),即樣本之間具有單位間隔。進一步假設在重採樣後,新的採樣點的座標之間的間隔是a。對於a > 1,縮小採樣(降採樣),對於a < 1,放大采樣(升採樣)。
放大倍數相較於更爲簡單,所以從放大開始講起。假設如上一節所示那樣重構了採樣信號,由於限制信號已被完美重建且是連續的,因此需要做的就是以固定間隔對重構的採樣信號進行重採樣,這個過程如圖5.22所示。
圖 5.22 左圖是採樣信號和重構後的信號。右圖是對重構後信號以兩倍採樣率重採樣後的信號,放大了信號。
然而,縮小信號這種方法就不行了。爲了避免走樣, 原始信號的頻率對採樣率來說太高了,取而代之的是,用sinc(x/a)濾波來創建一個連續信號,然後,重採樣,如圖5.23所示。換句話說,這裏用sinc(x/a)作爲濾波,低通濾波的寬度會增加,所以信號很多的高頻部分都被消除了。如圖所示,濾波的寬度(單個sinc的寬度)加倍,會將重採樣率降到了原採樣率的一半。如果將這和數字圖像關聯起來,這類似於首先進行模糊處理(除去高頻部分),然後以低分辨率對圖像進行重採樣。
圖 5.23 左圖是採樣信號和重構後的信號。右圖採樣間隔翻倍後濾波寬度也翻倍,這樣就縮小了信號。
5.4.2 基於屏幕的反走樣(Screen-based Antialiasing)
如果採樣或濾波做的不好,三角形邊緣會出現明顯的瑕疵,陰影邊界,鏡面高光和其他顏色快速變化的現象都可能導致這類問題。本節討論算法針對這些情況有助於提高渲染質量。它們都是基於屏幕的,都是僅對管線輸出的樣本進行操作。沒有最佳的反走樣技術,每個技術都自己的優勢:渲染質量,捕獲外形細節的能力,內存成本,GPU需求和速度。
在圖5.14所示的黑色三角形示例中,一個問題是低採樣率。在每個像素的網格單元的中心採樣一個樣本,判斷一個單元是否被覆蓋是看單元中心是否被三角形覆蓋。通過對每個屏幕網格單元使用更多樣本並且以某種方式混合它們,可以得到更好的顏色,如圖5.24所示。
圖 5.24 左圖展示的是一個像素只進行一次採樣,因爲三角形並未覆蓋住採樣點,所以像素顏色是白色,儘管像素部分被紅色三角形覆蓋。右圖是一個像素用了四次採樣,並且有兩個採樣點被紅色三角形覆蓋,所以像素的最終顏色爲粉紅色。
基於屏幕的反走樣方案的一般策略是對屏幕進行採樣,然後對樣本加權求和產生一像素顏色,P:
其中n表示一個像素被採樣的次數。c(i,x,y)函數是採樣顏色,Wi是權重(範圍爲[0,1])。根據樣本在序列中的位置(1,…,n)來獲取樣本的位置,函數還可以選擇使用像素位置的整數部分(x,y)。換句話說,每個樣本在屏幕網格上採樣的位置都不同,並且對不同的像素選擇採樣不同的採樣模式。在實時渲染中通常使用的是點採樣。因此,c函數可以被視爲兩個函數。首先,f(i,n)檢索採樣點在屏幕上的浮點位置(xf , yf ),然後在屏幕上該位置進行採樣,即檢索該精準位置處的顏色值。其中另外一個變量是Wi,即每個樣本的權重,這些權重加在一起等於1。在實時渲染系統中每個樣本的Wi都是一樣的,例如,Wi= 1/n。對於圖像硬件的默認模式是,對像素中心的的一次採樣,也就是反走樣方程中最簡單的例子。
反走樣算法中每個像素計算超過一個完整樣本的算法稱爲超級採樣(supersampling)算法,或者稱爲過採樣算法(oversampling)。從概念上講,全場景反走樣(full-scene antialiasing,FSAA)也是超級採樣反走樣(supersampling antialiasing),以高分辨率渲染場景,然後以相鄰的採樣進行濾波來創建圖像。例如,需要一個1280x1024的圖像,如果離屏渲染出一個2560x2048的圖像,然後對每2x2像素區域求平均,生成的圖像就是所求的圖像,每個像素採樣4次,並使用了盒式濾波過濾,對應着圖5.25中的2x2網格採樣。這種方法的成本很高,因爲所有的子樣本都需要着色和填充,每個樣本都有深度緩衝(z-buffer)。FSAA算法的主要優勢是簡單,此方法的其他低質量版本僅對屏幕一個軸以兩倍採樣率進行採樣,因此稱爲1x2或2x1超級採樣。通常爲了簡化,使用的是2的冪次方的分辨率和盒式濾波。NVIDIA的動態超級分辨率功能就是超級採樣的一種更精細形式,場景用更高的分辨率進行渲染,使用13個樣本高斯濾波來生成所需圖像。
圖 5.25 一些採樣方法的對比。Quincunx共享了邊角樣本,並且中心樣本的加權值佔整個像素的一半。2x2的旋轉網格(2x2 RGSS)比2x2正網格(2x2 grid)能捕獲更多水平邊緣上的灰度值。同樣,8 rooks 模式要比4x4網格(4x4 grid)模式在這些線上能捕獲更多灰度值,儘管使用的樣本數要少。
有一個和超級採樣有關的採樣方法是基於積累緩衝(accumulation buffer)的。此方法使用了和所需圖像相同分辨率的一個緩衝區,代替了很大的離屏緩衝,但每個通道具有更多的顏色位。對場景進行2x2網格採樣,需要生成4張圖像,並根據需要視圖在屏幕x和y軸方向上移動半個像素距離。生成的每個圖像都是基於網格單元內的不同採樣位置。額外花銷就是不得不每幀多繪製幾次場景,並把結果複製到實時渲染系統的算法中,這個成本也很貴。當需要高質量的效果的時候,這種方式很有用,因爲每個像素可以使用的樣本數和採樣位置都是沒有限制的。積累緩衝曾經由單獨的硬件支持,如今OpenGL API可直接支持,但在3.0版本中已啓用。在現代GPU中,積累緩衝概念可以在像素着色中實現,通過對輸出緩衝使用一個更高精度的顏色格式。
當物體邊緣,鏡面高光和其他尖銳陰影引起的顏色突變時,是需要進行一些額外的採樣。使陰影變柔和,高光更平滑,通常可以避免走樣。一些特定類型物體可以通過增加大小,例如電線,來確保它們在長度上每個位置至少覆蓋一個像素。物體邊緣的走樣仍是採樣的主要問題。可以根據分析,在渲染過程中檢測到物體邊緣則需考慮到走樣問題,但着會比簡單獲取更多樣本開銷更大且更不穩定。然而,GPU功能,像保守光柵化(conservative rasterization)和光柵化順序視圖(rasterizer order views)爲此打開了新的可能性。
像超級採樣和積累緩衝這類技術都是對着色和深度完全單獨計算的,因爲每個樣本都會完整執行一次像素着色過程,所以成本相當高,整體上效率很低。
多重採樣反走樣(multisampling antialiasing,MSAA)每個像素只執行一次像素着色過程,並且會在樣本間共享該結果,從而降低了高計算成本。每個像素有四個採樣位置,並且每個都有自己的顏色和深度值,但對每個片元的像素着色過程只評估一次。如果MSAA所有的採樣位置都被片元覆蓋,則對像素中心進行像素着色評估。相反,如果片元僅包含較少的採樣位置,則計算像素着色的樣本位置會移動到更好的位置。這樣可以避免圖像邊緣採樣的丟失。這個調整的位置稱爲重心採樣(centroid sampling)或重心插值(centroid interpolation),並且是由GPU自動完成。如圖5.26所示。
圖 5.26 中間圖展示的是一個像素被兩個物體覆蓋,其中紅色物體覆蓋了三個採樣點,藍色物體覆蓋一個採樣點,綠色圓點是像素着色的評估位置。因爲紅色物體覆蓋的像素中心,所以像素中心是紅色物體像素着色的評估位置,而藍色物體的像素着色評估位置就是採樣點位置。對MSAA來說,每個採樣點都自己的顏色和深度值。右圖展示的是EQAA的2f4x格式,四個採樣點都有對應的ID,並由表格記錄了每個ID值對應的顏色和深度值。
多重採樣反走樣(MSAA)要比淡出的超級採樣反走樣(SSAA)機制快得多,因爲每個片元只需要執行一次像素着色。它着重於已更高的速率對片元的像素採樣,並共享其計算結果給最終着色顏色。它爲進一步解耦採樣和覆蓋節省了很多的內存,反過來又是的反走樣變得更快,涉及的內存越少渲染速度久越快。NVIDIA在2006年推出了覆蓋採樣反走樣技術(coverage sampling antialiasing,CASAA),AMD隨後推出了增強質量反走樣技術(enhanced quality antialiasing,EQAA)。這些技術都是通過以較高的採樣率且僅存儲片元覆蓋範圍來工作的。例如,EQAA的2f4x模式存儲的顏色值和深度值,在四個採樣點間共享。這些顏色值和深度值並不存儲在特定位置,而是存儲在表格中,每個採樣點只需要一個bit來指定和它所在位置存儲相關的兩個值。圖5.26展示了覆蓋的樣本對每個片元最終像素顏色的貢獻。如果存儲的顏色數量超出了限制,則刪掉存儲的顏色並標記爲未知。這些顏色對最終顏色沒有貢獻。對大多數場景,很少有像素同時被三個或更多不透明片元包含。但是,對一些最高質量的遊戲,例如《Forza Horizon 2 went,極限競速2:地平線》,採樣了4倍MSAA,儘管EQAA具有很好的性能優勢。
一旦將所有幾何都渲染進一個多重採樣緩衝中,隨後就需要進行解析操作。這會將所有采樣顏色求平均值來決定像素的最終顏色,需要注意的是,當使用具有高動態範圍顏色值的多重採樣時,會有問題。這種情況下,爲了避免瑕疵,通常需要在解析前進行色調映射(tone-map)。這可能很昂貴,但是可以使用簡單近似色調映射的函數或其他方法。
默認情況下,MSAA採樣盒式濾波進行解析。2007年,ATI引入了自定義濾波反走樣(custom filter antialiasing,CFAA),使用或窄或寬的帳篷式濾波,這些濾波會延伸到其他的像素單元中,這種模式已經被EQAA取代了。在現代GPU中,像素着色器或計算着色器都可訪問MSAA樣本,無論使用什麼濾波進行重構,包括了從周圍像素樣本中取樣。較寬的濾波會減少走樣,儘管會丟失一些尖銳細節,Pettineo發現,使用濾波寬度爲2個像素或3個像素的三次平滑步長(cubic smoothstep)或B樣條(B-spline)濾波效果最好。但這會有性能成本,因爲即使使用的是默認的盒式濾波解析對自定義着色都會花費很長時間,而一個更寬的濾波內核意味着增加更多的採樣訪問成本。
NVIDIA的內置TXAA支持類似於使用比單個像素更寬的重構濾波,效果不錯。TXAA和較新的MFAA(多幀反走樣,multi-frame antialiasing)機制都使用了時間域反走樣(temporal antialiasing,TAA)技術,可以使用前先幀的結果來改善圖像。
想象一下,通過生成一系列圖像來“手動”執行採樣,其中每個圖像都是在像素內不同採樣點位置進行渲染得到。這種偏移是通過在投影矩陣上附加一個微小平移來完成的。生成的圖像越多,計算平均值的結果越好。這個使用了多偏移圖像的概念被時間域反走樣算法用到了,使用MSAA或其他方法生成一張圖像,然後和先前的圖像進行混合,通常用到2幀或4幀。較老的圖像的權重較輕,因爲這會導致幀閃爍,所以通常只對最近一幀和當前幀進行相等加權。由於每幀的樣本位於不同的子像素位置,因此這些樣本的加權總和要比單個幀具有更好的邊緣覆蓋率評估。如果使用最近兩幀進行加權平均會得到更好的效果。每幀都不需要額外的採樣,使得這種方法很吸引人。甚至可以使用時間域採樣來生成一些低分辨率圖像,然後這些圖像會被放大到顯示器分辨率。另外,光照算法或其他技術可以使用較少的樣本,而是通過混合多幀來得到更好的結果。
要想爲靜態場景提供反走樣且步添加額外採樣成本,使用時間域反走樣的時候會有點問題。如果每幀的權重不一樣,在靜態場景中的物體可能會出現,而快速移動物體或快速移動攝像機可能會導致重影,這是因爲上一幀對當前幀造成的影響。重影的一種解決方案是近對緩慢移動的物體進行反走樣處理。另外一種重要方法是使用重投影(reproduction)來關聯先前和當前幀的對象。在這些方案中,物體的移動向量存儲在一個單獨的“速度緩衝(velocity buffer)”中,這些向量用來關聯先前和當前幀的,即從當前像素位置減去向量來找到上一幀物體表面的顏色像素。因爲不需要額外的採樣,所以時間域反走樣也不需要多少額外的工作量,這種算法近幾年引起了廣泛的關注和採用。之所以會有關注,是因爲延遲着色技術(deferred shading)和MSAA及其他多重採樣技術不兼容。Wihlidal介紹瞭如何將EQAA,時間域反走樣和各種濾波技術組合起來應用到checkerboard 採樣模式中,提高質量的同時還降低了像素着色的調用次數。
採樣模式(Sampling Patterns)
有效的採樣模式是反走樣的一個關鍵因素。Naiman指出,人類在近水平和近垂直邊緣上對走樣最爲敏感,傾斜度近45度的邊緣是第二敏感。旋轉網格超級採樣(Rotated grid supersampling, RGSS)採樣旋轉正方形方式在像素內提供了更多的水平和垂直方向上的分辨率,如圖5.25有展示這種模式。
RGSS是一種拉丁超立方體(Latin hypercube)或N-rooks模式採樣,將n個採樣放在nxn網格中,每行和每列一個。在RGSS中,4個採樣點分別位於4x4子像素網格的行列上。和常規2x2網格模式相比,這種模式特別適合捕獲水平和垂直方向上的邊緣。
N-rooks模式是創建一個好採樣模式的開始,但還不夠。例如,所有的採樣點都沿着子像素網格的對角線放置,所以幾乎平行於這條對角線方向上的邊緣的效果很差,如圖5.27所示。爲了更好的採樣,要避免兩個採樣點彼此靠的太久,還希望採樣點分佈均勻在整個區域內。爲了形成這種模式,分層採樣技術(例如,拉丁超立方體採樣,Latin hypercube sampling)和其他方法(例如,抖動(jittering),霍爾頓序列(Halton sequences),柏松圓盤(Poisson disk)等採樣技術)相結合。
圖 5.27 N-rooks採樣。左圖是一個標準N-rooks模式,但是它在捕獲沿着對角線的三角形邊緣的效果很差,因爲隨着三角形移動,這些採樣點位置要麼全部在三角形內部,要不全部在三角形外面。而右圖的模式則更有效果。
實際上,GPU製造商通常將這種採樣模式硬連接到硬件中,以進行多重採樣反走樣,圖5.28展示了實際中使用的一些MSAA模式。對時間域反走樣,覆蓋模式(coverage pattern)是編程人員想要的。因爲採樣點的位置可逐幀變化。
圖 5.28 適用於AMD和NVIDIA圖像加速的MSAA採樣模式。綠色圓點是最終着色採樣的位置,紅色圓點是計算和保存採樣點的位置。從左到右,依次是2x,4x,6x(AMD),和8x(NVIDIA)採樣。
雖然子網格模式可以很好地近似每個三角形如何覆蓋網格單元,但是這並不理想,場景可以由任意物體組成,在屏幕上有可能會很小,這意味着沒有任何採樣率會完美捕捉到它們。如果這些微小物體或特徵形成圖安,以恆定間隔採樣的話可能會導致莫爾條紋或其他干涉圖案。網格模式在超級採樣中特別容易出現走樣。
有一種解決方案是使用隨機採樣(stochastic sampling),如圖5.28所示的模式。隨機化會讓瑕疵看起來像噪聲,人類的視覺系統對此會更容易忘記。結構較少的模式雖有幫助,但是在像素之間重複時仍會有走樣。所以有一種解決方案是對不同的像素使用不同的採樣模式,或動態改變每個採樣點的位置。在過去的幾十年裏,偶爾會有硬件支持交錯採樣,即一組像素中的每個像素具有不同的採樣模式。例如,ATI的SMOOTHVISION允許每個像素最多16次採樣,並且最多可以用16個用戶定義的採樣模式。
一些其他的GPU支持算法也值得注意。有一種實時的反走樣機制:NVIDIA的舊版Quincunx方法,會讓一個採樣點影響超過一個像素。“Quincunx”是指五個對象的排列,四個在一個正方形上,第五個在正方形中心。Quincunx 多重採樣反走樣方法就是使用的這種模式,讓四個採樣點分佈在像素的四個角上,見圖5.25,每個採樣點的值會分配給四個相鄰的像素。每個採樣點的權重分配是:中心採樣點的權重值是1/2,而四個角上的採樣點的權重值都是1/8。正是這種分享機制,每8個採樣點平均只需要2個像素,結果明顯要優於行2 sample FSAA方法。該模式近似於帳篷式濾波,如上一節所說,要優於盒式濾波。
如果一個像素一次採樣,Quincunx採樣模式也可以應用在時間域反走樣上。每一幀和前一幀都會每個軸上有半個像素的偏移,偏移方向在幀之間交替。前一幀給當前幀提供像素四個角的採樣,然後用雙線性採樣(bilinear interpolation)快速計算出對每個像素的貢獻,最後和當前幀的結果取平均值。每個幀的權重值相等時,意味着在靜態視圖裏沒有閃爍,而在齊次移動物體上還有問題,但仍比每幀每個像素僅一次採樣的效果好很多。
如果在單幀中使用Quincunx模式,因爲在像素邊界共享採樣點,所以像素邊界採樣成本很低,只有兩次採樣。RGSS模式更合適捕獲近似水平和近似垂直方向上的灰度值。在移動設備圖形上,最先使用的是FLIPQUAD模式,結合了這兩個理想功能。它的優勢是每個像素只有兩次採樣,並且質量類似於RGSS(成本是每個像素四次採樣)。這個採樣模式如圖5.29所示。
圖 5.29 左側展示的是RGSS採樣模式,成本是每個像素四次採樣。通過將這些採樣點位置移動到像素邊緣,採樣點就會被各像素邊緣共享。但是,其他的每個像素必須具有一個反射採樣模式,如右圖所示。右圖的模式被稱爲FLIPQUAD採樣模式,成本是每個像素兩次採樣。
和Quincunx一樣,兩次採樣的FLIPQUAD模式也可以用在時間域反走樣中。Drobot 解決了他在HRAA(hybrid reconstruction antialiasing)工作中的問題,他在探索兩次採樣中哪種模式最好,最後他發現FLIPQUAD是他探索的五種模式中最好的。checkerboard模式也同樣適用於時間域反走樣。EI Mansouri 討論了用兩個採樣的MSAA來創建一個checkerboard渲染,以減少成本的同時解決走樣問題。Jimene使用了SMAA時間域反走樣技術,這個技術的反走樣質量可以響應引擎負載而改變。Carpentier和Ishiyama通過旋轉採樣網格45度,在像素邊緣進行採樣,他們將這種時間域反走樣機制和FXAA結合在一起,可以在高分辨率上高效渲染。
形態學方法
走樣通常是邊緣引起的,例如幾何形狀邊緣,尖銳的陰影或明亮高光形成的邊緣。2009年Reshetov提出了一種算法,稱爲形態反走樣(morphological antialiasing,MLAA)。“形態”意味着和物體的形狀或結構有關。早在1983年,Bloomenthal就在這一領域做了早期工作。
這種反走樣是在後期處理中完成的,也就是,在完成渲染後,然後把渲染結果進行反走樣處理。自2009年以來,以及開發出了各種各樣的技術。那些依賴其他緩衝(深度緩衝、法線緩衝等)的算法,像SRAA(subpixel reconstruction antialiasing),可以提供更好的結果。分析方法,例如幾何緩衝反走樣(geometry buffer antialiasing,GBAA)和距離邊緣反走樣(distance-to-edge antialiasing,DEAA),讓渲染器計算了三角形邊緣的位置的附加信息,例如,邊緣距離像素中心的距離是多少。
大部分機制只需要顏色緩衝,意味着它們還需要改善陰影邊界,高光或各種預先應用的後期處理技術。例如,定向局部反走樣(directionally localized antialiasing,DLAA),近似垂直的邊緣應水平模糊,同樣,近似水平的邊緣應該垂直模糊。
邊緣檢測的更復雜形式是嘗試去找到可能以任意角度包含邊緣的像素,並確定它的覆蓋。檢查周邊潛在的邊緣,儘可能的重建出原始邊緣所在位置。邊緣對當前像素的貢獻可以用來和相鄰像素的顏色進行混合。如圖5.30所示。
圖 5.30 形態反走樣(morphological antialiasing)。左圖是走樣圖像。目的是確定邊緣可能的方向。中間圖展示了通過檢測相鄰像素來記錄是邊緣的可能性,給出了兩種可能是邊緣的示例。在右圖,找到最佳猜測邊緣,把相鄰像素的顏色和當前像素的中心位置的顏色進行混合,混合比例和像素覆蓋率成比例。對圖中每個像素重複此過程。
基於圖像的算法有幾種容易會人歧途。首先,如果兩個對象之間的色差低於算法的閾值,可能會檢測不到邊緣。具有三個或更多不同表面重疊的像素很難檢測。具有高對比度或高頻元素的表面,像素之間顏色快速變化,會導致算法丟失邊緣。特別的,將形態學反走樣技術用在文本上,文本通常會有影響。物體的邊角會是一個挑戰。單個像素的改變可能會引起邊緣重構發生較大變化,會在幀與幀之間有明顯的瑕疵。解決此問題的方法有,使用MSAA覆蓋蒙版(MSAA coverage masks)來改善邊緣檢測。
形態學反走樣方案只能使用被提供的信息。例如,一個物體的寬度要小於一個像素的寬度,例如電線或繩索,會在屏幕上沒有覆蓋住像素中心位置有空隙。這種情況下,採集更多樣本會提高質量,僅僅依靠基於圖像的反走樣上不行的。另外,執行時間可以根據查看的內容而變化,例如,一片草地所需要的反走樣時間是天空的三倍。
綜上所述,基於圖像的反走樣技術只需要少量的內存及處理成本,所以在很多應用程序中都有用到。最流行的兩種算法是快速近似反走樣(fast approximate antialiasing, FXAA)和子像素形態反走樣(subpixe morphological antialiasingl,SMAA),部分原因是兩者都提供了不同設備的可靠的免費的源代碼。都有自己可用的各種設置,在速度和質量間進行權衡。每幀的成本通常在1-2毫秒的範圍內,這是遊戲願意花費的時間。最後,兩種算法都可以使用時間域反走樣(TAA)。Jimenez提出了一種改進SMAA的實現,比FXAA更快,並給出了時間域實現方案。最後,推薦讀者閱讀Reshetov和Jimenez的形態技術及它們在電子遊戲中的使用。
5.5 透明度,alpha值,合成(Transparency,Alpha,Compositing)
有許多不同的方法可以使光線透過半透明物體。對於渲染算法,可以大致分爲基於燈光效果和基於視圖效果的。基於光的效果是指物體引起光衰減或轉移從而照亮場景中其他的物體。基於視圖的效果是指呈現半透明物體本身的效果。
本節會討論基於視圖的半透明最簡單形式,把半透明物體當作其後面物體顏色的衰減器。一種給人透明感的方法稱爲screen-door transparency。這個方法是用像素對齊的checkboard填充模式來渲染透明三角形,也就是說,其他像素也會被渲染,這樣在半透明物體後的物體就部分可見。通常,屏幕上的像素距離足夠近,以至於checkboard本身不可見。這個方法的缺點是在屏幕一塊區域只能渲染一個半透明對象才讓人信服。例如,如果在藍色對象上繪製了半透明的紅色對象和半透明的綠色對象,則三種顏色中,只會有兩種可以顯示在屏幕上。該技術的一個有點就是它的簡單性,透明物體可以隨時以任何順序繪製,且不需要特別的硬件。
由Enderton等人提出了隨機透明度,利用subpixel screen-door 掩膜(masks)和隨機採樣相結合。一個理由是,通過噪聲,利用隨機點畫模式來表示一個片元的alpha覆蓋,如圖5.31所示。每個像素需要大量的採樣才能看起來結合合理,並且這對應着需要大量的內存。優勢就是,不需要混合,並且已經考慮到了反走樣,透明度和其他只顯示部分像素的現象。
圖 5.31 隨機透明度。放大區域展示的是噪聲的利用。
大部分透明度算法會將透明物體的顏色和其後面物體的顏色進行混合,爲此,alpha blending(alpha 混合)的感念被提出了。當一個物體被渲染在屏幕上時,每個像素都要使用到RGB顏色值和z緩衝區深度值,還有一個就是alpha值,它描述了一個物體片元對一個像素的不透明度和覆蓋度。alpha值爲1.0,則意味着物體上不透明的,並且像素的關注區域都被覆蓋住了。等於0.0則意味着片元完全透明,像素完全沒有覆蓋到。
alpha值可以視爲不透明度或覆蓋率,視情況而定。例如,肥皂泡的邊緣會覆蓋像素的四分之三,即0.75,幾乎近似於透明,可以讓十分之九的光線透過到達眼睛,所以它由十分之一是不透明的,即0.1。那麼其alpha值爲0.75x0.1 = 0.075。然而,如果我們利用了MSAA或類似的反走樣算法,則採樣點本身像需要考慮進覆蓋率範圍內。四分之三的採樣點將收到肥皂泡的影響,然後在這些採樣點中,我們將使用0.1不透明值作爲alpha值。
5.5.1 混合順序(Blending Order)
爲了使物體看起來半透明,需要把物體渲染在現有場景的最上層,並且它的alpha值得小於1.0。每一個被該物體覆蓋到的像素,都會收到一個來自像素着色器的RGBA值,將這個片元的值和混合前的像素顏色使用over算子(over operator)進行混合,如下:
其Cs是透明物體的顏色(稱爲源),as是物體的alpha值,Cd是混合前的像素顏色(稱爲目標),C0則是最後混合後的最終顏色。實際上,如果RGBA是不透明的(as=1.0),則方程課簡化爲像素的顏色完全替換爲物體的顏色。
例子:混合(Blending)。一個紅色半透明物體渲染在一個藍色背景上,物體的RGB爲(0.9,0.2,0.1),背景的RGB爲(0.1,0.1,0.9),物體的透明度爲0.6。則混合後的顏色爲:
最後顏色爲(0.58,0.16,0.42)。
over算子爲渲染對象提供了半透明的外觀。通過這種方式實現了透明,從某種意義上講,只要透過它看到其後面的物體,就可以視爲透明的。使用over因子來模擬真實世界中的薄紗織物效果,織物後面的對象在視圖中被遮擋了一部分,因爲織物的線上不透明的。實際中,寬鬆的織物的alpha覆蓋率隨着角度變化。這裏的重點是alpha模擬了材質覆蓋像素的程度。
over算子在模擬其他的透明效果時效果不太令人信服,尤其是在透過有色玻璃或透過塑料觀看時。在現實世界中,紅色透明物體放置藍色物體前面會讓藍色物體看起來較暗,因爲反射的光線能透過紅色物體的很少,如圖5.32所示。當在混合時用over算子,結果是藍色和紅色疊加在一起。如果是兩種顏色相乘,結果會好點。
圖 5.32 一個紅色的織物和一個紅色塑料透明物體,具有不同的透明效果,注意,陰影也不同。
在基本的混合階段操作中,over通常是用於透明效果,另外一種有用的操作是additive混合(additive blending),將像素簡單的求和。如下,
這種混合模式可很好的用於發光效果,例如閃電和火花,不讓後面的像素衰減,而只會使它們變亮。但是這種模式並不適用於透明效果。對於具有好幾層半透明的表面,例如煙霧或火,additive blending讓其顏色更具有飽和。
爲了正確的繪製透明物體,需要在不透明物體之後繪製。首先,關閉混合,渲染所有的不透明物體,然後打開混合,再繪製透明物體。
z-buffer的一個限制是,每個像素僅存儲一個對象。如果在同一個像素上有好幾個透明物體,僅依靠z-buffer是不能正確繪製出透明效果的。當在透明表面上繪製其他物體時,通常需要從後到前的順序進行渲染,不這麼做的話,會得到錯誤的效果。一種得到渲染順序的方法是對存儲每個物體的質心沿着視圖方向的距離,並進行排序。這種粗略的排序效果不錯,但是還有很多問題。首先,這個順序只是一個近似值,可能有較遠的物體出現在了較近的物體前面。物體不可能在任何角度的視圖上解析成一個個網格,除非將每個網格分解成單獨的碎片。如圖5.33的左圖所示。即使是單個具有凹面的網格,當在屏幕上出現重疊時,這時的排序是有問題的。
圖 5.33 左圖是僅利用了z-buffer的渲染透明物體示例,以任意順序渲染網格會有嚴重錯誤。右圖是利用了深度剝離(depth peeling )技術可以得到正確效果,但成本會變多。
儘管如此,但是由於它的簡單性和快速,且不需要額外的內存或特殊GPU支持,這種粗略的排序還是經常被用到的。如果要實施,最好在執行透明度操作的時候關閉z深度替換功能。也就是說,z-buffer仍然測試正常,但是存在的曲面不會改變存儲的z深度值,最接近的不透明表面的深度保持不變。用這種方式,所有的透明物體都會以某種形式出現,而不是在攝像機旋轉時(沒關閉深度替換功能的話,排序會出現變化)突然出現或消失。當然還有其他的技術可以幫助改善外觀,例如,繪製每個透明網格兩次,先渲染背面然後渲染正面。
可以修改over算子方程,以從前到後混合得到相同的結果,這種混合模式稱爲under算子。
注意,under算子要求目標保持alpha值,而over算子是不需要的。換句話說,目標不是不透明的,所以需要具有alpha值。under公式和over公式很像,但是源和目標交換了。另外,計算alpha的公式和順序無關,交換源alpha和目標alpha,結果是相同的。
alpha公式考慮的是片元的alpha覆蓋度。Porter和Duff指出,由於不知道每個片元的覆蓋區域的形狀,假設每個片元覆蓋另一片元是按其alpha比例來覆蓋的。例如,如果as=0.7,那麼像素被分成兩部分,其中0.7被源片元覆蓋,另外0.3沒有。如圖5.34所示。
圖 5.34 一個像素和兩個片元,s和d。將兩個片元沿不同的軸對齊,兩個片元對像素的覆蓋率是各自獨立的,兩個片元都覆蓋住的面積可以用公式求出。兩個面積相加,然後減去重疊的面積。
5.5.2 順序無關的透明度(Order-Independent Transparency )
under算子的方程將所有透明對象繪製到一個單獨的顏色緩衝區,然後利用over算子將緩衝區的顏色混合到場景的不透明物體的視圖上。另外一個利用under算子的是順序無關的透明度(order-independent transparency,OIT)算法,深度剝離(depth peeling)。順序無關意味着應用程序不需要進行排序。深度剝離後面的思想是利用兩個z-buffers和多個passes。首先,渲染第一個pass,所有表面的深度值,包括透明表面,都存在了第一個z-buffer中。在第二個pass渲染所有的透明物體。如果一個物體的深度值和第一個z-buffer中的某值匹配上,我們就知道這是最近的透明物體的,把它的RGBA值存儲到一個單獨的顏色緩衝區中。然後剝離該層,如果保存的透明物體的深度值超過了第一個深度值,並且更靠近,那麼這個深度值就是第二個靠近的透明物體。通過幾個passes連續剝離和連續使用under算子添加透明層後,然後把透明圖像混合到不透明的圖像上,如圖5.35。
圖 5.35 每一個深度剝離的pass都會繪製一個透明層。左圖展示的是第一個pass,這層是明顯可以肉眼可見的。中間圖是第二層,繪製的是第二靠近靠近透明表面,本例中是物體的背面,右圖是第三層,是一組第三靠近透明物體的表面。最終結果如圖14.33所示。
已經發展出了好幾種這種機制的變種。例如,Thibieroz給出了一種算法,優點是能夠立馬混合透明值,意味着不需要特殊的alpha通道。深度剝離有一個問題,多少個passes對應能捕獲多少個透明層。一種硬件解決方案是提供一個像素繪製計數器,該計數器會顯示在渲染過程中寫入了多少個像素,當渲染一個pass時沒有像素,則渲染完成。這時用under算子的優勢就是,最重要的透明層,例如眼睛看到的第一層,會最早繪製。每個透明表面會增加當前像素的alpha值。如果alpha值接近1.0。混合會讓當前像素變得幾乎不透明,因此距離較遠的物體的影響可以忽略不計。從前到後的剝離可以縮短,如果當渲染的像素數量低於某個值或達到了指定pass固定的次數。但這對從後到前的剝離行不通,因爲通常最近的層時最後繪製,有可能會因提前終止而丟失。 深度剝離雖然有效,但是它時很慢的,每一層的剝離都是所有透明物體的單獨的渲染pass。
以合適的交互速率把透明物體混合在一起的問題不是缺少算法的問題,而是如何有效的將這些算法映射到GPU上的問題。1984年,Carpenter提出了A-buffer,另外一種形式的多重採樣。在A-buffer中,每個渲染的三角形都會爲其完全覆蓋或部分覆蓋的屏幕網格單元創建一個覆蓋蒙版。每個像素都有一個列表來存儲與其相關的片元。不透明的片元可以剔除它後面的片元,類似於z-buffer。所有的片元都是爲了透明表面,一旦所有的列表都完成了,就可以通過遍歷片元並解析每個樣本來生成最終結果。
在GPU上給每個像素一個片元列表的想法在DirectX11發佈後成爲了可能。DirectX11發佈了很多新功能,包括無序訪問視圖(unordered access views)和原子操作(atomic operations)。通過訪問覆蓋蒙版並評估每個樣本的像素着色,可以讓MSAA反走樣技術得以實現。
A-buffer的優勢是隻有每個像素需要的片元需要分配,就像GPU上鍊表實現一樣。在某種意義上,這樣也是不利的,因爲在一幀開始渲染之前不知道存儲量。在場景中有頭髮,煙霧和其他潛在物體都會重疊在透明表面上,從而生成大量的片元。Andersson指出,對一個複雜的遊戲場景,最多50個透明網格物體,例如樹葉,或最多200個半透明粒子可以重疊。
GPU通常有預先分配內存資源,例如緩衝區和數組,鏈表也不例外。用戶需要決定多少內存是足夠的,而內存不足會導致各種問題。Salvi和Vaidyanathan提出了一種方法解決這個問題,多層alpha混合(mult-layer alpha blending),利用了intel提供的GPU特徵,像素同步(pixel synchronization),如圖5.36所示。這種方法比原子操作開銷更小,提供了可編程混合。這種方法重新定義了存儲和混合,當內存不足時可以優雅的降低性能。DirectX 11.3引入了光柵化順序視圖(rasterizer order views),一種緩衝區,可以讓任何支持這個功能的GPU都可以實現這種方法。移動設備有個類似的功能,稱爲瓦片局部存儲(tile local storage),可以實現多層alpha混合。這種算法成本比較昂貴,會降低性能。
圖 5.36 左上圖是傳統的從後到前的alpha混合,因爲排序不對導致渲染錯誤。右上圖使用了A-buffer,效果正確,是非互動結果。左下圖使用的是多層alpha混合(mult-layer alpha blending)。右下圖展示的是A-buffer和多層alpha混合的區別,爲了可見乘以了4.
這種方法是建立在Bavoil等人提出的k-buffer概念上,保存了前面幾層可見層,並進行排序,而更深的層則進行了合併和丟棄。Maule等人使用了k-buffer並對較遠的深層進行了加權平均。加權求和(weighted sum)和加權平均(weighted average)的透明技術和順序無關,都是單pass,幾乎在任何GPU上都可以運行。問題在於它們沒有考慮物體的順序,例如,利用alpha來表示覆蓋率,紅色紗巾在藍色紗布圍巾上顯示出了紫羅蘭的顏色,而不是一條紅色圍巾,上面透着一點藍色。儘管對幾乎不透明的物體的結果很差,這類算法對可視化很有用,對高度透明表面和粒子也效果很好。如圖5.37所示。
圖 5.37 隨着不透明度的增加,物體的順序變得越來越重要。
加權求和透明的公式如下:
其中n表示的是透明表面的數量,ci和ai分別對應其透明值,cd是不透明部分的顏色。兩部分相加就是每個像素最終的顏色。這個方法的問題有:總和飽和,即生成的顏色值要大於(1.0,1.0,1.0),並且背景顏色會取反,因爲alpha的總和可能超過1.0。
通常會選擇加權平均,公式如下:
加權平均的一個限制是,對於相同的alpha,它會均勻混合所有顏色,不會考慮到順序。McGuire 和Bavoil引入了加權混合的與順序無關的透明算法(weighted blended order-independent transparency)。在他們的算法中,到表面的距離會影響權重,越靠近表面權重越大。而且,不是對alpha求平均,u的計算是,將項(1-ai)相乘在一起,然後1減去相乘的結果,這種算法會得出一組表面的真正的alpha平均值。這種算法會給出更令人信服的結果,如圖5.38所示。
圖 5.38 兩個不同攝像機觀察同一個引擎模型,都採樣的是加權混合與順序無關透明算法(weighted blended order-independent transparency)。按距離加權能夠弄清哪些面更靠近觀察者。
一個缺點是,在較大的環境中,物體彼此靠的太近,按距離加權和加權平均的結果會沒什麼區別。另外,隨着攝像機到透明物體的距離改變,深度權重雖然發生了變化,但是這種變化是漸進的。
5.5.3 預乘alpah和合成(Premultiplied Alphas and Compositing)
over算子也可以用於混合圖形或合成渲染對象,這個過程稱爲合成(Compositing)。每個像素中不僅存儲了物體的RGB顏色值也存儲了alpha值。由alpha通道形成的圖像有時稱爲matte(影像形板),它顯示了物體的輪廓形狀。
預乘alpha(premultiplied alpha)就是一種使用合成的RGBa數據的方式,RBG值在使用前先乘以了alpha值。這使得合成over算子方程更高效:
其中 是預乘源通道,替換了公式5.25中的。預乘alpha使得使用over算子和添加混合不需要改變混合狀態,因爲源顏色現在是在混合過程中添加。注意,預乘RGBa值中的RGB部分的值通常不大於alpha值,因爲這樣可創建一個特別明亮的半透明值。
一個白色(1,1,1)三角形在邊緣覆蓋40%的像素,因爲反走樣,像素值會設置成灰色值0.4,則這個像素的顏色值存爲(0.4,0.4,0.4)。如果存alpha值,則爲0.4,是三角形覆蓋的範圍。RGBa值爲(0.4,0.4,0.4,0.4),是一個預乘值。
5.6 顯示器編碼
當我們計算光照、紋理或其他操作的時候,使用的值都假定爲線性的。這意味着加法和乘法會按照預期工作, 然而,爲了避免各種各樣的視覺瑕疵,顯示緩衝區和紋理中使用非線性編碼也需要考慮到。例如,着色器輸出顏色範圍爲[0,1],然後將其提高1/2.2次冪,這就是伽馬矯正(gamma correction)。對傳入的紋理和顏色取反操作。在大多數情況下,你可以讓GPU爲你做這些事情。
當顯示器對線性顏色值進行編碼時,我們的目標是取消顯示傳遞函數的影響,這樣無論我們計算出什麼值,都會發射出相應的輻射水平。例如,如果我們計算出的值是原來的兩倍,我們想輸出的亮度也是原來的兩倍。爲了保證這種關係,我們使用了顯示傳遞函數的逆來抵消它的非線性影響。這種顯示器響應曲線無效的過程又稱爲伽馬矯正。當編碼紋理值時,我們需要顯示傳遞函數來生成一個線性值來給着色使用。圖5.39展示了編碼和解碼在顯示過程中的使用。
圖5.39 左邊是一個GPU shader訪問一個PNG格式的顏色紋理,將其非線性編碼值轉換(藍色)成線性值。經過着色和色調映射後,最終計算出的值被編碼(綠色)並存儲在幀緩衝中。這個值會和顯示傳遞函數會決定發射出的輻射量(紅色)。綠色功能和紅色功能會相互抵消,這樣發射出的輻射量就和線性計算出的值成比例關係。
個人電腦屏幕的標準轉換函數由一個叫sRGB的顏色空間規範來定義。當從紋理讀取值時或顏色緩衝寫入值時,大部分控制GPU的API可以設置成自動應用正確的sRGB轉換。如6.2.2節討論到的生成mipmap也會考慮到sRGB編碼。首先轉換成線性值,然後再進行插值,這樣紋理間雙線性插值也會正確工作。alpha混合中,需要將存儲的值解碼成線性值,然後混合新的值,最後再對新的結果進行編碼。
當值被寫入到幀緩衝中,用於顯示的時候,進行這個轉換很重要的。如果在顯示編碼後進行後期處理(post-processing),這些效果會是在非線性值上進行計算,通常是不正確的效果,也會引起各種瑕疵。顯示編碼可以認爲是一種壓縮形式。考慮這個問題的一個好的方式是,用線性值來執行物理計算,並且無論何時我們想顯示結果或訪問可顯示的圖像(例如 顏色紋理),我們需要使用合適的編碼或解碼變換,將數據轉換成顯示編碼形式或將數據由顯示編碼形式轉換回來。
如果你想手動應用sRGB,有標準轉換方程或一些簡化版本可以使用。實際中,顯示器由若干位的顏色通道控制,例如,消費級別的顯示器通常是8位,級別範圍會是[0,255]。這裏將顯示編碼的級別範圍設爲[0.0,1.0],線性值得範圍也是[0.0,1.0]。我們需要x是線性值,而存儲在幀緩衝的y是非線性編碼值。爲了將線性值轉換到sRBG非線性編碼值,我們使用到了sRGB顯示傳遞函數的逆:
其中,x表示的是線性RGB三個通道中的一個。這個方程會應用到每個通道,然後三個通道生成的值會組合一起來顯示。手動使用這個方程需要注意,錯誤通常由使用編碼顏色來代替它的線性值,還有對一個顏色進行了兩次編碼或解碼。
如果考慮到偏移量和縮放比例,這個函數可以簡化爲:
其中, γ = 2.2,希臘字母 γ就是伽馬矯正這個名字的來源。
靜態或視頻攝像機捕獲的圖像必須轉換成線性值後才能用於計算。你在顯示器或電視上看到的任何顏色都由顯示編碼RGB值,你可以通過屏幕截圖或顏色選取獲得這些值。這些值可以存儲爲PNG,JPEG,GIF等形式的文件,這些文件格式可以直接用於幀緩衝顯示在屏幕上,無需轉換。換句話說,你在屏幕上看到的都是顯示編碼數據。在着色計算中如果使用這些數據,必須將顯示編碼數據轉換回線性值,可以用到sRGB轉換:
其中,y表示一個規範化的顯示通道值,例如存儲在圖像或幀緩衝中的值,範圍在[0.0,1.0]。解碼函數是之前用到sRGB公式的逆。解碼函數和顯示傳遞函數類似,因爲存儲在紋理中的值都是被編碼過的。
最簡單的伽馬顯示傳遞函數是公式5.31的逆:
有時在移動設備或瀏覽器應用上會看到:
這是一種粗略的近似值,但是總比完全忽略的好。
如果我們不關注伽馬矯正,較低的線性值在屏幕上會顯得很暗。一個相關錯誤是如果沒有進行伽馬矯正,有些顏色的色調會發生改變。我們說 γ = 2.2,我們希望從顯示像素中發射出的輻射量和線性值成比例,需要對其進行計算,計算其的1/2.2次冪。線性值0.1對應0.351,0.2對應0.481,0.5對應了0.730。如果不進行編碼,使用這些值的話,會引起發射出的輻射量比需要的少。注意,0.0和1.0並不會發生變化。
忽略伽馬矯正的另外一個問題是着色計算,對物理線性輻射值而言是正確的,但是對非線性值是不對的。如圖5.40所示。
圖5.40 兩個重疊的聚光燈照亮了一個平面。作圖中,在添加完光源(光照度分別爲0.6和0.4)之後沒有執行伽馬矯正,加法是在非線性值上執行,引起了錯誤。注意,左邊的亮度要比右邊的亮度高很多,並且重疊部分的亮度不符合實際。右圖,在添加完光源之後進行了伽馬矯正,燈光本身亮度變得更亮了,重疊部分的亮度也變得合適了。
忽略伽馬矯正也會影響到對邊緣進行反走的質量。例如,一個三角形邊緣被四個屏幕網格單元覆蓋(圖5. 41),三角形的歸一化亮度爲1(白色),背景是0(黑色)。從左到右,網格覆蓋了1/8,3/8,5/8和7/8。所以,如果我們使用合適濾波器,我們希望像素的歸一化線性輻射度分別爲0.125, 0.375, 0.625, 0.875。正確的做法是對線性值進行反走樣,對四個結果值進行編碼操作,如果這步沒有做,那麼像素的輻射度表現出來就會很暗,從右圖就可以看到明顯的邊緣變形。這個叫做roping,因爲邊緣看起來像扭曲的繩索。圖5.42就是這種效果。
圖5.41 左圖,在黑色(用灰色表示)背景上一個白色三角形的邊緣覆蓋了四個像素。如果沒有進行伽馬矯正,中間色調的變暗會引起邊緣的感知的扭曲,如右圖所示。
圖5.42 左圖,反走樣的線是經過了伽馬矯正的。中間圖是經過了部分糾正的,而右邊圖是完全沒有進行伽馬矯正的。