DirectX12 3D 游戏开发与实战第四章内容(上)

Direct3D的初始化(上)

学习目标

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

4.1预备知识

要学习Direct3D的初始化流程,我们需要了解一些基本的图形学概念以及Direct3D中常用数据类型的相关知识,本节将会着重讲解这些细节,以防止在后面讲解Direct3D的初始化流程中被这些细枝末节影响

4.1.1Direct3D 12概述

通过Direct这种底层图形应用程序编程接口,可以在应用程序中对图形处理器进行控制和编程,便可以借此以硬件加速的方式渲染出虚拟的3D场景。

例子:如果要向GPU提交一个清除某渲染目标(例如清屏)的命令,我们可以调用Direct3D中的ID3D12GraphicsCommandList::ClearReanderTargetView方法,然后Direct3D层和硬件驱动会将此Direct3D命令转换为系统中GPU可以执行的本地机器指令。只要GPu支持当前所用的Direct3D版本,我们就可以不用考虑它的具体规格和硬件控制层面的实现细节

Driect3D 12新特性:12相对于11而言,在性能方面大大减少了CPU开销的同时,又改进了对多线程的支持。因此,Direct3D 12的API更接近底层,开发人员需要付出更多时间才能完成一个项目的开发,不过这样带来的回报就是:性能的提升

4.1.2组件对象模型

组件对象模型(Component Object Model,COM)是一种令DriectX不受编程语言限制,而且可以使他向后兼容的技术。我们一般把COM对象视为一种接口,但考虑到当前编程的目的,我们便将他是做一个c++类来使用。如果要获取指向某COM接口的指针,需要借助特定函数或另一COM接口的方法,而不是使用C++的关键字new去创建一个COM接口,使用完某接口之后,我们应该使用Release方法,而不是使用c++的关键字delete。

Windows运行时库(Windows Runtime Library,WRL)提供了Microsoft::WRL::ComPtr类,它相当于是COM对象的智能指针。当一个ComPtr实例超出作用域范围时,它会自动调用Release方法自动销毁实例。本书常用的3个ComPtr方法如下

//1、Get:返回一个指向此底层COM接口的指针,此方法常用于把原始的COM接口指针作为参数传递给函数
ThrowIfFailed(mCommandList->Reset(mDirectCmdListAlloc.Get(),nullptr));

//2、GetAddressof:返回指向此底层COM接口指针的地址,此方法即可利用函数参数返回COM接口的指针(函数输出),例如
D3D12SerializeRootSignature(&rootSigDesc, D3D_ROOT_SIGNATURE_VERSION_1,
serizlizedRootSig.GetAddressOf(),
errorBlod.GetAddressOf());

//3、Reset:将Comptr实例设置为nullptr(将指针置为空)

4.1.3纹理格式

2D纹理是一种由数据元素构成的矩阵,它的用途之一是存储2D图像数据。在这种情况之下,纹理中的每一个元素存储的都是一个像素的颜色。但纹理的用处并非作为存储2D图像数据的容器。后续会继续介绍。并不是任意类型的数据元素都可以用于组成纹理,它只能存储DXGI_FORMAT枚举类型中描述的特定格式的数据元素。下面是一些相关的格式实例

//1、每个元素由3个32位浮点数分量构成
DXGI_FORMAT_R32G32B32_FLOAT
//2、每个元素由4个16位分量构成,每个分量都被影映射到0-1区间
DXGI_FORMAT_R16G16B16A16_UNORM
//3、每个元素由2个32位无符号整数分量构成
DXGI_FORMAT_R32G32_UINT
//4、每个元素由4个8位无符号分量构成,每个分量都被映射到0-1区间
DXGI_FORMAT_R8G8B8A8_UNORM
//5、每个元素由4个8位有符号分量构成,每个分量都被映射到-1-1区间
DXGI_FORMAT_R8G8B8A8_SNORM
//6、每个元素由4个8位有符号整数分量构成,每个分量都被映射到-128-127区间
DXGI_FORMAT_R8G8B8A8_SINT
//7、每个元素由4个8位无符号整数分量构成,每个分量都被映射到映射到0-255区间
DXGI_FORMAT_R8G8B8A8_UINT

4.1.4交换链和页面翻转

为了避免动画中出现画面闪烁,我们需要利用硬件管理的两种纹理缓冲区,即前台缓冲区和后台缓冲区。

前台缓冲区:存储当前显示在屏幕上的图像数据

后台缓冲区:存储动画的下一帧绘制

当后台缓冲区的动画绘制完成之后,两种缓冲区的角色互换,后台缓冲区转换为前台缓冲区呈现新的画面,而前台缓冲区则作为展示动画的下一帧成为后台缓冲区,等待填充数据。前后台缓冲区互换的操作称为呈现。

前台缓冲区和后台缓冲区构成的交换链,在Direct3D中用IDXGISwapChain接口来表示,这个接口不仅存储了其那台缓冲区和后台缓冲区这两个纹理,还提供了修改缓冲区的大小(IDXGISwapChain::ResizeBuffers)和呈现缓冲区的内容(IDXGISwapChain::Present)的方法,在一些情况下,我们甚至可以使用三缓冲机制。

