dx11 龙书学习 第四章 dx11 准备工作

4.1 准备工作

Direct3D的初始化过程要求我们熟悉一些基本的Direct3D类型和基本绘图概念;本章第一节会向读者介绍些必要的基础知识。然后我们会详细讲解Direct3D初始化过程中的每一个必要步骤,并顺便介绍一下实时绘图应用程序必须使用的精确计时和时间测量。最后,我们将讨论一下示例框架代码,它是本书所有演示程序使用的统一编程接口。

学习目标:

  1. 对Direct3D在规划调度3D硬件方面所起的作用有一个基本了解。
  2. 理解COM在Direct3D运行时起到的作用。
  3. 学习基本的绘图概念,比如2D图像的存储方式、页面翻转、深度缓存和多重采样。
  4. 学习如何使用性能计数器函数来获取高精度的计时器读数。
  5. 阐述Direct3D的初始化过程。
  6. 熟悉本书所有演示程序采用的通用应用程序框架结构。

Direct3D的初始化过程要求我们熟悉一些基本的绘图概念和Direct3D类型。我们会在本节讲解这些概念和类型,以使读者可以顺利地阅读之后的章节。

4.1.1 Direct3D概述

Direct3D是一种底层绘图API(application programming interface,应用程序接口),它可以让我们可以通过3D硬件加速绘制3D世界。从本质上讲,Direct3D提供的是一组软件接口,我们可以通过这组接口来控制绘图硬件。例如,要命令绘图设备清空渲染目标(例如屏幕),我们可以调用Direct3D的ID3D11DeviceContext::ClearRenderTargetView方法来完成这一工作。Direct3D层位于应用程序和绘图硬件之间,这样我们就不必担心3D硬件的实现细节,只要设备支持Direct3D 11,我们就可以通过Direct3D 11 API来控制3D硬件了。

支持Direct3D 11的设备必须支持Direct3D 11规定的整个功能集合以及少数的额外附加功能(有一些功能,比如多重采样数量,仍然需要以查询方式实现,这是因为不同的Direct3D硬件这个值可能并不一样)。在Direct3D 9中,设备可以只支持Direct3D 9的部分功能;所以,当一个Direct3D 9应用程序要使用某一特性时,应用程序就必须先检查硬件是否支持该特性。如果要调用的是一个不为硬件支持Direct3D函数,那应用程序就会出错。而在Direct3D 11中,不需要再做这种设备功能检查,因为Direct3D 11强制要求设备实现Direct3D 11规定的所有功能特性。

4.1.2 COM

组件对象模型(COM)技术使DirectX独立于任何编程语言,并具有版本向后兼容的特性。我们经常把COM对象称为接口,并把它当成一个普通的C++类来使用。当使用C++编写DirectX程序时,许多COM的底层细节都不必考虑。唯一需要知道的一件事情是,我们必须通过特定的函数或其他的COM接口方法来获取指向COM接口的指针,而不能用C++的new关键字来创建COM接口。另外,当我们不再使用某个接口时,必须调用它的Release方法来释放它(所有的COM接口都继承于IUnknown接口,而Release方法是IUnknown接口的成员),而不能用delete语句——COM对象在其自身内部实现所有的内存管理工作。

当然,有关COM的细节还有很多,但是在实际工作中只需知道上述内容就足以有效地使用DirectX了。

注意:COM接口都以大写字母“I”为前缀。例如,表示2D纹理的接口为ID3D11Texture2D

4.1.3 纹理和数据资源格式

2D纹理(texture)是一种数据元素矩阵。2D纹理的用途之一是存储2D图像数据,在纹理的每个元素中存储一个像素颜色。但这不是纹理的唯一用途;例如, 有一种称为法线贴图映射(normal mapping)的高级技术在纹理元素中存储的不是颜色,而是3D向量。因此,从通常意义上讲,纹理用来存储图像数据,但是在实际应用中纹理可以有更广泛的用途。1D纹理类似于一个1D数据元素数组,3D纹理类似于一个3D数据元素数组。但是在随后的章节中我们会讲到,纹理不仅仅是一个数据数组;纹理可以带有多级渐近纹理层(mipmap level),GPU可以在纹理上执行特殊运算,比如使用过滤器(filter)和多重采样(multisampling)。此外,不是任何类型的数据都能存储到纹理中的;纹理只支持特定格式的数据存储,这些格式由DXGI_FORMAT枚举类型描述。一些常用的格式如下:

  1. DXGI_FORMAT_R32G32B32_FLOAT:每个元素包含3个32位浮点分量。
  2. DXGI_FORMAT_R16G16B16A16_UNORM:每个元素包含4个16位分量,分量的取值范围在[0,1]区间内。
  3. DXGI_FORMAT_R32G32_UINT:每个元素包含两个32位无符号整数分量。
  4. DXGI_FORMAT_R8G8B8A8_UNORM:每个元素包含4个8位无符号分量,分量的取值范围在[0,1]区间内。
  5. DXGI_FORMAT_R8G8B8A8_SNORM:每个元素包含4个8位有符号分量,分量的取值范围在[−1,1] 区间内。
  6. DXGI_FORMAT_R8G8B8A8_SINT:每个元素包含4个8位有符号整数分量,分量的取值范围在[−128, 127] 区间内。
  7. DXGI_FORMAT_R8G8B8A8_UINT:每个元素包含4个8位无符号整数分量,分量的取值范围在[0, 255]区间内。

