DirectX11 With Windows SDK--01 DirectX11初始化


前言

由於個人覺得龍書里面第4章提供的Direct3D 初始化項目封裝得比較好,而且DirectX SDK Samples里面的初始化程序過於精簡,不適合后續使用,故選擇了以Init Direct3D項目作為框架,然后還使用了微軟提供的示例項目,兩者結合到一起。你需要下載源碼后再開始閱讀

此外,在閱讀本章內容之前,你還需要先瀏覽下面這些內容

章節
教程項目無法編譯、運行的解決方法
HLSL語法入門
ComPtr智能指針
HR宏關於dxerr庫的替代方案

這一章內容大部分屬於龍書的內容,但仍有一些不同的地方,這些是跟龍書有差異的部分:

  1. ComPtr智能指針
  2. 新的HR宏
  3. D3D11.1設備的創建

學習目標

  1. 了解組件對象模型COM與Direct3D 11的聯系,以及掌握ComPtr的使用
  2. 掌握基本的圖形學概念,如頁面翻轉、深度緩沖、多重采樣
  3. 了解Direct3D設備、設備上下文,以及DXGI交換鏈的作用
  4. 了解Direct3D 11.x的初始化過程
  5. 了解如何新建Direct3D項目,熟悉當前項目的代碼框架
  6. Win32創建窗口和高精度計時器GameTimer的實現按實際需要自行學習,教程不提及

初始化部分的框架通常比較穩定,而且第一遍看下來可能會有很多不理解的地方。可以先從第二章往后開始入手,看到一定程度后再回頭看。

DirectX11 With Windows SDK完整目錄

Github項目源碼

歡迎加入QQ群: 727623616 可以一起探討DX11,以及有什么問題也可以在這里匯報。

如何開啟新項目

安裝准備

如果你打算從VS2015開始,則在安裝的時候需要勾選下列選項:

編程語言那一項會自動被勾選。

而如果是使用VS2017或19的話,則在安裝的時候需要勾選下列選項:

注意:VS2015使用的Windows SDK版本為10.0.14393.0,VS2017和VS2019可以使用當前你所安裝的Windows SDK版本

安裝完成后,新建項目從Windows桌面向導開始:

在填寫好項目名稱后,點擊創建會彈出這樣一個子窗口。我們需要按下圖的方式操作來創建出一個空項目的桌面應用程序,最后點擊確定即可:

移除你的項目中有關DX SDK的庫路徑和包含路徑

如果你曾經用過DX SDK來編寫DX項目,務必要把你之前配置的DX SDK庫路徑和包含路徑給清理掉,使用項目默認的庫路徑和包含路徑!

鏈接靜態庫

這里的每一個項目都需要包含靜態庫:d3d11.lib,dxgi.lib,dxguid.lib,D3DCompiler.libwinmm.lib。可以在d3dApp.h添加下面的語句:

#pragma comment(lib, "d3d11.lib")
#pragma comment(lib, "dxgi.lib")
#pragma comment(lib, "dxguid.lib")
#pragma comment(lib, "D3DCompiler.lib")
#pragma comment(lib, "winmm.lib")

也可以在項目屬性-鏈接器-輸入-附加依賴項 添加上面的庫。

字符集設置為Unicode

在項目屬性頁中可以直接進行修改。

Win7系統下的額外配置

由於Win10 SDK中的某些函數在Win7是不支持的,我們還需要在屬性頁-配置屬性-C/C++ -預處理器中,添加預處理器定義以限制API集合:_WIN32_WINNT=0x601

文件本身的編碼設置及編譯時編碼識別

現在你可以嘗試將本教程項目01中用到的所有頭文件和源文件復制到你的項目目錄,再把它們拖進你的項目中。然后你需要按下面的部分修改。

在項目屬性頁-C/C++ -命令行中添加/utf-8來強制指定代碼頁

一般拖過去的代碼文件,即所給項目下的所有*.cpp, *.h, *.hlsl, *.hlsli使用的文件編碼為Unicode(UTF-8無簽名)-代碼頁65001.但從VS創建出來的文件默認編碼是ANSI編碼(詳細的為簡體中文(GB2312)-代碼頁936),需要對非UTF-8無簽名編碼的文件進行轉碼。

有一種比較麻煩的方法是,在解決方案資源管理器中選中指定文件后,點擊文件-另存為-編碼保存,最后找到所需編碼即可。

當然更理想的辦法是在VS安裝擴展。在擴展-管理擴展,然后找到Force UTF-8(No BOM)安裝即可

安裝該插件后,在你保存代碼的時候就會強制修改文件編碼為UTF-8了。

注意:HLSL編譯器使用的是ANSI編碼,使用UTF-8編碼的HLSL文件如果只是注釋為中文還是沒有問題的,但不能在代碼部分出現中文,否則編譯不通過。

子系統問題

如果你是直接用空項目的模板創建的項目,那很可能會遇到下面的問題:

因為默認的空項目指定的子系統有可能是未明確,也可能是控制台(用的是傳統的int main()型函數而不是WinMain)。為此還需要修改項目配置。

安裝HLSL擴展

點擊菜單欄擴展-管理擴展,搜索HLSL Tools for Visual Studio並安裝,這樣后續查看、編寫HLSL文件就擁有了更加豐富的代碼高亮和提示。

項目結構

現在把目光拉回到我們的教程項目。目前項目中包含頭文件的具體功能如下:

頭文件 功能
d3dApp.h Direct3D應用程序框架類
d3dUtil.h 包含一些常用頭文件及自己編寫的函數
DXTrace.h 包含了HR宏與DXTraceW函數
GameApp.h 游戲應用程序擴展類,游戲邏輯在這里實現,繼承自D3DApp類
GameTimer.h 游戲計時器類

其中d3dApp類和GameTimer類是龍書源碼提供的,我們可以搬運過來,但是對d3dApp框架類我們還需要進行大幅度修改,畢竟我們的最終目的就是要完全脫離舊的DirectX SDK,使用Windows SDK來實現DX11。修改完成后,d3dApp就幾乎已經定型而不需要我們操心了。

GameApp類則是我們編寫游戲邏輯的地方,這里需要進行逐幀的更新及繪制。

D3DApp框架類

D3DApp.h展示了框架類的聲明,這里的接口類指針全部換上了ComPtr智能指針:

class D3DApp
{
public:
    D3DApp(HINSTANCE hInstance);              // 在構造函數的初始化列表應當設置好初始參數
    virtual ~D3DApp();

    HINSTANCE AppInst()const;                 // 獲取應用實例的句柄
    HWND      MainWnd()const;                 // 獲取主窗口句柄
    float     AspectRatio()const;             // 獲取屏幕寬高比

    int Run();                                // 運行程序,進行游戲主循環

                                              // 框架方法。客戶派生類需要重載這些方法以實現特定的應用需求
    virtual bool Init();                      // 該父類方法需要初始化窗口和Direct3D部分
    virtual void OnResize();                  // 該父類方法需要在窗口大小變動的時候調用
    virtual void UpdateScene(float dt) = 0;   // 子類需要實現該方法,完成每一幀的更新
    virtual void DrawScene() = 0;             // 子類需要實現該方法,完成每一幀的繪制
    virtual LRESULT MsgProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam);
    // 窗口的消息回調函數
