Direct3D11学习:(三)Direct3D11初始化

时间:2022-10-11 15:38:54

转载请注明出处:http://www.cnblogs.com/Ray1024

一、概述

做完一系列的准备工作之后,我们就正式进入Direct3D11的学习了。我们就从Direct3D11的初始化工作开始我们的学习之路。

这篇文章主要介绍了在一个空的Win32程序中,从头开始D3D11的初始化过程。

二、D3D11的初始化步骤

2.1 创建设备(Device)和上下文(Context)

要初始化D3D11,首先需要创建D3D11设备(ID3D11Device)和上下文(ID3D11DeviceContext)。它们是是最重要的DD接口,可以被看成是物理图形设备硬件的软控制器;也就是说,我们可以通过该接口与硬件进行交互,命令硬件完成一些工作(比如:在显存中分配资源、清空后台缓冲区、将资源绑定到各种管线阶段、绘制几何体)。具体而言:

  a.ID3D11Device接口用于检测显示适配器功能和分配资源。

  b.ID3D11DeviceContext接口用于设置管线状态、将资源绑定到图形管线和生成渲染命令。

设备和上下文可用如下函数创建:

HRESULT  D3D11CreateDevice (
IDXGIAdapter *pAdapter,
D3D_DRIVER_TYPE DriverType,
HMODULE Software ,
UINT Flags ,
CONST D3D_FEATURE_LEVEL *pFeatureLevels ,
UINT FeatureLevels ,
UINT SDKVersion,
ID3D11Device **ppDevice ,
D3D_FEATURE_LEVE L *pFeatureLevel,
ID3D11DeviceContext **ppImmediateContext
); 参数分析:
pAdapter 指定要为哪个物理显卡创建设备对象。为NULL时,表示使用主显卡;
DriverType 设置驱动类型,一般选择硬件加速D3D_DRIVER_TYPE_HARDWARE,此时下一个参数就是NULL;
Flags 为可选参数,一般为NULL,可以设为D3D11_CREATE_DEVICE_DEBUG或D3D11_CREATE_DEVICE_SINGLETHREADED,
或两者一起,前者让要用于调试时收集信息,后者在确定程序只在单线程下运行时设置为它,可以提高性能;
pFeatureLevels 为我们提供给程序的特征等级的一个数组,下一个参数为数组中元素个数;
SDKVersion 恒定为D3D11_SDK_VERSION;
ppDevice 为设备指针的地址,注意设备是指针类型,这里传递的是指针的地址(二维指针,d3d程序中所有的接口都声明为指针类型!);
pFeatureLevel 为最后程序选中的特征等级,我们定义相应的变量,传递它的地址进来;
ppImmediateContext 为设备上下文指针的地址,要求同设备指针。

使用此函数创建设备的代码示例:

UINT createDeviceFlags = 0;

#if  defined(DEBUG)||defined(_DEBUG)
createDeviceFlags |= D3D11_CREATE_DEVICE_DEBUG;
#endif D3D_FEATURE_LEVEL featureLevel;
ID3D11Device * md3dDevice;
ID3D11Device Context* md3dImmediate Context;
HRESULT hr = D3D11CreateDevice(
0, // 默认显示适配器
D3D_DRIVER_TYPE_HARDWARE ,
0, // 不使用软件设备
createDeviceFlags ,
0, 0, // 默认的特征等级数组
D3D11_SDK_VERSION,
& md3dDevice ,
& featureLevel,
& md3dImmediateContext);
if(FAILED(hr) )
{
MessageBox(0, L"D3D11CreateDevice Failed.", 0, 0);
return false ;
}
if(featureLevel != D3D_FEATURE_LEVEL_11_0)
{
MessageBox(0, L"Direct3D FeatureLevel 11 unsupported.", 0, 0);
return false;
}

上下文有两种:立即执行上下文(immediate context)和延迟执行上下文(ID3D11Device::CreateDeferredContext),这里我们使用立即执行上下文。延迟执行上下文主要用于D3D11支持的多线程编程。

2.2 检测4X多重采样质量支持

创建了设备后,我们就可以检查4X多重采样质量等级了。所有支持D3D11的设备都支持所有渲染目标格式的4X MSAA(支持的质量等级可能并不相同)。

UINT  m4xMsaaQuality;
HR(md3dDevice ->CheckMultisampleQualityLevels(DXGI_FORMAT_R8G8B8A8_UNORM, 4, & m4xMsaaQuality));
assert(m4xMsaaQuality > 0);

