DirectX11

3D数学基础

向量

矩阵

线性变换

  • 以几何的方式描述3D场景中的物体:一组三角形近似地模拟物体的外表面。

  • 为了使得创建的物体移动,我们可以对几何物体进行变换

  • 线性变换(函数)的输入输出不一定都是3D向量,但是在3D图形学中基本都是。

  • 向量u = (x, y, z) = xi + yj + zk = x(1,0,0) + y(0,1,0) + z(0,0,1)

    • 向量i,j,k是标准基向量
  • 矩阵的意义

    • 用于对向量进行线性变换:旋转,缩放,投影,镜像,仿射等
  • 正交投影

    • 降低维度操作,某个方向用零作为缩放因子
    • 正交矩阵
  • 镜像

    • 镜像矩阵
  • 切变:坐标轴的扭曲变换

    • 将一个坐标的乘积加到另一个坐标上。
    • 如即取出一个坐标,乘以不同的因子,再加到另一个坐标上,对应3个不同的切变矩阵。

缩放

  • 缩放变换的的定义 : S(X,Y,Z) = (Sx.X,Sy.Y,Sz.Z)

    • Sx ,Sy, Sz 是相应方向上的缩放单位
  • 意义:通过比例因子对向量的不同分量或相同的分量进行缩放

  • 应用

    • 缩放矩阵:向量乘以左乘(向量在左边)一个缩放矩阵,从而实现向量的缩放

旋转

  • 转旋:绕轴旋转
    • 有通用的旋转矩阵
    • 也有对应三个轴的旋转矩阵
    • 不过,用欧拉角处理旋转更方便更方便

仿射变换

  • 一个组合了平移的线性变换,即一个线性变换加上平移向量变换。

齐次坐标

  • 向量只表示方向和长度,与位置无关,所以平移一个向量是无意义的;平移只作用在点上,一起坐标就用于同一处理点和向量,以第四个坐标分量w来决定所描述的是一个点还是一个向量。

  • 在齐次坐标中

    • (x,y,z,0)用于向量
    • (x,y,z,1)用于点
  • 点与点相减的值是一个向量

  • 点与向量相加的值仍然是一个点

  • 仿射变换就是一个线性变换加上一个平移向量,从而构成一个齐次的仿射矩阵

平移

  • 平移矩阵
1 0 0 0
0 1 0 0
0 0 1 0
x y z 1
  • 平移矩阵的逆矩阵
1 0 0 0
0 1 0 0
0 0 1 0
-x-y-z 1 

组合变换

  • 根据矩阵乘法的结合律,将旋转矩阵R,平移矩阵T,缩放矩阵S。三个矩阵封装为一个净仿射变换矩阵,将他们连在一起,减少矩阵乘法次数,提高性能。

坐标转换变换

  • 如同水的标量沸点是100摄氏度,在华氏摄氏度中表示为212,为温度转换。

  • 坐标转换变换(change of coordinate transformation):就是将一个点或向量的坐标从一个参考系转换到另一个不同的参考系的变换。

  • 位置是点的属性,而不是向量的属性,所以点和向量在实现坐标转换时需要区别对待,使用不同的处理方式。

向量

  • 比如,连个参考坐标系A,B,一个向量p,假设p在参考系A中的坐标为PA = (x,y);,向量p相对于参考系B中的坐标pB = (x‘, y’)

  • p = xU + yV

    • U,V是单位向量,所指的方向分别于参考系A的x轴和y轴方向相同
  • PB = xUB + yVB

    • 只需要知道向量U,V相对于参考系B的的坐标,即UB(Ux,Uy)和VB(Vx,Vy),则对于任意给出的PA = (x,y),都可以计算出pB = (x,y)
  • 对于3D的向量

    • pA = (x,y,z)
    • pB = xUB + yVB + zWB

  • 与向量存在微小差异,因为位置是点的一个重要属性

  • 点p: p = xU + yV + Q

    • U,V同上,Q是参考系A的原点
  • pB = xUB + yVB + QB

    • 只需要求得向量U,V的坐标以及相对于参考系B的原点QB = (Qx, Qy),对于任意坐标PA(x,y), 就可以求出pB(x,y)
  • 对于3D的向量

    • pA = (x,y,z)
    • pB = xUB + yVB + zWB + QB

矩阵表示点和向量的坐标变换

  • 使用齐次坐标则可以通过同一个方程处理向量和点
    • (x,y,z`,w) = xUB + yVB + zWB + wQB
      • 当 w = 0 时,该方程用于处理向量的坐标转换变换,w = 1 时, 该方程用于处理点的坐标转换变换。
                            | Ux, Uy, Uz, 0 |
[x`,y`,z`,w] = [x,y,z,w] .  | Vx, Vy, Vz, 0 |
                            | Wx, Wy, Wz, 0 |
                            | Qx ,Qy ,Qz, 1 |
  • 坐标转换矩阵支持结合律

逆矩阵与坐标变换矩阵

  • 坐标矩阵与逆矩阵的关系也适用于坐标转换矩阵
    • PA M = PB
      • M为A坐标系到B坐标系的坐标转换矩阵
    • PB M(-1) = PA
      • M(-1)为B坐标系到A坐标系的坐标转换矩阵

转换矩阵与坐标转换矩阵

  • 仿射变换和坐标变换都是需要续传和平移坐标等,但是他们的变换的方式不一样。

  • 使用多个坐标系统,我们可以让物体自身保持不变,只是从一个参考系转换到另一个参考系中,由于参考系变化而导致物体坐标的变化。

  • 同一个参考系的物体位置发生变化而进行变换。

XNA数学库的变换函数

//创建一个缩放矩阵
XMMATRIX XMMatrixScaling(FLOAT ScaleX, FLOAT SCaleY, FLOAT ScaleZ); //缩放因子

