DirectX12 3D游戏开发实践(龙书)第四章 Direct3D的初始化

Direct3D的初始化

预备知识

学习Direct3D过程的初始化流程,我们需要了解一些图形学基本概念,避免后面学习Direct3D时被这些细枝末节知识点喧嚣夺主。

Direct3D概述

通过Direct3D这种底层图形应用程序变成接口,即可在GPU上进行控制和编程。列如清屏:ID3D12CommandList::ClearRenderTargetViewRenderTarget,渲染目标为了渲染场景而将像素绘制到特定缓冲区,在正式版本中是这样,预览版本中是ID3D12CommandList
Direct3D层和硬件驱动会协作将Direct3D命令转换为GPU可执行的本地机器指令。为此我们不用去考虑它的具体规格与硬件控制层面实现细节。GPU的各大生产厂商就必须与Direct3D团队一同合作,为用户提供Direct3D设备兼容的驱动。除了新渲染特性,Direct3D 12 经重新设计已焕然一新,与上一个版本的主要改变在于其性能优化方面在大大减少了CPU开销同时,又改进了对多线程的支持。Direct3D 12 API较于Direct3D 11更底层。API的抽象程度降低,使它更趋于具体化,与现代GPU的架构更为契合,所获得的回报是性能提升。

组件对象模型

组件对象模型(Component Object Model ,COM)是一种令DX不受编程语言束缚,并且使之向后兼容的技术。我们通常将COM对象视为一种接口,考虑到当前目的,遂将它当作C++类来使用。C++编程DX程序时,COM帮我们隐藏了大量底层细节。我们只需知道:要获取指向COM接口的指针,需借助特定函数或另一个COM接口,而不是去创建。它自己会统计引用次数,在完成某接口时候,我们需要自己调用Release接口。
为辅助管理COM对象生命周期,Windos运行库专门提供了Microsoft::WRL::ComPtr类((#include <wrl.h>),我们可以把它当作COM对象的智能指针。当一个ComPtr实例超出作用域范围时,它便会自动调用相应COM对象的Release方法。

下面是常用的3个ComPtr方法:
1.Get:返回一个指向底层COM接口的指针。此方法常用于把原始的接口指针作为参数传递给函数。例如:

ComPtr<ID3D12RootSignature> mRootSignature;
//...
//...
ID3D12RootSignature* argument.
mCommandList->SetGraphicsRootSignature(mRootSignature.Get());

2.GetAddressOf:返回指向此底层COM接口指针的地址。凭此方法即可利用函数参数返回COM接口的指针。例如:

ComPtr<ID3D12CommandAllocator> mDirectCmdListAlloc;
//...
//...
ThrowIfFailed(
md3dDevice->CreateCommandAllocator(
D3D12_COMMAND_LIST_TYPE_DIRECT,
mDirectCmdListAlloc.GetAddressOf())
);

3.Reset:将此ComPtr实例设置为nullptr,释放与之相关的所有引用(同时减少底层COM接口引用的计数)。此方法的功能与将ComPtr目标值赋值为nullptr的效果相同。

纹理格式(Textures Formats)

2D纹理(2D texture)是一种数据元素结构的矩阵(可将此“矩阵”看做2D数组),它的用途是一种存储2D图像数据。在这种情况下它的每个元素存储的都是一个像素的颜色。当然纹理还有其他作用,比如法线贴图,里面存储的就是3D向量信息。纹理用法非常广泛,比如1D、2D、3D纹理就相当于特定数据元素构成1D、2D、3D数组。当然,随着对纹理深入讨论,我们其实会知道纹理不只"数组数组"那样简单,他们可能还具备多种mipmap层级,而GPU则会对其特殊处理,例如过滤器和多重采样。也并不是任意类型数据元素都能组成纹理,他只能存储到DXGI_FORMAT枚举类型中特定格式的数据元素。例:

 DXGI_FORMAT_R32G32B32_FLOAT: //各个分量是32位浮点类型的RGB
 DXGI_FORMAT_R16G16B16A16_UNORM//各个元素是4个16位分量构成,每个分量的范围在[0,1]区间
 DXGI_FORMAT_R32G32_UINT//2个32无符号整数分量构成
 DXGI_FORMAT_R8G8B8A8_UNORM//4个8位无符号分量构成,范围是[0,1]
 DXGI_FORMAT_R8G8B8A8_SNORM//4个8位无符号分量构成,范围是[-1,1]
 DXGI_FORMAT_R8G8B8A8_SINT//4个8位有符号整数分量构成,范围是[-128,127]
 DXGI_FORMAT_R8G8B8A8_UINT//4个8位无符号整数分量构成,范围是[0,255]

颜色都是红绿蓝三基色组成,alpha通道则用于控制透明度。
当然这些结构体也不一定用于存储颜色数据,还可以用于存储向量等等。当然也有无类型(reinterpret cast 原文翻译成重新解释数据类型更合适)格式纹理,用于预留内存:

DXGI_FORMAT_R16G16B16A16_TYPELESS

交换链和页面翻转

为了避免画面出现闪烁情况下(比CPU还在计算一些数值,还没把数据传给GPU,然后GPU当前帧已经绘制完了准备绘制下一帧,却没有数据绘制,这时候,就会出现闪烁情况),最后有一个后台缓冲区用于存放还在计算的数据,计算完的完整一帧则用于前台缓冲区绘制。前台缓冲区存储的是当前屏幕的数据,动画的下一帧则是被存放在后台缓冲区,当后台缓冲区绘制完了则俩者互换,后台缓冲区变成为前台缓冲区呈现新一帧画面,而前台缓冲区则为下一帧动画展示,转换成后台缓冲区,等待数据填充。前后台缓冲区的互换操作称为呈现呈现是一种高效操作,只需要交换指向当前前台缓冲区和后台缓冲区的俩个指针即可实现
在这里插入图片描述
前后天缓冲区构成了交换链,在Direct3D中用IDXGISwapChain接口来表示。这个结构不仅存储了前后台缓冲区的俩种纹理,还提供了修改大小(IDXGISwapChain::ResizeBuffers)和呈现缓冲区内容(IDXGISwapChain::Present)的方法。
使用俩个缓冲区的情况称为双缓冲,三个缓冲区的叫三缓区,当然,一般来说俩个缓冲区就足够了。

深度缓冲

深度缓冲区存储的并不是图像数据而是特定的深度信息。深度值的范围是0.0~1.0,0.0代表观察者在视椎体,1.0代表观察者能见到的离自己最远的物体。
在这里插入图片描述
Direct3D采用了一种叫深度缓冲(depth buffering)或z 缓冲。这里强调一个细节:使用了深度缓冲,则物体的绘制顺序也变得无关紧要了。
在绘制过程中,只有找到最小的距离 d d d才会对观察窗口内的像素一级位于其深度缓冲区的缓冲值进行更新。(毕竟前面的物体会遮挡后面的物体)。

DXGI_FORMAT_D32_FLOAT_S8X24_UINT//该格式共占用64位,取其中32位指定浮点型缓冲深度区,另外8位(无符号整数)分配给模板缓冲区,并将该元素映射到[0,255]区间,剩下的24位用于填充对齐不作他用
DXGI_FORMAT_D32_FLOAT//指定一个32位浮点型深度缓冲区
DXGI_FORMAT_D24_UNORM_S8_UINT//指定一个无符号24位深度缓冲区,并将该元素映射到[0,1]区间。另外八位(无符号整形)分配给模板缓冲区,将瓷元素映射到[0,255]区间
DXGI_FORMAT_D16_UNORM//指定一个无符号16位深度缓冲区,把该元素映射到[0,1]区间

资源与描述符

在渲染过程中,GPU可能会对资源进行读写操作,在发出绘制命令之前,我们需要将本次绘制调用的相关资源绑定到渲染流水线上。但是,GPU资源并非直接与渲染流水线相绑定,而是要通过一种名为描述符(descriptor)的对象来对它间接引用。我们可以把描述符视为一种对送往GPU的资源进行描述的轻量级结构。从本质上来讲,它其实是一个中间层。若指定了资源描述符,GPU将能获得实际的资源数据,也能了解到资源的必要信息。
引用额外的中间层的原因是,GPU资源实质上都是一些普通的内存模块,由于资源的这种互通性,他们便能被设置到流水线的不同阶段供其使用。比如当不同地方在使用纹理时,用这种中间层更合理。出了指定资源数据,描述符还能为GPU解释资源,它会告知Direct3D某个资源改如何使用,而且我们可借助描述符来指定欲绑定资源中的局部数据。也就是,如若某个资源在创建时候采用了无类型格式,那么我们就必须在为它创建描述符时知名其具体类型。(视图与描述符是同义词,视图是Direct3D先前版本常用的属于,在Direct3D12中的部分API中交叉使用)。

下面是常用描述符类型:
1.COV(constant buffers view)/SRV( shader resources view)/UAV(unordered access view)描述符分别表示的是常量缓冲区视图、着色器资源视图和无序访问视图)
2.采样器(sampler 亦译为取样器)描述符表示的是采样器资源(用于纹理贴图)
3.RTV描述符表示的是渲染目标视图资源(render target view)
4.DSV描述符表示的是深度/模板视图资源(depth/stencil view)

描述符堆中存有一系列描述符,本质上是存放用户程序中某特定描述符的一块内存。我们需要为每一种类型描述符都创建单独的描述符堆,也可以为同一种描述符类型创建出多个堆,我们能用多个描述符来引用同一个资源。创建描述符的最佳时机在初始化期间,由于此过程中需要执行一些类型检测和验证工作,所以最好不要在运行时才创建描述符。

多重采样技术的原理

由于屏幕中的像素不可能是无穷小的,并不是任意一条直线都能在显示器上“平滑”而完美地呈现出来。下图是以像素矩阵毕竟直线的方法所产生的效果,类似地,显示器中呈现的三角形之边也存在不同程度的锯齿效应。通过提高显示器的分辨率就能缩小像素大小,继而使不易被用户察觉。
在不提示显示器分辨率下,或在显示器分辨率受限的情况下,我们就可以运用各种走反样(也有译作抗锯齿、反锯齿、反失真等)技术。有一种名为超级采样(SSAA,Super Sample Anti-Aliasing)的反走样技术,它是由4倍于屏幕分辨率大小后台缓冲区和缓冲深度。当数据要从后台缓冲区调往屏幕显示的时候,将会将四个像素为一组进行解析(降采样),把放大的采样点数降低回采样点数,每组求平均值得到相对平滑的像素颜色。实际上是通过软件的方式提升了画面的分辨率。
超级采样是一种高开销的高昂操作,因为它将像素的处理数量和占用的内存大小都增加到了之前的四倍。对此,D3D还支持一种性能与效果方面都比较折中的反走样技术叫多重采样。这种技术通过跨子像素共享一些计算信息,从而使它比超级采样的开销更低。现假设采用4X多重采样(即每个像素中都有4个子像素),并同样使用4倍于屏幕分辨率的后台缓冲区和深度缓冲区。这种技术并不是对每一个像素进行计算,而仅是计算一次像素中心的颜色,再基于可视性(每个子像素经深度/模板测试结果)和覆盖性(子像素的中心在多边形的里面还是外面)将得到的颜色分享给其子像素,如下图:
在这里插入图片描述

利用Direct3D进行多重采样

typedef struct DXGI_SAMPLE_DESC
{
    
    
UINT Count;//指定每个像素的采样次数
UINT Quality;//指示用户期望的图像质量级别
} DXGI_SAMPLE_DESC;

根据给定的纹理格式和采样数量,我们就能用ID3D12Device::CheckFeatureSupport查询对应的质量级别:

typedef struct
D3D12_FEATURE_DATA_MULTISAMPLE_QUALITY_LEVELS 
{
    
    
DXGI_FORMAT Format;
UINT SampleCount;
D3D12_MULTISAMPLE_QUALITY_LEVELS_FLAG Flags;
UINT NumQualityLevels;
} D3D12_FEATURE_DATA_MULTISAMPLE_QUALITY_LEVELS;

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->CheckFeatureSuppor
(
D3D12_FEATURE_MULTISAMPLE_QUALITY_LEVELS,
&msQualityLevels,
sizeof(msQualityLevels))
);

