DirectX11 With Windows SDK--01 DirectX11初始化
2018-05-21 22:07
621 查看
前言
由于个人觉得龙书里面第4章提供的Direct3D 初始化项目封装得比较好,而且DirectX SDK Samples里面的初始化程序过于精简,不适合后续使用,故选择了以Init Direct3D项目作为框架,然后还使用了微软提供的示例项目,两者结合到一起。建议下载项目配合阅读。DirectX11 With Windows SDK完整目录
Github项目源码
项目结构
该项目包含了下面这些文件![](https://oscdn.geek-share.com/Uploads/Images/Content/201805/a467b3ed676c8de170c7eb04e679b3d2.png)
其中头文件的具体功能
头文件 | 功能 |
---|---|
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
ComPtr<T>::ReleaseAndGetAddressOf方法则相当于先调用Reset方法,再调用GetAddressOf方法获取指针地址
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
一个完整的Direct3D初始化需要经历下面这些步骤:创建D3D设备、D3D设备上下文、DXGI交换链
获取交换链后备缓冲区的
ID3D11Buffer接口对象
创建渲染目标视图
ID3D11RenderTargetView,绑定刚才的后备缓冲区
通过D3D设备创建一个
ID3D11Texture2D跟后备缓冲区同等宽高的接口对象
创建深度模板视图
ID3D11DepthStrenilView,绑定刚才的2D纹理
通过D3D设备上下文,在渲染管线的输出合并阶段设置渲染目标视图和深度模板视图
在渲染管线的光栅化阶段设置好渲染的视口区域
现在假定你的电脑已经支持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]输出交换链对象
根据已有设备类型来创建合适的交换链
了解了前面的操作后,现在我们需要先拿到包含IDXGIFactory1或者
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维纹理资源,可用于后备缓冲区
ID3D11Texture3D通常用于创建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]获取到的缓冲区
在这里接口类型可以是
IDXGISurface(后续会用到)或者
ID3D11Texture2D。现在我们需要获取的是
ID3D11Texture2D接口类。演示部分在下面。
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); // [In]通常为0
GameApp::DrawScene的实现如下:
void GameApp::DrawScene() { assert(md3dImmediateContext); assert(mSwapChain); static float blue[4] = { 0.0f, 0.0f, 1.0f, 1.0f }; // RGBA = (0,0,255,255) md3dImmediateContext->ClearRenderTargetView(mRenderTargetView.Get(), reinterpret_cast<const float*>(&blue)); md3dImmediateContext->ClearDepthStencilView(mDepthStencilView.Get(), D3D11_CLEAR_DEPTH | D3D11_CLEAR_STENCIL, 1.0f, 0); HR(mSwapChain->Present(0, 0)); }
最终绘制的效果应该如下:
![](https://oscdn.geek-share.com/Uploads/Images/Content/201805/b78fbab59d4a2975f6cb414568993788.png)
DirectX11 With Windows SDK完整目录
Github项目源码
相关文章推荐
- DirectX11 With Windows SDK--12 深度/模板状态
- DirectX11 With Windows SDK--23 立方体映射:动态天空盒的实现
- DirectX11 With Windows SDK--09 纹理映射与采样器状态
- DirectX11 With Windows SDK--13 抛弃FX11并初步实现BasicFX类
- DirectX11 With Windows SDK--00 目录
- DirectX11 With Windows SDK--13 动手实现一个简易Effects框架、阴影效果绘制
- DirectX11 With Windows SDK--02 顶点/像素着色器的创建、顶点缓冲区
- DirectX11 With Windows SDK--07 添加光照与常用几何模型
- DirectX11 With Windows SDK--18 使用DirectXCollision库进行碰撞检测
- DirectX11 With Windows SDK--22 静态天空盒的读取与实现、模型反射
- DirectX11 With Windows SDK--10 摄像机类
- DirectX11 With Windows SDK--20 硬件实例化与视锥体裁剪
- DirectX11 With Windows SDK--03 索引缓冲区、常量缓冲区
- DirectX11 With Windows SDK--14 深度测试
- DirectX11 With Windows SDK--14 深度测试
- DirectX11 With Windows SDK--04 使用DirectX Tool Kit帮助开发
- DirectX11 With Windows SDK--19 模型加载:obj格式的读取及使用二进制文件提升读取效率
- DirectX11 With Windows SDK--06 DirectXMath数学库
- DirectX11 With Windows SDK--08 Direct2D与Direct3D互操作性以及利用DWrite显示文字
- DirectX11 With Windows SDK--05 键盘和鼠标输入