4.1.5深度缓冲

深度缓冲区这种纹理资源存储的不是图像数据,而是一些特定像素的深度信息。深度值的范围为0.0-1.0,其中0.0为观察者在视椎体中看到的离自己最近的物体。深度缓冲区的元素和后台缓冲区的元素是一一对应的,所以如果后台缓冲区有的分辨率为12801024,则深度缓冲区中有12801024个元素。

为了确定不同物体的像素的前后顺序,Direct3D采用了深度缓冲的技术。比如当我们要绘制三个物体时,我们首先要清除缓冲区,对像素以及它的深度元素进行初始化,然后依次对三个物体进行深度测试,当找到具有更小深度值的像素的时候,计算机才会对观察窗口内的像素以及其在深度缓冲区中对应的深度值进行更新。

总结:深度缓冲技术的原理是计算每一个像素的深度值,然后进行深度测试,然后对竞争写入后台缓冲区的同一像素的多个像素深度值进行比较,然后将最小的像素深度值所对应的像素写入后台缓冲区中。

附录:

由于深度缓冲区也是一种纹理资源,所以一定要用对应的数据格式去创建它,下面是一些常用的深度缓冲区数据格式:

DXGI_FORMAT_D32_FLOAT_S8X24_UINT;

DXGI_FORMAT_D32_FLOAT;

DXGI_FORMAT_D24_UNORM_S8_UINT;

DXGI_FORMAT_D16_UNORM;

4.1.6资源与描述符

视图(view)和描述符(descriptor)是同义词,视图是Direct3D先前版本的常用术语,不过它现在只出现在Direct3D12的部分API中,我们只要清楚,视图和描述符是用一个东西即可

在渲染处理过程中,GPU可能会对资源进行读写操作,在发出绘制命令之前,我们需要把与本次绘制调用的相关资源链接到渲染流水线上,部分资源可能每次绘制都会进行更新,所以我们要在每次绘制之前都对链接的资源进行更新。GPU资源并不是直接和渲染流水线相互链接,而是通过一种名为描述符的对象来实现对它的间接引用。我们可以把描述符看成一种对送往GPU的资源进行描述的轻量级结构。

为什么要引用描述符这个中间层呢?

因为GPU资源是指是一些普通的内存块,由于资源的这种通用性,同一个资源可以在渲染流水线上的不同阶段使用,但是也产生了一些问题。

  1. 不论是作为渲染目标,还是深度缓冲区或者模板缓冲区,仅靠资源本身是无法让计算机识别出来的
  2. 我们有时希望将资源中的部分数据链接到渲染流水线中,这仅靠资源本身也是无法实现的
  3. 当一个资源使用的是无类型格式时,GPU甚至无法识别该资源的具体格式

为了解决上述问题,我们便引入了描述符这个概念

每个描述符都有一种具体的类型,这些类型指定了资源的具体作用。以下是一些常用的描述符类型:

  1. CBV/SRV/UAV:常量缓冲区视图(constant buffer view)、着色器资源视图(shader resource view)、无序访问视图(unordered access view)
  2. 采样器(sampler)描述符描述的是采样器资源(用于纹理贴图)
  3. RTV描述符表示的是渲染目标视图资源(render target view)
  4. DSV描述符表示的是深度/模板视图资源(depth/stencil view)

描述符堆:描述符堆里面有一系列的描述符(可以将其视为描述符数组),本质上存放用户程序中某种特定类型描述符的一块内存。我们要为每一种描述符创建出单独的描述符堆(为每一种类型的描述符创建一个数组),也可以为同一种描述符创建多个描述符堆。

4.1.7多重采样技术的原理

由于屏幕中显示的像素不是无穷小的,所以并不是任意一条直线都可以在显示器中平滑的显示出来,大部分都会出现“阶梯”效果。为了改善这个问题,我们可以将提高显示器的分辨率,使“阶梯”效果不那么容易被用户发现。

但在大多数情况下,我们不能够提升显示器的分辨率,所以我们一般会运用各种反走样的技术,其中有一种技术叫做超级采样(SSAA),它是使用4倍于屏幕分辨率大小的后台缓冲区和深度缓冲区,在后台缓冲区将要把数据调往显示器显示的时候,会将后台缓冲区中每四个像素为一组进行解析。这种方法是通过软件的方式提高画面的分辨率。超级采用是一种需要耗费高额开销的操作,在Direct3D中,还支持一种效果和开销都较为折中的反走样技术,叫做多重采样(MSAA),现假设使用4X多重采样,并同样使用4倍的后台缓冲区和深度缓冲区,这种技术不用对每一个像素进行计算,它计算一次中心像素的颜色,然后基于可视性和覆盖性,将得到的信息分享给其他子像素(使一次计算,受惠4个像素,从而减少开销)。

4.1.8利用Direct3D进行多重采样

4.1.9功能级别

从Direct3D 11开始便有了功能级别(D3D_FEATURE_LEVEL)的概念,功能级别为不同级别所支持的功能进行了严格的界定,一款支持DX11的GPU,除了一些特定的功能之外,其他功能必须存在。这样可以方便程序员使用对应的API,否则程序员在使用一些API时还要先对该硬件是否支持该功能进行检测。为了照顾更多的用户,每一款程序都应该要从最新版到最旧版逐一进行检测,即:Direct11到10再到9.3