每个像素的最大采样数量:

#define D3D11_MAX_MULTISAMPLE_SAMPLE_COUNT ( 32 )

考虑到性能内存资源问题,通常会把采样数量设置为4或8,如果不希望用多重采样,则可将采样数量设置为1,并令质量级别为0。

功能级别

Direct3D9~Direct3D11之间的各种版本:

enum D3D_FEATURE_LEVEL
{
    
    
D3D_FEATURE_LEVEL_9_1 = 0x9100,
D3D_FEATURE_LEVEL_9_2 = 0x9200,
D3D_FEATURE_LEVEL_9_3 = 0x9300,
D3D_FEATURE_LEVEL_10_0 = 0xa000,
D3D_FEATURE_LEVEL_10_1 = 0xa100,
D3D_FEATURE_LEVEL_11_0 = 0xb000,
D3D_FEATURE_LEVEL_11_1 = 0xb100
}D3D_FEATURE_LEVEL;

DirectX图形基础结构

DirectX图形基础结构(DirectX Graphics Infrastructure,DXGI,也译作DirectX图形基础设施)是一种与Direct3D配合使用的API。设计DXGI的理念是使多图形API中共有的底层任务能借助一组通用API来处理。例如,为保障动画的流畅性,2D渲染与3D渲染都要用到交换链和页面翻转功能,这里的交换链接口:IDXGISwapChain。DXGI还提供其他图形功能,例如全屏模式与窗口模式,枚举显示适配器、适配器设备、支持的分辨率、刷新率等。它还定义了D3D支持的各种表面格式信息(DXGI_FORMAT)。
接下来描述的是一些关于D3D的API。IDXGIFactory是DXGI的关键接口,主要用于创建IDXGISwapChain接口以及枚举显示适配器。显示适配器则真正实现了图形处理能力,它是一种硬件设备(例如独立显卡),然而系统也可以用软件显示适配器来模拟硬件的图形处理功能。当然一个系统中可能存在多个适配器(比如一些有钱人装很多块3090显卡)。适配器用接口 IDXGIAdapter来表示。我们可以用下面的代码来枚举一个系统中的所有适配器:

void D3DApp::LogAdapters()
{
    
    
UINT i = 0;
IDXGIAdapter* adapter = nullptr;
std::vector<IDXGIAdapter*> adapterList;
while(mdxgiFactory->EnumAdapters(i, &adapter) !=
DXGI_ERROR_NOT_FOUND)
{
    
    
DXGI_ADAPTER_DESC desc;
adapter->GetDesc(&desc);
std::wstring text = L"***Adapter: ";
text += desc.Description;
text += L”\n”;
OutputDebugString(text.c_str());
adapterList.push_back(adapter);
++i;
}
for(size_t i = 0; i < adapterList.size(); ++i)
{
    
    
LogAdapterOutputs(adapterList[i]);
ReleaseCom(adapterList[i]);
}
}

运行结果:

***Adapter: NVIDIA GeForce GTX 760
***Adapter: Microsoft Basic Render Driver

Microsoft Basic Render Driver是Win8及后续版本中包含的软适配器。
另外,一个系统可能有多个输出设备,我们称每一台设备都是显示输出。用IDXGIOutput接口来表示。每个适配器都与一组显示输出相关联。举个例子,考虑这样一个系统,该系统共有俩快显卡和三台显示器,其中一块显卡与俩台显示器相连,第三台显示器与另一块相连,这种情况下,一块适配器与俩个显示输出相关联,而另一个仅与一个显示输出相关联,通过下面代码我们就可以枚举出某块适配器所关联的所有显示输出:

void D3DApp::LogAdapterOutputs(IDXGIAdapter* adapter)
{
    
    
UINT i = 0;
IDXGIOutput* output = nullptr;
while(adapter->EnumOutputs(i, &output) !=
DXGI_ERROR_NOT_FOUND)
{
    
    
DXGI_OUTPUT_DESC desc;
output->GetDesc(&desc);
std::wstring text = L"***Output: ";
text += desc.DeviceName;
text += L”\n”;
OutputDebugString(text.c_str());
LogOutputDisplayModes(output,
DXGI_FORMAT_B8G8R8A8_UNORM);
ReleaseCom(output);
++i;
}
}

注意,官方文档指出,在显卡驱动正常工作情况下,Microsoft Basic Render Driver不会关联任何显示输出。
每种显示设备都有一系列它所支持的显示模式,可以用下列结构体表示:

typedef struct DXGI_MODE_DESC
{
    
    
UINT Width; // 分辨率宽
UINT Height; // 分辨率高度
DXGI_RATIONAL RefreshRate;//刷新率,单位为赫兹Hz
DXGI_FORMAT Format; //显示格式
DXGI_MODE_SCANLINE_ORDER ScanlineOrdering;//逐行扫描    vs.   隔行扫描
DXGI_MODE_SCALING Scaling; //图像相对于屏幕的拉伸
stretched
// over the monitor.
} DXGI_MODE_DESC;

typedef struct DXGI_RATIONAL
{
    
    
UINT Numerator;
UINT Denominator;
} DXGI_RATIONAL;

typedef enum DXGI_MODE_SCANLINE_ORDER
{
    
    
DXGI_MODE_SCANLINE_ORDER_UNSPECIFIED = 0,
DXGI_MODE_SCANLINE_ORDER_PROGRESSIVE = 1,
DXGI_MODE_SCANLINE_ORDER_UPPER_FIELD_FIRST = 2,
DXGI_MODE_SCANLINE_ORDER_LOWER_FIELD_FIRST = 3
} DXGI_MODE_SCANLINE_ORDER;

typedef enum DXGI_MODE_SCALING
{
    
    
DXGI_MODE_SCALING_UNSPECIFIED = 0,
DXGI_MODE_SCALING_CENTERED = 1,//不做缩放,将图像显示在屏幕中
DXGI_MODE_SCALING_STRETCHED = 2//根据屏幕分辨率对图像进行拉伸
} DXGI_MODE_SCALING;

一旦确认显示模式的具体格式(DXGI_FORMAT),我们就能通过获得某个显示输出对此格式所支持的全部显示模式:

void D3DApp::LogOutputDisplayModes(IDXGIOutput*
output, DXGI_FORMAT format)
{
    
    
UINT count = 0;
UINT flags = 0;
// 以nullptr作为参数调用此函数来获取符合条件的显示模式个数
output->GetDisplayModeList(format, flags, &count, nullptr);
std::vector<DXGI_MODE_DESC> modeList(count);
output->GetDisplayModeList(format, flags, &count,
&modeList[0]);
for(auto& x : modeList)
{
    
    
UINT n = x.RefreshRate.Numerator;
UINT d = x.RefreshRate.Denominator;
std::wstring text =
L”Width =+ std::to_wstring(x.Width) + L” ” +
L”Height =+ std::to_wstring(x.Height) + L” ”
+
L”Refresh =+ std::to_wstring(n) + L”/+
std::to_wstring(d) +
L”\n”;
::OutputDebugString(text.c_str());
}
}

运行结果:

***Output: \.\DISPLAY2
… 
Width =1920 Height = 1080 Refresh = 59950/1000
Width = 1920 Height = 1200 Refresh = 59950/1000

进入全屏模式时,枚举显示模式显得尤为重要,所指定的模式一定要与显示器支持的模式完全匹配。
有关DXGI的资料:DXGI OverView(DXGI概述)DirectX Graphics Infrastructure: Best Practices:(DirectX图形基础结构:最佳实践)
DXGI 1.4 Improvements:(DXGI 1.4改进)

功能支持的检测

之前通过ID3D12Device::CheckFeatureSupport检测了对多重采样的支持,但还不够,这个方法原型:

HRESULT ID3D12Device::CheckFeatureSupport(
D3D12_FEATURE Feature,
void *pFeatureSupportData,
UINT FeatureSupportDataSize);

Feature:枚举类型D3D12_FEATURE成员之一,用于指定我们希望检测的功能支持类型。

D3D12_FEATURE_D3D12_OPTIONS//检测D3D12各种功能支持情况
D3D12_FEATURE_ARCHITECTURE//检测图形适配器中GPU硬件体系架构特性
D3D12_FEATURE_FEATURE_LEVELS//检测对功能级别的支持情况
D3D12_FEATURE_FORMAT_SUPPORT//检测对给定纹理格式的支持情况
D3D12_FEATURE_MULTISAMPLE_QUALITY_LEVELS//检测对多重采样的支持情况

