第四章 初始化D3D

4 初始化D3D

本章第一部我們會熟悉一些D3D的基本數據類型和基本的圖形概念。然後,我們詳細說明初始化Direct3D所需的步驟。之後,引入準確計算和實時圖形應用所需的時間測量。最後,我們將探索示例框架代碼,該代碼用於提供本書中所有演示應用程序的界面。

學習目標:
1.瞭解Direct3D在3D硬件編程中的作用。
2.瞭解COM與Direct3D的作用。
3.學習基本圖形概念,例如2D圖像的存儲方式,翻頁,深度緩衝和多重採樣。
4.學習如何使用性能計數器功能獲得高分辨率定時器讀數。
5.瞭解如何初始化Direct3D。
6.熟悉本書所有演示應用框架的一般結構。

4.1 初步

我們將在本節介紹一些Direct3D初始化過程中一些基本的圖形概念和Direct3D數據類型,爲以後打下基礎。

4.1.1 Direct3D概述

D3D是一個底層圖形API(Application Programming Interface),使我們能夠使用3D硬件加速渲染。本質上D3D提供了我們控制圖形硬件的軟件接口。如要清除渲染目標(如屏幕),可調用ID3D11DeviceContext::ClearRenderTargetView方法。在應用程序和圖形硬件之間擁有Direct3D層,意味着我們不用擔心3D硬件的細節,只要它是一個支持Direct3D 11的設備。

D3D 11功能的圖形設備必須支持整個D3D 11功能集,除了個別例外(比如多次採樣計數,因爲不同的D3D硬件會有所不同)。這與D3D 9的只需支持D3D 9功能的子集有所不同; 因此,如果D3D 9應用程序想要使用某個功能,則首先需要檢查硬件是否支持該功能,因爲調用不支持D3D功能的硬件會導致錯誤。在Direct3D 11中,不再需要設備能力檢查,因爲嚴格要求Direct3D 11設備實現整個Direct3D 11功能集。

4.1.2 COM組件

組件對象模型(COM)是允許DX獨立於編程語言並具有向後兼容性的技術。我們通常將COM對象稱爲接口,我們可以簡單的將其視爲C++類。當使用C++編程DirectX時,COM會對我們隱藏大部分細節。我們只需通過特定函數獲取COM接口的方法或獲取COM接口的指針 - 我們不會用C ++ new關鍵字創建一個COM接口。另外,當使用完後,調用Release方法,而不是刪除它(所有COM接口都繼承了提供Release方法的IUnknown COM接口的功能),COM對象執行自己的內存管理。當然,COM還有更多的東西,但對於更高效的使用DirectX並不必要。

NOTE: COM接口以首字母 I 爲前綴。例如,表示2D紋理的COM接口稱爲ID3D11Texture2D。

4.1.3紋理和數據資源格式

2D紋理是數據元素的矩陣。2D紋理的一個用途是存儲2D圖像數據,其中紋理中的每個元素存儲像素的顏色。但這不是唯一的用法; 例如,在稱爲“法線貼圖”的高級技術中,紋理中的每個元素存儲3D矢量而不是顏色。因此,雖然一般將紋理視爲圖像數據,但它們實際上還有更多用處。1D紋理就像1D數組元素,3D紋理像3D陣列的數組元素。稍後章節會講,紋理實際上不僅僅是數據數組; 它還具有mipmap級別,並且GPU可以對它們進行特殊操作,例如應用過濾器和多采樣。另外紋理不能存儲任意種類的數據; 它只能存儲由DXGI_FORMAT枚舉類型描述的數據格式。比如:
1. DXGI_FORMAT_R32G32B32_FLOAT:每個元素都有三個32位浮點分量。
2. DXGI_FORMAT_R16G16B16A16_UNORM:每個元素具有映射到[0,1]範圍的四個16位元件。
3. DXGI_FORMAT_R32G32_UINT:每個元素都有兩個32位無符號整數分量。
4. DXGI_FORMAT_R8G8B8A8_UNORM:每個元素具有映射到[0,1]範圍的四個8位無符號分量。
5. DXGI_FORMAT_R8G8B8A8_SNORM:每個元素具有映射到[-1,1]範圍的四個8位有符號組件。
6. DXGI_FORMAT_R8G8B8A8_SINT:每個元素具有映射到[-128,127]範圍的四個8位有符號整數組件。
7. DXGI_FORMAT_R8G8B8A8_UINT:每個元素具有映射到[0,255]範圍的四個8位無符號整數分量。

R,G,B,A字母分別用於代表紅,綠,藍和alpha。顏色被紅,綠和藍三種基礎顏色混合而成(例如,相等的紅色和相等的綠色變成黃色)。alpha通常用於控制透明度。然而紋理不是隻能存儲顏色信息;例如

DXGI_FORMAT_R32G32B32_FLOAT

具有三個浮點分量,因此可以存儲具有浮點座標的3D矢量。還有無類型的格式,我們只存數據,然後在紋理綁定到管道後指定如何重新解釋數據(類似於C++重新解釋的轉換)如:以下無格式格式保留具有四個8位組件的元素,但不指定數據類型(例如,整數,浮點,無符號整數):

DXGI_FORMAT_R8G8B8A8_TYPELESS

4.1.4交換鏈和頁面翻轉

爲了避免畫面閃爍,最好將整幀畫面繪製到後臺緩衝區的屏幕紋理中。一旦將整個場景繪製到後臺緩衝區給定的區域,就可將其作爲一個完整幀呈現給屏幕;以這種方式,觀衆不會看到畫面被繪製 - 觀衆只能看到完整的幀。爲了實現這一點,硬件用了兩個紋理緩衝區:前緩衝區&後緩衝區。前緩衝區存儲當前顯示在監視器上的圖像數據,而下一幀動畫被繪製到後緩衝區。幀被繪製到後臺緩衝區後,後緩衝區和前緩衝區對調:後緩衝區變爲前緩衝區,前緩衝區成爲下一幀的後緩衝區。交換後臺和前端緩衝區的角色稱爲presenting。Presenting是一個高效操作,因爲只需要交換指向當前前端緩衝區的指針和當前後臺緩衝區的指針。圖4.1說明了該過程。

前後緩衝區形成交換鏈。在D3D中交換鏈由IDXGISwapChain接口表示。該內容存儲前後緩衝區紋理,以及提供調整緩衝區大小的方法(IDXGISwapChain::ResizeBuffers)和呈現(IDXGISwapChain::Present)。我們將在§4.4中詳細討論這些方法。

使用兩個緩衝區(前和後)稱爲雙緩衝。也可以使用兩個以上的緩衝區;使用三個緩衝區稱爲三重緩衝。但一般兩個緩衝區就足夠了。

NOTE:即使後臺緩衝區是紋理(因此其元素應該稱爲紋素),但我們經常將其元素稱爲像素,因爲在後臺緩衝區的情況下,它存儲顏色信息。有時人們會將紋理的元素稱爲像素,即使它不存儲顏色信息(例如,“法線貼圖的像素”)。

4-1
圖4.1 從上到下,我們首先渲染到緩衝區B,該緩衝區B用作當前的後臺緩衝區。一幀完成,指針被交換,緩衝區B變成前緩衝區,緩衝區A成爲新的後緩衝區。然後,我們將下一幀渲染到緩衝區A.一旦幀完成,指針被交換,緩衝區A成爲前緩衝區,緩衝區B再次成爲後緩衝區。

4.1.5深度緩衝

深度緩衝器是不包含圖像數據紋理,而是關於特定像素的深度信息。可能的深度值範圍爲0.0至1.0,其中0.0表示對象可以與觀看者最接近,1.0表示物體距離觀察者最遠。深度緩衝器中的每個像素和後緩衝器中的每個像素之間存在一一對應關係(後緩衝器中的第i個像素對應於深度緩衝器中第i個像素)。因此,如果後臺緩衝區的分辨率爲1280×1024,則會有1280×1024個深度元素。

圖4.2
圖4.2 一組部分相互遮擋的物體

圖4.2顯示了一個簡單的場景,其中一些物體部分地掩蓋了它們後面的物體。爲了使D3D能夠確定物體的哪些像素在另一個像素之前,它使用一種稱爲深度緩衝或z緩衝的技術。在此強調一下,有了深度緩衝,我們繪製對象的順序並不重要。

NOTE:處理深度問題,可以建議以最遠到最近的順序繪製場景中的物體。以這種方式,近物體將被繪製在遠處的對象上,並且會呈現正確的結果。這就是畫家畫畫的方式。但是,這種方法有其自身的問題 - 會存儲大量的前後順序排序和相交幾何體數據。而且,圖形硬件本來就提供深度緩衝。

讓我們用一個例子說明深度緩存的工作原理。觀察圖4.3,它顯示了觀看者看到的空間和該空間的2D側視圖。從圖中,我們觀察到三個不同的像素競爭在視圖窗口上的像素P上呈現。(顯然最接近的像素應該渲染到P,因爲它掩蓋了它後面的像素,但是計算機不知道)首先,在渲染前,後臺緩衝區被置爲默認色(如黑或白),並且深度緩衝區被清除爲默認值,通常爲1.0(像素可以具有的最遠深度值)。現在,假設物體是以圓柱體,球體和錐體的順序呈現的。下表總結了繪製對象時如何更新像素P及其相應的深度值d;對於其他像素也會發生類似的過程。

操作 P d 描述
清除 1.0 像素和相應的深度初始化
畫圓柱 p3 d3 因爲d3 < d=1.0,深度測試通過,同時我們通過設置P=P3,d=d3更新buffer
畫球 p1 d1 因爲d1 < d= d3,深度測試通過,同時我們通過設置P=P1,d=d1更新buffer
畫圓錐 p1 d1 因爲d2 > d= d2,深度測試失敗,不更新buffer

圖4.3
圖4.3 視圖窗口對應於我們生成的3D場景的2D圖像(後臺緩衝區)。我們看到三個不同的像素可以投影到像素P。直覺告訴我們,P1應該被寫入到P,因爲它更接近於觀察者並且阻擋了另外兩個像素。深度緩衝算法提供了在計算機上確定該點的過程。請注意,我們顯示相對於正在查看的3D場景的深度值,但是當它們存儲在深度緩衝區中時,它們實際上被歸一化爲範圍[0.0,1.0]

可見,當我們找到一個較小深度值的像素時,我們會更新深度緩衝區中的像素及其相應的深度值。以這種方式,最接近觀衆的像素就是渲染的像素。(如果你仍然不信,可以切換繪圖順序並重做此示例。)

總而言之,深度緩衝通過計算每個像素的深度值並執行深度測試來起作用。深度測試將要寫入的像素的深度與後緩衝區上的特定像素位置進行比較。具有最接近觀察者的深度值的像素獲就是寫入後臺緩衝區的像素,因爲最接近觀衆的像素會掩蓋其背後的像素。

深度緩衝區是紋理,因此必須使用某些數據格式創建。用於深度緩衝的格式如下:
1.DXGI_FORMAT_D32_FLOAT_S8X24_UINT:指定一個32位浮點深度緩衝區,具有8位(無符號整數)保留映射到[0,255]範圍的模板緩衝區和不用於填充的24位。
2.DXGI_FORMAT_D32_FLOAT:指定一個32位浮點深度緩衝區。
3.DXGI_FORMAT_D24_UNORM_S8_UINT:指定一個無符號的24位深度緩衝區,映射到[0,1]範圍,8位(無符號整數)爲映射到[0,255]範圍的模板緩衝區保留。
4.DXGI_FORMAT_D16_UNORM:指定映射到[0,1]範圍的無符號16位深度緩衝區。

