D3D12渲染技术之初始化流程

掌握了矩阵向量运算后,接下来我们要做的事情利用D3D12图形库API编程,也就是我们通常说的初始化D3D12,D3D12的初始化工作与以前的D3D9是完全不一样的,D3D12做了大幅的升级。但是每个D3D图形API都有自己的初始化流程,大家只要记住这个流程,学习起来就比较容易,就跟生产车架的流水线作业一样的原理,先做什么后做什么。其他的内容向里面添加就可以了,我们先聊聊D3D12的初始化流程,然后再编程实现。

D3D初始化

1、创建窗体
2、创建ID3D12Device设备
3、创建 ID3D12Fence用于查询descriptor 大小
4、检查设备是否支持4X MSAA
5、创建指令队列,指令列表和主指令列表。
6、创建交换链
7、创建描述符堆(descriptor heaps)
8、创建渲染目标视图。
9、创建深度/模板缓冲区及其关联的深度/模板视图。
10、设置视口

共分为10步,严格来说,第一步不属于D3D12初始化里面的内容,因为我们要显示D3D12中的内容,必须要要有窗体作为载体,在这里就把他们加进来了,我们下面就按照这10步去编写程序实现我们的D3D12的初始化工作。

  • 创建窗体
    窗体是一个载体,用于显示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编程的正题了。

  • 创建ID3D12Device设备
    创建D3D12的设备,自然想到函数:D3D12CreateDevice,在D3D12中我们还需要做一件事情就是调用函数:CreateDXGIFactory1在这个函数中有DXGI字样,DXGI它是DirectX Graphics Infrastructure的缩写,DXGI 可以操控一些普通的图形功能,这其中包括全屏转换、枚举图形系统信息(比如显示适配器、显示器、被支持的显示模式(分辨率,刷新率等等)),它还定义了各种纹理格式(DXGI_FORMAT)。CreateDXGIFactory1就是DXGI下面的一个接口函数,可以枚举显示适配器。
    函数调用代码如下:
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了。

  • 创建 ID3D12Fence
    接下来我们需要创建Fence对象用于CPU/GPU同步,另外,一旦我们开始使用描述符,我们将需要知道它们的大小, 描述符大小可能因GPU而异,因此我们需要查询此信息, 我们缓存描述符的大小,以便在我们需要它时可用于各种描述符类型,代码如下所示:
    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);
  • 检查设备是否支持4X MSAA
    我们之所以选择支持4X MSAA,在这里还是要求电脑硬件配置相对高一些比较好,因为所有支持Direct3D 11的设备都支持所有渲染目标格式的4X MSAA, 因此,保证在Direct3D 11硬件上可用,我们无需验证对它的支持。 但是,我们必须检查支持的质量等级,可以使用以下方法完成:
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.");
  • 创建指令队列,指令列表和主指令列表
    指令队列由ID3D12CommandQueue接口表示,指令分配器由ID3D12CommandAllocator接口表示,指令列表由ID3D12GraphicsCommandList接口表示,如下图所示:
    D3D12渲染技术之初始化流程_第1张图片
    CPU 通过 Direct3D API 的指令列表(Command Lists)向指令队列提交一个命令后,GPU并不会将这些命令立即执行,而是让它们置于队列之中等待处理,因为 GPU 此时可能正在处理队列中其它的命令。
    一旦指令队列为空,则 GPU 不再工作,因为没有指令可以处理。反过来,如果队列已满,则 CPU 不再工作,而是等待 GPU 处理队列中的指令。当然这两种情况哪个都是不可取的,理想情况是存在一种动态的平衡,让指令队列中始终存在可以被处理的指令却不能处于满格状态,这样 GPU 和 CPU 都能够被充分利用,这个需要用到我们上文提到的Fence。以下函数显示了我们如何创建命令队列,指令分配器和命令列表:
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。 在本篇博客的示例程序中,我们不发出任何绘图命令,因此我们不需要有效的管道状态对象。

  • 创建交换链
    初始化过程的下一步是创建交换链, 这是通过填写DXGI_SWAP_CHAIN_DESC结构的实例来完成的,该结构描述了我们要创建的交换链的特征, 该结构定义如下:
 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中的实现效果如下所示:
D3D12渲染技术之初始化流程_第2张图片
利用两个缓冲区(前后缓冲)组成交换链的方式叫做双重缓冲(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()));
  • 创建描述符堆
    我们需要创建描述符堆来存储我们的应用程序需要的描述符/视图, 描述符堆由ID3D12DescriptorHeap接口表示, 使用ID3D12Device :: CreateDescriptorHeap方法创建堆。 在本篇博客的示例程序中,我们需要SwapChainBufferCount许多渲染目标视图(RTV)来描述我们将渲染到的交换链中的缓冲区资源,以及一个深度/模板视图(DSV)来描述深度/模板缓冲区资源以进行深度测试。 因此,我们需要一个堆来存储SwapChainBufferCount RTV,需要一个堆来存储一个DSV, 这些堆使用以下代码创建:
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();
}
  • 创建渲染目标视图
    我们不直接将资源绑定到管道; 相反,我们必须为资源创建资源视图(描述符)并将视图绑定到管道, 特别是,为了将后台缓冲区绑定到管道的输出合并(因此Direct3D可以渲染到它上面),我们需要为后台缓冲区创建一个渲染目标视图, 第一步是获取存储在交换链中的缓冲区资源:
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);
} 
  • 创建深度/模板缓冲和视图
    我们现在需要创建深度/模板缓冲区,深度缓冲区只是一个2D纹理,它存储最近的可见对象的深度信息(如果使用模板,则存储模板信息)。 纹理是一种GPU资源,因此我们通过填写描述纹理资源的D3D12_RESOURCE_DESC结构来创建一个,然后调用ID3D12Device :: CreateCommittedResource方法。 D3D12_RESOURCE_DESC结构定义如下:
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枚举类型的以下成员之一:

  • D3D12_HEAP_TYPE_DEFAULT,默认堆, 这是我们提交将由GPU单独访问的资源地方,
    以深度/模板缓冲区为例:GPU读取和写入深度/模板缓冲区, CPU永远不需要访问它,因此深度/模板缓冲区将被放置在默认堆中。
  • D3D12_HEAP_TYPE_UPLOAD,上传堆, 这是我们提交资源的地方,我们需要将数据从CPU上传到GPU资源。
  • D3D12_HEAP_TYPE_READBACK,回读堆,这是我们提交需要由CPU读取的资源的地方。
  • D3D12_HEAP_TYPE_CUSTOM,有关高级使用方案,请参阅MSDN文档以获取更多信息。
    HeapMiscFlags,有关我们要将资源提交到的堆的其他标志, 它通常是D3D12_HEAP_MISC_NONE。
    pResourceDesc,指向描述我们要创建的资源的D3D12_RESOURCE_DESC实例的指针。
    InitialResourceState, 使用此参数可在创建资源时设置资源的初始状态,对于深度/模板缓冲区,初始状态将为D3D12_RESOURCE_USAGE_INITIAL,然后我们将其转换为D3D12_RESOURCE_USAGE_DEPTH,以便它可以作为深度/模板缓冲区绑定到管道。
    pOptimizedClearValue,指向D3D12_CLEAR_VALUE对象的指针,该对象描述用于清除资源的优化值, 可以为此值指定Null,以不指定优化的清除值。
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。

  • 设置视口
    通常我们喜欢将3D场景绘制到整个后台缓冲区,其中后台缓冲区大小对应于整个屏幕(全屏模式)或窗口的整个客户区域。 但是,有时我们只想将3D场景绘制到后缓冲区的子矩形中;
    D3D12渲染技术之初始化流程_第3张图片
    将3D场景绘制到后缓冲区的子矩形中, 然后将后缓冲区呈现给窗口的客户区域。我们绘制的后缓冲区的子矩形称为视口,它由以下结构描述:
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初始化。。。。。。

你可能感兴趣的:(游戏制作,3D引擎)