因为4X MSAA总是被支持的,所以返回的质量等级总是大于0。

2.3 描述交换链

下一步是创建交换链,首先需要填充一个DXGI_SWAP_CHAIN_DESC结构体来描述我们将要创建的交换链的特性。该结构体的定义如下:

typedef struct DXGI_MODE_DESC
{
UINT Width; // 后台缓冲区宽度
UINT Height; // 后台缓冲区高度
DXGI_RATIONAL RefreshRate; // 显示刷新率
DXGI_FORMAT Format; // 后台缓冲区像素格式
DXGI_MODE_SCANLINE_ORDER ScanlineOrdering; // display scanline mode
DXGI_MODE_SCALING Scaling; // display scaling mode
} DXGI_MODE_DESC; typedef struct DXGI_SWAP_CHAIN_DESC {
DXGI_MODE_DESC BufferDesc; // 上面介绍的结构体类型,描述了我们所要创建的后台缓冲区的属性
DXGI_SAMPLE_DESC SampleDesc; // 多重采样数量和质量级别
DXGI_USAGE BufferUsage; // 对于交换链,为DXGI_USAGE_RENDER_TARGET_OUTPUT
UINT BufferCount; // 交换链中的后台缓冲区数量;我们一般只用一个后台缓冲区来实现双缓存。当然,使用两个后台缓冲区就可以实现三缓存
HWND OutputWindow; // 将要渲染到的窗口的句柄
BOOL Windowed; // 当设为true时,程序以窗口模式运行;当设为false时,程序以全屏(full-screen)模式运行
DXGI_SWAP_EFFECT SwapEffect; // 设为DXGI_SWAP_EFFECT_DISCARD,让显卡驱动程序选择最高效的显示模式
UINT Flags; // 可选,通常设为0
} DXGI_SWAP_CHAIN_DESC;

描述交换链的DXGI_SWAP_CHAIN_DESC结构体示例代码:

DXGI_SWAP_CHAIN_DESC 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;
// 是否使用4X MSAA?
if(mEnable4xMsaa)
{
sd.SampleDesc.Count = 4;
// m4xMsaaQuality是通过CheckMultisampleQualityLevels()方法获得的
sd.SampleDesc.Quality = m4xMsaaQuality-1;
}
// NoMSAA
else
{
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;

注意:如果需要在运行时改变多重采样的设置,那么必须销毁然后重新创建交换链。

注意:因为大多数显示器不支持超过24位以上的颜色,再多的颜色也是浪费,所以我们将后台缓冲区的像素格式设置为DXGI_FORMAT_R8G8B8A8_UNORM(红、绿、蓝、alpha各8位)。额外的8位alpha并不会输出在显示器上,但在后台缓冲区中可以用于特定的用途。

2.4 创建交换链

交换链(IDXGISwapChain)是通过IDXGIFactory实例的IDXGIFactory::CreateSwapChain方法创建的:

HRESULT IDXGIFactory::CreateSwapChain(
IUnknown *pDevice , // 指向ID3D11Device的指针
DXGI_SWAP_CHAIN_DESC *pDesc, // 指向一个交换链描述的指针
IDXGISwapChain **ppSwapChain); // 返回创建后的交换链

我们可以通过CreateDXGIFactory(需要链接dxgi.lib)获取指向一个IDXGIFactory实例的指针。但是使用这种方法获取IDXGIFactory实例,并调用IDXGIFactory::CreateSwapChain方法后,会出现如下的错误信息:

DXGI Warning: IDXGIFactory::CreateSwapChain: This function is being called with a device from a different IDXGIFactory. 

要避免这个错误,我们需要使用创建设备的那个IDXGIFactory实例,要获得这个实例,必须使用下面的COM查询(具体解释可参见IDXGIFactory的文档):

IDXGIDevice *  dxgiDevice  =  0;
HR(md3dDevice ->QueryInterface(__uuidof(IDXGIDevice),
(void**)&dxgiDevice ));
IDXGIAdapter* dxgiAdapter = 0;
HR(dxgiDevice ->GetParent(__uuidof(IDXGIAdapter),
(void**))&dxgiAdapte r ));
// 获得IDXGIFactory 接口
IDXGIFactory* dxgiFactory = 0;
HR(dxgiAdapter->GetParent(__uuid of(IDXGIFactory),
(void**))&dxgiFactor y));
// 现在,创建交换链
IDXGISwapChain* mSwapChain;
HR(dxgiFactory->CreateSwapChain(md3dDevice, &sd , &mSw ap Chain);
// 释放COM接口
ReleaseCOM (dxgiDevice ;
ReleaseCOM (dxgiAdapter);
ReleaseCOM (dxgiFactory);

注意:也可以使用D3D11CreateDeviceAndSwapChain方法同时创建设备、设备上下文和交换链。

注意:DXGI(DirectX Graphics Inf rastructure)是独立于Direct3D的API,用于处理与图形关联的东西,例如交换链等。DXGI与Direct3D分离的目的在于其他图形API(例如Direct2D)也需要交换链、图形硬件枚举、在窗口和全屏模式之间切换,通过这种设计,多个图形API都能使用DXGI API。

2.5 创建渲染目标视图

我们必须为资源创建资源视图,然后把资源视图绑定到不同的管线阶段。尤其是在把后台缓冲区绑定到管线的输出合并器阶段时(使Direct3D可以在后台缓冲区上执行渲染工作),我们必须为后台缓冲区创建一个渲染目标视图(render target view)。下面的代码说明了创建渲染目标视图过程:

ID3D11RenderTargetView* mRenderTargetView;
ID3D11Texture2D* backBuffer;
// 获取一个交换链的后台缓冲区指针
mSwapChain->GetBuffer(0,__uuidof(ID3D11Texture2D), reinterpret_cast<void**>(&backBuffer));
// 创建渲染目标视图
md3dDevice->CreateRenderTargetView(backBuffer, 0, &mRenderTargetView);
// 每调用一次GetBuffer方法,后台缓冲区的COM引用计数就会递增一次。我们需要在使用完之后释放它
ReleaseCOM(backBuffer);

2.6 创建深度/模板缓冲区及视图

创建缓冲区要即创建一个2维纹理,ID3D11Texture2D,创建它需要先给出描述D3D11_TEXTURE2D_DESC。定义如下:

typedef struct D3D11_TEXTURE2D_DESC {
UINT Width; // 纹理的宽度,单位为纹理元素(texel)
UINT Height; // 纹理的高度,单位为纹理元素(texel)
UINT MipLevels; // 多级渐近纹理层(mipmap level)的数量。对于深度/模板缓冲区来说,只需要一个多级渐近纹理层
UINT ArraySize; // 在纹理数组中的纹理数量。对于深度/模板缓冲区来说,我们只需要一个纹理
DXGI_FORMAT Format; // 数据格式。一般为DXGI_FORMAT_D24_UNORM_S8_UINT,24位用于深度,8位用于模板
DXGI_SAMPLE_DESC SampleDesc; // 多重采样数量和质量级别
D3D10_USAGE Usage; // 表示纹理用途的D3D11_USAGE枚举类型成员。默认为D3D11_USAGE_DEFAULT,表示GPU对资源进行读写操作
UINT BindFlags; // 指定该资源将会绑定到管线的哪个阶段。对于深度/模板缓冲区,该参数应设为D3D11_BIND_DEPTH_STENCIL
UINT CPUAccessFlags; // 指定CPU对资源的访问权限。对于深度/模板缓冲区来说,该参数设为0
UINT MiscFlags; // 可选的标志值,与深度/模板缓冲区无关,所以设为0
} D3D11_TEXTURE2D_DESC;

在本书中,我们会看到以各种不同选项来创建资源的例子;例如,使用不同的Usage标志值、绑定标志值和CPU访问权限标志值。但就目前来说,我们只需要关心那些与创建深度/模板缓冲区有关的标志值即可,其他选项可以以后再说。

另外,在使用深度/模板缓冲区之前,我们必须为它创建一个绑定到管线上的深度/模板视图。过程与创建渲染目标视图的过程相似。下面的代码示范了如何创建深度/模板纹理以及与它对应的深度/模板视图:

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?——必须与交换链的MSAA的值匹配
if( mEnable4xMsaa)
{
depthStencilDesc.SampleDesc.Count = 4;
depthStencilDesc.SampleDesc.Quality= m4xMsaaQuality-1;
}
// 不使用MSAA
else
{
depthStencilDesc.SampleDesc.Count = 1;
depthStencilDesc.SampleDesc.Quality = 0;
}
depthStencilDesc.Usage = D3D10_USAGE_DEFAULT;
depthStencilDesc.BindFlags = D3D10_BIND_DEPTH_STENCIL;
depthStencilDesc.CPUAccessFlags = 0;
depthStencilDesc.MiscFlags = 0;
ID3D10Texture2D* mDepthStencilBuffer;
ID3D10DepthStencilView* mDepthStencilView; HR(md3dDevice->CreateTexture2D(
&depthStencilDesc,
0, // 一个指向初始化数据的指针,用来填充纹理。对于深度/模板缓冲区,不需要为它填充任何初始化数据
// 当执行深度缓存和模板操作时,Direct3D会自动向深度/模板缓冲区写入数据
&mDepthStencilBuffer
)); HR(md3dDevice->CreateDepthStencilView(
mDepthStencilBuffer,
0, // 描述资源元素数据类型(格式)。如果资源是一个有类型的格式(非typeless),这个参数可以为空值,
// 表示创建一个资源的第一个mipmap等级的视图(深度/模板缓冲也只能使用一个 mipmap等级)。因为我们指定了深度/模板缓冲的格式,所以将这个参数设置为空值。
&mDepthStencilView
));

2.7 将视图绑定到输出合并器阶段

现在我们已经为后台缓冲区和深度缓冲区创建了视图,就可以将些视图绑定到管线的输出合并器阶段(output merger stage),使些资源成为管线的渲染目标和深度/模板缓冲区:

md3dImmediateContext->OMSetRenderTargets(
1,&mRenderTargetView,mDepthStencilView);

第一个参数是我们将要绑定的渲染目标的数量;我们在这里仅绑定了一个渲染目标,不过该参数可以为着色器同时绑定多个渲染目标(是一项高级技术)。第二个参数是我们将要绑定的渲染目标视图数组中的第一个元素的指针。第三个参数是将要绑定到管线的深度/模板视图。

注意:我们可以设置一组渲染目标视图,但是只能设置一个深度/模板视图。使用多个渲染目标是一项高级技术,会在之后加以介绍。

2.8 设置视口

通常我们会把3D场景渲染到整个后台缓冲区上,但也可以把3D场景渲染到后台缓冲区的一个子矩形区域中。

我们将后台缓冲区的子矩形区域称为视口(viewport),它由如下结构体描述:

typedef struct D3D11_VIEWPORT {
FLOAT TopLeftX; // 视口左上角坐标x值
FLOAT TopLeftY; // 视口左上角坐标y值
FLOAT Width; // 视口宽度
FLOAT Height; // 视口高度
FLOAT MinDepth; // 深度缓冲区最小值(0.f~1.f),默认0.f
FLOAT MaxDepth; // 深度缓冲区最大值(0.f~1.f),默认1.f
} D3D11_VIEWPORT;

在填充了D3D11_VIEWPORT结构体之后,我们可以使用ID3D11Device::RSSetViewports方法设置Direct3D的视口。下面的例子创建和设置了一个视口,该视口与整个后台缓冲区的大小相同:

D3D11_VIEWPORT vp;
vp.TopLeftX = 0;
vp.TopLeftY = 0;
vp.Width = static_cast<float>(mClientWidth);
vp.Height = static_cast<float>(mClientHeight);
vp.MinDepth = 0.0f;
vp.MaxDepth = 1.0f; md3dImmediateContext-->RSSetViewports(
1, // 绑定的视图的数量(可以使用超过1的数量用于高级的效果)
&vp // 指向一个viewports的数组
);

2.9 绘制

到了这个位置,D3D11初始化工作就完成了。在窗口绘制函数中,使用D3D11的函数绘制窗口背景。

代码如下:

void Render()
{
// 绘制青色背景
XMVECTORF32 color = {0.f, 1.f, 1.f, 1.0f};
g_deviceContext->ClearRenderTargetView(g_renderTargetView,reinterpret_cast<float*>(&color));
g_deviceContext->ClearDepthStencilView(g_depthStencilView,D3D11_CLEAR_DEPTH|D3D11_CLEAR_STENCIL,1.f,0); // 正式的场景绘制工作 // 显示
g_swapChain->Present(0,0);
}

显示效果如下:

Direct3D11学习:(三)Direct3D11初始化

在这里完整代码代码就不贴出了,有兴趣的朋友可以点击此处下载Demo源码,Demo源码是1_D3DInit文件。

三、结语

D3D11的整个初始化过程就结束了。

我们在这篇文章中,我们从一个空的Win32项目开始,将D3D11的整个初始化过程放到了程序中,一步一步搭建出来一个空白窗口程序。这就是一个最简单的、完整的D3D程序了。

在之后的学习中,我们将在这个最简单的D3D窗口程序中,一步一步添加新代码,由简入繁地实现越来越漂亮的场景。