NOTE:應用程序一般不需要模板緩衝區,如果需要,模板緩衝區總是附加到深度緩衝區。例如,32位格式:

DXGI_FORMAT_D24_UNORM_S8_UINT

深度緩衝區使用24位,模板緩衝區使用8位。因此,深度緩衝區稱爲深度/模板緩衝區更合適。使用模板緩衝區是一個更高級的問題,將在第10章中進行說明。

4.1.6紋理資源視圖

紋理可以綁定到渲染管道的不同階段; 一個常見的例子是使用紋理作爲渲染目標(即D3D中繪製到紋理中)和作爲着色器資源(即紋理將在着色器中被採樣)。爲這兩個目的創建的紋理資源將被賦予綁定標誌:

D3D11_BIND_RENDER_TARGET | D3D11_BIND_SHADER_RESOURCE

指示紋理將被綁定到的兩個渲染管線階段。實際上資源並不直接綁定到渲染管線階段;而是將其關聯的資源視圖綁定到不同的渲染管線階段。對於每種方式,我們將使用紋理,D3D要求我們在初始化時創建該紋理的資源視圖。這主要是爲了效率,因爲SDK文檔指出:“這允許運行時和驅動程序在視圖創建時進行驗證和映射,從而使綁定時的類型檢查時間最小化。”因此,使用紋理作爲渲染的示例目標和着色器資源,我們需要創建兩個視圖:渲染目標視圖(ID3D11RenderTargetView)和着色器資源視圖(ID3D11ShaderResourceView)。資源視圖基本上做了兩件事情:他們告訴Direct3D如何使用資源(即將其綁定到渲染管線的什麼階段),如果資源格式在創建時被指定爲無類型,則必須說明在創建視圖時確定。因此,使用無類型格式,可以將紋理的元素視爲一個渲染管線階段中的浮點值,並將其視爲另一個渲染管線階段中的整數。

爲了創建特定的資源視圖,必須使用該特定的綁定標誌創建資源。例如,如果資源沒有使用D3D11_BIND_DEPTH_STENCIL綁定標誌(這表示紋理將被綁定到管道作爲深度/模板緩衝區)創建,那麼我們不能爲該資源創建一個ID3D11DepthStencilView。如果試一下,你應該得到一個D3D調試錯誤,如下所示:

D3D11: ERROR: ID3D11Device::CreateDepthStencilView:A DepthStencilView cannot be created of a Resource that did not specify D3D11_BIND_DEPTH_STENCIL.

本章的§4.2會介紹創建渲染目標視圖和深度/模板視圖的代碼。創建着色器資源視圖將在第8章中討論。使用紋理作爲渲染目標和着色器資源將在本書的後面出現。

NOTE:2009年8月的SDK文檔說:“創建一個完全類型的資源將資源限制爲創建的格式。這使得運行時可以優化訪問[…]。“因此,如果您真的需要他們提供的靈活性(通過多種視圖以多種方式重新解釋數據的能力),則只應創建一個無類型的資源; 否則,創建一個確定類型的資源。

4.1.7多采樣理論

因爲顯示器上的像素不是無限小的,所以在計算機顯示器上不能完美地表示任意的直線。圖4.4示出了當通過像素矩陣近似線時可能發生的“階梯”(鋸齒)效應。類似的鋸齒效應會在三角形的邊緣發生。

圖4.4
圖4.4 上圖我們觀察到鋸齒(當通過像素矩陣來表示直線時的階梯效應)。在底部,我們看到一個抗鋸齒線,通過採樣和使用其相鄰像素生成像素的最終顏色; 這樣做緩解了階梯效應使得圖像更平滑。

通過提高顯示器分辨率來縮小像素尺寸可以在很大程度上顯著減輕鋸齒。

當無法將顯示器分辨率增加到那麼多時,我們可以應用抗鋸齒技術。超級採樣是一種通過使用4倍於屏幕分辨率的臺緩衝區和深度緩衝區來工作的技術。先以更大的分辨率將3D場景渲染到後臺緩衝區。如何將後臺緩衝區顯示到屏幕,後緩衝區被解析(或下采樣),使得4個像素塊顏色被平均在一起以獲得平均的像素顏色。實際上,超級採樣是通過增加軟件中的分辨率來實現的。

圖4.5
圖4.5 我們考慮多邊形邊緣的一個像素。(a)在像素中心評估的綠色顏色存儲在由多邊形覆蓋的三個可見子像素中。第四象限中的子像素不被多邊形覆蓋,因此不會用綠色更新 - 它只保留其以前繪製的幾何或清除先前顏色。(b)爲了計算分辨的像素顏色,我們將四個子像素(三個綠色像素和一個白色像素)平均,以沿着多邊形的邊緣獲得淺綠色。這通過沿着多邊形邊緣緩解階梯效應來獲得更平滑的圖像。

超級採樣十分耗費資源,因其將像素處理和存儲器的數量增加了四倍。D3D支持一種折衷的抗鋸齒技術,稱爲多采樣,它在子像素之間共享一些計算信息,使其比超採樣成本低。假設我們正在使用4X多重採樣(每像素4個子像素),多采樣也使用後置緩衝區和深度緩衝區4倍大於屏幕分辨率; 然而,代替計算每個子像素的圖像顏色,它僅在像素中心處每像素計算一次,然後基於可見度(每個子像素評估深度/模板測試)和覆蓋範圍來共享其子像素的顏色信息(子像素中心是否位於多邊形的內部或外部?)圖4.5顯示了一個例子。

NOTE:觀察超採樣和多采樣之間的關鍵區別。 通過超級採樣,每個子像素計算圖像顏色,因此每個子像素可能是不同的顏色。 使用多重採樣(圖4.5),每像素計算一次圖像顏色,並將該顏色複製到由多邊形覆蓋的所有可見子像素中。 因爲計算圖像顏色是圖形流水線中最費時的步驟之一,因此這就從超採樣的多次採樣中節省了大量時間。另一方面,超級採樣在技術上更準確,可以處理紋理和着色器的混疊,而多采樣則不能。

NOTE:在圖4.5中,我們示出了以均勻網格圖案細分爲四個子像素的像素。使用的實際模式(子像素所在的點)可能因硬件供應商而變化,因爲D3D不會定義子像素的位置。某些模式在某些情況下比其他更好。

4.1.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);

如果設備不支持格式和採樣數組,則此方法返回零。否則,給定組合的質量級別數將通過pNumQualityLevels參數返回。紋理格式和樣本計數組合的有效質量級別從零到pNumQualityLevels -1。

每個像素可以採取的最大采樣數由下式定義:

#define D3D11_MAX_MULTISAMPLE_SAMPLE_COUNT ( 32 )

然而,爲了保持多采樣的性能和內存成本合理,4或8的樣本計數是常見的。如果您不想使用多重採樣,請將樣品計數設置爲1,將質量等級設置爲零。所有支持Direct3D 11的設備都支持所有渲染目標格式的4X多重採樣。

NOTE:需要爲後臺緩衝區和深度緩衝區填寫DXGI_SAMPLE_DESC結構。必須使用相同的多采樣設置創建後緩衝區和深度緩衝區;說明這一點的示例代碼在下一節中給出。

4.1.9特徵級別

D3D 11引入了特徵級別(由D3D_FEATURE_LEVEL枚舉類型代碼表示)的概念,它們大致對應於從版本9到11的各種Direct3D版本:

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;

特徵級別定義了一組嚴格的功能(有關每個特徵級別支持的特定功能,請參閱SDK文檔)。這樣做的原因是,如果用戶的硬件不支持某個級別,應用程序可能會回退到較舊的級別。例如,爲了支持更廣泛的受衆,應用程序可能支持Direct3D 11,10.1,10和9.3級硬件。應用程序將檢查從最新到最舊的級別支持:即應用程序將首先檢查Direct3D 11是否受支持,第二個是Direct3D 10.1,然後是Direct3D 10,最後是Direct3D 9.3。爲了方便這個測試順序,將使用以下功能級別數組(排序陣列的元素意味着功能級別測試的順序):

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初始化函數(§4.2.1)中,該函數將輸出數組中第一個支持的特徵級別。例如,如果Direct3D報告了支持的陣列中的第一個功能級別D3D_FEATURE_LEVEL_10_0,則應用程序可能會禁用Direct3D 11和Direct3D 10.1功能,並使用Direct3D 10渲染路徑。在本書中,我們總是需要對功能級別D3D_FEATURE_LEVEL_11_0的支持,因爲這是一本Direct3D 11書。然而,現實中應用程序確實需要擔心支持舊的硬件才能最大限度地提高受衆。

4.2 初始化DIRECT3D

以下小節講解如何初始化D3D。我們的D3D初始化過程可以分爲以下幾個步驟:
1.使用D3D11CreateDevice函數創建ID3D11DeviceID3D11DeviceContext接口。
2.使用ID3D11Device::CheckMultisampleQualityLevels方法檢查4X MSAA質量等級的支持。
3.通過填寫一個DXGI_SWAP_CHAIN_DESC結構來描述我們將要創建的交換鏈實例的特徵。
4.查詢用於創建設備的IDXGIFactory實例,並創建一個IDXGISwapChain實例。
5.創建一個渲染目標視圖到交換鏈的後臺緩衝區。
6.創建深度/模板緩衝區及其相關的深度/模板視圖。
7.將渲染目標視圖和深度/模板視圖綁定到渲染管道的輸出合併階段,以便Direct3D可以使用它們。
8.設置視口。

4.2.1 創建設備和上下文

通過創建Direct3D 11設備(ID3D11Device)和上下文(ID3D11DeviceContext)開始初始化Direct3D。這兩個接口是主要的Direct3D接口,可以被認爲是我們的物理圖形設備硬件的軟件控制器;也就是說,通過這些接口,我們可以與硬件進行交互,並指示它執行操作(如在GPU內存中分配資源,清除後臺緩衝區,將資源綁定到各個流水線階段,並繪製幾何)。進一步來說:
1、ID3D11Device接口用於檢查功能支持,並分配資源。
2、ID3D11DeviceContext接口用於設置渲染狀態,將資源綁定到圖形管道,併發出渲染命令。
可以使用以下功能創建設備和內容:

HRESULT D3D11CreateDevice(
    IDXGIAdapter *pAdapter,
    D3D_DRIVER_TYPE DriverType,
    HMODULE Software,
    UINT Flags,
    CONST D3D_FEATURE_LEVEL *pFeatureLevels,
    UINT FeatureLevels,
    UINT SDKVersion,
    ID3D11Device **ppDevice,
    D3D_FEATURE_LEVEL *pFeatureLevel,
    ID3D11DeviceContext **ppImmediateContext
);

