掌握了矩阵向量运算后,接下来我们要做的事情利用D3D12图形库API编程,也就是我们通常说的初始化D3D12,D3D12的初始化工作与以前的D3D9是完全不一样的,D3D12做了大幅的升级。但是每个D3D图形API都有自己的初始化流程,大家只要记住这个流程,学习起来就比较容易,就跟生产车架的流水线作业一样的原理,先做什么后做什么。其他的内容向里面添加就可以了,我们先聊聊D3D12的初始化流程,然后再编程实现。
1、创建窗体
2、创建ID3D12Device设备
3、创建 ID3D12Fence用于查询descriptor 大小
4、检查设备是否支持4X MSAA
5、创建指令队列,指令列表和主指令列表。
6、创建交换链
7、创建描述符堆(descriptor heaps)
8、创建渲染目标视图。
9、创建深度/模板缓冲区及其关联的深度/模板视图。
10、设置视口
共分为10步,严格来说,第一步不属于D3D12初始化里面的内容,因为我们要显示D3D12中的内容,必须要要有窗体作为载体,在这里就把他们加进来了,我们下面就按照这10步去编写程序实现我们的D3D12的初始化工作。
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"MainWnd";
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"MainWnd", 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;
}
这个在DX9或者MFC中都会用到,这里就不介绍了,接下来开始进的入D3D12编程的正题了。
CreateDXGIFactory1(IID_PPV_ARGS(&mdxgiFactory))
其中 IID_PPV_ARGS 宏用来返回 COM 接口的 ID
接下来调用D3D12CreateDevice函数,代码实现如下:
HRESULT hardwareResult = D3D12CreateDevice(
nullptr, // default adapter
D3D_FEATURE_LEVEL_11_0,
IID_PPV_ARGS(&md3dDevice));
if(FAILED(hardwareResult))
{
ComPtr<IDXGIAdapter> pWarpAdapter;
ThrowIfFailed(mdxgiFactory->EnumWarpAdapter(IID_PPV_ARGS(&pWarpAdapter)));
ThrowIfFailed(D3D12CreateDevice(
pWarpAdapter.Get(),
D3D_FEATURE_LEVEL_11_0,
IID_PPV_ARGS(&md3dDevice)));
}
if条件语句内部就是枚举适配器的代码,设备创建好了以后,下面就是创建Fence了。
ThrowIfFailed(md3dDevice->CreateFence(0, D3D12_FENCE_FLAG_NONE,
IID_PPV_ARGS(&mFence)));
mRtvDescriptorSize = md3dDevice->GetDescriptorHandleIncrementSize(D3D12_DESCRIPTOR_HEAP_TYPE_RTV);
mDsvDescriptorSize = md3dDevice->GetDescriptorHandleIncrementSize(D3D12_DESCRIPTOR_HEAP_TYPE_DSV);
mCbvSrvUavDescriptorSize = md3dDevice->GetDescriptorHandleIncrementSize(D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV);
D3D12_FEATURE_DATA_MULTISAMPLE_QUALITY_LEVELS msQualityLevels;
msQualityLevels.Format = mBackBufferFormat;
msQualityLevels.SampleCount = 4;
msQualityLevels.Flags = D3D12_MULTISAMPLE_QUALITY_LEVELS_FLAG_NONE;
msQualityLevels.NumQualityLevels = 0;
ThrowIfFailed(md3dDevice->CheckFeatureSupport(
D3D12_FEATURE_MULTISAMPLE_QUALITY_LEVELS,
&msQualityLevels,
sizeof(msQualityLevels)));
m4xMsaaQuality = msQualityLevels.NumQualityLevels;
assert(m4xMsaaQuality > 0 && "Unexpected MSAA quality level.");
void D3DApp::CreateCommandObjects()
{
D3D12_COMMAND_QUEUE_DESC queueDesc = {};
queueDesc.Type = D3D12_COMMAND_LIST_TYPE_DIRECT;
queueDesc.Flags = D3D12_COMMAND_QUEUE_FLAG_NONE;
ThrowIfFailed(md3dDevice->CreateCommandQueue(&queueDesc, IID_PPV_ARGS(&mCommandQueue)));
ThrowIfFailed(md3dDevice->CreateCommandAllocator(
D3D12_COMMAND_LIST_TYPE_DIRECT,
IID_PPV_ARGS(mDirectCmdListAlloc.GetAddressOf())));
ThrowIfFailed(md3dDevice->CreateCommandList(
0,
D3D12_COMMAND_LIST_TYPE_DIRECT,
mDirectCmdListAlloc.Get(), // Associated command allocator
nullptr, // Initial PipelineStateObject
IID_PPV_ARGS(mCommandList.GetAddressOf())));
// Start off in a closed state. This is because the first time we refer
// to the command list we will Reset it, and it needs to be closed before
// calling Reset.
mCommandList->Close();
}
注意,对于CreateCommandList,我们为管道状态对象参数指定null。 在本篇博客的示例程序中,我们不发出任何绘图命令,因此我们不需要有效的管道状态对象。
mSwapChain.Reset();
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 = mBackBufferFormat;
sd.BufferDesc.ScanlineOrdering = DXGI_MODE_SCANLINE_ORDER_UNSPECIFIED;
sd.BufferDesc.Scaling = DXGI_MODE_SCALING_UNSPECIFIED;
sd.SampleDesc.Count = m4xMsaaState ? 4 : 1;
sd.SampleDesc.Quality = m4xMsaaState ? (m4xMsaaQuality - 1) : 0;
sd.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT;
sd.BufferCount = SwapChainBufferCount;
sd.OutputWindow = mhMainWnd;
sd.Windowed = true;
sd.SwapEffect = DXGI_SWAP_EFFECT_FLIP_DISCARD;
sd.Flags = DXGI_SWAP_CHAIN_FLAG_ALLOW_MODE_SWITCH;
在D3D12中的实现效果如下所示:
利用两个缓冲区(前后缓冲)组成交换链的方式叫做双重缓冲(double buffering),也就是说,交换链不是必须要用两个缓冲区才可以,也可以用两个数量以上的缓冲,但是通常情况下两个就已经绰绰有余了。
下面我们介绍这个结构体中的一些常用的属性:
BufferDesc,此结构描述了我们要创建的后台缓冲区的属性, 我们关注的主要属性是宽度和高度,以及像素格式; 其他可以查看帮助文档。
SampleDesc,多重采样的数量和质量等级; 对于单次采样,请指定样本计数为1,质量等级为0。
BufferUsage,指定DXGI_USAGE_RENDER_TARGET_OUTPUT,因为我们将渲染到后台缓冲区(即,将其用作渲染目标)。
BufferCount,交换链中使用的缓冲区数量; 为双缓冲指定两个。
OutputWindow,我们正在渲染的窗口的句柄。
Windowed,指定true以在窗口模式下运行,或指定为全屏模式时为false。
SwapEffect,指定DXGI_SWAP_EFFECT_FLIP_DISCARD。
Flags,可选标志, 如果指定DXGI_SWAP_CHAIN_FLAG_ALLOW_MODE_SWITCH,则当应用程序切换到全屏模式时,它将选择与当前应用程序窗口尺寸最匹配的显示模式。 如果未指定此标志,则当应用程序切换到全屏模式时,它将使用当前桌面显示模式。
在我们描述了交换链之后,我们可以使用IDXGIFactory :: CreateSwapChain方法创建它:
ThrowIfFailed(mdxgiFactory->CreateSwapChain(
mCommandQueue.Get(),
&sd,
mSwapChain.GetAddressOf()));
void D3DApp::CreateRtvAndDsvDescriptorHeaps()
{
D3D12_DESCRIPTOR_HEAP_DESC rtvHeapDesc;
rtvHeapDesc.NumDescriptors = SwapChainBufferCount;
rtvHeapDesc.Type = D3D12_DESCRIPTOR_HEAP_TYPE_RTV;
rtvHeapDesc.Flags = D3D12_DESCRIPTOR_HEAP_FLAG_NONE;
rtvHeapDesc.NodeMask = 0;
ThrowIfFailed(md3dDevice->CreateDescriptorHeap(
&rtvHeapDesc, IID_PPV_ARGS(mRtvHeap.GetAddressOf())));
D3D12_DESCRIPTOR_HEAP_DESC dsvHeapDesc;
dsvHeapDesc.NumDescriptors = 1;
dsvHeapDesc.Type = D3D12_DESCRIPTOR_HEAP_TYPE_DSV;
dsvHeapDesc.Flags = D3D12_DESCRIPTOR_HEAP_FLAG_NONE;
dsvHeapDesc.NodeMask = 0;
ThrowIfFailed(md3dDevice->CreateDescriptorHeap(
&dsvHeapDesc, IID_PPV_ARGS(mDsvHeap.GetAddressOf())));
}
在我们的实现中,我们定义了两个变量:
static const int SwapChainBufferCount = 2;
int mCurrBackBuffer = 0;
我们使用mCurrBackBuffer跟踪当前的后台缓冲区索引(回想一下前页和后台缓冲区在页面翻转中交换,所以我们需要跟踪哪个缓冲区是当前的后台缓冲区,以便我们知道要渲染哪个缓冲区)。
在我们创建堆之后,我们需要能够访问它们存储的描述符,通过句柄引用描述符, 使用ID3D12DescriptorHeap :: GetCPUDescriptorHandleForHeapStart方法获取堆中第一个描述符的句柄, 以下函数分别获取当前后台缓冲区RTV和DSV:
D3D12_CPU_DESCRIPTOR_HANDLE D3DApp::CurrentBackBufferView()const
{
return CD3DX12_CPU_DESCRIPTOR_HANDLE(
mRtvHeap->GetCPUDescriptorHandleForHeapStart(),
mCurrBackBuffer,
mRtvDescriptorSize);
}
D3D12_CPU_DESCRIPTOR_HANDLE D3DApp::DepthStencilView()const
{
return mDsvHeap->GetCPUDescriptorHandleForHeapStart();
}
HRESULT IDXGISwapChain::GetBuffer(
UINT Buffer,
REFIID riid,
void **ppSurface);
Buffer,标识我们想要获取的特定后台缓冲区的索引(如果有多个)。
riid,我们想要获取指针的ID3D12Resource接口的COM ID。
ppSurface,返回指向表示后台缓冲区的ID3D12Resource的指针。
对IDXGISwapChain :: GetBuffer的调用会将COM引用计数增加到后台缓冲区,因此我们必须在完成后释放它, 如果使用ComPtr,则会自动完成此操作。
要创建渲染目标视图,我们使用ID3D12Device :: CreateRenderTargetView方法:
void ID3D12Device::CreateRenderTargetView(
ID3D12Resource *pResource,
const D3D12_RENDER_TARGET_VIEW_DESC *pDesc,
D3D12_CPU_DESCRIPTOR_HANDLE DestDescriptor);
pResource,指定将用作渲染目标的资源,在上面的示例中,它是后台缓冲区(即,我们正在为后台缓冲区创建渲染目标视图)。
pDesc,指向D3D12_RENDER_TARGET_VIEW_DESC的指针, 除此之外,该结构描述了资源中元素的数据类型(格式), 如果资源是使用类型化格式(即非无类型)创建的,则此参数可以为null,这表示创建此资源的第一个mipmap级别的视图(后台缓冲区只有一个mipmap级别),格式为 资源创建的, 因为我们指定了后台缓冲区的类型,所以我们为这个参数指定了null。
DestDescriptor,处理将存储创建的渲染目标视图的描述符。
下面是调用这两种方法的示例,其中我们为交换链中的每个缓冲区创建一个RTV:
ComPtr<ID3D12Resource> mSwapChainBuffer[SwapChainBufferCount];
CD3DX12_CPU_DESCRIPTOR_HANDLE rtvHeapHandle(
mRtvHeap->GetCPUDescriptorHandleForHeapStart());
for (UINT i = 0; i < SwapChainBufferCount; i++)
{
// Get the ith buffer in the swap chain.
ThrowIfFailed(mSwapChain->GetBuffer(
i, IID_PPV_ARGS(&mSwapChainBuffer[i])));
// Create an RTV to it.
md3dDevice->CreateRenderTargetView(
mSwapChainBuffer[i].Get(), nullptr, rtvHeapHandle);
// Next entry in heap.
rtvHeapHandle.Offset(1, mRtvDescriptorSize);
}
typedef struct D3D12_RESOURCE_DESC
{
D3D12_RESOURCE_DIMENSION Dimension;
UINT64 Alignment;
UINT64 Width;
UINT Height;
UINT16 DepthOrArraySize;
UINT16 MipLevels;
DXGI_FORMAT Format;
DXGI_SAMPLE_DESC SampleDesc;
D3D12_TEXTURE_LAYOUT Layout;
D3D12_RESOURCE_MISC_FLAG MiscFlags;
} D3D12_RESOURCE_DESC;
Dimension,资源的维度,它是以下枚举类型之一:
enum D3D12_RESOURCE_DIMENSION
{
D3D12_RESOURCE_DIMENSION_UNKNOWN = 0,
D3D12_RESOURCE_DIMENSION_BUFFER = 1,
D3D12_RESOURCE_DIMENSION_TEXTURE1D = 2,
D3D12_RESOURCE_DIMENSION_TEXTURE2D = 3,
D3D12_RESOURCE_DIMENSION_TEXTURE3D = 4
} D3D12_RESOURCE_DIMENSION;
Width,纹理的宽度,以纹素为单位。 对于缓冲区资源,这是缓冲区中的字节数。
Height,纹理的高度,以纹素为单位。
DepthOrArraySize,纹理的纹理深度,或纹理数组大小(对于1D和2D纹理)。
MipLevels,mipmap等级的数量,为了创建深度/模板缓冲,我们的纹理只需要一个mipmap级别。
Format,指定文本格式的DXGI_FORMAT枚举类型的成员,深度/模板缓冲区也需要一个格式;
SampleDesc,多重采样数和质量等级, 回想一下,4X MSAA使用比屏幕分辨率大4倍的后缓冲和深度缓冲,以便存储每个子像素的颜色和深度/模板信息。 因此,用于深度/模板缓冲区的多重采样设置必须与用于渲染目标的设置相匹配。
Layout,D3D12_TEXTURE_LAYOUT枚举类型的成员,指定纹理布局。 目前,我们不必担心布局,可以指定D3D12_TEXTURE_LAYOUT_UNKNOWN。
MiscFlags,其它资源标志, 对于深度/模板缓冲区资源,请指定D3D12_RESOURCE_MISC_DEPTH_STENCIL。
GPU资源存在于堆中,这些GPU本质上是具有某些属性的GPU内存块。 ID3D12Device :: CreateCommittedResource方法使用我们指定的属性创建并向特定堆提交资源。
HRESULT ID3D12Device::CreateCommittedResource(
const D3D12_HEAP_PROPERTIES *pHeapProperties,
D3D12_HEAP_MISC_FLAG HeapMiscFlags,
const D3D12_RESOURCE_DESC *pResourceDesc,
D3D12_RESOURCE_USAGE InitialResourceState,
const D3D12_CLEAR_VALUE *pOptimizedClearValue,
REFIID riidResource,
void **ppvResource);
typedef struct D3D12_HEAP_PROPERTIES {
D3D12_HEAP_TYPE Type;
D3D12_CPU_PAGE_PROPERTIES CPUPageProperties;
D3D12_MEMORY_POOL MemoryPoolPreference;
UINT CreationNodeMask;
UINT VisibleNodeMask;
} D3D12_HEAP_PROPERTIES;
pHeapProperties,我们要将资源提交到的堆的属性, 其中一些属性用于高级用法, 目前,我们需要担心的主要属性是D3D12_HEAP_TYPE,它可以是D3D12_HEAP_PROPERTIES枚举类型的以下成员之一:
struct D3D12_CLEAR_VALUE
{
DXGI_FORMAT Format;
union
{
FLOAT Color[ 4 ];
D3D12_DEPTH_STENCIL_VALUE DepthStencil;
};
} D3D12_CLEAR_VALUE;
riidResource,我们想要获取指针的ID3D12Resource接口的COM ID。
ppvResource,返回指向ID3D12Resource的指针,该ID3D12Resource表示新创建的资源。
此外,在使用深度/模板缓冲区之前,我们必须创建一个关联的深度/模板视图以绑定到管道, 这与创建渲染目标视图类似。 下面的代码示例显示了我们如何创建深度/模板纹理及其相应的深度/模板视图:
D3D12_RESOURCE_DESC depthStencilDesc;
depthStencilDesc.Dimension = D3D12_RESOURCE_DIMENSION_TEXTURE2D;
depthStencilDesc.Alignment = 0;
depthStencilDesc.Width = mClientWidth;
depthStencilDesc.Height = mClientHeight;
depthStencilDesc.DepthOrArraySize = 1;
depthStencilDesc.MipLevels = 1;
depthStencilDesc.Format = mDepthStencilFormat;
depthStencilDesc.SampleDesc.Count = m4xMsaaState ? 4 : 1;
depthStencilDesc.SampleDesc.Quality = m4xMsaaState ? (m4xMsaaQuality - 1) : 0;
depthStencilDesc.Layout = D3D12_TEXTURE_LAYOUT_UNKNOWN;
depthStencilDesc.Flags = D3D12_RESOURCE_FLAG_ALLOW_DEPTH_STENCIL;
D3D12_CLEAR_VALUE optClear;
optClear.Format = mDepthStencilFormat;
optClear.DepthStencil.Depth = 1.0f;
optClear.DepthStencil.Stencil = 0;
ThrowIfFailed(md3dDevice->CreateCommittedResource(
&CD3DX12_HEAP_PROPERTIES(D3D12_HEAP_TYPE_DEFAULT),
D3D12_HEAP_FLAG_NONE,
&depthStencilDesc,
D3D12_RESOURCE_STATE_COMMON,
&optClear,
IID_PPV_ARGS(mDepthStencilBuffer.GetAddressOf())));
// Create descriptor to mip level 0 of entire resource using the
// format of the resource.
md3dDevice->CreateDepthStencilView(
mDepthStencilBuffer.Get(),
nullptr,
DepthStencilView());
// Transition the resource from its initial state to be used as a depth buffer.
mCommandList->ResourceBarrier(
1,
&CD3DX12_RESOURCE_BARRIER::Transition(
mDepthStencilBuffer.Get(),
D3D12_RESOURCE_STATE_COMMON,
D3D12_RESOURCE_STATE_DEPTH_WRITE));
请注意,我们使用CD3DX12_HEAP_PROPERTIES辅助构造函数来创建堆属性结构,其实现方式如下:
explicit CD3DX12_HEAP_PROPERTIES(
D3D12_HEAP_TYPE type,
UINT creationNodeMask = 1,
UINT nodeMask = 1 )
{
Type = type;
CPUPageProperty = D3D12_CPU_PAGE_PROPERTY_UNKNOWN;
MemoryPoolPreference = D3D12_MEMORY_POOL_UNKNOWN;
CreationNodeMask = creationNodeMask;
VisibleNodeMask = nodeMask;
}
CreateDepthStencilView的第二个参数是指向D3D12_DEPTH_STENCIL_VIEW_DESC的指针,除此之外,该结构描述了资源中元素的数据类型(格式), 如果资源是使用类型化格式创建的,则此参数可以为null,这表示创建此资源的第一个mipmap级别的视图(深度/模板缓冲区仅使用一个mipmap级别创建 )使用创建资源的格式, 因为我们指定了深度/模板缓冲区的类型,所以我们为此参数指定了null。
typedef struct D3D12_VIEWPORT {
FLOAT TopLeftX;
FLOAT TopLeftY;
FLOAT Width;
FLOAT Height;
FLOAT MinDepth;
FLOAT MaxDepth;
} D3D12_VIEWPORT;
前四个数据成员定义相对于后缓冲区的视口矩形(指定小数像素坐标,因为数据成员的类型为float)。 在Direct3D中,深度值以0到1的标准化范围存储在深度缓冲区中,MinDepth和MaxDepth成员用于将深度间隔[0,1]转换为深度间隔[MinDepth,MaxDepth]。 能够变换深度范围可以用于实现某些效果; 例如,您可以设置MinDepth = 0和MaxDepth = 0,以便使用此视口绘制的所有对象的深度值均为0,并显示在场景中所有其他对象的前面。 但是,通常将MinDepth设置为0并将MaxDepth设置为1,以便不修改深度值。这个通过Unity的相机视口就可以体验一下。
一旦我们填写了D3D12_VIEWPORT结构,我们使用ID3D12CommandList :: RSSetViewports方法设置带有Direct3D的视口。 以下示例创建并设置一个绘制到整个后台缓冲区的视口:
D3D12_VIEWPORT vp;
vp.TopLeftX = 0.0f;
vp.TopLeftY = 0.0f;
vp.Width = static_cast<float>(mClientWidth);
vp.Height = static_cast<float>(mClientHeight);
vp.MinDepth = 0.0f;
vp.MaxDepth = 1.0f;
mCommandList->RSSetViewports(1, &vp);
最后再给读者介绍一个技术点,我们可以定义相对于后缓冲区的裁剪矩形,使得该矩形外的像素被剔除(即,不被光栅化到后缓冲区)。 这可以用于优化。 例如,如果我们知道屏幕的某个区域将包含一个矩形UI元素,我们就不需要处理UI元素将模糊的3D世界的像素。
裁剪矩形由D3D12_RECT结构定义,该结构的类型定义为以下结构:
typedef struct tagRECT
{
LONG left;
LONG top;
LONG right;
LONG bottom;
} RECT;
我们使用ID3D12CommandList :: RSSetScissorRects方法设置带有Direct3D的裁剪矩形,以下示例创建并设置一个覆盖后缓冲区左上象限的裁剪矩形:
mScissorRect = { 0, 0, mClientWidth/2, mClientHeight/2 };
mCommandList->RSSetScissorRects(1, &mScissorRect);
与RSSetViewports类似,第一个参数是要绑定的裁剪矩形的数量(使用多个用于高级效果),第二个参数是指向矩形数组的指针。
关于D3D12初始化工作就已经完成了,下篇博客我们基于这个流程搭建一个小小的框架用于实现我们的D3D12初始化。。。。。。