Direct3D11基础知识
这是记录我学习DirectX一系列文章,目测需要掌握一些基本的Win32知识,当然C++是必须的
摘要
在本篇中,我会介绍创建最小DirectX应用程序所需要的一些元素,每个DX程序都必须具有这些元素才成正常运行。这些元素包括一个用来显示DX的窗口和一个DX相关设备,然后我们就可以在窗口上通过DX渲染图形了。
窗口相关
我们通过vs构建c++窗口程序
后面的我就不再多说了。如果对Win32构建桌面程序不太了解,可以上网查资料进行学习,我们目前差不多只要了解到回调函数就可以了
DX相关
现在我们拥有一个正在显示的窗口,然后我们需要初始化D3D。D3D初始化阶段首先需要创建D3D设备,D3D设备上下文和DXGI交换链
D3D设备(ID3D11Device)用来执行渲染和资源创建,它包含了各种创建各种资源的方法,最常用的类有:资源类(ID3D11Resource,包含纹理和缓冲),视图类以及着色器。
D3D设备上下文(ID3D11DeviceContext)用来负责渲染工作,他可以看作是一个渲染管线,他需要绑定来自D3D设备创建的各种资源,视图类和着色器才能正常执行渲染到渲染目标(一切供渲染引擎绘话图形的缓冲皆称为渲染目标)。
DXGI交换链(IDXGISwapChain)用来负责获取展示渲染内容的窗口的缓冲区,它缓存了一个或者多个表面(2D纹理),这些表面都可以称为缓冲区(Buffer)。主要是前端和后端。前缓冲区保存向用户展示的内容,后缓冲区则是我们主要进行的渲染的场所,我们将它通过合适的手段成为渲染管线将绘制的渲染目标。一旦渲染完成,交换链将通过交换两个缓冲区来展示后缓冲区。这样后缓冲区就变成了前缓冲区,反之亦然。
初始化它们在后边,我先讲下渲染一般流程。
- 创建D3D设备,D3D设备上下文和DXGI交换链
- 创建一个渲染目标视图(RenderTargetView),并将其绑定到渲染管道上。
- 渲染之前的最后一件事就是初始化视口。视口就是用来映射剪辑空间坐标。
- 渲染场景。
- 交换链交换缓冲区。
以上,就是一般渲染流程。现在,我开始介绍如何执行上述步骤:
D3D设备和D3D设备上下文的创建
D3D11CreateDevice函数--创建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设备上下文 );
pAdapter
(适配器),可以看作是显卡的一层封装,通过它我们设置选择哪个显卡。一般设置为nullptr
,这样可以由上层驱动替我们决定使用哪个显卡,我们也可到NVIDIA控制面板来设置设置当前程序使用哪个显卡。DriverType
指定驱动类型,不过通常大多数我们选择D3D_DRIVER_HADEWARE
,以享受硬件加速带来的收益。Software
是实现软光栅器的DLL的句柄。如果DriverType
是D3D_DRIVER_TYPE_SOFTWARE
,则Software
不得为NULL
。Flags
对应的是D3D11_CREATE_DEVICE_FLAG枚举值,如果需要D3D设备调试的话(在Debug模式下),可以指定D3D11_CREATE_DEVICE_DEBUG
枚举值,指定该值后,可以在程序出现异常的时候观察调试输出窗口的信息。该值可以按位进行OR运算。例如:UINT flags = 0; #if _DEBUG flags |= D3D11_CREATE_DEVICE_DEBUG; #endif // DEBUG
pFeatureLevels
是一个特征等级数组,通过函数内部进行查询以检测所支持的特征等级。如果pFeatureLevel
是nullptr
,则函数使用如下特征等级数组:
{ D3D_FEATURE_LEVEL_11_0, D3D_FEATURE_LEVEL_10_1, D3D_FEATURE_LEVEL_10_0, D3D_FEATURE_LEVEL_9_3, D3D_FEATURE_LEVEL_9_2, D3D_FEATURE_LEVEL_9_1, };
可以看出如果想创建D3D_FEATURE_LEVEL_11_1
设备,必须显式提供包含D3D_FEATURE_LEVEL_11_1
的D3D_FEATURE_LEVEL
数组。例如:
D3D_FEATURE_LEVEL featureLevels[] = { D3D_FEATURE_LEVEL_11_1, D3D_FEATURE_LEVEL_11_0, D3D_FEATURE_LEVEL_10_1 };
但是如果系统不支持D3D 11.1,则D3D11CreateDevice
会立刻返回一个E_INVALIDARG
。如果这样,你需要降低特征等级。
在Win10, Win8.x 或 Win7 SP1且安装了KB2670838补丁的系统都支持Direct3D 11.1的API
FeatureLevels
是pFeatureLevels
中元素数量。SDKVersion
默认使用D3D11_SDK_VERSION
。ppDevice``ppImmediateContext
分别是获取对应对象的指针。pFeatureLevel
是获取特征等级数组的第一个元素。如果不需要的话就输入nullptr
。示例代码:
HRESULT hr = S_OK; // D3D11_CREATE_DEVICE_FLAG枚举类型值 UINT flags = 0; #if _DEBUG flags |= D3D11_CREATE_DEVICE_DEBUG; #endif // DEBUG // 特性等级数组 D3D_FEATURE_LEVEL featureLevels[] = { D3D_FEATURE_LEVEL_11_1, D3D_FEATURE_LEVEL_11_0, D3D_FEATURE_LEVEL_10_1 }; UINT numFeatureLevel = ARRAYSIZE(featureLevels); D3D_FEATURE_LEVEL featureLevel; hr = D3D11CreateDevice(nullptr, D3D_DRIVER_TYPE_HARDWARE, NULL, flags, featureLevels, numFeatureLevel, D3D11_SDK_VERSION, &pD3D11Device, &featureLevel, &pImmediateContext); if (hr == E_INVALIDARG) { // Direct3D 11.0不识别D3D_FEATURE_11_1,所以我们可以取第二个元素试试 hr = D3D11CreateDevice(nullptr, D3D_DRIVER_TYPE_HARDWARE, NULL, flags, &featureLevels[1], numFeatureLevel - 1, D3D11_SDK_VERSION, &pD3D11Device, &featureLevel, &pImmediateContext); } // 检测是否创建成功 if (FAILED(hr)) { MessageBox(0, L"D3D11CreateDevice Failed.", 0, 0); return hr; } // 检测是否支持特征等级11.0或11.0 if (featureLevel != D3D_FEATURE_LEVEL_11_1 && featureLevel != D3D_FEATURE_LEVEL_11_0) { MessageBox(0, L"Direct3D Feature Level 11 unsupported.", 0, 0); return false; }
DXGI交换链初始化
我们需要先了解D3D和DXGI各版本的对应关系,这很重要
DXGI的交换链的创建需要通过IDXGIFactory::CreateSwapChain
方法进行,但是如果要创建D3D 11.1对应的交换链,则需要通过IDXGIFactory::CreateSwapChainForHwnd
进行。
所以,我们想要得到交换链,必须先获取包含IDXGIFactory接口的对象。
获取包含IDXGIFactory接口的对象
一开始我们在创建D3D设备的时候使用的是默认的显卡适配器IDXGIAdapter
。所以创建出来的D3D设备本身就实现了IDXGIDevice
接口,通过该对象,我们可以获取到当前所用的显卡适配器IDXGIAdapter
对象,这样我们再通过查询它的父级找到是哪个IDXGIFactory
枚举出来的适配器。所以得到IDXGIFactory接口实例代码如下:
IDXGIFactory1 *pFactory1 = nullptr; { IDXGIDevice* pDevice = nullptr; hr = g_pD3D11Device->QueryInterface(__uuidof(IDXGIDevice), reinterpret_cast<void**> (&pDevice)); if (SUCCEEDED(hr)) { IDXGIAdapter* pAdapter = nullptr; hr = pDevice->GetAdapter(&pAdapter); if (SUCCEEDED(hr)) { pAdapter->GetParent(__uuidof(IDXGIFactory1), reinterpret_cast<void**>(&pFactory1)); pAdapter->Release(); } pDevice->Release(); } }
我们也发现,每得到一节接口对象,用完后要手动释放,这样是不是太麻烦了。于是,我们引入Comptr
智能指针,所以上述代码就简化成这样了:
ComPtr<IDXGIDevice>dxgiDevice; ComPtr<IDXGIAdapter>dxgiAdapter; ComPtr<IDXGIFactory1>dxgiFactory1; ComPtr<IDXGIFactory2>dxgiFactory2; if (SUCCEEDED(g_pD3D11Device.As(&dxgiDevice))) { if (SUCCEEDED(dxgiDevice->GetAdapter(dxgiAdapter.GetAddressOf()))) { hr = dxgiAdapter->GetParent(__uuidof(IDXGIFactory1), reinterpret_cast<void**>(dxgiFactory1.GetAddressOf())); } } if (FAILED(hr)) return hr;
如果他支持D3D 11.1的话,我们就可以通过下面代码拿到DXGI 1.2的对象
dxgiFactory1.As(&dxgiFactory2);
IDXGIFactory::CreateSwapChainForHwnd——D3D 11.1创建交换链
如果是D3D 11.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;
填充玩交换链描述符后就可以构建交换链,一个简单的示例如下:
// dx 11.1 or later DXGI_SWAP_CHAIN_DESC1 sd = {}; sd.Width = 400; sd.Height = 600; sd.Format = DXGI_FORMAT_B8G8R8A8_UNORM; sd.SampleDesc.Count = 1; sd.SampleDesc.Quality = 0; sd.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT; sd.BufferCount = 1; hr = dxgiFactory2->CreateSwapChainForHwnd(g_pD3D11Device.Get(), g_hWnd, &sd, nullptr, nullptr, g_pSwapChain1.GetAddressOf()); if (SUCCEEDED(hr)) { hr = g_pSwapChain1.As(&g_pSwapChain); } if (SUCCEEDED(hr)) { // 注意我们上面因为没有填充DXGI_SWAP_CHAIN_FULLSCREEN_DESC结构体,所以我们要禁用ALT+ENTER与全屏的关联。 dxgiFactory2->MakeWindowAssociation(g_hWnd, DXGI_MWA_NO_ALT_ENTER); }
上面可以注意到我不进得到了g_pSwapChain1
,还通过它得到了g_pSwapChain
。其实这两个接口对象指的都是同一个对象,只是前者比后者多实现了一些额外的接口,我们在后续一般使用g_pSwapChain
就够了。
为渲染管道设置渲染目标
通过DXGI交换链得到原始内存
由于我们之前创建好的交换链已经包含一个后缓冲区了,我们可以通过IDXGI::GetBuffer()
方法直接获取到后缓冲区的ID3D11Texture2D
接口。
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, // [In]为NULl时则为选择默认渲染目标视图 ID3D11RenderTargetView **ppRTView); // [Out]获取渲染目标视图
为渲染管线的输出合并阶段设置渲染目标
我们可以通过渲染上下文,在渲染管线的输出合并阶段设置渲染目标。可以通过ID3D11DeviceContent::OMSetRenderTargets
方法实现。这样就可以确保管道呈现(Present)的输出被写入了后台缓冲区。
void ID3D11DeviceContext::OMSetRenderTargets( UINT NumViews, // [In] 视图数目 ID3D11RenderTargetView *const *ppRenderTargetViews, // [In] 渲染目标视图数组 ID3D11DepthStencilView *pDepthStencilView) = 0; // [In] 深度/模板视图
下面给出简单实现
// 得到原始内存 ComPtr<ID3D11RenderTargetView> g_pRenderTargetView; ComPtr<ID3D11Texture2D> pBackBuffer; g_pSwapChain->GetBuffer(0, __uuidof(ID3D11Texture2D), reinterpret_cast<void**>(pBackBuffer.GetAddressOf())); // 创建渲染目标视图 g_pD3D11Device->CreateRenderTargetView(pBackBuffer.Get(), nullptr, g_pRenderTargetView.GetAddressOf()); // 为渲染管道设置渲染目标 g_pImmediateContext->OMSetRenderTargets(1, g_pRenderTargetView.GetAddressOf(), nullptr);
初始化视口
最终我们还需决定将整个视图输出到窗口特定的范围。我们需要使用D3D11_VIEWPORT
来描述视口,然后通过ID3D11DeviceContext::RSSetViewports
方法来设置1个或者多个视口
typedef struct D3D11_VIEWPORT { FLOAT TopLeftX; // 屏幕左上角起始位置X FLOAT TopLeftY; // 屏幕左上角起始位置Y FLOAT Width; // 宽度 FLOAT Height; // 高度 FLOAT MinDepth; // 最小深度,必须为0.0f FLOAT MaxDepth; // 最大深度,必须为1.0f } D3D11_VIEWPORT; void ID3D11DeviceContext::RSSetViewports( UINT NumViewports, // 视口数目 const D3D11_VIEWPORT *pViewports); // 视口数组
简单实例
D3D11_VIEWPORT vp ; vp.Width = static_cast<float>(width); vp.Height = static_cast<float>(height); vp.MinDepth = 0.0f; vp.MaxDepth = 1.0f; vp.TopLeftX = 0; vp.TopLeftY = 0; g_pImmediateContext->RSSetViewports(1, &vp);
渲染场景并呈现
每一次渲染之前,我们需要清理一遍渲染目标视图绑定的缓冲区
void ID3D11DeviceContext::ClearRenderTargetView( ID3D11RenderTargetView *pRenderTargetView, // [In]渲染目标视图 const FLOAT ColorRGBA[4]); // [In]指定覆盖颜色
然后我们让DXGI交换前后缓冲区以呈现输出。
HRESULT ID3D11DeviceContext::Present( UINT SyncInterval, // [In]通常为0 UINT Flags); // [In]通常为0
示例代码
if (g_pImmediateContext&&g_pSwapChain) { static float blue[4] = { 0.0f, 0.0f, 1.0f, 1.0f }; // RGBA = (0,0,255,255) g_pImmediateContext->ClearRenderTargetView(g_pRenderTargetView.Get(), blue); g_pSwapChain->Present(0, 0); }
最终的绘制效果应该如下
。。。。。。。
。。。。。
报错了,,原来是我忘记引入静态库了
在程序最上方加入
#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")
这也加上,为后续做准备。
最终绘制效果如下
程序最后的清理
因为我们使用了智能指针,所以我们只需要通过ID3D11DeviceContext::ClearState
来恢复D3D设备上下文到默认状态就可以了,它会卸下所有绑定的资源。剩下的我们就交给智能指针就OK了。
DX相关代码如下
// DX相关全局变。 HWND g_hWnd; template <class T> using ComPtr = Microsoft::WRL::ComPtr<T>; ComPtr<ID3D11Device> g_pD3D11Device; ComPtr<ID3D11DeviceContext> g_pImmediateContext; ComPtr<IDXGISwapChain> g_pSwapChain; ComPtr<IDXGISwapChain1> g_pSwapChain1; ComPtr<ID3D11RenderTargetView> g_pRenderTargetView; UINT width = 400; UINT height = 600; // 初始化D3D HRESULT InitD3DDevice() { HRESULT hr = S_OK; // D3D11_CREATE_DEVICE_FLAG枚举类型值 UINT flags = 0; #if _DEBUG flags |= D3D11_CREATE_DEVICE_DEBUG; #endif // DEBUG // 特性等级数组 D3D_FEATURE_LEVEL featureLevels[] = { D3D_FEATURE_LEVEL_11_1, D3D_FEATURE_LEVEL_11_0, D3D_FEATURE_LEVEL_10_1 }; UINT numFeatureLevel = ARRAYSIZE(featureLevels); D3D_FEATURE_LEVEL featureLevel; hr = D3D11CreateDevice(nullptr, D3D_DRIVER_TYPE_HARDWARE, NULL, flags, featureLevels, numFeatureLevel, D3D11_SDK_VERSION, g_pD3D11Device.GetAddressOf(), &featureLevel, g_pImmediateContext.GetAddressOf()); if (hr == E_INVALIDARG) { // Direct3D 11.0不识别D3D_FEATURE_11_1,所以我们可以取第二个元素试试 hr = D3D11CreateDevice(nullptr, D3D_DRIVER_TYPE_HARDWARE, NULL, flags, &featureLevels[1], numFeatureLevel - 1, D3D11_SDK_VERSION, g_pD3D11Device.GetAddressOf(), &featureLevel, g_pImmediateContext.GetAddressOf()); } // 检测是否创建成功 if (FAILED(hr)) { MessageBox(0, L"D3D11CreateDevice Failed.", 0, 0); return hr; } // 检测是否支持特征等级11.0或11.0 if (featureLevel != D3D_FEATURE_LEVEL_11_1 && featureLevel != D3D_FEATURE_LEVEL_11_0) { MessageBox(0, L"Direct3D Feature Level 11 unsupported.", 0, 0); return hr; } // Obtain DXGI factory from device. ComPtr<IDXGIDevice>dxgiDevice; ComPtr<IDXGIAdapter>dxgiAdapter; ComPtr<IDXGIFactory1>dxgiFactory1; ComPtr<IDXGIFactory2>dxgiFactory2; if (SUCCEEDED(g_pD3D11Device.As(&dxgiDevice))) { if (SUCCEEDED(dxgiDevice->GetAdapter(dxgiAdapter.GetAddressOf()))) { hr = dxgiAdapter->GetParent(__uuidof(IDXGIFactory1), reinterpret_cast<void**>(dxgiFactory1.GetAddressOf())); } } if (FAILED(hr)) return hr; // // Create swap chain // // 查看对象是否有IDXGIFactory2接口 dxgiFactory1.As(&dxgiFactory2); if(dxgiFactory2) { // dx 11.1 or later DXGI_SWAP_CHAIN_DESC1 sd = {}; sd.Width = 400; sd.Height = 600; sd.Format = DXGI_FORMAT_B8G8R8A8_UNORM; sd.SampleDesc.Count = 1; sd.SampleDesc.Quality = 0; sd.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT; sd.BufferCount = 1; hr = dxgiFactory2->CreateSwapChainForHwnd(g_pD3D11Device.Get(), g_hWnd, &sd, nullptr, nullptr, g_pSwapChain1.GetAddressOf()); if (SUCCEEDED(hr)) { hr = g_pSwapChain1.As(&g_pSwapChain); } if (SUCCEEDED(hr)) { // 注意我们上面因为没有填充DXGI_SWAP_CHAIN_FULLSCREEN_DESC结构体,所以我们要禁用ALT+ENTER与全屏的关联。 dxgiFactory2->MakeWindowAssociation(g_hWnd, DXGI_MWA_NO_ALT_ENTER); } } // // DXGI和D3D设备交互 // // 得到原始内存 ComPtr<ID3D11Texture2D> pBackBuffer; g_pSwapChain->GetBuffer(0, __uuidof(ID3D11Texture2D), reinterpret_cast<void**>(pBackBuffer.GetAddressOf())); // 创建渲染目标视图 g_pD3D11Device->CreateRenderTargetView(pBackBuffer.Get(), nullptr, g_pRenderTargetView.GetAddressOf()); // 为渲染管道设置渲染目标 g_pImmediateContext->OMSetRenderTargets(1, g_pRenderTargetView.GetAddressOf(), nullptr); // 初始化视口 D3D11_VIEWPORT vp = {}; vp.Width = static_cast<float>(width); vp.Height = static_cast<float>(height); vp.MinDepth = 0.0f; vp.MaxDepth = 1.0f; vp.TopLeftX = 0; vp.TopLeftY = 0; g_pImmediateContext->RSSetViewports(1, &vp); return hr; } // 主程序退出后的清理。 void Clear() { g_pImmediateContext->ClearState(); } // 渲染 void Render() { if (g_pImmediateContext&&g_pSwapChain) { static float blue[4] = { 0.0f, 0.0f, 1.0f, 1.0f }; // RGBA = (0,0,255,255) g_pImmediateContext->ClearRenderTargetView(g_pRenderTargetView.Get(), blue); g_pSwapChain->Present(0, 0); } }