预备知识
COM
组件对象模型(Component Object Modle),由Windows运行时库(Windows Runtime Library)提供,位于Microsoft::WRL::ComPtr中,通过头文件wrl.h引用。通常将它视为一种接口,但在DirectX中暂且视为类。
所有COM对象都继承自IUnknown这一接口,会统计引用计数。当引用计数归零时,它会自行释放。
在DX12中,常会用到以下ComPtr方法:
-
Get()返回一个指向此底层COM接口的原始指针,也就是普通的c++指针,常用于把原始的COM接口指针作为参数传递给函数。
持有原始指针的函数可以直接用原始指针调用方法,除非在需要存为成员变量以便异步使用或类似的情况,不必转回ComPtr。
但持有原始指针的函数不应接管指针的生命周期,例如
ReleaseAndGetAddressOf()和Reset()。
-
GetAddressOf()返回指向此底层COM接口的原始指针的地址,通常用于初始化ComPtr对象。
由于
GetAddressOf()在使用时并不会操作引用计数,如果该ComPtr原始不为空,通过该方法获取地址并修改,则原始指向的对象不会被释放,容易造成内存泄漏。因此,在DX12开发中,大多时候都会使用更安全的ReleaseAndGetAddressOf()。
-
ReleaseAndGetAddressOf()释放原本的ComPtr对象,清除相关引用,并返回地址,通常用于初始化Comptr对象。
-
Reset()将该ComPtr对象设置为nullptr并释放与之相关的所有引用。
纹理格式
2D textrue是由数据元素构成的矩阵,用途之一是存储2d图像,允许存储DXGI_FORMAT枚举中的所描述的特定类型的数据类型。
DXGI_FORMAT枚举 示例>
| 类型 | 说明 |
|---|---|
| DXGI_FORMAT_R32G32B32_FLOAT | 每个元素由三个32位浮点数分量构成 |
| DXGI_FORMAT_R16G16RB16A16_UNORM | 每个元素由四个映射到\([0,1]\)区间的16位分量构成 |
| DXGI_FORMAT_R32G32_UINT | 每个元素由两个32位无符号整数构成 |
| DXGI_FORMAT_R8G8R8A8_UNORM | 每个元素由四个映射到\([0,1]\)区间的8位分量构成 |
| DXGI_FORMAT_R8G8B8A8_SNORM | 每个元素由四个映射到\([-1,1]\)区间的8位分量构成 |
| DXGI_FORMAT_R8G8B8A8_SINT | 每个元素由四个8位有符号整数分量构成 |
| DXGI_FORMAT_R8G8B8A8_UINT | 每个元素由四个8位无符号整数分量构成 |
除此以外,还有无类型格式的纹理,通常用于预留内存,待绑定到渲染管线时才指定类型。这一过程类似于指定如何翻译这一部分的数据,需要注意只能翻译为相同宽度(位)的类型。例如:
Swap Chain
为了避免画面闪烁,通常在渲染时会将整个场景绘制完成后再显示。为此需要存在至少两个缓冲区分别用于显示当前帧和绘制下一帧,当下一帧绘制完成后交换两个缓冲区的位置,并在非当前显示的缓冲区绘制下一帧。
这种多个缓冲区交替显示构成的系统叫Swap Chain(交换链),交换两个缓冲区的行为称为Presenting(呈现)。通常情况下两个缓冲区足矣,称为双缓冲,分别将两个缓冲区叫做前台缓冲区和后台缓冲区。
Swap Chain的缓冲区以2d纹理的方式存储。
Depth Buffer
深度缓冲是一种纹理,存储特定像素的深度信息,值为\([0,1]\)。它是一种相对的深度,\(0.0\)代表视锥体中看到的离自己最近的物体,\(1.0\)代表是视锥体中能看到的距离自己最远的物体。
绘制时,会对原有像素和新像素的深度进行比较(深度测试),只有深度更小的像素会被绘制。
深度缓冲区同样以2d纹理的方式存储,且其数据规模与后台缓冲区应相同。
深度缓冲允许使用的纹理格式列表
| 类型 | 说明 |
|---|---|
| DXGI_FORMAT_D32_FLOAT_S8X24_UINT | 每个元素宽64位,取其中32位作为浮点型深度缓冲区,8位无符号整数分配给模板缓冲区,剩余24位用于内存对齐不做他用。 |
| DXGI_FORMAT_D32_FLOAT | 每个元素为32位浮点数深度缓冲区 |
| DXGI_FORMAT_D24_UNORM_S8_UINT | 每个元素宽32位,取其中24位作为映射到\([0.0,1.0]\)的无符号深度缓冲,8位无符号整数分配给模板缓冲区 |
| DXGI_FORMAT_D16_UNORM | 每个元素为映射到\([0.0,1.0]\)的16位深度缓冲 |
Stencil Buffer
模板缓冲区,通常和深度缓冲区绑定。
Bind&Descriptor
发出绘制命令前,需要将该次绘制调用相关的资源绑定(Bind)到渲染管线。部分资源可能会在没每次绘制调用时都发生变化,所以需要在每次绘制调用前按需更新绑定。
渲染管线通过描述符(Descriptor)间接引用GPU资源。描述符是一个中间层,是对送往GPU的资源进行解释的结构。
注意事项
在DX12中,View和Descriptor是同义词,有时会交替使用。
每个描述符都有一个具体类型,用于指定所描述资源的作用,DX12中常用的描述符如下:
- CBV/SRV/UAV:分别表示常量缓冲区视图(描述符)、着色器资源视图(描述符)和无序访问视图(描述符);
- sample描述符表示采样器资源。
- RTV:表示渲染目标视图(描述符),常见的渲染目标包括前台缓冲区、后台缓冲区。
- DSV:表示深度/模板视图(描述符)。
一系列特定类型的描述符构成了描述符堆。堆是一段可以根据需要分配和释放的连续内存,描述符堆在底层接近于一个连续空间上的数组。
创建描述符通常(且最好)在初始化期间,最好不要在运行时创建描述符。
Multisampling
为了在显示像素受限的情况下改善走样(即锯齿),开发过程中会使用各种反走样(抗锯齿)技术,常见的抗锯齿技术包括SSAA(超级采样,Super Sample Anti-Aliasing)和MSAA(多重采样,MultuSample Anti-Aliasing)。
SSAA是一种开销昂贵的技术,它使用4倍屏幕分辨率大小的后台缓冲区和深度缓冲区,当后台缓冲区被推向前台时,会将后台缓冲区按4个像素一组进行解析(降采样),以四个像素的平均值作为实际像素的值。它需要计算四倍分辨率像素的颜色,而计算像素颜色是渲染管线中开销最大的操作之一。
MSAA同样使用多倍屏幕分辨率大小的后台缓冲区和深度缓冲区,和SSAA的区别在于MSAA对每组子像素只计算一次颜色(而不是SSAA的逐像素计算),但会对每个子像素进行深度测试,只有通过深度测试的的像素会更新为新的颜色;当后台缓冲区被推到前台时,会对子像素分组进行加权均值计算。
注意事项
采样时,子像素划分并非将一个子像素均匀地切分为多个子像素,实际的切割往往带有不同的旋转与平移,因此各个子像素的中心点和像素的中心点距离也不一定相等。
在DX12中,采样使用DXGI_SAMPLE_DESC描述符,它是一个结构体,原型如下:
Feature Level
功能级别。
DXGI
DirectX Graphics Infrastructure,一套与Direct3D配合使用的API,用于处理多种图形API中共有的底层任务,以及一些常用的图形功能。
性能计时器 Performance timer
相关方法位于<windows.h>中。
性能计时器使用count作为时间计量单位,常见用法如下:
-
QueryPerformanceCounter((LARGE_INTEGER*)&currtime)获取当前的时刻值,单位为
count。其中,
currtime的类型是__int64。
-
QueryPerformanceFrequency((LARGE_INTEGER*)&countsPerSec)获取性能计时器的频率,单位是
count/sec,由此可以推出:1.0/(double)countsPerSec:将count转换为以秒为单位的时长。
游戏计时器 GameTimer
GameTimer典型结构
GameTimer是一个由开发者实现的普通类,用于在游戏中处理时间。
具体时间可以详见DX12龙书4.4.2-4.4.4。
帧的统计信息
为了测量和记录FPS,会使用CalculateFrameStats方法,这个方法也需要自行实现,或使用已有框架。在DX12中建议使用包含该方法的D3DApp,但实践中往往需要更复杂的计算,D3DApp仅能作为参考。
CalculateFrameStates使用例
每一帧都要调用以上方法。