//从向量中的分量中创建一个缩放矩阵
XMMATR IX XMMatrixScalingFromVector(FXMVCTOR SCale); //缩放因子(sz,sy,sz)

//创建一个绕x轴旋转的矩阵:Rx
XMMATRIX XMMatrixRotationX(FLOAT Angle); //顺时针的旋转角度

//创建一个绕x轴旋转的矩阵:Ry
XMMATRIX XMMatrixRotationY(FLOAT Angle); //顺时针的旋转角度

//创建一个绕x轴旋转的矩阵:Rz
XMMATRIX XMMatrixRotationZ(FLOAT Angle); //顺时针的旋转角度

//创建一个绕任意轴轴旋转的矩阵:Rn
XMMATRIX XMMatrixRotationX(FXMVCTOR Axis, FLOAT Angle); //旋转轴,顺时针的旋转角度

//创建一个平移矩阵
XMMATRIX XMMatrixTranslation(FLOAT OffsetX,FLOAT OffsetY,FLOAT OffsetZ); //平移因子

//从向量中的分量中创建一个平移矩阵
XMMATRIX XMMatrixTranslationFromVector(FXMVCTOR Offset); //平移因子(tx, ty, tz)

//计算向量-矩阵乘积vM:
XMVECTOR XMVector3Transform(FXMVCTOR v, CXMMATRIX M);

//计算向量-矩阵乘积vM 其中vw = 1,用于变换点的坐标:
XMVECTOR XMVector3TransformCoord(FXMVCTOR v, CXMMATRIX M);

//计算向量-矩阵乘积vM 其中vw = 1,用于变换变量:
XMVECTOR XMVector3TransformNormal(FXMVCTOR v, CXMMATRIX M);

DirectX基础

  • Direct3D 是一种底层绘图 API(application programming interface,应用程序接口),它可以让我们可以通过 3D 硬件加速绘制 3D 世界。从本质上讲,Direct3D 提供的是一组软件接口,我们可以通过这组接口来控制绘图硬件。

  • Direct3D 层位于应用程序和绘图硬件之间,这样我们就不必担心 3D 硬件的实现细节,只要设备支持 Direct3D 11,我们就可以通过 Direct3D 11 API 来控制 3D 硬件了。

COM

  • 组件对象模型(COM)技术使DirectX独立于任何编程语言,并且具有版本向后兼容的特性,通常我们将COM对象成为接口,就像使用C++的类一样使用。

  • 在编写程序的过程中,我们必须通过特定的函数或其他COM接口方法来获取执行COM接口的指针,而不是new一个新的COM接口

  • 使用完毕后必须调用Release方法来释放它(所有的COM都继承与Iunknown接口,而Release是IUnknown接口的方法),不能使用delete,因为COM对象自身内部实现了所有的内存管理工作。

IUnknown接口类

  • DirectX11的API是由一系列的COM组件来管理的,这些前缀带I的接口类最终都继承自IUnknown接口类。IUnknown的三个方法如下:
IUnknown::AddRef
    内部引用计数加1。在每次复制了一个这样的指针后,应当调用该方法以保证计数准确性
IUnknown::QueryInterface
    查询该实例是否实现了另一个接口,如果存在则返回该接口的指针,并且对该接口的引用计数加1
IUnknown::Release
    内部引用数减1。只有当内部引用数到达0时才会真正释放
  • 实际的使用情况来看,通常我们几乎不会使用第一个方法。而用的最多的就是第三个方法了,每次用完该实例后,我们必须要使用类似下面的宏来释放接口指针:(忘了释放会报内存泄漏)
    • define ReleaseCOM(x) { if(x){ x->Release(); x = nullptr; } }

纹理和数据资源格式

  • 2D 纹理(texture)是一种数据元素矩阵。2D 纹理的用途之一是存储 2D 图像数据,在纹理的每个元素中存储一个像素颜色。

    • 但这不是纹理的唯一用途;例如, 有一种称为法线贴图映射(normal mapping)的高级技术在纹理元素中存储的不是颜色,而是 3D 向量。
    • 因此,从通常意义上讲,纹理用来存储图像数据,但是在实际应用中纹理可以有更广泛的用途。
  • 1D 纹理类似于一个 1D 数据元素数组

  • 3D 纹理类似于一个 3D 数据元素数组。纹理不仅仅是一个数据数组;纹理可以带有多级渐近纹理层(mipmaplevel),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

