第四章 初始化D3D
4 初始化D3D
本章第一部我们会熟悉一些D3D的基本数据类型和基本的图形概念。然后,我们详细说明初始化Direct3D所需的步骤。之后,引入准确计算和实时图形应用所需的时间测量。最后,我们将探索示例框架代码,该代码用于提供本书中所有演示应用程序的界面。
学习目标:
1.了解Direct3D在3D硬件编程中的作用。
2.了解COM与Direct3D的作用。
3.学习基本图形概念,例如2D图像的存储方式,翻页,深度缓冲和多重采样。
4.学习如何使用性能计数器功能获得高分辨率定时器读数。
5.了解如何初始化Direct3D。
6.熟悉本书所有演示应用框架的一般结构。
4.1 初步
我们将在本节介绍一些Direct3D初始化过程中一些基本的图形概念和Direct3D数据类型,为以后打下基础。
4.1.1 Direct3D概述
D3D是一个底层图形API(Application Programming Interface),使我们能够使用3D硬件加速渲染。本质上D3D提供了我们控制图形硬件的软件接口。如要清除渲染目标(如屏幕),可调用ID3D11DeviceContext::ClearRenderTargetView方法。在应用程序和图形硬件之间拥有Direct3D层,意味着我们不用担心3D硬件的细节,只要它是一个支持Direct3D 11的设备。
D3D 11功能的图形设备必须支持整个D3D 11功能集,除了个别例外(比如多次采样计数,因为不同的D3D硬件会有所不同)。这与D3D 9的只需支持D3D 9功能的子集有所不同; 因此,如果D3D 9应用程序想要使用某个功能,则首先需要检查硬件是否支持该功能,因为调用不支持D3D功能的硬件会导致错误。在Direct3D 11中,不再需要设备能力检查,因为严格要求Direct3D 11设备实现整个Direct3D 11功能集。
4.1.2 COM组件
组件对象模型(COM)是允许DX独立于编程语言并具有向后兼容性的技术。我们通常将COM对象称为接口,我们可以简单的将其视为C++类。当使用C++编程DirectX时,COM会对我们隐藏大部分细节。我们只需通过特定函数获取COM接口的方法或获取COM接口的指针 - 我们不会用C ++ new关键字创建一个COM接口。另外,当使用完后,调用Release方法,而不是删除它(所有COM接口都继承了提供Release方法的IUnknown COM接口的功能),COM对象执行自己的内存管理。当然,COM还有更多的东西,但对于更高效的使用DirectX并不必要。
NOTE: COM接口以首字母 I 为前缀。例如,表示2D纹理的COM接口称为ID3D11Texture2D。
4.1.3纹理和数据资源格式
2D纹理是数据元素的矩阵。2D纹理的一个用途是存储2D图像数据,其中纹理中的每个元素存储像素的颜色。但这不是唯一的用法; 例如,在称为“法线贴图”的高级技术中,纹理中的每个元素存储3D矢量而不是颜色。因此,虽然一般将纹理视为图像数据,但它们实际上还有更多用处。1D纹理就像1D数组元素,3D纹理像3D阵列的数组元素。稍后章节会讲,纹理实际上不仅仅是数据数组; 它还具有mipmap级别,并且GPU可以对它们进行特殊操作,例如应用过滤器和多采样。另外纹理不能存储任意种类的数据; 它只能存储由DXGI_FORMAT枚举类型描述的数据格式。比如:
1. DXGI_FORMAT_R32G32B32_FLOAT:每个元素都有三个32位浮点分量。
2. DXGI_FORMAT_R16G16B16A16_UNORM:每个元素具有映射到[0,1]范围的四个16位元件。
3. DXGI_FORMAT_R32G32_UINT:每个元素都有两个32位无符号整数分量。
4. DXGI_FORMAT_R8G8B8A8_UNORM:每个元素具有映射到[0,1]范围的四个8位无符号分量。
5. DXGI_FORMAT_R8G8B8A8_SNORM:每个元素具有映射到[-1,1]范围的四个8位有符号组件。
6. DXGI_FORMAT_R8G8B8A8_SINT:每个元素具有映射到[-128,127]范围的四个8位有符号整数组件。
7. DXGI_FORMAT_R8G8B8A8_UINT:每个元素具有映射到[0,255]范围的四个8位无符号整数分量。
R,G,B,A字母分别用于代表红,绿,蓝和alpha。颜色被红,绿和蓝三种基础颜色混合而成(例如,相等的红色和相等的绿色变成黄色)。alpha通常用于控制透明度。然而纹理不是只能存储颜色信息;例如
具有三个浮点分量,因此可以存储具有浮点坐标的3D矢量。还有无类型的格式,我们只存数据,然后在纹理绑定到管道后指定如何重新解释数据(类似于C++重新解释的转换)如:以下无格式格式保留具有四个8位组件的元素,但不指定数据类型(例如,整数,浮点,无符号整数):
4.1.4交换链和页面翻转
为了避免画面闪烁,最好将整帧画面绘制到后台缓冲区的屏幕纹理中。一旦将整个场景绘制到后台缓冲区给定的区域,就可将其作为一个完整帧呈现给屏幕;以这种方式,观众不会看到画面被绘制 - 观众只能看到完整的帧。为了实现这一点,硬件用了两个纹理缓冲区:前缓冲区&后缓冲区。前缓冲区存储当前显示在监视器上的图像数据,而下一帧动画被绘制到后缓冲区。帧被绘制到后台缓冲区后,后缓冲区和前缓冲区对调:后缓冲区变为前缓冲区,前缓冲区成为下一帧的后缓冲区。交换后台和前端缓冲区的角色称为presenting。Presenting是一个高效操作,因为只需要交换指向当前前端缓冲区的指针和当前后台缓冲区的指针。图4.1说明了该过程。
前后缓冲区形成交换链。在D3D中交换链由IDXGISwapChain接口表示。该内容存储前后缓冲区纹理,以及提供调整缓冲区大小的方法(IDXGISwapChain::ResizeBuffers)和呈现(IDXGISwapChain::Present)。我们将在§4.4中详细讨论这些方法。
使用两个缓冲区(前和后)称为双缓冲。也可以使用两个以上的缓冲区;使用三个缓冲区称为三重缓冲。但一般两个缓冲区就足够了。
NOTE:即使后台缓冲区是纹理(因此其元素应该称为纹素),但我们经常将其元素称为像素,因为在后台缓冲区的情况下,它存储颜色信息。有时人们会将纹理的元素称为像素,即使它不存储颜色信息(例如,“法线贴图的像素”)。
图4.1 从上到下,我们首先渲染到缓冲区B,该缓冲区B用作当前的后台缓冲区。一帧完成,指针被交换,缓冲区B变成前缓冲区,缓冲区A成为新的后缓冲区。然后,我们将下一帧渲染到缓冲区A.一旦帧完成,指针被交换,缓冲区A成为前缓冲区,缓冲区B再次成为后缓冲区。
4.1.5深度缓冲
深度缓冲器是不包含图像数据纹理,而是关于特定像素的深度信息。可能的深度值范围为0.0至1.0,其中0.0表示对象可以与观看者最接近,1.0表示物体距离观察者最远。深度缓冲器中的每个像素和后缓冲器中的每个像素之间存在一一对应关系(后缓冲器中的第i个像素对应于深度缓冲器中第i个像素)。因此,如果后台缓冲区的分辨率为1280×1024,则会有1280×1024个深度元素。
图4.2 一组部分相互遮挡的物体
图4.2显示了一个简单的场景,其中一些物体部分地掩盖了它们后面的物体。为了使D3D能够确定物体的哪些像素在另一个像素之前,它使用一种称为深度缓冲或z缓冲的技术。在此强调一下,有了深度缓冲,我们绘制对象的顺序并不重要。
NOTE:处理深度问题,可以建议以最远到最近的顺序绘制场景中的物体。以这种方式,近物体将被绘制在远处的对象上,并且会呈现正确的结果。这就是画家画画的方式。但是,这种方法有其自身的问题 - 会存储大量的前后顺序排序和相交几何体数据。而且,图形硬件本来就提供深度缓冲。
让我们用一个例子说明深度缓存的工作原理。观察图4.3,它显示了观看者看到的空间和该空间的2D侧视图。从图中,我们观察到三个不同的像素竞争在视图窗口上的像素P上呈现。(显然最接近的像素应该渲染到P,因为它掩盖了它后面的像素,但是计算机不知道)首先,在渲染前,后台缓冲区被置为默认色(如黑或白),并且深度缓冲区被清除为默认值,通常为1.0(像素可以具有的最远深度值)。现在,假设物体是以圆柱体,球体和锥体的顺序呈现的。下表总结了绘制对象时如何更新像素P及其相应的深度值d;对于其他像素也会发生类似的过程。
操作 | P | d | 描述 |
---|---|---|---|
清除 | 黑 | 1.0 | 像素和相应的深度初始化 |
画圆柱 | p3 | d3 | 因为d3 < d=1.0,深度测试通过,同时我们通过设置P=P3,d=d3更新buffer |
画球 | p1 | d1 | 因为d1 < d= d3,深度测试通过,同时我们通过设置P=P1,d=d1更新buffer |
画圆锥 | p1 | d1 | 因为d2 > d= d2,深度测试失败,不更新buffer |
图4.3 视图窗口对应于我们生成的3D场景的2D图像(后台缓冲区)。我们看到三个不同的像素可以投影到像素P。直觉告诉我们,P1应该被写入到P,因为它更接近于观察者并且阻挡了另外两个像素。深度缓冲算法提供了在计算机上确定该点的过程。请注意,我们显示相对于正在查看的3D场景的深度值,但是当它们存储在深度缓冲区中时,它们实际上被归一化为范围[0.0,1.0]
可见,当我们找到一个较小深度值的像素时,我们会更新深度缓冲区中的像素及其相应的深度值。以这种方式,最接近观众的像素就是渲染的像素。(如果你仍然不信,可以切换绘图顺序并重做此示例。)
总而言之,深度缓冲通过计算每个像素的深度值并执行深度测试来起作用。深度测试将要写入的像素的深度与后缓冲区上的特定像素位置进行比较。具有最接近观察者的深度值的像素获就是写入后台缓冲区的像素,因为最接近观众的像素会掩盖其背后的像素。
深度缓冲区是纹理,因此必须使用某些数据格式创建。用于深度缓冲的格式如下:
1.DXGI_FORMAT_D32_FLOAT_S8X24_UINT:指定一个32位浮点深度缓冲区,具有8位(无符号整数)保留映射到[0,255]范围的模板缓冲区和不用于填充的24位。
2.DXGI_FORMAT_D32_FLOAT:指定一个32位浮点深度缓冲区。
3.DXGI_FORMAT_D24_UNORM_S8_UINT:指定一个无符号的24位深度缓冲区,映射到[0,1]范围,8位(无符号整数)为映射到[0,255]范围的模板缓冲区保留。
4.DXGI_FORMAT_D16_UNORM:指定映射到[0,1]范围的无符号16位深度缓冲区。
NOTE:应用程序一般不需要模板缓冲区,如果需要,模板缓冲区总是附加到深度缓冲区。例如,32位格式:
DXGI_FORMAT_D24_UNORM_S8_UINT
深度缓冲区使用24位,模板缓冲区使用8位。因此,深度缓冲区称为深度/模板缓冲区更合适。使用模板缓冲区是一个更高级的问题,将在第10章中进行说明。
4.1.6纹理资源视图
纹理可以绑定到渲染管道的不同阶段; 一个常见的例子是使用纹理作为渲染目标(即D3D中绘制到纹理中)和作为着色器资源(即纹理将在着色器中被采样)。为这两个目的创建的纹理资源将被赋予绑定标志:
指示纹理将被绑定到的两个渲染管线阶段。实际上资源并不直接绑定到渲染管线阶段;而是将其关联的资源视图绑定到不同的渲染管线阶段。对于每种方式,我们将使用纹理,D3D要求我们在初始化时创建该纹理的资源视图。这主要是为了效率,因为SDK文档指出:“这允许运行时和驱动程序在视图创建时进行验证和映射,从而使绑定时的类型检查时间最小化。”因此,使用纹理作为渲染的示例目标和着色器资源,我们需要创建两个视图:渲染目标视图(ID3D11RenderTargetView)和着色器资源视图(ID3D11ShaderResourceView)。资源视图基本上做了两件事情:他们告诉Direct3D如何使用资源(即将其绑定到渲染管线的什么阶段),如果资源格式在创建时被指定为无类型,则必须说明在创建视图时确定。因此,使用无类型格式,可以将纹理的元素视为一个渲染管线阶段中的浮点值,并将其视为另一个渲染管线阶段中的整数。
为了创建特定的资源视图,必须使用该特定的绑定标志创建资源。例如,如果资源没有使用D3D11_BIND_DEPTH_STENCIL绑定标志(这表示纹理将被绑定到管道作为深度/模板缓冲区)创建,那么我们不能为该资源创建一个ID3D11DepthStencilView。如果试一下,你应该得到一个D3D调试错误,如下所示:
本章的§4.2会介绍创建渲染目标视图和深度/模板视图的代码。创建着色器资源视图将在第8章中讨论。使用纹理作为渲染目标和着色器资源将在本书的后面出现。
NOTE:2009年8月的SDK文档说:“创建一个完全类型的资源将资源限制为创建的格式。这使得运行时可以优化访问[…]。“因此,如果您真的需要他们提供的灵活性(通过多种视图以多种方式重新解释数据的能力),则只应创建一个无类型的资源; 否则,创建一个确定类型的资源。
4.1.7多采样理论
因为显示器上的像素不是无限小的,所以在计算机显示器上不能完美地表示任意的直线。图4.4示出了当通过像素矩阵近似线时可能发生的“阶梯”(锯齿)效应。类似的锯齿效应会在三角形的边缘发生。
图4.4 上图我们观察到锯齿(当通过像素矩阵来表示直线时的阶梯效应)。在底部,我们看到一个抗锯齿线,通过采样和使用其相邻像素生成像素的最终颜色; 这样做缓解了阶梯效应使得图像更平滑。
通过提高显示器分辨率来缩小像素尺寸可以在很大程度上显著减轻锯齿。
当无法将显示器分辨率增加到那么多时,我们可以应用抗锯齿技术。超级采样是一种通过使用4倍于屏幕分辨率的台缓冲区和深度缓冲区来工作的技术。先以更大的分辨率将3D场景渲染到后台缓冲区。如何将后台缓冲区显示到屏幕,后缓冲区被解析(或下采样),使得4个像素块颜色被平均在一起以获得平均的像素颜色。实际上,超级采样是通过增加软件中的分辨率来实现的。
图4.5 我们考虑多边形边缘的一个像素。(a)在像素中心评估的绿色颜色存储在由多边形覆盖的三个可见子像素中。第四象限中的子像素不被多边形覆盖,因此不会用绿色更新 - 它只保留其以前绘制的几何或清除先前颜色。(b)为了计算分辨的像素颜色,我们将四个子像素(三个绿色像素和一个白色像素)平均,以沿着多边形的边缘获得浅绿色。这通过沿着多边形边缘缓解阶梯效应来获得更平滑的图像。
超级采样十分耗费资源,因其将像素处理和存储器的数量增加了四倍。D3D支持一种折衷的抗锯齿技术,称为多采样,它在子像素之间共享一些计算信息,使其比超采样成本低。假设我们正在使用4X多重采样(每像素4个子像素),多采样也使用后置缓冲区和深度缓冲区4倍大于屏幕分辨率; 然而,代替计算每个子像素的图像颜色,它仅在像素中心处每像素计算一次,然后基于可见度(每个子像素评估深度/模板测试)和覆盖范围来共享其子像素的颜色信息(子像素中心是否位于多边形的内部或外部?)图4.5显示了一个例子。
NOTE:观察超采样和多采样之间的关键区别。 通过超级采样,每个子像素计算图像颜色,因此每个子像素可能是不同的颜色。 使用多重采样(图4.5),每像素计算一次图像颜色,并将该颜色复制到由多边形覆盖的所有可见子像素中。 因为计算图像颜色是图形流水线中最费时的步骤之一,因此这就从超采样的多次采样中节省了大量时间。另一方面,超级采样在技术上更准确,可以处理纹理和着色器的混叠,而多采样则不能。
NOTE:在图4.5中,我们示出了以均匀网格图案细分为四个子像素的像素。使用的实际模式(子像素所在的点)可能因硬件供应商而变化,因为D3D不会定义子像素的位置。某些模式在某些情况下比其他更好。
4.1.8 Direct3D中的多重采样
接下来,我们将需要写一个DXGI_SAMPLE_DESC结构。这个结构有两个成员,定义如下:
typedef struct DXGI_SAMPLE_DESC {
UINT Count;
UINT Quality;
} DXGI_SAMPLE_DESC, *LPDXGI_SAMPLE_DESC;
成员Count指定每像素采样的数量,成员Quality用于指定所需的质量水平(“质量水平”根据硬件制造商有所不同)。更高的样品数量或更高的质量更耗费资源,因此必须在质量和速度之间进行权衡。质量级别的范围取决于纹理格式和每像素需要采样的数量。使用以下方法查询给定纹理格式和样品数量的质量等级数:
HRESULT ID3D11Device::CheckMultisampleQualityLevels(DXGI_FORMAT Format, UINT SampleCount, UINT *pNumQualityLevels);
如果设备不支持格式和采样数组,则此方法返回零。否则,给定组合的质量级别数将通过pNumQualityLevels参数返回。纹理格式和样本计数组合的有效质量级别从零到pNumQualityLevels -1。
每个像素可以采取的最大采样数由下式定义:
#define D3D11_MAX_MULTISAMPLE_SAMPLE_COUNT ( 32 )
然而,为了保持多采样的性能和内存成本合理,4或8的样本计数是常见的。如果您不想使用多重采样,请将样品计数设置为1,将质量等级设置为零。所有支持Direct3D 11的设备都支持所有渲染目标格式的4X多重采样。
NOTE:需要为后台缓冲区和深度缓冲区填写DXGI_SAMPLE_DESC结构。必须使用相同的多采样设置创建后缓冲区和深度缓冲区;说明这一点的示例代码在下一节中给出。
4.1.9特征级别
D3D 11引入了特征级别(由D3D_FEATURE_LEVEL枚举类型代码表示)的概念,它们大致对应于从版本9到11的各种Direct3D版本:
typedef 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;
特征级别定义了一组严格的功能(有关每个特征级别支持的特定功能,请参阅SDK文档)。这样做的原因是,如果用户的硬件不支持某个级别,应用程序可能会回退到较旧的级别。例如,为了支持更广泛的受众,应用程序可能支持Direct3D 11,10.1,10和9.3级硬件。应用程序将检查从最新到最旧的级别支持:即应用程序将首先检查Direct3D 11是否受支持,第二个是Direct3D 10.1,然后是Direct3D 10,最后是Direct3D 9.3。为了方便这个测试顺序,将使用以下功能级别数组(排序阵列的元素意味着功能级别测试的顺序):
D3D_FEATURE_LEVEL featureLevels[4] =
{
D3D_FEATURE_LEVEL_11_0, // First check D3D 11 support
D3D_FEATURE_LEVEL_10_1, // Second check D3D 10.1 support
D3D_FEATURE_LEVEL_10_0, // Next, check D3D 10 support
D3D_FEATURE_LEVEL_9_3 // Finally, check D3D 9.3 support
};
该数组将被输入到Direct3D初始化函数(§4.2.1)中,该函数将输出数组中第一个支持的特征级别。例如,如果Direct3D报告了支持的阵列中的第一个功能级别D3D_FEATURE_LEVEL_10_0,则应用程序可能会禁用Direct3D 11和Direct3D 10.1功能,并使用Direct3D 10渲染路径。在本书中,我们总是需要对功能级别D3D_FEATURE_LEVEL_11_0的支持,因为这是一本Direct3D 11书。然而,现实中应用程序确实需要担心支持旧的硬件才能最大限度地提高受众。
4.2 初始化DIRECT3D
以下小节讲解如何初始化D3D。我们的D3D初始化过程可以分为以下几个步骤:
1.使用D3D11CreateDevice函数创建ID3D11Device和ID3D11DeviceContext接口。
2.使用ID3D11Device::CheckMultisampleQualityLevels方法检查4X MSAA质量等级的支持。
3.通过填写一个DXGI_SWAP_CHAIN_DESC结构来描述我们将要创建的交换链实例的特征。
4.查询用于创建设备的IDXGIFactory实例,并创建一个IDXGISwapChain实例。
5.创建一个渲染目标视图到交换链的后台缓冲区。
6.创建深度/模板缓冲区及其相关的深度/模板视图。
7.将渲染目标视图和深度/模板视图绑定到渲染管道的输出合并阶段,以便Direct3D可以使用它们。
8.设置视口。
4.2.1 创建设备和上下文
通过创建Direct3D 11设备(ID3D11Device)和上下文(ID3D11DeviceContext)开始初始化Direct3D。这两个接口是主要的Direct3D接口,可以被认为是我们的物理图形设备硬件的软件控制器;也就是说,通过这些接口,我们可以与硬件进行交互,并指示它执行操作(如在GPU内存中分配资源,清除后台缓冲区,将资源绑定到各个流水线阶段,并绘制几何)。进一步来说:
1、ID3D11Device接口用于检查功能支持,并分配资源。
2、ID3D11DeviceContext接口用于设置渲染状态,将资源绑定到图形管道,并发出渲染命令。
可以使用以下功能创建设备和内容:
HRESULT D3D11CreateDevice(
IDXGIAdapter *pAdapter,
D3D_DRIVER_TYPE DriverType,
HMODULE Software,
UINT Flags,
CONST D3D_FEATURE_LEVEL *pFeatureLevels,
UINT FeatureLevels,
UINT SDKVersion,
ID3D11Device **ppDevice,
D3D_FEATURE_LEVEL *pFeatureLevel,
ID3D11DeviceContext **ppImmediateContext
);
1、pAdapter:指定想要创建设备表示的显示适配器。为此参数指定null使用主显示适配器。我们总是在本书的示例程序中使用主适配器。在章节练习中您会使用其他显示器。
2、DriverType:通常,您将始终为此参数指定D3D_DRIVER_TYPE_HARDWARE以使用3D硬件加速进行渲染。但是,一些其他选项包括:
D3D_DRIVER_TYPE_REFERENCE:创建一个所谓的参考设备。 参考设备是Direct3D的软件实现,目标是正确性(由于它是软件实现,速度非常慢)。参考设备随DirectX SDK一起安装,仅供开发人员使用; 它不应该用于托管应用程序。使用参考设备有两个原因:
(i)要测试代码,您的硬件不支持; 例如,当您没有Direct3D 11功能的显卡时,要测试Direct3D 11代码。
(ii)测试驱动程序的错误。如果您的代码能够与参考设备正常工作,但不能与硬件配合使用,则可能是硬件驱动程序中的错误。
3D_DRIVER_TYPE_SOFTWARE:创建用于模拟3D硬件的软件驱动程序。要使用软件驱动程序,您必须自行构建或使用第三方软件驱动程序。Direct3D除了下面描述的WARP驱动程序之外,不提供软件驱动程序。
D3D_DRIVER_TYPE_WARP:创建高性能Direct3D 10.1软件驱动程序。 WARP代表Windows Advanced Rasterization Platform。 我们对此不感兴趣,因为它不支持Direct3D 11。
3.Software:用于提供软件驱动程序。我们总是指定null,因为我们正在使用硬件进行渲染。否则,必须有一个可以使用的软件驱动程序。
4.Flags:可选的设备创建标志(可以按位进行或运算)。两个常见的标志是:
D3D11_CREATE_DEVICE_DEBUG:对于调试模式构建,应将此标志设置为启用调试层。当指定调试标志时,Direct3D将调试消息发送到VC++输出窗口;图4.6显示了可以输出的一些错误消息的示例。
D3D11_CREATE_DEVICE_SINGLETHREADED:如果可以保证不会从多个线程调用Direct3D,则可提高性能。如果启用了此标志,则ID3D11Device::CreateDeferredContext方法将失败(请参阅之后的“NOTE”)。
5.pFeatureLevels:D3D_FEATURE_LEVEL的数组元素,其顺序表示测试特征级别支持的顺序(见§4.1.9)。为此参数指定null表示选择支持最大的功能级别。在我们的框架中,我们检查以确保这是D3D_FEATURE_LEVEL_11_0(即Direct3D 11支持),因为我们只在本书中使用Direct3D 11。
6.FeatureLevels:数组pFeatureLevels中D3D_FEATURE_LEVEL的数量。 如果为先前参数pFeatureLevels指定了null,则指定0。
7.SDKVersion:始终指定D3D11_SDK_VERSION。
8.ppDevice:返回创建的设备。
9.pFeatureLevel:返回pFeatureLevels数组中第一个受支持的功能级别(如果pFeatureLevels为null时,返回持最大的功能级别)。
10.ppImmediateContext:返回创建的设备上下文。
图4.6 Direct3D 11调试输出的一个例子。
该函数调用的一个例子:
UINT createDeviceFlags = 0;
#if defined(DEBUG) || defined(_DEBUG)
createDeviceFlags |= D3D11_CREATE_DEVICE_DEBUG;
#endif
D3D_FEATURE_LEVEL featureLevel;
ID3D11Device* md3dDevice;
ID3D11DeviceContext* md3dImmediateContext;
HRESULT hr = D3D11CreateDevice(
0, // default adapter
D3D_DRIVER_TYPE_HARDWARE,
0, // no software device
createDeviceFlags,
0, 0, // default feature level array
D3D11_SDK_VERSION,
& md3dDevice,
& featureLevel,
& md3dImmediateContext);
if( FAILED(hr) )
{
MessageBox(0, L"D3D11CreateDevice Failed.", 0, 0);
return false;
}
if( featureLevel != D3D_FEATURE_LEVEL_11_0 )
{
MessageBox(0, L"Direct3D Feature Level 11 unsupported.", 0, 0);
return false;
}
NOTE: 注意将设备上下文指针指向immediate context:
ID3D11DeviceContext* md3dImmediateContext;
还有一种称为延迟上下文(ID3D11Device :: CreateDeferredContext)的东西。这是Direct3D 11多线程支持中的一部分。考虑到多线程是一个高级问题,本书不做讨论,但基本思路如下:
1.在主渲染线程上设置immediate context。
2.在单独的工作线程上有任何额外的延迟上下文。
(a)每个工作线程都可以将图形命令记录到命令列表(ID3D11CommandList)中。
(b)然后可以在主渲染线程上执行每个工作线程的命令列表。
如果命令列表需要时间进行组合,那么它们可以用于复杂的渲染图,能够在多核系统上并行组合命令列表是有利的。
4.2.2检查4X MSAA的支持
现在我们已经创建了一个设备,我们可以检查4X MSAA的质量级别支持。回想一下,所有支持Direct3D 11的设备在所有渲染格式下支持4X MSAA(但支持的质量级别可能不同)。
UINT m4xMsaaQuality;
HR(md3dDevice->CheckMultisampleQualityLevels(
DXGI_FORMAT_R8G8B8A8_UNORM, 4, & m4xMsaaQuality));
assert(m4xMsaaQuality > 0 );
由于至少支持4X MSAA,因此返回的质量应始终大于0;因此,assert是这样的。
4.2.3 交换链
初始化过程的下一步是创建交换链。首先填写DXGI_SWAP_CHAIN_DESC结构的一个实例,该实例描述了我们要创建的交换链的特征。这个结构定义如下:
typedef struct DXGI_SWAP_CHAIN_DESC {
DXGI_MODE_DESC BufferDesc;
DXGI_SAMPLE_DESC SampleDesc;
DXGI_USAGE BufferUsage;
UINT BufferCount;
HWND OutputWindow;
BOOL Windowed;
DXGI_SWAP_EFFECT SwapEffect;
UINT Flags;
} DXGI_SWAP_CHAIN_DESC;
DXGI_MODE_DESC类型是另一种结构,定义为:
typedef struct DXGI_MODE_DESC
{
UINT Width; // desired back buffer width
UINT Height; // desired back buffer height
DXGI_RATIONAL RefreshRate; // display mode refresh rate
DXGI_FORMAT Format; // back buffer pixel format
DXGI_MODE_SCANLINE_ORDER ScanlineOrdering; // display scanline mode
DXGI_MODE_SCALING Scaling; // display scaling mode
} DXGI_MODE_DESC;
NOTE:在下面的数据成员描述中,我们只涵盖了对初学者来说最重要的常用标志和选项。有关更多标志和选项的说明,请参阅SDK文档。
1.BufferDesc:这个结构描述了我们想创建的后台缓冲区的属性。宽、高,像素格式是我们关心的主要属性;了解更多详细信息,请参阅SDK文档。
2.SampleDesc:多重采样的数量和质量水平;见§4.1.8。
3.BufferUsage:指定DXGI_USAGE_RENDER_TARGET_OUTPUT,因为我们要渲染到后台缓冲区(即用它作为渲染目标)。
4.BufferCount:交换链中使用的后台缓冲区的数量; 我们通常只使用一个后台缓冲区进行双缓冲,尽管可以使用两个三重缓冲。
5.OutputWindow:我们渲染的窗口句柄。
6.Windowed:指定true在窗口模式下运行或在全屏模式下为false。
7.SwapEffect:指定DXGI_SWAP_EFFECT_DISCARD为了让显示驱动程序选择最高效的呈现方式。
8.Flags:可选。如果指定为DXGI_SWAP_CHAIN_FLAG_ALLOW_MODE_SWITCH,那么当应用程序切换到全屏模式时,会选择与当前后台缓冲区设置最匹配的显示模式。如果未指定此标志,则当应用程序切换到全屏模式时,将使用当前的显示模式。在我们的示例中,我们不指定此标志,因为在全屏模式下使用当前显示模式我们的演示可以正常工作(大多数桌面显示设置为显示器的最佳分辨率)。
以下代码显示了我们如何填写示例中的DXGI_SWAP_CHAIN_DESC结构:
DXGI_SWAP_CHAIN_DESC sd;
sd.BufferDesc.Width = mClientWidth; // use window's client area dims
sd.BufferDesc.Height = mClientHeight;
sd.BufferDesc.RefreshRate.Numerator = 60;
sd.BufferDesc.RefreshRate.Denominator = 1;
sd.BufferDesc.Format = DXGI_FORMAT_R8G8B8A8_UNORM;
sd.BufferDesc.ScanlineOrdering = DXGI_MODE_SCANLINE_ORDER_UNSPECIFIED;
sd.BufferDesc.Scaling = DXGI_MODE_SCALING_UNSPECIFIED;
// Use 4X MSAA?
if( mEnable4xMsaa )
{
sd.SampleDesc.Count = 4;
// m4xMsaaQuality is returned via CheckMultisampleQualityLevels().
sd.SampleDesc.Quality = m4xMsaaQuality-1;
}
// No MSAA
else
{
sd.SampleDesc.Count = 1;
sd.SampleDesc.Quality = 0;
}
sd.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT;
sd.BufferCount = 1;
sd.OutputWindow = mhMainWnd;
sd.Windowed = true;
sd.SwapEffect = DXGI_SWAP_EFFECT_DISCARD;
sd.Flags = 0;
NOTE:如果想在运行时更改多重采样设置,则必须销毁并重新创建交换链。
NOTE:我们将DXGI_FORMAT_R8G8B8A8_UNORM格式(8位红,绿,蓝和alpha)用于后台缓冲区,因为显示器通常不支持24位以上的颜色,所以额外的精度将被浪费。监视器不输出alpha的额外8位,但是在后台缓冲区中有额外的8位可用于某些特殊效果。
4.2.4创建交换链
通过IDXGIFactory::CreateSwapChain方法实例化IDXGIFactory创建交换链接口(IDXGISwapChain):
HRESULT IDXGIFactory::CreateSwapChain(
IUnknown *pDevice, // Pointer to ID3D11Device.
DXGI_SWAP_CHAIN_DESC *pDesc, // Pointer to swap chain description.
IDXGISwapChain **ppSwapChain); // Returns created swap chain interface.
我们可以通过CreateDXGIFactory获得一个指向IDXGIFactory实例的指针(需要链接dxgi.lib)。但是,如果我们以这种方式获取一个IDXGIFactory实例,并调用IDXGIFactory::CreateSwapChain,那么我们将会得到以下错误:
**DXGI Warning: IDXGIFactory::CreateSwapChain: This function is being called with a device from a different
IDXGIFactory.**
必要的修改是使用用于创建设备的IDXGIFactory实例。要获取此实例,我们必须继续执行以下COM查询系列(在IDXGIFactory的文档中有描述):
IDXGIDevice* dxgiDevice = 0;
HR(md3dDevice->QueryInterface(__uuidof(IDXGIDevice),(void**)&dxgiDevice));
IDXGIAdapter* dxgiAdapter = 0;
HR(dxgiDevice->GetParent(__uuidof(IDXGIAdapter),(void**))&dxgiAdapter));
// Finally got the IDXGIFactory interface.
IDXGIFactory* dxgiFactory = 0;
HR(dxgiAdapter->GetParent(__uuidof(IDXGIFactory),(void**))&dxgiFactory));
// Now, create the swap chain.
IDXGISwapChain* mSwapChain;
HR(dxgiFactory->CreateSwapChain(md3dDevice, )&sd, )&mSwapChain));
// Release our acquired COM interfaces (because we are done with them).
ReleaseCOM(dxgiDevice);
ReleaseCOM(dxgiAdapter);
ReleaseCOM(dxgiFactory);
NOTE: DXGI(DirectX图形基础设施)是与Direct3D的独立API,可以处理图形相关的东西,如交换链,枚举图形硬件以及窗口和全屏模式之间的切换。将其与Direct3D分开的是因为,其他图形API(如Direct2D)也需要交换链,枚举图形硬件以及窗口和全屏模式之间的切换。这样,很多图形API都可以使用DXGI API。
4.2.5 创建渲染目标视图
如第4.1.6节所述,我们不直接将资源绑定到流水线阶段;相反,我们必须为资源创建资源视图,并将视图绑定到流水线阶段。特别是为了将后台缓冲区绑定到流水线的输出合并阶段(因此Direct3D可以渲染到其上),我们需要为后台缓冲区创建渲染目标视图。以下示例代码显示了如何完成:
ID3D11RenderTargetView* mRenderTargetView;
ID3D11Texture2D* backBuffer;
mSwapChain->GetBuffer(0, __uuidof(ID3D11Texture2D),reinterpret_cast<void**>(&backBuffer));
md3dDevice->CreateRenderTargetView(backBuffer, 0, &mRenderTargetView);
ReleaseCOM(backBuffer);
1、使用IDXGISwapChain::GetBuffer方法获取指向交换链的后台缓冲区的指针。该方法的第一个参数是一个索引,用于标识要获取的特定后台缓冲区(如果有多个)。在我们的演示中,我们只使用一个后台缓冲区,它的索引为零。第二个参数是缓冲区的接口类型,通常总是2D纹理(ID3D11Texture2D)。第三个参数返回一个指向后台缓冲区的指针。
2、要创建渲染目标视图,我们使用ID3D11Device::CreateRenderTargetView方法。第一个参数指定将用作渲染目标的资源,在前面的示例中,它是后缓冲区(即,我们正在向后缓冲区创建渲染目标视图)。第二个参数是指向D3D11_RENDER_TARGET_VIEW_DESC的指针。除此之外,该结构还描述了资源中元素的数据类型(格式)。如果资源是以类型格式创建的(即不是无类型的),则该参数可以为空,这表示为了创建一个到该资源的第一个mipmap级别的视图(后台缓冲区只有一个mipmap级别),格式为所创建资源的格式。(Mipmap在第8章中讨论)因为我们指定了我们的后台缓冲区的类型,所以我们为这个参数指定null。第三个参数返回一个指向create render目标视图对象的指针。
3、对IDXGISwapChain::GetBuffer的调用将COM参考增加到后台缓冲区,这就是为什么我们在完成它时在代码片段的末尾释放它(ReleaseCOM)。
4.2.6 创建深度/模板缓冲区和视图
我们现在需要创建深度/模板缓冲区。如§4.1.5所述,深度缓冲区只是一个2D纹理,用于存储深度信息(如果使用模板,则为模板信息)。要创建纹理,我们需要填写一个描述要创建的纹理的D3D11_TEXTURE2D_DESC结构,然后调用ID3D11Device::CreateTexture2D方法。D3D11_TEXTURE2D_DESC结构定义如下:
typedef struct D3D11_TEXTURE2D_DESC {
UINT Width;
UINT Height;
UINT MipLevels;
UINT ArraySize;
DXGI_FORMAT Format;
DXGI_SAMPLE_DESC SampleDesc;
D3D11_USAGE Usage;
UINT BindFlags;
UINT CPUAccessFlags;
UINT MiscFlags;
} D3D11_TEXTURE2D_DESC;
1.Width:纹理的纹理宽度。
2.Height:纹素的纹理高度。
3.MipLevels:mipmap的级别。Mipmaps在关于纹理的章节中有介绍。为了创建深度/模板缓冲区,我们的纹理只需要一个mipmap级别。
4.ArraySize:纹理数组中的纹理数。对于深度/模板缓冲区,我们只需要一个纹理。
5.Format:DXGI_FORMAT枚举类型的成员,指定纹素的格式。对于深度/模板缓冲区,这需要是§4.1.5中显示的格式之一。
6.SampleDesc:多样本和质量水平的数量;见§4.1.7和§4.1.8。回想一下,4X MSAA使用比屏幕分辨率大4倍的后台缓冲区和深度缓冲区,以便每个子像素存储颜色和深度/模板信息。因此,用于深度/模板缓冲区的多重采样设置必须与渲染目标所使用的设置相匹配。
7.Usage(用法):D3D11_USAGE枚举类型的成员,指定纹理将如何使用。四个使用值是:
(a)D3D11_USAGE_DEFAULT:如果GPU(图形处理单元)将读取和写入资源,请指定此用法。CPU无法使用此用法读取或写入资源。对于深度/模板缓冲区,我们指定D3D11_USAGE_DEFAULT,因为GPU将对深度/模板缓冲区进行所有读取和写入操作。
(b)D3D11_USAGE_IMMUTABLE:如果资源的内容在创建后不会更改,则指定此用法。这允许一些潜在的优化,因为资源将被GPU只读。除了在创建时初始化资源,CPU和GPU不能写入不可变资源。 CPU不能从不可变资源读取。
(c)D3D11_USAGE_DYNAMIC:如果应用程序(CPU)需要频繁更新资源的数据内容(例如,基于每帧),则指定此用法。具有这种用法的资源可以由GPU读取并由CPU写入。因为新的数据必须从CPU存储器(即系统RAM)转移到GPU存储器(即视频RAM),所以从CPU动态地更新GPU资源会导致性能下降。因此,除非必要,否则应避免动态使用。
(d)D3D11_USAGE_STAGING:如果应用程序(CPU)需要能够读取资源的副本(即资源支持将数据从视频内存复制到系统内存),请指定此用法。从GPU复制到CPU内存是一个缓慢的操作,应该避免,除非有必要。
8. BindFlags:一个或多个标记(或操作),指定资源将绑定到管道的位置。对于深度/模板缓冲区,需要D3D11_BIND_DEPTH_STENCIL。纹理的其他绑定标志是:
D3D11_BIND_RENDER_TARGET:纹理将被绑定到管道的渲染目标。
D3D11_BIND_SHADER_RESOURCE:纹理将被绑定到管道的着色器资源。
9.CPUAccessFlags:指定CPU如何访问资源。如果CPU需要写入资源,请指定D3D11_CPU_ACCESS_WRITE。具有写访问权限的资源必须使用D3D11_USAGE_DYNAMIC或D3D11_USAGE_STAGING。如果CPU需要从缓冲区中读取,请指定D3D11_CPU_ACCESS_READ。具有读取访问权限的缓冲区必须使用D3D11_USAGE_STAGING。对于深度/模板缓冲区,只有GPU写入并读取深度/缓冲区;因此,我们可以为此值指定零,因为CPU将不会读取或写入深度/模板缓冲区。
10.MiscFlags:可选标志,不适用于深度/模板缓冲区,因此设置为零。
NOTE:应该避免使用标志D3D11_USAGE_DYNAMIC和D3D11_USAGE_STAGING,因为有性能损失。常见的因素是CPU涉及到这两个标志。在CPU和GPU内存之间来回切换会导致性能下降。为了获得最大的速度,当我们创建我们所有的资源并将数据上传到GPU时,图形硬件效果最佳,资源保留在只有GPU读取和写入资源的GPU上。但是,对于某些应用程序,这些标志是无法避免的,并且必须涉及CPU,但是应该始终尽量减少这些标志的使用。
我们将看到不同的选项创建资源的示例; 例如不同用途的标志位,不同的绑定标志和不同的CPU访问标志。现在,只集中在我们需要指定的值来创建深度/模板缓冲区,而不必担心每一个选项。
另外,在使用深度/模板缓冲区之前,我们必须创建一个关联的深度/模板视图来绑定到管道。这与创建渲染目标视图类似。以下代码示例显示了我们如何创建深度/模板纹理及其对应的深度/模板视图:
D3D11_TEXTURE2D_DESC depthStencilDesc;
depthStencilDesc.Width = mClientWidth;
depthStencilDesc.Height = mClientHeight;
depthStencilDesc.MipLevels = 1;
depthStencilDesc.ArraySize = 1;
depthStencilDesc.Format = DXGI_FORMAT_D24_UNORM_S8_UINT;
// Use 4X MSAA? --must match swap chain MSAA values.
if( mEnable4xMsaa )
{
depthStencilDesc.SampleDesc.Count = 4;
depthStencilDesc.SampleDesc.Quality = m4xMsaaQuality-1;
}//No MSAA
else
{
depthStencilDesc.SampleDesc.Count = 1;
depthStencilDesc.SampleDesc.Quality = 0;
}
depthStencilDesc.Usage = D3D11_USAGE_DEFAULT;
depthStencilDesc.BindFlags = D3D11_BIND_DEPTH_STENCIL;
depthStencilDesc.CPUAccessFlags = 0;
depthStencilDesc.MiscFlags = 0;
ID3D11Texture2D* mDepthStencilBuffer;
ID3D11DepthStencilView* mDepthStencilView;
HR(md3dDevice->CreateTexture2D(
&depthStencilDesc, // Description of texture to create.
0,
&mDepthStencilBuffer)); // Return pointer to depth/stencil buffer.
HR(md3dDevice->CreateDepthStencilView(
mDepthStencilBuffer, // Resource we want to create a view to.
0,
&mDepthStencilView)); // Return depth/stencil view
CreateTexture2D的第二个参数是用于填充纹理的初始数据的指针。然而,因为这个纹理被用作深度/模板缓冲区,所以我们不需要用任何数据填充它。当执行深度缓冲和模板操作时,Direct3D将直接写入深度/模板缓冲区。因此,我们为第二个参数指定null。
CreateDepthStencilView的第二个参数是一个指向D3D11_DEPTH_STENCIL_VIEW_DESC的指针。此外,该结构还描述了资源中元素的数据类型(格式)。如果资源是以类型格式创建的(即,不是无类型的),则该参数可以为空,这表示可以创建一个到该资源的第一个mipmap级别的视图(只有一个mipmap级别创建了深度/模板缓冲区 )与资源创建的格式。(Mipmap在第8章中讨论)因为我们指定了深度/模板缓冲区的类型,所以我们为此参数指定null。
4.2.7将视图绑定到输出合并阶段
现在我们已经创建了后台缓冲区和深度缓冲区的视图,我们可以将这些视图绑定到管道的输出合并阶段,以使资源成为管道的渲染目标和深度/模板缓冲区:
md3dImmediateContext->OMSetRenderTargets(1, &mRenderTargetView, mDepthStencilView);
第一个参数是我们绑定的渲染目标的数量;我们在这里只绑定一个,但是可以同时绑定多个渲染目标(高级技术)。第二个参数是指向要绑定到管道的渲染目标视图指针数组中的第一个元素的指针。第三个参数是指向要绑定到管道的深度/模板视图的指针。
NOTE:我们可以设置一组渲染目标视图,但只能设置一个深度/模板视图。使用多个渲染目标是本书第三部分涉及的高级技术。
4.2.8设置视口
通常我们将3D场景绘制到整个后台缓冲区。然而,有时我们只想将3D场景绘制到后缓冲区的一个区域;见图4.7。
后台缓冲区我们所绘制的区域称为viewport,它由以下结构描述:
typedef struct D3D11_VIEWPORT {
FLOAT TopLeftX;
FLOAT TopLeftY;
FLOAT Width;
FLOAT Height;
FLOAT MinDepth;
FLOAT MaxDepth;
} D3D11_VIEWPORT;
图4.7 通过修改viewport,可以将3D场景绘制到后台缓冲区的部分区域中。然后,后台缓冲区被呈现给客户端窗口
前四个数据成员定义了我们正在绘制的客户端窗口的矩形区域对应的viewport(因为数据成员的类型为float,所以可以指定分数坐标)。MinDepth成员指定最小深度缓冲区值,MaxDepth指定最大深度缓冲区值。Direct3D使用0到1的深度缓冲区范围,因此除非需要特殊效果,否则MinDepth和MaxDepth应分别设置为0和1。
一旦我们填写了D3D11_VIEWPORT结构,就通过Direct3D的ID3D11DeviceContext::RSSetViewports方法设置了viewport。以下示例创建并设置绘制到整个后台缓冲区的视口:
D3D11_VIEWPORT vp;
vp.TopLeftX = 0.0f;
vp.TopLeftY = 0.0f;
vp.Width = static_cast<float>(mClientWidth);
vp.Height = static_cast<float>(mClientHeight);
vp.MinDepth = 0.0f;
vp.MaxDepth = 1.0f;
md3dImmediateContext->RSSetViewports(1, &vp);
第一个参数是要绑定的视口数(高级效果会用到多个),第二个参数是指向视口数组的指针。
例如,您可以使用视口来实现双人游戏模式的分屏。您将创建两个视口,一个用于屏幕的左半部分,另一个用于屏幕的右半部分。然后,您将从玩家1的视角将3D场景绘制到左侧视口中,并将3D场景从玩家2的视角绘制到右侧视口中。或者,您可以使用视口渲染到屏幕的一个子矩形,并使用UI(用户界面)控件(如按钮,滑块和列表框)填充剩余区域。
4.3时间和动画
要正确做动画,我们需要跟踪时间。尤其需要测量动画中帧之间经过的时间量。如果帧率高,帧之间的时间间隔将非常短; 因此,我们需要一个高精度的定时器。
4.3.1性能计时器
为了准确的时间测量,我们使用性能计时器(或性能计数器)。使用Win32函数查询性能计时器,必须#include
__int64 currTime;
QueryPerformanceCounter((LARGE_INTEGER*)&currTime);
此函数通过其参数返回当前时间值,该参数是64位整数值。
要获取性能计时器的频率(每秒计数),我们使用QueryPerformanceFrequency函数:
__int64 countsPerSec;
QueryPerformanceFrequency((LARGE_INTEGER*)&countsPerSec);
那么每个计数所消耗的秒数就是每秒计数的倒数:
mSecondsPerCount = 1.0 / (double)countsPerSec;
因此,要将时间读取值valueInCounts转换为秒,我们只需将其乘以转换因子mSecondsPerCount:
valueInSecs = valueInCounts* mSecondsPerCount ;
QueryPerformanceCounter函数返回的值本身并没有意义。我们做的是使用QueryPerformanceCounter获取当前时间值,稍后再次使用QueryPerformanceCounter获取当前时间值。那么两次调用的差就是经过的时间。也就是说,我们总是看看两个时间戳之间的相对差异,以测量时间,而不是性能计数器返回的实际值。以下更好地说明了这个想法:
__int64 A = 0;
QueryPerformanceCounter((LARGE_INTEGER*)&A);
/* Do work */
__int64 B = 0;
QueryPerformanceCounter((LARGE_INTEGER*)&B);
于是花了(B-A)的计量来做工作,或(B-A)* mSecondsPerCount秒做工作。
NOTE:MSDN关于QueryPerformanceCounter有以**释:“在多处理器计算机上,调用哪个处理器不重要。然而,由于基本输入/输出系统(BIOS)或硬件抽象层(HAL)中的错误,您可能在不同处理器上获得不同的结果。“您可以使用SetThreadAffinityMask函数,使主应用程序线程不会切换到另一个处理器。
4.3.2游戏计时器类
在接下来的两节中,我们将讨论以下Game Timer类的实现。
class GameTimer
{
public:
GameTimer();
float GameTime()const; // in seconds
float DeltaTime()const; // in seconds
void Reset(); // Call before message loop.
void Start(); // Call when unpaused.
void Stop(); // Call when paused.
void Tick(); // Call every frame.
private:
double mSecondsPerCount;
double mDeltaTime;
__int64 mBaseTime;
__int64 mPausedTime;
__int64 mStopTime;
__int64 mPrevTime;
__int64 mCurrTime;
bool mStopped;
};
计数器的构造函数特别是查询性能的频率。其他成员函数将在接下来的两节中讨论。
GameTimer::GameTimer()
: mSecondsPerCount(0.0), mDeltaTime(-1.0), mBaseTime(0),
mPausedTime(0), mPrevTime(0), mCurrTime(0), mStopped(false)
{
__int64 countsPerSec;
QueryPerformanceFrequency((LARGE_INTEGER*)&countsPerSec);
mSecondsPerCount = 1.0/(double)countsPerSec;
}
NOTE:Game Timer类和实现在Game Timer.h和GameTimer.cpp文件中,可以在示例代码的Common目录中找到。
4.3.3 帧之间的时间
当我们渲染我们的动画帧时,我们需要知道帧之间经过了多少时间,以便我们可以根据已经过去的时间来更新我们的游戏对象。帧之间经过的时间计算如下。令为第i帧期间性能计数器返回的时间,并使为前一帧期间性能计数器返回的时间。那么在读数和读数之间经过的时间是。对于实时渲染,通常需要每秒至少30帧的平滑动画(通常帧率会更高);因此,趋向于一个很小的数。
以下代码显示了如何在代码中计算Δt:
void GameTimer::Tick()
{
if(mStopped)
{
mDeltaTime = 0.0;
return;
}
//Get the time this frame.
__int64 currTime;
QueryPerformanceCounter((LARGE_INTEGER*)&currTime);
mCurrTime = currTime;
// Time difference between this frame and the previous.
mDeltaTime = (mCurrTime - mPrevTime)*mSecondsPerCount;
// Prepare for next frame.
mPrevTime = mCurrTime;
// Force nonnegative. The DXSDK's CDXUTTimer mentions that if the
// processor goes into a power save mode or we get shuffled to another
// processor, then mDeltaTime can be negative.
if(mDeltaTime < 0.0)
{
mDeltaTime = 0.0;
}
}
float GameTimer::DeltaTime()const
{
return (float)mDeltaTime;
}
函数Tick在应用程序消息循环中调用如下:
int D3DApp::Run()
{
MSG msg = {0};
mTimer.Reset();
while(msg.message != WM_QUIT)
{
// If there are Window messages then process them.
if(PeekMessage(&msg, 0, 0, 0, PM_REMOVE))
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
//Otherwise, do animation/game stuff.
else
{
mTimer.Tick();
if(!mAppPaused)
{
CalculateFrameStats();
UpdateScene(mTimer.DeltaTime());
DrawScene();
}
else
{
Sleep(100);
}
}
}
return (int)msg.wParam;
}
以这种方式,每个帧计算Δt并反馈到UpdateScene方法,以便可以根据从上一帧动画过去的时间来更新场景。Reset方法的实现是:
void GameTimer::Reset()
{
__int64 currTime;
QueryPerformanceCounter((LARGE_INTEGER*)&currTime);
mBaseTime = currTime;
mPrevTime = currTime;
mStopTime = 0;
mStopped = false;
}
所显示的一些变量尚未讨论(见§4.3.4)。但是,我们看到调用Reset时将初始化mPrevTime到当前时间。这样做很重要,因为对于第一帧动画,没有先前的帧,因此没有以前的时间戳。因此,在消息循环开始之前,需要在Reset方法中初始化该值。
4.3.4总时间
另一个可用的测量时间是自应用程序启动以来经过的时间,不包含暂停的时间; 我们称之为总时间。接下来说明如何使用。假设玩家有300秒完成一个关卡。当关卡开始时,我们可以得到从应用程序启动起经过的时间。然后在关卡开始之后,我们可以检查应用开始后的时间t。如果>300s(参见图4.8),则玩家已经在关卡中超过300秒并且失败。很明显,在这种情况下,我们不想包含玩游戏暂停的时间。
总时间的另一个应用是当我们想要根据时间函数制作动画。假设我希望有一个光轨作为时间的函数。其位置可以用参数方程来描述:
这里t表示时间,并且随着t(时间)增加,光的坐标被更新,使得光在y = 20平面中以半径为10的圆移动。同样,我们也不想计算暂停时间; 见图4.9。
要实现总时间,我们使用以下变量:
__int64 mBaseTime;
__int64 mPausedTime;
__int64 mStopTime;
正如我们在§4.3.3中看到的那样,mBaseTime被初始化为当前调用Reset的时间。我们可以将此视为应用程序启动的时间。在大多数情况下,您只会在消息循环之前调用重置一次,因此mBaseTime在应用程序的生命周期始终保持不变。变量mPausedTime累积所有暂停消耗的时间。我们要累积这个时间,所以我们可以从总运行时间减去它,以便不计算暂停时间。mStopTime变量给我们定时器停止(暂停)的时间; 这是用来帮助我们跟踪暂停的时间。
GameTimer类的两个重要方法是Stop和Start。当应用程序分别暂停和取消暂停时,应该调用它们,以便GameTimer可以跟踪暂停的时间。代码注释说明了这两种方法的细节。
图4.8。 计算关卡开始以来的时间。请注意,我们选择应用程序开始时间作为原点(零),并测量相对于该参考帧的时间值。
图4.9。 如果我们在t1暂停,在t2取消暂停,并计算暂停时间,那么当我们取消暂停时,位置将从p(t1)突然跳到p(t2)。
void GameTimer::Stop()
{
// If we are already stopped, then don't do anything.
if(!mStopped)
{
__int64 currTime;
QueryPerformanceCounter((LARGE_INTEGER*)&currTime);
// Otherwise, save the time we stopped at, and set
// the Boolean flag indicating the timer is stopped.
mStopTime = currTime;
mStopped = true;
}
}
void GameTimer::Start()
{
__int64 startTime;
QueryPerformanceCounter((LARGE_INTEGER*)&startTime);
// Accumulate the time elapsed between stop and start pairs.
//
// |<-------d------->|
// ---------------*-----------------*------------> time
// mStopTime startTime// If we are resuming the timer from a stopped state...
if( mStopped )
{
// then accumulate the paused time.
mPausedTime += (startTime - mStopTime);
// since we are starting the timer back up, the current
// previous time is not valid, as it occurred while paused.
// So reset it to the current time.
mPrevTime = startTime;
// no longer stopped...
mStopTime = 0;
mStopped = false;
}
}
最后,TotalTime成员函数返回自复位以来经过的时间不计算暂停时间,具体实现如下:
float GameTimer::TotalTime()const
{
// If we are stopped, do not count the time that has passed
// since we stopped. Moreover, if we previously already had
// a pause, the distance mStopTime - mBaseTime includes paused
// time,which we do not want to count. To correct this, we can
// subtract the paused time from mStopTime:
//
// previous paused time
// |<----------->|
// ---*------------*-------------*-------*-----------*------> time
// mBaseTime mStopTime mCurrTime
if(mStopped)
{
return (float)(((mStopTime - mPausedTime)-mBaseTime)*mSecondsPerCount);
}
//The distance mCurrTime - mBaseTime includes paused time,
// which we do not want to count. To correct this, we can subtract
// the paused time from mCurrTime:
//
// (mCurrTime - mPausedTime) - mBaseTime
//
// |<--paused time-->|
// ----*---------------*-----------------*------------*------> time
// mBaseTime mStopTime startTime mCurrTime
else
{
return (float)(((mCurrTime-mPausedTime)-mBaseTime)*mSecondsPerCount);
}
}
Note: 我们的示例创建了一个GameTimer实例,用于测量自应用程序启动以来的总时间,和帧之间经过的时间; 但是,您也可以创建其他实例并将其用作通用“秒表”。例如,当一个炸弹被点燃时,你可以启动一个新的GameTimer,当TotalTime达到5秒钟,触发炸弹爆炸事件。
4.4演示应用框架
本书中的演示使用d3dUtil.h,d3dApp.h和d3dApp.cpp文件中的代码,可以从本书的网站下载。这些在每个演示应用程序中使用的通用文件位于本书第二部分和第三部分的Common目录中,以便每个项目中的文件不会重复。d3dUtil.h文件包含有用的实用程序代码,d3dApp.h和d3dApp.cpp文件包含用于封装Direct3D应用程序的核心Direct3D应用程序类代码。请自行研习相关代码,我们不会详细讲解每一行代码(例如,我们不显示如何创建窗口,因为基本的Win32编程是本书的先决条件)。该框架的目标是隐藏窗口创建代码和Direct3D初始化代码; 通过隐藏这段代码,我们觉得它使得演示更不会分散注意力,因为您只能关注示例代码试图说明的具体细节。
4.4.1 D3DApp
D3DApp类是基本的Direct3D应用程序类,它提供了创建主应用程序窗口,运行应用程序消息循环,处理窗口消息以及初始化Direct3D的功能。此外,该类定义了演示应用程序的框架功能。客户端将从D3DApp派生,覆盖虚拟框架函数,并且仅实例化派生的D3DApp类的单个实例。D3DApp类定义如下:
class D3DApp
{
public:
D3DApp(HINSTANCE hInstance);
virtual ~D3DApp();
HINSTANCE AppInst()const;
HWND MainWnd()const;
float AspectRatio()const;
int Run();
// Framework methods. Derived client class overrides these methods to
// implement specific application requirements.
// 框架方法。派生类需要重载这些方法实现所需的功能。
virtual bool Init();
virtual void OnResize();
virtual void UpdateScene(float dt)=0;
virtual void DrawScene()=0;
virtual LRESULT MsgProc(HWND hwnd, UINT msg,WPARAM wParam, LPARAM lParam);
// Convenience overrides for handling mouse input.
// 处理鼠标输入事件的便捷重载函数
virtual void OnMouseDown(WPARAM btnState, int x, int y){ }
virtual void OnMouseUp(WPARAM btnState, int x, int y){ }
virtual void OnMouseMove(WPARAM btnState, int x, int y){ }
protected:
bool InitMainWindow();
bool InitDirect3D();
void CalculateFrameStats();
protected:
HINSTANCE mhAppInst; // application instance handle应用程序实例句柄
HWND mhMainWnd; // main window handle主窗口句柄
bool mAppPaused; // is the application paused?程序是否处在暂停状态
bool mMinimized; // is the application minimized?程序是否最小化
bool mMaximized; // is the application maximized?程序是否最大化
bool mResizing; // are the resize bars being dragged?程序是否处在改变大小的状态
UINT m4xMsaaQuality; // quality level of 4X MSAA 4X MSAA质量等级
// Used to keep track of the "delta-time" and game time (§4.3).
// 用于记录"delta-time"和游戏时间(§4.3)
GameTimer mTimer;
// The D3D11 device (§4.2.1), the swap chain for page flipping
// (§4.2.4), the 2D texture for the depth/stencil buffer (§4.2.6),
// the render target (§4.2.5) and depth/stencil views (§4.2.6), and
// the viewport (§4.2.8).
// D3D11设备(§4.2.1),交换链(§4.2.4),用于深度/模板缓存的2D纹理(§4.2.6),
// 渲染目标(§4.2.5)和深度/模板视图(§4.2.6),和视口(§4.2.8)。
ID3D11Device* md3dDevice;
ID3D11DeviceContext* md3dImmediateContext;
IDXGISwapChain* mSwapChain;
ID3D11Texture2D* mDepthStencilBuffer;
ID3D11RenderTargetView* mRenderTargetView;
ID3D11DepthStencilView* mDepthStencilView;
D3D11_VIEWPORT mScreenViewport;
// The following variables are initialized in the D3DApp constructor
// to default values. However, you can override the values in the
// derived class constructor to pick different defaults.
// Window title/caption. D3DApp defaults to "D3D11 Application".
// 下面的变量是在D3DApp构造函数中设置的。但是,你可以在派生类中重写这些值。
// 窗口标题。D3DApp的默认标题是"D3D11 Application"。
std::wstring mMainWndCaption;
// Hardware device or reference device? D3DApp defaults to
// D3D_DRIVER_TYPE_HARDWARE.
// Hardware device还是reference device?D3DApp默认使用D3D_DRIVER_TYPE_HARDWARE。
D3D_DRIVER_TYPE md3dDriverType;
// Initial size of the window's client area. D3DApp defaults to
// 800x600. Note, however, that these values change at runtime
// to reflect the current client area size as the window is resized.
// 窗口的初始大小。D3DApp默认为800x600。注意,当窗口大小在运行阶段改变时,这些值也会随之改变。
int mClientWidth;
int mClientHeight;
// True to use 4X MSAA (§4.1.8). The default is false.
// 设置为true则使用4XMSAA(§4.1.8),默认为false。
bool mEnable4xMsaa;
};
我们在前面的代码中讲解了一些数据成员; 这些方法将在后续章节中讨论。
4.4.2 Non-Framework 方法
1.D3DApp:构造函数简单地将数据成员初始化为默认值。
2.~D3DApp:析构函数释放D3DApp获取的COM接口。
3.AppInst:普通的访问函数返回应用程序实例句柄的副本。
4.MainWnd:简单访问功能返回主窗口句柄的副本。
5.AspectRatio:纵横比定义为后缓冲区宽度与其高度的比值。宽高比将在第五章中用到。它被简单地实现为:
float D3DApp::AspectRatio()const
{
return static_cast<float>(mClientWidth) / mClientHeight;
}
6.Run:此方法将应用程序消息循环。它使用Win32 PeekMessage功能,以便在没有消息存在时可以处理我们的游戏逻辑。该功能的实现见§4.3.3。
7.InitMainWindow:初始化主应用程序窗口; 我们假设读者熟悉基本的Win32窗口初始化。
8.InitDirect3D:通过执行§4.2中讨论的步骤来初始化Direct3D。
9.CalculateFrameStats:计算每秒的平均帧数和每帧的平均毫秒数。§4.4.4中讨论了这种方法的实现。
4.4.3 Framework 方法
对于本书中的每个示例应用程序,我们始终覆盖了D3DApp的五个虚拟函数。 这五个函数用于实现特定样本的特定代码。此设置的好处是初始化代码,消息处理等在D3DApp类中实现,因此派生类只需要专注于演示应用程序的特定代码。以下是框架方法的描述:
1.Init:使用此方法为应用程序初始化代码,如分配资源,初始化对象和设置灯光。该方法的D3DApp实现调用InitMainWindow和InitDirect3D; 因此,您应该首先在派生的实现中调用此方法的D3DApp版本,如下所示:
bool TestApp::Init()
{
if(!D3DApp::Init())
return false;
/* Rest of initialization code goes here */
}
以便ID3D11Device可用于其余的初始化代码。(Direct3D资源获取需要一个有效的ID3D11Device。)
2.OnResize:当接收到WM_SIZE消息时,此方法由D3DApp::MsgProc调用。当窗口调整大小时,需要更改一些Direct3D属性,因为它们取决于客户区域的尺寸。特别地,后台缓冲区和深度/模板缓冲区需要重新创建以匹配窗口的新客户区。可以通过调用IDXGISwapChain :: ResizeBuffers方法调整后台缓冲区大小。深度/模板缓冲区需要被销毁,然后根据新的维度进行重构。此外,还需要重新创建渲染目标和深度/模板视图。OnResize的D3DApp实现处理调整back和depth/stencil缓冲区大小所需的代码; 看到源代码的简单细节。除缓冲区之外,其他属性取决于客户区域的大小(例如,投影矩阵),因此该方法是框架的一部分,因为客户端代码可能需要在调整窗口大小时执行其自己的一些代码。
3.UpdateScene:该抽象方法被每帧调用,并且应该用于随时间更新3D应用(例如,执行动画,移动相机,进行碰撞检测,检查用户输入等)。
4.DrawScene:这个抽象方法被每帧调用,并且是我们发出渲染命令的地方,以便将我们当前的帧实际绘制到后台缓冲区。完成绘制框架后,我们调用IDXGISwapChain::Present方法将后台缓冲区呈现给屏幕。
5.MsgProc:该方法实现主应用程序窗口的窗口过程功能。通常情况下,只有在需要处理D3DApp::MsgProc的消息时才需要重写此方法(或者不符合您的喜好时)。§4.4.5中探讨了该方法的D3DApp实现。如果您重写此方法,则任何不处理的消息都应转发给D3DApp::MsgProc。
Note:除了前五种帧方法之外,我们还提供了另外三种虚拟函数,以方便用户按下,释放鼠标按钮,鼠标移动时处理事件。
virtual void OnMouseDown(WPARAM btnState, int x, int y){ }
virtual void OnMouseUp(WPARAM btnState, int x, int y) { }
virtual void OnMouseMove(WPARAM btnState, int x, int y){ }
这样,如果要处理鼠标消息,则可以覆盖这些方法,而不是覆盖MsgProc方法。第一个参数与各种鼠标消息的WPARAM参数相同,存储鼠标按钮状态(即哪个事件发生时按下鼠标按钮)。第二和第三个参数是鼠标光标的客户区(x,y)坐标。
4.4.4 Frame 统计
游戏和图形应用程序通常测量每秒渲染的帧数(FPS)。为此,我们简单地计算在特定时间段 t 内处理的帧的数量(并将其存储在变量n中)。然后,时间段 t 内的平均FPS为。如果我们设置t=1,则。在我们的代码中,我们使用t=1(秒),因为它避免了一个除法,而且一秒给出了相当不错的平均值 - 它不是太长而不是太短。计算FPS的代码是由D3DApp :: CalculateFrameStats方法提供:
void D3DApp::CalculateFrameStats()
{
// Code computes the average frames per second, and also the
// average time it takes to render one frame. These stats
// are appeneded to the window caption bar.static int frameCnt = 0;
static float timeElapsed = 0.0f;
frameCnt++;
// Compute averages over one second period.
if( (mTimer.TotalTime() - timeElapsed) >= 1.0f )
{
float fps = (float)frameCnt; // fps = frameCnt / 1
float mspf = 1000.0f / fps;
std::wostringstream outs;
outs.precision(6);
outs << mMainWndCaption << L" "
<< L"FPS: " << fps << L" "
<< L"Frame Time: " << mspf << L" (ms)";
SetWindowText(mhMainWnd, outs.str().c_str());
// Reset for next average.
frameCnt = 0;
timeElapsed += 1.0f;
}
}
这个方法每帧调用,以便对帧进行计数。
除了计算FPS之外,以前的代码还计算平均处理一个帧所需的毫秒数:
float mspf = 1000.0f / fps;
NOTE:每帧的秒数只是FPS的倒数,但是我们乘以1000 ms / 1 s,从秒到毫秒转换(每秒钟有1000毫秒)。
这一行的想法是计算渲染帧所需的时间(以毫秒为单位);这是与FPS不同的数量(但是观察该值可以从FPS导出)。实际上,渲染帧所需的时间比FPS更有用,因为我们可以直接看到在修改场景时渲染帧所需的时间的增加/减少。另一方面,当我们修改我们的场景时,FPS不会轻易告诉我们时间的增加/减少。此外,正如[Dunlop03]在他的文章FPS与Frame Time中所指出的,由于FPS曲线的非线性,使用FPS可能会产生误导的结果。例如,考虑情况(1):假设我们的应用程序运行在1000 FPS,以1 ms(毫秒)渲染帧。如果帧速率降至250 FPS,则渲染帧需要4 ms。现在考虑情况(2):假设我们的应用程序运行在100 FPS,需要10 ms渲染一帧。如果帧速率下降到约76.9 FPS,则渲染帧大约需要13 ms。在这两种情况下,每帧的渲染增加了3 ms,因此两者都代表渲染帧所需的时间相同。阅读FPS并不直接。从1000 FPS到250 FPS的下降似乎比从100 FPS下降到76.9 FPS要大得多;然而,正如我们刚刚显示的,它们实际上代表了渲染帧所花费的时间的增加。
4.4.5消息处理程序
我们为我们的应用程序框架实现的窗口过程是最低限度的。一般来说,我们不关心Win32消息运行原理。实际上,我们的应用程序代码的核心在空闲处理期间被执行(即,当没有窗口消息存在时)。同时,还需要处理一些重要的信息。但由于窗口过程的长度,我们并没有在这里嵌入所有的代码。相反,我们只是解释我们处理的每个消息背后的动机。有兴趣的朋友可以下载源代码文件,花一些时间熟悉应用程序框架代码,因为它是本书每个示例的基础。
我们处理的第一条消息是WM_ACTIVATE消息。当应用程序**或停用时,会发送此消息。具体实现如下:
case WM_ACTIVATE:
if(LOWORD(wParam) == WA_INACTIVE)
{
mAppPaused = true;
mTimer.Stop();
}else
{
mAppPaused = false;
mTimer.Start();
}return 0;
如您所见,当我们的应用程序停用时,我们将数据成员mAppPaused设置为true,当我们的应用程序变为活动状态时,我们将数据成员mAppPaused设置为false。另外,当应用程序暂停时,我们停止定时器,然后一旦应用程序再次起作用就恢复定时器。如果我们回顾D3DApp::Run(§4.3.3)的实现,我们发现如果我们的应用程序暂停,我们不会更新我们的应用程序代码,而是将一些CPU周期释放回操作系统; 这样,当我们的应用程序处于非活动状态时,它不会占用CPU周期。
我们处理的下一个消息是WM_SIZE消息。回想一下,在调整窗口大小时调用此消息。处理此消息的主要原因是我们希望后台缓冲区和深度/模板尺寸与客户区域矩形的尺寸匹配(因此不会发生拉伸)。因此,每次调整窗口大小时,我们要调整缓冲区大小的大小。调整缓冲区大小的代码在D3DApp :: OnResize中实现。如前所述,可以通过调用IDXGISwapChain :: ResizeBuffers方法调整后台缓冲区大小。深度/模板缓冲区需要被破坏,然后根据新的维度进行重新构建。此外,还需要重新创建渲染目标和深度/模板视图。如果用户正在拖动调整大小的栏,我们必须小心,因为拖动调整大小的条可以发送连续的WM_SIZE消息,我们不想不断调整缓冲区的大小。因此,如果我们确定用户通过拖动调整大小,实际上什么也不做(暂停应用程序除外),直到用户拖动调整大小的栏。我们可以通过处理WM_EXITSIZEMOVE消息来实现。当用户释放调整大小时,会发送此消息
// WM_ENTERSIZEMOVE is sent when the user grabs the resize bars.
case WM_ENTERSIZEMOVE:
mAppPaused = true;
mResizing = true;
mTimer.Stop();
return 0;
// WM_EXITSIZEMOVE is sent when the user releases the resize bars.
// Here we reset everything based on the new window dimensions.
case WM_EXITSIZEMOVE:
mAppPaused = false;
mResizing = false;
mTimer.Start();
OnResize();
return 0;
我们处理的接下来的三个消息是微不足道的,所以我们只显示代码:
// WM_DESTROY is sent when the window is being destroyed.
case WM_DESTROY:
PostQuitMessage(0);
return 0;
// The WM_MENUCHAR message is sent when a menu is active and the user presses
// a key that does not correspond to any mnemonic or accelerator key.
case WM_MENUCHAR:
// Don't beep when we alt-enter.
return MAKELRESULT(0, MNC_CLOSE);
// Catch this message to prevent the window from becoming too small.
case WM_GETMINMAXINFO:
((MINMAXINFO*)lParam)->ptMinTrackSize.x = 200;
((MINMAXINFO*)lParam)->ptMinTrackSize.y = 200;
return 0;
最后,为了支持您的鼠标输入虚函数,我们处理以下鼠标消息如下:
case WM_LBUTTONDOWN:case WM_MBUTTONDOWN:
case WM_RBUTTONDOWN:
OnMouseDown(wParam, GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam));
return 0;
case WM_LBUTTONUP:
case WM_MBUTTONUP:
case WM_RBUTTONUP:
OnMouseUp(wParam, GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam));
return 0;
case WM_MOUSEMOVE:
OnMouseMove(wParam, GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam));
return 0;
Note:我们必须 #include <Windows.h>为GET_X_LPARAM和GET_X_LPARAM宏。
4.4.6全屏
我们创建的IDXGISwapChain接口自动捕获ALT-ENTER组合键并将应用程序切换到全屏模式。在全屏模式下按ALT-ENTER将切换回窗口模式。在模式切换期间,应用程序窗口将被调整大小,并向应用程序发送WM_SIZE消息; 这使应用程序有机会调整后台和深度/模板缓冲区的大小以匹配新的屏幕尺寸。此外,如果切换到全屏模式,窗口样式将更改为全屏。您可以使用Visual Studio Spy + +工具来查看在ALT-ENTER中为一个演示应用程序生成的Windows消息。
图4.10 第4章示例程序的截图
练习会探索如何禁用默认的ALT-ENTER功能。
可能复习一下§4.2.3中的DXGI_SWAP_CHAIN_DESC :: Flags描述。
4.4.7 “Init Direct3D”演示
现在我们讨论了应用程序框架,让我们用它做一个小应用程序。父类D3DApp做了这个演示所需的大部分工作,所以该程序几乎不需要我们做什么工作。要注意的是我们如何从D3DApp派生一个类并实现框架函数,我们将在这里编写特定的示例代码。本书中的所有程序都将遵循相同的模板。
#include "d3dApp.h"
class InitDirect3DApp : public D3DApp
{
public:
InitDirect3DApp(HINSTANCE hInstance);
~InitDirect3DApp();
bool Init();
void OnResize();
void UpdateScene(float dt);
void DrawScene();
};
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE prevInstance,
PSTR cmdLine, int showCmd)
{
// Enable run-time memory check for debug builds.
#if defined(DEBUG) | defined(_DEBUG)
_CrtSetDbgFlag( _CRTDBG_ALLOC_MEM_DF | _CRTDBG_LEAK_CHECK_DF );
#endif
InitDirect3DApp theApp(hInstance);
if( !theApp.Init() )
return 0;
return theApp.Run();
}
InitDirect3DApp::InitDirect3DApp(HINSTANCE hInstance)
: D3DApp(hInstance)
{
}
InitDirect3DApp::~InitDirect3DApp()
{
}
bool InitDirect3DApp::Init()
{
if(!D3DApp::Init())
return false;
return true;
}
void InitDirect3DApp::OnResize()
{
D3DApp::OnResize();
}
void InitDirect3DApp::UpdateScene(float dt)
{
}
void InitDirect3DApp::DrawScene()
{
assert(md3dImmediateContext);
assert(mSwapChain);
// Clear the back buffer blue. Colors::Blue is defined in d3dUtil.h.
md3dImmediateContext->ClearRenderTargetView(mRenderTargetView,reinterpret_cast<const float*>(&Colors::Blue));
// Clear the depth buffer to 1.0f and the stencil buffer to 0.
md3dImmediateContext->ClearDepthStencilView(mDepthStencilView,D3D11_CLEAR_DEPTH|D3D11_CLEAR_STENCIL, 1.0f, 0);
// Present the back buffer to the screen.
HR(mSwapChain->Present(0, 0));
}
4.5调试DIRECT3D应用程序
为了缩短代码并尽量减少干扰,我们主要在本书中省略错误处理。然而,我们实现一个宏来检查许多Direct3D函数返回的HRESULT返回码。我们的宏在d3dUtil.h中定义如下:
#if defined(DEBUG) | defined(_DEBUG)
#ifndef HR
#define HR(x) \
{ \
HRESULT hr = (x); \
if(FAILED(hr)) \
{ \
DXTrace(__FILE__, (DWORD)__LINE__, hr, L#x, true); \
} \
}#endif
#else
#ifndef HR
#define HR(x) (x)
#endif
#endif
如果返回函数的返回码指示失败,那么我们将返回码传递到DXTrace函数(#include <dxerr.h>和链接dxerr.lib)中:
HRESULT WINAPI DXTraceW(const char* strFile, DWORD dwLine,HRESULT hr, const WCHAR* strMsg, BOOL bPopMsgBox);
此功能显示一条漂亮的消息框,指示发生错误的文件和行号,以及错误的文本描述以及生成错误的函数的名称; 图4.11展示了一个例子。请注意,如果为DXTrace的最后一个参数指定了false,则不会显示消息框,调试信息将被输出到Visual C ++输出窗口。请注意,如果我们不在调试模式下,宏HR不执行任何操作。此外,HR必须是宏而不是函数;否则__FILE__和__LINE__将引用函数实现的文件和行,而不是调用函数HR的文件和行。
要使用这个宏,我们只是围绕一个返回HRESULT的Direct3D函数,如下例所示:
HR(D3DX11CreateShaderResourceViewFromFile(md3dDevice,L"grass.dds", 0, 0, &mGrassTexRV, 0 ));
这适用于调试我们的演示示例,但是真正的应用程序需要处理应用程序运行后可能发生的错误(例如,不支持的硬件,缺少的文件等)。
Note:L#x将HR宏参数令牌转换为Unicode字符串。这样,我们可以将导致错误的函数调用输出到消息框。
4.6 总结
1.Direct3D可以被认为是程序员和图形硬件之间的中介者。例如,程序员调用Direct3D函数将资源视图绑定到硬件渲染管道,配置渲染管道的输出,并绘制3D几何。
2.在Direct3D 11中,Direct3D 11功能的图形设备必须支持整个Direct3D 11功能集,除了少数例外。
3.组件对象模型(COM)是允许DirectX与语言无关并具有向后兼容性的技术。Direct3D程序员不需要知道COM的细节及其工作原理;他们只需要知道如何获取COM接口以及如何释放它们。
4.1D纹理像数组元素的1D阵列,2D纹理就像数组元素的2D阵列,3D纹理像数组元素的3D阵列。纹理的元素必须具有由DXGI_FORMAT枚举类型的成员描述的格式。纹理通常包含图像数据,但它们也可以包含其他数据,例如深度信息(例如,深度缓冲区)。GPU可以对纹理进行特殊操作,例如过滤和多重采样。
5.在Direct3D中,资源不直接绑定到管道。相反,资源视图绑定到管道。可以创建单个资源的不同视图。以这种方式,单个资源可能绑定到呈现管道的不同阶段。如果使用无类型格式创建资源,则必须在创建视图时指定该类型。
6.ID3D11Device和ID3D11DeviceContext接口可以被认为是我们的物理图形设备硬件的软件控制器; 也就是说,通过这些接口,我们可以与硬件进行交互并指示它执行操作。ID3D11Device接口负责检查功能支持和分配资源。ID3D11DeviceContext接口负责设置渲染状态,将资源绑定到图形管道,并发出渲染命令。
图4.11 如果Direct3D函数返回错误,则由DXTrace函数显示的消息框。
7.为了避免动画中的闪烁,最好将整个动画帧绘制到一个称为后台缓冲区的屏幕的纹理上。一旦整个场景被绘制到给定的动画帧的后台缓冲区中,它被呈现给屏幕为一体完整帧以这种方式,观看者不会看到帧的绘制。在将帧拖到后台缓冲区之后,后台缓冲区和前端缓冲区的交换:后台缓冲区成为前端缓冲区,前端缓冲区成为下一帧动画的后台缓冲区。交换后台和前端缓冲区称为呈现。前端和后端缓冲区形成一个由IDXGISwapChain接口表示的交换链。使用两个缓冲区称为双缓冲。
8.假设不透明的场景对象,最靠近相机的点会遮挡其后面的任何点。深度缓冲是用于确定最靠近相机的场景中的点的技术。这样我们就不必担心我们的场景对象的顺序了。
9.性能计数器是一种高分辨率定时器,可以提供精确的定时测量,用于测量小的时差,例如帧间经过的时间。性能计时器以称为计数的时间单位工作。TheQueryPerformanceFrequency输出性能计时器的每秒计数,然后可以将其从计数单位转换为秒。性能计时器的当前时间值(以计数计)由此获得QueryPerformanceCounter函数。
10.为了计算平均每秒帧数(FPS),我们计算一段时间间隔Δt处理的帧数。令n是随时间Δt计数的帧数,则在该时间间隔内每秒的平均帧数为。帧率可以给出关于性能的误导性结论;处理框架所需的时间更多。花费处理帧的时间(秒)是帧速率的倒数(即)。
11.示例框架用于提供本书中所有演示应用程序的一致性界面。d3dUtil.h,d3dApp.h和d3dApp.cpp文件中提供的代码包装每个应用程序必须实现的标准初始化代码。通过封装这个代码,隐藏细节,这样可以让样本更加专注于展示当前的主题。
12.对于调试模式构建,使用D3D11_CREATE_DEVICE_DEBUG标志创建Direct3D设备以启用调试层。当指定调试标志时,Direct3D将向VC ++输出窗口发送调试消息。还可以使用D3DX库的调试版本(如 d3dx11d.lib)进行调试版本。
4.7 练习
1.通过禁用ALT-ENTER功能在全屏和窗口模式之间切换来修改以前的练习方案;使用IDXGIFactory :: MakeWindowAssociation方法并指定DXGI_MWA_NO_WINDOW_CHANGES标志,以便DXGI不监视消息队列。请注意,IDXGIFactory :: CreateSwapChain被调用后需要调用IDXGIFactory :: MakeWindowAssociation方法。
2.一些系统具有多个适配器(视频卡),并且应用程序可能希望让用户选择使用哪个适配器,而不是始终使用默认适配器。使用IDXGIFactory :: EnumAdapters方法来确定系统上有多少适配器。
3.对于系统所拥有的每个适配器,IDXGIFactory :: EnumAdapters输出一个指向已填写的IDXGIAdapter接口的指针。该接口可用于查询有关适配器的信息。使用IDXGIAdapter :: CheckInterfaceSupport方法查看系统上的适配器是否支持Direct3D 11。
适配器具有与其相关联的输出(例如,监视器)。您可以使用IDXGIAdapter :: EnumOutputs方法枚举特定适配器的输出。使用此方法确定默认适配器的输出数量。
- 适配器具有与其相关联的输出(例如,监视器)。 您可以使用IDXGIAdapter :: EnumOutputs方法枚举特定适配器的输出。 使用此方法确定默认适配器的输出数量。
5.每个输出都有一个给定像素格式支持的显示模式列表(DXGI_MODE_DESC)。 对于每个输出(IDXGIOutput),显示输出支持的每个显示模式的宽度,高度和刷新率DXGI_FORMAT_R8G8B8A8_UNORM格式使用IDXGIOutput :: GetDisplayModeList方法。练习2,3,4和5的输出示例如下。 使用OutputDebugString函数快速输出到VC ++输出窗口是非常有用的。
*** NUM ADAPTERS = 1
*** D3D11 SUPPORTED FOR ADAPTER 0
*** NUM OUTPUTS FOR DEFAULT ADAPTER = 1
***WIDTH = 640 HEIGHT = 480 REFRESH = 60000/1000
***WIDTH = 640 HEIGHT = 480 REFRESH = 72000/1000
***WIDTH = 640 HEIGHT = 480 REFRESH = 75000/1000
***WIDTH = 720 HEIGHT = 480 REFRESH = 56250/1000
***WIDTH = 720 HEIGHT = 480 REFRESH = 56250/1000
***WIDTH = 720 HEIGHT = 480 REFRESH = 60000/1000
***WIDTH = 720 HEIGHT = 480 REFRESH = 60000/1000
***WIDTH = 720 HEIGHT = 480 REFRESH = 72188/1000
***WIDTH = 720 HEIGHT = 480 REFRESH = 72188/1000
***WIDTH = 720 HEIGHT = 480 REFRESH = 75000/1000
***WIDTH = 720 HEIGHT = 480 REFRESH = 75000/1000
***WIDTH = 720 HEIGHT = 576 REFRESH = 56250/1000
***WIDTH = 720 HEIGHT = 576 REFRESH = 56250/1000
***WIDTH = 720 HEIGHT = 576 REFRESH = 60000/1000
***WIDTH = 720 HEIGHT = 576 REFRESH = 60000/1000
***WIDTH = 720 HEIGHT = 576 REFRESH = 72188/1000
***WIDTH = 720 HEIGHT = 576 REFRESH = 72188/1000
***WIDTH = 720 HEIGHT = 576 REFRESH = 75000/1000
***WIDTH = 720 HEIGHT = 576 REFRESH = 75000/1000
***WIDTH = 800 HEIGHT = 600 REFRESH = 56250/1000
***WIDTH = 800 HEIGHT = 600 REFRESH = 60000/1000
***WIDTH = 800 HEIGHT = 600 REFRESH = 60000/1000
***WIDTH = 800 HEIGHT = 600 REFRESH = 72188/1000
***WIDTH = 800 HEIGHT = 600 REFRESH = 75000/1000
***WIDTH = 848 HEIGHT = 480 REFRESH = 60000/1000
***WIDTH = 848 HEIGHT = 480 REFRESH = 60000/1000
***WIDTH = 848 HEIGHT = 480 REFRESH = 70069/1000
***WIDTH = 848 HEIGHT = 480 REFRESH = 70069/1000
***WIDTH = 848 HEIGHT = 480 REFRESH = 75029/1000
***WIDTH = 848 HEIGHT = 480 REFRESH = 75029/1000
***WIDTH = 960 HEIGHT = 600 REFRESH = 60000/1000
***WIDTH = 960 HEIGHT = 600 REFRESH = 60000/1000
***WIDTH = 960 HEIGHT = 600 REFRESH = 70069/1000
***WIDTH = 960 HEIGHT = 600 REFRESH = 70069/1000
***WIDTH = 960 HEIGHT = 600 REFRESH = 75029/1000
***WIDTH = 960 HEIGHT = 600 REFRESH = 75029/1000
***WIDTH = 1024 HEIGHT = 768 REFRESH = 60000/1000
***WIDTH = 1024 HEIGHT = 768 REFRESH = 60000/1000
***WIDTH = 1024 HEIGHT = 768 REFRESH = 70069/1000
***WIDTH = 1024 HEIGHT = 768 REFRESH = 75029/1000
***WIDTH = 1152 HEIGHT = 864 REFRESH = 60000/1000
***WIDTH = 1152 HEIGHT = 864 REFRESH = 60000/1000
***WIDTH = 1152 HEIGHT = 864 REFRESH = 75000/1000
***WIDTH = 1280 HEIGHT = 720 REFRESH = 60000/1000
***WIDTH = 1280 HEIGHT = 720 REFRESH = 60000/1000
***WIDTH = 1280 HEIGHT = 720 REFRESH = 60000/1001
***WIDTH = 1280 HEIGHT = 768 REFRESH = 60000/1000
***WIDTH = 1280 HEIGHT = 768 REFRESH = 60000/1000
***WIDTH = 1280 HEIGHT = 800 REFRESH = 60000/1000
***WIDTH = 1280 HEIGHT = 800 REFRESH = 60000/1000
***WIDTH = 1280 HEIGHT = 960 REFRESH = 60000/1000
***WIDTH = 1280 HEIGHT = 960 REFRESH = 60000/1000
***WIDTH = 1280 HEIGHT = 1024 REFRESH = 60000/1000
***WIDTH = 1280 HEIGHT = 1024 REFRESH = 60000/1000
***WIDTH = 1280 HEIGHT = 1024 REFRESH = 75025/1000
***WIDTH = 1360 HEIGHT = 768 REFRESH = 60000/1000
***WIDTH = 1360 HEIGHT = 768 REFRESH = 60000/1000
***WIDTH = 1600 HEIGHT = 1200 REFRESH = 60000/1000
6.尝试修改视口设置以将场景绘制为后缓冲区的子矩形。 例如,尝试:
D3D11_VIEWPORT vp;
vp.TopLeftX = 100.0f;
vp.TopLeftY = 100.0f;
vp.Width = 500.0f;
vp.Height = 400.0f;
vp.MinDepth = 0.0f;
vp.MaxDepth = 1.0f;
上一篇: C盘文件清理
下一篇: 清理 Xcode 相关文件 - iOS