注意,字母R、G、B、A分别表示red(红)、green(绿)、blue(蓝)和alpha(透明度)。每种颜色都是由红、绿、蓝三种基本颜色组成的(例如,黄色是由红色和绿色组成的)。alpha通道(或alpha分量)用于控制透明度。不过,正如我们之前所述,纹理存储的不一定是颜色信息;例如,格式DXGI_FORMAT_R32G32B32_FLOAT包含3个浮点分量,可以存储一个使用浮点坐标的3D向量。另外,还有一种弱类型(typeless格式,可以预先分配内存空间,然后在纹理绑定到管线时再指定如何重新解释数据内容(这一过程与C++中的数据类型转换颇为相似);例如,下面的弱类型格式为每个元素预留4个8位分量,且不指定数据类型(例如:整数、浮点数、无符号整数):

DXGI_FORMAT_R8G8B8A8_TYPELESS

4.1.4 交换链和页面翻转

为了避免在动画中出现闪烁,最好的做法是在一个离屏(off-screen)纹理中执行所有的动画帧绘制工作,这个离屏纹理称为后台缓冲区(back buffer。当我们在后台缓冲区中完成给定帧的绘制工作后,便可以将后台缓冲区作为一个完整的帧显示在屏幕上;使用这种方法,用户不会察觉到帧的绘制过程,只会看到完整的帧。从理论上讲,将一帧显示到屏幕上所消耗的时间小于屏幕的垂直刷新时间。硬件会自动维护两个内置的纹理缓冲区来实现这一功能,这两个缓冲区分别称为前台缓冲区(front buffer和后台缓冲区。前台缓冲区存储了当前显示在屏幕上的图像数据,而动画的下一帧会在后台缓冲区中执行绘制。当后台缓冲区的绘图工作完成之后,前后两个缓冲区的作用会发生翻转:后台缓冲区会变为前台缓冲区, 而前台缓冲区会变为后台缓冲区,为下一帧的绘制工作提前做准备。我们将前后缓冲区功能互换的行为称做呈现(presenting。提交是一个运行速度很快的操作,因为它只是将前台缓冲区的指针和后台缓冲区的指针做了一个简单的交换。图4.1说明了这一过程。

图4.1:我们首先渲染缓冲区B,它是当前的后台缓冲区。一旦帧渲染完成,前后缓冲区的指针会相互交换,缓冲区B会变为前台缓冲区,而缓冲区A会变为新的后台缓冲区。之后,我们将在缓冲区A中进行下一帧的渲染。一旦帧渲染完成,前后缓冲区的指针会再次进行交换,缓冲区A会变为前台缓冲区,而缓冲区B会再次变为后台缓冲区。

前后缓冲区形成了一个交换链(swap chain。在Direct3D中,交换链由IDXGISwapChain接口表示。该接口保存了前后缓冲区纹理,并提供了用于调整缓冲区尺寸的方法(IDXGISwapChain::ResizeBuffers)和呈现方法(IDXGISwapChain::Present)。我们会在4.4节中详细讨论些方法。

使用(前后)两个缓冲区称为双缓冲(double buffering。缓冲区的数量可多于两个;比如,当使用三个缓冲区时称为三缓冲(triple buffering)。不过,两个缓冲区已经足够用了。

注意:虽然后台缓冲区是一个纹理(纹理元素称为texel),但是我们更习惯于将纹理元素称为像素(pixel),因为后台缓冲区存储的是颜色信息。有时,即使纹理中存储的不是颜色信息,人们还是会将纹理元素称为像素(例如,“法线贴图像素”)。

4.1.5 深度缓冲区

深度缓冲区(depth buffer是一个不包含图像数据的纹理对象。在一定程度上,深度信息可以被认为是一种特殊的像素。常见的深度值范围在0.0到1.0之间,其中0.0表示离观察者最近的物体,1.0表示离观察者最远的物体。深度缓冲区中的每个元素与后台缓冲区中的每个像素一一对应(即,后台缓冲区的第ij个元素对应于深度缓冲区的第ij个元素)。所以,当后台缓冲区的分辨率为1280×1024时,在深度缓冲区中有1280×1024个深度元素。

图4.2 彼此遮挡的一组物体

图4.2是一个简单的场景,其中一些物体挡住了它后面的一些物体的一部分区域。为了判定物体的哪些像素位于其他物体之前,Direct3D使用了一种称为深度缓存(depth

bufferingz缓存(z-buffering的技术。我们所要强调的是在使用深度缓存时,我们不必关心所绘物体的先后顺序。

注意:要处理深度的问题,有人可能会建议按照从远至近的顺序绘制场景中的物体。使用这种方法,离得近的物体会覆盖在离得远的物体之上,这样就会产生正确的绘制结果,这也是画家作画时用到的方法。但是,这种方法会导致另一个问题——如何将大量的物体和相交的几何体按从远到近的方式进行排序?此外,图形硬件本身就提供了深度缓存供我们使用,因此我们不会采用画家算法。

为了说明深度缓存的工作方式,让我们来看一个例子。如图4.3所示,它展示的是观察者看到的立体空间(左图)以及该立体空间的2D侧视图(右图)。从这个图中我们可以发现,3个不同的像素会被渲染到视图窗口的同一个像素点P上。(当然,我们知道只有最近的像素会被渲染到P上,因为它挡住了后面的其他像素,可是计算机不知道这些事情。)首先,在渲染之前,我们必须把后台缓冲区清空为一个默认颜色(比如黑色或白色),把深度缓冲区清空为默认值——通常设为1.0(像素所具有的最远深度值)。

图4.3:视图窗口相当于从3D场景生成的2D图像(后台缓冲区)。我们看到,有3个不同的像素可以被投影到像素P上。直觉告诉我们,P1是P的最终颜色,因为它离观察者最近,而且遮挡了其他两个像素。深度缓冲区算法提供了一种可以在计算机上实现的判定过程。注意,我们所说的深度值是相对于观察坐标系而言的。实际上,当深度值存入深度缓冲区时,它会被规范到[0.0,1.0]区间内。

现在,假设物体的渲染顺序依次为:圆柱体、球体和圆锥体。下面的表格汇总了在绘制些物体时像素P及相关深度值的变化过程;其他像素的处理过程与之类似。

表 4.1

操作

P

d

说明

清空

黑色

1.0

初始化像素以及相应的深度元素。

绘制圆柱体

P3

d3

因为d3≤d=1.0,所以深度测试通过,更新缓冲区,设置P=P3、d=d3。

绘制球体

P1

d1

因为d1≤d=d3,所以深度测试通过,更新缓冲区,设置P=P1、d=d1。

绘制圆锥体

P1

d1

因为d2>d=d1,所以深度测试未通过,不更新缓冲区。

从上表可以看到,当我们发现某个像素具有更小的深度值时,就更新该像素以及它在深度缓冲区中的相应深度值。通过一方式,在最终得到的渲染结果中只会包含那些离观察者最近的像素。(如果读者对此仍有疑虑,那么可以试着交换本例的绘图顺序,看看得到的计算结果是否相同。)

综上所述,深度缓冲区用于为每个像素计算深度值和实现深度测试。深度测试通过比较像素深度来决定是否将该像素写入后台缓冲区的特定像素位置。只有离观察者最近的像素才会胜出,成为写入后台缓冲区的最终像素。这很容易理解,因为离观察者最近的像素会遮挡它后面的其他像素。

深度缓冲区是一个纹理,所以在创建它时必须指定一种数据格式。用于深度缓存的格式如下:

  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:无符号16位深度缓冲区,每个深度值的取值范围为[0,1]。

注意:模板缓冲区对应用程序来说不是必须的,但是如果用到了模板缓冲区,那么模板缓冲区必定是与深度缓冲区存储在一起的。例如,32位格式DXGI_FORMAT_D24_UNORM_S8_UINT使用24位用于深度缓冲区,8位用于模板缓冲区。 所以,将深度缓冲区称为“深度/模板缓冲区”更为合适。模板缓冲区是一个比较高级的主题,我们会在第10章讲解模板缓冲区的用法。

4.1.6 纹理资源视图

纹理可以被绑定到渲染管线(rendering pipeline的不同阶段(stage;例如,比较常见的情况是将纹理作为渲染目标(即,Direct3D渲染到纹理)或着色器资源(即,在着色器中对纹理进行采样)。当创建用于这两种目的的纹理资源时,应使用绑定标志值:

D3D11_BIND_RENDER_TARGET | D3D10_BIND_SHADER_RESOURCE

指定纹理所要绑定的两个管线阶段。其实,资源不能被直接绑定到一个管线阶段;我们只能把与资源关联的资源视图绑定到不同的管线阶段。无论以哪种方式使用纹理,Direct3D始终要求我们在初始化时为纹理创建相关的资源视图(resource view。这样有助于提高运行效率,正如SDK文档指出的那样:“运行时环境与驱动程序可以在视图创建执行相应的验证和映射,减少绑定时的类型检查”。所以,当把纹理作为一个渲染目标和着色器资源时,我们要为它创建两种视图:渲染目标视图(ID3D11RenderTargetView)和着色器资源视图(ID3D11ShaderResourceView)。资源视图主要有两个功能:(1)告诉Direct3D如何使用资源(即,指定资源所要绑定的管线阶段);(2)如果在创建资源时指定的是弱类型(typeless)格式,那么在为它创建资源视图时就必须指定明确的资源类型。对于弱类型格式,纹理元素可能会在一个管线阶段中视为浮点数,而在另一个管线阶段中视为整数。

为了给资源创建一个特定视图,我们必须在创建资源时使用特定的绑定标志值。例如,如果在创建资源没有使用D3D11_BIND_DEPTH_STENCIL绑定标志值(该标志值表示纹理将作为一个深度/模板缓冲区绑定到管线上),那我们就无法为该资源创建ID3D11DepthStencilView视图。只要你试一下就会发现Direct3D会给出如下调试错误:

ERROR: ID3D11Device::CreateDepthStencilView: A DepthStencilView cannot be created of a Resource that did not specify D3D10_BIND_DEPTH_STENCIL.

我们会在本章的4.2节中看到用来创建渲染目标视图和深度/模板视图的代码。在第8章中看到用于创建着色器资源视图的代码。本书随后的许多例子都有会把纹理用作渲染目标和着色器资源。

注意:2009年8月的SDK文档指出:“当创建资源时,为资源指定强类型(fully-typed)格式,把资源的用途限制在格式规定的范围内,有利于提高运行时环境对资源的访问速度……”。所以,你只应该在真正需要弱类型资源时(使用弱类型的优点是可以使用不同的视图将数据用于不同的用途),才创建弱类型资源;否则,应尽量创建强类型资源。

4.1.7 多重采样

因为计算机显示器上的像素分辨率有限,所以当我们绘制一条任意直线时,该直线很难精确地显示在屏幕上。图4.4中的第一条直线说明了“阶梯”(aliasing,锯齿)效应,当使用像素矩阵近似地表示一条直线时就会出现这种现象,类似的锯齿也会发生在三角形的边缘上。

图4.4:我们可以看到,第一条直线带有明显的锯齿(当使用像素矩阵近似地表示一条直线时就会出现阶梯效应)。而第二条直线使用了抗锯齿技术,通过对一个像素周围的邻接像素进行采样得到该像素的最终颜色;这样可以形成一条较为平滑的直线,使阶梯效果得到缓解。

通过提高显示器的分辨率,缩小像素的尺寸,也可以有效地缓解一问题,使阶梯效应明显降低。

当无法提高显示器分辨率或分辨率不够高时,我们可以使用抗锯齿(antialiasing技术。其中的一种技术叫做超级采样(supersampling,它把后台缓冲和深度缓冲的大小提高到屏幕分辨率的4倍。3D场景会以这个更大的分辨率渲染到后台缓存中,当在屏幕上呈现后台缓冲时,后台缓冲会将4个像素的颜色取平均值后得到一个像素的最终颜色。从效果上来说,超级采样的工作原理就是以软件的方式提升分辨率。

超级采样代价昂贵,因为它处理的像素数量和所需的内存数量增加为原来的4倍。Direct3D支持另一种称为多重采样(multisampling的抗锯齿技术,它通过对一个像素的子像素进行采样计算出该像素的最终颜色,比超级采样节省资源。假如我们使用的是4X多重采样(每个像素采样4个邻接像素),多重采样仍然会使用屏幕分辨率4倍大小的后台缓冲和深度缓冲,但是,不像超级采样那样计算每个子像素的颜色,而是只计算像素中心颜色一次,然后基于子像素的可见性(基于子像素的深度/模板测试)和范围(子像素中心在多边形之外还是之内)共享颜色信息。图4.5展示了这样的一个例子。

图4.5:如图(a)所示,一个像素与多边形的边缘相交,像素中心的绿颜色存储在可见的三个子像素中,而第4个子像素没有被多边形覆盖,因此不会被更新为绿色,它仍保持为原来绘制的几何体颜色或Clear操作后的颜色。如图(b)所示,要获得最后的像素颜色,我们需要对4个子像素(3个绿色和一个白色)取平均值,获得淡绿色,通过这个操作,可以减弱多边形边缘的阶梯效果,实现更平滑的图像。

注意:supersampling与multisampling的关键区别在于:使用supersampling时,图像的颜色需要通过每个子像素的颜色计算得来,而每个子像素颜色可能不同;使用multisampling(图4.5)时,每个像素的颜色只计算一次,这个颜色会填充到所有可见的、被多边形覆盖的子像素中,即这个颜色是共享的。因为计算图像的颜色是图形管线中最昂贵的操作之一,因此multisampling相比supersampling而言节省的资源是相当可观的。但是,supersampling更为精确,这是multisampling做不到的。

注意:在图4.5中,我们用标准的网格图形表示一个像素的4个子像素,但由于硬件的不同,实际的子像素放置图形也是不同的,Direct3D并不定义子像素的放置方式,在特定情况下,某些放置方式会优于其他的放置方式。

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);

如果纹理格式和采样数量的组合不被设备支持,则该方法返回0。反之,通过pNumQualityLevels参数返回符合给定的质量等级数值。有效的质量级别范围为0到pNumQualityLevels−1。

采样的最大数量可以由以下语句定义:

#define D3D11_MAX_MULTISAMPLE_SAMPLE_COUNT(32)

采样数量通常使用4或8,可以兼顾性能和内存消耗。如果你不使用多重采样,可以将采样数量设为1,将质量级别设为0。所有符合Direct3D 11功能特性的设备都支持用于所有渲染目标格式的4X多重采样。

注意:我们需要为交换链缓冲区和深度缓冲区各填充一个DXGI_SAMPLE_DESC结构体。当创建后台缓冲区和深度缓冲区时,必须使用相同的多重采样设置;具体的代码会在下一节给出。

4.1.9  特征等级

Direct3D 11提出了特征等级(feature levels,在代码中由枚举类型D3D_FEATURE_LEVEL表示)的概念,对应了定义了d3d11中定义了如下几个等级以代表不同的d3d版本:

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;

特征等级定义了一系列支持不同d3d功能的相应的等级(每个特征等级支持的功能可参见SDK文档),用意即如果一个用户的硬件不支持某一特征等级,程序可以选择较低的等级。例如,为了支持更多的用户,应用程序可能需要支持Direct3D 11,10.1,9.3硬件。程序会从最新的硬件一直检查到最旧的,即首先检查是否支持Direct3D 11,第二检查Direct3D 10.1,然后是Direct3D 10,最后是Direct3D 9。要设置测试的顺序,可以使用下面的特征等级数组(数组内元素的顺序即特征等级测试的顺序):

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

4.2 对Direct3D进行初始化

下面的各小节将讲解如何初始化Direct3D。我们将Direct3D的初始化过程分为如下几个步骤:

  1. 使用D3D11CreateDevice方法创建ID3D11DeviceID3D11DeviceContext
  2. 使用ID3D11Device::CheckMultisampleQualityLevels方法检测设备支持的4X多重采样质量等级。
  3. 填充一个IDXGI_SWAP_CHAIN_DESC结构体,该结构体描述了所要创建的交换链的特性。
  4. 查询IDXGIFactory实例,这个实例用于创建设备和一个IDXGISwapChain实例。
  5. 为交换链的后台缓冲区创建一个渲染目标视图。
  6. 创建深度/模板缓冲区以及相关的深度/模板视图。
  7. 将渲染目标视图和深度/模板视图绑定到渲染管线的输出合并阶段,使它们可以被Direct3D使用。
  8. 设置视口。

4.2.1 创建设备(Device)和上下文(Context)

要初始化Direct3D,首先需要创建Direct3D 11设备(ID3D11Device)和上下文(ID3D11DeviceContext)。它们是是最重要的Direct3D接口,可以被看成是物理图形设备硬件的软控制器;也就是说,我们可以通过该接口与硬件进行交互,命令硬件完成一些工作(比如:在显存中分配资源、清空后台缓冲区、将资源绑定到各种管线阶段、绘制几何体)。具体而言:

  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_LEVE L  *pFeatureLevel,

ID3D11DeviceContext  **ppImmediateContext

);

1.pAdapter:指定要为哪个物理显卡创建设备对象。当该参数设为空值时,表示使用主显卡。在本书的示例程序中,我们只使用主显卡。

2.DriverType:一般来讲,该参数总是指定为D3D_DRIVER_TYPE_HARDWARE,表示使用3D硬件来加快渲染速度。但是,也可以有两个其他选择:

D3D_DRIVER_TYPE_REFERENCE:创建所谓的引用设备(reference device)。引用设备是Direct3D的软件实现,它具有Direct3D的所有功能(只是运行速度非常慢,因为所有的功能都是用软件来实现的)。引用设备随DirectX SDK一起安装,只用于程序员,而不应该用于程序发布。使用引用设备有两个原因:

  1. 测试硬件不支持的代码;例如,在一块不支持Direct3D 11的显卡上测试一段Direct3D 11的代码。
  2. 测试驱动程序缺陷。当代码能在引用设备上正常运行,而不能在硬件上正常工作时,说明硬件的驱动程序可能存在缺陷。

D3D_DRIVER_TYPE_SOFTWARE:创建一个用于模拟3D硬件的软件驱动器。要使用软件驱动器,你必须自己创建一个,或使用第三方的软件驱动器。与下面要说的WARP驱动器不同,Direct3D不提供软件驱动器。

D3D_DRIVER_TYPE_WARP:创建一个高性能的Direct3D 10.1软件驱动器。WARP代表Windows Advanced Rasterizati on Platform。因为WARP不支持Direct3D 11,因此我们对它不感兴趣。

3.Software:用于支持软件光栅化设备(software rasterizer)。我们总是将该参数设为空值,因为我们使用硬件进行渲染。如果读者想要使用这一功能,那么就必须先安装一个软件光栅化设备。

4.Flags:可选的设备创建标志值。当以release模式生成程序时,该参数通常设为0(无附加标志值);当以debug模式生成程序时,该参数应设为:

D3D11_CREATE_DEVICE_DEBUG:用以激活调试层。当指定调试标志值后,Direct3D会向VC++的输出窗口发送调试信息;图4.6展示了输出错误信息的一个例子。

图4.6  Direct3D 11调试输出的一个例子。

5.pFeatureLevelsD3D_FEATURE_LEVEL数组,元素的顺序表示要特征等级(见§4.1.9)的测试顺序。将这个参数设置为null表示选择可支持的最高等级。

6.FeatureLevelspFeatureLevels数组中的元素D3D_FEATURE_LEVELs的数量,若pFeatureLevels设置为null,则这个值为0。

7.SDKVersion:始终设为D3D11_SDK_VERSION

8.ppDevice:返回创建后的设备对象。

9.pFeatureLevel:返回pFeatureLevels数组中第一个支持的特征等级(如果pFeatureLevels 为null,则返回可支持的最高等级)。

10.ppImmediateContext:返回创建后的设备上下文。

下面是调用该函数的一个示例:

UINT createDeviceFlags = 0;



#if  defined(DEBUG)||defined(_DEBUG)

createDeviceFlags  |= D3D11_CREATE_DEVICE_DEBUG;

#endif



D3D_FEATURE_LEVEL featureLevel;

ID3D11Device *  md3dDevice;

ID3D11Device Context*  md3dImmediate Context;

HRESULT  hr = D3D11CreateDevice(

0,                     //  默认显示适配器

D3D_DRIVER_TYPE_HARDWARE ,

0,                     //  不使用软件设备

createDeviceFlags ,

0, 0,               //  默认的特征等级数组

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 FeatureLevel 11 unsupported.", 0, 0);

return  false;

}

从上面的代码可以看到我们使用的是立即执行上下文(immediate context):

ID3D11DeviceContext* md3dImmediateContext;

还有一种上下文叫做延迟执行上下文(ID3D11Device::CreateDeferredContext)。该上下文主要用于Direct3D 11支持的多线程程序。多线程编程是一个高级话题,本书并不会介绍,但下面介绍一点基本概念:

1.在主线程中使用立即执行上下文。

2.在工作线程总使用延迟执行上下文。

(a)每个工作线程可以将图形指令记录在一个命令列表(ID3D11CommandList)中。

(b)随后,每个工作线程中的命令列表可以在主渲染线程中加以执行。

在多核系统中,可并行处理命令列表中的指令,这样可以缩短编译复杂图形所需的时间。

4.2.2 检测4X多重采样质量支持

创建了设备后,我们就可以检查4X多重采样质量等级了。所有支持Direct3D 11的设备都支持所有渲染目标格式的4X MSAA(支持的质量等级可能并不相同)。

UINT  m4xMsaaQuality;

HR(md3dDevice ->CheckMultisampleQualityLevels(

DXGI_FORMAT_R8G8B8A8_UNORM, 4, &  m4xMsaaQuality));

assert(m4xMsaaQuality > 0);

因为4X MSAA总是被支持的,所以返回的质量等级总是大于0。

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;                      // 后台缓冲区宽度

UINT Height;                    // 后台缓冲区高度

DXGI_RATIONAL RefreshRate;      // 显示刷新率

DXGI_FORMAT Format;              // 后台缓冲区像素格式

DXGI_MODE_SCANLINE_ORDER ScanlineOrdering;// display scanline mode

DXGI_MODE_SCALING Scaling;      // display scaling mode

} DXGI_MODE_DESC;

注意:在下面的数据成员描述中,我们只涵盖了一些常用的标志值和选项,它们对于初学者来说非常重要。对于其他标志值和选项的描述,请参阅SDK文档。

  1. BufferDesc:该结构体描述了我们所要创建的后台缓冲区的属性。我们主要关注的属性有:宽度、高度和像素格式;其他属性的详情请参阅SDK文档。
  2. SampleDesc:多重采样数量和质量级别(参阅4.1.8节)。
  3. BufferUsage:设为DXGI_USAGE_RENDER_TARGET_OUTPUT,因为我们要将场景渲染到后台缓冲区(即,将它用作渲染目标)。
  4. BufferCount:交换链中的后台缓冲区数量;我们一般只用一个后台缓冲区来实现双缓存。当然,你也可以使用两个后台缓冲区来实现三缓存。
  5. OutputWindow:我们将要渲染到的窗口的句柄。
  6. Windowed:当设为true时,程序以窗口模式运行;当设为false时,程序以全屏(full-screen)模式运行。
  7. SwapEffect:设为DXGI_SWAP_EFFECT_DISCARD,让显卡驱动程序选择最高效的显示模式。
  8. Flags:可选的标志值。如果设为DXGI_SWAP_CHAIN_FLAG_ALLOW_MODE_SWITCH,那么当应用程序切换到全屏模式时,Direct3D会自动选择与当前的后台缓冲区设置最匹配的显示模式。如果未指定该标志值,那么当应用程序切换到全屏模式时,Direct3D会使用当前的桌面显示模式。我们在示例框架中没有使用该标志值,因为对于我们的演示程序来说,在全屏模式下使用当前的桌面显示模式可以得到很好的效果。

下面是在我们的示例框架中填充DXGI_SWAP_CHAIN_DESC结构体的代码:

DXGI_SWAP_CHAIN_DESC sd;

sd.BufferDesc.Width    = mClientWidth;    // 使用窗口客户区宽度

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;

// 是否使用4X MSAA?

if(mEnable4xMsaa)

{

sd.SampleDesc.Count = 4;

// m4xMsaaQuality是通过CheckMultisampleQualityLevels()方法获得的

sd.SampleDesc.Quality = m4xMsaaQuality-1;

}

// NoMSAA

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;

注意:如果你想在运行时改变多重采样的设置,那么必须销毁然后重新创建交换链。

注意:因为大多数显示器不支持超过24位以上的颜色,再多的颜色也是浪费,所以我们将后台缓冲区的像素格式设置为DXGI_FORMAT_R8G8B8A8_UNORM(红、绿、蓝、alpha各8位)。额外的8位alpha并不会输出在显示器上,但在后台缓冲区中可以用于特定的用途。

4.2.4 创建交换链

交换链(IDXGISwapChain)是通过IDXGIFactory实例的IDXGIFactory::CreateSwapChain方法创建的:

HRESULT  IDXGIFactory::CreateSwapChain(

IUnknown *pDevice , // 指向ID3D11Device的指针

DXGI_SWAP_CHAIN_DESC *pDesc, // 指向一个交换链描述的指针

IDXGISwapChain **ppSwapChain); // 返回创建后的交换链

我们可以通过CreateDXGIFactory(需要链接dxgi.lib)获取指向一个IDXGIFactory实例的指针。但是使用这种方法获取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**))&dxgiAdapte r ));

// 获得IDXGIFactory 接口

IDXGIFactory*  dxgiFactory  =  0;

HR(dxgiAdapter->GetParent(__uuid of(IDXGIFactory),

(void**))&dxgiFactor y));

// 现在,创建交换链

IDXGISwapChain*  mSwapChain;

HR(dxgiFactory->CreateSwapChain(md3dDevice, &sd , &mSw ap Chain);

// 释放COM接口

ReleaseCOM (dxgiDevice ;

ReleaseCOM (dxgiAdapter);

ReleaseCOM (dxgiFactory);

DXGI(DirectX Graphics Inf rastructure)是独立于Direct3D的API,用于处理与图形关联的东西,例如交换链等。DXGI与Direct3D分离的目的在于其他图形API(例如Direct2D)也需要交换链、图形硬件枚举、在窗口和全屏模式之间切换,通过这种设计,多个图形API都能使用DXGI API。

补充:你也可以使用D3D11CreateDeviceAndSwapChain方法同时创建设备、设备上下文和交换链,详情请见Direct3D 11 教程1:Direct3D 11基础

4.2.5 创建渲染目标视图

如4.1.6节所述,资源不能被直接绑定到一个管线阶段;我们必须为资源创建资源视图,然后把资源视图绑定到不同的管线阶段。尤其是在把后台缓冲区绑定到管线的输出合并器阶段时(使Direct3D可以在后台缓冲区上执行渲染工作),我们必须为后台缓冲区创建一个渲染目标视图(render target view。下面的代码说明了一实现过程:

ID3D11RenderTargetView* mRenderTargetView;

ID3D11Texture2D* backBuffer;

mSwapChain->GetBuffer(0,__uuidof(ID3D11Texture2D),

reinterpret_cast<void**>(&backBuffer));

md3dDevice->CreateRenderTargetView(backBuffer, 0, &mRenderTargetView);

ReleaseCOM(backBuffer);

  1. IDXGISwapChain::GetBuffer方法用于获取一个交换链的后台缓冲区指针。该方法的第一个参数表示所要获取的后台缓冲区的索引值(由于后台缓冲区的数量可以大于1,所以这里必须指定索引值)。在我们的演示程序中,我们只使用一个后台缓冲区,所以该索引值设为0。第二个参数是缓冲区的接口类型,它通常是一个2D纹理(ID3D11Texture2D)。第三个参数返回指向后台缓冲区的指针。
  2. 我们使用ID3D11Device::CreateRenderTargetView方法创建渲染目标视图。第一个参数指定了将要作为渲染目标的资源,在上面的例子中,渲染目标是后台缓冲区(即,我们为后台缓冲区创建了一个渲染目标视图)。第二个参数是一个指向D3D11_RENDER_TARGET_VIEW_DESC结构体的指针,该结构体描述了资源中的元素的数据类型。如果在创建资源时使用的是某种强类型格式(即,非弱类型格式),则该参数可以为空,表示以资源的第一个mipmap层次(后台缓冲区也只有一个mipmap层次)作为视图格式。第三个参数通过指针返回了创建后的渲染目标视图对象。
  3. 每调用一次IDXGISwapChain::GetBuffer方法,后台缓冲区的COM引用计数就会向上递增一次,这便是我们在代码片段的结尾处释放它(ReleaseCOM)的原因。

4.2.6 创建深度/模板缓冲区及其视图

我们现在需要创建深度/模板缓冲区。如4.1.5节所述,深度缓冲区只是一个存储深度信息的2D纹理(如果使用模板,则模板信息也在该缓冲区中)。要创建纹理,我们必须填充一个D3D11_TEXTURE2D_DESC结构体来描述所要创建的纹理,然后再调用ID3D11Device::CreateTexture2D方法。该结构体的定义如下:

typedef struct D3D11_TEXTURE2D_DESC {

UINT Width;

UINT Height;

UINT MipLevels;

UINT ArraySize;

DXGI_FORMAT Format;

DXGI_SAMPLE_DESC SampleDesc;

D3D10_USAGE Usage;

UINT BindFlags;

UINT CPUAccessFlags;

UINT MiscFlags;

} D3D11_TEXTURE2D_DESC;

  1. Width:纹理的宽度,单位为纹理元素(texel)。
  2. Height:纹理的高度,单位为纹理元素(texel)。
  3. MipLevels:多级渐近纹理层(mipmap level)的数量。多级渐近纹理将在后面的章节“纹理”中进行讲解。对于深度/模板缓冲区来说,我们的纹理只需要一个多级渐近纹理层。
  4. ArraySize:在纹理数组中的纹理数量。对于深度/模板缓冲区来说,我们只需要一个纹理。
  5. Format:一个DXGI_FORMAT枚举类型成员,它指定了纹理元素的格式。对于深度/模板缓冲区来说,它必须是4.1.5节列出的格式之一。
  6. SampleDesc:多重采样数量和质量级别;请参阅4.1.7和4.1.8节。
  7. Usage:表示纹理用途的D3D11_USAGE枚举类型成员。有4个可选值:
  1. D3D11_USAGE_DEFAULT:表示GPU(graphics processing unit,图形处理器)会对资源执行读写操作。CPU不能读写这种资源。对于深度/模板缓冲区,我们使用D3D11_USAGE_DEFAULT标志值,因为GPU会执行所有读写深度/模板缓冲区的操作。
  2. D3D10_USAGE_IMMUTABLE:表示在创建资源后,资源中的内容不会改变。这样可以获得一些内部优化,因为GPU会以只读方式访问这种资源。除了在创建资源时CPU会写入初始化数据外,其他任何时候CPU都不会对这种资源执行任何读写操作。
  3. D3D10_USAGE_DYNAMIC:表示应用程序(CPU)会频繁更新资源中的数据内容(例如,每帧更新一次)。GPU可以从这种资源中读取数据,而CPU可以向这种资源中写入数据。
  4. D3D10_USAGE_STAGING:表示应用程序(CPU)会读取该资源的一个副本(即,该资源支持从显存到系统内存的数据复制操作)。
  1. BindFlags:指定该资源将会绑定到管线的哪个阶段。对于深度/模板缓冲区,该参数应设为D3D11_BIND_DEPTH_STENCIL。其他可用于纹理的绑定标志值还有:
  1. D3D11_BIND_RENDER_TARGET:将纹理作为一个渲染目标绑定到管线上。
  1. D3D11_BIND_SHADER_RESOURCE:将纹理作为一个着色器资源绑定到管线上。
  1. CPUAccessFlags:指定CPU对资源的访问权限。如果CPU需要向资源写入数据,则应指定D3D11_CPU_ACCESS_WRITE。具有写访问权限的资源的Usage参数应设为D3D11_USAGE_DYNAMICD3D11_USAGE_STAGING。如果CPU需要从资源读取数据,则应指定D3D11_CPU_ACCESS_READ。具有读访问权限的资源的Usage参数应设为D3D11_USAGE_STAGING。对于深度/模板缓冲区来说,只有GPU会执行读写操作;所以,我们将该参数设为0,因为CPU不会在深度/模板缓冲区上执行读写操作。
  2. MiscFlags:可选的标志值,与深度/模板缓冲区无关,所以设为0。

注意:推荐避免使用D3D11_USAGE_DYNAMICD3D11_USAGE_STAGING,因为有性能损失。要获得最佳性能,我们应创建所有的资源并将它们上传到GPU并保留其上,只有GPU在读取或写入这些资源。但是,在某些程序中必须有CPU的参与,因此这些标志无法避免,但你应该将这些标志的使用减到最小。

在本书中,我们会看到以各种不同选项来创建资源的例子;例如,使用不同的Usage标志值、绑定标志值和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;

// 使用4X MSAA?——必须与交换链的MSAA的值匹配

if( mEnable4xMsaa)

{

depthStencilDesc.SampleDesc.Count  = 4;

depthStencilDesc.SampleDesc.Quality  = m4xMsaaQuality-1;

}

//  不使用MSAA

else

{

depthStencilDesc.SampleDesc.Count  =  1;

depthStencilDesc.SampleDesc.Quality  = 0;

}

depthStencilDesc.Usage                = D3D10_USAGE_DEFAULT;

depthStencilDesc.BindFlags            = D3D10_BIND_DEPTH_STENCIL;

depthStencilDesc.CPUAccessFlags      = 0;

depthStencilDesc.MiscFlags            = 0;

ID3D10Texture2D* mDepthStencilBuffer;

ID3D10DepthStencilView* mDepthStencilView;



HR(md3dDevice->CreateTexture2D(

&depthStencilDesc, 0, &mDepthStencilBuffer));



HR(md3dDevice->CreateDepthStencilView(

mDepthStencilBuffer, 0, &mDepthStencilView));

CreateTexture2D的第二个参数是一个指向初始化数据的指针,这些初始化数据用来填充纹理。不过,由于个纹理被用作深度/模板缓冲区,所以我们不需要为它填充任何初始化数据。当执行深度缓存和模板操作时,Direct3D会自动向深度/模板缓冲区写入数据。所以,我们在这里将第二个参数指定为空值。

CreateDepthStencilView的第二个参数是一个指向D3D11_DEPTH_STENCIL_VIEW_DESC的指针。这个结构体描述了资源中这个元素数据类型(格式)。如果资源是一个有类型的格式(非typeless),这个参数可以为空值,表示创建一个资源的第一个mipmap等级的视图(深度/模板缓冲也只能使用一个 mipmap等级)。因为我们指定了深度/模板缓冲的格式,所以将这个参数设置为空值。

4.2.7 将视图绑定到输出合并器阶段

现在我们已经为后台缓冲区和深度缓冲区创建了视图,就可以将些视图绑定到管线的输出合并器阶段(output merger stage,使些资源成为管线的渲染目标和深度/模板缓冲区:

md3dImmediateContext->OMSetRenderTargets(

1,&mRenderTargetView,mDepthStencilView);

第一个参数是我们将要绑定的渲染目标的数量;我们在这里仅绑定了一个渲染目标,不过该参数可以为着色器同时绑定多个渲染目标(是一项高级技术)。第二个参数是我们将要绑定的渲染目标视图数组中的第一个元素的指针。第三个参数是将要绑定到管线的深度/模板视图。

注意:我们可以设置一组渲染目标视图,但是只能设置一个深度/模板视图。使用多个渲染目标是一项高级技术,会在本书的第三部分加以介绍。

4.2.8 设置视口

通常我们会把3D场景渲染到整个后台缓冲区上。不过,有时我们只希望把3D场景渲染到后台缓冲区的一个子矩形区域中,如图4.7所示。

图4.7:通过修改视口,我们可以把3D场景渲染到后台缓冲区的一个子矩形区域中。随后,后台缓冲区中的渲染结果会被呈现到窗口客户区上。

我们将后台缓冲区的子矩形区域称为视口(viewport,它由如下结构体描述:

typedef struct D3D11_VIEWPORT {

FLOAT TopLeftX;

FLOAT TopLeftY;

FLOAT Width;

FLOAT Height;

FLOAT MinDepth;

FLOAT MaxDepth;

} D3D11_VIEWPORT;

前4个数据成员定义了相对于窗口客户区的视口矩形范围。MinDepth 成员表示深度缓冲区的最小值,MaxDepth表示深度缓冲区的最大值。Direct3D使用的深度缓冲区取值范围是0到1,除非你想要得到一些特殊效果,否则应将MinDepthMaxDepth分别设为0和1。

在填充了D3D11_VIEWPORT结构体之后,我们可以使用ID3D11Device::RSSetViewports方法设置Direct3D的视口。下面的例子创建和设置了一个视口,该视口与整个后台缓冲区的大小相同:

D3D11_VIEWPORT vp;

vp.TopLeftX = 0;

vp.TopLeftY = 0;

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的数量用于高级的效果),第二个参数指向一个viewports的数组。

例如,你可以使用视口来实现双人游戏模式中的分屏效果。创建两个视口,各占屏幕的一半,一个居左,另一个居右。然后在左视口中以第一个玩家的视角渲染3D场景,在右视口中以第二个玩家的视角渲染3D场景。你也可以使用视口只绘制到屏幕的一个子矩形中,而在其他区域保留诸如按钮、列表框之类的UI控件。

4.3 计时和动画

要正确实现动画效果,我们就必须记录时间,尤其是要精确测量动画帧之间的时间间隔。当帧速率高时,帧之间的时间间隔就会很短;所以,我们需要一个高精确度计时器。

4.3.1 性能计时器

我们使用性能计时器(或性能计数器)来实现精确的时间测量。为了使用用于查询性能计时器的Win32函数,我们必须在代码中添加包含语句“#include<windows.h>”。

性能计时器采用的时间单位称为计数(count)。我们使用QueryPerformanceCounter函数来获取以计数测量的当前时间值:

__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函数的主要目的是为了获取两次调用之间的时间差——在执行一段代码之前记下当前时间,在该段代码结束之后再获取一次当前时间,然后计算两者之间的差值。也就是,我们总是查看两个时间戳之间的相对差,而不是由性能计数器返回的实际值。下面的代码更好地说明了这一概念:

__int64 A = 0;

QueryPerformanceCounter((LARGE_INTEGER*)&A);

/* Do work */

__int64 B = 0;

QueryPerformanceCounter((LARGE_INTEGER*)&B);

这样我们就可以知道执行这段代码所要花费的计数时间为(B−A),或者以秒表示的时间为(B−A)*mSecondsPerCount。

注意:MSDN指出当使用QueryPerformanceCounter函数时,有以下注意事项:“在多处理器计算机中,任何一个处理器单独调用该函数都不会出现问题。但是,由于基础输入/输出系统(BIOS)或硬件抽象层(HAL)存在技术瓶颈,所以你在不同的处理器上调用该函数会得到不同的结果”。你可以使用SetThreadAffinityMask函数让主应用程序线程只运行在一个处理器上,不在处理器之间进行切换。

4.3.2 游戏计时器类

在下面的两节中,我们将讨论GameTimer类的实现。

class GameTimer

{

public:

    GameTimer();



    float TotalTime()const;  // 单位为秒

    float DeltaTime()const; // 单位为秒



    void Reset(); // 消息循环前调用

    void Start(); // 取消暂停时调用

    void Stop();  // 暂停时调用

    void Tick();  // 每帧调用



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;

}

注意GameTimer类的定义和实现部分都保存在了GameTimer.h和GameTimer.cpp文件中,你可以在示例代码的Common目录中找到它们。

4.3.3 帧之间的时间间隔

当渲染动画帧时,我们必须知道帧之间的时间间隔,以使我们根据逝去的时间长度来更新游戏中的物体。我们可以采用以下步骤来计算帧之间的时间间隔:设ti为第i帧时性能计数器返回的时间值,设ti-1为前一帧时性能计数器返回的时间值,那么两帧之间的时间差为Δt = ti – ti-1。对于实时渲染来说,我们至少要达到每秒30帧的频率才能得到比较平滑的动画效果(我们一般可以达到更高的频率);所以,Δt = ti – ti-1通常是一个非常小的值。

下面的代码示范了Δt的计算过程:

void GameTimer::Tick()

{

    if( mStopped )

    {

       mDeltaTime = 0.0;

       return;

    }



    __int64 currTime;

    QueryPerformanceCounter((LARGE_INTEGER*)&currTime);

    mCurrTime = currTime;



    // 当前帧和上一帧之间的时间差

    mDeltaTime = (mCurrTime - mPrevTime)*mSecondsPerCount;



    // 为计算下一帧做准备

    mPrevTime = mCurrTime;



    // 确保不为负值。DXSDK中的CDXUTTimer提到:如果处理器进入了节电模式

    // 或切换到另一个处理器,mDeltaTime会变为负值。

    if(mDeltaTime < 0.0)

    {

       mDeltaTime = 0.0;

    }

}



float GameTimer::getDeltaTime() const

{

    return (float)mDeltaTime;

}

函数Tick在应用程序消息循环中的调用如下:

int D3DApp::Run()

{

    MSG msg = {0};

 

    mTimer.Reset();



    while(msg.message != WM_QUIT)

    {

       // 如果接收到Window消息,则处理这些消息

       if(PeekMessage( &msg, 0, 0, 0, PM_REMOVE ))

       {

            TranslateMessage( &msg );

            DispatchMessage( &msg );

       }

       // 否则,则运行动画/游戏

       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.3节)。不过,我们可以看到,当调用Reset方法时,mPrevTime被初始化为当前时间。这一点非常重要,因为对于动画的第一帧来说,没有前面的那一帧,也就是说没有前面的时间戳。所以个值必须在消息循环开始之前初始化。

4.3.4 游戏时间

另一个需要测量的时间是从应用程序开始运行时起经过的时间总量,其中不包括暂停时间;我们将这一时间称为游戏时间(game time)。下面的情景说明了游戏时间的用途。假设玩家有300秒的时间来完成一个关卡。当关卡开始时,我们会获取时间tstart,它是从应用程序开始运行时起经过的时间总量。当关卡开始后,我们不断地将tstart与总时间t进行比较。如果ttstart >300(如图4.8所示),就说明玩家在关卡中的用时超过了300秒,输掉了这一关。很明显,在一情景中我们不希望计算游戏的暂停时间。

图4.8:计算从关卡开始时起的时间。注意,我们将应用程序的开始时间作为原点(0),测量相对于这个时间原点的时间值。

游戏时间的另一个用途是通过时间函数来驱动动画运行。例如,我们希望一个灯光在时间函数的驱动下环绕着场景中的一个圆形轨道运动。灯光位置可由以下参数方程描述:

x = 10 cost

y = 20

z = 10 sint

这里t表示时间,随着t(时间)的增加,灯光的位置会发生改变,使灯光在平面y = 20上围绕着半径为10的圆形轨道运动。对于这种类型的动画,我们也不希望计算游戏的暂停时间;参见图4.9。

图4.9  如果我们在t1时暂停,在t2时取消暂停,并计算暂停时间,那么当我们取消暂停时,灯光的位置会从p(t1) 突然跳到p(t2)

我们使用以下变量来实现游戏计时:

__int64 mBaseTime;

__int64 mPausedTime;

__int64 mStopTime;

如4.3.3节所述,当调用Reset方法时,mBaseTime会被初始化为当前时间。我们可以把它视为从应用程序开始运行时起经过的时间总量。在多数情况下,你只会在消息循环开始之前调用一次Reset,之后不会再调用个方法,因为mBaseTime在应用程序的整个运行周期中保持不变。变量mPausedTime用于累计游戏的暂停时间。我们必须累计这一时间,以使我们从总的运行时间中减去暂停时间。当计时器停止时(或者说,当暂停时),mStopTime会帮我们记录暂停时间。

GameTimer类包含两个重要的方法StopStart,它们分别在应用程序暂停和取消暂停时调用,让GameTimer记录暂停时间。代码中的注释解释了这两个方法的实现思路。

void GameTimer::Stop()

{

    // 如果正处在暂停状态,则略过下面的操作

    if( !mStopped )

    {

       __int64 currTime;

       QueryPerformanceCounter((LARGE_INTEGER*)&currTime);



       // 记录暂停的时间,并设置表示暂停状态的标志

       mStopTime = currTime;

       mStopped  = true;

    }

}



void GameTimer::Start()

{

    __int64 startTime;

    QueryPerformanceCounter((LARGE_INTEGER*)&startTime);





    // 累加暂停与开始之间流逝的时间

    //

    //                     |<-------d------->|

    // ----*---------------*-----------------*------------> time

    //  mBaseTime       mStopTime        startTime    



    // 如果仍处在暂停状态

    if( mStopped )

    {

       // 则累加暂停时间

       mPausedTime += (startTime - mStopTime);

       // 因为我们重新开始计时,因此mPrevTime的值就不正确了,

       // 要将它重置为当前时间

       mPrevTime = startTime;

       // 取消暂停状态

       mStopTime = 0;     

       mStopped  = false;

    }

}

最后,成员函数TotalTime返回了自调用Reset之后经过的时间总量,其中不包括暂停时间。它的代码实现如下:

// 返回自调用Reset()方法之后的总时间,不包含暂停时间

float GameTimer::TotalTime()const

{

    // 如果处在暂停状态,则无需包含自暂停开始之后的时间。

    // 此外,如果我们之前已经有过暂停,则mStopTime - mBaseTime会包含暂停时间, 我们不想包含这个暂停时间,

    // 因此还要减去暂停时间: 

    //

    //                     |<--paused time-->|

    // ----*---------------*-----------------*------------*------------*------> time

    //  mBaseTime       mStopTime        startTime     mStopTime    mCurrTime



    if( mStopped )

    {

       return (float)(((mStopTime - mPausedTime)-mBaseTime)*mSecondsPerCount);

    }



    // mCurrTime - mBaseTime包含暂停时间,而我们不想包含暂停时间,

    // 因此我们从mCurrTime需要减去mPausedTime:

    //

    //  (mCurrTime - mPausedTime) - mBaseTime

    //

    //                     |<--paused time-->|

    // ----*---------------*-----------------*------------*------> time

    //  mBaseTime       mStopTime        startTime     mCurrTime

   

    else

    {

       return (float)(((mCurrTime-mPausedTime)-mBaseTime)*mSecondsPerCount);

    }

}

注意:我们的演示框架创建了一个GameTimer实例用于计算应用程序开始后的总时间和两帧之间的时间;你也可以创建额外的实例作为通用的秒表使用。例如,当点着一个炸弹时,你可以启动一个新的GameTimer,当TotalTime达到5秒时,你可以引发一个事件让炸弹爆炸。

4.4 演示程序框架

本书中的演示程序均使用d3dUtil.h、d3dApp.h、d3dApp.cpp文件中的代码,这些文件可以从本书网站下载。由于本书的第Ⅱ部分和第Ⅲ部分的所有演示程序都会用到些常用文件,所以我们把些文件保存在了Common目录下,使些文件被所有的工程共享,避免多次复制文件。d3dUtil.h文件包含了一些有用的工具代码,d3dApp.h和d3dApp.cpp文件包含了Direct3D应用程序类的核心代码。我们希望读者在阅读本章之后,仔细研究一下些文件,因为我们不会涵盖些文件中的每一行代码(例如,我们不会讲解如何创建一个Windows窗口,因为基本的Win32编程是阅读本书的先决条件)。该框架的目标是隐藏窗口的创建代码和Direct3D的初始化代码;通过隐藏些代码,我们可以在设计演示程序时减少注意力的分散,把注意力集中在示例程序所要表达的特定细节上。

4.4.1 D3DApp

D3DApp是所有Direct3D应用程序类的基类,它提供了用于创建主应用程序窗口、运行应用程序消息循环、处理窗口消息和初始化Direct3D的函数。另外,这个类还定义了一些框架函数。所有的Direct3D 应用程序类都继承于D3DApp类,重载它的virtual框架函数,并创建一个D3DApp派生类的单例对象。D3DApp类的定义如下:

#ifndef D3DAPP_H

#define D3DAPP_H



#include "d3dUtil.h"

#include "GameTimer.h"

#include <string>



class D3DApp

{

public:

    D3DApp(HINSTANCE hInstance);

    virtual ~D3DApp();

   

    HINSTANCE AppInst()const;

    HWND      MainWnd()const;

    float     AspectRatio()const;

   

    int Run();

 

    // 框架方法。派生类需要重载这些方法实现所需的功能。



    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);



    // 处理鼠标输入事件的便捷重载函数

    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;     // 应用程序实例句柄

    HWND      mhMainWnd;     // 主窗口句柄

    bool      mAppPaused;    // 程序是否处在暂停状态

    bool      mMinimized;    // 程序是否最小化

    bool      mMaximized;    // 程序是否最大化

    bool      mResizing;     // 程序是否处在改变大小的状态

    UINT      m4xMsaaQuality;// 4X MSAA质量等级



    // 用于记录"delta-time"和游戏时间(§4.3)

    GameTimer mTimer;



    //  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;



    //  下面的变量是在D3DApp构造函数中设置的。但是,你可以在派生类中重写这些值。

   

    //  窗口标题。D3DApp的默认标题是"D3D11 Application"。

    std::wstring mMainWndCaption;

   

    //  Hardware device还是reference device?D3DApp默认使用D3D_DRIVER_TYPE_HARDWARE。

    D3D_DRIVER_TYPE md3dDriverType;

    // 窗口的初始大小。D3DApp默认为800x600。注意,当窗口大小在运行阶段改变时,这些值也会随之改变。

    int mClientWidth;

    int mClientHeight;

    //  设置为true则使用4XMSAA(§4.1.8),默认为false。

    bool mEnable4xMsaa;

};

#endif // D3DAPP_H

在上面的代码中,我们使用注释描述了一些数据成员的含义;这些方法将在随后的几节中讨论。

4.4.2 非框架方法

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编程知识,知道如何初始化一个Windows窗口。

8.InitDirect3D:通过4.2节描述的各个步骤初始化Direct3D。

9.CalculateFrameStats:计算每秒的平均帧数和每帧的平均时间(单位为毫秒),这个方法的实现在4.4.4节中介绍。

4.4.3 框架方法

在本书的每个演示程序中,我们都会重载D3DApp的5个virtual函数。这5个函数用于实现特定示例中的代码细节。D3DApp类实现的这种结构可以将所有的初始化代码、消息处理代码和其他代码安排得井井有条,使派生类专注于实现演示程序的特定代码。下面是对这些框架方法的描述:

1.Init:该方法包含应用程序的初始化代码,比如分配资源、初始化对象和设置灯光。该方法在D3DApp的实现中包含InitMainWindowInitDirect3D方法的调用语句;所以,当在派生类中重载该方法时,应首先调用该方法的D3DApp版本,就像下面这样:

void TestApp::Init()

{

    if(!D3DApp::Init())

        return false;

    /* 剩下的初始化代码从这里开始 */

}

为你的后续初始化代码提供一个可用的ID3D11Device设备对象。(通常在获取 Direct3D资源时都要传递一个有效的ID3D11Device设备对象。)

2.OnResize:该方法在D3DApp::MsgProc收到WM_SIZE消息时调用。当窗口的尺寸改变时,一些与客户区大小相关的 Direct3D属性也需要改变。尤其是需要重新创建后台缓冲区和深度/模板缓冲区,使它们与窗口客户区的大小一致。后台缓冲区的大小可以通过调用IDXGISwapChain::ResizeBuffers方法来进行调整。而深度/模板缓冲区必须被销毁,然后根据新的大小重新创建。另外,渲染目标视图和深度/模板视图也必须重新创建。OnResize方法在D3DApp的实现中包含了调整后台缓冲区和深度/模板缓冲区的代码;详情请直接参见源代码。除缓冲区外,依赖于客户区大小的其他属性(例如,投影矩阵)也必须重新创建。我们把该方法作为框架的一部分是因为当窗口大小改变时,客户代码可能需要执行一些它自己的逻辑。

3.UpdateScene:该抽象方法每帧都会调用,用于随着时间更新3D应用程序(例如,实现动画和碰撞检测、检查用户输入、计算每秒帧数等等)。

4.DrawScene:该抽象方法每帧都会调用,用于将3D场景的当前帧绘制到后台缓冲区。当绘制当前帧时,我们调用了IDXGISwapChain::Present方法将后台缓冲区的内容呈现在屏幕上。

5.MsgProc:该方法是主应用程序窗口的消息处理函数。通常,当你只需重载该方法,就可以处理未由D3DApp::MsgProc处理(或者没按照你所希望的方式处理)的消息。该方法的D3DApp实现版本会在4.4.5节中讲解。如果你重载了这个方法,那么那些你没有处理的消息都会送到D3DApp::MsgProc中进行处理。

注意:除了上述的五个框架方法之外,为了使用起来更方便,我们还提供了三个虚函数,用于处理鼠标点击、释放和移动的事件。

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 帧的统计数值

通常游戏和绘图应用程序都要测量每秒的渲染帧数(FPS)。要实现这一工作,我们只需计算在某一特定时间段t中处理的总帧数(并存储在中变量n中)。然后得到时间段t中的平均FPS为fpsavg=n/t。如果我们将t设为1,那么fpsavg=n/1=n。在我们的代码中,我们将t设为1,这样可以减少一次除法操作,而且,以1秒为限可以得到一个最恰当的平均值——个时间间隔既不长也不短。计算FPS的代码由D3Dapp::CalculateFrameStats方法实现:

void D3DApp::CalculateFrameStats()

{

    // 计算每秒平均帧数的代码,还计算了绘制一帧的平均时间。

    // 这些统计信息会显示在窗口标题栏中。

    static int frameCnt = 0;

    static float timeElapsed = 0.0f;



    frameCnt++;



    // 计算一秒时间内的平均值

    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());

      

       // 为了计算下一个平均值重置一些值。

       frameCnt = 0;

       timeElapsed += 1.0f;

    }

}

为了统计帧数,我们在每帧中都会调用该方法。

除了计算FPS外,上面的代码还计算了处理一帧所花费的平均时间,单位为毫秒:

float mspf = 1000.0f / fps;

注意:帧时间与FPS是倒数关系,通过乘以1000ms/ 1s可以将秒转换为毫秒(1秒等于1000毫秒)。

这条语句的含义是:以毫秒为单位计算渲染一帧所花费的时间;是一个与FPS不同的值(虽然个值源于FPS)。实际上,计算帧时间比计算FPS更有用,因为它可以更直观地反映出由于修改场景而产生的渲染时间变化(增加或减少)。另一方面,FPS无法反映出这一变化。而且,[Dunlop03]在他的文章《FPS  versus Frame Time》中指出:由于FPS曲线是非线性的,所以使用FPS可能会得到误导性的结果。例如,考虑情景一:假设我们的应用程序以1000FPS的速率运行,每1ms(毫秒)渲染一帧。当帧速率下降到250FPS时,每4ms渲染一帧。现在,再考虑情景二:假设我们的应用程序以的100FPS的速率运行,每10ms渲染一帧。当帧速率下落到大约76.9 FPS时,大约为每13ms渲染一帧。在两个情景中,帧时间都是增加了3毫秒,增加的渲染时间完全相同。但是FPS的读数不够直观。从表面看上,似乎从1000FPS下降到250FPS,要比从100FPS下降到76.9FPS更严重一些。然而,正如我们之前所说,它们实际表示的渲染时间的增长量是相同的。

4.4.5 消息处理函数

我们在消息处理函数中实现的代码与整个应用程序框架相比微不足道。通常,我们不会用到许多Win32消息。其实,我们的应用程序的核心代码会在处理器空闲执行(即,当没有窗口消息执行)。不过,有一些重要的消息我们必须处理。因为考虑到篇幅问题,我们不可能在这里列出所有的代码; 我们只能对本例使用的几个消息做以讲解。我们希望读者下载源代码文件,花一些时间熟悉应用程序框架代码,因为它是本书每个示例的基础。

我们处理的第1个消息是WM_ACTIVATE。当应用程序获得焦点或失去焦点时,该消息被发送。我们这样来处理它:

// 当窗口被激活或非激活时会发送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。另外,当应用程序暂停时,计时器停止运行,当应用程序再次激活时,计时器恢复运行。如果回顾4.3.3节中D3DApp::Run方法,我们会发现当应用程序暂停时,我们并没有执行应用程序中的更新3D场景的代码,而是将空闲的CPU周期返回给了操作系统;通过这一方式,应用程序不会在处于非活动状态时独占CPU周期。

我们处理的第二个消息是WM_SIZE。该消息在改变窗口大小时发生。我们处理该消息的主要原因是希望后台缓冲区和深度/模板缓冲区的大小与窗口客户区的大小相同(为了不出现图像拉伸)。所以,每次改变窗口大小时,我们希望同改变缓冲区的大小。这一任务由D3DApp::OnResize方法实现。如前所述,后台缓冲区的大小可以通过调用IDXGISwapChain::ResizeBuffers方法来进行调整。而深度/模板缓冲区必须被销毁,然后根据新的大小重新创建。另外,渲染目标视图和深度/模板视图也必须重新创建。当用户拖动窗口边框时,我们必须格外小心,因为此时会有接连不断的WM_SIZE消息发出,我们不希望连续地调整缓冲区大小。所以,当用户拖动窗口边框时,我们(除了暂停应用程序外)不应该执行任何代码,等到用户的拖动操作结束之后我们再调整缓冲区的大小。我们通过处理WM_EXITSIZEMOVE消息来完成一工作。该消息在用户释放窗口边框时发送。

// 当用户拖动窗口边框时会发送WM_EXITSIZEMOVE消息。

case WM_ENTERSIZEMOVE:

    mAppPaused = true;

    mResizing  = true;

    mTimer.Stop();

    return 0;

// 当用户是否窗口边框时会发送WM_EXITSIZEMOVE消息。

// 然后我们会基于新的窗口大小重置所有图形变量

case WM_EXITSIZEMOVE:

    mAppPaused = false;

    mResizing  = false;

    mTimer.Start();

    OnResize();

    return 0;

最后处理的3个消息的实现过程非常简单,所以我们直接来看代码:

// 窗口被销毁时发送WM_DESTROY消息

case WM_DESTROY:

    PostQuitMessage(0);

    return 0;

// 如果使用者按下Alt和一个与菜单项不匹配的字符时,或者在显示弹出式菜单而

// 使用者按下一个与弹出式菜单里的项目不匹配的字符键时。

case WM_MENUCHAR:

    // 按下alt-enter切换全屏时不发出声响

    return MAKELRESULT(0, MNC_CLOSE);



// 防止窗口变得过小。

case WM_GETMINMAXINFO:

    ((MINMAXINFO*)lParam)->ptMinTrackSize.x = 200;

    ((MINMAXINFO*)lParam)->ptMinTrackSize.y = 200;

    return 0;

4.4.6 全屏模式

我们创建的IDXGISwapChain接口可以自动捕获Alt+Enter组合键消息,将应用程序切换到全屏模式(full-screen mode)。在全屏模式下,再次按下Alt+Enter组合键,可以返回到窗口模式。在这两种模式的切换中,应用程序的窗口大小会发生变化,会有一个WM_SIZE消息发送到应用程序的消息队列中;应用程序可以在此时调整后台缓冲区和深度/模板缓冲区的大小,使缓冲区与新的窗口大小匹配。另外,当切换到全屏模式时,窗口样式也会发生改变(即,窗口边框和标题栏会消失)。读者可以使用Visual Studio的Spy++工具查看一下在按下Alt+Enter组合键时由演示程序产生的Windows消息。

图4.10 第4章示例程序的屏幕截图。

注意:读者可以回顾一下4.2.3节描述的DXGI_SWAP_CHAIN_DESC::Flags标志值。

4.4.7 初始化 Direct3D 演示程序

现在,我们已经讨论了应用程序框架的所有内容,下面让我们来使用该框架生成一个小程序。基本上,我们用不着做任何实际工作就可以实现这个程序,因为基类D3DApp已经实现了它所需要的大部分功能。读者在这里应该关注是如何编写D3DApp的派生类以及实现框架方法,我们将要在这些框架方法中编写特定的示例代码。本书中的所有程序都使用这一模板。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值