protected:
    bool InitMainWindow();       // 窗口初始化
    bool InitDirect3D();         // Direct3D初始化

    void CalculateFrameStats();  // 計算每秒幀數並在窗口顯示

protected:

    HINSTANCE m_hAppInst;        // 應用實例句柄
    HWND      m_hMainWnd;        // 主窗口句柄
    bool      m_AppPaused;       // 應用是否暫停
    bool      m_Minimized;       // 應用是否最小化
    bool      m_Maximized;       // 應用是否最大化
    bool      m_Resizing;        // 窗口大小是否變化
    bool      m_Enable4xMsaa;    // 是否開啟4倍多重采樣
    UINT      m_4xMsaaQuality;   // MSAA支持的質量等級


    GameTimer m_Timer;           // 計時器

    // 使用模板別名(C++11)簡化類型名
    template <class T>
    using ComPtr = Microsoft::WRL::ComPtr<T>;
    // Direct3D 11
    ComPtr<ID3D11Device> m_pd3dDevice;                    // D3D11設備
    ComPtr<ID3D11DeviceContext> m_pd3dImmediateContext;   // D3D11設備上下文
    ComPtr<IDXGISwapChain> m_pSwapChain;                  // D3D11交換鏈
    // Direct3D 11.1
    ComPtr<ID3D11Device1> m_pd3dDevice1;                  // D3D11.1設備
    ComPtr<ID3D11DeviceContext1> m_pd3dImmediateContext1; // D3D11.1設備上下文
    ComPtr<IDXGISwapChain1> m_pSwapChain1;                // D3D11.1交換鏈
    // 常用資源
    ComPtr<ID3D11Texture2D> m_pDepthStencilBuffer;        // 深度模板緩沖區
    ComPtr<ID3D11RenderTargetView> m_pRenderTargetView;   // 渲染目標視圖
    ComPtr<ID3D11DepthStencilView> m_pDepthStencilView;   // 深度模板視圖
    D3D11_VIEWPORT m_ScreenViewport;                      // 視口

    // 派生類應該在構造函數設置好這些自定義的初始參數
    std::wstring m_MainWndCaption;                        // 主窗口標題
    int m_ClientWidth;                                    // 視口寬度
    int m_ClientHeight;                                   // 視口高度
};

而在d3dApp.cpp中,可以看到有一個全局變量g_pd3dApp

namespace
{
    // This is just used to forward Windows messages from a global window
    // procedure to our member function window procedure because we cannot
    // assign a member function to WNDCLASS::lpfnWndProc.
    D3DApp* g_pd3dApp = 0;
}

設置該全局變量是因為在窗口創建的時候需要綁定一個回調函數,受到回調函數指針類型的限制,我們不可以綁定d3dApp::MainWndProc的成員方法,所以還需要實現一個全局函數用於回調函數的綁定:

LRESULT CALLBACK
MainWndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam)
{
    // Forward hwnd on because we can get messages (e.g., WM_CREATE)
    // before CreateWindow returns, and thus before m_hMainWnd is valid.
    return g_pd3dApp->MsgProc(hwnd, msg, wParam, lParam);
}

D3DApp::InitWindowD3DApp::MsgProc涉及到Win32 初始化的方法目前在這里不做過多描述,因為這不是教程的重點部分,但后續可能還要回頭修改這兩個方法。有興趣的可以去MSDN查閱這些函數和結構體的信息。

Direct3D初始化

注意當前項目使用的是d3d11_1.h頭文件

Direct3D初始化階段首先需要創建D3D設備D3D設備上下文