交换链与页面翻转

  • 双缓存

    • 为了避免在动画中出现闪烁,最好的做法是在一个离屏(off-screen)纹理中执行所有的动画帧绘制工作
    • 后台缓冲区(back buffer)
      • 我们在后台缓冲区中完成给定帧的绘制工作后,便可以将后台缓冲区作为一个完整的帧显示在屏幕上;使用这种方法,用户不会察觉到帧的绘制过程,只会看到完整的帧。从理论上讲,将一帧显示到屏幕上所消耗的时间小于屏幕的垂直刷新时间。
    • 前台缓冲区(front buffer)
      • 前台缓冲区存储了当前显示在屏幕上的图像数据,而动画的下一帧会在后台缓冲区中执行绘制。当后台缓冲区的绘图工作完成之后,前后两个缓冲区的作用会发生翻转:后台缓冲区会变为前台缓冲区,而前台缓冲区会变为后台缓冲区,为下一帧的绘制工作提前做准备。
  • 呈现(presenting)

    • 前后缓冲功能互换的过程。提交过程很快,因为只是将前后缓冲区的指针进行交换。
  • 交换链(swap chain)

    • 前后缓冲区形成一个交换链 ( swap chain )。
    • 在 Direct3D 中 , 交 换 链 由IDXGISwapChain 接口表示。
    • 该接口保存了前后缓冲区纹理,并提供了用于调整缓冲区尺寸的方法(IDXGISwapChain::ResizeBuffers)和呈现方法(IDXGISwapChain::Present)。
  • 使用(前后)两个缓冲区称为双缓冲(double buffering)。缓冲区的数量可多于两个;

  • 比如,当使用三个缓冲区时称为三缓冲(triple buffering)。不过,两个缓冲区已经足够用

  • 后台缓冲区存储的是一个纹理(texel),但是我们更习惯称之为像素(pixel),因为后台缓冲区存储的是颜色信息,及时纹理中存储的不是颜色信息,人们还是会将其称之为像素((如法线贴图像素)

深度缓冲区

  • 深度缓存 - depth buffer

    • 一般用于计算物体的遮挡,不存像素的颜色,而是存储像素的深度信息

    • 深度缓冲区(depth buffer)是一个不包含图像数据的纹理对象。在一定程度上,深度信息可以被认为是一种特殊的像素。常见的深度值范围在 0.0 到 1.0 之间,其中 0.0 表示离观察者最近的物体,1.0 表示离观察者最远的物体

    • 深度缓冲区中的每个元素与后台缓冲区中的每个像素一一对应(即,后台缓冲区的第 ij 个元素对应于深度缓冲区的第 ij 个元素)。所以,当后台缓冲区的分辨率为 1280×1024 时,在深度缓冲区中有 1280×1024 个深度元

    • 使用

      • 一个简单的场景,其中一些物体挡住了它后面的一些物体的一部分区域。为了判定物体的哪些像素位于其他物体之前,Direct3D 使用了一种称为深度缓存(depthbuffering)或 z 缓存(z-buffering)的技术。我们所要强调的是在使用深度缓存时,我们不必关心所绘物体的先后顺序,;;离得近的物体会覆离得远的物体,

      • 在渲染前,后台缓冲区清空为一个颜色(黑或者白),把深度缓冲区清空为无限远1.0,当多个深度缓冲投影到同一个像素点,离观察者最近的会覆盖其他的像素

    • 在光栅化三角形时,计算各像素的插值深度,在渲染前,将此深度与深度缓存中该像素的深度值比较,如果新的深度比现有的摄像机更远,则丢弃新的像素,否则像素的颜色被写入缓存,并用更新深度缓存。

  • 深度缓冲区用途

    • 为每个像素计算深度值和进行深度测试。
  • 深度测试

    • 深度测试通过比较像素深度来决定是否将该像素写入后台缓冲区的特定像素位置。只有离观察者最近的像素才会胜出,成为写入后台缓冲区的最终像素
  • 深度缓冲区也是一个纹理,创建时需要指定数据格式

    • 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]

  • 模板缓冲区对于应用程序不是必须的,是一个比较高级的主题,用到的话必定与深度缓冲区存储在一起。

