通过Direct3D这种底层图形应用程序编程接口(Application Progamming Interface,API),即可在应用程序中对图形处理器(Graphics Processing Unit,GPU)进行控制和编程。我们能够借此以硬件加速的方式渲染出虚拟的3D场景。
只要GPU支持当前所用的Direct3D版本,我们就无须再考虑它的具体规格和硬件控制层面的实现细节。
例: 若要向GPU提交一个清除某渲染目标(如清屏)的命令,我们就可以调用Direct3D中的ID3D12GraphicsCommandList::ClearRenderTargetView方法。随后,Direct3D层和硬件驱动会协作将此Direct3D命令转换为系统中GPU可以执行的本地机器指令。
除了添加一些新的渲染特性以外,Direct3D 12经重新设计已焕然一新,较之上一个版本的主要改变在于其性能优化方面再大大减少了CPU开销的同时,又改进了对多线程的支持。为了达到这些性能目标,Direct3D 12的API较Direct3D 11更偏向于底层。另外,API抽象程度的降低使它更趋向于具体化,与现代GPU的构架也更为契合。当然,是哟个这种更复杂的API所得到的回报是:性能的提升。
组件对象模型(Component Object Model,COM)是一种令DirectX不受编程语言束缚,并且使之向后兼容的技术。
用C++语言编写DirectX程序时,COM帮我们隐藏了大量底层细节。
例: 要获取指向某COM接口的指针,需借助特定函数或另一COM接口的方法——而不是用C++语言中的关键字new去创建一个COM接口。另外COM对象会统计其引用次数;因此,在使用完某接口时,我们便应调用它的Release方法(COM接口的所有功能都是从IUnknown这个COM接口继承而来的,包括Release方法在内),而不是用delete来删除——当COM对象的引用计数为0时,它将自行释放自己所占用的内存。
为了辅助用户管理COM对象的生命周期,Window运行时库(Windows Runtime Library,WRL)专门为此提供了Microsoft::WRL::ComPtr类(#include
注意: COM接口都以大写字母"I"作为开头。
2D纹理(2D texture)是一种由数据元素构成的矩阵(可将此“矩阵”看作2D数组)。它的用途之一是存储2D图像数据,在这种情况下,纹理中每个元素存储的都是一个像素的颜色。
然而,纹理的用处并非仅此而已。例如,有种称作法线贴图(normal mapping) 的高级技术,其纹理内的每个元素存储的就是一个3D向量而不是颜色信息。
因此,尽管纹理给人的第一印象通常是用来存储图像数据,但其实际用途却十分广泛.
简单来讲,1D、2D、3D纹理就相当于特定数据元素所构成1D、2D、3D数组。
纹理其实还不只是像“数据数组”那样简单。它们可能还具有多种mipmap层级,而GPU则会据此对它们进行特殊的处理,例如运用过滤器(filter)和进行多重采样(multisample)。另外,并不是任意类型的数据元素都能用于组成纹理,它只能存储DXGI_FORMAT枚举类型中描述的特定格式的数据元素。
一些相关的格式示例:
1.DXGI_FORMAT_R32G32B32_FLOAT:每个元素由3个32位浮点数整数分量构成。
2.DXGI_FORMAT_R16G16B16A16_UNORM:每个元素由4个16位分量构成,每个分量都被映射到[0,1]区间。
3.DXGI_FORMAT_R32G32_UINT:每个元素由2个32位无符号整数分量构成。
4.DXGI_FORMAT_R8G8B8A8_UNORM:每个元素由4个8位无符号分量构成,每个分量都被映射到[0,1]区间。
5.DXGI_FORMAT_R8G8B8A8_SNORM:每个元素由4个8位有符号分量构成,每个分量都被映射到[-1,1]区间。
6.DXGI_FORMAT_R8G8B8A8_SINT:每个元素由4个8位有符号整数分量构成,每个分量都被映射到[-128,127]区间。
7.DXGI_FORMAT_R8G8B8A8_UINT:每个元素由4个8位无符号整数分量构成,每个分量都被映射到[0,255]区间。
注意:
大写字母R、G、B、A分别代表红色(red)、绿色(green)、蓝色(blue)和alpha。所有的颜色都是由红、绿、蓝三基色组合而成。alpha通道(或称为alpha分量)则通常用于控制透明度。
尽管格式名称在字面上指示的是颜色和alpha值,但纹理存储的却不一定是颜色信息:
可以利用坐标格式为浮点数的方式存储任意3D向量。
除此之外,亦有无类型(typeless)格式的纹理,我们仅用它来预留内存,待纹理被绑定到渲染流水线(rendering pipeline)之后,再具体解释它的数据类型(有点像C++语言里的强制转换)
为了避免动画中出现画面闪烁的现象,最好将动画帧完整地绘制在一种称为后台缓冲区的离屏(off-screen,即不可直接呈现在显示设备上之意)纹理内。只要将制定动画帧的整个场景绘到后台缓冲区中,它就会以一个完整的帧画面展现在屏幕上。依照此法,观者便不会察觉出帧的绘制过程——而只会观赏到完整的动画帧。为此,需要利用硬件管理的两种纹理缓冲区:即所谓的前台缓冲区(front buffer) 和 后台缓冲区(back buffer) 。
前台缓冲区存储的是当前显示在屏幕上的图像数据,而动画的下一帧则被绘制在后台缓冲区里。当后台缓冲区中的动画帧绘制完之后,两种缓冲区的角色互换:后台缓冲区变为前台缓冲区呈现新一帧的画面,而前台缓冲区则为了展示动画的下一帧转为后台缓冲区,等待填充数据。前后台缓冲的这种互换操作成为呈现(pressing)。
呈现是一种高效的操作,只需交换指向当前前台缓冲区和后台缓冲区的两个指针即可实现。
前台缓冲区和后台缓冲区构成了交换链(swap chain),在Direct3D中用IDXGISwapChain接口来表示。这个接口不仅存储了前台缓冲区和后台缓冲区两种纹理,而且还提供了修改缓冲区大小(IDXGISwapChain::ResizeBuffers)和呈现缓冲区内容(IDXGISwapChain::Present)的方法。
使用两个缓冲区(前台和后台)的情况成为双缓冲(double buffering, 亦有译作双重缓冲、双倍缓冲等)。当然,也可以运用更多的缓冲区,例,使用3个缓冲区就叫做三重缓冲(triple buffering)。对于一般的应用来说,使用两个缓冲区就足够了。
注意:
尽管后台缓冲区是一个纹理(因为构成纹理的基本元素又称纹素,texel),但我们仍常将其组成元素称为像素,因为就后台缓冲区这种情况而言,它所存储的内容是颜色信息。即便纹理中存储的不是颜色信息,有时也称纹理的元素为像素(如“法线图中的像素”)。
补:至于二者是否需要区分,具体还要看应用场景。比如谈到像素与纹素的映射关系时,必须将这两个概念予以区分。
深度缓冲区(depth buffer) 这种纹理资源存储的并非图像数据,而是特定像素的深度信息。深度值的范围为0.0~1.0。
0.0代表观察者在视锥体(view frustum,常称该形为平截头体)中能看到离自己最近的物体,1.0则代表观察者在视锥体中能看到离自己最远的物体。
深度缓冲区中的元素与后台缓冲区内的像素呈一一对应关系(即后台缓冲区中第i行第j列的元素队对应于深度缓冲区内第i行第j列的元素)。所以,如果后台缓冲区的分辨率为1280×1024,那么深度缓冲区中就应当有1280×1024个深度元素。
为了确定不同物体间的像素前后顺序,Direct3D采用了一种叫做深度缓冲(depth buffering) 或 z缓冲(z-buffering,其中z指z坐标) 的技术。着重强调:若使用了深度缓冲,则物体的绘制顺序也就变得无关紧要了。
以下图进行举例。途中展示了观察者看到的立体空间,以及该立体空间的2D侧视图。从图中可以看到,有3种不同物体的像素都争着渲染在观察窗口内的像素P上。在开始渲染之前,后台缓冲区会被清理为默认颜色,深度缓冲区也将被清除为默认值——通常为1.0(即像素能够取到的最远深度值)。现在,假设这些物体的渲染顺序依次为圆柱体->球体->圆锥体。下面的列表总结了像素P和它对应的深度值d按物体的绘制顺序依次更新的过程。类似的处理流程也发生在其他像素上。
操作步骤 | P | d | 步骤叙述 |
---|---|---|---|
清除缓冲区操作 | 黑色 | 1.0 | 对像素及其对应的深度元素进行初始化 |
绘制圆柱体 | P3 | d3 | 因为d3<=d=1.0,深度测试通过,更新缓冲区,使P=P3,d=d3 |
绘制球体 | P1 | d1 | 因为d1<=d=d3,深度测试通过,更新缓冲区,使P=P1,d=d1 |
绘制圆锥体 | P2 | d2 | 因为d2>d=d1,深度测试失败,不更新缓冲区 |
可以看出,只有找到具有更小深度值的像素,才会对观察窗口内的像素及其位于深度缓冲区种的对应深度值进行更新。按照这种方法逐步处理,待完成所有的比较和更新工作后,最终得到渲染的即为距离观察者最近的像素。
总而言之,深度缓冲技术的原理使计算每个像素的深度值,并执行深度测试(depth test)。而深度测试则用于对竞争写入后台缓冲区中同意像素的多个像素深度值进行比较。具有最小深度值的像素(也说明该像素离观察者最近)会获得最终的胜利,它将被写入后台缓冲区中。
深度缓冲区也是一种纹理,所以一定要用明确的数据格式来创建它。深度缓冲可用的格式包括一下几种:
1.DXGI_FORMAT_D32_FLOAT_S8X24_UINT:该格式共占用64位,取其中的32位指定一个浮点型深度缓冲区,另有8位(无符号整数)分配给模板缓冲区(stencil buffer),并将该元素映射到[0,255]区间,剩下的24位仅用于填充对齐(padding)不作他用。
2.DXGI_FORMAT_D32_FLOAT:指定一个32位浮点型深度缓冲区。
3.DXGI_FORMAT_D24_UNORM_S8_UINT:指定一个无符号24位深度缓冲区,并将该元素映射到[0,1]区间。另有8位(无符号整型)分配给模板缓冲区,并将此元素映射到[0,255]区间。
4.DXGI_FORMAT_D16_UNORM:指定一个无符号16位深度缓冲区,把该元素映射到[0,1]区间。
注意:
一个应用程序不一定要用到模板缓冲区。但一经使用,则深度缓冲区将总是与模板缓冲区如影随形,共同进退。例,32位格式DXGI_FORMAT_D24_UNORM_S8_UINT使用24位作为深度缓冲区,其他8位作为模板缓冲区。出于这个原因,深度缓冲区叫作深度/模板缓冲区更为得体。
在渲染处理的过程中,GPU可能会对资源进行读(例,从描述物体表面样貌的纹理或者存有3D场景中几何体位置信息的缓冲区中读取数据)和写(例,向后台缓冲区或深度/模板缓冲区写入数据)两种操作。
在发出绘制命令之前,我们需要将与本次绘制调用(draw call)相关的资源绑定(bind或称链接,link) 到渲染流水线上。部分资源可能在每次绘制调用时都会有所变化,所以我们也就要每次按需更新绑定。但是,GPU资源并非直接与渲染流水线相绑定,而是要通过一种名为绑定符(descriptor) 的对象来对它间接引用,我们可以把描述符视为一种对送往GPU的资源进行描述的轻量级结构。从本质上来讲,它实际上即为一个中间层:若指定了资源描述符,GPU将既能获得实际的资源数据,也能了解到资源的必要信息。因此,我们将把绘制调用需要引用的资源,通过指定描述符的方式绑定到渲染流水线。
Q: 为什么我们要额外使用描述符这个中间层呢?
A: 究其原因,GPU资源实质都是一些普通的内存块。由于资源的这种通用性,它们便能被设置到渲染流水线的不同阶段供其使用。一个常见的例子是先把纹理用作渲染目标(即Direct3D的绘制到纹理技术),随后再将该纹理作为一个着色器资源(即此纹理会经采样而用作着色器的输入数据)。不管是充当渲染目标、深度/模板缓冲区还是着色器资源等角色,仅靠资源本身是无法体现出来的。而且,我们有时也许只希望将资源中的部分数据绑定至渲染流水线。但如何从整个资源中将它们选取出来呢?再者,创建一个资源可能用的是无类型格式,这样的话,GPU甚至不会知道这个资源的具体格式。
解决上述问题就是引入描述符的原因。除了指定资源数据,描述符还会为GPU解释资源:它们会告知Direct3D某个资源将如何使用(即此资源将被绑定在流水线的哪个阶段上),而且我们可借助描述符来指定欲绑定资源中的局部数据。这就是说,如果某个资源在创建的时候采用了无类型格式,那么我们就必须在为它创建描述符时指明其具体类型。
每个描述符都有一种具体类型,此类型指明了资源的具体作用。此处列举出一些描述符:
1.CBV/SRV/UAV 描述符分别表示的是常量缓冲区视图 (constant buffer view) 、着色器资源视图 (shader resource view) 和无序访问视图 (unordered access view) 这3种资源。
2.采样器(sample, 亦有译为取样器)描述符表示的是采样器资源(用于纹理贴图)。
3.RTV 描述符表示的是渲染目标视图资源 (render target view) 。
4.DSV 描述符表示的是深度/模板视图资源 (depth/stencil view) 。
描述符堆(descriptor heap) 中存有一系列描述符(可将其看作是描述符数组),本质上是存放用户程序中某种特定类型描述符的一块内存。我们需要为每一种类型的描述符都创建出单独的描述符堆。另外也可以为同一种描述符类型创建出多个描述符堆。
我们能用多个描述符来引用同一个资源。例,可以通过多个描述符来引用同一个资源中不同的局部数据。而且,一种资源可以绑定到渲染流水线的不同阶段。因此,对于每个阶段都需要设置独立的描述符。例,当一个纹理需要被用作渲染目标与着色器资源时,我们就要为它分别创建两个描述符:一个RTV描述符和一个SRV描述符。类似地,如果以无类型格式创建了一个资源,又希望该纹理中的元素可以根据需求当作浮点值或整数值来使用,那么就需要为它分别创建两个描述符:一个指定为浮点格式,另一个指定为整数格式。
创建描述符的最佳时机为初始化期间。由于在此过程中需要执行一些类型的检测和验证工作,所以最好不要在运行时 (runtime) 才创建描述符。
注意:
当确实需要用到无类型资源所带来的灵活性时(即根据不同的视图对同一种数据进行多种不同解释的能力),再以这种方式来创建资源,否则应创建完整类型的资源。