D3D設備(ID3D11Device通常代表一個顯示適配器(即顯卡),它最主要的功能是用於創建各種所需資源,最常用的資源有:資源類(ID3D11Resource, 包含紋理和緩沖區)視圖類以及着色器。此外,D3D設備還能夠用於檢測系統環境對功能的支持情況。

D3D設備上下文(ID3D11DeviceContext)可以看做是一個渲染管線。通常我們在創建D3D設備的同時也會附贈一個立即設備上下文(Immediate Context)。一個D3D設備僅對應一個D3D立即設備上下文,並且只要我們擁有其中一方,就能通過各自的方法獲取另一方(即ID3D11Device::GetImmediateContextID3D11DeviceContext::GetDevice)。渲染管線主要負責渲染和計算工作,它需要綁定來自與它關聯的D3D設備所創建的各種資源、視圖和着色器才能正常運轉,除此之外,它還能夠負責對資源的直接讀寫操作。

如果你的系統支持Direct3D 11.1的話,則對應的接口類為:ID3D11Device1ID3D11DeviceContext1,它們分別繼承自上面的兩個接口類,區別在於額外提供了少數新的接口,並且接口方法的實現可能會有所區別。

現在,我們從D3DApp::InitDirect3D方法開始,一步步進行分析。

D3D設備與D3D設備上下文的創建

D3D11CreateDevice函數--創建D3D設備與D3D設備上下文

創建D3D設備、D3D設備上下文使用如下函數:

HRESULT WINAPI D3D11CreateDevice(
    IDXGIAdapter* pAdapter,             // [In_Opt]適配器
    D3D_DRIVER_TYPE DriverType,         // [In]驅動類型
    HMODULE Software,                   // [In_Opt]若上面為D3D_DRIVER_TYPE_SOFTWARE則這里需要提供程序模塊
    UINT Flags,                         // [In]使用D3D11_CREATE_DEVICE_FLAG枚舉類型
    D3D_FEATURE_LEVEL* pFeatureLevels,  // [In_Opt]若為nullptr則為默認特性等級,否則需要提供特性等級數組
    UINT FeatureLevels,                 // [In]特性等級數組的元素數目
    UINT SDKVersion,                    // [In]SDK版本,默認D3D11_SDK_VERSION
    ID3D11Device** ppDevice,            // [Out_Opt]輸出D3D設備
    D3D_FEATURE_LEVEL* pFeatureLevel,   // [Out_Opt]輸出當前應用D3D特性等級
    ID3D11DeviceContext** ppImmediateContext ); //[Out_Opt]輸出D3D設備上下文
  1. 關於pAdapter(顯示適配器),我們可以將它看做是對顯示卡設備的一層封裝,通過該參數,我們可以指定需要使用哪個顯示卡設備。通常該參數我們設為nullptr,這樣就可以交由上層驅動來幫我們決定使用哪個顯卡,或者在NVIDIA控制面板來設置當前程序要使用哪個顯卡。如果想要在應用層決定,使用IDXGIFactory::EnumAdapters方法可以枚舉當前可用的顯示卡設備。在最底下的練習題你將學會如何指定顯示卡設備來創建Direct3D 11.x設備
  2. DriverType則指定了驅動類型,不過通常大多數情況都會支持D3D_DRIVER_TYPE_HARDWARE,以享受硬件加速帶來的效益。現在我們建立一個驅動數組,然后自己通過for循環的方式進行輪詢:
// 驅動類型數組
D3D_DRIVER_TYPE driverTypes[] =
{
    D3D_DRIVER_TYPE_HARDWARE,       // 硬件驅動
    D3D_DRIVER_TYPE_WARP,           // WARP驅動
    D3D_DRIVER_TYPE_REFERENCE,      // 軟件驅動
};
UINT numDriverTypes = ARRAYSIZE(driverTypes);

如果D3D_DRIVER_TYPE_HARDWARE不支持,則需要自己通過循環的形式再檢查D3D_DRIVER_TYPE_WARP是否支持。通常虛擬機環境下是不支持硬件驅動的。

關於D3D_DRIVER_TYPE的詳細描述,可以去查閱MSDN官方文檔詳細了解一下。

  1. Flags對應的是D3D11_CREATE_DEVICE_FLAG枚舉值,如果需要D3D設備調試的話(在Debug模式下),可以指定D3D11_CREATE_DEVICE_DEBUG枚舉值。指定該值后,可以在出現程序異常的時候觀察調試輸出窗口的信息。
  2. pFeatureLevels是一個特性等級數組,通過函數內部進行輪詢以檢測所支持的特性等級:
// 特性等級數組
D3D_FEATURE_LEVEL featureLevels[] =
{
    D3D_FEATURE_LEVEL_11_1,
    D3D_FEATURE_LEVEL_11_0,
};
UINT numFeatureLevels = ARRAYSIZE(featureLevels);

注意:如果你的系統支持Direct3D 11.1的API,卻把pFeatureLevels設置為nullptrD3D11CreateDevice將創建出特性等級為D3D_FEATURE_LEVEL_11_0的設備。而如果你的系統不支持Direct3D 11.1的API,D3D11CreateDevice會立即停止特性數組的輪詢並返回E_INVALIDARG。為此,你必須要從D3D_FEATURE_LEVEL_11_0或更低特性等級開始輪詢。

在Win10, Win8.x 或 Win7 SP1且安裝了KB2670838補丁的系統都支持Direct3D 11.1的API,而純Win7系統僅支持Direct3D 11的API

從上面的描述我們可以得知,特性等級D3D設備的版本並不是互相對應的:

1. 特性等級的支持情況取決於當前使用的顯示適配器,只要顯示適配器支持某一特性等級,意味着它能夠支持該特性等級下的統一功能(如特性等級11.0支持紋理寬高最大為16384,而10.1僅支持紋理寬高最大為8192)

2. D3D設備的版本取決於所處的系統(有時候可以打特定的系統補丁來支持高版本的DX,比如讓Win7支持DX12的部分)

點此可以查看Direct3D特性等級

由於該函數可以創建Direct3D 11.0或更高子版本的D3D設備與設備上下文,但都統一輸出ID3D11DeviceID3D11DeviceContext。如果想要查看是否支持Direct3D 11.1的API,可以使用下面的方式:

ComPtr<ID3D11Device1> m_pd3dDevice1;
HRESULT hr = m_pd3dDevice.As(&m_pd3dDevice1);

同理,想要查看是否支持Direct3D 11.2的API,則可以這樣:

ComPtr<ID3D11Device2> m_pd3dDevice2;
HRESULT hr = m_pd3dDevice.As(&m_pd3dDevice2);

由於每個電腦的顯示卡設備情況有所差異,該教程采用的是默認顯示卡(如果你是在筆記本上運行,有可能會用到集成顯卡),而不是指定顯示卡:

HRESULT hr = S_OK;

// 創建D3D設備 和 D3D設備上下文
UINT createDeviceFlags = 0;
#if defined(DEBUG) || defined(_DEBUG)  
createDeviceFlags |= D3D11_CREATE_DEVICE_DEBUG;
#endif
// 驅動類型數組
D3D_DRIVER_TYPE driverTypes[] =
{
    D3D_DRIVER_TYPE_HARDWARE,
    D3D_DRIVER_TYPE_WARP,
    D3D_DRIVER_TYPE_REFERENCE,
};
UINT numDriverTypes = ARRAYSIZE(driverTypes);

// 特性等級數組
D3D_FEATURE_LEVEL featureLevels[] =
{
    D3D_FEATURE_LEVEL_11_1,
    D3D_FEATURE_LEVEL_11_0,
};
UINT numFeatureLevels = ARRAYSIZE(featureLevels);

D3D_FEATURE_LEVEL featureLevel;
D3D_DRIVER_TYPE d3dDriverType;
for (UINT driverTypeIndex = 0; driverTypeIndex < numDriverTypes; driverTypeIndex++)
{
    d3dDriverType = driverTypes[driverTypeIndex];
    hr = D3D11CreateDevice(nullptr, d3dDriverType, nullptr, createDeviceFlags, featureLevels, numFeatureLevels,
        D3D11_SDK_VERSION, m_pd3dDevice.GetAddressOf(), &featureLevel, m_pd3dImmediateContext.GetAddressOf());

    if (hr == E_INVALIDARG)
    {
        // Direct3D 11.0 的API不承認D3D_FEATURE_LEVEL_11_1,所以我們需要嘗試特性等級11.0以及以下的版本
        hr = D3D11CreateDevice(nullptr, d3dDriverType, nullptr, createDeviceFlags, &featureLevels[1], numFeatureLevels - 1,
            D3D11_SDK_VERSION, m_pd3dDevice.GetAddressOf(), &featureLevel, m_pd3dImmediateContext.GetAddressOf());
    }

    if (SUCCEEDED(hr))
        break;
}

if (FAILED(hr))
{
    MessageBox(0, L"D3D11CreateDevice Failed.", 0, 0);
    return false;
}

// 檢測是否支持特性等級11.0或11.1
if (featureLevel != D3D_FEATURE_LEVEL_11_0 && featureLevel != D3D_FEATURE_LEVEL_11_1)
{
    MessageBox(0, L"Direct3D Feature Level 11 unsupported.", 0, 0);
    return false;
}

// 檢測 MSAA支持的質量等級
m_pd3dDevice->CheckMultisampleQualityLevels(
    DXGI_FORMAT_R8G8B8A8_UNORM, 4, &m_4xMsaaQuality);
assert(m_4xMsaaQuality > 0);

注意:

  1. 支持特性等級11_0的顯示適配器必然支持所有渲染目標紋理格式的4倍多重采樣
  2. 即便m_4xMsaaQuality的返回值為1,也不一定就是沒法啟動4倍多重采樣,該成員只是代表模式的種類數目

DXGI初始化

多重采樣

由於屏幕中顯示的像素不可能是無窮小的,所以並不是任意一條直線都能在顯示器上“平滑”而完美地呈現出來。如下圖所示的上一條直線,因為它的每個像素的寬高較大,顯示出來的直線會有明顯的鋸齒感。因此我們說這條直線是走樣的。

雖然說提高像素的分辨率能夠縮小像素的大小,但還有一種辦法,就是采用各種反走樣(antialiasing)技術。

例如,超采樣技術使用4倍(寬度x2,高度x2)於屏幕分辨率大小的后備緩沖區和深度緩沖區。當數據局要從后備緩沖區調往屏幕顯示的時候,會將后備緩沖區按4個像素一組進行解析resolve,或稱降采樣downsample)。通過求平均值的方法,把放大的采樣點數降低回原采樣點數來得到一種相對平滑的像素顏色。超采樣不僅需要原來4倍的內存空間,還需要4倍的繪制開銷和額外的解析開銷,用空間和時間來換取畫面的分辨率。