纹理资源视图

  • 纹理可以被绑定到渲染管线(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 be60 / 529
      • created of a Resource that did not specify D3D10_BIND_DEPTH_STENCI

多重采样

  • 计算机显示器上的像素分辨率有限,所以当我们绘制一条任意直线时,该直线很难精确地显示在屏幕上,就会产生阶梯(锯齿,aliasing)效应

    • 当使用像素矩阵近似地表示一条直线时就会出现这种现象类似的锯齿也会发生在三角形的边缘上
  • 可以通过提高显示屏的分辨率,缩小像素的尺寸,是的阶梯效应明显降低。

  • 而也可以采用抗锯齿技术(antialiasing)

    • 超级采样(supersampling)

      • 把后台缓冲和深度缓冲的大小提高到屏幕分辨率的4倍,3D 场景会以这个更大的分辨率渲染到后台缓存中,当在屏幕上呈现后台缓冲时,后台缓冲会将 4 个像素的颜色取平均值后得到一个像素的最终颜色。从效果上来说,超级采样的工作原理就是以软件的方式提升分辨率
      • 缺点:超级采样代价昂贵,因为它处理的像素数量和所需的内存数量增加为原来的 4 倍
    • 多重采样(multisampling)

      • 它通过对一个像素的子像素进行采样计算出该像素的最终颜色,比超级采样节省资源。假如我们使用的是 4X 多重采样(每个像素采样 4 个邻接像素),多重采样仍然会使用屏幕分辨率 4 倍大小的后台缓冲和深度缓冲,但是,不像超级采样那样计算每个子像素的颜色,而是只计算像素中心颜色一次,然后基于子像素的可见性(基于子像素的深度/模板测试)和范围(子像素中心在多形之外还是之内)共享颜色信息
    • 关键区别

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

Direct3Dd的多重采样

  • DXGI_SAMPLE_DESC 结构体
typedef struct DXGI_SAMPLE_DESC {
UINT Count;       //指定每个像素的采样数量
UINT Quality;     //指定希望得到的质量级别,质量级别越高,占用的系统资源就越多
} DXGI_SAMPLE_DESC, *LPDXGI_SAMPLE_DESC;
  • 通过指定纹理格式和采样数量来查询相应的质量级别
    • 成功返回响应的质量等级数值 0 ~ pNumQualityLevels-1
    • 失败返回0
HRESULT ID3D11Device::CheckMultisampleQualityLevels(
DXGI_FORMAT Format, UINT SampleCount, UINT *pNumQualityLevels);

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

  • 我们需要为交换链缓冲区和深度缓冲区各填充一个 DXGI_SAMPLE_DESC 结构体。当创建后台缓冲区和深度缓冲区时,必须使用相同的多重采样设置。

特征等级

  • Direct3D11提出了特征等级(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 功能的相应的等级(每个特征等级支持的功能可参见 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 初始化方法中,方法会输出数组中第一个可被支持的特征等级。
    • 例如,如果 Direct3D 报告数组中第一个可被支持的特征等级是D3D_FEATURE_LEVEL_10_0,程序就会禁用 Direct3D 11 和 Direct3D 10.1 的特征,而使用 Direct3D 10 的绘制路径。

Direct3D初始化

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

第一步:创建设备(Device)和上下文(Context)

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

  • 具体而言:

    • ID3D11Device 接口用于检测显示适配器功能和分配资源。
    • ID3D11DeviceContext 接口用于设置管线状态、将资源绑定到图形管线和生成渲染命令。
  • 设备和上下文可用如下函数创建

HRESULT D3D11CreateDevice (
IDXGIAdapter *pAdapter,                   // 指定要为哪个物理显卡创建设备对象。当该参数设为空值时,表示使用主显卡。
D3D_DRIVER_TYPE DriverType,             // 该参数总是指定为 D3D_DRIVER_TYPE_HARDWARE,表示使用 3D 硬件来加快渲染速度。但是,也可以有用软件模拟硬件的其他选择
HMODULE Software ,                        //用于支持软件光栅化设备(software rasterizer),一般设置为0,因为我们使用硬件渲染。
UINT Flags ,             //可选的设备创建标志,以release模式生成程序是参数设置为0,以debug模式生成程序时,参数设置为D3D11_CREATE_DEVICE_DEBUG:用以激活调试层
CONST D3D_FEATURE_LEVEL *pFeatureLevels , //D3D_FEATURE_LEVEL 数组,元素的顺序表示要特征等级的测试顺序
UINT FeatureLevels ,                      //pFeatureLevels 数组中的元素 D3D_FEATURE_LEVELs 的数量
UINT SDKVersion,                          //始终设为 D3D11_SDK_VERSION
ID3D11Device **ppDevice ,                 //返回创建后的设备
D3D_FEATURE_LEVE L *pFeatureLevel,        //返回pFeatureLevels数组中第一个支持的等级,null则返回最高等级
ID3D11DeviceContext **ppImmediateContext  // 返回创建的设备上下文
);
  • 上下文用于绘制
    • 上下文用于发出渲染命令并配置渲染管道
    • 分类
      • 即时的
        • 让硬件立马执行渲染工作,帮忙发出渲染命令,设置管道以及查询驱动程序
      • 延迟的
        • 建立指令集并把执行发布到即时上下文,在多线程下表现良好,唯一延迟上下文做不到的就是查询图形驱动程序,因为只会建立以下命令列表在将来的某个时间执行
      • 多线程编程
        • 在主线程中使用立即执行上下文
        • 在工作线程总使用延迟执行上下文
          • 每个工作线程可以将图形指令记录在一个命令列表(ID3D11CommandList)中
          • 随后,每个工作线程中的命令列表可以在主渲染线程中加以执行
        • 在多核系统中,可并行处理命令列表中的指令,这样可以缩短编译复杂图形所需的时间
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;
}

第二步:检测4X多重采样质量支持

  • 创建了设备后,我们就可以检查 4X 多重采样质量等级了。所有支持 Direct3D 11 的设备都支持所有渲染目标格式的 4X MSAA(支持的质量等级可能并不相同)
UINT m4xMsaaQuality;
HR(md3dDevice ->CheckMultisampleQualityLevels(
  DXGI_FORMAT_R8G8B8A8_UNORM, 4, & m4xMsaaQuality));
assert(m4xMsaaQuality > 0)

第三步:描述交换链

  • 创建交换链前,需要填充一个DXGI_SWAP_CHAIN_DESC结构体来描述交换链的特性(dxgi.h)
typedef struct DXGI_SWAP_CHAIN_DESC {
  DXGI_MODE_DESC   BufferDesc;      //要创建的后备缓冲显示的属性
  DXGI_SAMPLE_DESC SampleDesc;      //多重采样数量和质量级别
  DXGI_USAGE       BufferUsage;     //设为 DXGI_USAGE_RENDER_TARGET_OUTPUT,因为我们要将场景渲染到后台缓冲区(即,将它用作渲染目标)
  UINT             BufferCount;     //交换链中的后台缓冲区数量;我们一般只用一个后台缓冲区来实现双缓存
  HWND             OutputWindow;    //将要渲染到的窗口的句柄
  BOOL             Windowed;        //当设为 true 时,程序以窗口模式运行;当设为 false 时,程序以全屏(full-screen)模式运行。
  DXGI_SWAP_EFFECT SwapEffect;      //设为 DXGI_SWAP_EFFECT_DISCARD,让显卡驱动程序选择最高效的显示模式
  UINT             Flags;           //可选的标志,如果设为DXGI_SWAP_CHAIN_FLAG_ALLOW_MODE_SWITCH,那么当应用程序切换到全屏模式时,Direct3D 会自动选择与当前的后台缓冲区设置最匹配的显示模式
} DXGI_SWAP_CHAIN_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_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 并不会输出在显示器上,但在后台缓冲区中可以用于特定的用途

第三步:创建交换链

  • 交 换 链 ( 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 isbeing called with a device from a different IDXGIFactory.
    • 要修复这个错误,我们需要使用创建设备的那个 IDXGIFactory 实例,要获得这个实例,必须使用下面的 COM 查询
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 可以在后台缓冲区上执行渲染工作),我们必须为后台缓冲区创建一个渲染目标视图(render target view)
ID3D11RenderTargetView* mRenderTargetView;
ID3D11Texture2D* backBuffer;
//于获取一个交换链的后台缓冲区指针
mSwapChain->GetBuffer(0,                    // 第一个参数为看后台缓冲区的索引(当后台缓冲区数量大于1时需要制指定索引)
  __uuidof(ID3D11Texture2D),                // 缓冲区的接口类型,通常是一个2D纹理
  reinterpret_cast<void**>(&backBuffer));   // 返回指向缓冲区的指针

//法创建渲染目标视图
md3dDevice->CreateRenderTargetView(
  backBuffer,           //指定了作为渲染目标的资源(此处是指后台缓冲区,即为它创建一个渲染目标视图)
  0,                    //D3D11_RENDER_TARGET_VIEW_DESC 结构体的指针,该结构体描述了资源中的元素的数据类型,当使用的是强类型格式则可以为空
  &mRenderTargetView);  //通过指针返回了创建后的渲染目标视图对象。

//释放后台缓冲区,每调用一次 IDXGISwapChain::GetBuffer 方法,后台缓冲区的 COM 引用计数就会向上递增一次,在代码片段的结尾处释放它(ReleaseCOM)。
ReleaseCOM(backBuffer);

第六步:创建深度/模板缓冲区及其视图

  • 深度缓冲区只是一个存储深度信息的 2D 纹理(如果使用模板,则模板信息也在该缓冲区中)。

  • 要创建纹理,我们必须填充一 个 D3D11_TEXTURE2D_DESC 结 构 体 来 描 述 所 要 创 建 的 纹 理 , 然 后 再 调 用ID3D11Device::CreateTexture2D 方法。

typedef struct D3D11_TEXTURE2D_DESC {
UINT Width;                   //纹理的宽度,单位为纹理元素(texel)
UINT Height;                  //纹理的高度,单位为纹理元素(texel)
UINT MipLevels;               //多级渐近纹理层(mipmap level)的数量
UINT ArraySize;               //在纹理数组中的纹理数量
DXGI_FORMAT Format;           //一个 DXGI_FORMAT 枚举类型成员,它指定了纹理元素的格式
DXGI_SAMPLE_DESC SampleDesc;  //多重采样数量和质量级别
D3D10_USAGE Usage;            //表示纹理用途的 D3D11_USAGE 枚举类型成员
UINT BindFlags;               // 指定该资源将会绑定到管线的哪个阶段。对于深度/模板缓冲区,该参数应设为 D3D11_BIND_DEPTH_STENCIL      
UINT CPUAccessFlags;          //指定 CPU 对资源的访问权限
UINT MiscFlags;               //可选的标志值,与深度/模板缓冲区无关,所以设为 0
} D3D11_TEXTURE2D_DESC;
  • D3D10_USAGE Usage;- 表示纹理用途的 D3D11_USAGE 枚举类型成员。有 4 个可选值:

    • D3D11_USAGE_DEFAULT:表示 GPU(graphics processing unit,图形处理器)会对资源执行读写操作。CPU 不能读写这种资源。
    • D3D11_USAGE_DEFAULT:表示 GPU(graphics processing unit,图形处理器)会对资源执行读写操作。CPU 不能读写这种资源。
    • D3D10_USAGE_DYNAMIC:表示应用程序(CPU)会频繁更新资源中的数据内容(例如,每帧更新一次)。GPU 可以从这种资源中读取数据,而 CPU 可以向这种资源中写入数据
    • D3D10_USAGE_STAGING:表示应用程序(CPU)会读取该资源的一个副本(即,该资源支持从显存到系统内存的数据复制操作)。
  • UINT BindFlags -- 指定该资源将会绑定到管线的哪个阶段。对于深度/模板缓冲区,该参数应设为 D3D11_BIND_DEPTH_STENCIL

    • D3D11_BIND_RENDER_TARGET:将纹理作为一个渲染目标绑定到管线上。
    • D3D11_BIND_SHADER_RESOURCE:将纹理作为一个着色器资源绑定到管线上。
  • UINT CPUAccessFlags -- 指定 CPU 对资源的访问权限

    • D3D11_CPU_ACCESS_WRITE - CPU 需要向资源写入数据
    • D3D11_USAGE_DYNAMIC 或 D3D11_USAGE_STAGIN - 具有写访问权限的资源
    • D3D11_CPU_ACCESS_READ - CPU 需要从资源读取数据
    • D3D11_USAGE_STAGING - 具有读访问权限的资源
  • 注意

    • 推荐避免使用 D3D11_USAGE_DYNAMIC 和 D3D11_USAGE_STAGING,因为有性能损失。
    • 要获得最佳性能,我们应创建所有的资源并将它们上传到 GPU 并保留其上,只有 GPU 在读取或写入这些资源。但是,在某些程序中必须有 CPU 的参与,因此这些标志无法避免,但你应该将这些标志的使用减到最小。
  • 了如何创建深度/模板纹理以及与它对应的深度/模板视图:

D3D11_TEXTURE2D_DESC depthStencilDesc;

depthStencilDesc.Width = mClientWidth;
depthStencilDesc.Height = mClientHeight;
depthStencilDesc.MipLevels = 1;
depthStencilDesc.ArraySize = 1;
depthStencilDesc.Format =71 / 529
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;

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

//第二个参数是一个指向D3D11_DEPTH_STENCIL_VIEW_DESC 的指针。这个结构体描述了资源中这个元素数据类型(格式)。
//指定了深度/模板缓冲的格式,所以将这个参数设置为空值
HR(md3dDevice->CreateDepthStencilView(
  mDepthStencilBuffer, 0, &mDepthStencilView));

第七步:将视图绑定到输出合并器阶段

  • 输出合并器阶段(output merger stage),
    • 将视图绑定到管线,使资源成为管线的渲染目标和深度/模板缓冲区
md3dImmediateContext->OMSetRenderTargets(
  1,                  //将要绑定的渲染目标的数量
  &mRenderTargetView, //将要绑定的渲染目标视图数组中的第一个元素的指针
  mDepthStencilView   //将要绑定到管线的深度/模板视图
);

第八步:设置视口

  • 通常我们会把 3D 场景渲染到整个后台缓冲区上。不过,有时我们只希望把 3D 场景渲染到后台缓冲区的一个子矩形区域中,后台缓冲区的子矩形区域称为视口(viewport)

  • 视口描述结构体

typedef struct D3D11_VIEWPORT {
FLOAT TopLeftX;     //前 4 个数据成员定义了相对于窗口客户区的视口矩形范围
FLOAT TopLeftY;
FLOAT Width;
FLOAT Height;
FLOAT MinDepth;     //表示深度缓冲区的最小值,Direct3D 使用的深度缓冲区取值范围是0到1,除非你想要得到一些特殊效果,否则应将 MinDepth 和 MaxDepth分别设为0和1。
FLOAT MaxDepth;     //表示深度缓冲区的最小值
} D3D11_VIEW
  • 创建视口
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,    //绑定的视图的数量(可以使用超过 1 的数量用于高级的效果)
  &vp   //数指向一个 viewports 的数组
);
  • 使用样例
    • 你可以使用视口来实现双人游戏模式中的分屏效果。创建两个视口,各占屏幕的一半,一个居左,另一个居右。然后在左视口中以第一个玩家的视角渲染 3D 场景,在右视口中以第二个玩家的视角渲染 3D 场景。你也可以使用视口只绘制到屏幕的一个子矩形中,而在其他区域保留诸如按钮、列表框之类的 UI 控件

计时和动画

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

性能计时器

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

  • 时间单位称为计数(count),获取计数器测量的当前时间值:

__int64 currTime;
QueryPerformanceCounter((LARGE_INTEGER*)&currTime); //返回当前时间值,参数是一个64位的整数
  • 每次计数的时间长度等于频率的倒数,获取性能计时器的频率(每秒的计数次数)
__int64 countsPerSec;
QueryPerformanceFrequency((LARGE_INTEGER*)&countsPerSec)
  • 将时间读数转为秒,需要乘以转换因子mSecondsPerCount
valueInSecs = valueInCounts * mSecondsPerCount;
  • 计算一段代码的所花销的计数时间,可以在代码执行前后调用QueryPerformanceCounter获取当前时间值A,B,计数时间为(B-A)

游戏计时器类


  • 计算帧的时间间隔的函数Tick()在应用程序中的应用
int D3DApp::Run()
{
  MSG msg = {0};
  mTimer.Reset();
  while(msg.message != WM_QUIT)
  {
    // 如果接收到 Window 消息,则处理这些消息
    if(PeekMessage( &msg, 0, 0, 0, PM_REMOVE ))
    {
      ranslateMessage( &msg );
      DispatchMessage( &msg );
    }
    // 否则,则运行动画/游戏
    else
    { 
      mTimer.Tick();
      if( !mAppPaused )
      {
        CalculateFrameStats();
        //通过这一方式,每帧都会计算出一个Δt 并将它传送给 UpdateScene 方法,根据当前帧与前一帧之间的时间间隔来更新场景
        UpdateScene(mTimer.DeltaTime());
        DrawScene();
      }
      else
      {
        Sleep(100);
      }
    }
  }
  return (int)msg.wParam;
}

游戏时间

  • 游戏时间(game time):

    • 另一个需要测量的时间是从应用程序开始运行时起经过的时间总量,其中不包括暂停时间。
    • 用途
      • 假设玩家有 300 秒的时间来完成一个关卡。当关卡开始时,我们会获取时间 tstart,它是从应用程序开始运行时起经过的时间总量。当关卡开始后,我们不断地将 tstart与总时间 t 进行比较。如果 t – tstart >300(如图 4.8 所示),就说明玩家在关卡中的用时超过了 300 秒,输掉了这一关。很明显,在一情景中我们不希望计算游戏的暂停时间。
      • 游戏时间的另一个用途是通过时间函数来驱动动画运行。例如,我们希望一个灯光在时间函数的驱动下环绕着场景中的一个圆形轨道运动。灯光位置可由以下参数方程描述:x = 10 cost,y = 20,z = 10 sint。这里 t 表示时间,随着 t(时间)的增加,灯光的位置会发生改变,使灯光在平面 y = 20上围绕着半径为 10 的圆形轨道运动。对于这种类型的动画,我们也不希望计算游戏的暂停时间
  • 实现

    • 记录游戏开始时间起的总量,每次暂停都计算当前暂停的时间(恢复时间-暂定时间),然后累积暂停时间
#include<windows.h>

#include "GameTimer.h"

/*
  构造函数:计算每次计数的时间长度,初始化操作
*/
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;
}