4.1.10DirectX图形基础结构(重点)

略(在这一节会介绍DirectX图像基础结构,即DXGI(DirectX Graphics Infrastructure))

4.1.11功能支持的检测

4.1.12资源驻留

复杂的游戏一般都会运用大量的纹理和3D网格,但其中大多数并不是要一直放在显存中给GPU使用,所以我们应该要在应用程序中通过控制资源在显存中的去留,主动管理资源的驻留情况,所以我们一般要将短时间内不会再次使用的资源清出显存

4.2CPU和GPU之间的交互

在进行图形编程的时候,有两种处理器在参与处理工作,即CPU和GPU。为了获得最佳性能,我们一般让两者尽量同时工作,少一点同步。

4.2.1命令队列和命令列表

每个GPU都维护着至少一个命令队列(本质是环形缓冲区),CPU可以利用命令列表将命令提交到这个队列中,在一系列命令被提交到队列中之后,这些命令并不会马上执行,GPU会处理先前插入命令队列的命令。后来的新命令必须在前面的命令被执行完毕之后才会执行。

在Direct12中,命令队列被抽象为ID3D12CommandQueue接口来表示,我们要通过填写D3D12_COMMAND_QUEUE_DESC结构体来描述队列,然后再调用ID3D12Device::CreateCommandQueue方法来创建队列。

ExecuteCommandList是一种常用的ID3D12CommandQueue接口方法,它可以将命令列表中的命令添加到命令队列中进行等待,供GPU在合适的时机执行。在代码中,我们一般通过ID3D12GraphicsCommandList接口的方法来向命令列表中添加命令,只有调用ExecuteCommandList方法才会把命令列表中的命令添加到命令队列中进行等待。同时在所有的命令都被添加到命令列表中之后,我们应该要使用ID3D12GraphicsCommandList::Clsoe方法来结束命令的记录。

命令分配器(ID3D12CommandAllocator):记录在命令列表中的命令,实际上是存储在命令分配器中,当调用ID3D12CommandList::ExecuteCommandList方法之后,命令队列会引用命令分配器中的命令,而命令分配器则有ID3D12Device接口来创建。我们可以创建出多个关联于同一命令分配器的命令列表,但不能同时用他们来记录命令,即我们在使用一个命令列表记录命令时,必须关闭同一命令分配器所关联的其他命令列表。这意味着命令列表中的命令必须要一个一个的添加到命令分配器中。

注意:重置命令列表不会影响命令队列中的命令,因为相关的命令分配器依旧在维护着那些即将被命令队列引用的命令,反而言之,重置命令分配器会影响到命令队列中命令的执行,因此,在没有确定GPU执行完命令分配器中所有的命令之前,千万不要重置命令分配器

4.2.2CPU和GPU的同步

本节主要介绍围栏的使用,当我们通过CPU将数据a传入GPU中,CPU提前于GPU完成了数据b的接收,这时如果CPU提交命令,将会让数据a被数据b覆盖。为了避免出现这种严重错误,我们可以采用围栏(相当于标识),起初我们将围栏值设为0,每次提交一次命令列表+1,然后强制CPU等待,当GPU执行完命令队列中的命令之后,解除CPU的等待,即可避免出现覆盖的错误。但是这种方法并不完美,因为它会使CPU处于空闲状态,后续将会介绍更好的方法。

4.2.3资源转换

资源冒险:为了实现常见的渲染效果,我们经常通过GPU对某个资源按顺序进行读写操作,但是有些时候GPU的写操作还没有完成,却开始读取资源,这样便会出现资源冒险这个问题。

为了解决资源冒险,Direct3D针对资源设计了一组相关状态,资源在创建时会处于默认状态,该状态会一直持续到应用程序通过Direct3D将它转变成另一种状态为止,这样GPU便可以根据资源的状态来确定是否要对它进行读取操作,从而防止资源冒险的行为出现。

在Direct3D12之前,有关资源状态的转换的一系列工作都是交由驱动来管理,因此性能会较差,在DX12中,资源状态要依靠我们手动进行转换(如果转换不当,会使性能比DX11更差),通过命令列表设置转换资源屏障数据,就可以指定资源的转换,在代码中,资源屏障用D3D12_RESOURCE_BARRIER结构体表示,在Direct3D12中,许多结构体都有其所对应的扩展辅助结构变体,这些扩展结构体使用起来更加方便,我们一般都使用这些变体,以CD3DX12作为前缀的变体都定义在d3dx12.h文件中,转换资源屏障对应的变体为:CD3DX12_RESOURCE_BARRIER。(事实上,我们可以把转换资源屏障看作是一条用来告知GPU某资源状态正在进行转换的命令,在收到这条命令之后,GPU便会采取措施避免产生资源冒险)。

4.2.4命令与多线程

新手暂时跳过

posted @ 2019-09-16 22:59  风雅yaya  阅读(1617)  评论(0编辑  收藏  举报