而Direct3D還支持一種在性能和效果等方面都較為折中的反走樣技術,即多重采樣(multisample,或MSAA,全稱MultiSample Anti-Aliasing)技術。現在假設我們使用4X多重采樣(原來的1個像素對應現在的2x2像素),則他同樣使用4倍於游戲分辨率的后備緩沖區和深度緩沖區(注意我們在創建后備緩沖區的時候是指定分辨率而不是指定緩沖區寬高)。跟上面那種方法不同,對每個2x2像素,我們是對這四個像素的中心位置僅采樣一次,然后再對這4個像素基於可視性(深度/模板測試)和覆蓋性(這些子像素的中心是否在多邊形的里面)來決定是否把采樣的顏色賦值給它。最后的解析行為則和超采樣的一致。這種做法節省了繪制開銷,雖然精准度沒上面的高,但也能取得比較不錯的效果。

頁面翻轉

為了避免動畫中出現畫面閃爍的現象,最好將動畫幀完整地繪制在一種稱為后備緩沖區(back buffer)的離屏紋理內。這樣觀者就不會察覺出幀的繪制過程而只看到完整的動畫幀。而前台緩沖區(front buffer)則為當前顯示在屏幕上的動畫幀,它的構造和前台緩沖區是一樣的。通過硬件可以做到,在后備緩沖區的動畫幀繪制完成后,通過指針交換的形式,將原來的后備緩沖區變為前台緩沖區,而前台緩沖區則變成下一幀待繪制的后備緩沖區。這種做法避免了內容上的拷貝,有效提升效率。這種翻轉操作是在桌面窗口管理器(DWM)內實現的。

DXGI交換鏈

DXGI交換鏈(IDXGISwapChain)緩存了一個或多個表面(2D紋理),它們都可以稱作后備緩沖區(backbuffer)。后備緩沖區則是我們主要進行渲染的場所,我們可以將這些緩沖區通過合適的手段成為渲染管線的輸出對象。在進行呈現(Present)的時候有兩種方法:

  1. BitBlt Model(位塊傳輸模型):將后備緩沖區的數據進行BitBlt(位塊傳輸,即內容上的拷貝),傳入到DWM與DX共享的后備緩沖區,然后進行翻轉以顯示其內容。使用這種模型至少需要一個后備緩沖區。事實上,這也是Win32應用程序最常使用的方式,在進行呈現后,渲染管線仍然是對同一個后備緩沖區進行輸出。(支持Windows 7及更高版本)
  2. Flip Model(翻轉模型):該模型可以避免上一種方式多余的復制,后備緩沖區表面可以直接與DWM內的前台緩沖區進行翻轉。但是需要創建至少兩個后備緩沖區,並且在每次完成呈現后通過代碼切換到另一個后備緩沖區進行渲染。該模型可以用於Win32應用程序以及UWP應用程序(需要DXGI1.2,支持Windows 8及更高版本)

注意:考慮到要兼容Win7系統,而且由於我們編寫的是Win32應用程序,因此這里使用的是第一種模型。同時這也是絕大多數教程所使用的。對第二種感興趣的可以了解下面的鏈接:

DXGI翻轉模型

接下來我們需要了解D3D與DXGI各版本的對應關系,這十分重要:

Direct3D API支持版本 對應包含DXGI版本 對應DXGI接口 可枚舉的顯示適配器 可創建的交換鏈
Direct3D 11.1 DXGI 1.2 IDXGIFactory2 IDXGIAdaptor2 IDXGISwapChain1
Direct3D 11.0/10.1 DXGI 1.1 IDXGIFactory1 IDXGIAdaptor1 IDXGISwapChain
Direct3D 10.0 DXGI 1.0 IDXGIFactory IDXGIAdaptor IDXGISwapChain

d3d與dxgi版本的對應關系你可以通過觀察這些d3d頭文件所包含的dxgi頭文件來了解。

DXGI交換鏈的創建需要通過IDXGIFactory::CreateSwapChain方法進行。但是,如果是要創建Direct3D 11.1對應的交換鏈,則需要通過IDXGIFactory2::CreateSwapChainForHwnd方法進行。

獲取IDXGIFactory1或IDXGIFactory2接口類

現在我們需要先拿到包含IDXGIFactory1接口的對象,但是為了拿到該對象還需要經歷一些磨難。

之前在創建D3D設備時使用的是默認的顯卡適配器IDXGIAdapter(對於雙顯卡的筆記本大概率使用的是集成顯卡),而創建出來的D3D設備本身實現了IDXGIDevice接口,通過該對象,我們可以獲取到當前所用的顯卡適配器IDXGIAdapter對象,這樣我們再通過查詢它的父級找到是哪個IDXGIFactory枚舉出來的適配器。

ComPtr<IDXGIDevice> dxgiDevice = nullptr;
ComPtr<IDXGIAdapter> dxgiAdapter = nullptr;
ComPtr<IDXGIFactory1> dxgiFactory1 = nullptr;    // D3D11.0(包含DXGI1.1)的接口類
ComPtr<IDXGIFactory2> dxgiFactory2 = nullptr;    // D3D11.1(包含DXGI1.2)特有的接口類

// 為了正確創建 DXGI交換鏈,首先我們需要獲取創建 D3D設備 的 DXGI工廠,否則會引發報錯:
// "IDXGIFactory::CreateSwapChain: This function is being called with a device from a different IDXGIFactory."
HR(m_pd3dDevice.As(&dxgiDevice));
HR(dxgiDevice->GetAdapter(dxgiAdapter.GetAddressOf()));
HR(dxgiAdapter->GetParent(__uuidof(IDXGIFactory1), reinterpret_cast<void**>(dxgiFactory1.GetAddressOf())));

// 查看該對象是否包含IDXGIFactory2接口
hr = dxgiFactory1.As(&dxgiFactory2);
// 如果包含,則說明支持D3D11.1
if (dxgiFactory2 != nullptr)
{
    HR(m_pd3dDevice.As(&m_pd3dDevice1));
    HR(m_pd3dImmediateContext.As(&m_pd3dImmediateContext1));
    // ... 省略交換鏈IDXGISwapChain1的創建
}
else
{
    // ... 省略交換鏈IDXGISwapChain的創建
}

同時之前也提到,如果系統支持Direct3D 11.1的話,我們就可以拿到DXGI 1.2的相關對象(如IDXGIFactory2)。