1、pAdapter:指定想要創建設備表示的顯示適配器。爲此參數指定null使用主顯示適配器。我們總是在本書的示例程序中使用主適配器。在章節練習中您會使用其他顯示器。
2、DriverType:通常,您將始終爲此參數指定D3D_DRIVER_TYPE_HARDWARE以使用3D硬件加速進行渲染。但是,一些其他選項包括:
D3D_DRIVER_TYPE_REFERENCE:創建一個所謂的參考設備。 參考設備是Direct3D的軟件實現,目標是正確性(由於它是軟件實現,速度非常慢)。參考設備隨DirectX SDK一起安裝,僅供開發人員使用; 它不應該用於託管應用程序。使用參考設備有兩個原因:
(i)要測試代碼,您的硬件不支持; 例如,當您沒有Direct3D 11功能的顯卡時,要測試Direct3D 11代碼。
(ii)測試驅動程序的錯誤。如果您的代碼能夠與參考設備正常工作,但不能與硬件配合使用,則可能是硬件驅動程序中的錯誤。
3D_DRIVER_TYPE_SOFTWARE:創建用於模擬3D硬件的軟件驅動程序。要使用軟件驅動程序,您必須自行構建或使用第三方軟件驅動程序。Direct3D除了下面描述的WARP驅動程序之外,不提供軟件驅動程序。
D3D_DRIVER_TYPE_WARP:創建高性能Direct3D 10.1軟件驅動程序。 WARP代表Windows Advanced Rasterization Platform。 我們對此不感興趣,因爲它不支持Direct3D 11。
3.Software:用於提供軟件驅動程序。我們總是指定null,因爲我們正在使用硬件進行渲染。否則,必須有一個可以使用的軟件驅動程序。
4.Flags:可選的設備創建標誌(可以按位進行或運算)。兩個常見的標誌是:
D3D11_CREATE_DEVICE_DEBUG:對於調試模式構建,應將此標誌設置爲啓用調試層。當指定調試標誌時,Direct3D將調試消息發送到VC++輸出窗口;圖4.6顯示了可以輸出的一些錯誤消息的示例。
D3D11_CREATE_DEVICE_SINGLETHREADED:如果可以保證不會從多個線程調用Direct3D,則可提高性能。如果啓用了此標誌,則ID3D11Device::CreateDeferredContext方法將失敗(請參閱之後的“NOTE”)。
5.pFeatureLevelsD3D_FEATURE_LEVEL的數組元素,其順序表示測試特徵級別支持的順序(見§4.1.9)。爲此參數指定null表示選擇支持最大的功能級別。在我們的框架中,我們檢查以確保這是D3D_FEATURE_LEVEL_11_0(即Direct3D 11支持),因爲我們只在本書中使用Direct3D 11。
6.FeatureLevels:數組pFeatureLevelsD3D_FEATURE_LEVEL的數量。 如果爲先前參數pFeatureLevels指定了null,則指定0。
7.SDKVersion:始終指定D3D11_SDK_VERSION
8.ppDevice:返回創建的設備。
9.pFeatureLevel:返回pFeatureLevels數組中第一個受支持的功能級別(如果pFeatureLevelsnull時,返回持最大的功能級別)。
10.ppImmediateContext:返回創建的設備上下文。

4.6
圖4.6 Direct3D 11調試輸出的一個例子。

該函數調用的一個例子:

UINT createDeviceFlags = 0;

#if defined(DEBUG) || defined(_DEBUG)
    createDeviceFlags |= D3D11_CREATE_DEVICE_DEBUG;
#endif

D3D_FEATURE_LEVEL featureLevel;
ID3D11Device* md3dDevice;
ID3D11DeviceContext* md3dImmediateContext;
HRESULT hr = D3D11CreateDevice(
    0, // default adapter
    D3D_DRIVER_TYPE_HARDWARE,
    0, // no software device
    createDeviceFlags,
    0, 0, // default feature level array
    D3D11_SDK_VERSION,
    & md3dDevice,
    & featureLevel,
    & md3dImmediateContext);
if( FAILED(hr) )
{
    MessageBox(0, L"D3D11CreateDevice Failed.", 0, 0);
    return false;
} 
if( featureLevel != D3D_FEATURE_LEVEL_11_0 )
{
    MessageBox(0, L"Direct3D Feature Level 11 unsupported.", 0, 0);
    return false;
}

NOTE: 注意將設備上下文指針指向immediate context:
   ID3D11DeviceContext* md3dImmediateContext;
還有一種稱爲延遲上下文(ID3D11Device :: CreateDeferredContext)的東西。這是Direct3D 11多線程支持中的一部分。考慮到多線程是一個高級問題,本書不做討論,但基本思路如下:
1.在主渲染線程上設置immediate context。
2.在單獨的工作線程上有任何額外的延遲上下文。
(a)每個工作線程都可以將圖形命令記錄到命令列表(ID3D11CommandList)中。
(b)然後可以在主渲染線程上執行每個工作線程的命令列表。
如果命令列表需要時間進行組合,那麼它們可以用於複雜的渲染圖,能夠在多核系統上並行組合命令列表是有利的。

4.2.2檢查4X MSAA的支持

現在我們已經創建了一個設備,我們可以檢查4X MSAA的質量級別支持。回想一下,所有支持Direct3D 11的設備在所有渲染格式下支持4X MSAA(但支持的質量級別可能不同)。

UINT m4xMsaaQuality;
HR(md3dDevice->CheckMultisampleQualityLevels(
    DXGI_FORMAT_R8G8B8A8_UNORM, 4, & m4xMsaaQuality));
assert(m4xMsaaQuality > 0 );

由於至少支持4X MSAA,因此返回的質量應始終大於0;因此,assert是這樣的。

4.2.3 交換鏈

初始化過程的下一步是創建交換鏈。首先填寫DXGI_SWAP_CHAIN_DESC結構的一個實例,該實例描述了我們要創建的交換鏈的特徵。這個結構定義如下:

typedef struct DXGI_SWAP_CHAIN_DESC {
    DXGI_MODE_DESC BufferDesc;
    DXGI_SAMPLE_DESC SampleDesc;
    DXGI_USAGE BufferUsage;
    UINT BufferCount;
    HWND OutputWindow;
    BOOL Windowed;
    DXGI_SWAP_EFFECT SwapEffect;
    UINT Flags;
} DXGI_SWAP_CHAIN_DESC;

DXGI_MODE_DESC類型是另一種結構,定義爲:

typedef struct DXGI_MODE_DESC
{
    UINT Width; // desired back buffer width
    UINT Height; // desired back buffer height
    DXGI_RATIONAL RefreshRate; // display mode refresh rate
    DXGI_FORMAT Format; // back buffer pixel format
    DXGI_MODE_SCANLINE_ORDER ScanlineOrdering; // display scanline mode
    DXGI_MODE_SCALING Scaling; // display scaling mode
} DXGI_MODE_DESC;

NOTE:在下面的數據成員描述中,我們只涵蓋了對初學者來說最重要的常用標誌和選項。有關更多標誌和選項的說明,請參閱SDK文檔。

1.BufferDesc:這個結構描述了我們想創建的後臺緩衝區的屬性。寬、高,像素格式是我們關心的主要屬性;瞭解更多詳細信息,請參閱SDK文檔。
2.SampleDesc:多重採樣的數量和質量水平;見§4.1.8。
3.BufferUsage:指定DXGI_USAGE_RENDER_TARGET_OUTPUT,因爲我們要渲染到後臺緩衝區(即用它作爲渲染目標)。
4.BufferCount:交換鏈中使用的後臺緩衝區的數量; 我們通常只使用一個後臺緩衝區進行雙緩衝,儘管可以使用兩個三重緩衝。
5.OutputWindow:我們渲染的窗口句柄。
6.Windowed:指定true在窗口模式下運行或在全屏模式下爲false。
7.SwapEffect:指定DXGI_SWAP_EFFECT_DISCARD爲了讓顯示驅動程序選擇最高效的呈現方式。
8.Flags:可選。如果指定爲DXGI_SWAP_CHAIN_FLAG_ALLOW_MODE_SWITCH,那麼當應用程序切換到全屏模式時,會選擇與當前後臺緩衝區設置最匹配的顯示模式。如果未指定此標誌,則當應用程序切換到全屏模式時,將使用當前的顯示模式。在我們的示例中,我們不指定此標誌,因爲在全屏模式下使用當前顯示模式我們的演示可以正常工作(大多數桌面顯示設置爲顯示器的最佳分辨率)。

以下代碼顯示了我們如何填寫示例中的DXGI_SWAP_CHAIN_DESC結構:

DXGI_SWAP_CHAIN_DESC sd;
sd.BufferDesc.Width = mClientWidth; // use window's client area dims
sd.BufferDesc.Height = mClientHeight;
sd.BufferDesc.RefreshRate.Numerator = 60;
sd.BufferDesc.RefreshRate.Denominator = 1;
sd.BufferDesc.Format = DXGI_FORMAT_R8G8B8A8_UNORM;
sd.BufferDesc.ScanlineOrdering = DXGI_MODE_SCANLINE_ORDER_UNSPECIFIED;
sd.BufferDesc.Scaling = DXGI_MODE_SCALING_UNSPECIFIED;

// Use 4X MSAA?
if( mEnable4xMsaa )
{
    sd.SampleDesc.Count = 4;

    // m4xMsaaQuality is returned via CheckMultisampleQualityLevels().
    sd.SampleDesc.Quality = m4xMsaaQuality-1;
}
// No MSAA
else
{
    sd.SampleDesc.Count = 1;
    sd.SampleDesc.Quality = 0;
}
sd.BufferUsage  = DXGI_USAGE_RENDER_TARGET_OUTPUT;
sd.BufferCount  = 1;
sd.OutputWindow = mhMainWnd;
sd.Windowed     = true;
sd.SwapEffect   = DXGI_SWAP_EFFECT_DISCARD;
sd.Flags        = 0;

NOTE:如果想在運行時更改多重採樣設置,則必須銷燬並重新創建交換鏈。

NOTE:我們將DXGI_FORMAT_R8G8B8A8_UNORM格式(8位紅,綠,藍和alpha)用於後臺緩衝區,因爲顯示器通常不支持24位以上的顏色,所以額外的精度將被浪費。監視器不輸出alpha的額外8位,但是在後臺緩衝區中有額外的8位可用於某些特殊效果。

4.2.4創建交換鏈

通過IDXGIFactory::CreateSwapChain方法實例化IDXGIFactory創建交換鏈接口(IDXGISwapChain):

HRESULT IDXGIFactory::CreateSwapChain(
        IUnknown *pDevice,              // Pointer to ID3D11Device.
        DXGI_SWAP_CHAIN_DESC *pDesc,    // Pointer to swap chain description.
        IDXGISwapChain **ppSwapChain);  // Returns created swap chain interface.

我們可以通過CreateDXGIFactory獲得一個指向IDXGIFactory實例的指針(需要鏈接dxgi.lib)。但是,如果我們以這種方式獲取一個IDXGIFactory實例,並調用IDXGIFactory::CreateSwapChain,那麼我們將會得到以下錯誤:

**DXGI Warning: IDXGIFactory::CreateSwapChain: This function is being called with a device from a different
IDXGIFactory.**

必要的修改是使用用於創建設備的IDXGIFactory實例。要獲取此實例,我們必須繼續執行以下COM查詢系列(在IDXGIFactory的文檔中有描述):

IDXGIDevice* dxgiDevice = 0;
HR(md3dDevice->QueryInterface(__uuidof(IDXGIDevice),(void**)&dxgiDevice));

IDXGIAdapter* dxgiAdapter = 0;
HR(dxgiDevice->GetParent(__uuidof(IDXGIAdapter),(void**))&dxgiAdapter));

// Finally got the IDXGIFactory interface.
IDXGIFactory* dxgiFactory = 0;
HR(dxgiAdapter->GetParent(__uuidof(IDXGIFactory),(void**))&dxgiFactory));

// Now, create the swap chain.
IDXGISwapChain* mSwapChain;
HR(dxgiFactory->CreateSwapChain(md3dDevice, )&sd, )&mSwapChain));

// Release our acquired COM interfaces (because we are done with them).
ReleaseCOM(dxgiDevice);
ReleaseCOM(dxgiAdapter);
ReleaseCOM(dxgiFactory);

NOTE: DXGI(DirectX圖形基礎設施)是與Direct3D的獨立API,可以處理圖形相關的東西,如交換鏈,枚舉圖形硬件以及窗口和全屏模式之間的切換。將其與Direct3D分開的是因爲,其他圖形API(如Direct2D)也需要交換鏈,枚舉圖形硬件以及窗口和全屏模式之間的切換。這樣,很多圖形API都可以使用DXGI API。