/*
  计算帧之间的间隔时间;
    通过计算当前帧的计数事件,从而得出与上一帧的时间间隔,对于实时渲染,每秒30帧的频率才能得到比较平滑的动画效果
*/
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;
}

/*
  重置性能计时器
*/
void GameTimer::Reset()
{
  __int64 currTime;
QueryPerformanceCounter((LARGE_INTEGER*)&currTime);
mBaseTime = currTime;
//mPrevTime 被初始化为当前时间。这一点非常重要,因为对于动画的第一帧来说,没有前面的那一帧,也就是说没有前面的时间戳。所以个值必须在消息循环开始之前初始化。
mPrevTime = currTime;
mStopTime = 0;
mStopped = false;
}
/*
暂停计数器
*/
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;
	}
}

// 返回自调用 Reset()方法之后的总时间,不包含暂停时间
float GameTimer::TotalTime()const
{
	// 如果处在暂停状态,则无需包含自暂停开始之后的时间。
	// 此外,如果我们之前已经有过暂停,则 mStopTime - mBaseTime 会包含暂停时间, 我们不想包含这个暂停时间,
	// 因此还要减去暂停时间:
	if (mStopped)
	{
		return (float)(((mStopTime -mPausedTime) - mBaseTime) * mSecondsPerCount);
	}
	// mCurrTime - mBaseTime 包含暂停时间,而我们不想包含暂停时间
	// 因此我们从 mCurrTime 需要减去 mPausedTime
	else
	{
		return (float)(((mCurrTime - mPausedTime) - mBaseTime) * mSecondsPerCount);
  }
}
  • 注意:我们的演示框架创建了一个 GameTimer 实例用于计算应用程序开始后的总时间和两帧之间的时间;你也可以创建额外的实例作为通用的秒表使用。例如,当点着一个炸弹时,你可以启动一个新的 GameTimer,当 TotalTime 达到 5 秒时,你可以引发一个事件让炸弹爆炸