這里要仔細品味ComPtr::As方法的作用。該方法相當於調用IUnknown::QueryInterface,用於查詢這個接口類指針指向的對象是否實現了某個特定接口(即有沒有繼承,但這個繼承不一定是直接繼承關系,還可以是間接繼承),若有則返回這個特定接口的指針。比如說IDXGIFactory2繼承自IDXGIFactory1,此時我們有了一個IDXGIFactory1*,但我們不知道它能不能轉換成IDXGIFactory2*,因此可以用ComPtr::As方法來嘗試獲取,失敗了也只不過是返回一個nullptr指針。但如果此時你用有一個IDXGIFactory2*,你調用ComPtr::As來獲取IDXGIFactory1*則是必然成功的操作。

剛才也說到ComPtr::As也可以查詢間接繼承的接口,比如微軟告訴你ID3D11Device可以通過IUnknown::QueryInterface方法獲取IDXGIDevice,雖然你從繼承關系上看不出來有什么聯系,但至少在它的實例類中的某一層關系上肯定是有一個內部類繼承了接口類IDXGIDevice的。

回歸正題,這時m_pd3dDevicem_pd3dDevice1其實都指向同一個對象,m_pd3dImmediateContextm_pd3dImmediateContext1m_pSwapChainm_pSwapChain1也是一樣的,區別在於后者實現了額外的一些接口,以及某些特定操作(后面第八章會講到)。現階段本教程只會用到和Direct3D 11.0 的API,因此不管是Direct3D 11.1還是Direct3D 11.0,后續都主要使用m_pd3dDevicem_pd3dImmediateContextm_pSwapChain來進行操作。

IDXGIFactory2::CreateSwapChainForHwnd方法--Direct3D 11.1創建交換鏈

如果系統支持Direct3D 11.1的話,需要填充DXGI_SWAP_CHAIN_DESC1DXGI_SWAP_CHAIN_FULLSCREEN_DESC這兩個結構體:

typedef struct DXGI_SWAP_CHAIN_DESC1
{
    UINT Width;                        // 分辨率寬度
    UINT Height;                    // 分辨率高度
    DXGI_FORMAT Format;                // 緩沖區數據格式
    BOOL Stereo;                    // 忽略    
    DXGI_SAMPLE_DESC SampleDesc;    // 采樣描述
    DXGI_USAGE BufferUsage;            // 緩沖區用途
    UINT BufferCount;                // 緩沖區數目
    DXGI_SCALING Scaling;            // 忽略
    DXGI_SWAP_EFFECT SwapEffect;    // 交換效果
    DXGI_ALPHA_MODE AlphaMode;        // 忽略
    UINT Flags;                        // 使用DXGI_SWAP_CHAIN_FLAG枚舉類型
} DXGI_SWAP_CHAIN_DESC1;

typedef struct DXGI_SAMPLE_DESC
{
    UINT Count;                     // MSAA采樣數
    UINT Quality;                   // MSAA質量等級
} DXGI_SAMPLE_DESC;

typedef struct DXGI_SWAP_CHAIN_FULLSCREEN_DESC
{
    DXGI_RATIONAL RefreshRate;                    // 刷新率
    DXGI_MODE_SCANLINE_ORDER ScanlineOrdering;    // 忽略
    DXGI_MODE_SCALING Scaling;                    // 忽略
    BOOL Windowed;                                // 是否窗口化
} DXGI_SWAP_CHAIN_FULLSCREEN_DESC;

typedef struct DXGI_RATIONAL
{
    UINT Numerator;                 // 刷新率分子
    UINT Denominator;               // 刷新率分母
} DXGI_RATIONAL;

填充好后,這里使用的創建方法為IDXGIFactory2::CreateSwapChainForHwnd

HRESULT IDXGIFactory2::CreateSwapChainForHwnd(
    IUnknown *pDevice,                        // [In]D3D設備
    HWND hWnd,                                // [In]窗口句柄
    const DXGI_SWAP_CHAIN_DESC1 *pDesc,        // [In]交換鏈描述1
    const DXGI_SWAP_CHAIN_FULLSCREEN_DESC *pFullscreenDesc,    // [In]交換鏈全屏描述,可選
    IDXGIOutput *pRestrictToOutput,            // [In]忽略
    IDXGISwapChain1 **ppSwapChain);            // [Out]輸出交換鏈對象

上面第一個省略的部分代碼如下:

// 填充各種結構體用以描述交換鏈
DXGI_SWAP_CHAIN_DESC1 sd;
ZeroMemory(&sd, sizeof(sd));
sd.Width = m_ClientWidth;
sd.Height = m_ClientHeight;
sd.Format = DXGI_FORMAT_R8G8B8A8_UNORM;
// 是否開啟4倍多重采樣?
if (m_Enable4xMsaa)
{
    sd.SampleDesc.Count = 4;
    sd.SampleDesc.Quality = m_4xMsaaQuality - 1;
}
else
{
    sd.SampleDesc.Count = 1;
    sd.SampleDesc.Quality = 0;
}
sd.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT;
sd.BufferCount = 1;
sd.SwapEffect = DXGI_SWAP_EFFECT_DISCARD;
sd.Flags = 0;

DXGI_SWAP_CHAIN_FULLSCREEN_DESC fd;
fd.RefreshRate.Numerator = 60;
fd.RefreshRate.Denominator = 1;
fd.Scaling = DXGI_MODE_SCALING_UNSPECIFIED;
fd.ScanlineOrdering = DXGI_MODE_SCANLINE_ORDER_UNSPECIFIED;
fd.Windowed = TRUE;
// 為當前窗口創建交換鏈
HR(dxgiFactory2->CreateSwapChainForHwnd(m_pd3dDevice.Get(), m_hMainWnd, &sd, &fd, nullptr, m_pSwapChain1.GetAddressOf()));
HR(m_pSwapChain1.As(&m_pSwapChain));

后續我們還可以通過該交換鏈來手動指定是否需要全屏

IDXGIFactory::CreateSwapChain方法--Direct3D 11創建交換鏈

如果是Direct3D 11.0的話,需要先填充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_FLAG枚舉類型
}     DXGI_SWAP_CHAIN_DESC;

typedef struct DXGI_SAMPLE_DESC
{
    UINT Count;                     // MSAA采樣數
    UINT Quality;                   // MSAA質量等級
} DXGI_SAMPLE_DESC;

typedef struct DXGI_MODE_DESC
{
    UINT Width;                     // 分辨率寬度
    UINT Height;                    // 分辨率高度
    DXGI_RATIONAL RefreshRate;      // 刷新率分數表示法
    DXGI_FORMAT Format;             // 緩沖區數據格式
    DXGI_MODE_SCANLINE_ORDER ScanlineOrdering;  // 忽略
    DXGI_MODE_SCALING Scaling;      // 忽略
} DXGI_MODE_DESC;

typedef struct DXGI_RATIONAL
{
    UINT Numerator;                 // 刷新率分子
    UINT Denominator;               // 刷新率分母
} DXGI_RATIONAL;