pFeatureSupportData:指向某种数据结构类型指针,该结构中,存有检索到的特定功能的支持的信息,此结构的具体类型取决于Feature参数。

Feature是D3D12_FEATURE_D3D12_OPTIONS,则返回的是D3D12_FEATURE_DATA_D3D12_OPTIONS的实例
Feature是D3D12_FEATURE_ARCHITECTURE,则返回的是D3D12_FEATURE_DATA_ARCHITECTURE的实例
Feature是D3D12_FEATURE_FEATURE_LEVELS,则返回的是D3D12_FEATURE_DATA_FEATURE_LEVELS的实例
Feature是D3D12_FEATURE_FORMAT_SUPPORT,则返回的是D3D12_FEATURE_FORMAT_SUPPORT的实例
Feature是3D12_FEATURE_MULTISAMPLE_QUALITY_LEVELS,则返回的是D3D12_FEATURE_DATA_D3D12_OPTIOND3D12_FEATURE_DATA_MULTISAMPLE_QUALITY_LEVELSS的实例

FeatureSupportDataSize传回pFeatureSupportData参数中的数据结构大小。
ID3D12Device::CheckFeatureSupport不会对高级的功能进行检测。
检测示例:

typedef struct D3D12_FEATURE_DATA_FEATURE_LEVELS {
    
    
UINT NumFeatureLevels;
const D3D_FEATURE_LEVEL *pFeatureLevelsRequested;
D3D_FEATURE_LEVEL MaxSupportedFeatureLevel;
} D3D12_FEATURE_DATA_FEATURE_LEVELS;
D3D_FEATURE_LEVEL featureLevels[3] =
{
    
    
D3D_FEATURE_LEVEL_11_0, // 首先检测是否支持D3D11
support
D3D_FEATURE_LEVEL_10_0,  // 其次检测是否支持D3D10
support
D3D_FEATURE_LEVEL_9_3 //  // 最后检测是否支持D3D9.3
support
};
D3D12_FEATURE_DATA_FEATURE_LEVELS featureLevelsInfo;
featureLevelsInfo.NumFeatureLevels = 3;
featureLevelsInfo.pFeatureLevelsRequested =
featureLevels;
md3dDevice->CheckFeatureSupport(
D3D12_FEATURE_FEATURE_LEVELS,
&featureLevelsInfo,
sizeof(featureLevelsInfo));

资源留驻

复杂的游戏运算会运用大量的纹理和3D网格等资源,其中并不需要存在显存中供GPU使用,显存的控件有限,在D3D12中,应用程序通过控制资源在显存中的去留,主动管理资源的留驻情况。该技术的基本思路是让应用程序占最小的显存空间(理清哪些资源在短时间内不会使用),我们可以用下列方法来控制资源留驻:

HRESULT ID3D12Device::MakeResident(UINT NumObjects,ID3D12Pageable *const *ppObjects);//NumObjects 该数组中资源数量,ID3D12Pageable 资源数组

HRESULT ID3D12Device::Evict(UINT NumObjects,ID3D12Pageable *const *ppObjects);

更多参考留驻

CPU与GPU间的交互

为了获得最好的性能,最好的情况是让俩者尽量同时工作,少同步。同步是一种我们不乐于执行的操作,因为这意味着一种处理器要以空闲的状态等待另一种处理器完成默写任务,换句话说,它破坏了俩者的工作机制。

命令队列和命令列表

每个GPU至少维护着一个命令队列,CPU可以利用D3D API将命令提到这个队列中。当一系列命令被提交至命令队列之时,它们不会被CPU立即执行,而是GPU按照队列的顺序执行。当命令队列空空乳液,GPU只能空闲下来,相反,如果命令队列被填满,CPU必将随着GPU的步伐在某些时候保持空闲,这俩种情况都不是我们希望碰见的。对于游戏这样的高性能程序来说,它们的目标是充分利用硬件资源,保持CPU和GPU同时忙碌。
在D3D12中,命令队列被抽象为ID3D12CommandQueue的接口来表示。要通过D3D12_COMMAND_QUEUE_DESC结构体来描述队列,在调用ID3D12Device::CreateCommandQueue方法创建队列。如:

Microsoft::WRL::ComPtr<ID3D12CommandQueue>mCommandQueue;
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)));

IID_PPV_ARGS辅助宏定义:

#define IID_PPV_ARGS(ppType) __uuidof(**(ppType)), IID_PPV_ARGS_Helper(ppType)

__uuidof(**(ppType))将会获取到ppType的COM接口ID(全局唯一标识符GUID),上述代码段即为ID3D12CommandQueue接口的COM ID。
ExecuteCommandLists是一种常用的ID3D12CommandQueue接口方法,利用它可以将命令列表里的命令添加到命令队列之中。

void ID3D12CommandQueue::ExecuteCommandLists(UINT Count,ID3D12CommandList *const *ppCommandLists);//Count命令数量,ppCommandLists待执行的命令列表数组

ID3D12GraphicsCommandList封装了一系列图形渲染命令,他实际上继承于ID3D12CommandList,ID3D12GraphicsCommandList接口有数种方法命令列表添加命令。例如,下面代码一次向命令列表添加了设置视口、清楚渲染目标视图和发起绘制调用的命令:

// mCommandList是ID3D12CommandList类型指针
mCommandList->RSSetViewports(1, &mScreenViewport);
mCommandList->ClearRenderTargetView(mBackBufferView,Colors::LightSteelBlue, 0, nullptr);
mCommandList->DrawIndexedInstanced(36, 1, 0, 0, 0);

虽然这些方法名字看起来像是会使对应的命令立即执行,但其实是将代码加入命令列表而已。调用ExecuteCommandLists方法才会将命令真正地送入命令队列。当命令加入命令列表之后,我们必须调用ID3D12GraphicsCommandList::Close方法来结束命令的记录;
在调用ID3D12CommandQueue::ExecuteCommandLists之前一定要将其关闭。
还有一种命令列表有关的名为ID3D12CommandAllocator的内存管理类接口,用于记录命令列表内的命令,实际上是存储在与之关联的命令分配器上。通过ID3D12CommandQueue::ExecuteCommandLists方法执行命令列表的时候,命令队列就会引用分配器里的命令。而命令分配器则由ID3D12Device:接口来创建:

/*
*type:指定与此命令分配器相关联的命令列表类型,下列是俩种常用的命令类型:D3D12_COMMAND_LIST_TYPE_DIRECT,D3D12_COMMAND_LIST_TYPE_BUNDLE
*前者存储一系列可供GPU直接执行的命令,后者将命令列表打包,为D3D12提供了一种优化的方法,允许我们将一系列命令打包成所谓的包,当打包完毕
*之后驱动就会对其中命令进行预处理,以使它们在渲染执行过程中得到优化。因此我们应当在初始化的时候使用包记录命令。D3D的API效率很高,一般
*不会用到打包技术,当我们发现构造某些命令列表会花费大量的时间,就可以考虑使用打包技术。
*
*riid:待创建的ID3D12CommandAllocator接口的COM ID
*
*ppCommandAllocator:输出指向所建命令分配器的指针
/
HRESULT ID3D12Device::CreateCommandAllocator(D3D12_COMMAND_LIST_TYPE type,REFIID riid,void **ppCommandAllocator);

命令列表同样由ID3D12Device接口创建:

/*
*nodeMask:对于仅有一个GPU系统而言,要将此值设为0,对于多个GPU系统而言,此节点掩码指定的是所建命令列表相关联的物理GGPU(假设我们使用的
*都是但GPU系统)
*
*type:命令列表类型
*
*pCommandAllocator:命令分配器
*
*pInitialState:指定命令列表渲染流水管线初始状态。对于打包技术来说可将此值设为nullptr
*
*riid:待创建的ID3D12CommandAllocator接口的COM ID
*
*ppCommandList:命令列表指针
/
HRESULT ID3D12Device::CreateCommandList(UINT nodeMask,D3D12_COMMAND_LIST_TYPE type,ID3D12CommandAllocator *pCommandAllocator, ID3D12PipelineState *pInitialState,REFIID riid,void **ppCommandList);

ID3D12Device::GetNodeCount可用于查询GPU适配节点的数量。
我们可以创建多个关联于同一分配器的命令列表,但是不能用它们来记录命令,因此,我们需要关闭其他命令列表。
当我们ID3D12CommandQueue::ExecuteCommandList之后,可用ID3D12CommandList::Reset安全地复用命令列表C所占用的相关联底层内存来记录新的命令集。(注意:重置命令列表不会影响命令队列中的命令)

HRESULT ID3D12CommandList::Reset(
ID3D12CommandAllocator *pAllocator,//:命令分配器
ID3D12PipelineState *pInitialState);//:指定命令列表渲染流水管线初始状态。对于打包技术来说可将此值设为nullptr

CPU与GPU间的同步

CPU与GPU同步是很大的问题,解决的一种办法就是强制CPU等待GPU命令处理完成,达到某个指定的围栏点(fence point)为止,我们将这种方法称为刷新命令队列,可通过围栏来实现这一点。围栏用接口ID3D12Fence来表示,创建方法如下:

HRESULT ID3D12Device::CreateFence(
UINT64 InitialValue,
D3D12_FENCE_FLAGS Flags,
REFIID riid,
void **ppFence);
// 示例
ThrowIfFailed(md3dDevice->CreateFence(
0,
D3D12_FENCE_FLAG_NONE,
IID_PPV_ARGS(&mFence)));

围栏中有个UINT64类型值用于标识围栏点的整数,每当需要标记一个新的围栏点的时候它将+1,如:

UINT64 mCurrentFence = 0;
void D3DApp::FlushCommandQueue()
{
    
    
mCurrentFence++;
// 向命令队列中添加一条新设置围栏点的命令
// 由于这条命令会交给GPU,所以在GPU处理完队列中Singnal()的命令之前,它并不会设置新的围栏值
ThrowIfFailed(mCommandQueue->Signal(mFence.Get(),mCurrentFence));
//在CPU端等待,直到后者执行完这个围栏点之前的所有命令
if(mFence->GetCompletedValue() < mCurrentFence)
{
    
    
HANDLE eventHandle = CreateEventEx(nullptr, false, false, EVENT_ALL_ACCESS);
// 所GPU命中当前的围栏(即执行Singnal()指令,修改了围栏值),则激发预定事件
ThrowIfFailed(mFence->SetEventOnCompletion(mCurrentFence, eventHandle));
// 等待GPU命中围栏,激发事件
WaitForSingleObject(eventHandle, INFINITE);
CloseHandle(eventHandle);
}
}

资源转换

为了实现常见的渲染效果,我们经常会通过GPU对某个资源进行先后顺序的读写。当GPU的写操作还没有完成亦或者还没开始却开始读取资源,便会导致资源冒险(Resource hazard)。针对这一问题,D3D做了特殊处理,资源在创建的时候会存在默认状态,直到转换成其他状态为止。例如纹理资源写操作时,将它改为渲染目标状态,而针对纹理资源读操作时候,再将它的状态变为着色器资源状态。当然自动跟踪状态转换的系统与资源转换都会导致程序的额外开销。
通过命令列表设置转换资源屏障数组,即可指定资源的转换,我们喜欢以一次API调用来转换多个资源的时候,则可选择用它。在代码中表示:D3D12_RESOURCE_BARRIER

struct CD3DX12_RESOURCE_BARRIER : public
D3D12_RESOURCE_BARRIER
{
    
    
// […] 辅助方法
static inline CD3DX12_RESOURCE_BARRIER Transition(
_In_ ID3D12Resource* pResource,
D3D12_RESOURCE_STATES stateBefore,
D3D12_RESOURCE_STATES stateAfter,
UINT subresource =
D3D12_RESOURCE_BARRIER_ALL_SUBRESOURCES,
D3D12_RESOURCE_BARRIER_FLAGS flags =D3D12_RESOURCE_BARRIER_FLAG_NONE)
{
    
    
CD3DX12_RESOURCE_BARRIER result;
ZeroMemory(&result, sizeof(result));
D3D12_RESOURCE_BARRIER &barrier = result;
result.Type =
D3D12_RESOURCE_BARRIER_TYPE_TRANSITION;
result.Flags = flags;
barrier.Transition.pResource = pResource;
barrier.Transition.StateBefore = stateBefore;
barrier.Transition.StateAfter = stateAfter;
barrier.Transition.Subresource = subresource;
return result;
}
// […] 其他辅助方法
};

d3dx12.h不属于DX12 SDK核心部分,但是可通过官网下载获得
此处用到的辅助函数:

mCommandList->ResourceBarrier(1,
&CD3DX12_RESOURCE_BARRIER::Transition(
CurrentBackBuffer(),
D3D12_RESOURCE_STATE_PRESENT,
D3D12_RESOURCE_STATE_RENDER_TARGET));

此代码将以图片形式显示在屏幕中的纹理,从呈现状态转为渲染目标。

命令与多线程

Direct3D l2的设计目标是为用户提供一个高效的多线程环境,命令列表也是一种发挥Direct3D l2多线程优势的途径。对于内含许多物体的庞大场景而言,仅通过一个构建命令列表来绘制整个场景会占用不Direct3D的初始化少的CPU时间。因此,可以采取一种并行创建命令列表的思路。例如,我们可以创建4条线程,每条分别负责构建一个命令列表来绘制25%的场景物体。
以下是一些在多线程环境中使用命令列表要注意的问题:
l.命令列表并非自由线程(not free-threaded)对象。也就是说,多线程既不能同时共享相同的命令列表,也不能同时调用同一命令列表的方法。所以,每个线程通常都只使用各自的命令列表。
2.命令分配器亦不是线程自由的对象。这就是说,多线程既不能同时共享同一个命令分配器,也不能同时调用同一命令分配器的方法。所以,每个线程一般都仅使用属于自己的命令分配器。
3.命令队列是线程自由对象,所以多线程可以同时访问同一命令队列,也能够同时调用它的方法。特别是每个线程都能同时向命令队列提交它们自己所生成的命令列表。
4.出于性能的原因,应用程序必须在初始化期间,指出用于并行记录命令的命令列表最大数量。

初始化Direct3D

创建设备

要初始化Direct3D,必须先创建D3D12设备(ID3D12Device)。此设备代表着一个显示适配器。一般来说,显示适配器是一种3D图形硬件(如显卡)。但是,一个系统也能用软件显示适配器来模拟3D图形硬件的功能(如WARP适配器)。D3D12设备既可检测系统环境对功能的支持情况,又能创建所有其他的D3D12接口对象(如资源、视图和命令列表)。通过下面的函数就可以创建D3D12设备:

HRESULT WINAPI D3D12CreateDevice(
IUnknown* pAdapter,//指定创建设备使用的显示适配器,如果此参数为空,则显示主线是适配器
D3D_FEATURE_LEVEL MinimumFeatureLevel,//应用程序所需硬件所支持的最低功能级别,如果适配器不支持功能级别则设备创建失败
REFIID riid, // ID3D12Device的COMID
void** ppDevice );//返回创建的D3D设备

示例:

#if defined(DEBUG) || defined(_DEBUG)
// Enable the D3D12 debug layer.
{
    
    
ComPtr<ID3D12Debug> debugController;
ThrowIfFailed(D3D12GetDebugInterface(IID_PPV_ARGS(&debugController)));
debugController->EnableDebugLayer();
}#
endif
ThrowIfFailed(CreateDXGIFactory1(IID_PPV_ARGS(&mdxgiFactory)));
// 尝试创建硬件设备
HRESULT hardwareResult = D3D12CreateDevice(
nullptr, // default adapter
D3D_FEATURE_LEVEL_11_0,
IID_PPV_ARGS(&md3dDevice));
// 回退WARP(windows高级光栅化平台)设备
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)));
}

为了创建WARP适配器,需先创建IDXGIFactory4,通过它来枚举WARP

ComPtr<IDXGIFactory4> mdxgiFactory;
CreateDXGIFactory1(IID_PPV_ARGS(&mdxgiFactory));
mdxgiFactory->EnumWarpAdapter(
IID_PPV_ARGS(&pWarpAdapter));

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

一创建好设备,便可以为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);
mCbvSrvDescriptorSize = md3dDevice->GetDescriptorHandleIncrementSize(D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV);

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

我们可以用下列方法实现对4X MSAA(多重采样反锯齿)的支持:

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

创建命令队列和命令列表

ComPtr<ID3D12CommandQueue> mCommandQueue;
ComPtr<ID3D12CommandAllocator> mDirectCmdListAlloc;
ComPtr<ID3D12GraphicsCommandList> 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();
}

创建交换链

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;

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;
HRESULT IDXGIFactory::CreateSwapChain(
IUnknown *pDevice, 
DXGI_SWAP_CHAIN_DESC *pDesc, // 交换链
IDXGISwapChain **ppSwapChain);// swap
chain interface.
DXGI_FORMAT mBackBufferFormat =
DXGI_FORMAT_R8G8B8A8_UNORM;
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()));
}

创建描述符堆

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

描述符访问:
RTV :渲染目标视图
DSV : 深度/模板视图

D3D12_CPU_DESCRIPTOR_HANDLE CurrentBackBufferView()const
{
    
    
	return CD3DX12_CPU_DESCRIPTOR_HANDLE(mRtvHeap->GetCPUDescriptorHandleForHeapStart(),mCurrBackBuffer, mRtvDescriptorSize);
}
D3D12_CPU_DESCRIPTOR_HANDLE DepthStencilView()const
{
    
    
	return mDsvHeap->GetCPUDescriptorHandleForHeapStart();
}

创建渲染目标视图

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->CreateRenderTargetView(mSwapChainBuffer[i].Get(), nullptr, rtvHeapHandle);
// 便宜到描述符堆中的下一个缓冲区
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;
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;
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;
// 创建深度/模板缓冲区及其视图
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())));
// 利用此资源为第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));

设置视口

通常我们会将整个3D场景绘制在整个屏幕中,或者是窗口的部分工作工作区,如下图:
在这里插入图片描述
视口:

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

例子:

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);//设置视口

游戏中有很多双人游戏的分屏模式,这时候只需要创建俩个视口即可

设置剪裁矩阵

我们可以相对于后台缓冲区定义一个剪切矩阵,在此矩形之外的像素都将被剔除(即这些像素部分将不会被光栅化至后台缓冲区)。此方法可以优化游戏性能。

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

下方示例会创建一个覆盖后台缓冲区1/4的剪裁矩阵:

mScissorRect = {
    
     0, 0, mClientWidth/2, mClientHeight/2
};
mCommandList->RSSetScissorRects(1, &mScissorRect);

计时与动画

为了制作出精确地计量事件,特别是要准确地度量出动画每帧。如果帧率较高,那么帧与帧之间的间隔时间就会比较短,此时我们就要用到高精度的计时器。

性能计时器

为了精确度量时间,我们将采用性能计时器,如果希望调用查询计时器的函数,需引入#include <windows.h>。性能计时器所用到的计量单位叫计数。

__int64 currTime;
QueryPerformanceCounter((LARGE_INTEGER*)&currTime);//获取当前性能计时器测了的时刻值
__int64 countsPerSec;
QueryPerformanceFrequency((LARGE_INTEGER*)&countsPerS;//计时器频率
mSecondsPerCount = 1.0 / (double)countsPerSec;//计数代表的时间(时间是频率分之一)

俩次计数的时间差就是我们常说的时间戳。

游戏计时器类

class GameTimer
{
    
    
public:
GameTimer();
float GameTime()const; // in seconds
float DeltaTime()const; // in seconds
void Reset(); // Call before message loop.
void Start(); // Call when unpaused.
void Stop(); // Call when paused.
void Tick(); // Call every frame.
private:
double mSecondsPerCount;
double mDeltaTime;
__int64 mBaseTime;
__int64 mPausedTime;
__int64 mStopTime;
__int64 mPrevTime;
__int64 mCurrTime;
bool mStopped;
};

总时间

指的时从应用程序开始,不记其暂停时间的总和。

应用程序框架

(基本阅读必备知识Win32编程)

D3DApp类

D3DApp是一种基础的D3D应用程序类,它提供了创建应用程序主窗口、运行程序消息循环、处理窗口信息以及初始化D3D等多种功能函数,此外该类还未应用程序定义了一组框架函数,给用于用于重载。

#include "d3dUtil.h"
#include "GameTimer.h"
// 连接所需的d3d12库
#pragma comment(lib,"d3dcompiler.lib")
#pragma comment(lib, "D3D12.lib")
#pragma comment(lib, "dxgi.lib")
class D3DApp
{
    
     protected:
D3DApp(HINSTANCE hInstance);
D3DApp(const D3DApp& rhs) = delete;
D3DApp& operator=(const D3DApp& rhs) = delete;
virtual ˜D3DApp();
public:
static D3DApp* GetApp();
HINSTANCE AppInst()const;
HWND MainWnd()const;
float AspectRatio()const;
bool Get4xMsaaState()const;
void Set4xMsaaState(bool value);
int Run();
virtual bool Initialize();
virtual LRESULT MsgProc(HWND hwnd, UINT msg, WPARAM
wParam, LPARAM lParam);
protected:
virtual void CreateRtvAndDsvDescriptorHeaps();
virtual void OnResize();
virtual void Update(const GameTimer& gt)=0;
virtual void Draw(const GameTimer& gt)=0;
// Convenience overrides for handling mouse input.
virtual void OnMouseDown(WPARAM btnState, int x, int
y){
    
     }
virtual void OnMouseUp(WPARAM btnState, int x, int
y) {
    
     }
virtual void OnMouseMove(WPARAM btnState, int x, int
y){
    
     }
protected:
bool InitMainWindow();
bool InitDirect3D();
void CreateCommandObjects();
void CreateSwapChain();
void FlushCommandQueue();
ID3D12Resource* CurrentBackBuffer()const
{
    
    
return mSwapChainBuffer[mCurrBackBuffer].Get();
}
D3D12_CPU_DESCRIPTOR_HANDLE
CurrentBackBufferView()const
{
    
    
return CD3DX12_CPU_DESCRIPTOR_HANDLE(
mRtvHeap->GetCPUDescriptorHandleForHeapStart(),
mCurrBackBuffer,
mRtvDescriptorSize);
}
D3D12_CPU_DESCRIPTOR_HANDLE DepthStencilView()const
{
    
    
return mDsvHeap-
>GetCPUDescriptorHandleForHeapStart();
}
void CalculateFrameStats();
void LogAdapters();
void LogAdapterOutputs(IDXGIAdapter* adapter);
void LogOutputDisplayModes(IDXGIOutput* output,
DXGI_FORMAT format);
protected:
static D3DApp* mApp;
HINSTANCE mhAppInst = nullptr; // 应用程序
instance handle
HWND mhMainWnd = nullptr; // 主窗口handle
bool mAppPaused = false; // 是否是程序暂停
bool mMinimized = false; // 是否是最小化
bool mMaximized = false; // 是否是最大化
bool mResizing = false; // 大小调整栏是否拖拽
bool mFullscreenState = false;// 是否全屏
// Set true to use 4X MSAA (§4.1.8). The default is false.
bool m4xMsaaState = false; // 4X MSAA enabled
UINT m4xMsaaQuality = 0; // quality level of 4X
MSAA
// Used to keep track of the “delta-time” and game time (§4.4).
GameTimer mTimer;
Microsoft::WRL::ComPtr<IDXGIFactory4> mdxgiFactory;
Microsoft::WRL::ComPtr<IDXGISwapChain> mSwapChain;
Microsoft::WRL::ComPtr<ID3D12Device> md3dDevice;
Microsoft::WRL::ComPtr<ID3D12Fence> mFence;
UINT64 mCurrentFence = 0;
Microsoft::WRL::ComPtr<ID3D12CommandQueue>
mCommandQueue;
Microsoft::WRL::ComPtr<ID3D12CommandAllocator>
mDirectCmdListAlloc;
Microsoft::WRL::ComPtr<ID3D12GraphicsCommandList>
mCommandList;
static const int SwapChainBufferCount = 2;
int mCurrBackBuffer = 0;
Microsoft::WRL::ComPtr<ID3D12Resource>
mSwapChainBuffer[SwapChainBufferCount];
Microsoft::WRL::ComPtr<ID3D12Resource>
mDepthStencilBuffer;
Microsoft::WRL::ComPtr<ID3D12DescriptorHeap>
mRtvHeap;
Microsoft::WRL::ComPtr<ID3D12DescriptorHeap>
mDsvHeap;
D3D12_VIEWPORT mScreenViewport;
D3D12_RECT mScissorRect;
UINT mRtvDescriptorSize = 0;
UINT mDsvDescriptorSize = 0;
UINT mCbvSrvDescriptorSize = 0;
// Derived class should set these in derived
constructor to customize
// starting values.
std::wstring mMainWndCaption = L”d3d App”;
D3D_DRIVER_TYPE md3dDriverType =
D3D_DRIVER_TYPE_HARDWARE;
DXGI_FORMAT mBackBufferFormat =
DXGI_FORMAT_R8G8B8A8_UNORM;
DXGI_FORMAT mDepthStencilFormat =
DXGI_FORMAT_D24_UNORM_S8_UINT;
int mClientWidth = 800;
int mClientHeight = 600;
};

GameTimer

//***************************************************************************************
// GameTimer.cpp by Frank Luna (C) 2011 All Rights Reserved.
//***************************************************************************************

#include <windows.h>
#include "GameTimer.h"

GameTimer::GameTimer()
: mSecondsPerCount(0.0), mDeltaTime(-1.0), mBaseTime(0), 
  mPausedTime(0), mPrevTime(0), mCurrTime(0), mStopped(false)
{
    
    
	__int64 countsPerSec;
	QueryPerformanceFrequency((LARGE_INTEGER*)&countsPerSec);
	mSecondsPerCount = 1.0 / (double)countsPerSec;
}

// Returns the total time elapsed since Reset() was called, NOT counting any
// time when the clock is stopped.
float GameTimer::TotalTime()const
{
    
    
	// If we are stopped, do not count the time that has passed since we stopped.
	// Moreover, if we previously already had a pause, the distance 
	// mStopTime - mBaseTime includes paused time, which we do not want to count.
	// To correct this, we can subtract the paused time from mStopTime:  
	//
	//                     |<--paused time-->|
	// ----*---------------*-----------------*------------*------------*------> time
	//  mBaseTime       mStopTime        startTime     mStopTime    mCurrTime

	if( mStopped )
	{
    
    
		return (float)(((mStopTime - mPausedTime)-mBaseTime)*mSecondsPerCount);
	}

	// The distance mCurrTime - mBaseTime includes paused time,
	// which we do not want to count.  To correct this, we can subtract 
	// the paused time from mCurrTime:  
	//
	//  (mCurrTime - mPausedTime) - mBaseTime 
	//
	//                     |<--paused time-->|
	// ----*---------------*-----------------*------------*------> time
	//  mBaseTime       mStopTime        startTime     mCurrTime
	
	else
	{
    
    
		return (float)(((mCurrTime-mPausedTime)-mBaseTime)*mSecondsPerCount);
	}
}

float GameTimer::DeltaTime()const
{
    
    
	return (float)mDeltaTime;
}

void GameTimer::Reset()
{
    
    
	__int64 currTime;
	QueryPerformanceCounter((LARGE_INTEGER*)&currTime);

	mBaseTime = currTime;
	mPrevTime = currTime;
	mStopTime = 0;
	mStopped  = false;
}

void GameTimer::Start()
{
    
    
	__int64 startTime;
	QueryPerformanceCounter((LARGE_INTEGER*)&startTime);


	// Accumulate the time elapsed between stop and start pairs.
	//
	//                     |<-------d------->|
	// ----*---------------*-----------------*------------> time
	//  mBaseTime       mStopTime        startTime     

	if( mStopped )
	{
    
    
		mPausedTime += (startTime - mStopTime);	

		mPrevTime = startTime;
		mStopTime = 0;
		mStopped  = false;
	}
}

void GameTimer::Stop()
{
    
    
	if( !mStopped )
	{
    
    
		__int64 currTime;
		QueryPerformanceCounter((LARGE_INTEGER*)&currTime);

		mStopTime = currTime;
		mStopped  = true;
	}
}

void GameTimer::Tick()
{
    
    
	if( mStopped )
	{
    
    
		mDeltaTime = 0.0;
		return;
	}

	__int64 currTime;
	QueryPerformanceCounter((LARGE_INTEGER*)&currTime);
	mCurrTime = currTime;

	// Time difference between this frame and the previous.
	mDeltaTime = (mCurrTime - mPrevTime)*mSecondsPerCount;

	// Prepare for next frame.
	mPrevTime = mCurrTime;

	// Force nonnegative.  The DXSDK's CDXUTTimer mentions that if the 
	// processor goes into a power save mode or we get shuffled to another
	// processor, then mDeltaTime can be negative.
	if(mDeltaTime < 0.0)
	{
    
    
		mDeltaTime = 0.0;
	}
}

实例:

书中源代码

//***************************************************************************************
// Init Direct3D.cpp by Frank Luna (C) 2015 All Rights Reserved.
//
// Demonstrates the sample framework by initializing Direct3D, clearing 
// the screen, and displaying frame stats.
//
//***************************************************************************************

#include "../../Common/d3dApp.h"
#include <DirectXColors.h>

using namespace DirectX;

class InitDirect3DApp : public D3DApp
{
    
    
public:
	InitDirect3DApp(HINSTANCE hInstance);
	~InitDirect3DApp();

	virtual bool Initialize()override;

private:
    virtual void OnResize()override;
    virtual void Update(const GameTimer& gt)override;
    virtual void Draw(const GameTimer& gt)override;

};

int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE prevInstance,
				   PSTR cmdLine, int showCmd)
{
    
    
	// Enable run-time memory check for debug builds.
#if defined(DEBUG) | defined(_DEBUG)
	_CrtSetDbgFlag( _CRTDBG_ALLOC_MEM_DF | _CRTDBG_LEAK_CHECK_DF );
#endif

    try
    {
    
    
        InitDirect3DApp theApp(hInstance);
        if(!theApp.Initialize())
            return 0;

        return theApp.Run();
    }
    catch(DxException& e)
    {
    
    
        MessageBox(nullptr, e.ToString().c_str(), L"HR Failed", MB_OK);
        return 0;
    }
}

InitDirect3DApp::InitDirect3DApp(HINSTANCE hInstance)
: D3DApp(hInstance) 
{
    
    
}

InitDirect3DApp::~InitDirect3DApp()
{
    
    
}

bool InitDirect3DApp::Initialize()
{
    
    
    if(!D3DApp::Initialize())
		return false;
		
	return true;
}

void InitDirect3DApp::OnResize()
{
    
    
	D3DApp::OnResize();
}

void InitDirect3DApp::Update(const GameTimer& gt)
{
    
    

}

void InitDirect3DApp::Draw(const GameTimer& gt)
{
    
    
    // Reuse the memory associated with command recording.
    // We can only reset when the associated command lists have finished execution on the GPU.
	ThrowIfFailed(mDirectCmdListAlloc->Reset());

	// A command list can be reset after it has been added to the command queue via ExecuteCommandList.
    // Reusing the command list reuses memory.
    ThrowIfFailed(mCommandList->Reset(mDirectCmdListAlloc.Get(), nullptr));

	// Indicate a state transition on the resource usage.
	mCommandList->ResourceBarrier(1, &CD3DX12_RESOURCE_BARRIER::Transition(CurrentBackBuffer(),
		D3D12_RESOURCE_STATE_PRESENT, D3D12_RESOURCE_STATE_RENDER_TARGET));

    // Set the viewport and scissor rect.  This needs to be reset whenever the command list is reset.
    mCommandList->RSSetViewports(1, &mScreenViewport);
    mCommandList->RSSetScissorRects(1, &mScissorRect);

    // Clear the back buffer and depth buffer.
	mCommandList->ClearRenderTargetView(CurrentBackBufferView(), Colors::LightSteelBlue, 0, nullptr);
	mCommandList->ClearDepthStencilView(DepthStencilView(), D3D12_CLEAR_FLAG_DEPTH | D3D12_CLEAR_FLAG_STENCIL, 1.0f, 0, 0, nullptr);
	
    // Specify the buffers we are going to render to.
	mCommandList->OMSetRenderTargets(1, &CurrentBackBufferView(), true, &DepthStencilView());
	
    // Indicate a state transition on the resource usage.
	mCommandList->ResourceBarrier(1, &CD3DX12_RESOURCE_BARRIER::Transition(CurrentBackBuffer(),
		D3D12_RESOURCE_STATE_RENDER_TARGET, D3D12_RESOURCE_STATE_PRESENT));

    // Done recording commands.
	ThrowIfFailed(mCommandList->Close());
 
    // Add the command list to the queue for execution.
	ID3D12CommandList* cmdsLists[] = {
    
     mCommandList.Get() };
	mCommandQueue->ExecuteCommandLists(_countof(cmdsLists), cmdsLists);
	
	// swap the back and front buffers
	ThrowIfFailed(mSwapChain->Present(0, 0));
	mCurrBackBuffer = (mCurrBackBuffer + 1) % SwapChainBufferCount;

	// Wait until frame commands are complete.  This waiting is inefficient and is
	// done for simplicity.  Later we will show how to organize our rendering code
	// so we do not have to wait per frame.
	

FlushCommandQueue();
}

运行结果:
在这里插入图片描述

环境配置

真就是把龙书代码拷贝过来都报错,打开下载源码,却能使用,那么无疑是高版本转低版本的问题,首先我们要创建的是窗口程序项目,将项目属性里的符合模式设置成默认即可成功:
在这里插入图片描述
环境呢,vs安装时候,安装好windos需要的sdk,系统支持dx12即可

猜你喜欢

转载自blog.csdn.net/weixin_56946623/article/details/127533818
今日推荐