一、概述
在正式開始學習D3D11之前,我們必需首先學習必要的基礎知識。
在這篇文章中,我們將介紹一下Direct3D中常用的一些基本類型和基本繪圖概念。
二、基本繪圖概念和基本類型介紹
2.1 Direct3D概述
Direct3D是一種底層繪圖API,它可以讓我們可以通過3D硬件加速繪制3D世界。從本質上講,Direct3D提供的是一組軟件接口,我們可以通過這組接口來控制繪圖硬件。例如,要命令繪圖設備清空渲染目標(例如屏幕),我們可以調用Direct3D的ID3D11DeviceContext::ClearRenderTargetView方法來完成這一工作。Direct3D層位於應用程序和繪圖硬件之間,這樣我們就不必擔心3D硬件的實現細節,只要設備支持Direct3D11,我們就可以通過Direct3D11的API來控制3D硬件了。
支持Direct3D11的設備必須支持Direct3D11規定的整個功能集合以及少數的額外附加功能。在Direct3D9中,設備可以只支持Direct3D9的部分功能;所以,當一個Direct3D9應用程序要使用某一特性時,應用程序就必須先檢查硬件是否支持該特性。如果要調用的是一個不為硬件支持Direct3D函數,那應用程序就會出錯。而在Direct3D11中,不需要再做這種設備功能檢查,因為Direct3D11強制要求設備實現Direct3D11規定的所有功能特性。
2.2 COM接口
組件對象模型(COM)技術使DirectX獨立於任何編程語言,並具有版本向后兼容的特性。我們經常把COM對象稱為接口,並把它當成一個普通的C++類來使用。當使用C++編寫DirectX程序時,許多COM的底層細節都不必考慮。唯一需要知道的一件事情是,我們必須通過特定的函數或其他的COM接口方法來獲取指向COM接口的指針,而不能用C++的new關鍵字來創建COM接口。另外,當我們不再使用某個接口時,必須調用它的Release方法來釋放它(所有的COM接口都繼承於IUnknown接口,而Release方法是IUnknown接口的成員),而不能用delete語句——COM對象在其自身內部實現所有的內存管理工作。
當然,有關COM的細節還有很多,但是在實際工作中只需知道上述內容就足以有效地使用DirectX了。
注意:COM接口都以大寫字母“I”為前綴。例如,表示2D紋理的接口為ID3D11Texture2D。
2.3 紋理和數據資源格式
2D紋理(texture)是一種數據元素矩陣。2D紋理的用途之一是存儲2D圖像數據,在紋理的每個元素中存儲一個像素顏色。但這不是紋理的唯一用途;例如, 有一種稱為法線貼圖映射(normal mapping)的高級技術在紋理元素中存儲的不是顏色,而是3D向量。因此,從通常意義上講,紋理用來存儲圖像數據,但是在實際應用中紋理可以有更廣泛的用途。1D紋理類似於一個1D數據元素數組,3D 紋理類似於一個3D數據元素數組。但是在隨后的章節中我們會講到,紋理不僅僅是一個數據數組;紋理可以帶有多級漸近紋理層(mipmap level),GPU可以在紋理上執行特殊運算,比如使用過濾器(filter)和多重采樣(multisampling)。此外,不是任何類型的數據都能存儲到紋理中的;紋理只支持特定格式的數據存儲,這些格式由DXGI_FORMAT枚舉類型描述。一些常用的格式如下:
DXGI_FORMAT_R32G32B32_FLOAT :每個元素包含3個32位浮點分量。
DXGI_FORMAT_R16G16B16A16_UNORM :每個元素包含4個16位分量,分量的取值范圍在[0,1]區間內。
DXGI_FORMAT_R32G32_UINT :每個元素包含兩個32位無符號整數分量。
DXGI_FORMAT_R8G8B8A8_UNORM :每個元素包含4個8位無符號分量,分量的取值范圍在[0,1]區間內。
DXGI_FORMAT_R8G8B8A8_SNORM :每個元素包含4個8位有符號分量,分量的取值范圍在[-11,1] 區間內。
DXGI_FORMAT_R8G8B8A8_SINT :每個元素包含4個8位有符號整數分量,分量的取值范圍在[?128, 127] 區間內。
DXGI_FORMAT_R8G8B8A8_UINT :每個元素包含4個8位無符號整數分量,分量的取值范圍在[0, 255]區間內。
注意,字母R、G、B、A分別表示red(紅)、green(綠)、blue(藍)和alpha(透明度)。每種顏色都是由紅、綠、藍三種基本顏色組成的(例如,黃色是由紅色和綠色組成的)。alpha通道(或alpha分量)用於控制透明度。不過,正如我們之前所述,紋理存儲的不一定是顏色信息;例如,格式DXGI_FORMAT_R32G32B32_FLOAT包含3個浮點分量,可以存儲一個使用浮點坐標的3D向量。另外,還有一種弱類型(typeless)格式,可以預先分配內存空間,然后在紋理綁定到管線時再指定如何重新解釋數據內容(這一過程與C++中的數據類型轉換頗為相似);例如,下面的弱類型格式為每個元素預留4個8位分量,且不指定數據類型(例如:整數、浮點數、無符號整數):
DXGI_FORMAT_R8G8B8A8_TYPELESS
2.4 交換鏈和頁面翻轉
為了避免在動畫中出現閃爍,最好的做法是在一個離屏(off-screen)紋理中執行所有的動畫幀繪制工作,這個離屏紋理稱為后台緩沖區(back buffer)。當我們在后台緩沖區中完成給定幀的繪制工作后,便可以將后台緩沖區作為一個完整的幀顯示在屏幕上;使用這種方法,用戶不會察覺到幀的繪制過程,只會看到完整的幀。從理論上講,將一幀顯示到屏幕上所消耗的時間小於屏幕的垂直刷新時間。硬件會自動維護兩個內置的紋理緩沖區來實現這一功能,這兩個緩沖區分別稱為前台緩沖區(front buffer)和后台緩沖區。前台緩沖區存儲了當前顯示在屏幕上的圖像數據,而動畫的下一幀會在后台緩沖區中執行繪制。當后台緩沖區的繪圖工作完成之后,前后兩個緩沖區的作用會發生翻轉:后台緩沖區會變為前台緩沖區, 而前台緩沖區會變為后台緩沖區,為下一幀的繪制工作提前做准備。我們將前后緩沖區功能互換的行為稱做呈現(presenting)。提交是一個運行速度很快的操作,因為它只是將前台緩沖區的指針和后台緩沖區的指針做了一個簡單的交換。
前后緩沖區形成了一個交換鏈(swap chain)。在Direct3D中,交換鏈由IDXGISwapChain接口表示。該接口保存了前后緩沖區紋理,並提供了用於調整緩沖區尺寸的方法(IDXGISwapChain::ResizeBuffers)和呈現方法(IDXGISwapChain::Present)。
使用(前后)兩個緩沖區稱為雙緩沖(double buffering)。緩沖區的數量可多於兩個;比如,當使用三個緩沖區時稱為三緩沖(triple buffering)。不過,兩個緩沖區已經足夠用了。
注意:雖然后台緩沖區是一個紋理(紋理元素稱為texel),但是我們更習慣於將紋理元素稱為像素(pixel),因為后台緩沖區存儲的是顏色信息。有時,即使紋理中存儲的不是顏色信息,人們還是會將紋理元素稱為像素(例如,“法線貼圖像素”)。
2.5 深度緩沖區
深度緩沖區(depth buffer)是一個不包含圖像數據的紋理對象。在一定程度上,深度信息可以被認為是一種特殊的像素。常見的深度值范圍在0.0到1.0之間,其中0.0表示離觀察者最近的物體,1.0表示離觀察者最遠的物體。深度緩沖區中的每個元素與后台緩沖區中的每個像素一一對應(即,后台緩沖區的第ij個元素對應於深度緩沖區的第ij個元素)。所以,當后台緩沖區的分辨率為1280×1024時,在深度緩沖區中有1280×1024個深度元素。
注意:要處理深度的問題,有人可能會建議按照從遠至近的順序繪制場景中的物體。使用這種方法,離得近的物體會覆蓋在離得遠的物體之上,這樣就會產生正確的繪制結果,這也是畫家作畫時用到的方法。但是,這種方法會導致另一個問題——如何將大量的物體和相交的幾何體按從遠到近的方式進行排序?此外,圖形硬件本身就提供了深度緩存供我們使用,因此我們不會采用畫家算法。
為了說明深度緩存的工作方式,讓我們來看一個例子。如下圖示,它展示的是觀察者看到的立體空間(左圖)以及該立體空間的2D側視圖(右圖)。從這個圖中我們可以發現,3個不同的像素會被渲染到視圖窗口的同一個像素點P上。(當然,我們知道只有最近的像素會被渲染到P上,因為它擋住了后面的其他像素,可是計算機不知道這些事情。)首先,在渲染之前,我們必須把后台緩沖區清空為一個默認顏色(比如黑色或白色),把深度緩沖區清空為默認值——通常設為1.0(像素所具有的最遠深度值)。
當我們發現某個像素具有更小的深度值時,就更新該像素以及它在深度緩沖區中的相應深度值。通過一方式,在最終得到的渲染結果中只會包含那些離觀察者最近的像素。(如果讀者對此仍有疑慮,那么可以試着交換本例的繪圖順序,看看得到的計算結果是否相同。)
綜上所述,深度緩沖區用於為每個像素計算深度值和實現深度測試。深度測試通過比較像素深度來決定是否將該像素寫入后台緩沖區的特定像素位置。只有離觀察者最近的像素才會勝出,成為寫入后台緩沖區的最終像素。這很容易理解,因為離觀察者最近的像素會遮擋它后面的其他像素。
深度緩沖區是一個紋理,所以在創建它時必須指定一種數據格式。用於深度緩存的格式如下:
DXGI_FORMAT_D32_FLOAT_S8X24_UINT:32位浮點深度緩沖區。為模板緩沖區預留8位(無符號整數),每個模板值的取值范圍為[0,255]。其余24位閑置。
DXGI_FORMAT_D32_FLOAT :32位浮點深度緩沖區。
DXGI_FORMAT_D24_UNORM_S8_UINT :無符號24位深度緩沖區,每個深度值的取值范圍為[0,1]。為模板緩沖區預留8位(無符號整數),每個模板值的取值范圍為[0,255]。
DXGI_FORMAT_D16_UNORM :無符號16位深度緩沖區,每個深度值的取值范圍為[0,1]。
注意:模板緩沖區對應用程序來說不是必須的,但是如果用到了模板緩沖區,那么模板緩沖區必定是與深度緩沖區存儲在一起的。例如,32位格式DXGI_FORMAT_D24_UNORM_S8_UINT使用24位用於深度緩沖區,8位用於模板緩沖區。 所以,將深度緩沖區稱為“深度/模板緩沖區”更為合適。模板緩沖區是一個比較高級的主題,我們會在以后學習模板緩沖區的用法。
2.6 紋理資源視圖
紋理可以被綁定到渲染管線(rendering pipeline)的不同階段(stage);例如,比較常見的情況是將紋理作為渲染目標(即,Direct3D渲染到紋理)或着色器資源(即,在着色器中對紋理進行采樣)。當創建用於這兩種目的的紋理資源時,應使用綁定標志值:
D3D11_BIND_RENDER_TARGET | D3D10_BIND_SHADER_RESOURCE
指定紋理所要綁定的兩個管線階段。其實,資源不能被直接綁定到一個管線階段;我們只能把與資源關聯的資源視圖綁定到不同的管線階段。無論以哪種方式使用紋理,Direct3D始終要求我們在初始化時為紋理創建相關的資源視圖(resource view)。這樣有助於提高運行效率,正如SDK文檔指出的那樣:“運行時環境與驅動程序可以在視圖創建執行相應的驗證和映射,減少綁定時的類型檢查”。所以,當把紋理作為一個渲染目標和着色器資源時,我們要為它創建兩種視圖:渲染目標視圖(ID3D11RenderTargetView)和着色器資源視圖(ID3D11ShaderResourceView)。資源視圖主要有兩個功能:(1)告訴Direct3D如何使用資源(即,指定資源所要綁定的管線階段);(2)如果在創建資源時指定的是弱類型(typeless)格式,那么在為它創建資源視圖時就必須指定明確的資源類型。對於弱類型格式,紋理元素可能會在一個管線階段中視為浮點數,而在另一個管線階段中視為整數。為了給資源創建一個特定視圖,我們必須在創建資源時使用特定的綁定標志值。例如,如果在創建資源沒有使用D3D11_BIND_DEPTH_STENCIL綁定標志值(該標志值表示紋理將作為一個深度/模板緩沖區綁定到管線上),那我們就無法為該資源創建ID3D11DepthStencilView視圖。只要你試一下就會發現Direct3D會給出如下調試錯誤:
ERROR: ID3D11Device::CreateDepthStencilView:
A DepthStencilView cannot be created of a Resource that did not specify D3D10_BIND_DEPTH_STENCIL.
我們會在之后看到用來創建渲染目標視圖和深度/模板視圖的代碼。在第8章中看到用於創建着色器資源視圖的代碼。本書隨后的許多例子都有會把紋理用作渲染目標和着色器資源。
2.7 多重采樣
因為計算機顯示器上的像素分辨率有限,所以當我們繪制一條任意直線時,該直線很難精確地顯示在屏幕上。下圖中的第一條直線說明了“階梯”(aliasing,鋸齒)效應,當使用像素矩陣近似地表示一條直線時就會出現這種現象,類似的鋸齒也會發生在三角形的邊緣上。
從上圖中我們可以看到,第一條直線帶有明顯的鋸齒(當使用像素矩陣近似地表示一條直線時就會出現階梯效應)。而第二條直線使用了抗鋸齒技術,通過對一個像素周圍的鄰接像素進行采樣得到該像素的最終顏色;這樣可以形成一條較為平滑的直線,使階梯效果得到緩解。
通過提高顯示器的分辨率,縮小像素的尺寸,也可以有效地緩解一問題,使階梯效應明顯降低。
當無法提高顯示器分辨率或分辨率不夠高時,我們可以使用抗鋸齒(antialiasing)技術。其中的一種技術叫做超級采樣(supersampling),它把后台緩沖和深度緩沖的大小提高到屏幕分辨率的4倍。3D場景會以這個更大的分辨率渲染到后台緩存中,當在屏幕上呈現后台緩沖時,后台緩沖會將4個像素的顏色取平均值后得到一個像素的最終顏色。從效果上來說,超級采樣的工作原理就是以軟件的方式提升分辨率。
超級采樣代價昂貴,因為它處理的像素數量和所需的內存數量增加為原來的4倍。Direct3D支持另一種稱為多重采樣(multisampling)的抗鋸齒技術,它通過對一個像素的子像素進行采樣計算出該像素的最終顏色,比超級采樣節省資源。假如我們使用的是4X多重采樣(每個像素采樣4個鄰接像素),多重采樣仍然會使用屏幕分辨率4倍大小的后台緩沖和深度緩沖,但是,不像超級采樣那樣計算每個子像素的顏色,而是只計算像素中心顏色一次,然后基於子像素的可見性(基於子像素的深度/模板測試)和范圍(子像素中心在多邊形之外還是之內)共享顏色信息。下圖展示了這樣的一個例子。
從上圖中我們可以看到,一個像素與多邊形的邊緣相交,像素中心的綠顏色存儲在可見的三個子像素中,而第4個子像素沒有被多邊形覆蓋,因此不會被更新為綠色,它仍保持為原來繪制的幾何體顏色或Clear操作后的顏色。如右圖所示,要獲得最后的像素顏色,我們需要對4個子像素(3個綠色和一個白色)取平均值,獲得淡綠色,通過這個操作,可以減弱多邊形邊緣的階梯效果,實現更平滑的圖像。
注意:supersampling與multisampling的關鍵區別在於:使用supersampling時,圖像的顏色需要通過每個子像素的顏色計算得來,而每個子像素顏色可能不同;使用multisampling(上圖)時,每個像素的顏色只計算一次,這個顏色會填充到所有可見的、被多邊形覆蓋的子像素中,即這個顏色是共享的。因為計算圖像的顏色是圖形管線中最昂貴的操作之一,因此multisampling相比supersampling而言節省的資源是相當可觀的。但是,supersampling更為精確,這是multisampling做不到的。
注意:在上圖中,我們用標准的網格圖形表示一個像素的4個子像素,但由於硬件的不同,實際的子像素放置圖形也是不同的,Direct3D並不定義子像素的放置方式,在特定情況下,某些放置方式會優於其他的放置方式。
2.8 Direct3D中的多重采樣
在下一節中, 我們要填充一個DXGI_SAMPLE_DESC結構體。該結構體包含兩個成員,其定義如下:
typedef struct DXGI_SAMPLE_DESC
{
UINT Count;
UINT Quality;
} DXGI_SAMPLE_DESC, *LPDXGI_SAMPLE_DESC;
Count成員用於指定每個像素的采樣數量,Quality成員用於指定希望得到的質量級別(不同硬件的質量級別表示的含義不一定相同)。質量級別越高,占用的系統資源就越多,所以我們必須在質量和速度之間權衡利弊。質量級別的取值范圍由紋理格式和單個像素的采樣數量決定。我們可以使用如下方法,通過指定紋理格式和采樣數量來查詢相應的質量級別:
HRESULT ID3D11Device::CheckMultisampleQualityLevels( DXGI_FORMAT Format, UINT SampleCount, UINT *pNumQualityLevels);
如果紋理格式和采樣數量的組合不被設備支持,則該方法返回0。反之,通過pNumQualityLevels參數返回符合給定的質量等級數值。有效的質量級別范圍為0到pNumQualityLevels−1。
采樣的最大數量可以由以下語句定義:
#define D3D11_MAX_MULTISAMPLE_SAMPLE_COUNT(32)
采樣數量通常使用4或8,可以兼顧性能和內存消耗。如果你不使用多重采樣,可以將采樣數量設為1,將質量級別設為0。所有符合Direct3D 11功能特性的設備都支持用於所有渲染目標格式的4X多重采樣。
注意:我們需要為交換鏈緩沖區和深度緩沖區各填充一個DXGI_SAMPLE_DESC結構體。當創建后台緩沖區和深度緩沖區時,必須使用相同的多重采樣設置;具體的代碼會在下一節給出。
2.9 特征等級
Direct3D 11提出了特征等級(feature levels,在代碼中由枚舉類型D3D_FEATURE_LEVEL表示)的概念,對應了定義了d3d11中定義了如下幾個等級以代表不同的d3d版本:
typedef enum D3D_FEATURE_LEVEL
{
D3D_FEATURE_LEVEL_9_1 = 0x9100,
D3D_FEATURE_LEVEL_9_2 = 0x9200,
D3D_FEATURE_LEVEL_9_3 = 0x9300,
D3D_FEATURE_LEVEL_10_0 = 0xa000,
D3D_FEATURE_LEVEL_10_1 = 0xa100,
D3D_FEATURE_LEVEL_11_0 = 0xb000
} D3D_FEATURE_LEVEL;
特征等級定義了一系列支持不同d3d功能的相應的等級(每個特征等級支持的功能可參見SDK文檔),用意即如果一個用戶的硬件不支持某一特征等級,程序可以選擇較低的等級。例如,為了支持更多的用戶,應用程序可能需要支持Direct3D 11,10.1,9.3硬件。程序會從最新的硬件一直檢查到最舊的,即首先檢查是否支持Direct3D 11,第二檢查Direct3D 10.1,然后是Direct3D 10,最后是Direct3D 9。要設置測試的順序,可以使用下面的特征等級數組(數組內元素的順序即特征等級測試的順序):
D3D_FEATURE_LEVEL featureLevels [4] ={
D3D_FEATURE_LEVEL_11_0, // First check D3D 11 support
D3D_FEATURE_LEVEL_10_1, // Second check D3D 10.1 support
D3D_FEATURE_LEVEL_10_0, // Next,check D3D 10 support
D3D_FEATURE_LEVEL_9_3 // Finally,check D3D 9.3 support
} ;
這個數組可以放置在Direct3D初始化方法中,方法會輸出數組中第一個可被支持的特征等級。例如,如果Direct3D報告數組中第一個可被支持的特征等級是D3D_FEATURE_LEVEL_10_0,程序就會禁用Direct3D 11和Direct3D 10.1的特征,而使用Direct3D 10的繪制路徑。本書中我們要求必須能支持D3D_FEATURE_LEVEL_11_0。
三、結語
這篇文章中學習了Direct3D中常用的一些基本類型和基本繪圖概念。
接下來我們就可以正式進入Direct3D11的學習了。