Direct3D 11.0下使用的創建方法為IDXGIFactory::CreateSwapChain

HRESULT IDXGIFactory::CreateSwapChain(
    IUnknown *pDevice,                    // [In]D3D設備
    DXGI_SWAP_CHAIN_DESC *pDesc,        // [In]交換鏈描述
    IDXGISwapChain **ppSwapChain);      // [Out]輸出交換鏈對象

第二個省略的部分代碼如下:

// 填充DXGI_SWAP_CHAIN_DESC用以描述交換鏈
DXGI_SWAP_CHAIN_DESC sd;
ZeroMemory(&sd, sizeof(sd));
sd.BufferDesc.Width = m_ClientWidth;
sd.BufferDesc.Height = m_ClientHeight;
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;
// 是否開啟4倍多重采樣?
if (m_Enable4xMsaa)
{
    sd.SampleDesc.Count = 4;
    sd.SampleDesc.Quality = m_4xMsaaQuality - 1;
}
else
{
    sd.SampleDesc.Count = 1;
    sd.SampleDesc.Quality = 0;
}
sd.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT;
sd.BufferCount = 1;
sd.OutputWindow = m_hMainWnd;
sd.Windowed = TRUE;
sd.SwapEffect = DXGI_SWAP_EFFECT_DISCARD;
sd.Flags = 0;
HR(dxgiFactory1->CreateSwapChain(m_pd3dDevice.Get(), &sd, m_pSwapChain.GetAddressOf()));

禁用ALT+ENTER與全屏的關聯

默認情況下按ALT+ENTER可以切換成全屏,如果不想要這種操作,可以使用剛才創建的dxgiFactory1,按照下面的方式來調用即可:

dxgiFactory1->MakeWindowAssociation(mhMainWnd, DXGI_MWA_NO_ALT_ENTER | DXGI_MWA_NO_WINDOW_CHANGES);

這樣DXGI就不會監聽Windows消息隊列,並且屏蔽掉了對接收到ALT+ENTER消息的處理。

DXGI交換鏈與Direct3D設備的交互

在創建好上述對象后,如果窗口的大小是固定的,則需要經歷下面的步驟:

  1. 獲取交換鏈后備緩沖區的ID3D11Texture2D接口對象
  2. 為后備緩沖區創建渲染目標視圖ID3D11RenderTargetView
  3. 通過D3D設備創建一個ID3D11Texture2D用作深度/模板緩沖區,要求與后備緩沖區等寬高
  4. 創建深度/模板視圖ID3D11DepthStrenilView,綁定剛才創建的2D紋理
  5. 通過D3D設備上下文,在渲染管線的輸出合並階段設置渲染目標
  6. 在渲染管線的光柵化階段設置好渲染的視口區域

接下來需要快速了解一遍上述步驟所需要用到的API。

獲取交換鏈的后備緩沖區

由於此前我們創建好的交換鏈已經包含1個后備緩沖區了,我們可以通過IDXGISwapChain::GetBuffer方法直接獲取后備緩沖區的ID3D11Texture2D接口:

HRESULT IDXGISwapChain::GetBuffer( 
    UINT Buffer,        // [In]緩沖區索引號,從0到BufferCount - 1
    REFIID riid,        // [In]緩沖區的接口類型ID
    void **ppSurface);  // [Out]獲取到的緩沖區

為后備緩沖區創建渲染目標視圖

渲染目標視圖用於將渲染管線的運行結果輸出給其綁定的資源,很明顯它也只能夠設置給輸出合並階段。渲染目標視圖要求其綁定的資源是允許GPU讀寫的,因為在作為管線輸出時會通過GPU寫入數據,並且在以后進行混合操作時還需要在GPU讀取該資源。通常渲染目標是一個二維的紋理,但它依舊可能會綁定其余類型的資源。這里不做討論。

現在我們需要將后備緩沖區綁定到渲染目標視圖,使用ID3D11Device::CreateRenderTargetView方法來創建:

HRESULT ID3D11Device::CreateRenderTargetView( 
    ID3D11Resource *pResource,                      // [In]待綁定到渲染目標視圖的資源
    const D3D11_RENDER_TARGET_VIEW_DESC *pDesc,     // [In]忽略
    ID3D11RenderTargetView **ppRTView);             // [Out]獲取渲染目標視圖

現在這里演示了獲取后備緩沖區紋理,並綁定到渲染目標視圖的過程:

// 重設交換鏈並且重新創建渲染目標視圖
ComPtr<ID3D11Texture2D> backBuffer;
HR(m_pSwapChain->GetBuffer(0, __uuidof(ID3D11Texture2D), reinterpret_cast<void**>(backBuffer.GetAddressOf())));
HR(m_pd3dDevice->CreateRenderTargetView(backBuffer.Get(), nullptr, m_pRenderTargetView.GetAddressOf()));

創建深度/模板緩沖區

在這里我們先簡單提一下最常用到的深度緩沖區。而之所以叫深度/模板緩沖區,是因為這個緩沖區可以同時存有深度和模板值,更細節的東西就留到后面的章節將。

深度緩沖區(depth buffer)這種紋理資源存儲的而非圖像數據,而是特定像素的深度信息。深度值的范圍通常為0.0到1.0,0.0意味着觀察者在視錐體(view frustum)能看到的最近位置,1.0則意味着觀察者在視錐體能看到的最遠位置。深度緩沖區中的元素與后台緩沖區中的像素是一一對應的。因此深度緩沖區的寬高應和后備緩沖區的分辨率保持一致。

Direct3D中同樣采用了一種叫做深度緩沖z緩沖(通常z坐標反映深度)的技術。只要使用了深度緩沖,我們就無須關心物體的繪制順序,因為默認情況下我們只會保留深度值最小的像素。它能夠有效處理像鐵鏈這種環環相扣的物體,反之對物體按深度排序的畫家算法就無法處理了。

如下圖所示,無論繪制物體的順序如何,新像素的深度值和深度緩沖區中對應位置的像素的深度值比較,若更小,則替換深度值和后備緩沖區的像素顏色。因此最終能保留下來的像素必然是圓球P1點處的像素。

ID3D11Device::CreateTexture2D--創建一個2D紋理

除了渲染目標視圖外,我們還需要創建深度/模板緩沖區用於深度測試。深度/模板緩沖區也是一個2D紋理,要求其寬度和高度必須要和窗口寬高保持一致。

通過D3D設備可以新建一個2D紋理,但在此之前我們需要先描述該緩沖區的信息:

typedef struct D3D11_TEXTURE2D_DESC
{
    UINT Width;         // 緩沖區寬度
    UINT Height;        // 緩沖區高度
    UINT MipLevels;     // Mip等級
    UINT ArraySize;     // 紋理數組中的紋理數量,默認1
    DXGI_FORMAT Format; // 緩沖區數據格式
    DXGI_SAMPLE_DESC SampleDesc;    // MSAA采樣描述
    D3D11_USAGE Usage;  // 數據的CPU/GPU訪問權限
    UINT BindFlags;     // 使用D3D11_BIND_FLAG枚舉來決定該數據的使用類型
    UINT CPUAccessFlags;    // 使用D3D11_CPU_ACCESS_FLAG枚舉來決定CPU訪問權限
    UINT MiscFlags;     // 使用D3D11_RESOURCE_MISC_FLAG枚舉,這里默認0
}     D3D11_TEXTURE2D_DESC;   

