Direct3D的初始化(下)

学习目标

  1. 了解Direct3D在3D编程中相对于硬件所扮演的角色
  2. 理解组件对象模型COM在Direct3D中的作用
  3. 掌握基础的图像学概念,例如2D图像的存储方式,页面翻转,深度缓冲,多重采样以及CPU和GPU之间的交互
  4. 学习使用性能计数器函数,依次读取高精度计时器的数值
  5. 了解Direct3D的初始化过程
  6. 熟悉本书应用程序框架的整体结构,在后续的演示程序中可以经常看到应用程序框架的整体结构

4.3初始化Direct3D

对Direct3D进行初始化可以分为以下几个步骤

  1. 用D3D12CreateDevice函数创建ID3D12Device接口实例
  2. 创建一个ID3D12Fence对象,并查询描述符的大小
  3. 检测用户设备对4X MSAA质量级别的支持情况
  4. 依次创建命令队列,命令列表分配器,主命令列表
  5. 描述并创建交换链
  6. 创建应用程序需要的描述符堆
  7. 调整后台缓冲区的大小,并为它创建渲染目标视图
  8. 创建深度/模板缓冲区以及与之关联的深度/模板缓冲区视图
  9. 设置视口(viewport)和裁剪矩形(scissor rectangle)

4.3.1创建设备

要初始化Direct3D,必须先创建Direct3D12设备。Direct3D12设备相当于一个显示适配器,显示适配器一般都是一种3D图像硬件(如显卡),但也可以用软件显示适配器来模拟3D图形硬件功能,该设备可以检测系统环境对功能的支持情况,又可以用来创建所有其他的Direct3D接口对象(如资源,命令列表,视图(描述符)等等)。我们可以通过一下函数创建Direct3D12设备:

//@param:指定在创建设备的时候所用的显示适配器,如果把该指针设为空,则默认使用主显示适配器
//@param:应用程序需要硬件所支持的最低功能级别
//@parma:该ID3DDevice接口的COM ID
//@parma:返回所创建的Direct3D12设备
HRESULT WINAPI mD3D12CreateDevice(
    IUnknown* pAdapter,
    D3D_FEATURE_LEVEL MinimumFeatureLevel,
    REFIID riid,
    void** ppDevice
);

4.3.2创建围栏并获取描述符的大小

创建好设备之后,我们便可以为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);

4.3.3检测对4X MSAA质量级别的支持

凡是支持Direct3D11的硬件,都可以支持多重采样技术的开启。所以我们可以不用对此进行检测,但是,对质量级别的检测还是必不可少的。我们可以采用下面的方法进行检测:

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)));

4.3.4创建命令队列和命令列表

在前面的章节可知,ID3D12CommandQueue接口表示命令队列,ID3D12CommandAllocator接口表示命令分配器,ID3D12CommandList接口表示命令列表,下面我们将分别展示这几种对象的创建流程:

ComPtr<ID3D12CommandQueue> mCommandQueue;
ComPtr<ID3D12CommandAllocator> mDirectCmdListAlloc;
ComPtr<ID3D12CommandList> mCommandList;

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(),
        nullptr, IID_PPV_ARGS(mCommandList.GetAddressOf())));

    //首先要把命令列表关闭,因为第一次使用命令列表时我们要把命令列表重置,重置之前必须确保命令列表
    //已经关闭
    mCommandList->Close();
    }

4.3.5描述并创建交换链

在创建交换链之前,我们要先填写一份DXGI_SWAP_CHAIN_DESC结构体实例,用它来描述即将创建的交换链的特性。此结构体定义如下:

typedef struct DXGI_SWAP_CHAIN_DESC {
    DXGI_MODE_DESC BufferDesc;
    DXGI_SAMPLE_DESC SampleDesc;
    DXGI_USAGE BufferUsage;
    UINT BufferCount;
    HWND OutputWindow;
    BOOL Windowed;
    DXGI_SWAP_EFFECT SwapEffect;
    UINT Flags;
}DXGI_SWAP_CHAIN_DESC;

其中DXGI_MODE_DESC则是另一个结构体,该结构体定义如下:

typedef struct DXGI_MODE_DESC {
    UINT Width;
    UINT Height;
    DXGI_RATIONAL refreshRate;
    DXGI_FORMAT Format;
    DXGI_MODE_SCANLINE_ORDER ScanlineOrdering;
    DXGI_MODE_SCALING scaling;
}DXGI_MODE_DESC;

下面的代码将会展示如何在本书的演示框架下方便的创建交换链:

void D3DApp::CreateSwapChain()
{

    //释放之前创建的交换链,然后进行重建
    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;

    //注意,交换链需要通过命令队列才能刷新
    ThrowIfFailed(mdxgiFactory->CreateSwapChain(mCommandQueue.Get(),
        &sd, mSwapChain.GetAddressOf()));

}

4.3.6创建描述符堆

在程序中,我们需要创建描述符堆来存储程序中需要用到的描述符(视图),在Direct12中,ID3D12DescriptorHeap接口表示描述符堆,并用ID3D12Device::CreateDescriptorHeap方法来创建描述符堆,在下面的演示代码中,我们将创建两个描述符堆,一个用来存储SwapChainBufferCount个渲染目标视图(Render Target View),还有一个用来存储1个深度/模板视图(Depth/Stencil View)。

ComPtr<ID3D12DescriptorHeap> mRtvHeap;
ComPtr<ID3D12DescriptorHeap> mDsvHeap;

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())));
}

4.3.7创建渲染目标视图

由于资源不能直接和渲染流水线直接进行绑定,所以我们需要先为资源创建视图(描述符),并将其绑定到渲染流水线中。为了向后台缓冲区创建一个渲染目标视图,我们需要先获得交换链中的缓冲区资源。

我们可以通过IDXGISwapChain::GetBuffer()方法后去交换链中的缓冲区资源,每次调用该方法之后,会增加相关后台缓冲区的引用次数,所以在每一次使用后都要释放,我们可以通过Comptr自动实现这个功能。

接下来,我们可以使用ID3D12Device::CreateRenderTargetView()方法来为获取的后台缓冲区资源创建渲染目标视图。

以下实例将会通过调用这两个方法为交换链中的每一个缓冲区都创建一个RTV:

ComPtr<ID3D12Resource> mSwapChainBuffer[SwapChainBufferCount];
CD3DX12_CPU_DESCRIPTOR_HANDLE rtvHeapHandle(
    mRtvHeap->GetCPUDescriptorHandleForHeapStart()
);
for (UINT i = 0; i < SwapChainBufferCount; i++)
{
    //获取交换链中第i个缓冲区
    ThrowIfFailed(mSwapChain->GetBuffer(i,
        IID_PPV_ARGS(&mSwapChainBuffer[i])));

    //为此缓冲区创建一个RTV
    md3dDevice->CreateDepthStencilView(mSwapChainBuffer[i].Get(), nullptr, rtvHeapHandle);

    //偏移到描述符的下一个缓冲区
    rtvHeapHandle.Offset(1, mRtvDescriptorSize);
}

4.3.8创建深度/模板缓冲区及其视图(描述符)

深度缓冲区是一种纹理资源,它存储着离观察者最近的可视对象的深度信息(如果使用了模板,还有附有模板信息)。因为纹理是一种资源,所以我们需要通过填写D3D12_RESOURCE_DESC结构体来描述纹理资源,在使用ID3D12DeveiceCreateCommittedResource方法来创建它。以下代码为D3D12_RESOURCE_DESC结构体的定义:

typedef struct D3D12_RESOURCE_DESC
{
    D3D12_RESOURCE_DIMENSION Dimension;
    UINT64 Alignment;
    UINT64 Width;
    UINT Height;
    UINT16 DepthOrArraySize;
    DXGI_FORMAT Format;
    DXGI_SAMPLE_DESC SampleDesc;
    D3D12_TEXTURE_LAYOUT Layout;
    D3D12_RESOURCE_FLAGS Flags;
}D3D12_RESOURCE_DESC;

GPU资源都存于堆(Heap)中,其本质是具有特定属性的GPU显存块,ID3D12Device::CreateCommittedResource将根据我们所提供的属性,创建一个资源和一个堆,并把该资源提交到这个堆中。

    HRESULT ID3D12Device::CreateCommittedResource(
        const D3D12_HEAP_PROPERTIES * pHeapProperties,
        D3D12_HEAP_FLAGS HeapFlags,
        const D3D12_RESOURCE_DESC * pDesc,
        D3D12_RESOURCE_STATES InitialResourceState,
        const D3D12_CLEAR_VALUE * pOptimizedClearValue,
        REFIID riidResource,
        void ** ppvResource
        );

    typedef struct D3D12_HEAP_PROPERTIES {
        D3D12_HEAP_TYPE Type;
        D3D12_CPU_PAGE_PROPERTY CPUPageProperty;
        D3D12_MEMORY_POOL MemoryPoolPreference;
        UINT CreationNodeMask;
        UINT VisibleNodeMask;
    }D3D12_HEAP_PROPERTIES;

