学习目标
- 对Direct 3D编程在3D硬件中扮演的角色有基本了解;
- 理解COM在Direct 3D中扮演的角色;
- 学习基本的图形学概念,比如存储2D图像、页面切换,深度缓冲、多重纹理映射和CPU与GPU如何交互;
- 学习如何使用性能计数函数读取高精度时间;
- 学习如何初始化Direct 3D;
- 熟悉本书Demo通用的应用框架中的基本结构。
1 前言
在理解Direct3D初始化步骤前,需要我们先熟悉一些图形学概念和Direct3D类型。
1.1 Direct3D 12 概述
Direct3D是一个底层图形API用来控制和对GPU编程,它可以让我们使用硬件加速来渲染3D图形;比如要向GPU提交一个清空渲染目标的命令,我们可以调用方法ID3D12CommandList::ClearRenderTargetView。
Direct3D 12添加了一些新的渲染特性,但是主要的提升在于它被重新设计用来减少CPU的开销和提高多线程支持。
1.2 COM
Component Object Model (COM)可以让DirectX成为独立的编程语言并且让它向下兼容。我们通常像使用C++类一样,以接口的形式应用COM对象。值得注意的是,我们通常使用特定的函数或者其他COM接口来获得COM接口引用的指针,我们不能使用C++中的new关键字直接创建COM对象;另外COM对接口是引用计数的,当我们使用完毕后,需要调用它的Release方法后(而不是Delete),当其引用计数等于0时,COM对象会释放其占用的内存。
为了管理COM对象的生命周期,Windows Runtime Library (WRL)提供了Microsoft::WRL::ComPtr类(#include <wrl.h>)可以看做是COM对象的智能指针,当ComPtr超出范围时,它会自动调用它包含的COM对象的Release方法。本书中主要用到的ComPtr的三个方法如下:
- Get:返回其包含的COM接口,这个主要用于在函数中传递参数;
- GetAddressOf:返回其包含的COM接口的指针指向的地址,这个主要用于在函数参数中传递COM指针;
- Reset:将ComPtr接口设置为空指针nullptr,并且减少其包含的COM接口的引用计数,相同的,你也可以给ComPtr对象赋值为nullptr;
当然,COM还有更多方法,但是对高效实用Direct3D来说,不需要。
1.3 贴图格式
一张2D贴图是数据元素的矩阵。2D贴图的其中一个用法是用来保存2D图像,其每个元素用来保存像素的颜色,当然,它的用途不仅于此,比如在法线贴图中,其每个元素用来保存3D向量;一张贴图也不仅限于是保存数据数组,它们可以包含纹理映射等级,还可以让GPU对其进行过滤和多重纹理映射等特殊操作。贴图中不能保存任意格式的数据,它只能保存在DXGI_FORMAT共用体中定义了的几种类型,其中一些格式类型如下:
- DXGI_FORMAT_R32G32B32_FLOAT:每个元素包含3个32位的浮点数组件;
- 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;
还有一些无类型格式,我们只是申请了内存,等到使用它的时候在申明它的类型,比如DXGI_FORMAT_R16G16B16A16_TYPELESS
1.4 交换链和页面切换
为了避免动画中的闪烁问题,使用多个缓冲来交换显示,只有画面在离屏缓冲中渲染完毕后,才切换到屏幕中显示;前和后缓冲形式的交换链在Direct3D中使用IDXGISwapChain接口来表示;其提供重置尺寸方法:IDXGISwapChain::ResizeBuffers和呈现方法:IDXGISwapChain::Present(交换2个缓冲前后位置)。
使用两个缓存称之为双缓冲,使用三个缓存的称之为三缓冲,大部分情况下双缓冲就够用了。
1.5 深度缓冲
深度缓存用来保存每个像素的深度信息,其值域为0到1,0代表距离是椎体最近距离,1代表最远距离,因为其余像素是一一对应的,所以它的分辨率和back buffer的分辨率是一样的;
深度缓存是一张贴图,所以它必须用特定的格式来创建:
- DXGI_FORMAT_D32_FLOAT_S8X24_UINT:使用32位浮点数深度缓存,和8位无符号整形预留给模板缓存(0~255)和24位未使用数据;
- DXGI_FORMAT_D32_FLOAT:使用32位浮点数深度缓存;
- DXGI_FORMAT_D24_UNORM_S8_UINT:使用24位无符号深度缓存,并映射到0到1,和8位无符号整形预留给模板缓存;
- DXGI_FORMAT_D16_UNORM:使用16位无符号深度缓存,并映射到0到1。
应用不需要一定有模板缓存,但是如果有的话,它经常附加到深度缓存中,比如32位格式:
DXGI_FORMAT_D24_UNORM_S8_UINT
所以深度缓存最好称之为深度/模板缓存;
1.6 资源和描述符(Descriptors)
GPU资源并不是直接绑定的,而是通过descriptor对象来引用,之所以这样做是因为GPU资源本质上是一堆普通的内存块,所以它们可以在渲染管线中不同阶段中被使用;更进一步,GPU资源可以创建成无类型的,所以GPU可能不知道资源的类型。所以就需要使用descriptors来描述资源。
(View和descriptor是一样的,老版本中使用View,DX12中部分地方也沿用View)
Descriptors拥有类型,用来定义它将如何被使用,在本书中使用到的类型有:
- CBV/SRV/UAV descriptors用来描述尝试缓存(constant buffers)、着色器资源(shader resources)和unordered access view resources;
- Sampler descriptors用来描述纹理映射资源;
- RTV descriptors用来描述渲染目标资源;
- DSV descriptors用来描述深度/模板资源;
一个descriptor heap是一个descriptors的数组,它用来保存所有特定类型的descriptors,不同类型的descriptors需要用不用descriptors heap保存,你也可以针对同一个类型的descriptors创建多个descriptors heap;同时也可以多个descriptors heap引用同一个资源。
Descriptors应该在初始化的时候创建,因为它需要做一些类型检查和验证;
1.7 多重纹理映射理论
因为显示器上的像素不是无限小,所以任意线段都不能在显示器上完美呈现出来;当无法增加显示器分辨率的时候,我们可以使用抗锯齿技术。
其中一种叫超级纹理映射技术,它使用4倍于屏幕分辨率的back buffer 和 深度缓存(depth buffer),当显示到屏幕上时,取4个像素的平均值;这种计数计算量和内存占用都太大了,Direct3D选用了一种折中的方案称为多重纹理映射:该计数也使用4倍于屏幕分辨率的back buffer 和 深度缓存(depth buffer),它并不计算每个字像素的颜色,而是每个像素只计算一遍,然后分享给每个可见和未被遮挡的子像素,如下图所示:
1.8 Direct3D中的多重纹理映射
在下一个部分中,我们需要填写一个结构体DXGI_SAMPLE_DESC,这个结构体有2个成员变量如下:
typedef struct DXGI_SAMPLE_DESC
{
UINT Count;
UINT Quality;
} DXGI_SAMPLE_DESC;
Count用来指定对每个像素进行多少次采样,Quality用来指定品质等级(quality level 指可以兼容不同硬件厂商?);高采样次数和品质等级代表更好的效果也代表更大的运算和内存开销;品质等级的范围只要基于纹理格式,采样次数基于每个像素。
我们可以使用函数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->CheckFeatureSupport(
D3D12_FEATURE_MULTISAMPLE_QUALITY_LEVELS,
&msQualityLevels,
sizeof(msQualityLevels)));
第二个参数同时是输入和输出参数,对于输入参数,我们必须指定纹理格式,采样次数和我们需要确认的多重纹理映射支持flag进行赋值;函数在输出的时候回对quality level进行赋值。无效的纹理格式的品质等级和采样次数组合范围是0到NumQualityLevels–1。
最大采样次数定义为:
#define D3D11_MAX_MULTISAMPLE_SAMPLE_COUNT ( 32 )
采样次数设置为4或者8,对于性能和内存开销都是合理的;如果你不想使用多重纹理映射,可以把采样次数设置为1,品质等级设置为0(back buffer 和 depth buffer要设置一样的采样设置)。
1.9 特征级别(Feature Levels)
Direct3D 11中介绍了特征级别的概念,它直接对应到每个版本的Direct3D:
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;
特征级别定义了一系列严格的功能;比如,如果一个GPU支持11,它必须支持所有Direct 11的功能,除了少量一些功能(比如多重纹理映射还是需要被确认下,因为它可以在不同支持Direct 11的硬件之间变化)。特征级别让开发变得容易一些,因为当你知道特征级别时,你就知道了你处理的Direct3D支持的功能。
如果用户的硬件不支持当前特征级别,应用程序会返回到上一个(更老的)特征级别。
1.10 DirectX 图像基础设施(DXGI)
DXGI是一套和Direct3D一起使用的API。它的基本思想是:一些图形任务对于一些图形API是相同的。比如:为了平滑动画的交换链(swap chain)和页面切换(page flipping)在2D和3D情况是相同的,所以交换链的接口IDXGISwapChain就是DXGI API的一部分。DXGI还有其他功能,比如:全屏切换,遍历系统信息比如显示适配器(display adapters),显示器,支持的显示模式(分辨率,刷新频率等);它也定义了各种支持的表面格式(surface formats (DXGI_FORMAT))。
在这里我们简单介绍一些后面将要用到的DXGI接口的概念。一个主要的接口是IDXGIFactory,它主要用来创建IDXGISwapChain接口和遍历显示适配器。显示适配器用来执行图形功能,通常它是物理硬件上的一部分;但是系统也可以包含一个软件的显示适配器;一个系统可以拥有多个显示适配器,每个适配器可以用一个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]);
}
}
一个系统可以由多个显示器,一个显示器输出可以使用IDXGIOutput接口表示。每个适配器关联一个显示输出列表;比如,一个系统包含2个显卡和3个显示器,其中一个显卡与2个显示器挂钩,另一个显卡和一个显示器挂钩,那么在这种情况下,一个适配器关联2个输出,另一个适配器关联一个输出。
这些信息我们可以使用下列代码遍历出来:
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;
}
}
每个显示器又可以支持一些列显示模式,一个显示模式用DXGI_MODE_DESC结构体表示:
typedef struct DXGI_MODE_DESC
{
UINT Width; // Resolution width
UINT Height; // Resolution height
DXGI_RATIONAL RefreshRate;
DXGI_FORMAT Format; // Display format
DXGI_MODE_SCANLINE_ORDER ScanlineOrdering; //Progressive vs. interlaced
DXGI_MODE_SCALING Scaling; // How the image is stretched
// over the monitor.
} DXGI_MODE_DESC;
typed