4.2.5 創建渲染目標視圖

如第4.1.6節所述,我們不直接將資源綁定到流水線階段;相反,我們必須爲資源創建資源視圖,並將視圖綁定到流水線階段。特別是爲了將後臺緩衝區綁定到流水線的輸出合併階段(因此Direct3D可以渲染到其上),我們需要爲後臺緩衝區創建渲染目標視圖。以下示例代碼顯示瞭如何完成:

ID3D11RenderTargetView* mRenderTargetView;
ID3D11Texture2D* backBuffer;
mSwapChain->GetBuffer(0, __uuidof(ID3D11Texture2D),reinterpret_cast<void**>(&backBuffer));
md3dDevice->CreateRenderTargetView(backBuffer, 0, &mRenderTargetView);
ReleaseCOM(backBuffer);

1、使用IDXGISwapChain::GetBuffer方法獲取指向交換鏈的後臺緩衝區的指針。該方法的第一個參數是一個索引,用於標識要獲取的特定後臺緩衝區(如果有多個)。在我們的演示中,我們只使用一個後臺緩衝區,它的索引爲零。第二個參數是緩衝區的接口類型,通常總是2D紋理(ID3D11Texture2D)。第三個參數返回一個指向後臺緩衝區的指針。

2、要創建渲染目標視圖,我們使用ID3D11Device::CreateRenderTargetView方法。第一個參數指定將用作渲染目標的資源,在前面的示例中,它是後緩衝區(即,我們正在向後緩衝區創建渲染目標視圖)。第二個參數是指向D3D11_RENDER_TARGET_VIEW_DESC的指針。除此之外,該結構還描述了資源中元素的數據類型(格式)。如果資源是以類型格式創建的(即不是無類型的),則該參數可以爲空,這表示爲了創建一個到該資源的第一個mipmap級別的視圖(後臺緩衝區只有一個mipmap級別),格式爲所創建資源的格式。(Mipmap在第8章中討論)因爲我們指定了我們的後臺緩衝區的類型,所以我們爲這個參數指定null。第三個參數返回一個指向create render目標視圖對象的指針。

3、對IDXGISwapChain::GetBuffer的調用將COM參考增加到後臺緩衝區,這就是爲什麼我們在完成它時在代碼片段的末尾釋放它(ReleaseCOM)。

4.2.6 創建深度/模板緩衝區和視圖

我們現在需要創建深度/模板緩衝區。如§4.1.5所述,深度緩衝區只是一個2D紋理,用於存儲深度信息(如果使用模板,則爲模板信息)。要創建紋理,我們需要填寫一個描述要創建的紋理的D3D11_TEXTURE2D_DESC結構,然後調用ID3D11Device::CreateTexture2D方法。D3D11_TEXTURE2D_DESC結構定義如下:

typedef struct D3D11_TEXTURE2D_DESC {
    UINT Width;
    UINT Height;
    UINT MipLevels;
    UINT ArraySize;
    DXGI_FORMAT Format;
    DXGI_SAMPLE_DESC SampleDesc;
    D3D11_USAGE Usage;
    UINT BindFlags;
    UINT CPUAccessFlags;
    UINT MiscFlags;
} D3D11_TEXTURE2D_DESC;

1.Width:紋理的紋理寬度。
2.Height:紋素的紋理高度。
3.MipLevels:mipmap的級別。Mipmaps在關於紋理的章節中有介紹。爲了創建深度/模板緩衝區,我們的紋理只需要一個mipmap級別。
4.ArraySize:紋理數組中的紋理數。對於深度/模板緩衝區,我們只需要一個紋理。
5.FormatDXGI_FORMAT枚舉類型的成員,指定紋素的格式。對於深度/模板緩衝區,這需要是§4.1.5中顯示的格式之一。
6.SampleDesc:多樣本和質量水平的數量;見§4.1.7和§4.1.8。回想一下,4X MSAA使用比屏幕分辨率大4倍的後臺緩衝區和深度緩衝區,以便每個子像素存儲顏色和深度/模板信息。因此,用於深度/模板緩衝區的多重採樣設置必須與渲染目標所使用的設置相匹配。
7.Usage(用法):D3D11_USAGE枚舉類型的成員,指定紋理將如何使用。四個使用值是:
(a)D3D11_USAGE_DEFAULT:如果GPU(圖形處理單元)將讀取和寫入資源,請指定此用法。CPU無法使用此用法讀取或寫入資源。對於深度/模板緩衝區,我們指定D3D11_USAGE_DEFAULT,因爲GPU將對深度/模板緩衝區進行所有讀取和寫入操作。
(b)D3D11_USAGE_IMMUTABLE:如果資源的內容在創建後不會更改,則指定此用法。這允許一些潛在的優化,因爲資源將被GPU只讀。除了在創建時初始化資源,CPU和GPU不能寫入不可變資源。 CPU不能從不可變資源讀取。
(c)D3D11_USAGE_DYNAMIC:如果應用程序(CPU)需要頻繁更新資源的數據內容(例如,基於每幀),則指定此用法。具有這種用法的資源可以由GPU讀取並由CPU寫入。因爲新的數據必須從CPU存儲器(即系統RAM)轉移到GPU存儲器(即視頻RAM),所以從CPU動態地更新GPU資源會導致性能下降。因此,除非必要,否則應避免動態使用。
(d)D3D11_USAGE_STAGING:如果應用程序(CPU)需要能夠讀取資源的副本(即資源支持將數據從視頻內存複製到系統內存),請指定此用法。從GPU複製到CPU內存是一個緩慢的操作,應該避免,除非有必要。
8. BindFlags:一個或多個標記(或操作),指定資源將綁定到管道的位置。對於深度/模板緩衝區,需要D3D11_BIND_DEPTH_STENCIL。紋理的其他綁定標誌是:
D3D11_BIND_RENDER_TARGET:紋理將被綁定到管道的渲染目標。
D3D11_BIND_SHADER_RESOURCE:紋理將被綁定到管道的着色器資源。

9.CPUAccessFlags:指定CPU如何訪問資源。如果CPU需要寫入資源,請指定D3D11_CPU_ACCESS_WRITE。具有寫訪問權限的資源必須使用D3D11_USAGE_DYNAMICD3D11_USAGE_STAGING。如果CPU需要從緩衝區中讀取,請指定D3D11_CPU_ACCESS_READ。具有讀取訪問權限的緩衝區必須使用D3D11_USAGE_STAGING。對於深度/模板緩衝區,只有GPU寫入並讀取深度/緩衝區;因此,我們可以爲此值指定零,因爲CPU將不會讀取或寫入深度/模板緩衝區。

10.MiscFlags:可選標誌,不適用於深度/模板緩衝區,因此設置爲零。

NOTE:應該避免使用標誌D3D11_USAGE_DYNAMICD3D11_USAGE_STAGING,因爲有性能損失。常見的因素是CPU涉及到這兩個標誌。在CPU和GPU內存之間來回切換會導致性能下降。爲了獲得最大的速度,當我們創建我們所有的資源並將數據上傳到GPU時,圖形硬件效果最佳,資源保留在只有GPU讀取和寫入資源的GPU上。但是,對於某些應用程序,這些標誌是無法避免的,並且必須涉及CPU,但是應該始終儘量減少這些標誌的使用。

我們將看到不同的選項創建資源的示例; 例如不同用途的標誌位,不同的綁定標誌和不同的CPU訪問標誌。現在,只集中在我們需要指定的值來創建深度/模板緩衝區,而不必擔心每一個選項。

另外,在使用深度/模板緩衝區之前,我們必須創建一個關聯的深度/模板視圖來綁定到管道。這與創建渲染目標視圖類似。以下代碼示例顯示了我們如何創建深度/模板紋理及其對應的深度/模板視圖:

D3D11_TEXTURE2D_DESC depthStencilDesc;
depthStencilDesc.Width = mClientWidth;
depthStencilDesc.Height = mClientHeight;
depthStencilDesc.MipLevels = 1;
depthStencilDesc.ArraySize = 1;
depthStencilDesc.Format = DXGI_FORMAT_D24_UNORM_S8_UINT;

// Use 4X MSAA? --must match swap chain MSAA values.
if( mEnable4xMsaa )
{
    depthStencilDesc.SampleDesc.Count = 4;
    depthStencilDesc.SampleDesc.Quality = m4xMsaaQuality-1;
}//No MSAA
else
{
    depthStencilDesc.SampleDesc.Count = 1;
    depthStencilDesc.SampleDesc.Quality = 0;
}
depthStencilDesc.Usage = D3D11_USAGE_DEFAULT;
depthStencilDesc.BindFlags = D3D11_BIND_DEPTH_STENCIL;
depthStencilDesc.CPUAccessFlags = 0;
depthStencilDesc.MiscFlags = 0;

ID3D11Texture2D* mDepthStencilBuffer;
ID3D11DepthStencilView* mDepthStencilView;

HR(md3dDevice->CreateTexture2D(
&depthStencilDesc, // Description of texture to create.
0,
&mDepthStencilBuffer)); // Return pointer to depth/stencil buffer.

HR(md3dDevice->CreateDepthStencilView(
mDepthStencilBuffer, // Resource we want to create a view to.
0,
&mDepthStencilView)); // Return depth/stencil view

CreateTexture2D的第二個參數是用於填充紋理的初始數據的指針。然而,因爲這個紋理被用作深度/模板緩衝區,所以我們不需要用任何數據填充它。當執行深度緩衝和模板操作時,Direct3D將直接寫入深度/模板緩衝區。因此,我們爲第二個參數指定null。

CreateDepthStencilView的第二個參數是一個指向D3D11_DEPTH_STENCIL_VIEW_DESC的指針。此外,該結構還描述了資源中元素的數據類型(格式)。如果資源是以類型格式創建的(即,不是無類型的),則該參數可以爲空,這表示可以創建一個到該資源的第一個mipmap級別的視圖(只有一個mipmap級別創建了深度/模板緩衝區 )與資源創建的格式。(Mipmap在第8章中討論)因爲我們指定了深度/模板緩衝區的類型,所以我們爲此參數指定null。

4.2.7將視圖綁定到輸出合併階段

現在我們已經創建了後臺緩衝區和深度緩衝區的視圖,我們可以將這些視圖綁定到管道的輸出合併階段,以使資源成爲管道的渲染目標和深度/模板緩衝區:

md3dImmediateContext->OMSetRenderTargets(1, &mRenderTargetView, mDepthStencilView);

第一個參數是我們綁定的渲染目標的數量;我們在這裏只綁定一個,但是可以同時綁定多個渲染目標(高級技術)。第二個參數是指向要綁定到管道的渲染目標視圖指針數組中的第一個元素的指針。第三個參數是指向要綁定到管道的深度/模板視圖的指針。

NOTE:我們可以設置一組渲染目標視圖,但只能設置一個深度/模板視圖。使用多個渲染目標是本書第三部分涉及的高級技術。

4.2.8設置視口

通常我們將3D場景繪製到整個後臺緩衝區。然而,有時我們只想將3D場景繪製到後緩衝區的一個區域;見圖4.7。

後臺緩衝區我們所繪製的區域稱爲viewport,它由以下結構描述:

typedef struct D3D11_VIEWPORT {
    FLOAT TopLeftX;
    FLOAT TopLeftY;
    FLOAT Width;
    FLOAT Height;
    FLOAT MinDepth;
    FLOAT MaxDepth;
} D3D11_VIEWPORT;

