《dx12 龙书》第四部分学习笔记——预备知识(下)

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进行多重采样

cpp 复制代码
typedef struct DXGI_SAMPLE_DESC
{
	UINT Count;
	 UINT Quality;
}DXGI_SAMPLE_DESC;

Count成员指定了每个像素的采样次数,Quality成员则用于指示用户期望的图像质量级别。采样数量越多或质量级别越高,其渲染操作的代价也就会愈发高昂,所以需要在质量与速度之间做出利弊权衡。至于质量级别的范围,则要取决于纹理格式和每个像素的采样数量。

根据给定的纹理格式和采样数量,我们就能用ID3D12Device::CheckFeatureSupport方法查询到对应的质量级别:

cpp 复制代码
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之间的各种版本:

cpp 复制代码
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来表示。我们可以用下面的代码来枚举一个系统中的所有适配器:

cpp 复制代码
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结构体中的数据成员来加以表示:

cpp 复制代码
#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),我们就能通过下列代码,获得某个显示输出对此格式所支持的全部显示模式:

cpp 复制代码
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方法,检测了当前图形驱动对多重采样的支持。然而,这只是此函数对功能支持检测的冰山一角。这个方法的原型为:

cpp 复制代码
#include <winError.h>
#include <minwindef.h>
#include <d3d12.h>

HRESULT ID3D12Device::CheckFeatureSupport(
	D3D12_FEATURE Feature,
	void *pFeatureSupportData,
	UINT fEATUREsUPPORTdATASize
);

1.Feature:枚举类型D3D12_FEATURE中的成员之一,用于指定我们希望检测的功能支持类型。

cpp 复制代码
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参数。

cpp 复制代码
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参数中的数据结构的大小。

举例,如何对功能级别的支持情况进行检测:

cpp 复制代码
#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中,应用程序通过控制资源在显存中的去留,主动管理资源的驻留情况。该技术的基本思路为使应用程序占用最小的显存空间。这是因为现存的空间有限,很可能不足以容下整个游戏的所有资源,或者用户还有运行中的程序也在同时使用显存。性能优化提示:程序应当避免在短时间内于显存中交换进出相同的资源,这会引起过高的开销。最理想的情况使,所清出的资源在短时间内不会再次使用。

相关推荐
田梓燊几秒前
湘潭大学软件工程算法设计与分析考试复习笔记(五)
笔记·算法·软件工程
hlsd#4 分钟前
go web单体项目 学习总结
开发语言·学习·golang
垂杨有暮鸦⊙_⊙9 分钟前
蒙特卡洛方法(Monte Carlo,MC)
人工智能·笔记·机器学习·概率论
awonw18 分钟前
[运维][Nginx]Nginx学习(5/5)-Nginx高级
运维·学习·nginx
red_redemption22 分钟前
自由学习记录(24)
学习·lua
落霞的思绪28 分钟前
Maven学习笔记
笔记·学习·maven
studyForMokey41 分钟前
【Android零碎笔记】
android·笔记
白水4651 小时前
对于某些原型或UI软件的个人看法(2024/11)
学习·ui·产品经理·个人开发·墨刀
IT19951 小时前
Qt笔记-获取HTTP的POST请求提交的数据时需要注意的地方(2024-09-02)
笔记·qt·http
mljy.2 小时前
C++《二叉搜索树》
c++·学习