程序框架

D3DAPP

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

框架代码

  • D3DApp 类实现的这种结构可以将所有的初始化代码、消息处理代码和其他代码安排得井井有条,使派生类专注于实现演示程序的特定代码

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

    • 为你的后续初始化代码提供一个可用的 ID3D11Device 设备对象。(通常在获取
      Direct3D 资源时都要传递一个有效的 ID3D11Device 设备对象。)
{
if(!D3DApp::Init())
return false;
/* 剩下的初始化代码从这里开始 */
}
  • OnResize:该方法在 D3DApp::MsgProc 收到 WM_SIZE 消息时调用。

    • 当窗口的尺寸改变时,一些与客户区大小相关的 Direct3D 属性也需要改变。
      • 尤其是需要重新创建后台缓冲区和深度/模板缓冲区,使它们与窗口客户区的大小一致。后台缓冲区的大小可以通过调用 IDXGISwapChain::ResizeBuffers 方法来进行调整。
      • 而深度/模板缓冲区必须被销毁,然后根据新的大小重新创建。
      • 另外,渲染目标视图和深度/模板视图也必须重新创建。
    • OnResize 方法在 D3DApp 的实现中包含了调整后台缓冲区和深度/模板缓冲区的代码;除缓冲区外,依赖于客户区大小的其他属性(例如,投影矩阵)也必须
      重新创建。我们把该方法作为框架的一部分是因为当窗口大小改变时,客户代码可能需要执行一些它自己的逻辑。
  • UpdateScene:该抽象方法每帧都会调用,用于随着时间更新 3D 应用程序(例如,实现动画和碰撞检测、检查用户输入、计算每秒帧数等等)。

  • DrawScene:该抽象方法每帧都会调用,用于将 3D 场景的当前帧绘制到后台缓冲区。

    • 当绘制当前帧时,我们调用了 IDXGISwapChain::Present 方法将后台缓冲区的内容呈现在屏幕上。
  • 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)坐标。