4-7
圖4.7 通過修改viewport,可以將3D場景繪製到後臺緩衝區的部分區域中。然後,後臺緩衝區被呈現給客戶端窗口

前四個數據成員定義了我們正在繪製的客戶端窗口的矩形區域對應的viewport(因爲數據成員的類型爲float,所以可以指定分數座標)。MinDepth成員指定最小深度緩衝區值,MaxDepth指定最大深度緩衝區值。Direct3D使用0到1的深度緩衝區範圍,因此除非需要特殊效果,否則MinDepth和MaxDepth應分別設置爲0和1。

一旦我們填寫了D3D11_VIEWPORT結構,就通過Direct3D的ID3D11DeviceContext::RSSetViewports方法設置了viewport。以下示例創建並設置繪製到整個後臺緩衝區的視口:

    D3D11_VIEWPORT vp;

    vp.TopLeftX = 0.0f;
    vp.TopLeftY = 0.0f;
    vp.Width = static_cast<float>(mClientWidth);
    vp.Height = static_cast<float>(mClientHeight);
    vp.MinDepth = 0.0f;
    vp.MaxDepth = 1.0f;

    md3dImmediateContext->RSSetViewports(1, &vp);

第一個參數是要綁定的視口數(高級效果會用到多個),第二個參數是指向視口數組的指針。

例如,您可以使用視口來實現雙人遊戲模式的分屏。您將創建兩個視口,一個用於屏幕的左半部分,另一個用於屏幕的右半部分。然後,您將從玩家1的視角將3D場景繪製到左側視口中,並將3D場景從玩家2的視角繪製到右側視口中。或者,您可以使用視口渲染到屏幕的一個子矩形,並使用UI(用戶界面)控件(如按鈕,滑塊和列表框)填充剩餘區域。

4.3時間和動畫

要正確做動畫,我們需要跟蹤時間。尤其需要測量動畫中幀之間經過的時間量。如果幀率高,幀之間的時間間隔將非常短; 因此,我們需要一個高精度的定時器。

4.3.1性能計時器

爲了準確的時間測量,我們使用性能計時器(或性能計數器)。使用Win32函數查詢性能計時器,必須#include

__int64 currTime;
QueryPerformanceCounter((LARGE_INTEGER*)&currTime);

此函數通過其參數返回當前時間值,該參數是64位整數值。
要獲取性能計時器的頻率(每秒計數),我們使用QueryPerformanceFrequency函數:

__int64 countsPerSec;
QueryPerformanceFrequency((LARGE_INTEGER*)&countsPerSec);

那麼每個計數所消耗的秒數就是每秒計數的倒數:

mSecondsPerCount = 1.0 / (double)countsPerSec;

因此,要將時間讀取值valueInCounts轉換爲秒,我們只需將其乘以轉換因子mSecondsPerCount

valueInSecs = valueInCounts* mSecondsPerCount ;

QueryPerformanceCounter函數返回的值本身並沒有意義。我們做的是使用QueryPerformanceCounter獲取當前時間值,稍後再次使用QueryPerformanceCounter獲取當前時間值。那麼兩次調用的差就是經過的時間。也就是說,我們總是看看兩個時間戳之間的相對差異,以測量時間,而不是性能計數器返回的實際值。以下更好地說明了這個想法:

__int64 A = 0;
QueryPerformanceCounter((LARGE_INTEGER*)&A);

/* Do work */
__int64 B = 0;
QueryPerformanceCounter((LARGE_INTEGER*)&B);

於是花了(B-A)的計量來做工作,或(B-A)* mSecondsPerCount秒做工作。

NOTE:MSDN關於QueryPerformanceCounter有以下注釋:“在多處理器計算機上,調用哪個處理器不重要。然而,由於基本輸入/輸出系統(BIOS)或硬件抽象層(HAL)中的錯誤,您可能在不同處理器上獲得不同的結果。“您可以使用SetThreadAffinityMask函數,使主應用程序線程不會切換到另一個處理器。

4.3.2遊戲計時器類

在接下來的兩節中,我們將討論以下Game Timer類的實現。

class GameTimer
{
public:
    GameTimer();

    float GameTime()const; // in seconds
    float DeltaTime()const; // in seconds

    void Reset(); // Call before message loop.
    void Start(); // Call when unpaused.
    void Stop(); // Call when paused.
    void Tick(); // Call every frame.
private:
    double mSecondsPerCount;
    double mDeltaTime;

    __int64 mBaseTime;
    __int64 mPausedTime;
    __int64 mStopTime;
    __int64 mPrevTime;
    __int64 mCurrTime;
    bool mStopped;
};

計數器的構造函數特別是查詢性能的頻率。其他成員函數將在接下來的兩節中討論。

GameTimer::GameTimer()
: mSecondsPerCount(0.0), mDeltaTime(-1.0), mBaseTime(0),
mPausedTime(0), mPrevTime(0), mCurrTime(0), mStopped(false)
{
    __int64 countsPerSec;
    QueryPerformanceFrequency((LARGE_INTEGER*)&countsPerSec);
    mSecondsPerCount = 1.0/(double)countsPerSec;
}

NOTE:Game Timer類和實現在Game Timer.h和GameTimer.cpp文件中,可以在示例代碼的Common目錄中找到。

4.3.3 幀之間的時間

當我們渲染我們的動畫幀時,我們需要知道幀之間經過了多少時間,以便我們可以根據已經過去的時間來更新我們的遊戲對象。幀之間經過的時間計算如下。令ti 爲第i幀期間性能計數器返回的時間,並使ti1 爲前一幀期間性能計數器返回的時間。那麼在ti1 讀數和ti 讀數之間經過的時間是Δt=titi1 。對於實時渲染,通常需要每秒至少30幀的平滑動畫(通常幀率會更高);因此,Δt=titi1 趨向於一個很小的數。

以下代碼顯示瞭如何在代碼中計算Δt:

void GameTimer::Tick()
{
    if(mStopped)
    {
        mDeltaTime = 0.0;
        return;
    }
    //Get the time this frame.
    __int64 currTime;
    QueryPerformanceCounter((LARGE_INTEGER*)&currTime);
    mCurrTime = currTime;

    // Time difference between this frame and the previous.
    mDeltaTime = (mCurrTime - mPrevTime)*mSecondsPerCount;

    // Prepare for next frame.
    mPrevTime = mCurrTime;

    // Force nonnegative. The DXSDK's CDXUTTimer mentions that if the
    // processor goes into a power save mode or we get shuffled to another
    // processor, then mDeltaTime can be negative.
    if(mDeltaTime < 0.0)
    {
        mDeltaTime = 0.0;
    }
}
float GameTimer::DeltaTime()const
{
    return (float)mDeltaTime;
}

函數Tick在應用程序消息循環中調用如下:

int D3DApp::Run()
{
    MSG msg = {0};
    mTimer.Reset();
    while(msg.message != WM_QUIT)
    {
        // If there are Window messages then process them.
        if(PeekMessage(&msg, 0, 0, 0, PM_REMOVE))
        {
            TranslateMessage(&msg);
            DispatchMessage(&msg);
        }
        //Otherwise, do animation/game stuff.
        else
        {
            mTimer.Tick();
            if(!mAppPaused)
            {
                CalculateFrameStats();
                UpdateScene(mTimer.DeltaTime());
                DrawScene();
            }
            else
            {
                Sleep(100);
            }
        }
    }
    return (int)msg.wParam;
}

以這種方式,每個幀計算Δt並反饋到UpdateScene方法,以便可以根據從上一幀動畫過去的時間來更新場景。Reset方法的實現是:

void GameTimer::Reset()
{
    __int64 currTime;
    QueryPerformanceCounter((LARGE_INTEGER*)&currTime);
    mBaseTime = currTime;
    mPrevTime = currTime;
    mStopTime = 0;
    mStopped = false;
}

所顯示的一些變量尚未討論(見§4.3.4)。但是,我們看到調用Reset時將初始化mPrevTime到當前時間。這樣做很重要,因爲對於第一幀動畫,沒有先前的幀,因此沒有以前的時間戳ti1 。因此,在消息循環開始之前,需要在Reset方法中初始化該值。

4.3.4總時間

另一個可用的測量時間是自應用程序啓動以來經過的時間,不包含暫停的時間; 我們稱之爲總時間。接下來說明如何使用。假設玩家有300秒完成一個關卡。當關卡開始時,我們可以得到從應用程序啓動起經過的時間tstart 。然後在關卡開始之後,我們可以檢查應用開始後的時間t。如果ttstart >300s(參見圖4.8),則玩家已經在關卡中超過300秒並且失敗。很明顯,在這種情況下,我們不想包含玩遊戲暫停的時間。

總時間的另一個應用是當我們想要根據時間函數製作動畫。假設我希望有一個光軌作爲時間的函數。其位置可以用參數方程來描述:

{x=10costy=20z=10sint

這裏t表示時間,並且隨着t(時間)增加,光的座標被更新,使得光在y = 20平面中以半徑爲10的圓移動。同樣,我們也不想計算暫停時間; 見圖4.9。

要實現總時間,我們使用以下變量:

__int64 mBaseTime;
__int64 mPausedTime;
__int64 mStopTime;

正如我們在§4.3.3中看到的那樣,mBaseTime被初始化爲當前調用Reset的時間。我們可以將此視爲應用程序啓動的時間。在大多數情況下,您只會在消息循環之前調用重置一次,因此mBaseTime在應用程序的生命週期始終保持不變。變量mPausedTime累積所有暫停消耗的時間。我們要累積這個時間,所以我們可以從總運行時間減去它,以便不計算暫停時間。mStopTime變量給我們定時器停止(暫停)的時間; 這是用來幫助我們跟蹤暫停的時間。

GameTimer類的兩個重要方法是StopStart。當應用程序分別暫停和取消暫停時,應該調用它們,以便GameTimer可以跟蹤暫停的時間。代碼註釋說明了這兩種方法的細節。

4-8
圖4.8。 計算關卡開始以來的時間。請注意,我們選擇應用程序開始時間作爲原點(零),並測量相對於該參考幀的時間值。

4-9
圖4.9。 如果我們在t1暫停,在t2取消暫停,並計算暫停時間,那麼當我們取消暫停時,位置將從p(t1)突然跳到p(t2)

void GameTimer::Stop()
{
    // If we are already stopped, then don't do anything.
    if(!mStopped)
    {
        __int64 currTime;
        QueryPerformanceCounter((LARGE_INTEGER*)&currTime);
        // Otherwise, save the time we stopped at, and set
        // the Boolean flag indicating the timer is stopped.
        mStopTime = currTime;
        mStopped = true;
    }
}

void GameTimer::Start()
{
    __int64 startTime;
    QueryPerformanceCounter((LARGE_INTEGER*)&startTime);
    // Accumulate the time elapsed between stop and start pairs.
    //
    // |<-------d------->|
    // ---------------*-----------------*------------> time
    // mStopTime startTime// If we are resuming the timer from a stopped state...
    if( mStopped )
    {
        // then accumulate the paused time.
        mPausedTime += (startTime - mStopTime);
        // since we are starting the timer back up, the current
        // previous time is not valid, as it occurred while paused.
        // So reset it to the current time.
        mPrevTime = startTime;
        // no longer stopped...
        mStopTime = 0;
        mStopped = false;
    }
}

最後,TotalTime成員函數返回自復位以來經過的時間不計算暫停時間,具體實現如下:

float GameTimer::TotalTime()const
{
    // If we are stopped, do not count the time that has passed
    // since we stopped. Moreover, if we previously already had
    // a pause, the distance mStopTime - mBaseTime includes paused
    // time,which we do not want to count. To correct this, we can
    // subtract the paused time from mStopTime:
    //
    // previous paused time
    // |<----------->|
    // ---*------------*-------------*-------*-----------*------> time
    // mBaseTime mStopTime mCurrTime
    if(mStopped)
    {
        return (float)(((mStopTime - mPausedTime)-mBaseTime)*mSecondsPerCount);
    } 

    //The distance mCurrTime - mBaseTime includes paused time,
    // which we do not want to count. To correct this, we can subtract
    // the paused time from mCurrTime:
    //
    // (mCurrTime - mPausedTime) - mBaseTime
    //
    // |<--paused time-->|
    // ----*---------------*-----------------*------------*------> time
    // mBaseTime mStopTime startTime mCurrTime
    else
    {
        return (float)(((mCurrTime-mPausedTime)-mBaseTime)*mSecondsPerCount);
    }
}

Note: 我們的示例創建了一個GameTimer實例,用於測量自應用程序啓動以來的總時間,和幀之間經過的時間; 但是,您也可以創建其他實例並將其用作通用“秒錶”。例如,當一個炸彈被點燃時,你可以啓動一個新的GameTimer,當TotalTime達到5秒鐘,觸發炸彈爆炸事件。

4.4演示應用框架

本書中的演示使用d3dUtil.hd3dApp.hd3dApp.cpp文件中的代碼,可以從本書的網站下載。這些在每個演示應用程序中使用的通用文件位於本書第二部分和第三部分的Common目錄中,以便每個項目中的文件不會重複。d3dUtil.h文件包含有用的實用程序代碼,d3dApp.hd3dApp.cpp文件包含用於封裝Direct3D應用程序的核心Direct3D應用程序類代碼。請自行研習相關代碼,我們不會詳細講解每一行代碼(例如,我們不顯示如何創建窗口,因爲基本的Win32編程是本書的先決條件)。該框架的目標是隱藏窗口創建代碼和Direct3D初始化代碼; 通過隱藏這段代碼,我們覺得它使得演示更不會分散注意力,因爲您只能關注示例代碼試圖說明的具體細節。

4.4.1 D3DApp

D3DApp類是基本的Direct3D應用程序類,它提供了創建主應用程序窗口,運行應用程序消息循環,處理窗口消息以及初始化Direct3D的功能。此外,該類定義了演示應用程序的框架功能。客戶端將從D3DApp派生,覆蓋虛擬框架函數,並且僅實例化派生的D3DApp類的單個實例。D3DApp類定義如下:

class D3DApp
{
    public:
    D3DApp(HINSTANCE hInstance);
    virtual ~D3DApp();
    HINSTANCE AppInst()const;
    HWND MainWnd()const;
    float AspectRatio()const;

    int Run();

    // Framework methods. Derived client class overrides these methods to
    // implement specific application requirements.
    // 框架方法。派生類需要重載這些方法實現所需的功能。
    virtual bool Init();
    virtual void OnResize();
    virtual void UpdateScene(float dt)=0;
    virtual void DrawScene()=0;
    virtual LRESULT MsgProc(HWND hwnd, UINT msg,WPARAM wParam, LPARAM lParam);
    // Convenience overrides for handling mouse input.
     // 處理鼠標輸入事件的便捷重載函數
    virtual void OnMouseDown(WPARAM btnState, int x, int y){ }
    virtual void OnMouseUp(WPARAM btnState, int x, int y){ }
    virtual void OnMouseMove(WPARAM btnState, int x, int y){ }

    protected:
    bool InitMainWindow();
    bool InitDirect3D();

    void CalculateFrameStats();

    protected:
    HINSTANCE mhAppInst; // application instance handle應用程序實例句柄
    HWND mhMainWnd; // main window handle主窗口句柄
    bool mAppPaused; // is the application paused?程序是否處在暫停狀態
    bool mMinimized; // is the application minimized?程序是否最小化
    bool mMaximized; // is the application maximized?程序是否最大化
    bool mResizing; // are the resize bars being dragged?程序是否處在改變大小的狀態
    UINT m4xMsaaQuality; // quality level of 4X MSAA 4X MSAA質量等級
    // Used to keep track of the "delta-time" and game time (§4.3).
    // 用於記錄"delta-time"和遊戲時間(§4.3)
    GameTimer mTimer;

    // The D3D11 device (§4.2.1), the swap chain for page flipping
    // (§4.2.4), the 2D texture for the depth/stencil buffer (§4.2.6),
    // the render target (§4.2.5) and depth/stencil views (§4.2.6), and
    // the viewport (§4.2.8).
    //  D3D11設備(§4.2.1),交換鏈(§4.2.4),用於深度/模板緩存的2D紋理(§4.2.6),
    //  渲染目標(§4.2.5)和深度/模板視圖(§4.2.6),和視口(§4.2.8)。
    ID3D11Device* md3dDevice;
    ID3D11DeviceContext* md3dImmediateContext;
    IDXGISwapChain* mSwapChain;
    ID3D11Texture2D* mDepthStencilBuffer;
    ID3D11RenderTargetView* mRenderTargetView;
    ID3D11DepthStencilView* mDepthStencilView;
    D3D11_VIEWPORT mScreenViewport;

    // The following variables are initialized in the D3DApp constructor
    // to default values. However, you can override the values in the
    // derived class constructor to pick different defaults.
    // Window title/caption. D3DApp defaults to "D3D11 Application".
    //  下面的變量是在D3DApp構造函數中設置的。但是,你可以在派生類中重寫這些值。
    //  窗口標題。D3DApp的默認標題是"D3D11 Application"。
    std::wstring mMainWndCaption;

    // Hardware device or reference device? D3DApp defaults to
    // D3D_DRIVER_TYPE_HARDWARE.
    //  Hardware device還是reference device?D3DApp默認使用D3D_DRIVER_TYPE_HARDWARE。
    D3D_DRIVER_TYPE md3dDriverType;

    // Initial size of the window's client area. D3DApp defaults to
    // 800x600. Note, however, that these values change at runtime
    // to reflect the current client area size as the window is resized.
    // 窗口的初始大小。D3DApp默認爲800x600。注意,當窗口大小在運行階段改變時,這些值也會隨之改變。
    int mClientWidth;
    int mClientHeight;

    // True to use 4X MSAA (§4.1.8). The default is false.
    // 設置爲true則使用4XMSAA(§4.1.8),默認爲false。
    bool mEnable4xMsaa;
};

我們在前面的代碼中講解了一些數據成員; 這些方法將在後續章節中討論。

4.4.2 Non-Framework 方法

1.D3DApp:構造函數簡單地將數據成員初始化爲默認值。
2.~D3DApp:析構函數釋放D3DApp獲取的COM接口。
3.AppInst:普通的訪問函數返回應用程序實例句柄的副本。
4.MainWnd:簡單訪問功能返回主窗口句柄的副本。
5.AspectRatio:縱橫比定義爲後緩衝區寬度與其高度的比值。寬高比將在第五章中用到。它被簡單地實現爲:

float D3DApp::AspectRatio()const
{
    return static_cast<float>(mClientWidth) / mClientHeight;
}

6.Run:此方法將應用程序消息循環。它使用Win32 PeekMessage功能,以便在沒有消息存在時可以處理我們的遊戲邏輯。該功能的實現見§4.3.3。
7.InitMainWindow:初始化主應用程序窗口; 我們假設讀者熟悉基本的Win32窗口初始化。
8.InitDirect3D:通過執行§4.2中討論的步驟來初始化Direct3D。
9.CalculateFrameStats:計算每秒的平均幀數和每幀的平均毫秒數。§4.4.4中討論了這種方法的實現。

4.4.3 Framework 方法

對於本書中的每個示例應用程序,我們始終覆蓋了D3DApp的五個虛擬函數。 這五個函數用於實現特定樣本的特定代碼。此設置的好處是初始化代碼,消息處理等在D3DApp類中實現,因此派生類只需要專注於演示應用程序的特定代碼。以下是框架方法的描述:
1.Init:使用此方法爲應用程序初始化代碼,如分配資源,初始化對象和設置燈光。該方法的D3DApp實現調用InitMainWindowInitDirect3D; 因此,您應該首先在派生的實現中調用此方法的D3DApp版本,如下所示:

bool TestApp::Init()
{
    if(!D3DApp::Init())
        return false;
    /* Rest of initialization code goes here */
}

以便ID3D11Device可用於其餘的初始化代碼。(Direct3D資源獲取需要一個有效的ID3D11Device。)
2.OnResize:當接收到WM_SIZE消息時,此方法由D3DApp::MsgProc調用。當窗口調整大小時,需要更改一些Direct3D屬性,因爲它們取決於客戶區域的尺寸。特別地,後臺緩衝區和深度/模板緩衝區需要重新創建以匹配窗口的新客戶區。可以通過調用IDXGISwapChain :: ResizeBuffers方法調整後臺緩衝區大小。深度/模板緩衝區需要被銷燬,然後根據新的維度進行重構。此外,還需要重新創建渲染目標和深度/模板視圖。OnResizeD3DApp實現處理調整back和depth/stencil緩衝區大小所需的代碼; 看到源代碼的簡單細節。除緩衝區之外,其他屬性取決於客戶區域的大小(例如,投影矩陣),因此該方法是框架的一部分,因爲客戶端代碼可能需要在調整窗口大小時執行其自己的一些代碼。
3.UpdateScene:該抽象方法被每幀調用,並且應該用於隨時間更新3D應用(例如,執行動畫,移動相機,進行碰撞檢測,檢查用戶輸入等)。
4.DrawScene:這個抽象方法被每幀調用,並且是我們發出渲染命令的地方,以便將我們當前的幀實際繪製到後臺緩衝區。完成繪製框架後,我們調用IDXGISwapChain::Present方法將後臺緩衝區呈現給屏幕。
5.MsgProc:該方法實現主應用程序窗口的窗口過程功能。通常情況下,只有在需要處理D3DApp::MsgProc的消息時才需要重寫此方法(或者不符合您的喜好時)。§4.4.5中探討了該方法的D3DApp實現。如果您重寫此方法,則任何不處理的消息都應轉發給D3DApp::MsgProc

Note:除了前五種幀方法之外,我們還提供了另外三種虛擬函數,以方便用戶按下,釋放鼠標按鈕,鼠標移動時處理事件。
virtual void OnMouseDown(WPARAM btnState, int x, int y){ }
virtual void OnMouseUp(WPARAM btnState, int x, int y) { }
virtual void OnMouseMove(WPARAM btnState, int x, int y){ }
這樣,如果要處理鼠標消息,則可以覆蓋這些方法,而不是覆蓋MsgProc方法。第一個參數與各種鼠標消息的WPARAM參數相同,存儲鼠標按鈕狀態(即哪個事件發生時按下鼠標按鈕)。第二和第三個參數是鼠標光標的客戶區(x,y)座標。

4.4.4 Frame 統計

遊戲和圖形應用程序通常測量每秒渲染的幀數(FPS)。爲此,我們簡單地計算在特定時間段 t 內處理的幀的數量(並將其存儲在變量n中)。然後,時間段 t 內的平均FPS爲fpsavg=n/t 。如果我們設置t=1,則fpsavg=n/1=n 。在我們的代碼中,我們使用t=1(秒),因爲它避免了一個除法,而且一秒給出了相當不錯的平均值 - 它不是太長而不是太短。計算FPS的代碼是由D3DApp :: CalculateFrameStats方法提供:

void D3DApp::CalculateFrameStats()
{
    // Code computes the average frames per second, and also the
    // average time it takes to render one frame. These stats
    // are appeneded to the window caption bar.static int frameCnt = 0;
    static float timeElapsed = 0.0f;
    frameCnt++;

    // Compute averages over one second period.
    if( (mTimer.TotalTime() - timeElapsed) >= 1.0f )
    {
        float fps = (float)frameCnt; // fps = frameCnt / 1
        float mspf = 1000.0f / fps;

        std::wostringstream outs;
        outs.precision(6);
        outs << mMainWndCaption << L" "
            << L"FPS: " << fps << L" "
            << L"Frame Time: " << mspf << L" (ms)";
        SetWindowText(mhMainWnd, outs.str().c_str());

        // Reset for next average.
        frameCnt = 0;
        timeElapsed += 1.0f;
    }
}

這個方法每幀調用,以便對幀進行計數。

除了計算FPS之外,以前的代碼還計算平均處理一個幀所需的毫秒數:

float mspf = 1000.0f / fps;

NOTE:每幀的秒數只是FPS的倒數,但是我們乘以1000 ms / 1 s,從秒到毫秒轉換(每秒鐘有1000毫秒)。

這一行的想法是計算渲染幀所需的時間(以毫秒爲單位);這是與FPS不同的數量(但是觀察該值可以從FPS導出)。實際上,渲染幀所需的時間比FPS更有用,因爲我們可以直接看到在修改場景時渲染幀所需的時間的增加/減少。另一方面,當我們修改我們的場景時,FPS不會輕易告訴我們時間的增加/減少。此外,正如[Dunlop03]在他的文章FPS與Frame Time中所指出的,由於FPS曲線的非線性,使用FPS可能會產生誤導的結果。例如,考慮情況(1):假設我們的應用程序運行在1000 FPS,以1 ms(毫秒)渲染幀。如果幀速率降至250 FPS,則渲染幀需要4 ms。現在考慮情況(2):假設我們的應用程序運行在100 FPS,需要10 ms渲染一幀。如果幀速率下降到約76.9 FPS,則渲染幀大約需要13 ms。在這兩種情況下,每幀的渲染增加了3 ms,因此兩者都代表渲染幀所需的時間相同。閱讀FPS並不直接。從1000 FPS到250 FPS的下降似乎比從100 FPS下降到76.9 FPS要大得多;然而,正如我們剛剛顯示的,它們實際上代表了渲染幀所花費的時間的增加。

4.4.5消息處理程序

我們爲我們的應用程序框架實現的窗口過程是最低限度的。一般來說,我們不關心Win32消息運行原理。實際上,我們的應用程序代碼的核心在空閒處理期間被執行(即,當沒有窗口消息存在時)。同時,還需要處理一些重要的信息。但由於窗口過程的長度,我們並沒有在這裏嵌入所有的代碼。相反,我們只是解釋我們處理的每個消息背後的動機。有興趣的朋友可以下載源代碼文件,花一些時間熟悉應用程序框架代碼,因爲它是本書每個示例的基礎。

我們處理的第一條消息是WM_ACTIVATE消息。當應用程序激活或停用時,會發送此消息。具體實現如下:

case WM_ACTIVATE:
if(LOWORD(wParam) == WA_INACTIVE)
{
    mAppPaused = true;
    mTimer.Stop();
}else
{
    mAppPaused = false;
    mTimer.Start();
}return 0;

如您所見,當我們的應用程序停用時,我們將數據成員mAppPaused設置爲true,當我們的應用程序變爲活動狀態時,我們將數據成員mAppPaused設置爲false。另外,當應用程序暫停時,我們停止定時器,然後一旦應用程序再次起作用就恢復定時器。如果我們回顧D3DApp::Run(§4.3.3)的實現,我們發現如果我們的應用程序暫停,我們不會更新我們的應用程序代碼,而是將一些CPU週期釋放回操作系統; 這樣,當我們的應用程序處於非活動狀態時,它不會佔用CPU週期。

我們處理的下一個消息是WM_SIZE消息。回想一下,在調整窗口大小時調用此消息。處理此消息的主要原因是我們希望後臺緩衝區和深度/模板尺寸與客戶區域矩形的尺寸匹配(因此不會發生拉伸)。因此,每次調整窗口大小時,我們要調整緩衝區大小的大小。調整緩衝區大小的代碼在D3DApp :: OnResize中實現。如前所述,可以通過調用IDXGISwapChain :: ResizeBuffers方法調整後臺緩衝區大小。深度/模板緩衝區需要被破壞,然後根據新的維度進行重新構建。此外,還需要重新創建渲染目標和深度/模板視圖。如果用戶正在拖動調整大小的欄,我們必須小心,因爲拖動調整大小的條可以發送連續的WM_SIZE消息,我們不想不斷調整緩衝區的大小。因此,如果我們確定用戶通過拖動調整大小,實際上什麼也不做(暫停應用程序除外),直到用戶拖動調整大小的欄。我們可以通過處理WM_EXITSIZEMOVE消息來實現。當用戶釋放調整大小時,會發送此消息

// WM_ENTERSIZEMOVE is sent when the user grabs the resize bars.
case WM_ENTERSIZEMOVE:
    mAppPaused = true;
    mResizing = true;
    mTimer.Stop();
    return 0;

// WM_EXITSIZEMOVE is sent when the user releases the resize bars.
// Here we reset everything based on the new window dimensions.
case WM_EXITSIZEMOVE:
    mAppPaused = false;
    mResizing = false;
    mTimer.Start();
    OnResize();
    return 0;

我們處理的接下來的三個消息是微不足道的,所以我們只顯示代碼:

// WM_DESTROY is sent when the window is being destroyed.
case WM_DESTROY:
    PostQuitMessage(0);
    return 0;

// The WM_MENUCHAR message is sent when a menu is active and the user presses
// a key that does not correspond to any mnemonic or accelerator key.
case WM_MENUCHAR:
    // Don't beep when we alt-enter.
    return MAKELRESULT(0, MNC_CLOSE);

// Catch this message to prevent the window from becoming too small.
case WM_GETMINMAXINFO:
    ((MINMAXINFO*)lParam)->ptMinTrackSize.x = 200;
    ((MINMAXINFO*)lParam)->ptMinTrackSize.y = 200;
    return 0;

最後,爲了支持您的鼠標輸入虛函數,我們處理以下鼠標消息如下:

case WM_LBUTTONDOWN:case WM_MBUTTONDOWN:
case WM_RBUTTONDOWN:
    OnMouseDown(wParam, GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam));
    return 0;

