7、多重采样技术的原理
由于屏幕中显示的像素不可能是无穷小的,所以并不是任意一条直线都能在显示器上“平滑”而完美地呈现出来。即为以像素矩阵 (matrix of pixels, 可以理解为“像素2D数组”)逼近直线的方法所产生的“阶梯” (aliasing, 锯齿状走样)效果。类似地,显示器中呈现的三角形之边也存在着不同程度的锯齿效应。
通过提高显示器的分辨率就能够缩小像素的大小,继而使上述问题得到显著地改善,使阶梯效应在很大程度上不易被用户所察觉。
在不能提升显示器分辨率,或在显示器分辨率受限的情况下,我们就可以运用各种反走样(antialiasing, 也有译作抗锯齿、反锯齿、反失真等)技术。有一种名为超级采样(supersampling, 可简记作SSAA,即Super Sample Anti-Aliasing)的反走样技术,它使用4倍于屏幕分辨率大小的后台缓冲区和深度缓冲区。3D场景将以这种更大的分辨率渲染到后台缓冲区中。当数据要从后台缓冲区调往屏幕显示的时候,会将后台缓冲区按4个像素一组进行解析(resolve, 或称降采样,downsample。把放大的采样点数降低回原采样点数):每组用求平均值的方法得到一组相对平滑的像素颜色。因此,超级采样实际上是通过软件的方式提升了画面的分辨率。
超级采样是一种开销高昂的操作,因为它将像素的处理数量和占用的内存大小都增加到之前的4倍。
对此,Direct3D还支持一种在性能于效果等方面都较为折中的反走样技术,叫做多重采样(multisampling, 可简记作MSAA,即MultiSample Anti-Aliasing)。这种技术通过跨子像素共享一些计算信息,从而使它比超级采样的开销更低。现假设采用4X多重采样(即每个像素都有4个子像素),并同样使用4倍于屏幕分辨率的后台缓冲区和深度缓冲区。值得注意的是,这种技术并不需要对每一个子像素都进行计算,而是仅计算一次像素中心处的颜色,再基于可视性(每个子像素经深度/模板测试的结果)和覆盖性(子像素的中心在多边形的里面还是外面?)将得到的颜色信息分享给其子像素。下图展示了一个多重采样的实例。
注意:
超级采样和多重采样的关键区别是显而易见的。对于超级采样来说,图像颜色要根据每一个子像素来计算,因此每个子像素都可能各具不同的颜色。而以多重采样的方式来求取图像颜色时,每个像素只需计算一次,最后,再将得到的颜色数据复制到多边形覆盖的所有可见子像素之中。由于计算图像颜色是图形流水线中开销最大的步骤之一,所以用多重采样来代替超级采样对节省资源而言意义非凡。但是,超级采样的精准度确实更高一筹。
上图所示的是一种将每个像素都以均匀栅格划分为4个子像素的反锯齿采样模式。实际上,每家硬件厂商所采用的模式(即选定的子像素位置,可以说决定了采样的位置)可能会各不相同,而Direct3D也并没有定义子像素的具体布局。在各种特定的情况下,不同的布局模式各有千秋。
8、利用Direct3D进行多重采样
typedef struct DXGI_SAMPLE_DESC
{
UINT Count;
 UINT Quality;
}DXGI_SAMPLE_DESC;
Count成员指定了每个像素的采样次数,Quality成员则用于指示用户期望的图像质量级别。采样数量越多或质量级别越高,其渲染操作的代价也就会愈发高昂,所以需要在质量与速度之间做出利弊权衡。至于质量级别的范围,则要取决于纹理格式和每个像素的采样数量。
根据给定的纹理格式和采样数量,我们就能用ID3D12Device::CheckFeatureSupport方法查询到对应的质量级别:
typedef struct D3D12_FEATURE_DATA_MULTISAMPLE_QUALITY_LEVELS{
DXGI_FORMAT Format;
UINT SampleCount;
D3D12_MULTISAMPLE_QUALITY_LEVEL_FLAGS 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)));
注意,此方法的第二个参数兼具输入和输出的属性。当它作为输入参数时,我们必须指定纹理格式、采样数量以及希望查询的多重采样所支持的标志。接着,待函数执行后便会填写图像质量级别作为输出。对于某种纹理格式和采样数量的组合来讲,其质量级别的有效范围为0至NumQualityLevels-1。
每个像素的最大采样数量被定义为:
#define D3D12_MAX_MULTISAMPLE_SAMPLE_COUNT(32)
但是,考虑到多重采样会占用内存资源,又为了保证程序性能等原因,通常会把采样数量设定为4或8。如果不希望使用多重采样,则可将采样数量设置为1,并令质量级别为0。其实在所有支持Direct3D11的设备上,就已经可以对所有的渲染目标格式用用4X多重采样了。
注意:
在创建交换链缓冲区和深度缓冲区时都需要填写DXGI_SAMPLE_DESC结构体。当创建后台缓冲区和深度缓冲区时,多重采样的有关设置一定要相同。
9、功能级别
举例以下参数大致对应于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;
“功能级别”为不同级别所支持的功能进行了严格的界定(每个功能级别所支持的特定功能可参见SDK文档)。
例,一款支持功能级别11的GPU,除了个别特例之外,必须支持完整的Direct3D11功能集。
功能集使程序员的开发工作更加便捷——只要了解所支持的功能集,就能知道有哪些Direct3D功能可供使用。
如果用户的硬件不支持某特定功能级别,应用程序理当回退至版本更低的功能级别。在现实的应用程序中,我们往往需要考虑支持稍旧的硬件,以获得更多的用户。
10、DirectX图形基础结构
DirectX图形基础结构(DirectX Graphics Infrastructure,DXGI,也有译作DirectX图形基础设施)是一种与Direct3D配合使用的API设计DXGI的基本理念是使多种图形API中所共有的底层任务能借助一组通用API来进行处理。例如,为了保证动画的流畅性,2D渲染与3D渲染两组API都要用到交换链和页面翻转功能,这里所用的交换链接口IDXGISwapChain实际上就属于DXGI API。DXGI还用于处理一些其他常用的图形功能,如切换全屏模式,枚举显示适配器、显示设备及其支持的显示模式(分辨率、刷新率等)等这类图形系统信息。除此之外,它还定义了Direct3D支持的各种表面格式信息(DXGI_FORMAT)。
我们刚刚简单地叙述了DXGI的概念,下面来介绍一些在Direct3D初始化时会用到的相关接口。IDXGIFactory是DXGI中的关键接口之一。通常来说,显示适配器(display adapter) 是一种硬件设备(如独立显卡),然而系统也可以用软件显示适配器来模拟硬件的图形处理功能。一个系统中可能会存在数个适配器(比如装有数块显卡)。适配器用接口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]);
}
}
注: D3DAPP类后续会讲到,这里先写着,暂时不介绍
另外,一个系统也可能装有数个显示设备。我们称每一台显示设备都是一个显示输出(display output, 有的文档也作adapter output, 适配器输出)实例,用IDXGIOutput接口来表示。每个适配器都与一组显示输出相关联。
每种显示设备都有一系列它所支持的显示模式,可以用下列DXGI_MODE_DESC结构体中的数据成员来加以表示:
#include <minwindef.h>
#include <Dxgiformat.h>
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; //图像相对于屏幕的拉伸
} 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());
}
}
注: D3DAPP类后续会讲到,这里先写着,暂时不介绍
在进入全屏模式之时,枚举显示模式就显得尤为重要。为了获得最优的全屏性能,我们所指定的显示模式(包括刷新率)一定要与显示器支持的显示模式完全匹配。根据枚举出来的显示模式进行选定,便可以保证这一点。
11、功能支持的检测
我们已经通过ID3D12Device::CheckFeatureSupport方法,检测了当前图形驱动对多重采样的支持。然而,这只是此函数对功能支持检测的冰山一角。这个方法的原型为:
#include <winError.h>
#include <minwindef.h>
#include <d3d12.h>
HRESULT ID3D12Device::CheckFeatureSupport(
D3D12_FEATURE Feature,
void *pFeatureSupportData,
UINT fEATUREsUPPORTdATASize
);
1.Feature:枚举类型D3D12_FEATURE中的成员之一,用于指定我们希望检测的功能支持类型。
D3D12_FEATURE_D3D12_OPTIONS //检测当前图形驱动对Direct3D 12各种功能的支持情况
D3D12_FEATURE_ARCHITECTURE //检测图形适配器中GPU的硬件体系架构特性
D3D12_FEATURE_FEATURE_LEVELS //检测对功能级别的支持情况
D3D12_FEATURE_FORMAT_SUPPORT //检测对给定纹理格式的支持情况(例,指定的格式能否用于渲染目标?或,指定的格式能否用于混合技术?)
D3D12_FEATURE_MULTISAMPLE_QUALITY_LEVELS //检测对多重采样功能的支持情况
2.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_DATA_FORMAT_SUPPORT实例。
Feature参数指定为D3D12_FEATURE_MULTISAMPLE_QUALITY_LEVELS
传回的是一个D3D12_FEATURE_DATA_MULTISAMPLE_QUALITY_LEVELS实例。
3.FeatureSupportDataSize:传回pFeatureSupportData参数中的数据结构的大小。
举例,如何对功能级别的支持情况进行检测:
#include <d3d12.h>
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, // 首先检测是否支持D3D 11
D3D_FEATURE_LEVEL_10_0, // 其次检测是否支持D3D 10
D3D_FEATURE_LEVEL_9_3 // 最后检测是否支持D3D 9.3
};
int main()
{
D3D12_FEATURE_DATA_FEATURE_LEVELS featureLevelsInfo;
featureLevelsInfo.NumFeatureLevels = 3;
featureLevelsInfo.pFeatureLevelsRequested = featureLevels;
md3dDevice->CheckFeatureSupport(
D3D12_FEATURE_FEATURE_LEVELS,
&featureLevelsInfo,
sizeof(featureLevelsInfo));
}
注意:
CheckFeatureSupport方法的第二个参数兼有输入和输出的属性。作为输入的时候,先要指定功能级别数组中元素的个数,再令指针指向功能级别数组,其中应包括我们希望检测的一系列硬件支持功能级别。最后,此函数将用MaxSupportedFeatureLevel字段返回当前硬件可支持的最高功能级别。
12、资源驻留
复杂的游戏会运用大量纹理和3D网格(3d mesh)等资源,但是其中的大多数并不需要总是置于显存中供GPU使用。
在Direct3D 12中,应用程序通过控制资源在显存中的去留,主动管理资源的驻留情况。该技术的基本思路为使应用程序占用最小的显存空间。这是因为现存的空间有限,很可能不足以容下整个游戏的所有资源,或者用户还有运行中的程序也在同时使用显存。性能优化提示:程序应当避免在短时间内于显存中交换进出相同的资源,这会引起过高的开销。最理想的情况使,所清出的资源在短时间内不会再次使用。