* 这两个月我看了看数据可视化,又看了看OpenCV,草稿箱里的东西阅存越多,最后还是决定入坑计算机图形学了。因为本科的时候学过一点OpenGLES和计算机图形学原理,再加上自己对游戏开发感兴趣,所以不打算再继续跟老师的方向走了。本来选现在的老师是打算学学游戏服务器的,奈何失算了,o(︶︿︶)o 唉。
本学习笔记的原始素材来自龙书DirectX11,全部代码和部分文字来自GitHub及其整理龙书代码的大佬的博客。
本博文记录的是我自己的思维方式和理解方法,还有部分内容来自其他博客,MSDN,StackOverflow,视频网站等,比他们的更详细,尽量不一笔带过(因为我比较笨o(︶︿︶)o )。
本文主要使用DirectX11.1,Win10,VS2019,代码里各个类之间的调用关系比较简单,就不一一对应了。
一.Direct3D概述
Direct3D是一种底层绘图API(application programming interface,应用程序接口),它可以让我们可以通过3D硬件加速绘制3D世界。从本质上讲,Direct3D提供的是一组软件接口,我们可以通过这组接口来控制绘图硬件。例如,要命令绘图设备清空渲染目标(例如屏幕),我们可以调用Direct3D的ID3D11DeviceContext::ClearRenderTargetView方法来完成这一工作。Direct3D层位于应用程序和绘图硬件之间,这样我们就不必担心3D硬件的实现细节,只要设备支持Direct3D 11,我们就可以通过Direct3D 11 API来控制3D硬件了。
支持Direct3D 11的设备必须支持Direct3D 11规定的整个功能集合以及少数的额外附加功能(有一些功能,比如多重采样数量,仍然需要以查询方式实现,这是因为不同的Direct3D硬件这个值可能并不一样)。在Direct3D 9中,设备可以只支持Direct3D 9的部分功能;所以,当一个Direct3D 9应用程序要使用某一特性时,应用程序就必须先检查硬件是否支持该特性。如果要调用的是一个不为硬件支持Direct3D函数,那应用程序就会出错。而在Direct3D 11中,不需要再做这种设备功能检查,因为Direct3D 11强制要求设备实现Direct3D 11规定的所有功能特性。
组件对象模型(COM)技术使DirectX独立于任何编程语言,并具有版本向后兼容的特性。我们经常把COM对象称为接口,并把它当成一个普通的C++类来使用。当使用C++编写DirectX程序时,许多COM的底层细节都不必考虑。唯一需要知道的一件事情是,我们必须通过特定的函数或其他的COM接口方法来获取指向COM接口的指针,而不能用C++的new关键字来创建COM接口。另外,当我们不再使用某个接口时,必须调用它的Release方法来释放它(所有的COM接口都继承于IUnknown接口,而Release方法是IUnknown接口的成员),而不能用delete语句——COM对象在其自身内部实现所有的内存管理工作。
当然,有关COM的细节还有很多,但是在实际工作中只需知道上述内容就足以有效地使用DirectX了。
注意:COM接口都以大写字母“I”为前缀。例如,表示2D纹理的接口为ID3D11Texture2D。
二.D3D初始化
D3D初始化前还要初始化Window窗口程序的一些属性,因为不属于D3D,略过了,有兴趣的可以直接看代码,都是API的东西。
D3D的初始化其实也是完全代码上的东西,可以先不了解图形渲染管线的知识,就当是扫盲和入门了。
从bool D3DApp::InitDirect3D()
方法出发,需要以下几个步骤:
-
- 创建D3D设备和设备上下文
-
- 创建交换链
-
- 设置DXGI交换链与Direct3D设备的交互
1.创建D3D设备和设备上下文
什么是D3D设备和设备上下文?
它们是是最重要的Direct3D接口,可以被看成是物理图形设备硬件的软控制器;也就是说,我们可以通过该接口与硬件进行交互,命令硬件完成一些工作(比如:在显存中分配资源、清空后台缓冲区、将资源绑定到各种管线阶段、绘制几何体)。具体而言:
ID3D11Device接口用于检测显示适配器功能和分配资源。
ID3D11DeviceContext接口用于设置管线状态、将资源绑定到图形管线和生成渲染命令。
本文的设备上下文是ID3D11DeviceContext* md3dImmediateContext;
,叫做立即执行上下文。
还有种上下文叫延迟执行上下文(ID3D11Device::CreateDeferredContext)。该上下文主要用于DirectX11支持的多线程程序。
创建D3D设备和设备上下文的方法:
HRESULT WINAPI D3D11CreateDevice(
IDXGIAdapter* pAdapter, // 1. [In_Opt]适配器
D3D_DRIVER_TYPE DriverType, // 2. [In]驱动类型
HMODULE Software, // 3. [In_Opt]若上面为D3D_DRIVER_TYPE_SOFTWARE则这里需要提供程序模块
UINT Flags, // 4. [In]使用D3D11_CREATE_DEVICE_FLAG枚举类型
D3D_FEATURE_LEVEL* pFeatureLevels, // 5. [In_Opt]若为nullptr则为默认特性等级,否则需要提供特性等级数组
UINT FeatureLevels, // 6. [In]特性等级数组的元素数目
UINT SDKVersion, // 7. [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控制面板来设置当前程序要使用哪个显卡。如果想要在应用层决定,使用IDXGIFactory::EnumAdapters
方法可以枚举当前可用的显示卡设备。
②DriverType
驱动类型
D3D_DRIVER_TYPE
原型:
typedef enum D3D_DRIVER_TYPE {
D3D_DRIVER_TYPE_UNKNOWN,
D3D_DRIVER_TYPE_HARDWARE,
D3D_DRIVER_TYPE_REFERENCE,
D3D_DRIVER_TYPE_NULL,
D3D_DRIVER_TYPE_SOFTWARE,
D3D_DRIVER_TYPE_WARP
} ;
驱动类型 | 描述 |
---|---|
D3D_DRIVER_TYPE_UNKNOWN | 表示未知驱动类型 |
D3D_DRIVER_TYPE_HARDWARE | 一种可以应用D3D特性的硬件驱动。这是你D3D程序中应该主要使用的驱动,因为它效率最好。硬件驱动可以在支持的设备上使用硬件加速,也同样可以在设备中不支持的渲染管线部分使用软件加速。硬件驱动经常是硬件设备的一种抽象。 |
D3D_DRIVER_TYPE_REFERENCE | 一种由软件驱动的支持所有D3D特性的引用驱动。引用驱动的目的是提高精度而不是速度。只用来作特性测试,或者debug,只是为了开发和测试来使用。 |
D3D_DRIVER_TYPE_NULL | 空驱动,代表不支持渲染的引用驱动。主要用来debug非渲染API调用。这个驱动只有DirectX SDK有。 |
D3D_DRIVER_TYPE_SOFTWARE | 软件驱动,纯粹用软件应用D3D特性。不适合高性能场景,因为它很慢。是硬件加速失效的替代品。 |
D3D_DRIVER_TYPE_WARP | 高性能软件渲染器。 |
一般都用D3D_DRIVER_TYPE_HARDWARE,但是我们要先检查一下当前系统环境到底支持哪一个,顺序从硬件-WARP-软件驱动来轮询,依次尝试能否成功。
④Flags 枚举值
Flags
对应的是D3D11_CREATE_DEVICE_FLAG
枚举值,如果需要D3D设备调试的话(在Debug模式下),可以指定D3D11_CREATE_DEVICE_DEBUG
枚举值。指定该值后,可以在出现程序异常的时候观察调试输出窗口的信息。
⑤⑥pFeatureLevels
特征等级
D3D_FEATURE_LEVEL
本身是一个枚举类型,里面是各种D3D的版本。只需要在D3D11CreateDevice
方法里传入一个包含你想支持的等级的数组就可以了,⑥是数组的格元素个数。
代码
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;
}
2. 创建交换链
什么是交换链?
当模型的图元经过层层计算和测试后,就会显示到我们的屏幕上。我们的屏幕显示的就是颜色缓冲区中的颜色值。但是,为了避免我们看到那些正在进行光栅化的图元,GPU会使用双重缓冲(Double Buffering)的策略。这意味着,对场景的渲染时在幕后发生的,即在后置缓冲(Back Buffer)中。一旦场景已经被渲染到了后置缓冲中,GPU就会交换后置缓冲区和前置缓冲区(Front Buffer)中的内容,而前置缓冲区是之前显示在屏幕上的图像。由此,保证了我们看到的图像总是连续的。图4.1说明了这一过程。
我们首先渲染缓冲区B,它是当前的后台缓冲区。一旦帧渲染完成,前后缓冲区的指针会相互交换,缓冲区B会变为前台缓冲区,而缓冲区A会变为新的后台缓冲区。之后,我们将在缓冲区A中进行下一帧的渲染。一旦帧渲染完成,前后缓冲区的指针会再次进行交换,缓冲区A会变为前台缓冲区,而缓冲区B会再次变为后台缓冲区。
前后缓冲区形成了一个交换链(swap chain)。在Direct3D中,交换链由IDXGISwapChain接口表示。该接口保存了前后缓冲区纹理,并提供了用于调整缓冲区尺寸的方法(IDXGISwapChain::ResizeBuffers)和呈现方法(IDXGISwapChain::Present)。我们会在4.4节中详细讨论些方法。
使用(前后)两个缓冲区称为双缓冲(double buffering)。缓冲区的数量可多于两个;比如,当使用三个缓冲区时称为三缓冲(triple buffering)。不过,两个缓冲区已经足够用了。
注意:虽然后台缓冲区是一个纹理(纹理元素称为texel),但是我们更习惯于将纹理元素称为像素(pixel),因为后台缓冲区存储的是颜色信息。有时,即使纹理中存储的不是颜色信息,人们还是会将纹理元素称为像素(例如,“法线贴图像素”)。
但是创建交换链还要做一些准备工作。
①描述交换链
要创建交换链,首先需要填充一个DXGI_SWAP_CHAIN_DESC1结构体来描述我们将要创建的交换链的特性。该结构体的定义如下。
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(
_In_ IUnknown *pDevice, // 1.D3D设备
_In_ HWND hWnd, // 2.窗体程序句柄
_In_ const DXGI_SWAP_CHAIN_DESC1 *pDesc, // 3.传入交换链描述
_In_opt_ const DXGI_SWAP_CHAIN_FULLSCREEN_DESC *pFullscreenDesc, // 4.全屏运行用的交换链描述
_In_opt_ IDXGIOutput *pRestrictToOutput, // 5.传空指针就行
_COM_Outptr_ IDXGISwapChain1 **ppSwapChain // 接收创建的交换链
显然易见,我们得先得到IDXGIFactory2的对象,然后再调用这个方法。
之前在创建D3D设备时使用的是默认的显卡适配器IDXGIAdapter(对于双显卡的笔记本大概率使用的是集成显卡),而创建出来的D3D设备本身实现了IDXGIDevice接口,通过该对象,我们可以获取到当前所用的显卡适配器IDXGIAdapter对象,这样我们再通过查询它的父级找到是哪个IDXGIFactory枚举出来的适配器。
具体什么意思呢?就是先得到D3D对象保存的适配器,然后通过HR(dxgiAdapter->GetParent(__uuidof(IDXGIFactory1), reinterpret_cast<void**>(dxgiFactory1.GetAddressOf())));
得到IDXGIFactory2对象。
那么IDXGIFactory2,IDXGIAdapter,IDXGIDevice之间到底是什么关系呢???GetParent意义是什么???
先看一下StackOverflow上关于创建交换链的一些解释。
这段代码是为了用DX11或之后的版本的接口创建交换链,同时这是特别为确保你使用的DXGI factor实例就是你创建D3D设备所使用的factory实例而开发的。
基本上,当你第一次创建D3D11设备时,你可以选择提供一个IDXGIAdapter
适配器对象来使用。大多数都用一个空指针让D3D设备自动指定一个默认的适配器了。为了完成交换链的初始化,然而,你必须得有一个DXGI factory对象。理论上你可以用DXGICreateFactory1
来创建一个,但是你很容易搞砸而调用了传入错误枚举值的DXGICreateFactory
或DXGICreateFactory2
。
相反,最安全的方法是从你的ID3D11Device
中得到IDXGIDevice
。使用标准COM组件的IUnknown::QueryInterface:
。IDXGIDevice * dxgiDevice = 0; HRESULT hr = mD3dDevice->QueryInterface( __uuidof( IDXGIDevice ),( void ** ) & dxgiDevice ); if ( SUCCEEDED(hr) )
接着从
IDXGIDevice
对象中使用IDXGIObject::GetParent
得到IDXGIAdapter
的适配器对象。IDXGIAdapter * dxgiAdapter = 0; hr = dxgiDevice->GetParent( __uuidof( IDXGIAdapter ),( void ** ) & dxgiAdapter ); if ( SUCCEEDED(hr) )
然后再从
IDXGIAdapter
对象中再次使用IDXGIObject::GetParent
IDXGIFactory * dxgiFactory = 0; hr = dxgiAdapter->GetParent( __uuidof( IDXGIFactory ),( void ** ) & dxgiFactory ); if ( SUCCEEDED(hr) )
现在你就得到了与你的D3D设备关联的
IDXGIFactory
,不管你之前是怎么创建的D3D设备。一定记得COM引用计数器的意思是你现在得到了所有的这些引用还得释放掉。dxgiFactory->Release(); dxgiAdapter->Release(); dxgiDevice->Release();
注意
IDXGIFactory::CreateSwapChain
是DX11.0创建交换链的方法,而且D3D11CreateDeviceAndSwapChain
方法的结果类似,而D3D11CreateDevice
就不是。对于DX11.1或以后的版本,你最好用IDXGIFactory2::CreateSwapChainForHwnd
,如果不是Win32桌面应用的话。对于Windows商店应用,Windows phone 8和XBOX One,你可以一直用IDXGIFactory2::CreateSwapChainForCoreWindow
。
对于Win32桌面应用,你可以用以下的代码。IDXGIFactory2* dxgiFactory2 = 0; hr = dxgiFactory->QueryInterface( __uuidof(IDXGIFactory2), reinterpret_cast<void**>(&dxgiFactory2) ); if ( SUCCEEDED(hr) ) { // This system has DirectX 11.1 or later installed, so >we can use this interface dxgiFactory2->CreateSwapChainForHwnd( /* >parameters */ ); dxgiFactory2->Release(); } else { // This system only has DirectX 11.0 installed dxgiFactory->CreateSwapChain( /* parameters */ >