case WM_LBUTTONUP:
case WM_MBUTTONUP:
case WM_RBUTTONUP:
    OnMouseUp(wParam, GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam));
    return 0;

case WM_MOUSEMOVE:
    OnMouseMove(wParam, GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam));
    return 0;

Note:我們必須 #include <Windows.h>爲GET_X_LPARAMGET_X_LPARAM宏。

4.4.6全屏

我們創建的IDXGISwapChain接口自動捕獲ALT-ENTER組合鍵並將應用程序切換到全屏模式。在全屏模式下按ALT-ENTER將切換回窗口模式。在模式切換期間,應用程序窗口將被調整大小,並嚮應用程序發送WM_SIZE消息; 這使應用程序有機會調整後臺和深度/模板緩衝區的大小以匹配新的屏幕尺寸。此外,如果切換到全屏模式,窗口樣式將更改爲全屏。您可以使用Visual Studio Spy + +工具來查看在ALT-ENTER中爲一個演示應用程序生成的Windows消息。

4-10
圖4.10 第4章示例程序的截圖

練習會探索如何禁用默認的ALT-ENTER功能。
可能複習一下§4.2.3中的DXGI_SWAP_CHAIN_DESC :: Flags描述。

4.4.7 “Init Direct3D”演示

現在我們討論了應用程序框架,讓我們用它做一個小應用程序。父類D3DApp做了這個演示所需的大部分工作,所以該程序幾乎不需要我們做什麼工作。要注意的是我們如何從D3DApp派生一個類並實現框架函數,我們將在這裏編寫特定的示例代碼。本書中的所有程序都將遵循相同的模板。

#include "d3dApp.h"
class InitDirect3DApp : public D3DApp
{
    public:
    InitDirect3DApp(HINSTANCE hInstance);
    ~InitDirect3DApp();

    bool Init();
    void OnResize();
    void UpdateScene(float dt);
    void DrawScene();
};

int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE prevInstance,
PSTR cmdLine, int showCmd)
{
    // Enable run-time memory check for debug builds.
#if defined(DEBUG) | defined(_DEBUG)
    _CrtSetDbgFlag( _CRTDBG_ALLOC_MEM_DF | _CRTDBG_LEAK_CHECK_DF );
#endif
    InitDirect3DApp theApp(hInstance);
    if( !theApp.Init() )
        return 0;

    return theApp.Run();
} 

InitDirect3DApp::InitDirect3DApp(HINSTANCE hInstance)
: D3DApp(hInstance)
{
}

InitDirect3DApp::~InitDirect3DApp()
{
}

bool InitDirect3DApp::Init()
{
    if(!D3DApp::Init())
        return false;
    return true;
} 

void InitDirect3DApp::OnResize()
{
    D3DApp::OnResize();
}

 void InitDirect3DApp::UpdateScene(float dt)
{
}

void InitDirect3DApp::DrawScene()
{
    assert(md3dImmediateContext);
    assert(mSwapChain);
    // Clear the back buffer blue. Colors::Blue is defined in d3dUtil.h.
    md3dImmediateContext->ClearRenderTargetView(mRenderTargetView,reinterpret_cast<const float*>(&Colors::Blue));

    // Clear the depth buffer to 1.0f and the stencil buffer to 0.
    md3dImmediateContext->ClearDepthStencilView(mDepthStencilView,D3D11_CLEAR_DEPTH|D3D11_CLEAR_STENCIL, 1.0f, 0);

    // Present the back buffer to the screen.
    HR(mSwapChain->Present(0, 0));
}

4.5調試DIRECT3D應用程序

爲了縮短代碼並儘量減少干擾,我們主要在本書中省略錯誤處理。然而,我們實現一個宏來檢查許多Direct3D函數返回的HRESULT返回碼。我們的宏在d3dUtil.h中定義如下:

#if defined(DEBUG) | defined(_DEBUG)
    #ifndef HR
    #define HR(x)                                               \
    {                                                           \
        HRESULT hr = (x);                                       \
        if(FAILED(hr))                                          \
        {                                                       \
            DXTrace(__FILE__, (DWORD)__LINE__, hr, L#x, true);  \
        }                                                       \
    }#endif
#else
    #ifndef HR
    #define HR(x) (x)
    #endif
#endif

如果返回函數的返回碼指示失敗,那麼我們將返回碼傳遞到DXTrace函數(#include <dxerr.h>和鏈接dxerr.lib)中:

HRESULT WINAPI DXTraceW(const char* strFile, DWORD dwLine,HRESULT hr, const WCHAR* strMsg, BOOL bPopMsgBox);

此功能顯示一條漂亮的消息框,指示發生錯誤的文件和行號,以及錯誤的文本描述以及生成錯誤的函數的名稱; 圖4.11展示了一個例子。請注意,如果爲DXTrace的最後一個參數指定了false,則不會顯示消息框,調試信息將被輸出到Visual C ++輸出窗口。請注意,如果我們不在調試模式下,宏HR不執行任何操作。此外,HR必須是宏而不是函數;否則__FILE____LINE__將引用函數實現的文件和行,而不是調用函數HR的文件和行。

要使用這個宏,我們只是圍繞一個返回HRESULT的Direct3D函數,如下例所示:

HR(D3DX11CreateShaderResourceViewFromFile(md3dDevice,L"grass.dds", 0, 0, &mGrassTexRV, 0 ));

這適用於調試我們的演示示例,但是真正的應用程序需要處理應用程序運行後可能發生的錯誤(例如,不支持的硬件,缺少的文件等)。

Note:L#x將HR宏參數令牌轉換爲Unicode字符串。這樣,我們可以將導致錯誤的函數調用輸出到消息框。

4.6 總結

1.Direct3D可以被認爲是程序員和圖形硬件之間的中介者。例如,程序員調用Direct3D函數將資源視圖綁定到硬件渲染管道,配置渲染管道的輸出,並繪製3D幾何。
2.在Direct3D 11中,Direct3D 11功能的圖形設備必須支持整個Direct3D 11功能集,除了少數例外。
3.組件對象模型(COM)是允許DirectX與語言無關並具有向後兼容性的技術。Direct3D程序員不需要知道COM的細節及其工作原理;他們只需要知道如何獲取COM接口以及如何釋放它們。
4.1D紋理像數組元素的1D陣列,2D紋理就像數組元素的2D陣列,3D紋理像數組元素的3D陣列。紋理的元素必須具有由DXGI_FORMAT枚舉類型的成員描述的格式。紋理通常包含圖像數據,但它們也可以包含其他數據,例如深度信息(例如,深度緩衝區)。GPU可以對紋理進行特殊操作,例如過濾和多重採樣。
5.在Direct3D中,資源不直接綁定到管道。相反,資源視圖綁定到管道。可以創建單個資源的不同視圖。以這種方式,單個資源可能綁定到呈現管道的不同階段。如果使用無類型格式創建資源,則必須在創建視圖時指定該類型。
6.ID3D11DeviceID3D11DeviceContext接口可以被認爲是我們的物理圖形設備硬件的軟件控制器; 也就是說,通過這些接口,我們可以與硬件進行交互並指示它執行操作。ID3D11Device接口負責檢查功能支持和分配資源。ID3D11DeviceContext接口負責設置渲染狀態,將資源綁定到圖形管道,併發出渲染命令。

4-11
圖4.11 如果Direct3D函數返回錯誤,則由DXTrace函數顯示的消息框。

7.爲了避免動畫中的閃爍,最好將整個動畫幀繪製到一個稱爲後臺緩衝區的屏幕的紋理上。一旦整個場景被繪製到給定的動畫幀的後臺緩衝區中,它被呈現給屏幕爲一體完整幀以這種方式,觀看者不會看到幀的繪製。在將幀拖到後臺緩衝區之後,後臺緩衝區和前端緩衝區的交換:後臺緩衝區成爲前端緩衝區,前端緩衝區成爲下一幀動畫的後臺緩衝區。交換後臺和前端緩衝區稱爲呈現。前端和後端緩衝區形成一個由IDXGISwapChain接口表示的交換鏈。使用兩個緩衝區稱爲雙緩衝。
8.假設不透明的場景對象,最靠近相機的點會遮擋其後面的任何點。深度緩衝是用於確定最靠近相機的場景中的點的技術。這樣我們就不必擔心我們的場景對象的順序了。
9.性能計數器是一種高分辨率定時器,可以提供精確的定時測量,用於測量小的時差,例如幀間經過的時間。性能計時器以稱爲計數的時間單位工作。TheQueryPerformanceFrequency輸出性能計時器的每秒計數,然後可以將其從計數單位轉換爲秒。性能計時器的當前時間值(以計數計)由此獲得QueryPerformanceCounter函數。
10.爲了計算平均每秒幀數(FPS),我們計算一段時間間隔Δt處理的幀數。令n是隨時間Δt計數的幀數,則在該時間間隔內每秒的平均幀數爲fpsavg=n/Δt 。幀率可以給出關於性能的誤導性結論;處理框架所需的時間更多。花費處理幀的時間(秒)是幀速率的倒數(即1/fpsavg )。
11.示例框架用於提供本書中所有演示應用程序的一致性界面。d3dUtil.h,d3dApp.h和d3dApp.cpp文件中提供的代碼包裝每個應用程序必須實現的標準初始化代碼。通過封裝這個代碼,隱藏細節,這樣可以讓樣本更加專注於展示當前的主題。
12.對於調試模式構建,使用D3D11_CREATE_DEVICE_DEBUG標誌創建Direct3D設備以啓用調試層。當指定調試標誌時,Direct3D將向VC ++輸出窗口發送調試消息。還可以使用D3DX庫的調試版本(如 d3dx11d.lib)進行調試版本。

4.7 練習

1.通過禁用ALT-ENTER功能在全屏和窗口模式之間切換來修改以前的練習方案;使用IDXGIFactory :: MakeWindowAssociation方法並指定DXGI_MWA_NO_WINDOW_CHANGES標誌,以便DXGI不監視消息隊列。請注意,IDXGIFactory :: CreateSwapChain被調用後需要調用IDXGIFactory :: MakeWindowAssociation方法。

2.一些系統具有多個適配器(視頻卡),並且應用程序可能希望讓用戶選擇使用哪個適配器,而不是始終使用默認適配器。使用IDXGIFactory :: EnumAdapters方法來確定系統上有多少適配器。

3.對於系統所擁有的每個適配器,IDXGIFactory :: EnumAdapters輸出一個指向已填寫的IDXGIAdapter接口的指針。該接口可用於查詢有關適配器的信息。使用IDXGIAdapter :: CheckInterfaceSupport方法查看系統上的適配器是否支持Direct3D 11。
適配器具有與其相關聯的輸出(例如,監視器)。您可以使用IDXGIAdapter :: EnumOutputs方法枚舉特定適配器的輸出。使用此方法確定默認適配器的輸出數量。

  1. 適配器具有與其相關聯的輸出(例如,監視器)。 您可以使用IDXGIAdapter :: EnumOutputs方法枚舉特定適配器的輸出。 使用此方法確定默認適配器的輸出數量。

5.每個輸出都有一個給定像素格式支持的顯示模式列表(DXGI_MODE_DESC)。 對於每個輸出(IDXGIOutput),顯示輸出支持的每個顯示模式的寬度,高度和刷新率DXGI_FORMAT_R8G8B8A8_UNORM格式使用IDXGIOutput :: GetDisplayModeList方法。練習2,3,4和5的輸出示例如下。 使用OutputDebugString函數快速輸出到VC ++輸出窗口是非常有用的。

*** NUM ADAPTERS = 1
*** D3D11 SUPPORTED FOR ADAPTER 0
*** NUM OUTPUTS FOR DEFAULT ADAPTER = 1
***WIDTH = 640 HEIGHT = 480 REFRESH = 60000/1000
***WIDTH = 640 HEIGHT = 480 REFRESH = 72000/1000
***WIDTH = 640 HEIGHT = 480 REFRESH = 75000/1000
***WIDTH = 720 HEIGHT = 480 REFRESH = 56250/1000
***WIDTH = 720 HEIGHT = 480 REFRESH = 56250/1000
***WIDTH = 720 HEIGHT = 480 REFRESH = 60000/1000
***WIDTH = 720 HEIGHT = 480 REFRESH = 60000/1000
***WIDTH = 720 HEIGHT = 480 REFRESH = 72188/1000
***WIDTH = 720 HEIGHT = 480 REFRESH = 72188/1000
***WIDTH = 720 HEIGHT = 480 REFRESH = 75000/1000
***WIDTH = 720 HEIGHT = 480 REFRESH = 75000/1000
***WIDTH = 720 HEIGHT = 576 REFRESH = 56250/1000
***WIDTH = 720 HEIGHT = 576 REFRESH = 56250/1000
***WIDTH = 720 HEIGHT = 576 REFRESH = 60000/1000
***WIDTH = 720 HEIGHT = 576 REFRESH = 60000/1000
***WIDTH = 720 HEIGHT = 576 REFRESH = 72188/1000
***WIDTH = 720 HEIGHT = 576 REFRESH = 72188/1000
***WIDTH = 720 HEIGHT = 576 REFRESH = 75000/1000
***WIDTH = 720 HEIGHT = 576 REFRESH = 75000/1000
***WIDTH = 800 HEIGHT = 600 REFRESH = 56250/1000
***WIDTH = 800 HEIGHT = 600 REFRESH = 60000/1000
***WIDTH = 800 HEIGHT = 600 REFRESH = 60000/1000
***WIDTH = 800 HEIGHT = 600 REFRESH = 72188/1000
***WIDTH = 800 HEIGHT = 600 REFRESH = 75000/1000
***WIDTH = 848 HEIGHT = 480 REFRESH = 60000/1000
***WIDTH = 848 HEIGHT = 480 REFRESH = 60000/1000
***WIDTH = 848 HEIGHT = 480 REFRESH = 70069/1000
***WIDTH = 848 HEIGHT = 480 REFRESH = 70069/1000
***WIDTH = 848 HEIGHT = 480 REFRESH = 75029/1000
***WIDTH = 848 HEIGHT = 480 REFRESH = 75029/1000
***WIDTH = 960 HEIGHT = 600 REFRESH = 60000/1000
***WIDTH = 960 HEIGHT = 600 REFRESH = 60000/1000
***WIDTH = 960 HEIGHT = 600 REFRESH = 70069/1000
***WIDTH = 960 HEIGHT = 600 REFRESH = 70069/1000
***WIDTH = 960 HEIGHT = 600 REFRESH = 75029/1000
***WIDTH = 960 HEIGHT = 600 REFRESH = 75029/1000
***WIDTH = 1024 HEIGHT = 768 REFRESH = 60000/1000
***WIDTH = 1024 HEIGHT = 768 REFRESH = 60000/1000
***WIDTH = 1024 HEIGHT = 768 REFRESH = 70069/1000
***WIDTH = 1024 HEIGHT = 768 REFRESH = 75029/1000
***WIDTH = 1152 HEIGHT = 864 REFRESH = 60000/1000
***WIDTH = 1152 HEIGHT = 864 REFRESH = 60000/1000
***WIDTH = 1152 HEIGHT = 864 REFRESH = 75000/1000
***WIDTH = 1280 HEIGHT = 720 REFRESH = 60000/1000
***WIDTH = 1280 HEIGHT = 720 REFRESH = 60000/1000
***WIDTH = 1280 HEIGHT = 720 REFRESH = 60000/1001
***WIDTH = 1280 HEIGHT = 768 REFRESH = 60000/1000
***WIDTH = 1280 HEIGHT = 768 REFRESH = 60000/1000
***WIDTH = 1280 HEIGHT = 800 REFRESH = 60000/1000
***WIDTH = 1280 HEIGHT = 800 REFRESH = 60000/1000
***WIDTH = 1280 HEIGHT = 960 REFRESH = 60000/1000
***WIDTH = 1280 HEIGHT = 960 REFRESH = 60000/1000
***WIDTH = 1280 HEIGHT = 1024 REFRESH = 60000/1000
***WIDTH = 1280 HEIGHT = 1024 REFRESH = 60000/1000
***WIDTH = 1280 HEIGHT = 1024 REFRESH = 75025/1000
***WIDTH = 1360 HEIGHT = 768 REFRESH = 60000/1000
***WIDTH = 1360 HEIGHT = 768 REFRESH = 60000/1000
***WIDTH = 1600 HEIGHT = 1200 REFRESH = 60000/1000

6.嘗試修改視口設置以將場景繪製爲後緩衝區的子矩形。 例如,嘗試:

D3D11_VIEWPORT vp;
    vp.TopLeftX = 100.0f;
    vp.TopLeftY = 100.0f;
    vp.Width = 500.0f;
    vp.Height = 400.0f;
    vp.MinDepth = 0.0f;
    vp.MaxDepth = 1.0f;
發佈了7 篇原創文章 · 獲贊 7 · 訪問量 1萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章