回到 DirectX11 使用Windows SDK來進行開發: "http://www.cnblogs.com/X Jun/p/9028764.html" 由於個人覺得龍書裡面第4章提供的Direct3D 初始化項目封裝得比較好,而且DirectX SDK Samples裡面的初始化程式過於精簡, ...
回到 DirectX11--使用Windows SDK來進行開發:http://www.cnblogs.com/X-Jun/p/9028764.html
由於個人覺得龍書裡面第4章提供的Direct3D 初始化項目封裝得比較好,而且DirectX SDK Samples裡面的初始化程式過於精簡,不適合後續使用,故選擇了以Init Direct3D項目作為框架,然後還使用了微軟提供的示例項目,兩者結合到一起。建議下載項目配合閱讀。
項目源碼點此:https://github.com/MKXJun/DX11-Without-DirectX-SDK
項目結構
該項目包含了下麵這些文件
其中頭文件的具體功能
頭文件 | 功能 |
---|---|
d3dApp.h | Direct3D應用程式框架類 |
dxerr.h | DirectX錯誤庫 |
GameApp.h | 游戲應用程式擴展類,游戲邏輯在這裡實現,繼承自D3DApp類 |
GameTimer.h | 游戲計時器類 |
其中d3dApp.h
, d3dApp.cpp
, GameTimer.h
, GameTimer.cpp
是龍書源碼提供的,我們可以搬運過來,但是對d3dApp
框架類我們還需要進行大幅度修改,畢竟我們的最終目的就是要完全脫離舊的DirectX SDK,使用Windows SDK來實現DX11.
而dxerr.h
在Windows SDK是沒有提供的,我們需要尋找新的dxerr
進行替代,在後續會提到
GameApp.h
則是我們編寫游戲邏輯的地方,這裡需要進行逐幀的更新及繪製。
初期配置
鏈接靜態庫
這裡的每一個項目都需要包含靜態庫:d3d11.lib
,dxgi.lib
,dxguid.lib
,D3DCompiler.lib
和winmm.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")
也可以在項目屬性-鏈接器-輸入-附加依賴項 添加上面的庫。
移植新的dxerr.h和dxerr.cpp
directx-sdk-samples-master的GitHub地址:https://github.com/walbourn/directx-sdk-samples
在directx-sdk-samples-master\DXUT\Core中可以找到dxerr.h
和dxerr.cpp
,把它們拉進我們的項目中。然後使用下麵的巨集來進行檢查(加在d3dApp.h
)
#if defined(DEBUG) | defined(_DEBUG)
#ifndef HR
#define HR(x) \
{ \
HRESULT hr = (x); \
if(FAILED(hr)) \
{ \
DXTrace(__FILEW__, (DWORD)__LINE__, hr, L#x, true);\
} \
}
#endif
#else
#ifndef HR
#define HR(x) (x)
#endif
#endif
由於新的dxerr.h
僅提供了DXTrace
的Unicode字元集版本,需要將原來的__FILE__
替換為__FILEW__
,併在項目屬性頁中將字元集設置為Unicode。
COM組件智能指針
考慮到DirectX11的API是由一系列的COM組件來管理的,我們可以使用智能指針來管理這些對象,而無需過多擔心記憶體的泄漏。所以該項目並不會用到介面類ID3D11Debug
來協助檢查記憶體泄漏。
使用該智能指針需要包含頭文件wrl/client.h
,並且智能指針類模板ComPtr
位於名稱空間Microsoft::WRL
內。我們主要關註下麵這幾個方法:
ComPtr<T>::Get
方法返回T*
,若需要賦值操作也可以使用重載的=運算符進行
ComPtr<T>::GetAddressOf
方法返回T**
,也可以用重載的&運算符來獲取
ComPtr<T>::Reset
方法將對裡面的對象調用Release方法,並將指針置為nullptr
GameTimer類
GameTimer
類是一個基於高精度時鐘頻率的計時器,主要用於獲取游戲時間和每一幀的間隔時間,併進行一些特殊的操作。下麵給出了GameTimer
類的聲明部分:
class GameTimer
{
public:
GameTimer();
float TotalTime()const; // 總游戲時間
float DeltaTime()const; // 幀間隔時間
void Reset(); // 在消息迴圈之前調用
void Start(); // 在取消暫停的時候調用
void Stop(); // 在暫停的時候調用
void Tick(); // 在每一幀的時候調用
private:
double mSecondsPerCount; // 一個時鐘周期經過的秒數
double mDeltaTime; // 幀間隔時間
__int64 mBaseTime; // 基準時間
__int64 mPausedTime; // 暫停的時間
__int64 mStopTime; // 停止的時間
__int64 mPrevTime; // 上一幀的時間
__int64 mCurrTime; // 當前時間
bool mStopped; // 是否停止計時
};
構造函數
在Windows.h
中,提供了QueryPerformanceFrequency
函數用於獲取當前處理器的時鐘頻率(1秒經過的時鐘周期數),然後我們就可以求出1個時鐘周期經過的時間數目了。此時計時器為開啟狀態。觀看構造函數的代碼:
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;
}
GameTimer::Reset方法
GameTimer::Reset
方法用於重置當前游戲用時為0,並開啟計時器,具體的做法如下:
void GameTimer::Reset()
{
__int64 currTime;
QueryPerformanceCounter((LARGE_INTEGER*)&currTime);
mBaseTime = currTime;
mPrevTime = currTime;
mStopTime = 0;
mStopped = false;
}
其中,QueryPerformanceCounter
函數用於獲取當前經過的時鐘周期數。當前我們是用它獲取的值作為基準時間,然後將暫停的總計時間設置為0,並將暫停狀態設置為否。
GameTimer::Start方法
GameTimer::Start
方法用於開啟計時器計時(設置開始時間),並統計上次暫停的總時間:
void GameTimer::Start()
{
__int64 startTime;
QueryPerformanceCounter((LARGE_INTEGER*)&startTime);
// Accumulate the time elapsed between stop and start pairs.
//
// |<-------d------->|
// ----*---------------*-----------------*------------> time
// mBaseTime mStopTime startTime
if( mStopped )
{
mPausedTime += (startTime - mStopTime);
mPrevTime = startTime;
mStopTime = 0;
mStopped = false;
}
}
若之前曾經暫停過,則需要統計當前暫停經過的時間,並加進總的暫停用時。然後這時停止時間也要歸零,並將暫停狀態設置為否。
GameTimer::Stop方法
GameTimer::Stop
方法用於暫停計時器計時,設置暫停時間點:
void GameTimer::Stop()
{
if( !mStopped )
{
__int64 currTime;
QueryPerformanceCounter((LARGE_INTEGER*)&currTime);
mStopTime = currTime;
mStopped = true;
}
}
GameTimer::Tick方法
GameTimer::Tick
方法在計時器開啟的時候返回距離上次Tick
的間隔時間,若計時器沒有開啟或者間隔時間為負值,則設間隔時間為0:
void GameTimer::Tick()
{
if( mStopped )
{
mDeltaTime = 0.0;
return;
}
__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;
if(mDeltaTime < 0.0)
{
mDeltaTime = 0.0;
}
}
GameTimer::TotalTime方法
GameTimer::TotalTime
方法返回的是距離上次Reset
方法調用到現在,游戲運行的總時間(不包括所有暫停過的時間,單位為秒):
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:
//
// |<--paused time-->|
// ----*---------------*-----------------*------------*------------*------> time
// mBaseTime mStopTime startTime 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);
}
}
GameTimer::DeltaTime方法
GameTimer::TotalTime
方法返回當前Tick
和上次Tick
之間的時間間隔,單位為秒:
float GameTimer::DeltaTime()const
{
return (float)mDeltaTime;
}
D3DApp框架類
D3DApp.h
展示了框架類的聲明:
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 mhAppInst; // 應用實例句柄
HWND mhMainWnd; // 主視窗句柄
bool mAppPaused; // 應用是否暫停
bool mMinimized; // 應用是否最小化
bool mMaximized; // 應用是否最大化
bool mResizing; // 視窗大小是否變化
UINT m4xMsaaQuality; // MSAA支持的質量等級
GameTimer mTimer; // 計時器
// DX11
Microsoft::WRL::ComPtr<ID3D11Device> md3dDevice; // D3D11設備
Microsoft::WRL::ComPtr<ID3D11DeviceContext> md3dImmediateContext; // D3D11設備上下文
Microsoft::WRL::ComPtr<IDXGISwapChain> mSwapChain; // D3D11交換鏈
// DX11.1
Microsoft::WRL::ComPtr<ID3D11Device1> md3dDevice1; // D3D11.1設備
Microsoft::WRL::ComPtr<ID3D11DeviceContext1> md3dImmediateContext1; // D3D11.1設備上下文
Microsoft::WRL::ComPtr<IDXGISwapChain1> mSwapChain1; // D3D11.1交換鏈
// 常用資源
Microsoft::WRL::ComPtr<ID3D11Texture2D> mDepthStencilBuffer; // 深度模板緩衝區
Microsoft::WRL::ComPtr<ID3D11RenderTargetView> mRenderTargetView; // 渲染目標視圖
Microsoft::WRL::ComPtr<ID3D11DepthStencilView> mDepthStencilView; // 深度模板視圖
D3D11_VIEWPORT mScreenViewport; // 視口
// 派生類應該在構造函數設置好這些自定義的初始參數
std::wstring mMainWndCaption; // 主視窗標題
int mClientWidth; // 視口寬度
int mClientHeight; // 視口高度
};
而在d3dApp.cpp
中,可以看到有一個全局變數gd3dApp
:
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* gd3dApp = 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 mhMainWnd is valid.
return gd3dApp->MsgProc(hwnd, msg, wParam, lParam);
}
構造函數
在構造函數中,這些參數通常只會設置一次,所以需要在初始化列表中進行修改:
D3DApp::D3DApp(HINSTANCE hInstance)
: mhAppInst(hInstance),
mMainWndCaption(L"DirectX11 Initialization"),
mClientWidth(800),
mClientHeight(600),
mhMainWnd(nullptr),
mAppPaused(false),
mMinimized(false),
mMaximized(false),
mResizing(false),
m4xMsaaQuality(0),
md3dDevice(nullptr),
md3dImmediateContext(nullptr),
mSwapChain(nullptr),
mDepthStencilBuffer(nullptr),
mRenderTargetView(nullptr),
mDepthStencilView(nullptr)
{
ZeroMemory(&mScreenViewport, sizeof(D3D11_VIEWPORT));
// 讓一個全局指針獲取這個類,這樣我們就可以在Windows消息處理的回調函數
// 讓這個類調用內部的回調函數了
gd3dApp = this;
}
D3DApp::Init方法--初始化
初始化主要完成視窗的創建和Direct3D 11的初始化:
bool D3DApp::Init()
{
if (!InitMainWindow())
return false;
if (!InitDirect3D())
return false;
return true;
}
D3DApp::InitMainWindow方法--完成視窗的創建
該方法的代碼如下:
bool D3DApp::InitMainWindow()
{
WNDCLASS wc;
wc.style = CS_HREDRAW | CS_VREDRAW;
wc.lpfnWndProc = MainWndProc;
wc.cbClsExtra = 0;
wc.cbWndExtra = 0;
wc.hInstance = mhAppInst;
wc.hIcon = LoadIcon(0, IDI_APPLICATION);
wc.hCursor = LoadCursor(0, IDC_ARROW);
wc.hbrBackground = (HBRUSH)GetStockObject(NULL_BRUSH);
wc.lpszMenuName = 0;
wc.lpszClassName = L"D3DWndClassName";
if (!RegisterClass(&wc))
{
MessageBox(0, L"RegisterClass Failed.", 0, 0);
return false;
}
// Compute window rectangle dimensions based on requested client area dimensions.
RECT R = { 0, 0, mClientWidth, mClientHeight };
AdjustWindowRect(&R, WS_OVERLAPPEDWINDOW, false);
int width = R.right - R.left;
int height = R.bottom - R.top;
mhMainWnd = CreateWindow(L"D3DWndClassName", mMainWndCaption.c_str(),
WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, width, height, 0, 0, mhAppInst, 0);
if (!mhMainWnd)
{
MessageBox(0, L"CreateWindow Failed.", 0, 0);
return false;
}
ShowWindow(mhMainWnd, SW_SHOW);
UpdateWindow(mhMainWnd);
return true;
}
視窗的創建這裡不做過多描述,因為這不是教程的重點部分。有興趣的可以去MSDN查閱這些函數和結構體的信息。
D3DApp::MsgProc方法--回調函數
D3DApp::MsgProc
回調方法的定義如下:
LRESULT D3DApp::MsgProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam)
{
switch (msg)
{
// WM_ACTIVATE is sent when the window is activated or deactivated.
// We pause the game when the window is deactivated and unpause it
// when it becomes active.
case WM_ACTIVATE:
if (LOWORD(wParam) == WA_INACTIVE)
{
mAppPaused = true;
mTimer.Stop();
}
else
{
mAppPaused = false;
mTimer.Start();
}
return 0;
// WM_SIZE is sent when the user resizes the window.
case WM_SIZE:
// Save the new client area dimensions.
mClientWidth = LOWORD(lParam);
mClientHeight = HIWORD(lParam);
if (md3dDevice)
{
if (wParam == SIZE_MINIMIZED)
{
mAppPaused = true;
mMinimized = true;
mMaximized = false;
}
else if (wParam == SIZE_MAXIMIZED)
{
mAppPaused = false;
mMinimized = false;
mMaximized = true;
OnResize();
}
else if (wParam == SIZE_RESTORED)
{
// Restoring from minimized state?
if (mMinimized)
{
mAppPaused = false;
mMinimized = false;
OnResize();
}
// Restoring from maximized state?
else if (mMaximized)
{
mAppPaused = false;
mMaximized = false;
OnResize();
}
else if (mResizing)
{
// If user is dragging the resize bars, we do not resize
// the buffers here because as the user continuously
// drags the resize bars, a stream of WM_SIZE messages are
// sent to the window, and it would be pointless (and slow)
// to resize for each WM_SIZE message received from dragging
// the resize bars. So instead, we reset after the user is
// done resizing the window and releases the resize bars, which
// sends a WM_EXITSIZEMOVE message.
}
else // API call such as SetWindowPos or mSwapChain->SetFullscreenState.
{
OnResize();
}
}
}
return 0;
// WM_EXITSIZEMOVE 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 so 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:
return 0;
case WM_LBUTTONUP:
case WM_MBUTTONUP:
case WM_RBUTTONUP:
return 0;
case WM_MOUSEMOVE:
return 0;
}
return DefWindowProc(hwnd, msg, wParam, lParam);
}
WM_ACTIVATE
事件處理視窗激活或無效的情況,若視窗激活,則啟動計時器;否則停止計時器。
WM_SIZE
事件處理視窗大小變化的情況。若視窗最小化,進行標記。若視窗已經發生變化,需要調用d3dApp::OnReSize
方法處理視窗變化的情況。在視窗大小正在改變的過程中,我們不需要調用剛纔的方法(此時視窗邊緣可能正在被拖動),否則太影響運行效率。
WM_ENTERSIZEMOVE
事件處理視窗正在移動或大小正在變化的情況。這個時候需要暫停計時器。
WM_EXITSIZEMOVE
事件處理視窗移動完成或大小變化結束的情況。這個時候需要啟動計時器。
其餘事件這裡不做具體描述,有些事件當前沒有處理,但是後續的內容會對這裡進行修改。
D3DApp::InitDirect3D方法--初始化Direct3D
現在假定你的電腦已經支持DirectX 11,但同時也有可能支持DirectX 11.1。因此在該項目中使用的頭文件是d3d11_1.h
。
要初始化DirectX11,我們需要創建這三樣東西:D3D設備、D3D設備上下文和DXGI交換鏈。
D3D設備包含了創建各種所需資源的方法。
D3D設備上下文負責對緩衝區進行渲染,綁定D3D設備創建的各種資源到不同的渲染管線。
DXGI交換鏈可以包含兩個或多個緩衝區,通常一個用於前端顯示,其餘的用於後端渲染。前臺緩衝區通常是只讀的,而後備緩衝區則是我們主要進行渲染的場所。當後備緩衝區渲染完成後,通過呈現方式將前後臺緩衝區交換,在屏幕上顯示出原來剛繪製好的畫面。
這三樣東西對應的介面類為:ID3D11Device
、ID3D11DeviceContext
、IDXGISwapChain
而如果支持DirectX11.1的話,則對應的介面類為:ID3D11Device1
、ID3D11DeviceContext1
、IDXGISwapChain1
,它們分別繼承自上面的三個介面類,區別在於額外提供了少數新的介面,並且介面方法的實現可能會有所區別。
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設備上下文
該函數可以創建DirectX11.1或者DirectX11.0的設備與設備上下文,取決於最終應用的D3D特性等級。
首先需要創建驅動類型數組進行輪詢,不過通常大多數情況都會支持D3D_DRIVER_TYPE_HARDWARE
,以享受硬體加速帶來的效益:
// 驅動類型數組
D3D_DRIVER_TYPE driverTypes[] =
{
D3D_DRIVER_TYPE_HARDWARE, // 硬體驅動
D3D_DRIVER_TYPE_WARP, // WARP驅動
D3D_DRIVER_TYPE_REFERENCE, // 軟體驅動
};
UINT numDriverTypes = ARRAYSIZE(driverTypes);
然後就是提供特性等級數組,這裡只考慮DirectX11:
// 特性等級數組
D3D_FEATURE_LEVEL featureLevels[] =
{
D3D_FEATURE_LEVEL_11_1,
D3D_FEATURE_LEVEL_11_0,
};
UINT numFeatureLevels = ARRAYSIZE(featureLevels);
最後就會可以創建D3D設備和設備上下文了:
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, md3dDevice.GetAddressOf(), &featureLevel, md3dImmediateContext.GetAddressOf());
if (hr == E_INVALIDARG)
{
// DirectX 11.0 平臺不承認D3D_FEATURE_LEVEL_11_1所以我們需要嘗試特性等級11.0
hr = D3D11CreateDevice(nullptr, d3dDriverType, nullptr, createDeviceFlags, &featureLevels[1], numFeatureLevels - 1,
D3D11_SDK_VERSION, md3dDevice.GetAddressOf(), &featureLevel, md3dImmediateContext.GetAddressOf());
}
if (SUCCEEDED(hr))
break;
}
if (FAILED(hr))
{
MessageBox(0, L"D3D11CreateDevice Failed.", 0, 0);
return false;
}
如果支持DirectX11.1的話,featureLevel
的結果應該為D3D_FEATURE_LEVEL_11_1
,並且md3dDevice
指向的是一個包含ID3D11Device1
介面的對象,以及md3dImmediateContext
指向的是一個包含ID3D11DeviceContext1
介面的對象;而如果只支持DirectX11.0的話則為D3D_FEATURE_LEVEL_11_0
。
IDXGIFactory2::CreateSwapChainForHwnd方法--DirectX11.1創建交換鏈
如果是DirectX11.1的話,需要先填充DXGI_SWAP_CHAIN_DESC1
和DXGI_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;
填充好後,DirectX11.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]輸出交換鏈對象
具體操作在後面一併演示
IDXGIFactory::CreateSwapChain方法--DirectX11創建交換鏈
如果是DirectX11.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;
DirectX11.0下使用的創建方法為IDXGIFactory::CreateSwapChain
:
HRESULT IDXGIFactory::CreateSwapChain(
IUnknown *pDevice, // [In]D3D設備
DXGI_SWAP_CHAIN_DESC *pDesc, // [In]交換鏈描述
IDXGISwapChain **ppSwapChain); // [Out]輸出交換鏈對象
根據已有設備類型來創建合適的交換鏈
瞭解了前面的操作後,現在我們需要先拿到包含IDXGIFactory
或者IDXGIFactory2
介面的對象:
ComPtr<IDXGIDevice> dxgiDevice = nullptr;
ComPtr<IDXGIAdapter> dxgiAdapter = nullptr;
ComPtr<IDXGIFactory1> dxgiFactory1 = nullptr;
ComPtr<IDXGIDevice1> dxgiDevice1 = nullptr;
ComPtr<IDXGIAdapter1> dxgiAdapter1 = nullptr;
ComPtr<IDXGIFactory2> dxgiFactory2 = nullptr;
// 為了正確創建 DXGI交換鏈,首先我們需要獲取創建 D3D設備 的 DXGI工廠,否則會引發報錯:
// "IDXGIFactory::CreateSwapChain: This function is being called with a device from a different IDXGIFactory."
// 從屬關係為 DXGI工廠-> DXGI適配器 -> DXGI設備 {D3D11設備}
HR(md3dDevice->QueryInterface(__uuidof(IDXGIDevice), reinterpret_cast<void**>(dxgiDevice.GetAddressOf())));
HR(dxgiDevice->GetAdapter(dxgiAdapter.GetAddressOf()));
HR(dxgiAdapter->GetParent(__uuidof(IDXGIFactory1), reinterpret_cast<void**>(dxgiFactory1.GetAddressOf())));
這時候可以確定dxgiFactory1
包含介面IDXGIFactory1
,然後檢查它是否包含介面IDXGIFactory2
,包含的話就說明支持DirectX11.1,然後獲取ID3D11Device1
和ID3D11DeviceContext1
介面對象並創建包含IDXGISwapChain1
介面的對象,否則就創建IDXGISwapChain
介面的對象:
// 如果包含,則說明支持DX11.1
if (dxgiFactory2 != nullptr)
{
HR(md3dDevice->QueryInterface(__uuidof(ID3D11Device1), reinterpret_cast<void**>(md3dDevice1.GetAddressOf())));
HR(md3dImmediateContext->QueryInterface(__uuidof(ID3D11DeviceContext1), reinterpret_cast<void**>(md3dImmediateContext1.GetAddressOf())));
// 填充各種結構體用以描述交換鏈
DXGI_SWAP_CHAIN_DESC1 sd;
ZeroMemory(&sd, sizeof(sd));
sd.Width = mClientWidth;
sd.Height = mClientHeight;
sd.Format = DXGI_FORMAT_R8G8B8A8_UNORM;
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(md3dDevice.Get(), mhMainWnd, &sd, &fd, nullptr, mSwapChain1.GetAddressOf()));
mSwapChain1->QueryInterface(__uuidof(IDXGISwapChain), reinterpret_cast<void**>(mSwapChain.GetAddressOf()));
}
else
{
// 填充DXGI_SWAP_CHAIN_DESC用以描述交換鏈
DXGI_SWAP_CHAIN_DESC sd;
ZeroMemory(&sd, sizeof(sd));
sd.BufferDesc.Width = mClientWidth;
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;
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;
HR(dxgiFactory1->CreateSwapChain(md3dDevice.Get(), &sd, mSwapChain.GetAddressOf()));
}
這時候,如果支持DirectX11.1的話,md3dDevice
和md3dDevice1
其實都指向同一個對象,md3dImmediateContext
和md3dImmediateContext1
,mSwapChain
和mSwapChain1
也是一樣的,區別僅僅在於後者實現了額外的一些介面,問題不大。因此不管是DirectX11.1還是DirectX11.0,後續都主要使用md3dDevice
,md3dImmediateContext
和mSwapChain
來進行操作。
設置全屏
預設情況下按ALT+ENTER可以切換成全屏,如果不想要這種操作,可以使用剛纔創建的dxgiFactory1
,按照下麵的方式來調用即可:
dxgiFactory1->MakeWindowAssociation(mhMainWnd, DXGI_MWA_NO_ALT_ENTER | DXGI_MWA_NO_WINDOW_CHANGES);
這樣DXGI就不會監聽Windows消息隊列,並且屏蔽掉了對接收到ALT+ENTER消息的處理。
在上述操作完成後,最後還調用了D3DApp::OnReSize
方法。
D3DApp::OnReSize方法--視窗調整後的操作
無論是初始化操作,還是視窗大小變化後的操作,參與繪製的後備緩衝區和深度模板緩衝區大小都需要重新設置,因此需要重新創建。
ID3D11Resource資源類型
Direct3D 11的資源可以主要分為四個大類,它們都派生自ID3D11Resource
:
ID3D11Buffer
通常用於頂點緩衝區、索引緩衝區等
ID3D11Texture1D
通常用於創建1維紋理資源
ID3D11Texture2D
通常用於創建2維紋理資源,可用於後備緩衝區
ID3D11Texture2D
通常用於創建3維紋理資源
ID3D11View資源視圖類型
Direct3D 11的資源視圖也可以分為四個大類,它們都派生自ID3D11View
:
ID3D11RenderTargetView
渲染目標視圖通常會綁定一個ID3D11Texture2D
的資源,而且通常綁定的是交換鏈指向的一個後備緩衝區。該視圖還需要綁定到渲染管線的輸出合併階段,輸出的結果將會寫入到所綁定的資源。
ID3D11DepthStencilView
深度模板視圖通常會綁定一個ID3D11Texture2D
的資源,該資源用於存儲深度和模板信息。該視圖還需要綁定到渲染管線的輸出合併階段,輸出的結果將會寫入到所綁定的資源。
ID3D11ShaderResourceView
著色資源視圖可以綁定資源,然後將該視圖綁定到渲染管線的著色器階段,使得著色器代碼可以訪問綁定的資源。
ID3D11UnorderedAccessView
目前還不瞭解該視圖的作用,可能會在後續進行更新補充說明。
IDXGISwapChain::GetBuffer方法--獲取後備緩衝區
由於此前我們創建好的交換鏈已經包含1個後備緩衝區了,在創建渲染目標視圖之前我們還需要獲取該後備緩衝區:
HRESULT IDXGISwapChain::GetBuffer(
UINT Buffer, // [In]緩衝區索引號,從0到BufferCount - 1
REFIID riid, // [In]緩衝區的介面類型ID
void **ppSurface); // [Out]獲取到的緩衝區
ID3D11Device::CreateRenderTargetView方法--創建渲染目標視圖
使用下麵的方法來獲取渲染目標視圖:
HRESULT ID3D11Device::CreateRenderTargetView(
ID3D11Resource *pResource, // [In]緩衝區資源
const D3D11_RENDER_TARGET_VIEW_DESC *pDesc, // 忽略
ID3D11RenderTargetView **ppRTView); // [Out]獲取渲染目標視圖
因此D3DApp::OnReSize
方法前面可以這樣寫:
assert(md3dImmediateContext);
assert(md3dDevice);
assert(mSwapChain);
if (md3dDevice1 != nullptr)
{
assert(md3dImmediateContext1);
assert(md3dDevice1);
assert(mSwapChain1);
}
// 釋放交換鏈的相關資源
mRenderTargetView.Reset();
mDepthStencilView.Reset();
mDepthStencilBuffer.Reset();
// 重設交換鏈並且重新創建渲染目標視圖
ComPtr<ID3D11Texture2D> backBuffer;
HR(mSwapChain->ResizeBuffers(1, mClientWidth, mClientHeight, DXGI_FORMAT_R8G8B8A8_UNORM, 0));
HR(mSwapChain->GetBuffer(0, __uuidof(ID3D11Texture2D), reinterpret_cast<void**>(backBuffer.GetAddressOf())));
HR(md3dDevice->CreateRenderTargetView(backBuffer.Get(), 0, mRenderTargetView.GetAddressOf()));
backBuffer.Reset();
ID3D11Device::CreateTexture2D--創建一個2D紋理
除了渲染目標視圖外,我們還需要創建深度模板緩衝區用於深度測試。通過D3D設備可以新建一個緩衝區,但在此之前我們需要先描述該緩衝區的信息:
typedef struct D3D11_TEXTURE2D_DESC
{
UINT Width; // 緩衝區寬度
UINT Height; // 緩衝區高度
UINT MipLevels; // Mip等級
UINT ArraySize; // 紋理數組中的紋理數量,預設1
DXGI_FORMAT Format; // 緩衝區數據格式
DXGI_SAMPLE_DESC SampleDesc; // 忽略
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紋理
ID3D11Device::CreateDepthStencilView方法--創建深度模板視圖
HRESULT ID3D11Device::CreateDepthStencilView(
ID3D11Resource *pResource, // [In] 需要綁定的資源
const D3D11_DEPTH_STENCIL_VIEW_DESC *pDesc, // [In] 深度緩衝區描述
ID3D11DepthStencilView **ppDepthStencilView); // [Out] 獲取到的深度模板視圖
ID3D11DeviceContext::OMSetRenderTargets方法--輸出合併階段綁定渲染目標視圖和深度模板視圖
void ID3D11DeviceContext::OMSetRenderTargets(
UINT NumViews, // [In] 視圖數目
ID3D11RenderTargetView *const *ppRenderTargetViews, // [In] 渲染目標視圖數組
ID3D11DepthStencilView *pDepthStencilView) = 0; // [In] 深度模板視圖
下麵演示瞭如何創建深度模板視圖,並將渲染目標視圖和深度模板視圖綁定到渲染管線的輸出合併階段:
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? --需要給交換鏈設置MASS參數
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(md3dDevice->CreateTexture2D(&depthStencilDesc, 0, mDepthStencilBuffer.GetAddressOf()));
HR(md3dDevice->CreateDepthStencilView(mDepthStencilBuffer.Get(), 0, mDepthStencilView.GetAddressOf()));
// 將渲染目標視圖和深度/模板緩衝區結合到管線
md3dImmediateContext->OMSetRenderTargets(1, mRenderTargetView.GetAddressOf(), mDepthStencilView.Get());
ID3D11DeviceContext::RSSetViewports方法--光柵化階段設置視口區域
最終我們還需要決定將整個視圖輸出到視窗特定的範圍。因此我們需要使用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); // 視口數組
將視圖輸出到整個屏幕需要進行下麵的操作:
mScreenViewport.TopLeftX = 0;
mScreenViewport.TopLeftY = 0;
mScreenViewport.Width = static_cast<float>(mClientWidth);
mScreenViewport.Height = static_cast<float>(mClientHeight);
mScreenViewport.MinDepth = 0.0f;
mScreenViewport.MaxDepth = 1.0f;
md3dImmediateContext->RSSetViewports(1, &mScreenViewport);
這些就是D3DApp
框架類最主要的部分了,在後續的部分,該框架的代碼基本上不會有什麼太大的變動。因此後續代碼的添加主要在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]指定覆蓋顏色
ID3D11DeviceContext::ClearDepthStencilView方法--清空深度模板緩衝區
同樣在進行渲染之前,我們也要清理一遍深度模板緩衝區
void ID3D11DeviceContext::ClearDepthStencilView(
ID3D11DepthStencilView *pDepthStencilView, // [In]深度模板視圖
UINT ClearFlags, // [In]D3D11_CLEAR_FLAG枚舉
FLOAT Depth, // [In]深度
UINT8 Stencil); // [In]模板初始值
IDXGISwapChain::Present方法--前後臺緩衝區交換並呈現
完成一切繪製操作後就可以調用該方法了
HRESULT ID3D11DeviceContext::STDMETHODCALLTYPE Present(
UINT SyncInterval, // [In]通常為0
UINT Flags);