dx11 龙书学习 第四章 dx11 准备工作
4.1 准备工作
Direct3D的初始化过程要求我们熟悉一些基本的Direct3D类型和基本绘图概念;本章第一节会向读者介绍些必要的基础知识。然后我们会详细讲解Direct3D初始化过程中的每一个必要步骤,并顺便介绍一下实时绘图应用程序必须使用的精确计时和时间测量。最后,我们将讨论一下示例框架代码,它是本书所有演示程序使用的统一编程接口。
学习目标:
- 对Direct3D在规划调度3D硬件方面所起的作用有一个基本了解。
- 理解COM在Direct3D运行时起到的作用。
- 学习基本的绘图概念,比如2D图像的存储方式、页面翻转、深度缓存和多重采样。
- 学习如何使用性能计数器函数来获取高精度的计时器读数。
- 阐述Direct3D的初始化过程。
- 熟悉本书所有演示程序采用的通用应用程序框架结构。
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枚举类型描述。一些常用的格式如下:
- DXGI_FORMAT_R32G32B32_FLOAT:每个元素包含3个32位浮点分量。
- DXGI_FORMAT_R16G16B16A16_UNORM:每个元素包含4个16位分量,分量的取值范围在[0,1]区间内。
- DXGI_FORMAT_R32G32_UINT:每个元素包含两个32位无符号整数分量。
- DXGI_FORMAT_R8G8B8A8_UNORM:每个元素包含4个8位无符号分量,分量的取值范围在[0,1]区间内。
- DXGI_FORMAT_R8G8B8A8_SNORM:每个元素包含4个8位有符号分量,分量的取值范围在[−1,1] 区间内。
- DXGI_FORMAT_R8G8B8A8_SINT:每个元素包含4个8位有符号整数分量,分量的取值范围在[−128, 127] 区间内。
- DXGI_FORMAT_R8G8B8A8_UINT:每个元素包含4个8位无符号整数分量,分量的取值范围在[0, 255]区间内。
注意,字母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
buffering)或z缓存(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,所以深度测试未通过,不更新缓冲区。 |
从上表可以看到,当我们发现某个像素具有更小的深度值时,就更新该像素以及它在深度缓冲区中的相应深度值。通过一方式,在最终得到的渲染结果中只会包含那些离观察者最近的像素。(如果读者对此仍有疑虑,那么可以试着交换本例的绘图顺序,看看得到的计算结果是否相同。)
综上所述,深度缓冲区用于为每个像素计算深度值和实现深度测试。深度测试通过比较像素深度来决定是否将该像素写入后台缓冲区的特定像素位置。只有离观察者最近的像素才会胜出,成为写入后台缓冲区的最终像素。这很容易理解,因为离观察者最近的像素会遮挡它后面的其他像素。
深度缓冲区是一个纹理,所以在创建它时必须指定一种数据格式。用于深度缓存的格式如下:
- DXGI_FORMAT_D32_FLOAT_S8X24_UINT:32位浮点深度缓冲区。为模板缓冲区预留8位(无符号整数),每个模板值的取值范围为[0,255]。其余24位闲置。
- DXGI_FORMAT_D32_FLOAT:32位浮点深度缓冲区。
- DXGI_FORMAT_D24_UNORM_S8_UINT:无符号24位深度缓冲区,每个深度值的取值范围为[0,1]。为模板缓冲区预留8位(无符号整数),每个模板值的取值范围为[0,255]。
- 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的初始化过程分为如下几个步骤:
- 使用D3D11CreateDevice方法创建ID3D11Device和ID3D11DeviceContext。
- 使用ID3D11Device::CheckMultisampleQualityLevels方法检测设备支持的4X多重采样质量等级。
- 填充一个IDXGI_SWAP_CHAIN_DESC结构体,该结构体描述了所要创建的交换链的特性。
- 查询IDXGIFactory实例,这个实例用于创建设备和一个IDXGISwapChain实例。
- 为交换链的后台缓冲区创建一个渲染目标视图。
- 创建深度/模板缓冲区以及相关的深度/模板视图。
- 将渲染目标视图和深度/模板视图绑定到渲染管线的输出合并阶段,使它们可以被Direct3D使用。
- 设置视口。
4.2.1 创建设备(Device)和上下文(Context)
要初始化Direct3D,首先需要创建Direct3D 11设备(ID3D11Device)和上下文(ID3D11DeviceContext)。它们是是最重要的Direct3D接口,可以被看成是物理图形设备硬件的软控制器;也就是说,我们可以通过该接口与硬件进行交互,命令硬件完成一些工作(比如:在显存中分配资源、清空后台缓冲区、将资源绑定到各种管线阶段、绘制几何体)。具体而言:
- ID3D11Device接口用于检测显示适配器功能和分配资源。
- 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一起安装,只用于程序员,而不应该用于程序发布。使用引用设备有两个原因:
- 测试硬件不支持的代码;例如,在一块不支持Direct3D 11的显卡上测试一段Direct3D 11的代码。
- 测试驱动程序缺陷。当代码能在引用设备上正常运行,而不能在硬件上正常工作时,说明硬件的驱动程序可能存在缺陷。
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.pFeatureLevels:D3D_FEATURE_LEVEL数组,元素的顺序表示要特征等级(见§4.1.9)的测试顺序。将这个参数设置为null表示选择可支持的最高等级。
6.FeatureLevels:pFeatureLevels数组中的元素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;#endifD3D_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 modeDXGI_MODE_SCALING Scaling; // display scaling mode} DXGI_MODE_DESC;
注意:在下面的数据成员描述中,我们只涵盖了一些常用的标志值和选项,它们对于初学者来说非常重要。对于其他标志值和选项的描述,请参阅SDK文档。
- BufferDesc:该结构体描述了我们所要创建的后台缓冲区的属性。我们主要关注的属性有:宽度、高度和像素格式;其他属性的详情请参阅SDK文档。
- SampleDesc:多重采样数量和质量级别(参阅4.1.8节)。
- BufferUsage:设为DXGI_USAGE_RENDER_TARGET_OUTPUT,因为我们要将场景渲染到后台缓冲区(即,将它用作渲染目标)。
- BufferCount:交换链中的后台缓冲区数量;我们一般只用一个后台缓冲区来实现双缓存。当然,你也可以使用两个后台缓冲区来实现三缓存。
- OutputWindow:我们将要渲染到的窗口的句柄。
- Windowed:当设为true时,程序以窗口模式运行;当设为false时,程序以全屏(full-screen)模式运行。
- SwapEffect:设为DXGI_SWAP_EFFECT_DISCARD,让显卡驱动程序选择最高效的显示模式。
- 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;}// NoMSAAelse{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);
- IDXGISwapChain::GetBuffer方法用于获取一个交换链的后台缓冲区指针。该方法的第一个参数表示所要获取的后台缓冲区的索引值(由于后台缓冲区的数量可以大于1,所以这里必须指定索引值)。在我们的演示程序中,我们只使用一个后台缓冲区,所以该索引值设为0。第二个参数是缓冲区的接口类型,它通常是一个2D纹理(ID3D11Texture2D)。第三个参数返回指向后台缓冲区的指针。
- 我们使用ID3D11Device::CreateRenderTargetView方法创建渲染目标视图。第一个参数指定了将要作为渲染目标的资源,在上面的例子中,渲染目标是后台缓冲区(即,我们为后台缓冲区创建了一个渲染目标视图)。第二个参数是一个指向D3D11_RENDER_TARGET_VIEW_DESC结构体的指针,该结构体描述了资源中的元素的数据类型。如果在创建资源时使用的是某种强类型格式(即,非弱类型格式),则该参数可以为空,表示以资源的第一个mipmap层次(后台缓冲区也只有一个mipmap层次)作为视图格式。第三个参数通过指针返回了创建后的渲染目标视图对象。
- 每调用一次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;
- Width:纹理的宽度,单位为纹理元素(texel)。
- Height:纹理的高度,单位为纹理元素(texel)。
- MipLevels:多级渐近纹理层(mipmap level)的数量。多级渐近纹理将在后面的章节“纹理”中进行讲解。对于深度/模板缓冲区来说,我们的纹理只需要一个多级渐近纹理层。
- ArraySize:在纹理数组中的纹理数量。对于深度/模板缓冲区来说,我们只需要一个纹理。
- Format:一个DXGI_FORMAT枚举类型成员,它指定了纹理元素的格式。对于深度/模板缓冲区来说,它必须是4.1.5节列出的格式之一。
- SampleDesc:多重采样数量和质量级别;请参阅4.1.7和4.1.8节。
- Usage:表示纹理用途的D3D11_USAGE枚举类型成员。有4个可选值:
- D3D11_USAGE_DEFAULT:表示GPU(graphics processing unit,图形处理器)会对资源执行读写操作。CPU不能读写这种资源。对于深度/模板缓冲区,我们使用D3D11_USAGE_DEFAULT标志值,因为GPU会执行所有读写深度/模板缓冲区的操作。
- D3D10_USAGE_IMMUTABLE:表示在创建资源后,资源中的内容不会改变。这样可以获得一些内部优化,因为GPU会以只读方式访问这种资源。除了在创建资源时CPU会写入初始化数据外,其他任何时候CPU都不会对这种资源执行任何读写操作。
- D3D10_USAGE_DYNAMIC:表示应用程序(CPU)会频繁更新资源中的数据内容(例如,每帧更新一次)。GPU可以从这种资源中读取数据,而CPU可以向这种资源中写入数据。
- D3D10_USAGE_STAGING:表示应用程序(CPU)会读取该资源的一个副本(即,该资源支持从显存到系统内存的数据复制操作)。
- BindFlags:指定该资源将会绑定到管线的哪个阶段。对于深度/模板缓冲区,该参数应设为D3D11_BIND_DEPTH_STENCIL。其他可用于纹理的绑定标志值还有:
- D3D11_BIND_RENDER_TARGET:将纹理作为一个渲染目标绑定到管线上。
- D3D11_BIND_SHADER_RESOURCE:将纹理作为一个着色器资源绑定到管线上。
- CPUAccessFlags:指定CPU对资源的访问权限。如果CPU需要向资源写入数据,则应指定D3D11_CPU_ACCESS_WRITE。具有写访问权限的资源的Usage参数应设为D3D11_USAGE_DYNAMIC或D3D11_USAGE_STAGING。如果CPU需要从资源读取数据,则应指定D3D11_CPU_ACCESS_READ。具有读访问权限的资源的Usage参数应设为D3D11_USAGE_STAGING。对于深度/模板缓冲区来说,只有GPU会执行读写操作;所以,我们将该参数设为0,因为CPU不会在深度/模板缓冲区上执行读写操作。
- MiscFlags:可选的标志值,与深度/模板缓冲区无关,所以设为0。
注意:推荐避免使用D3D11_USAGE_DYNAMIC和D3D11_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;}// 不使用MSAAelse{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,除非你想要得到一些特殊效果,否则应将MinDepth和MaxDepth分别设为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进行比较。如果t – tstart >300(如图4.8所示),就说明玩家在关卡中的用时超过了300秒,输掉了这一关。很明显,在一情景中我们不希望计算游戏的暂停时间。
图4.8:计算从关卡开始时起的时间。注意,我们将应用程序的开始时间作为原点(0),测量相对于这个时间原点的时间值。
游戏时间的另一个用途是通过时间函数来驱动动画运行。例如,我们希望一个灯光在时间函数的驱动下环绕着场景中的一个圆形轨道运动。灯光位置可由以下参数方程描述:
x = 10 costy = 20z = 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类包含两个重要的方法Stop和Start,它们分别在应用程序暂停和取消暂停时调用,让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 mCurrTimeif( mStopped ){return (float)(((mStopTime - mPausedTime)-mBaseTime)*mSecondsPerCount);}// mCurrTime - mBaseTime包含暂停时间,而我们不想包含暂停时间,// 因此我们从mCurrTime需要减去mPausedTime://// (mCurrTime - mPausedTime) - mBaseTime//// |<--paused time-->|// ----*---------------*-----------------*------------*------> time// mBaseTime mStopTime startTime mCurrTimeelse{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的实现中包含InitMainWindow和InitDirect3D方法的调用语句;所以,当在派生类中重载该方法时,应首先调用该方法的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 / 1float 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的派生类以及实现框架方法,我们将要在这些框架方法中编写特定的示例代码。本书中的所有程序都使用这一模板。
相关文章:
dx11 龙书学习 第四章 dx11 准备工作
4.1 准备工作 Direct3D的初始化过程要求我们熟悉一些基本的Direct3D类型和基本绘图概念;本章第一节会向读者介绍些必要的基础知识。然后我们会详细讲解Direct3D初始化过程中的每一个必要步骤,并顺便介绍一下实时绘图应用程序必须使用的精确计时和时间测…...
运维打铁:域名详解及常见问题解决
文章目录 前言一、域名基础概念1. 什么是域名2. 域名结构3. 域名解析 二、域名工作原理1. DNS 服务器层次结构2. 域名解析过程 三、常见域名问题及解决办法1. 域名无法解析2. 域名解析延迟3. 域名解析结果不一致 四、总结 前言 在当今数字化的时代,互联网已经成为我…...
【大模型ChatGPT+R-Meta】AI赋能R-Meta分析核心技术:从热点挖掘到高级模型、助力高效科研与论文发表“
Meta分析是针对某一科研问题,根据明确的搜索策略、选择筛选文献标准、采用严格的评价方法,对来源不同的研究成果进行收集、合并及定量统计分析的方法,现已广泛应用于农林生态,资源环境等方面,成为Science、Nature论文的…...
ElasticSearch深入解析(五):如何将一台电脑上的Elasticsearch服务迁移到另一台电脑上
文章目录 0.安装数据迁移工具1.导出数据2.导出mapping3.导出查询模板4.拷贝插件5.拷贝配置6.导入到目标电脑上 0.安装数据迁移工具 Elasticsearch dump是一个用于将Elasticsearch索引数据导出为JSON格式的工具。你可以使用Elasticsearch dump通过命令行或编程接口来导出数据。…...
QT中的多线程
Qt中的多线程和Linux中的线程本质是相同的,Qt中针对系统提供的线程API进行了重新封装 QThread类 Qt中的多线程一般通过QThread类实现,要想创建线程就要创建这个类的实例 QThread代表一个在应用程序中可以独立控制的线程,也可以和进程中的其…...
Win11安装Ubuntu20.04简记
写在前面 之前装的22.04,不稳定,把22.04卸载了,重新安装20.04系统。这里主要把卸载和安装的过程中参考到的博客在这记录一下。 卸载ubuntu系统参考的博文 卸载参考博文1 卸载参考博文2 Ubuntu20.04安装参考博文 安装参考博文1 安装参考博…...
电子电器架构 ---电气/电子架构将在塑造未来出行方面发挥啥作用?
我是穿拖鞋的汉子,魔都中坚持长期主义的汽车电子工程师。 老规矩,分享一段喜欢的文字,避免自己成为高知识低文化的工程师: 钝感力的“钝”,不是木讷、迟钝,而是直面困境的韧劲和耐力,是面对外界噪音的通透淡然。 生活中有两种人,一种人格外在意别人的眼光;另一种人无论…...
pdf.js移动端预览PDF文件时,支持双指缩放
在viewer.html中添加手势缩放代码 <script>// alert("Hello World");let agent navigator.userAgent.toLowerCase();// if (!agent.includes("iphone")) {let pinchZoomEnabled false;function enablePinchZoom(pdfViewer) {let startX 0, start…...
机器人--激光雷达
教程 教程 激光雷达 激光 激光(Laser),是一种人造的、高度纯净的单色光。 雷达 激光器旋转机构雷达。 雷达根据激光探头发出激光束的数量,一般可以分为单线激光雷达(2D激光雷达)和多线激光雷(3D激光雷达)。 作用 测距原理 激…...
最新ios开发证书/发布证书/免费证书/企业证书制作教程
本文介绍了如何制作或者苹果开发证书p12文件,含开发证书,推送证书,发布证书,企业证书,免费证书,您在iphone和ipad开发构建 IOS App 应用和苹果ios app签名需要用到。如果嫌麻烦,可以使用懒人工具…...
【Keil5-开发指南】
Keil5-编程指南 ■ Keil5 介绍■ J-Flash 使用■ Keil5-Debug调试工具 Jlink---STLink---DAP仿真器■ Keil5 使用 AStyle插件格式化代码■ Keil5-编译4个阶段■ Keil5-Boot和APP配置■ Keil5-报错■ 芯片手册区别 ■ Keil5 介绍 Keil5 介绍 ■ J-Flash 使用 J-Flash 使用 ■…...
蓝桥杯 18. 机器人繁殖
机器人繁殖 原题目链接 题目描述 X 星系的机器人可以自动复制自己。它们用 1 年的时间可以复制出 2 个自己,然后就失去复制能力。 每年 X 星系都会选出 1 个新出生的机器人发往太空。也就是说,如果 X 星系原有机器人 5 个,1 年后总数是&a…...
从微服务到AI服务:Nacos 3.0如何重构下一代动态治理体系?
在现代微服务架构的浪潮中,Nacos早已成为开发者手中的“瑞士军刀”。作为阿里巴巴开源的核心中间件,它通过动态服务发现、统一配置管理和服务治理能力,为云原生应用提供了坚实的基石。从初创公司到全球500强企业,Nacos凭借其开箱即…...
60、微服务保姆教程(三)Sentinel---高可用流量管理框架/服务容错组件
Sentinel—高可用流量管理框架/服务容错组件 一.为什么要用Sentinel? 1.微服务架构中当某服务挂掉的时候常见的原因有哪些? 1.异常没处理 比如DB连接失败,文件读取失败等 2.突然的流量激增 比如:用户经常会在京东、淘宝、天猫、拼多多等平台上参与商品的秒杀、限时抢…...
[特殊字符] 基于Docker部署Nacos注册中心及微服务注册发现详解(含MySQL持久化配置)
📚 目录 项目背景与准备 Docker部署Nacos并配置MySQL持久化 微服务注册到Nacos(item-service示例) 微服务服务发现与调用(cart-service示例) 小结 1. 项目背景与准备 在微服务架构中,服务注册与发现是…...
CentOS 7环境配置DHCP服务器
主播多次测试,没有什么问题。如果有问题可以私信主播,主播不定时查看 目录 1. 初始配置 1.1 配置VMware workstation配置 1.2 设置虚拟机网路适配器为NAT模式 2. 环境搭建 2.1下载相应的软件包 2.2 查找下载的软件包中给出的示例代码所在目录 2.2 …...
基于常微分方程的神经网络(Neural ODE)
参考资料:B站的视频解析 知乎神经常微分方程总结 论文链接:论文 什么是常微分方程? 微分方程式包含未知函数及其导数的方程,未知函数导数的最高阶数称为给i微分方程的阶。 常微分方程(ordinary differential equation࿰…...
对VTK中的Volume Data体数据进行二维图像处理
文章目录 概要Cpp代码处理前效果处理后效果 概要 在 VTK 中对体数据进行二维图像处理的过程通常涉及从三维体数据中提取二维切片,并对这些切片进行处理。然后,可以选择性地将处理后的切片数据重新合并成新的体数据。 以下是对 VTK 中的体数据进行二维图…...
阿里云ftp服务器登录要怎么做?如何访问ftp服务器?
阿里云ftp服务器登录要怎么做?如何访问ftp服务器? 访问FTP服务器通常需要以下步骤,具体方法取决于您使用的工具和操作系统: 一、FTP服务器登录所需信息 服务器地址:通常是IP地址(如 ftp.example.com 或 192…...
中国的国产化进程
中国的国产化进程是一个涉及国家安全、经济发展和技术自主的长期战略,其历史进程和动因可以从以下几个关键阶段和核心原因来理解: 一、国产化的历史进程 1. 建国初期(1949–1978):自力更生与基础工业建设 背景:新中国成立后,面临西方国家的技术…...
突破语言藩篱:从Seq2Seq到智能翻译的范式革命
## 一、语言之桥的智能进化:超越字面转换的深层理解 在慕尼黑工业大学实验室的深夜,一个搭载最新神经网络的翻译系统正逐字解析着歌德诗句的韵律。这并非简单的词语替换,而是一场跨越时空的文化解码——机器首次在《浮士德》的英译本中保留了德文诗歌特有的头韵结构。这个突…...
Java写项目前的准备工作指南(技术栈选择 环境搭建和工具配置 项目结构设计与模块划分)
前言 📝 在开始编写一个 Java 项目之前,做好充分的准备工作是至关重要的。很多初学者可能在没有清晰规划的情况下就开始编写代码,导致项目开发进度缓慢、结构混乱,甚至最终无法按预期完成。而事实上,项目的成功不仅仅…...
如何使用 Redis 缓存验证码
目录 🧠 Redis 缓存验证码的工作原理 🧰 实现流程 1. 安装 Redis 和 Python 客户端 2. 生成并缓存验证码 示例代码:生成并存储验证码 3. 发送验证码(以短信为例) 4. 校验验证码 示例代码:校验验证码…...
(八)RestAPI 毛子(Unit Testing)
文章目录 项目地址一、Unit Testing1.1 创建X unit 测试项目1. 创建项目目录2. 管理包 1.2 创建CreateEntryDtoValidator测试1.3 创建CreateEntryDtoValidator测试 二、Integration test2.1 创建Integration test环境1. 安装所需要的包 2.2 配置基础设置1. 数据库链接DevHabitW…...
德州仪器(TI)—TDA4VM芯片详解(2)—产品应用和介绍
写在前面 本系列文章主要讲解德州仪器(TI)TDA4VM芯片的相关知识,希望能帮助更多的同学认识和了解德州仪器(TI)TDA4VM芯片。 若有相关问题,欢迎评论沟通,共同进步。(*^▽^*) 错过其他章节的同学…...
vue2,3:v-model的语法糖
Vue2的v-model 语法糖 **1. **v-model 的作用 v-model 是 Vue 中用于实现双向数据绑定的指令,主要用于表单元素(如 、、)和自定义组件。它简化了数据与视图之间的同步,使得开发者可以方便地处理用户输入。 **2. **v-model 的语…...
【深度学习】#10 注意力机制
主要参考学习资料: 《动手学深度学习》阿斯顿张 等 著 【动手学深度学习 PyTorch版】哔哩哔哩跟李牧学AI 目录 注意力提示生物学中的注意力提示查询、键和值 注意力汇聚注意力评分函数掩蔽softmax操作加性注意力缩放点积注意力 Bahdanau注意力多头注意力自注意力和位…...
Modbus总线协议智能网关协议转换案例解析:提升系统兼容性
Modbus是一种串行通信协议,是Modicon公司(现在的施耐德电气,Schneider Electic)于1979年为使用可编程逻辑控制器(PLC)通信而发表。Modbus已经成为工业领域通信协议的业界标准(Defacto),并日现在是工业电子设备之间常用的连接方式 Modbus是一种串行通信协…...
echarts自定义图表--仪表盘
基于仪表盘类型的自定义表盘 上图为3层结构组成 正常一个仪表盘配置要在外圈和内圈之间制造一条缝隙间隔 再创建一个仪表盘配置 背景透明 进度条拉满 进度条颜色和数据的背景相同开始处的线 又一个仪表盘配置 数值固定一个比较小的值 <!DOCTYPE html> <html><h…...
第五章:Execution Flow Framework
Chapter 5: Execution Flow Framework 从消息记忆到执行流程:如何让多个AI“同事”协同完成复杂任务? 在上一章的消息与记忆系统中,我们已经能让AI记住之前的对话内容。但你是否想过:如果用户要求“预订从北京到上海的高铁&#…...
01 C++概述
一、C语言发展史 起源与演进 • 1960s:剑桥大学Martin Richards开发BCPL语言,用于系统软件开发。 • 1970年:贝尔实验室Ken Thompson在BCPL基础上发明B语言。 • 1972年:Dennis Ritchie和Brian Kernighan设计出C语言,兼…...
Kotlin DSL 深度解析:从 Groovy 迁移的困惑与突破
引言 Gradle 作为现代构建工具,支持 Groovy 和 Kotlin 两种 DSL(领域特定语言)。Kotlin DSL 因其类型安全和更好的 IDE 支持逐渐流行,但它的语法设计却让许多开发者感到困惑,尤其是从 Groovy 迁移时。 本文将从 Kotl…...
2025年二级造价师考点总结
二级造价师考点总结 一、建设工程造价管理 工程造价构成:重点掌握建筑安装工程费(人工费、材料费、机械费、企业管理费、利润、规费、税金)的组成及计算。 计价依据:熟悉工程量清单计价规范,掌握定额计价与清单计价的…...
Typecho博客使用阿里云cdn和oss:handsome主题进阶版
Typecho使用阿里云cdn和oss 设置前需要保证阿里云cdn和oss已配置好且可以正常使用一、准备工作二、修改 Handsome 主题的静态资源链接方法 1:直接修改主题文件(推荐)方法 2:通过主题设置自定义(方便) 三、处…...
知识体系_用户研究_用户体验度量模型
1 用户体验度量常见模型 1.1 满意度(CSAT/PSAT) CSAT(Customer Satisfaction)指客户满意度,PAST(Product Satisfaction)指产品满意度。顾名思义,其用于衡量客户对产品或服务的体验度量指标。在用户完成某个产品或某项服务的体验后,对其进行…...
邮件分类特征维度实验分析
活动发起人小虚竹 想对你说: 这是一个以写作博客为目的的创作活动,旨在鼓励大学生博主们挖掘自己的创作潜能,展现自己的写作才华。如果你是一位热爱写作的、想要展现自己创作才华的小伙伴,那么,快来参加吧!…...
Linux服务之Nginx服务部署及基础配置
目录 一.Nginx介绍 1.Nginx功能介绍 2.基础特性 3.Web服务相关的功能 4.I/O模型相关概念 5.nginx模块 6.Nginx文件存放位置 7.Nginx事件驱动模型 二.平滑升级及信号使用 1.Nginx 程序当作命令使用 2.信号类型 3.平滑升级nginx 4.回滚 三.Nginx调优 1.隐藏版本号或…...
Centos小白之在CentOS8.5中安装Rabbitmq 3.10.8
注意事项 安装以及运行等其他操作,要使用root账号进行,否则会遇到很多麻烦的事情。 使用命令行进行远程登录 ssh root192.168.0.167 安装make 执行安装命令 yum -y install make gcc gcc-c kernel-devel m4 ncurses-devel openssl-devel这里有可能会…...
基于单片机的游泳馆智能管理系统设计与实现
标题:基于单片机的游泳馆智能管理系统设计与实现 内容:1.摘要 随着游泳馆规模的不断扩大和管理需求的日益提高,传统的管理方式已难以满足高效、精准的管理要求。本文旨在设计并实现一种基于单片机的游泳馆智能管理系统。采用单片机作为核心控制单元,结合…...
深度相机(一)——深度相机模型及用途介绍
一、深度相机概述 深度相机,又称 3D 相机,是一种能够获取场景中物体深度信息(即物体到相机的距离)的设备。与传统相机只能拍摄二维平面图像不同,深度相机不仅能记录物体的颜色和纹理,还能通过特定技术手段测…...
【Torch】nn.Conv1d、nn.Conv2d、nn.Conv3d算法详解
1. nn.Conv1d 1.1 输入(Input)和输出(Output) 输入张量 形状:(batch_size, in_channels, length) batch_size:一次过网络的样本数in_channels:每个样本的通道数(特征维度࿰…...
Android WebRTC回声消除
文章目录 安卓可用的回声消除手段各种回声消除技术优缺点WebRTC回声消除WebRTC回声消除回声消除处理流程WebRTC AECM APP 安卓可用的回声消除手段 硬件回声消除 使用 AudioRecord 的 VOICE_COMMUNICATION 模式:通过 AudioRecord 的 VOICE_COMMUNICATION 音频源可以…...
[Linux运维] [Ubuntu/Debian]在Lightsail Ubuntu服务器上安装Python环境的完整指南
在之前的教程中,我们已经讲过如何开通亚马逊Lightsail服务器并安装宝塔面板。今天,我们来进一步补充:如何在Lightsail上的Ubuntu/Debian系统中安装和配置Python开发环境。 本教程不仅适用于Lightsail服务器,也适用于所有使用Ubunt…...
2025医疗领域AI发展五大核心趋势与路线研究
引言 人工智能技术正在全球范围内深刻改变医疗服务的提供方式,推动全球医疗的普惠化、技术合作、产业升级以及公共卫生防控发生巨变[0]。医疗AI的浪潮奔涌向前,从2024年开始,生成式AI的爆发式发展更是将医疗AI推到了新的十字路口[1]。在这一背景下,本报告将深入探讨医疗领…...
【学习笔记】机器学习(Machine Learning) | 第六周|过拟合问题
机器学习(Machine Learning) 简要声明 基于吴恩达教授(Andrew Ng)课程视频 BiliBili课程资源 文章目录 机器学习(Machine Learning)简要声明 摘要过拟合与欠拟合问题一、回归问题中的过拟合1. 欠拟合(Underfit&#x…...
【MQ篇】RabbitMQ之惰性队列!
目录 引言:当“生产”大于“消费”,队列就“胖”了!肥宅快乐队列?🤔队列界的“躺平”大师:惰性队列(Lazy Queues)驾到!😴如何“激活”你的队列的“惰性”属性…...
计算机视觉——通过 OWL-ViT 实现开放词汇对象检测
介绍 传统的对象检测模型大多是封闭词汇类型,只能识别有限的固定类别。增加新的类别需要大量的注释数据。然而,现实世界中的物体类别几乎无穷无尽,这就需要能够检测未知类别的开放式词汇类型。对比学习(Contrastive Learning&…...
第二部分:网页的妆容 —— CSS(下)
目录 6 布局基础:Display 与 Position - 元素如何排列和定位6.1 小例子6.2 练习 7 Flexbox 弹性布局:一维布局利器7.1 小例子7.2 练习 8 Grid 网格布局:强大的二维布局系统8.1 小例子8.2 练习 9 响应式设计与媒体查询:适应不同设备…...
vite项目tailwindcss4的使用
1、安装taillandcss 前几天接手了一个项目,看到别人用tailwindcss节省了很多css代码的编写,所以自己也想在公司项目中接入tailwindcss。 官网教程如下: Installing Tailwind CSS with Vite - Tailwind CSS 然而,我在vite中按…...
css中:is和:where 伪函数
在 CSS 里,:is() 属于伪类函数,其作用是对一组选择器进行匹配,只要元素与其中任何一个选择器相匹配,就可以应用对应的样式规则。以下是详细介绍: 基本语法 :is() 函数的参数是一个或多个选择器,各个选择器之…...