在使用深度/模板缓冲区之前,一定要先创建相关的深度/模板缓冲区视图(描述符),并将它绑定到渲染流水线中。下面的代码将会展示如何创建深度/模板纹理资源以及相对应的深度/模板缓冲区视图(描述符)

//创建深度/模板缓冲区视图
D3D12_RESOURCE_DESC depthStencilDesc;
//资源的维度
depthStencilDesc.Dimension = D3D12_RESOURCE_DIMENSION_TEXTURE2D;
depthStencilDesc.Alignment = 0;
//以纹素为单位来表示纹理宽度(如果是缓冲区资源,此项表示缓冲区占用的字节数)
depthStencilDesc.Width = mClientWidth;
//以纹素为单位来表示纹理高度
depthStencilDesc.Height = mClientHeight;
//以纹素为单位来表示纹理深度
depthStencilDesc.DepthOrArraySize = 1;
//mipmap层级的数量(后续讲纹理时会介绍mipmap)
depthStencilDesc.MipLevels = 1;
//DXGI_FORMAT枚举类型中的成员之一,用于指定纹素的格式
depthStencilDesc.Format = mDepthStencilFormat;
//多重采样的质量级别以及对每一个像素的采样次数
depthStencilDesc.SampleDesc.Count = m4xMsaaState ? 4 : 1;
depthStencilDesc.SampleDesc.Quality = m4xMsaaState ? (m4xMsaaQuality - 1) : 0;
//D3D12_TEXTURE_LAYOUT枚举类型的成员之一,用来指定纹理的布局
depthStencilDesc.Layout = D3D12_TEXTURE_LAYOUT_UNKNOWN;
//与资源有关的杂项标志
depthStencilDesc.Flags = D3D12_RESOURCE_FLAG_ALLOW_DEPTH_STENCIL;

//创建一个指向一个D3D12_CLEAR_VALUE对象的指针,该指针描述了一个用于清除资源的优化值,
//选择适当的优化值可以提高清除操作的效率,如果不希望指定优化值,也可以不创建。
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())
));

//利用此资源的格式,为整个资源的第0层mip创建描述符
md3dDevice->CreateDepthStencilView(
    mDepthStencilBuffer.Get(),
    nullptr,
    DepthStencilView()
);

//将资源从初始状态转换到深度缓冲区
mCommandList->ResourceBarrier(1, &CD3DX12_RESOURCE_BARRIER::Transition(mDepthStencilBuffer.Get(),
        D3D12_RESOURCE_STATE_COMMON,D3D12_RESOURCE_STATE_DEPTH_WRITE));

4.3.9设置视口

视口:我们通常会把3D场景绘制到整个屏幕中,或整个窗口工作区大小相当的后台缓冲区中,但是,有些时候我们只希望把3D场景绘制到后台缓冲区中的某一个矩形子区域中,而这个矩形子区域就称为视口

视口的结构体定义如下:

typedef struct D3D12_VIEWPORT {
    FLOAT TopLeftX;
    FLOAT TopLeftY;
    FLOAT Width;
    FLOAT Height;
    FLOAT MinDepth;
    FLOAT MaxDepth;
};

填写好D3D12_VIEWPORT结构体之后,我们便可以通过ID3D12GraphicsComandList::RSSetViewPort()方法来设置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;

//@param:绑定的视口数量
//@param:指向视口数组的指针
mCommandList->RSSetViewports(1, &vp);

4.3.10设置裁剪矩形
我们可以在相对于后台缓冲区定义一个裁剪矩形,在这个矩形之外的像素都不会被光栅化到后台缓冲区(被剔除),这个方法可以优化程序的性能。比如我们在游戏界面放置了一个UI,我们可以通过设置裁剪矩形使程序不必对3D空间中那些被它遮挡的像素进行处理了。设置裁剪矩形和设置视口一样,要先填写一个D3D12_RECT结构体,该结构体由类型为RECT的D3D12结构体定义而成:

typedef struct tagRECT {
    LONG left;
    LONG top;
    LONG right;
    LONG bottom;
}RECT;

在Direct3D中,要用ID3D12GraphicsCommandList::RSSetScisorRects方法来设置裁剪矩形,下面的实例展现了如何创建并设置一个覆盖后台缓冲区左上角四分之一区域的裁剪矩形

mScissorRect = { 0,0,mClientWidth / 2,mClientHeight / 2 };
mCommandList->RSSetScissorRects(1, &mScissorRect);
01-23 13:54