帧的统计数值

  • 通常游戏和绘图应用程序都要测量每秒的渲染帧数(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++;
  // 程序运行时间超过1秒计算一秒时间内的平均值
  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 更有用,因为它可以更直观地反映出由于修改场景而产生的渲染时间变化(增加或减少)。另一方面,FPS 无法反映出这一变化。

消息处理函数

  • 与整个应用程序框架相比微不足道。通常,我们不会用到许多 Win32 消息。其实,我们的应用程序的核心代码会在处理器空闲执行(即,当没
    有窗口消息执行)。不过,有一些重要的消息我们必须处理。

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

    • 当应用程序失去焦点时,我们将数据成员 mAppPaused 设为 true,当应用程序获得焦点时,我们将数据成员 mAppPaused 设为 false。
    • 另外,当应用程序暂停时,计时器停止运行,当应用程序再次激活时,计时器恢复运行。
    • D3DApp::Run方法,我们会发现当应用程序暂停时,我们并没有执行应用程序中的更新 3D 场景的代码,而是将空闲的 CPU 周期返回给了操作系统;通过这一方式,应用程序不会在处于非活动状态时独占 CPU
// 当窗口被激活或非激活时会发送 WM_ACTIVATE 消息。
// 当非激活时我们会暂停游戏,当激活时则重新开启游戏。
case WM_ACTIVATE:
  if( LOWORD(wParam) == WA_INACTIVE )
  {
    mAppPaused = true;
    mTimer.Stop();
  }
  else
  {
  mAppPaused = false;
  mTimer.Start();
  }
  return 0
  • 第二个消息是 WM_SIZE。该消息在改变窗口大小时发生。我们处理该消息的主要原因是希望后台缓冲区和深度/模板缓冲区的大小与窗口客户区的大小相同(为了不出现图像拉伸)。所以,每次改变窗口大小时,我们希望同改变缓冲区的大小。这一任务由D3DApp::OnResize 方 法 实 现 。
    • 如 前 所 述 , 后 台 缓 冲 区 的 大 小 可 以 通 过 调 用IDXGISwapChain::ResizeBuffers 方法来进行调整。
    • 而深度/模板缓冲区必须被销毁,然后根据新的大小重新创建。另外,渲染目标视图和深度/模板视图也必须重新创建。
    • 当用户拖动窗口边框时,我们必须格外小心,因为此时会有接连不断的 WM_SIZE 消息发出,我们不希望连续地调整缓冲区大小。所以,当用户拖动窗口边框时,我们(除了暂停应用程序外)不应该执行任何代码,等到用户的拖动操作结束之后我们再调整缓冲区的大小。我们通过处理WM_EXITSIZEMOVE 消息来完成一工作。该消息在用户释放窗口边框时发送。
// 当用户拖动窗口边框时会发送 WM_EXITSIZEMOVE 消息。
case WM_ENTERSIZEMOVE:
  mAppPaused = true;
  mResizing = true;88 / 529
  mTimer.Stop();
  return 0;
// 当用户是否窗口边框时会发送 WM_EXITSIZEMOVE 消息。
// 然后我们会基于新的窗口大小重置所有图形变量
case WM_EXITSIZEMOVE:
  mAppPaused = false;
  mResizing = false;
  mTimer.Start();
  OnResize();
  return 0;

// 窗口被销毁时发送 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;

全屏模式

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

初始化 Direct3D 演示程序

#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);
  md3dImmediateContext->ClearRenderTargetView(mRenderTargetView,
  reinterpret_cast<const float*>(&Colors::Blue));
  md3dImmediateContext->ClearDepthStencilView(mDepthStencilView,
  D3D11_CLEAR_DEPTH|D3D11_CLEAR_STENCIL, 1.0f, 0);91 / 529
  HR(mSwapChain->Present(0, 0));
}

