DirectX11 With Windows SDK--01 DirectX11初始化

前言

因爲我的以爲龍書裏面第4章提供的Direct3D 初始化項目封裝得比較好,並且DirectX SDK Samples裏面的初始化程序過於精簡,不適合後續使用,故選擇了以Init Direct3D項目做爲框架,而後還使用了微軟提供的示例項目,二者結合到一塊兒。建議下載項目配合閱讀。html

這一章內容大部分屬於龍書的內容,但仍有一些不一樣的地方。由於後續的全部項目都使用該基礎框架,你也能夠直接使用第一章的項目源碼,而後須要瞭解如下差別部分:git

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

其中前面兩個部分在下面的連接能夠看到:github

章節
ComPtr智能指針
HR宏關於dxerr庫的替代方案

DirectX11 With Windows SDK完整目錄編程

Github項目源碼windows

歡迎加入QQ羣: 727623616 能夠一塊兒探討DX11,以及有什麼問題也能夠在這裏彙報。數組

如何開啓新項目

安裝準備

若是你打算從VS2015開始,則在安裝的時候須要勾選下列選項:緩存

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

而若是是使用VS2017或19的話,則在安裝的時候須要勾選下列選項:框架

注意:VS2015使用的Windows SDK版本爲10.0.14393.0,而VS2017與VS2019使用的Windows SDK版本爲10.0.17763.0編程語言

安裝完成後,新建項目須要從空項目開始。

移除你的項目中有關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中全部的頭文件和源文件添加進你的項目,而後生成項目並運行以檢測。

項目結構

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

頭文件 功能
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方法目前在這裏不作過多描述,由於這不是教程的重點部分,但後續可能還要回頭修改這兩個方法。有興趣的能夠去MSDN查閱這些函數和結構體的信息。

Direct3D初始化

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

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

D3D設備(ID3D11Device)包含了建立各類所需資源的方法,最經常使用的有:資源類(ID3D11Resource, 包含紋理和緩衝區)視圖類以及着色器

D3D設備上下文(ID3D11DeviceContext)能夠看作是一個渲染管線,負責渲染工做,它須要綁定來自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. 特性等級的支持狀況取決於當前使用的顯示適配器

2. D3D設備的版本取決於所處的系統

因爲該函數能夠建立Direct3D 11.1(或者Direct3D 11.0)的設備與設備上下文,但都統一輸出ID3D11DeviceID3D11DeviceContext。若是想要查看是否支持Direct3D 11.1的API,可使用下面的方式:

ComPtr<ID3D11Device1> md3dDevice1;
HRESULT hr = md3dDevice.As(&md3dDevice1);

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

ComPtr<ID3D11Device2> md3dDevice2;
HRESULT hr = md3dDevice.As(&md3dDevice2);

因爲每一個電腦的顯示卡設備狀況有所差別,該教程採用的是默認顯示卡(有可能會用到集成顯卡),而不是指定顯示卡:

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支持的質量等級
md3dDevice->CheckMultisampleQualityLevels(
    DXGI_FORMAT_R8G8B8A8_UNORM, 4, &m_4xMsaaQuality);
assert(m_4xMsaaQuality > 0);

注意:

  1. 支持特性等級11_0的顯示適配器必然支持全部渲染目標紋理格式的4倍多重採樣
  2. 即使m_4xMsaaQuality的返回值爲1,也不表明無法啓動4倍多重採樣,該成員只是表明模式的種類數目

DXGI初始化

DXGI交換鏈

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

  1. BitBlt Model(位塊傳輸模型):將後備緩衝區的數據進行BitBlt(位塊傳輸),傳入到DX應用中的桌面窗口管理器(DWM)表面,而後進行翻轉以交給前臺顯示。使用這種模型至少須要一個後備緩衝區。事實上,這也是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)。

這時m_pd3dDevicem_pd3dDevice1其實都指向同一個對象,m_pd3dImmediateContextm_pd3dImmediateContext1m_pSwapChainm_pSwapChain1也是同樣的,區別僅僅在於後者實現了額外的一些接口,問題不大。所以無論是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;

填充好後,Direct3D 11.1使用的建立方法爲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()));

建立深度/模板緩衝區

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}
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,以及有什麼問題也能夠在這裏彙報。

相關文章
相關標籤/搜索