由於要填充的內容很多,並且目前只有在初始化環節才用到,因此這部分代碼可以先粗略看一下,在后續的章節還會詳細講到。

填充好后,這時我們就可以用方法ID3D11Device::CreateTexture2D來創建2D紋理:

HRESULT ID3D11Device::CreateTexture2D( 
    const D3D11_TEXTURE2D_DESC *pDesc,          // [In] 2D紋理描述信息
    const D3D11_SUBRESOURCE_DATA *pInitialData, // [In] 用於初始化的資源
    ID3D11Texture2D **ppTexture2D);             // [Out] 獲取到的2D紋理

下面的代碼是關於深度/模板緩沖區創建的完整過程:

D3D11_TEXTURE2D_DESC depthStencilDesc;

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

// 要使用 4X MSAA?
if (mEnable4xMsaa)
{
    depthStencilDesc.SampleDesc.Count = 4;
    depthStencilDesc.SampleDesc.Quality = m_4xMsaaQuality - 1;
}
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;

HR(m_pd3dDevice->CreateTexture2D(&depthStencilDesc, nullptr, m_pDepthStencilBuffer.GetAddressOf()));

創建深度/模板視圖

有了深度/模板緩沖區后,就可以通過ID3D11Device::CreateDepthStencilView方法將創建好的2D紋理綁定到新建的深度/模板視圖:

HRESULT ID3D11Device::CreateDepthStencilView( 
    ID3D11Resource *pResource,                      // [In] 需要綁定的資源
    const D3D11_DEPTH_STENCIL_VIEW_DESC *pDesc,     // [In] 深度緩沖區描述,這里忽略
    ID3D11DepthStencilView **ppDepthStencilView);   // [Out] 獲取到的深度/模板視圖

演示如下:

HR(m_pd3dDevice->CreateDepthStencilView(m_pDepthStencilBuffer.Get(), nullptr, m_pDepthStencilView.GetAddressOf()));

為渲染管線的輸出合並階段設置渲染目標

ID3D11DeviceContext::OMSetRenderTargets方法要求同時提供渲染目標視圖和深度/模板視圖,不過這時我們都已經准備好了:

void ID3D11DeviceContext::OMSetRenderTargets( 
    UINT NumViews,                                      // [In] 視圖數目
    ID3D11RenderTargetView *const *ppRenderTargetViews, // [In] 渲染目標視圖數組
    ID3D11DepthStencilView *pDepthStencilView) = 0;     // [In] 深度/模板視圖

因此這里同樣也是一句話的事情:

m_pd3dImmediateContext->OMSetRenderTargets(1, m_pRenderTargetView.GetAddressOf(), m_pDepthStencilView.Get());

視口設置

最終我們還需要決定將整個視圖輸出到窗口特定的范圍。我們需要使用D3D11_VIEWPORT來設置視口

typedef struct D3D11_VIEWPORT
{
    FLOAT TopLeftX;     // 屏幕左上角起始位置X
    FLOAT TopLeftY;     // 屏幕左上角起始位置Y
    FLOAT Width;        // 寬度
    FLOAT Height;       // 高度
    FLOAT MinDepth;     // 最小深度,必須為0.0f
    FLOAT MaxDepth;     // 最大深度,必須為1.0f
}     D3D11_VIEWPORT;

ID3D11DeviceContext::RSSetViewports方法將設置1個或多個視口:

void ID3D11DeviceContext::RSSetViewports(
    UINT  NumViewports,                     // 視口數目
    const D3D11_VIEWPORT *pViewports);      // 視口數組

將視圖輸出到整個屏幕需要按下面的方式進行填充:

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

m_pd3dImmediateContext->RSSetViewports(1, &m_ScreenViewport);

完成了這六個步驟后,基本的初始化就完成了。但是,如果涉及到窗口大小變化的情況,那么前面提到的后備緩沖區、深度/模板緩沖區、視口都需要重新調整大小。

D3DApp::OnResize方法

已知深度模板緩沖區和視口都可以直接重新創建一份來進行替換。至於后備緩沖區,我們可以通過IDXGISwapChain::ResizeBuffers來重新調整后備緩沖區的分辨率:

HRESULT IDXGISwapChain::ResizeBuffers(
  UINT        BufferCount,            // [In]緩沖區數目
  UINT        Width,                // [In]新分辨率寬度
  UINT        Height,                // [In]新分辨率高度
  DXGI_FORMAT NewFormat,            // [In]DXGI格式
  UINT        SwapChainFlags        // [In]忽略
);

下面的方法演示了在窗口大小發生改變后,以及初次調用時進行的操作:

void D3DApp::OnResize()
{
    assert(m_pd3dImmediateContext);
    assert(m_pd3dDevice);
    assert(m_pSwapChain);

    if (m_pd3dDevice1 != nullptr)
    {
        assert(m_pd3dImmediateContext1);
        assert(m_pd3dDevice1);
        assert(m_pSwapChain1);
    }

    // 釋放交換鏈的相關資源
    m_pRenderTargetView.Reset();
    m_pDepthStencilView.Reset();
    m_pDepthStencilBuffer.Reset();

    // 重設交換鏈並且重新創建渲染目標視圖
    ComPtr<ID3D11Texture2D> backBuffer;
    HR(m_pSwapChain->ResizeBuffers(1, m_ClientWidth, m_ClientHeight, DXGI_FORMAT_R8G8B8A8_UNORM, 0));
    HR(m_pSwapChain->GetBuffer(0, __uuidof(ID3D11Texture2D), reinterpret_cast<void**>(backBuffer.GetAddressOf())));
    HR(m_pd3dDevice->CreateRenderTargetView(backBuffer.Get(), nullptr, m_pRenderTargetView.GetAddressOf()));
    
    
    backBuffer.Reset();


    D3D11_TEXTURE2D_DESC depthStencilDesc;

    depthStencilDesc.Width = m_ClientWidth;
    depthStencilDesc.Height = m_ClientHeight;
    depthStencilDesc.MipLevels = 1;
    depthStencilDesc.ArraySize = 1;
    depthStencilDesc.Format = DXGI_FORMAT_D24_UNORM_S8_UINT;

    // 要使用 4X MSAA? --需要給交換鏈設置MASS參數
    if (m_Enable4xMsaa)
    {
        depthStencilDesc.SampleDesc.Count = 4;
        depthStencilDesc.SampleDesc.Quality = m_4xMsaaQuality - 1;
    }
    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;

    // 創建深度緩沖區以及深度模板視圖
    HR(m_pd3dDevice->CreateTexture2D(&depthStencilDesc, nullptr, m_pDepthStencilBuffer.GetAddressOf()));
    HR(m_pd3dDevice->CreateDepthStencilView(m_pDepthStencilBuffer.Get(), nullptr, m_pDepthStencilView.GetAddressOf()));


    // 將渲染目標視圖和深度/模板緩沖區結合到管線
    m_pd3dImmediateContext->OMSetRenderTargets(1, m_pRenderTargetView.GetAddressOf(), m_pDepthStencilView.Get());

    // 設置視口變換
    m_ScreenViewport.TopLeftX = 0;
    m_ScreenViewport.TopLeftY = 0;
    m_ScreenViewport.Width = static_cast<float>(m_ClientWidth);
    m_ScreenViewport.Height = static_cast<float>(m_ClientHeight);
    m_ScreenViewport.MinDepth = 0.0f;
    m_ScreenViewport.MaxDepth = 1.0f;

    m_pd3dImmediateContext->RSSetViewports(1, &m_ScreenViewport);
}