调试Direct3D应用程序

  • 实现了一个宏,用它来检查许多 Direct3D 函数返回的 HRESULT 值
#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)
  • 该函数可以弹出一个消息框,显示出现错误的文件、行号、有关错误的描述信息以及导致错误的函数名。

    • 当 DXTrace 函数的最后一个参数设为 false时,该函数不会显示消息框,而是把调试信息输出到 Visual C++的输出窗口。
    • 当我们不使用调试模式时,HR 宏不执行任何代码。另外,HR 必须是一个宏而不能是一个函数;否则__FILE__和__LINE__将无法引用调用 HR 宏的函数所在的文件和行
  • 使用 HR 宏来包围返回 HRESULT 值的 Direct3D函数

HR(D3DX11CreateShaderResourceViewFromFile(md3dDevice,L"grass.dds", 0, 0, &mGrassTexRV, 0 ));
  • 当我们调试演示程序时,这个宏可以很好地运作,但是对于一个实际的应用程序来说,我们应该使用更完善的错误处理机制。

  • 注意:L#x 将 HR 宏的参数转换成一个 Unicode 字符串。通过这一方式,我们可以把导致错误的函数调用语句输出到消息框上面

总结

  1. Direct3D 可以被视为程序员和图形硬件之间的一个中介。例如,程序员调用 Direct3D函数将资源视图绑定到硬件渲染管线、设定渲染管线的输出并绘制 3D 几何体。

  2. 在 Direct3D 11 中,一个支持 Direct3D 11 的图形设备必须支持 Direct3D 11 规定的整个功能集合以及少量的额外功能。

  3. 组件对象模型(COM)技术使 DirectX 独立于任何编程语言,并具有版本向后兼容的特性。Direct3D 程序员不必知道 COM 的实现细节及工作方式;只需要知道如何获取和释放 COM 接口即可

  4. 1D 纹理如同一维数据元素数组,2D 纹理如同二维数据元素数组,3D 纹理如同三维数据元素数组。纹理元素的格式由 DXGI_FORMAT 枚举类型成员描述。纹理通常用于存储图像数据,但是也可以用于存储其他数据,比如深度信息(例如,深度缓冲区)。GPU 可以在纹理上执行特殊运算,比如过滤器和多重采样。

  5. 在 Direct3D 中,资源不能被直接绑定到一个管线阶段;我们只能把与资源关联的资源视图绑定到不同的管线阶段。我们可以为一个资源创建多个不同的视图。通过这一方式,一个资源可以被绑定到多个不同的渲染管线阶段。如果在创建资源时使用的是弱类型格式,那么在为该资源创建视图时必须指定明确的类型。

  6. ID3D11Device 和 ID3D11DeviceContext 接口可以被视为物理图形设备硬件的软控制器;也就是,我们可以通过这些接口与硬件进行交互。ID3D11Device 接口负责检查硬件支持的功能、分配资源。ID3D11DeviceContext 接口负责设置渲染状态,将资源绑定到图形管线,发送渲染指令。

  7. 为了避免动画出现闪烁,最好是将整个帧绘制到一个叫做后台缓冲区的离屏纹理中。当整个屏幕绘制到后台缓冲区之后,它就会以一个完整帧的形式呈现在屏幕上,通过这种方式,观察者就不会觉察到图像的绘制过程了。当帧绘制到后台缓冲区之后,后台缓冲和前台缓冲就会发生互换:后台变前台,前台变后台。交换两者的过程叫做呈现(presenting)。前台缓冲和后台缓冲构成一个交换链,由 IDXGISwapChain 接口表示,使用两个缓冲被称为双缓冲。

  8. 对于屏幕上不透明的物体来说,离相机近的点会遮挡后面的点。深度缓冲就是一种判断哪个点离相机近的技术。通过这一方式,我们就无需关心对象的绘制顺序。

  9. 性能计数器是一种高精度计时器,它为测量微小的时间差提供了准确无误的计时测量 方 法 , 比 如 帧 之 间 的 时 间 间 隔 。 性 能 计 时 器 采 用 的 时 间 单 位 叫 做 计 数 。QueryPerformanceFrequency 函数用于输出性能计时器每秒的计数值,这个值的单位可以从计数转换为秒。我们可以通过 QueryPerformanceCounter 函数获取性能计时器的当前时间。

  10. 我们通过累计某一时间段Δt 内的帧数来计算 FPS(frames per second,每秒帧数)。设 n 为时间段Δt 中的帧数;在一段时间中的平均每秒帧数为 fpsavg=n/Δt。帧速率会对性能评定产生误导;帧时间是更有效的信息。以秒为单位的帧时间等于帧速率的倒数,即 1/fpsavg。

  11. 示例框架用于为本书的所有演示程序提供统一的编程接口。这些代码保存在d3dUtil.h、d3dApp.h 和 d3dApp.cpp 文件中,它们封装了每个应用程序必须实现的标准初始化代码。通过封装些代码,可以使示例程序更专注于所要演示的技术。

  12. 当以调试模式生成程序时,我们可以使用 D3D11_CREATE_DEVICE_DEBUG 标志值创建 Direct3D 设备来启用调试层。在指定了调试标志值后,Direct3D 会把调试信息发送到 VC++的输出窗口。另外,当以调试模式生成程序时,我们应使用 D3DX 库的调试版本(即d3dx11d.lib)