在后續的部分,該框架的代碼基本上不會有什么太大的變動。因此后面代碼的添加主要在GameApp類實現。如果現在對上面的一些過程不理解,也是正常的,可以在后續學習到視圖相關的知識后再來回看這一整個過程。

GameApp類

對於一個初始化應用程序來說,目前GameApp類的非常簡單:

class GameApp : public D3DApp
{
public:
    GameApp(HINSTANCE hInstance);
    ~GameApp();

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

GameApp::DrawScene方法--每幀畫面的繪制

ID3D11DeviceContext::ClearRenderTargetView方法--清空需要繪制的緩沖區

在每一幀畫面繪制的操作中,我們需要清理一遍渲染目標視圖綁定的緩沖區

void ID3D11DeviceContext::ClearRenderTargetView(
    ID3D11RenderTargetView *pRenderTargetView,  // [In]渲染目標視圖
    const FLOAT  ColorRGBA[4]);                 // [In]指定覆蓋顏色

這里的顏色值范圍都是0.0f到1.0f

比如我們要對后備緩沖區(R8G8B8A8)使用藍色進行清空,可以這樣寫:

float blue[4] = {0.0f, 0.0f, 1.0f, 1.0f}    // RGBA = (0,0,255,255)
m_pd3dImmediateContext->ClearRenderTargetView(m_pRenderTargetView.Get(), blue);

ID3D11DeviceContext::ClearDepthStencilView方法--清空深度/模板緩沖區

同樣在進行渲染之前,我們也要清理一遍深度/模板緩沖區

void ID3D11DeviceContext::ClearDepthStencilView(
    ID3D11DepthStencilView *pDepthStencilView,  // [In]深度/模板視圖
    UINT ClearFlags,    // [In]D3D11_CLEAR_FLAG枚舉
    FLOAT Depth,        // [In]深度
    UINT8 Stencil);     // [In]模板初始值

若要清空深度緩沖區,則需要指定D3D11_CLEAR_DEPTH,模板緩沖區則是D3D11_CLEAR_STENCIL

每一次清空我們需要將深度值設為1.0f,模板值設為0.0f。其中深度值1.0f表示距離最遠處:

m_pd3dImmediateContext->ClearDepthStencilView(m_pDepthStencilView.Get(), D3D11_CLEAR_DEPTH | D3D11_CLEAR_STENCIL, 1.0f, 0);

IDXGISwapChain::Present方法--前后備緩沖區交換並呈現

完成一切繪制操作后就可以調用該方法了

HRESULT ID3D11DeviceContext::Present( 
    UINT SyncInterval,  // [In]通常為0
    UINT Flags);        // [In]通常為0

GameApp::DrawScene的實現如下:

void GameApp::DrawScene()
{
    assert(m_pd3dImmediateContext);
    assert(m_pSwapChain);
    static float blue[4] = { 0.0f, 0.0f, 1.0f, 1.0f };    // RGBA = (0,0,255,255)
    m_pd3dImmediateContext->ClearRenderTargetView(m_pRenderTargetView.Get(), blue);
    m_pd3dImmediateContext->ClearDepthStencilView(m_pDepthStencilView.Get(), D3D11_CLEAR_DEPTH | D3D11_CLEAR_STENCIL, 1.0f, 0);

    HR(m_pSwapChain->Present(0, 0));
}

最終繪制的效果應該如下:

程序退出后的清理

因為之前我們用的是智能指針,所以D3DApp的析構函數十分簡單,只需要通過ID3D11DeviceContext::ClearState方法來恢復D3D設備上下文到默認狀態,卸下所有綁定的資源即可。剩下的事情就交給COM智能指針完成:

D3DApp::~D3DApp()
{
    // 恢復所有默認設定
    if (m_pd3dImmediateContext)
        m_pd3dImmediateContext->ClearState();
}

練習題

粗體字為自定義題目,本章練習題可以先不做

  1. 嘗試修改項目代碼,讓窗口內的顯示變紅。
  2. 某些電腦可能有多於一個的顯示適配器(顯卡)。首先要通過CreateDXGIFactory創建IDXGIFactory,然后使用IDXGIFactory::EnumAdapters來枚舉顯示適配器。嘗試通過這種方式查看你的電腦有多少個顯示適配器(IDXGIAdapter),並察看它們的信息。
  3. 一個顯示適配器可能關聯了多個輸出設備(IDXGIOutput),你可以使用IDXGIAdapter::EnumOutputs方法來枚舉出特定的輸出,嘗試觀察它們的信息。
  4. 對於給定的像素格式,一個輸出設備可以支持許多種顯示模式(DXGI_MODE_DESC),通過它可以看到全屏寬度、高度、刷新率。嘗試使用IDXGIOutput::GetDisplayModeList方法觀察所有支持的模式(傳遞DXGI_FORMAT_R8G8B8A8_UNORM格式進去)。
  5. 默認情況下的窗口程序是可以通過ALT+ENTER來進入/退出全屏的。此外,我們可以通過IDXGISwapChain來動態設置窗口全屏屬性,找到對應的方法並嘗試一下。
  6. 現在嘗試指定顯示適配器來創建D3D設備。通過CreateDXGIFactory函數來創建IDXGIFactory,通常它會包含接口IDXGIFactory1,但有可能它也會包含接口IDXGIFactory2。在沒有創建D3D設備的情況下,這種方式就可以幫助我們了解是否可以創建出Direct3D 11.1的設備。為了能夠指定顯示適配器來創建D3D11設備,我們需要將D3D_DRIVER_TYPE強行設置為D3D_DRIVER_TYPE_UNKNOWN,否則在創建設備的時候會得到如下報錯信息:DX ERROR: D3D11CreateDevice: When creating a device from an existing adapter (i.e. pAdapter is non-NULL), DriverType must be D3D_DRIVER_TYPE_UNKNOWN. [ INITIALIZATION ERROR #3146141: ]

DirectX11 With Windows SDK完整目錄

Github項目源碼

歡迎加入QQ群: 727623616 可以一起探討DX11,以及有什么問題也可以在這里匯報。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM