186
DirectX 11 游戏编程入门 Allen Sherrod and Wendy Jones 致谢:略 作者简介: AllenSherrod 是一个在计算机游戏领域富有经验的作者。 Allen 过去的工作包括出版两本书《DX 终极 3D 游戏引擎的架构和 设计之终极游戏开发指南》和《游戏开发者的数据结构和算法》。 Allen 也对游戏开发者杂志,《游戏编程精粹 6》和 Gamasutra.com 有所贡献。Allen 还创建了 UltimateGameProgramming.com 网站。 WendyJones KittyCode LLC 公司的 CTO 和创立者之一,这家游戏工作站主要在移动平台,诸如 iPhoneWP7 Xbox360 上开发。Wendy 过去在 Atari Electronic Arts 的工作经验包括在 PC,操纵台(译者注:就是手摇式和手柄式游戏机)和移动设备 上的游戏开发。 Wendy 也在 Full Sail 大学教授 DX,并且是人机交互部门的主席。可以通过她的网站 www.fasterkittycodecode.com 网站联系她。 译者:[email protected] 时间:2013.05.10

DirectX 11 游戏编程入门 - read.pudn.comread.pudn.com/downloads727/ebook/2906804/DX11GameProgram.pdf · DirectX 11 游戏编程入门 Allen Sherrod and Wendy Jones 致谢:略

  • Upload
    others

  • View
    10

  • Download
    0

Embed Size (px)

Citation preview

DirectX 11 游戏编程入门

Allen Sherrod and Wendy Jones

致谢:略

作者简介:

AllenSherrod 是一个在计算机游戏领域富有经验的作者。Allen 过去的工作包括出版两本书《DX 终极 3D 游戏引擎的架构和

设计之终极游戏开发指南》和《游戏开发者的数据结构和算法》。Allen 也对游戏开发者杂志,《游戏编程精粹 6》和 Gamasutra.com

有所贡献。Allen 还创建了 UltimateGameProgramming.com 网站。

WendyJones 是 KittyCode LLC 公司的 CTO 和创立者之一,这家游戏工作站主要在移动平台,诸如 iPhone,WP7 和 Xbox360

上开发。Wendy 过去在 Atari 和 Electronic Arts 的工作经验包括在 PC,操纵台(译者注:就是手摇式和手柄式游戏机)和移动设备

上的游戏开发。Wendy 也在 Full Sail 大学教授 DX,并且是人机交互部门的主席。可以通过她的网站 www.fasterkittycodecode.com

网站联系她。

译者:[email protected]

时间:2013.05.10

目录

介绍

第一章 DirectX 的前世今身

DirectX 是什么?

DirectX 10 对比与 DirectX9

DirectX11 对比与 DirectX10

DirectX11 对比与 OpenGL4.0

为什么需要 DirectX?

DirectX 的组成部分

DirectX11 的组件

过时的 DirectX 组件

Direct3D 11 组件介绍

Direct3D 11 的几个阶段

Direct3D 11 的思考

DirectX 的附属工具

示例程序浏览器和文档

PIX

Caps Viewer

诊断工具

贴图工具

错误查看器

控制面板

跨平台声音创建工具

游戏定义文件编辑器

底层硬件

总结

习题

第二章 第一个 DirectX 程序

创建工程

添加窗口代码

Main 函数入口

窗口初始化

窗口回调过程

Direct3D

添加 DirectX 库

初始化 Direct3D 组件

驱动类型和特征级别

创建设备和缓存交换链 SwapChain

创建渲染目标视图

视口 Viewport

场景的清除和显示

清理 Cleaning Up

格式 Formats

空白的 D3D 窗口

模板框架的设计

Direct3D 类

空窗口演示类

程序的循环更新

DirectX 错误处理库

错误处理函数

错误处理宏

总结

习题

第三章 2D 渲染

开发 2D 游戏

贴图

精灵

2D 几何图形

什么是顶点

三角形的定义

顶点缓存 Vertex Buffers

输入布局

绘制 2D 三角形

2D 三角形演示 Demo

载入几何体

渲染几何体

贴图映射

贴图接口

MIP 映射

贴图的细节

贴图映射演示

精灵

深度缓存 Z-Ordering

精灵图像

从场景中取得精灵

精灵的位置以及缩放

精灵游戏演示

创建和渲染游戏中的精灵

总结

你已经学到的东西

习题

实验

第四章 文本和字体的渲染

游戏中的文本

增加文字

字体贴图

一个字体系统的讲解

使用精灵创建字体系统

高级话题

游戏中的文字窗 Text Box

游戏中的控制台

游戏菜单

游戏状态提示 Heads-Up Display

总结

习题

实验

第五章 输入检测和响应

玩家的输入

输入操作

键盘

Win32 键盘

DirectInput 组件的键盘

DirectInput 键盘输入演示

鼠标

DirectInput 鼠标

XInput-游戏控制器

设置 XInput 输入

控制器的摆动

XInput 中的输入

游戏控制器功能

电量检测

按键检测

耳机声音

XInput 演示

总结

已经学到的东西

习题

实验

第六章 3D 入门

XNA 数学库

实用特性

向量

向量运算

位置之间的距离

向量的长度

向量的标准化

向量的叉积

向量的点积

3D 空间

坐标系统

变换

世界变换

视图变换

投影变换

一个对象的变换

矩阵

单位矩阵

放缩矩阵

平移矩阵

旋转矩阵

矩阵间的结合

立方体 Cube 演示

附加的 XNA 数学库主题

编译检测

常量

结构和类型

增加的函数

附加的数学结构主题

游戏的物理特性和碰撞检测

总结

已经学到的东西

习题

第七章 着色器和特效

Direct3D 中的着色器

可编程着色器的历史

特效文件

特效文件的布局

载入特效文件

外部变量和常量缓存

输入输出结构

技术模块

光栅化状态

高级着色语言

变量类型

语法

函数声明

顶点着色器

像素着色器

贴图颜色的转化

颜色偏移

多重贴图

几何着色器

几何着色函数声明

几何着色器的阐述

光照介绍

总结

已经学到的东西

习题

实验

第八章 Direct3D 中的照相机和模型

Direct3D 组件中的照相机

Look-At 照相演示

Arc-Ball 照相演示

网格和模型

OBJ 文件格式

从文件中读取标记

从 OBJ 文件中载入网格模型

高级主题

照相机的复杂性

3D 分级文件

总结

你所学到的东西

习题

索引

介绍

许多人都梦想过制作属于自己的电脑游戏。对于一些人来说游戏是一种爱好,而还有一部分人却无意体会其中的乐趣。但

是对于剩下的我们,游戏和制作我们自己的游戏更多的是一种激情。如果你买了这本书,你将会跟随你的渴望和热情参与其中

并与其他人分享乐趣。

关于此书

这本书是 WendyJones 的《DirectX10 游戏编程入门》的后续版本。此书中,我们教会你使用最新的 Direct3D 版本 Direct3D 11

来开始游戏的学习。

此书的目标就是教会你 DirectX11 的方方面面。内容的适应人群是没有 DirectX 经验的初级或中级 C++开发人员。虽然前面

版本的 DirectX 或者 OpenGL 的经验有用,但这不是必须的。

当你学完本书的全部内容后,你将有足够的 DirectX11 的经验来制作简单的视频游戏和 Demo 演示。理想的情况是,你将

能够制作复杂的游戏和演示程序直到你完全能够完成和发布你自己的 PC 游戏。

推荐材料

本书假设你有关于 C++编程方面的知识,并且假定你有在 VS2010 上开发的知识以及至少是高中水平的数学知识。本书主要

是讲解 DirectX 的知识,故阅读本书前并不需要知道 DirectX。

全书使用的代码环境是 VSC++2010快速版,可以通过微软官网www.microsoft.com/express/Downloads 下载。我们使用的 June

2010 DirectX SDK 能够从 http://msdn.microsoft.com/en-us/directx 下载

尽 管 不 是 必 须 的 , 我 们 还 是 推 荐 使 用 Adobe 公 司 的 Photoshop 软 件 来 创 建 贴 图

(www.adobe.com/products/photoshop/photoshopextended/)和 Autodesk 公司的 XSI Mod Tool 7.5 来创建 3D 模型和网格

(http://usa.autodesk.com)。

伴随网站:代码和附加内容

几乎每一章每个主题的讨论都给你动手练习的示例代码和 Demo。 为了能够观察执行或者操纵那些代码,你需要从以下网

站下载本书的附加代码:www.ultimategameprogramming.com/BeginningDirectX11/ or www.courseptr.com/downloads 通过这两个

地址,你能够找到一些材料,包括第九章,总结,附录 A,各章节习题答案,附录 B,DirectX 中的声音(介绍了 DirectX 声音的

各个 API,包括 XAudio2 和 XACT3)。

勘误表

有时在本书发布之前就存在一些没有被发现的错误或者一些我们不可预料的外界因素造成的错误。如果你找到任何书中存

在的错误,请提交到这个网站上去 www.ultimategameprogramming.com/BeginningDX11。查看该网站看其他读者提交的错误对你

也许有用。

第一章

DirectX 的前世今生

DirectX 之前,游戏开发人员在各种硬件的不兼容性上苦苦挣扎,由于各种各样的硬件配置的存在使得让每个人

享受同样的游戏几乎是不可能的。为了解决工业界的标准化问题,微软在windows95上提供了windows游戏开发包,

也就是 DirectX1.0 版本。DirectX 提供给游戏制作者一个统一的 API 集合,它几乎保证了不同 PC 硬件的兼容性问题。

随着 DirectX 的发布,windows 平台下的游戏数量有了戏剧性的增长。而在 15 年前这的确是真的。

本章内容:

了解 DirectX 是什么

为什么 DirectX 那么有用

数据流是怎样通过 Direct3D 11 的管线

Direct3D 11 的新特性是什么

什么是 DirectX

DirectX,微软提供的应用程序接口集(APIs),被设计为在运行 windows 操作系统平台上用来提供给开发人员控

制硬件的底层接口。它的各个组件提供了访问不同硬件的能力,包括图形(显卡),声音(声卡),GPU,输入设备以及

所有的标准接口(译者注:比如游戏操纵杆,鼠标等)。

这种体系允许开发者使用标准 API(DirectX)开发他们的游戏,而不用担心用户提供的不同硬件的访问接口不同了。

可以想象一旦开发者要面对用户所提供的各种可能的硬件配置上写代码将是多么的困难。你也许会问:如果玩家有

不同的输入设备怎么办?又或者是有不同的操作系统,即使是同一家公司都会产生不同的版本像 DOS 和 Windows95?

那么不同的声卡和各种驱动呢?不同种类的显卡又怎么办?

相比于为市场上的各种可能的设备编写不同的代码,要是能提供一种标准 API 来操纵不同的硬件会是一个更好

的主意,特别是在游戏发布之后市场上出现了一种新的设备,如果是前一种方法则设备是不太可能被游戏识别的,

而如果提供统一的 API 则不难解决这个问题。DirectX 是首先被游戏制作者用来在 Windows 和 Xbox 平台上使用的标

准 API 集。它是在设备上提供驱动层来操作硬件的。(注意:Xbox369 使用 DirectX9 之后的版本开发)

DirectX10 与 DirectX9 的比较

在 2006 年,DirectX10 作为 DirectX 开发包的将来的主要版本被提上日程。通常讨论 DirectX 指的是它其中的组

件 Direct3D,在 DirectX 的 API 中每个版本 Direct3D 都有根本性的变化。DirectX 的其他大部分 API 都或者是过时的(这

意味着它们将在后续版本中移除或者不推荐在新的软件中使用它们),或者主版本的变化它们保持相同,或者被 SDK

完全移除。

Direct3D 10 的 API 相比于前一个版本是显得轻量的,并且其 API 比过去的版本都更加的容易使用。实际上 DirectX

的早期版本是出了名的既难学又难用,但是微软已经对其 API 进行了多年的改近和提升(译者注:微软的成功原因之

一就是不懈的坚持)。Direct3D 10 并不仅仅是升级,而是在各个方面,其 API 都有新的不同。其中一个新的不同的感

受就是它在 Windows Vista 上启动,需要支持 DirectX 10 的硬件,和拥有一个强悍的 API 集,几乎在软件的各个方面

都有极大的影响。

与其他版本一个显而易见的不同就是在 Direct3D 10 中移除了固定的函数管线,而被移除的部分却是渲染状态和

在 API 中构建算法来允许使用公共特效渲染对象的一个必须的集合。在图形硬件上面移除固定函数管线取而代之的

是可编程着色器。通过本书的讨论,图形着色器被用来编写定制的图形硬件处理几何体的特殊代码。图形着色器第

一次被介绍是在 DirectX8 中,但是从那以后图形硬件和着色模型使它演变为 API 中一颗耀眼的明星。

在 Direct3D 9 中,我们能够通过着色器来渲染几何体,开启光照只需要设置少量的性质和渲染状态,那些 API

会转化我们的几何体使得发布时只需要调用少量的 Direct3D 函数。在 Direct3D 10 中我们依旧能够通过着色器做上

述所有的事情,而且还能做得更多。初学者需要记住的关键一点是当第一次学习图形编程时固定管线被内建的 API

所限制,而着色器允许我们创建任何特效,唯一的限制是游戏中的帧率。想使用光照?只需要掉了 Direct3D 函数来

开启它并且设置它的属性(上限是 8 种光照)。想通过多幅图像渲染表面?只需要开启它。甚至能够表现你所想要的

基于像素的模糊运行和深度域,而使用固定管线就没那么幸运了。

如果固定管线不能明显的支持的特效,不用着色器的话你就不能创建你所希望的效果。虽然一些开发者会发现

特效能够通过一些技巧而不依赖于着色器来解决,但是那往往都是非常困难并且低效的做法。我的建议就是这些可

以做得更好,不要错过使用着色器的机会。

现在如果我们需要表现凹凸纹理(凹凸贴图),我们能够编写像素着色器来指导光线的方向,使用它来执行计算

并且在像素级的表面方向被载入一个特殊的图片即法线贴图,一起结合来渲染出最终的像素的颜色。在着色器发布

之前,或者早期版本的那些所提供的特性受限的着色器(比如那个需要与早期的 OpenGL 结合的黑暗的年代),要实

现这种效果几乎是不可能的,或者是做甚至简单的效果比如法线贴图只能仅仅通过低效的方法或者低品质的方法或

者近似的方法来做。当然,这只是一种简单的效果基于人们的经验上。

从 DirectX9 到 DirectX10 是一个巨大的飞跃。DiectX10 通过了两次迭代(译者注 9.0b,9.0c)才被市场所接受。

DirectX 11 与 DirectX 10 的比较

Direct3D 11 构建于 Direct3D 10.1 之上,增加了渲染下一代图形的新特性集。在 DirectX 11 中新增的内容包括:

使用新的 API 组件 DirectComput 在 GPU 上做通用计算

真正的支持多线程渲染

新的基于硬件的细分曲面技术(Tessellation)

着色器模型 5.0 和在着色器中加入面向对象编程概念

HDR 和 LDR 图像的 BC6 和 BC7 纹理压缩

增强的贴图(纹理)分辨率

等等 (注意:应用与几何体表面的图片资源叫做贴图或者纹理。就是经常提到的彩色图像被映射到表面使

得表面看起来更加的细致)

DirectX 11 更像是 DirectX10.1 的一个增强版本而不像从 DirectX9 到 DirectX10 的主版本升级。微软在 DirectX10

上做了冒险的尝试,不仅要求新的硬件支持而且 windows 的版本最低也要求 Vista。几年过去了,现在来看微软所

做的冒险有了回报,因为不仅得到硬件的广泛支持,而且 windows 用户的主要群体现在已经升级到了 Vista 和 Win7

操作系统。DirectX 已经被将来的考虑所采用(译者注:这里主要是指 DirectX10 和 11),并且随着次世代游戏的多年

发展,DirectX11 在未来将会变得越来越重要。

DirectX 11 与 OpenGL 4.0 的比较

OpenGL 很长时间被作为 Direct3D 的图形 API 的对手所考虑。OpenGL 支持 Windows 操作系统以外的平台,比如

Mac,LinuxOS,iPhone 和 iPad,Sony 的 Playstation3(至少有一个实现),和各种各样的移动设备比如手机和 PDA,还

有其他的平台。虽然本地设备所支持的 OpenGL 能够从一个平台到另一个平台,但 API 核心被设计为平台独立的,

不包括拥有很长历史的硬件设备扩展和来自于不同 OpenGL 版本的竞争对手的特性。DirectX,一方面在所有的不同

版本的 WindowsOS 和 Xbox 游戏机都有效,而且还考虑了 DirectX10 和 11 的兼容性,硬件必须提供一个严格的兼容

性列表,说明在DirectX10之前的不同情况。而在此种情况下对于硬件厂商所提供的特定扩展OpenGL是工作受限的。

硬件市场的各种不兼容性导致要想在所有支持的设备上实现同样的效果需要做各种重复的工作。

整个的 Direct3D 和 OpenGL 的争辩历史,就好像不同宗教的教徒之间的斗争一样,但是事实上 OpenGL 已经落

后于 Direct3D 多年。微软在 Direct3D 上投入了巨大的工作量并且通过了多年的改进,OpenGL 不仅已经落后,而且

也没有履行发布新版本的承诺(译者注很长时间主版本都是 2),而且一次又一次的多年遭受同样的问题困扰(译者注:

OpenGL 各厂商之间的内讧)。当 OpenGL3.0 发布时,OpenGL 的世界已经落后 Direct3D 太多。不幸的是,在 OpenGL

世界,后面所支持它的厂商围绕怎样与 Direct3D 的 API 对抗进行着此起彼伏的讨论时,Direct3D 已经获得了统治地

位。

OpenGL4.0 展现了许多 DirectX11 所拥有的特性,最引人注目的是使用 OpenCL 来支持 GPU 的计算和对

Tessellation(细分曲面)技术的支持,现在的它很接近当年做出的各种没有行动的的承诺了,与 2.0 和 3.0 比较的话就

显得特别的耀眼了。尽管 OpenGL 不再是原地踏步,但是不幸的是,要想既要对抗 DirecX 而且赢回当年的核心地位

和已经转到 Direct3D 阵营的人们(那时 OpenGL 的错误决定太多)的心还需要做大量的工作。图形 API 直接的比赛就好

像是高中时流行的跳山羊赢选票比赛一样。

为什么需要 DirectX

在 Windows 视窗操作系统发布之前,开发者们都在 DOS 上写游戏。那是一种单线程,非 GUI 界面的操作系统,

它提供开发者在他们的应用程序和底层硬件直接交互的方式。这种方式有利也有弊,比如因为游戏代码是直接驱动

底层硬件的,开发者能够压榨机器的每一寸资源和能力,当他们的游戏执行时给他们的硬件的完全控制权。而消极

的一面是需要为每一种硬件写直接的驱动代码或者他们的游戏想要得到任何硬件的支持需要各种各样的第三方库,

包括机器上的公共硬件比如显卡和声卡。

由于并不是所有的显卡(Video card)都遵循同样的标准,使得显卡特别容易让开发者迷惑。尽管大部分显卡都支

持一系列的公共分辨率,可是开发者不得不被强制性的直接访问显存,这使得在屏幕上绘图十分困难。开发者都一

致的在寻找更好和更容易的开发方式。

当 windows3.1 发布时,它带来了 DOS 下的同样的限制,原因就是该版本的 windows 运行在 DOS 之上,它所提

供给游戏的有效资源受到限制,开发者不得不像之前那样直接操作底层。大部分支持 Windows 系统所编写的游戏主

要是卡牌和棋盘类游戏,那些游戏仅仅是继续由 DOS 支持。这种情况下,微软发布了 DX1 来吸引游戏开发者开发

Windows 下的游戏来证明视窗操作系统并没有想象中的那么慢,并且还想将 DOS 下的人群转移至新的视窗操作系统。

微软发布的 Windows95 并没有解决上述所有的问题,直到 DrectX 1 的发布,DX1 就是众所周知的 windows 游戏

开发包。它提供给开发者一个单一的库编写游戏,就像是在他们的游戏之间和 PC 硬件之间架起了一个公共层;在

屏幕上绘制图像就变得如此的轻而易举了。DirectX 的第一个版本并没有提供当时所有的硬件支持,但是对于游戏开

发者他们想要的东西来说,这却是一个极好的开端。多年之后,DirectX 的众多版本的发布,每一次都有改进和新的

技术的支持比如网络支持,流媒体,和新的各种输入设备。当前 DirectX 的最新版本包括 Direct3D 11,它兼容于 Vista

系统和 Win7 系统。

DirectX 的组成部分

DirectX 是一个代码库集合,提供给游戏和多媒体应用一个公共的函数集合。为了确保你的游戏只需要使用必须

的函数,DirectX 被划分为多个组件。

DirectX 11 的组件

DirectX API 被划分为多个组件,使得每一部分都只响应系统的一个方面。组件之间的 API 相互独立,从而允许

使用你的游戏所要求的那部分。在最新版本中,一些组件已经被更新,例如 Direct3D 组件,而另外一些部分像之前

的版本一样被保留下来直到微软将他们移除(如果他们是过时的组件)。DX 之间的组件应新函数要求被独立的更新。

Direct2D 组件

Direct2D 在 Win32 程序中被用于 2D 图形的绘制,它是一个高性能的矢量函数渲染库。

DirectWrite 组件

该组件被用于在使用 Direct2D 的应用程序中进行字体和文字的渲染。

DXGI 组件

DirectX 图形基础设施库,也就是著名的 DXGI 组件,用于创建 Direct3D 的缓存交换链和枚举设备适配器。

Direct3D 组件

Direct3D 组件用于在 DirectX 中构建所有的 3D 图形。它就是最受注意的并且更新最频繁的 API。本书的学

习重点就是 Direct3D 组件。

XAudio2 组件

XAudio2 是一个低级的音频处理 API,是 XDK 的一部分(Xbox 开发套件),而现在是 DirectX SDK 的一部分。

XAudio2 取代了 DirectSound 组件。XAudio 的最初版本用于 Xbox 游戏平台。

XACT3 组件

XACT3 是一个构建于 XAudio2 之上的高级音频处理 API。XACT3 允许开发者使用跨平台的音频创建工具来构

建他们的应用程序中的声音。开发者如果需要在低层次上控制他们的音频系统可以使用 XAudio2 或者使用它来

构建类似于 XACT3 的组件。XACT3 的讨论在附录 B 中有更多的内容,可以从伴随网站上下载“Audio in DirectX”,

它是一种极具威力并且非常易用,来构建游戏中的声音部分。

XInput 组件

XInput 组件是 XDK 和 DirectX SDK 中的输入控制 API 部分,被用于处理 Xbox360 游戏机的所有输入操作。本

质上,你在 Xbox360 上的任何输入控制器都可被用于 PC 机,而 XInput 就是你用于在这些设备上工作的 API。这

些设备不仅包括 Xbox 的游戏手柄而且还有摇杆式和 Guitar Hero(译者注:抱歉,这个我也没用过)控制器,大按

钮控制器(游戏 Scene It 中使用过),arcade stick(例如 Tekken 6 中的这种东西)等等。XInput 取缔了 DirectInput。

(注意:XAudio 是只能在 Xbox 平台下使用的声音 API。它的后继版本 XAudio2 则支持 Xbox 和 WindowsPC 平台)。

XNA Math 组件

新的 XNA Math 组件不仅仅是一个 API 而且更像一个在常见的视频游戏中实现了优化操作的数学库。XNA

Math 组件使用 SIMD(Single Instruction Multiple Data,单指令多数据流)在单指令调用中执行多种操作。XNA Math

组件被 Xbox360 和 WindowsPC 平台所支持。除 XNA Math 之外我们也讨论游戏中的数学,更多细节见于第六章。

(注意:XNA 游戏套件是构建于 DirectX 之上的游戏开发工具,允许开发者使用 C#和.NET 语言在 Xbox360 和

WindowsPC 开发游戏。XNA Math 组件在新的 DirectX SDK 中以数学库命名能够用于 XNA 游戏套件之外。你不需

要下载 XNA 游戏套件 SDK。)

DirectCompute 组件

DirectCompute 组件是一个新加进 DirectX 11 的 API 集,允许使用 GPU 执行通用多线程计算。GPU 能够并行

处理多任务,比如物理模拟,视频压缩及解压,音频处理等等。并不是所有的任务都适合 GPU 处理,但是上述

这些任务,确极适合 GPU 的处理。

如果需要更多的 DirectCompute 方面的知识,建议看看《Game Development with Microsoft’s DirectCompute

for Beginner》这本书。

DirectSetup 组件

一旦你的游戏完成后,你想发布给其他人玩。DirectSetup 组件提供一些用于在用户计算机上面安装最新版

本的 DirectX 运行时的函数。它也能够检测用户电脑所安装的最新版本的 DirectX。

Windows Games Explorer

游戏管理器是 Vista 和 Win7 系统的特性,允许开发者在 OS 上展示他们的游戏。游戏管理器处理诸如游戏

的展示,标题,评估,描述,region-specific box art,内容评级(例如 M 代表成人,T 代表青少年等),游戏统计

和通知,家长控制等等。DirectX SDK 提供了大量的信息用于怎样使用游戏管理器来管理你自己的游戏,当游戏

安装时特别有用。图 1.1 显示了游戏管理器的一个示例。

插图 1.1:Win7 中游戏管理器的展示

DirectInput 组件

DirectInput 组件用来检测键盘,鼠标和游戏操纵杆的输入。现在 XInput 被用于所有游戏的输入控制。对于

键盘和鼠标我们可以使用 Win32 函数或者使用 DirectInput 处理,在第五章中我们将使用它们进行输入检查。根

据 DirectX SDK 的废弃机制,DirectInput 将继续保留,直到新的技术完全取代它。

过时的 DirectX 组件

以下介绍的组件都不建议使用或者已经从 DirectX SDK 中移除的组件:

DirectDraw 组件

假如使用 2D 渲染,现在我们一般使用 Direct2D 或者 Direct3D 进行 2D 图形处理。在 DirectX 8 中 DirectDraw

被融入进 Direct3D 中。

(注意在之前的 DirectX 版本中,2D 图形绘制函数由组件 DirectDraw 提供。因为 DirectDraw 不再被更新升级,现

在你使用的所有绘制都在 Direct3D 或 Direct2D 组件中。)

DirectPlay 组件

DirectPlay 被用于在线游戏的网络处理。它构建于 UDP(User Datagram Protocol)协议之上并且做为网络通信

的高级抽象来提供。现在这个 API 组件已经被移除,不再是 DirectX SDK 的一部分。DirectPlay 组件不再被推荐

使用于 PC 机上的 WindowsLive 游戏和 Xbox 平台上的游戏。

DirectShow 组件

DirectShow 组件被用于多媒体的渲染和记录。这意味着 DirectShow 组件能够播放多种公共视频文件格式,

并且提供 DVD 的导航以及更多其他功能。现在 DirectShow 组件是 Windows SDK 的一部分,不再是 DirectX SDK

的部分。这样就有两种选择,Vista 和 Win7 用户使用微软的多媒体基础设施来播放多媒体,它也是 Windows SDK

的一部分。如果视频游戏在场景中需要播放 CG 动画和视频文件依旧可以考虑使用该组件。

DirectMusic 组件

DirectMusic 组件自从 DirectX 7 中就已经过时,只是在更早期的时候在应用程序中用于播放音频内容。

DirectMusic 提供在低层次上访问音频和硬件的能力,它常年是 DirectX 书籍和材料的主题之一。现在我们使用

XAudio2(低层次)或 XACT3(高层次)来控制游戏中的声音和多媒体。

DirectSound 组件

DirectSound 是另一个废弃的音频 API 组件,给开发者访问低层次的音频需求。现在 XAudio2 已经取代了它。

在附录 B 的“Audio in DirectX”中(位于伴随网站),我们讨论了更多的细节。

组件对象模型 COM

DirectX API 基于组件对象模型 COM。COM 对象由一些暴露了方法的接口集组成,提供给开发者访问 DirectX。

COM 对象是已经在操作系统中注册了的普通 DLL(动态库)文件,提供具体的硬件支持。对于 DirectX COM 对象,注

册发生在 DirectX 的安装时期。类似于 C++对象,COM 对象要求使用接口来访问内部实现的方法。相对于标准对象

的一个实际的好处是在 COM 对象内部一个接口有不同版本的实现,从而允许向下兼容性。

一个例子就是,每一个 DirectX 版本通过 API 访问所包含的新的 DirectDraw 组件,只要不移除已经存在的代码

则该组件一直包含前面版本的功能。因此使用 DirectX7 创建的游戏能够在 DirectX9 中运行。换句话说,就是老游戏

能够被安装和运行于最新版本的 DirectX 运行时。

COM 组件的附加的优势是它们能够被用于多种语言的工作,不局限于 C++语言。开发者能够使用 VB,C++,或

者 C#来调用相同的 DirectX库。微软在 DirectX中更新或者增加新的功能,每一个已经被更新的组件都将增加版本号。

你会发现并不是所有的组件都有同样的版本级别。例如 DirectX 11 中 DirectInput 的版本号是 8.0,而 Direct3D 的版

本号是 11。

介绍 Direct3D 11

随着 Win7 系统的发布,Direct3D 11 也同步发布。它兼容于 Vista 和 Win7 系统,连同支持 DirectX 10 的硬件。

DirectX 11 中的某些特性如,细分曲面技术和着色器 5.0 模型,要求支持 DirectX 11 的硬件。但是大部分的 API 能够

工作于支持 DX10 的硬件。

Direct3D 11 的阶段

Direct3D 不仅仅是一个 API 接口;它是一个转换几何形状和现实世界图像的工具集。甚至你能够将 Direct3D 看

作是一个黑盒而不用担心内部的实现细节,不过至少了解它大致的工作过程还是很有用的。

Direct3D 通过多个阶段处理你的场景,每一个阶段执行一个具体的任务集来产生最终的图像。这些阶段就是著

名的 Direct3D 的渲染管线,每一阶段的截屏如图 1.2 所示。

渲染管线的第一个阶段叫做输入装配阶段。此阶段我们设置的几何体以及必要的 Direct3D 信息将执行输入装配

任务。

第二阶段是顶点着色器(VS)阶段。顶点就是组成各种形状如三角形上的单一的点。此阶段我们能够运行顶点着

色器代码处理每一个顶点,来建立我们所设置的特效。本书将会讨论更多的着色器内容。一个顶点着色器总是以单

一的顶点作为输入并且同样输入单一的顶点。这些被输入的顶点数据就来自于输入装配阶段处理后的结果。

插图 1.2:Direct3D 11 各阶段的示意图

第三,四,五阶段是处理细分曲面技术(Tessellation)的阶段。Tessellation 技术是一个高级主题,它由两个新的着

色器外壳着色器(Hull)和域(Domain)着色器完成。硬件的细分曲面器,处理输入的几何体表面,增加或者减少几何体

的表面细节。它有能力处理实时中的高阶多面体模型的渲染,即使该多面体拥有数以千计甚至数以百万计的多边形。

通过硬件来创建几何体的更多表面细节,应用程序只需要使用一个很小的数据集定义低阶模型。低阶模型的形式必

须是可细分的 3D 模型。硬件在此之上来创建高精度逼着的模型。可细分的多边形意味着组成它的每一个多边形可

被再细分为更小的多边形。

外壳着色器的输入来自于顶点着色器的输出,它在控制点和数据上进行处理,不同于传统的顶点,并且输出支

配各个块状的控制点(译者注:其实一块区域并不是由所有的点进行支配,只需要选择其中的一部分点即可定义此块

区域)。细分曲面技术阶段就发生在 HS 和 DS 阶段之间,它是一个固定的函数阶段,使用外壳着色器输出的控制点

在硬件上进行曲面的细分。而域着色器就处理那些细分后所产生的大量的点。

第六阶段是几何(Geometry)着色器阶段,同样是着色器操纵的阶段。如果没有发生细分曲面的步骤,则几何着

色器阶段就直接在 VS 阶段后发生。几何着色器操纵几何实体例如三角形,而 VS 只是处理构成几何形状的单个点。

几何着色器能根据你所尝试创建特效的需要来创建或者销毁几何体。一个常见的例子就是 GS 从几何模型中创建阴

影几何形状,这就是著名的阴影体积。另一个示例就是根据需要创建的粒子特效来产生粒子,例如下雨或者通过在

爆炸中心产生一系列的点和小多边形围绕着的爆炸特效。

第七阶段就是光栅化阶段,其任务就是通过裁剪和剔除几何体(在第六章中将会讨论)来决定哪些点需要被显示,

设置好像素着色器,并且决定像素着色器怎样被调用。

第八阶段是像素着色器(PS)阶段。在 PS 阶段,着色器接受所有前面的阶段产生的几何数据并且将组成那些形状

的数据转化为像素(有时被叫做片段)。PS 输出的是单一的颜色值供最后一阶段来构建最终显示在屏幕上的图像。如

果没有发生细分曲面阶段和 GS 阶段,则 PS 直接接受来自于 VS 产生的数据。PS 所接受的数据是已经经过内插值后

的数据,这些数据产生于形状的顶点之间的那些点。下一章我将会有更多这方面的讨论。

最后一阶段是联合输出(Output Merger)阶段,将所有的输入结合在一起。OM 阶段使用其他阶段输出的片段来

构建最终的图像发送到屏幕上去显示。

Direct3D 11 的注意事项

也许你已经熟悉使用 DirectX 来编写游戏,如果是这样的话,当你把游戏迁移到最新版本时有些注意事项。如

果是从 Direct3D 10 更新到 Direct3D 11,处理比较简单,因为大部分 API 在 Direct3D 11 都有对应不需要手动修改。

Direct3D 11 就像是 Direct3D 10 和 10.1 版本的超集,所以这样的迁移没什么事情需要做。但是从 Direct3D 9.0 到 11.0

的迁移就是另一回事了。

从 Direct3D 9 迁移到 Direct3D 11 的最大的挑战就是移除固定函数管线。之前的版本(译者注:指从更低版本迁

移到 9)迁移可以使用默认的方式来处理你的 3D 场景,Direct3D 将会处理好裁剪,灯光和阴影。而现在,在 D3D10

和 D3D11 中,所有的这种特定的功能处理都将使用可编程管线。第七章讲解着色器,它的目的就是让你处理那些事

情时完全的加速起来。在第七章对着色器广泛的探讨之前,我们就开始使用着色器并且讨论它们存在的必要性。

另一个主要变化是移除了 CAPS bits(译者注,sorry~不知道翻译)。在之前的 D3D 版本中,需要检查视频硬件能

力来确保类似于像素着色器的功能被游戏的使用。而现在,任何不被硬件支持的特性都将被系统中的软件模拟,使

得能够展现你所使用的任何功能。这使得使用 D3D10 或 D3D11 的游戏的初始化变得十分的简单。这一点是十分重

要的,因为过去有些厂商的硬件只支持全部特性的一个子集,当粘合不同硬件的支持时,将是一个问题。现在为了

考虑与 DX10 或者 DX11 的兼容性,硬件必须严格的依从标准。

相比于 D3D10 和 11,D3D 9.0 几乎是完全不同的 API 集。不能像从 D3D10 迁移代码到 D3D11 只需大量的拷贝

和复制函数。这种情况的代码迁移,最好是在游戏代码中创建一个新的渲染层来代替一对一的代码替换。

DirectX 的工具集

DX SDK 包含了丰富的信息和众多与 DX 不同主题相关的工具。在学习或者开发应用程序时,SDK 本身就能够被

任何使用 DX 的开发者所探索,因为它拥有海量的文档和很多高级的工具。在这部分,我们将简略的浏览一下 SDK

中的工具。

示例浏览器(Sample Brower)和文档

SDK 中的示例浏览器能够展示所有的示例 Demo,技术文章,教程,文献和一些其他的 SDK 中的工具。示

例浏览器中列举的材料包括了 DX11,DX10 和 DX9。无论哪一新版 SDK 发布,都应该看看示例浏览器。说不定

能够找到新技术或者特效的实现,有用的教程,或者关于 DX 的任何技术信息。

插图 1.3 显示了示例浏览器的截屏。示例浏览器能够从开始菜单中的 DXSDK 的安装子目录中找到。

DX 的文档和图形文档也非常的有用,SDK 的图形方面有它自己的文档,如果你查找一些有关图形方面的知

识或者就显得很重要了。当然微软的 MSDN 网站有全部的这些文档并且在线有更多的资料。

插图 1.3:DX SDK 中的示例浏览器

PIX 工具

PIX 工具用于在D3D 应用程序执行时调试和分析。PIX 能够得到很多有用的信息诸如 API 的调用,时间统计,

在变化前后的网格信息,等等。PIX 能够在 GPU 上调试着色器代码,打断点和进入代码调试。

Caps Viewer 工具

DX 的 Caps Viewer 工具能够显示硬件的兼容性方面的信息,显示支持 Direct3D,DirectDraw,DirectSound

和 DirectInput 方面的细节信息。通过此工具能显示硬件所支持的每一个方面的版本,截屏如图 1.4。

插图 1.4:CapsViewer

诊断(Diagnostic)工具

DX 的诊断工具用于测试 DX 的各个组件十分工作正确。此工具用于创建诊断报告保存为文件或者是发生给

微软。发送给微软的报告由更多帮助 tab 中的报告按钮来发送。如果你怀疑某些组件在一些特定的机器上工作

不正常可以运行这个工具来提供一些参考。

贴图(Texture 纹理)工具

DX 的贴图工具使用 DXTn 压缩用于转换图像格式为 D3D 的贴图格式。这个工具已经过时,因为它只支持

DX9 的贴图格式,而不支持 DX10 和 11 的格式。如果你想使用 DXT1-DXT5 压缩方法来创建图像的贴图,仍然可

以使用这个工具。

(注意:此工具不支持 BC6 和 BC7 的新格式,在压缩格式方法中“BC”已经替代了“DXT”)

DX SDK 也包括贴图转换器。该工具随着 SDK 的安装被安装到 TXVIEW.dll 中。该工具的目的是将图片的一种

格式转换到另一种格式。其文件格式包括 BMP,JPEG,DDS,TGA,PNG,DIB,HDR,PMF。

贴图转换器通过在 Windows 资源管理器中右键选择一个或多个图片并且从下拉列表中选择目标文件格式

进行工作。当对话框出现时,能够设置目标图片的属性,比如大小,格式,输出文件名等等。也能够通过命令

行调用 TexConv.exe 或者 TexConvEx.exe 转换图片为 D3D10 和 11 所使用的贴图。贴图转换工具的扩展支持 DX9

的最后一个子版本(即 9.0c)。

Error Lookup 错误查看器

DX 的错误查看器能够显示运行 DX 应用程序时所接受到的任何错误代码的详细描述。在此工具中能够键入

错误代码,点击查看按钮后将会有该错误的详细描述。由于不能清除所有的错误,该工具还是很有用的。能够

在 DX SDK 安装的实用目录中找到错误查看器。

控制面板

DX 的控制面板,在实用工具目录中,用于检测和修改 DX 的各个组件。通过该控制面板工具能够做:

开启 D3D10 或 11 的调试层

通过设置能够改变调试信息的输出级别

查看驱动信息

查看硬件支持信息

查看各组件的版本信息

跨平台音频创建工具

跨平台音频创建工具是 GUI 工具(也有命令行的版本)使用 XACT3 组件来创建音频文件。我们能够通过该工

具来创建我们的音频剪辑文件并且组织好,它的处理细节见于附录 B(在线,伴随网站可以下载)。

该工具的截屏见于插图 1.5。XACT3 工具能够创建用于 PC 和 Xbox 上的音频文件。对于那些使用 XNA 开发

的游戏,能够使用在 Xbox360 处理器上所匹配的字节流格式的音频文件。因为 Xbox360 基于 PowerPC 的架构,

在 PC 上的输出的那些文件不能被移植到 Xbox360 平台上。这一点并不会影响我们,因为此书假定读者的开发

环境是在基于 x86 架构的 Vista 或者 Win7 系统的 PC 机上使用 DX 11。

插图 1.5:跨平台音频开发工具界面

游戏定义文件编辑器

游戏定义文件编辑器用于在 Vista 和 Win7 上创建本地的游戏定义文件。游戏定义文件的信息将被展现在游

戏资源管理器中,在标题为“DX 11 的各组件”部分已经介绍了游戏资源管理器。该编辑器的截图在 1.6 中。

游戏定义文件编辑器允许开发者设置游戏的发布日期,管理器中的图标和点击行为,游戏所属于等级(例如,

未成年级,成人级),游戏名字和描述,以及其他的属性。使用该编辑器的细节材料可以在 DX SDK 的文档中找

到。或者在 MSDN 上搜索主题“Game Definition File Editor:Tool Tutorial”。

插图 1.6:游戏定义文件编辑器界面

Down-Level Hardware

DX11 支持著名的硬件向下检查概念。当 DX10 和 Vista 系统发布时,它们需要新的驱动模型和硬件支持。

虽然 DX11 有它自己的版本,但是它能够使用单一的函数集在支持 DX10 和 10.1 的硬件上工作。在第二章中将

会看到,当构建我们的第一个 Demo 时将首先检查 DirectX11 的兼容性,如果没有支持 11 的硬件环境,其次才

是检查 10.1,10.0 的兼容性。如果选择的是 10.1 或 10.0,仍然可以使用同样的 API,只要我们使用的是 4.0 着

色器模型和一些不要求 DX11 的特性,则应用程序仍然可以在硬件上良好的工作。如果你没有 DX11 没关系,因

为本书的代码仍然可以在只支持 DX10 的硬件上运行。

总结

在你学习 DX 的过程中,如果有时很迷惑不用太担心,如果你是第一次学习视频游戏图形编程,对于 DX 或者

OpenGL 这类的东西,自然一个陡峭的学习曲线。如果你有 DX 之前版本的经验,你将会发现其实学习 D3D10/11 将

会更加的容易和清晰。如果读者来自于 OpenGL 阵营,就说明已经接触了更多的困境和知道图形编程的一些概念,

读者则可以重点放在 API 的学习上。

当学习 DX 时,选择一个系统和编写一些力所能及的示例代码是对学习很有帮助的。目标开始定小些,再慢慢

的变大。开始时通过达到小一些的目标逐步的提升,直到熟练掌握 DX 为止。当开始有些了解之后,通过示例 Demo

来 写 自 己 的 demo 来 提 高 。 如 果 某 些 地 方 卡 住 了 , 记 住 你 不 是 一 个 人 在 战 斗 ; 诸 如

UltimateGameProgramming.com,GameDev.net 和微软的 MSDN 这些地方都可以寻求帮助。就像是生活中的很多事情

一样,学会它的最好办法就是一遍又一遍的实践,直到完全掌握。

章节习题

所 有 章 节 的 习 题 答 案 都 在 附 录 A 上 , 可 以 通 过 本 书 的 伴 随 网 站

www.ultimategameprogramming.com/Beginning-Direct11/或者 www.courseptr.com/downloads 上下载。

1. DirectX 的第一版的原始名字是什么?

A.XNA B.Games SDK

C.Direct3D D.Windows Foundations SDK

2.下列哪一个不是 DirectX 11 的特性?

A.固定函数渲染管线 B.GPU 多线程

C.提供的 HDR 贴图压缩 D.以上都不是

3.下列哪个 DirectX 版本能够用于 Xbox360 平台?

A.DirectX 10 修改版 B.DirectX 11 修改版

C.DirectX 9 修改版 D.DirectX 8 修改版

4.DirectCompute 组件是在下列哪个 DirectX 版本中介绍的?

A.DirectX 11 B.DirectX 10

C.DirectX 9 D.在 Xbox360 平台上的 DirectX

5.DirectX 11 所介绍的着色器模型是?

A.着色器模型 4.0 B.着色器模型 4.1

C.着色器模型 5.0 D.以上都不是

6.下列哪个阶段出现在像素着色阶段之后?

A.几何着色 GS 阶段 B.输出联合阶段

C.外壳和域着色阶段(用于细分曲面技术) D.顶点着色阶段

7.DirectX 控制面板是什么?

A.DirectX SDK 中没有控制面板 B.用于 DirectX 的安装和卸载

C.是 Win7 的控制面板的扩展 D.用于检查组件性质。

8.游戏定义文件被用于何种目的?

A.编辑游戏的资源,如图片和模型

B.创建游戏的帮助手册

C.创建游戏的安装包

D.创建游戏管理器的本地化文件

9.PIX 在 DirectX SDK 中是哪一类型的工具?

A.用于性能分析 B.用于贴图浏览

C.用于贴图转换 D.以上都不是

10.在 Direct3D 11 中我们讨论了几个阶段?

A.12 个 B.7 个 C.9 个 D.11 个

11.几何着色器 GS 阶段是否被用于 Tessellation 技术?(true or false)

A.True B.False

12.几何着色器阶段发生在 VS 之前和 PS 之后。

A.True B.False

13.DirectX 11 要求 Vista 或者更高的操作系统。

A.True B.False

14.Xbox360 平台使用 DirectX10 的后续版本。

A.True B.False

15.Compute shaders 是一种新的着色器类型在 DirectX 中,用于通用计算。

A.True B.False

第二章 你的第一个 DirectX 程序

DX 学习入门的最好方式是开始创建简单的 Demo 应用。在本章中,我们将一步步的教你创建你的第一个 DX 程

序,特别是对 Direct3D 的注意。通过本章的学习后,你将会对 Direct3D 应用从开始到结束有一个坚实的理解。

本章目标:

怎样创建一个工程

怎样建立窗口程序

怎样初始化 DirectX

怎样清除屏幕(清屏)

怎样显示场景

创建工程

本书假定你已经有在 VStudio 上创建 C++工程的经验。此部分我们讲解下 Win32 应用程序的初始化。并且将修

改这段代码用于 D3D 的初始化和基本的渲染。本章结束部分我们将创建一套用于本书所有 Demo 的 D3D 的初始化

的代码。

应用工程的第一步是创建 VStudio 的项目。本书使用 VStudio C++ 2010Express 版本,可以在网站

www.microsoft.com/express/downloads/上下载。(注意:如果已经安装 VStudio.Net 和 DX SDK,不知道执行上述步骤,

请查看说明)

我们将通过如下步骤创建叫 Blank Win32 Window 的新工程:

1. 打开 VStudio,开始菜单选择新建工程,弹出新建工程的对话框如图 2.1 所示。

插图 2.1:Visual Studio 新建工程对话框

2. 从工程模板列表中选择空工程并命名为 BlankWindow,点击 OK 按钮完成,对话框插图 2.2.

插图 2.2:创建一个空工程

3. 点击结束按钮。

新的工程将被创建。下一部分我们将添加用于本书中所有 Demo 的模板代码。

添加窗体代码

VStudio 创建空工程之后,下一步就是创建源代码文件来初始化应用程序主窗口。在工程中添加空的源代码文

件,该将成为我们的源代码主文件,命名为 main.cpp。创建 main.cpp 的步骤如下:

1.在解决方案管理器中右键源文件文件夹,选择添加->新建项(如图 2.3)。

插图 2.3:在工程中创建新建项

2.选择新建项对话框中的 C++源文件,命名为 main.cpp(见插图 2.4)。

插图 2.4:创建 main.cpp 源文件

3.点击 OK 按钮结束。

在工程中创建好 main.cpp 之后,我们现在就能够添加 Win32 的具体的代码来创建空窗体了。进入主函数入口

之后,我们将创建初始化 D3D11 并且使用 D3D 来渲染窗体的画布。

主函数入口点

在 main.cpp 中需要做得第一件事是包含 Win32 程序所需的头文件和定义函数入口点。需要注意的是,Win32

窗体应用程序的主函数入口点是 WinMain 函数(译者注:控制台程序的入口点是 main 函数)。对于现在来说,我们

只需要在源文件的顶部包含 windows.h 头文件即可。源文件 main.cpp 的头文件和空的 WinMain 函数见于程序清单

2.1。

清单 2.1 空的 WinMain 函数(Blank Win32 窗体步骤 1)。

#include<Windows.h>

int WINAPI wWinMain(HINSTANCE hInstance, HINSTANCE prevInstance, LPWSTR cmdLine, int cmdShow)

{

return 0;

}

代码清单 2.1 中能看出我们使用 wWinMain 代替 WinMain。这两个主函数的不同在于 wWinMain 的第三个参数

cmdLine 使用 Unicode 编码,而另一个使用 ANSI 编码将会转换 Unicode 为 ANSI。因此将导致 Unicode 字符串中的字

符丢失,而使用 wWinMain 则允许我们正确处理传入应用程序的 Unicode 参数。

(w)WinMain 函数有四个参数,其定义如下:

HINSTANCE hInstance:应用程序当前实例的句柄(译者注:了解 Win32 的基本知识最好是看 Windows 程序

设计第五版)。

HINSTANCE prevInstance:应用程序的前一个实例的句柄。根据 MSDN 的文档现在此参数将一直是 NULL。

虽然此参数一直是 NULL,如果你想要确定该应用程序是否已经有实例在运行,文档推荐使用 CreateMutex

函数来创建唯一名字的 mutex(互斥体)。当已经有实例运行时,再次创建 mutex,CreateMutex 函数将会返

回 ERROR_ALREADY_EXISTS。

LPSTR cmdLine (或使用 Unicode 编码的 LPWSTR):应用程序的命令行由程序外部输入。允许你传递命令给程

序,例如通过 cmd 命令终端,或者是通过快捷方式提供命令参数,等等。

int cmdShow:窗口被显示为哪个模式的 ID 号(译者注:例如最小化,正常,最大化等)。

窗口初始化

尽管上述程序能够编译运行,可是由于没有创建窗口,运行时什么也没显示。So,下一个步骤就是创建 Win32

窗口。首先要注册一个窗口类并且创建窗口本身。应用程序必须在系统中注册它的窗口,程序清单 2.2。

清单 2.2:窗口类的注册和窗口的创建(Blank Win32 窗体步骤 2)

#include <Windows.h>

int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR cmdLine, int nShow)

{

UNREFERENCED_PARAMETER(hPrevInstance);

UNREFERENCED_PARAMETER(cmdLine);

WNDCLASSEX wndClass = {0};

wndClass.cbSize = sizeof(WNDCLASSEX);

wndClass.style = CS_HREDRAW | CS_VREDRAW;

wndClass.lpfnWndProc = WndProc;

wndClass.hInstance = hInstance;

wndClass.hCursor = LoadCursor(NULL, IDC_ARROW);

wndClass.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1);

wndClass.lpszMenuName = NULL;

wndClass.lpszClassName = "DX11BookWindowClass";

if(!RegisterClass(&wndClass))

return -1;

RECT rc = {0, 0, 640, 480};

AdjustWindowRect(&rc, WS_OVERLAPPEDWINDOW, false);

HWND hwnd = CreateWindow("Dx11BookWindowClass", "Blank Win32 Window",

WS_OVERLAPPEDWINDOW, 0, 0, rc.right - rc.left, rc.bottom - rc.top,

NULL, NULL, hInstance, NULL);

if(!hwnd) return -1;

ShowWindow(hwnd, nShow);

return 0; }

Win32 宏 UNREFERENCED_PARAMETER 用于消除编译时未被函数使用的参数所产生的警告。虽然这一技巧不是

必须的,但是却是一个很好的编程习惯——要求编译源码时产生 0 警告。这个宏没有做任何事情,VStudio 的编译

器会优化它。

随后处理窗口类中未被用到的参数。窗口类由 WNDCLASSEX 所定义,包含 Win32 窗口的各种属性,比如窗口的

图标,菜单,该窗口所属的应用程序句柄,鼠标外观,等。能够在 Winuers.h 中找到该结构,当然该头文件已经被

包含在 windows.h 中,WNDCLASSEX 的定义如下:

typedef struct tagWNDCLASSEXA {

UINT cbSize;

/* Win 3.x */

UINT style;

WNDPROC lpfnWndProc;

int cbClsExtra;

int cbWndExtra;

HINSTANCE hInstance;

HICON hIcon;

HCURSOR hCursor;

HBRUSH hbrBackground;

LPCSTR lpszMenuName;

LPCSTR lpszClassName;

/* Win 4.0 */

HICON hIconSm;

} WNDCLASSEXA, *PWNDCLASSEXA, NEAR *NPWNDCLASSEXA, FAR *LPWNDCLASSEXA;

该结构的成员定义如下:

cbSize:该结构所占的字节数

style:用于定义窗口的外观

lpfnWndProc:著名的窗口回调函数(窗口过程),任何来自于操作系统的事件通知都将调用此函数。这里我

们设置的 WndProc 函数将稍后介绍。它其实是一个函数指针。

cbClsExtra:分配给该窗口结构额外的字节数。

cbWndExtra:分配给该窗口实例额外的字节数。

hInstance:该窗口类的包含窗口过程的程序实例句柄。

hIcon:程序显示的图标的资源 ID 号。如果为 NULL,则使用默认图标(例如,微软 Word 软件的文档左上角

的 W 图标)。

hCursor:鼠标形状的资源 ID 号。本书中我们使用标准的箭头鼠标(游标)形状。

hbrBackground:用于绘制窗口背景的背景画刷句柄。

lpszMenuName:菜单,以 null 结束的字符串的资源名字。

lpszClassName:以 null 结束的你所创建的窗口类名,最大长度是 256 字符。

hIconSm:窗口小图标句柄(例如在任务栏上的程序图标)。

窗口结构的大部分成员由 Win32 编程处理,这些知识不在本书范围之内(译者注:建议看看《windows 程序设计

第五版》),例如创建菜单(我们不太可能在游戏中创建 Win32 菜单)。那些大部分成员我们都简单的设为 0 即可。

创建 WNDCLASSEX 结构后,我们调用函数 RegisterClassEx()来注册窗口类,该函数以创建好的窗口类结构地址做

为参数来完成注册。如果返回值是 0,则表示注册失败,因此需要仔细查看窗口类的各个属性值,保证它们都有效,

我们假定这不会出现大的问题。

其下一步就是完成创建实际的窗口。首先调用 AdjustWindowRect 来计算我们所希望的窗口尺寸。窗体类型决定

了我们需要的窗口的真正的尺寸。观察大部分的 Win32 窗口程序,都有一个非客户区的空间,例如标题栏,程序的

边框等等。如果要确定窗体的具体尺寸,必须注意客户区和非客户区。

AdjustWindowRect 函数首先采用一个矩形(lpRect)来定义窗口区域的尺寸。左上角是窗口的起点,右下方向代表

宽高。此函数也使用窗口将被创建的风格标记,并且最后 bool 标记表明窗口是否有菜单,此标记影响非客户区。该

函数就是使用上述的三个参数计算出整个窗口的尺寸。

接下来调用 Win32 函数 CreateWindow 来创建窗口。在清单 2.2 中我们调用的是 CreateWindowA,该函数接受

ANSI 字符串,而 CreateWindowW 则接受 Unicode 字符串。如果使用后一个版本,需要在字符串前添加 L 标识表明

该字符串是 Unicode 编码(译者注:L”hello, 你好!”就是 Unicode 字符串)。

函数 CreateWindowA 的参数定义如下:

lpClassName:窗口类名字(使用与窗口类结构相同的名字)。

lpWindowName:窗口标题栏名称。

dwStyle:窗口风格标识。

X:窗口的水平位置。

Y:窗口的垂直位置。

nWidth:窗口的宽度

nHeight:窗口的高度

hWndParent:窗口的父亲句柄(如果该窗口是弹出式或者子窗口)。

hMenu:窗口的菜单资源句柄。

hInstance:程序的实例句柄

lpParam:传递给窗口的数据,用于窗口过程的处理(在窗口过程部分会讨论到)。

该函数的返回值是一个非空的句柄,如果创建成功,我们能够传递窗口句柄和命令显示标记参数(nShow,是

WinMain 函数的最后一个参数)来调用函数 ShowWindow 显示窗口。

创建窗口后,应用程序就开始它的工作了。Win32 GUI 程序是事件驱动类型的程序,意思是当一个事件发生后,

程序接收到它的通知(事件通知由 OS 传递给程序),采取一些行动来响应该通知。该过程一直运行,直到退出事件的

发生。例如,当 Microsoft Word 软件启动时,一个“create”事件发生,程序完成载入动作。当用户在工具栏,菜

单等区域点击鼠标时,就触发一个事件,OS 发送给程序去处理。如果在打开文件按钮上触发鼠标点击事件,则将

会显示允许用户选择打开文件的对话框。许多应用程序都是基于事件驱动的。

在视频游戏中,应用程序是实时的,意味着无论一些事件是否发生,都不会阻止程序在其生命期内执行许多任

务。如果在游戏控制器上用户按下按钮,它会被游戏循环中的更新阶段所检测到,游戏再来响应该事件。如果没有

事件发生,游戏也一直不断地渲染当前游戏状态(例如:渲染菜单,影片,游戏世界等等),执行逻辑的更新,查找

并且响应网络数据,播放声音,等等。

实时的和事件驱动的程序都一直运行直到用户退出为止。这里介绍应用程序循环的概念,程序循环就是一个无

限的循环,直到用户跳出该循环为止。由接受 WM_QUIT 事件(Win32 的退出消息)来发生结束指令,假如在主菜单中

设定了用户按下 Esc 键产生该事件(当游戏中时,按下 Esc 键并没有退出,而是暂停,除非是制作者设计了该退出方

式),或者你设定它退出的任何其他方式。本书中,所有 Demo 退出都设计为按下 Esc 键退出,或者点击窗口的右上

角的“X”按钮,或在 Xbox360 游戏控制器上按下 Back 按钮。

清单 2.3 展示了我们在此书中还没有联系 D3D 或者其他 Demo 的,使用程序循环的一个例子。在清单 2.3 中的

注释,稍后将由与 D3D 关联的代码所替代。如果我们实现了状态管理系统,用于游戏菜单和游戏接口或者其他的,

我们将在它本身的循环中初始化一次 Demo 和退出。每次运行时,状态管理不允许初始化和关闭状态超过一次,因

此它工作得很好。在 www.UltimateGameProgramming.com 网站有一些游戏菜单的材料,这些高级主题不属于本书的

范围。

清单 2.3:程序循环。

#include <Windows.h> int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR cmdLine, int nShow) { UNREFERENCED_PARAMETER(hPrevInstance); UNREFERENCED_PARAMETER(cmdLine); …… //Demo Initialize MSG msg = {0}; while(msg.message != WM_QUIT) { if(PeekMessage(&msg, 0, 0, 0, PM_REMOVE)) { TranslateMessage(&msg); DispatchMessage(&msg); } else { //Update //Draw } } //Demo Shutdown return static_cast<int>(msg.wParam); }

操作符 static_cast<>用于 C++中的强制转换,而 C 风格,例如 int a = (int)floatVar,尽管在 C++中是合法的却不被

推荐。使用 C++的强制转换来代替 C 风格的是一种好的编程习惯。该操作符最好是用于数值数据之间的强制转换,

像清单 2.3 中那样。但如果用该操作符强制转换基类指针到继承类或者相反,都是不安全的。在此种情况最好是使

用 dynamic_cast<>操作符(译者注:可以看《Effective C++》)。Static_cast<>不像 dynamic_cast<>一样做运行时类型检

测。当转换发生在指针之间时,使用 dynmic_cast<>更加的安全。虽然 C 风格的转换是合法的,但是对于以下几点

需要保持注意:

C++风格的强制转换使用的目的很清楚(dynamic_cast 用于指针,const_cast 用于常量类型等)。

如果使用 C++风格的转换尝试非法转换的话,编译器将给与一个错误信息。

使用 C++风格比使用 C 风格的强制转换,在代码中更容易被识别(在 VStudio 中 C++风格的将高亮显示)。

C 风格的强制转换作用于多重继承对象或者转换对象地址到 char*类型,在之上使用指针运算是未定义的行为。

C 风格的更容易出错。

在清单 2.3 中,MSG 是一个用于持有来自于操作系统中 window 消息的 Win32 结构,它用于程序响应这些消息。

如果发送一定数量的消息后程序一直没有响应,则操作系统将会报告该程序未响应。通常我们假定它的意思就是程

序被冻结或者发生了某些错误,这也意味着应用程序没有从操作系统中恢复过来。如果程序被设计为响应式的,假

设程序已经被冻结即窗口显示“未响应”消息或任务管理器显示该应用未响应,过一会儿它仍然是安全的。我就曾

经多次运行十分复杂并且长时间查询在网络上的微软 Access 数据库,操作系统就报告程序未响应,尽管程序只是忙

于做一些任务没有处理事件而已。

对于 window 消息,我们需要做两件事。首先需要取得新的消息并且处理它们,其次需要调度(响应)这些消息。

PeekMessage 函数取得相关窗口的消息(我们使用 CreateWindow 函数创建的窗口)。此函数的第一个参数是持有消息

的结构的地址,其次是窗口句柄(可选),最小最大消息过滤标志,移除标志。PM_REMOVE 移除标志,将会将消息从

它的队列中移除。因为我们处理该消息,一旦处理后它将不需要留在队列中。

如果有消息被 PeekMessage 函数获得,我们调用 TranslateMessage 函数和 DispatchMessage 函数来响应该消息。

Win32 函数 TranslateMessage 将虚拟键消息翻译为字符消息,调度函数 DispatchMessage 将消息传给窗口过程处理,

这将在下一部分讲解。窗口过程将对于它所接受到的消息执行实际的行动。

如果没有收到消息,在游戏中就只做更新 Update 和渲染 rendering 这两件事。在本书中与之对应的就是根据所

侦测到的用户输入,物理计算,动画,更新音频缓存等的更新操作和渲染场景几何体。

查看游戏循环就一目了然了,对于每一帧的渲染做一系列的具体事情。游戏中的帧是游戏循环的单一的反复过

程。大多数游戏力求达到每秒 30 或 60 帧,换句话说就是相对于真实世界中的每一秒钟,游戏做 30 到 60 次的循环。

当游戏变得越来越复杂时,这一目标更难达到。每秒的帧数通常用来衡量游戏渲染的速度,但是游戏不仅仅有渲染

代码,还需要每数帧就处理游戏中的物理,碰撞,AI(人工智能),声音,数据流和游戏的特效。

在应用程序的循环中我们执行一个游戏循环。清单 2.3 中我们添加注释的地方,在稍后的 Demo 中用实际的

Update 和 Render 函数代替。因为我们还没涉及到 D3D(Direct3D)部分,这稍后讲解。

Blank 程序中 WinMain 函数最后一行返回 0。通常在应用程序中最后返回退出代码,当在另一个应用程序中启

动一个应用程序,当被启动的程序退出时表明其状态。稍后,一旦我们添加了 D3D 代码,我们将返回退出代码信息,

虽然我们不论返回什么对我们的目的没有关系。甚至在 C++控制台程序中,大部分情况也仅仅是返回 0 而已。

在 Blank 程序的 WinMain 函数中,我们已经讨论过的全部信息见于清单 2.4。

清单 2.4:Blank 应用 Demo 的 WinMain 的全部代码。

#include <Windows.h> LRESULT CALLBACK WndProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam); int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR szCmd, int nShow) { WNDCLASSEX cls; cls.cbSize = sizeof(cls); cls.hbrBackground = (HBRUSH)GetStockObject(WHITE_BRUSH); cls.hCursor = LoadCursor(NULL, IDC_ARROW); cls.style = CS_HREDRAW | CS_VREDRAW; cls.lpfnWndProc = WndProc; cls.hInstance = hInstance; cls.lpszMenuName = NULL; cls.lpszClassName = "DX11BookWindowClass";

if(!RegisterClassEx(&cls)) return -1; RECT rc = {0, 0, 640, 480}; AdjustWindowRect(&rc, WS_OVERLAPPEDWINDOW, FALSE); HWND hwnd = CreateWindow(cls.lpszClassName, "Blank Win32 Window", WS_OVERLAPPEDWINDOW, 0, 0, rc.right - rc.left, rc.bottom - rc.top, 0, 0, hInstance, 0); if(!hwnd) return -1; ShowWindow(hwnd, nShow); //Demo Initialize MSG msg = {0}; while(msg.message != WM_QUIT) { if(PeekMessage(&msg, 0, 0, 0, PM_REMOVE)) { TranslateMessage(&msg); DispatchMessage(&msg); } //Update //Draw } //Demo Shutdown return static_cast<int>(msg.wParam); }

窗口过程

在我们能够编译和运行我们的程序之前的最后一部分迷题就是所提供的窗口过程,也就是窗口过程函数。清单

2.4 代码的前面部分有该函数的前置声明,在 WinMain 函数中被赋值给 WNDCLASSEX 结构的一个指针成员。窗口过

程函数是回调函数,意味着在我们的程序中当获得消息后就调用该函数来处理。Blank 程序 Demo 的窗口过程函数

见于清单 2.5。

清单 2.5:Blank 窗体 Demo 的窗口过程函数。

LRESULT CALLBACK WndProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) { PAINTSTRUCT ps; HDC hDC; switch(message) { case WM_PAINT: hDC = BeginPaint(hwnd, &ps); EndPaint(hwnd, &ps); break; case WM_DESTROY: PostQuitMessage(0); break; default: return DefWindowProc(hwnd, message, wParam, lParam); } return 0; }

窗口过程函数返回 LRESULT 类型并且有回调 CALLBACK 修饰。此函数的命名遵循普通函数命名标准,但是在本

书中都用 WndProc 这个名字。该回调函数接受参数窗口的句柄来调度该窗口的消息,无符号整型消息代号,两个可

携带额外信息的参数(wParam 和 lParam)。最后两个参数用于补充的数据,在回调函数中处理消息需要更多的数据时。

在 Win32 编程中有关于该主题更多的知识,本书中我们都不使用最后两个参数。

在 Blank 程序的 Demo 中我们响应绘制消息(WM_PAINT)和退出消息(WM_DESTROY)。绘制消息被 Win32 函数用

来处理窗口背景的绘制,通过调用 BeginPaint 函数和 EndPaint 函数来处理绘制。因为 D3D 将会做我们的所有需要的

渲染的情况,只是这里必须使用该消息而已。在该消息中我们没有具体的 D3D 代码,但是它要求被响应。

退出消息由调用 Win32 函数 PostQuitMessage 来处理,这将导致在应用程序循环中的 MSG 对象取得 WM_QUIT

消息,然后就按照我们的设定结束应用程序循环并且退出程序。假如想退出而又不立即退出的话,只需要发送一个

退出消息即可。例如,在微软的 Word 软件中如果没有保存就尝试退出的话,将会弹出一个对话框询问你是否保存

之后再退出。

窗口过程函数的最后一部分就是调用 DefWindowProc 函数,它有着与窗口过程函数同样的参数。这个函数只被

那些我们没有写自定义响应代码的消息所调用。在此 Demo 中我们只响应了绘制消息和退出消息,但是还有其他大

量的可以查到的消息,如按键,鼠标点击,鼠标移动,计时器等等。DefWindowProc 函数是一个 Win32 函数,它调

用默认行为处理所有的消息,来确保我们响应每一个消息,即使我们对那些消息不感兴趣。

这个时候,你可以编译和构建 Demo 了。如果没有敲错代码的话就是 0 警告 0 错误,并且运行程序的话将会弹

出一个白色的窗口。为了关闭此窗口,可以点击右上角的“X”按钮,点击后将导致发生退出消息,然后跳出程序

循环,程序成功退出。Blank 窗口 Demo 运行时的截图如 2.5 所示。

该 Demo 的代码和工程能够在本书的配套网站第二章的目录 BlankWindow 中找到。

插图 2.5:Blank Win32 窗口 Demo 运行时截图

Direct3D

现在准备添加 D3D11 的代码到 Blank 窗口 Demo 中,来创建一个新的叫 Blank Direct3D 窗口的 Demo。该 Demo

能够在配套代码中的第二章的 BlankD3DWindow 目录中找到。该部分我们首先讨论从 Direct3D 11 中开始必须用到的

所有的 D3D 函数和对象。下一部分我们再讲解 Blank Direct3D Demo 的细节。在具体 Demo 之外,首先讨论 D3D 开

始所必须的事情,对于初学者来说将有利于 Blank D3D Demo 的实现。

更随 Blank D3D Demo 的创建,先创建标题为 BlankD3DWindow 的新工程,并且从 Blank 窗体 Demo 中拷贝

main.cpp 文件,该新 Demo 将在之上写代码。

增加 DirectX 的库

在开始写代码之前,先包含我们需要用到的 DirectX 11 的库。为了链接 DX 库,右键项目选择属性->VC++目录,

在包含目录中添加$(DXSDK_DIR)Include,在库目录中添加$(DXSDK_DIR)Lib\x86(译者注:这段翻译减省了,原文太罗

嗦。安装好 DXSDK 后需要在我的电脑属性->高级系统设置->环境变量->系统变量中新建 DXSDK_DIR,将安装的 DX 路

径写入即可)。在链接器->输入的附加依赖项中添加 d3d11.lib,d3dx11.lib,和 dxerr.lib 库(如图 2.6)。对于 Debug 和

Release 版本做同样的操作即可。这些操作之后重新构建项目,确保能够找到这些库没有错误报告即可。如果报告

错误,检查安装的 DX SDK 的路径或者键入路径时的拼写错误。

插图 2.6:VC++目录(左)和链接器(右)

初始化 Direct3D

为了准备 Direct3D,我们需要完成如下四个步骤:

1.定义我们想检查的设备类型和特征级别

2.创建 Direct3D 设备,渲染环境和交换链

3.创建渲染对象

4.设置视口观察区(Viewport)

在此部分,我们将讨论每一步骤覆盖到各种需要使用到的 D3D 对象和函数细节。

驱动设备类型和特征级别

在 Direct3D 11 中我们可以使用硬件设备,WARP 设备,软件驱动设备,或者是引用设备(参考设备)。

硬件设备是运行在图形硬件上的 D3D 设备,在所有设备中是最快的。在这种设备上能够创建图形硬件所支持的

特性,稍后我们就会讨论到。

引用设备用于在 CPU 上执行图形硬件所不支持的特性渲染。换句话说,就是引用设备是在软件中完全模拟硬件

渲染。这种处理方式十分的慢并且低效,仅仅在没有其他替代设备的时候使用。这种方式对于新的 DirectX 版本发

布时却是有益的,因为此时在市场上还没有能运行新特性的硬件设备。

软件驱动设备允许开发者编写自己的软件渲染驱动用于 Direct3D 中,称为插件式的软件驱动。通常该方式不被

推荐用于高性能,对硬件要求苛刻的程序,甚至选择 WARP 设备都比它好。

WARP 设备是一种高效 CPU 渲染设备,可模拟 Direct3D 的全部特性。WARP 设备使用 Vista 和 Win7 系统中的图

形运行时,由高度优化的代码和指令所编写,是一种比引用设备更好的选择。该设备用于低端机器也能够实现我们

所希望看到的游戏极端性能。在实时应用中如果硬件设备还没有有效的支持,而引用设备又嫌太慢时,这种方式一

个很好的选择。当然由于 WARP 设备是一种软件模拟,不能和硬件设备相比。(注意:还有很多不知名的设备类型)

Direct3D 的不同特征级别允许我们对于想要的目标使用具体的某一特征,消除了从 DX9.0 以及以前的版本对设

备能力的依赖。本书我们希望使用三种设备目标,首先是 D3D 11.0 设备,其次是 D3D 10.1 设备,最后是 D3D 10.0

设备。如果没有支持 DX 11 类型的硬件,则 Demo 就运行于 D3D 10.1 或者 10.0 作为替代,如果后两个设备被硬件所

支持,则不需要修改任何代码。如果没有硬件支持上述设备的任何一种,我们将尝试在 WARP 或者引用模式中查找

能够支持的一种 DX 特征。

清单 2.6 显示了我们稍后要用到的驱动类型和特征级别声明。通过数组创建每一种类型,就能够在循环失败之

前的尝试创建最希望的设备类型。该部分中 Win32 宏 ARRAYSIZE 能够测试数组的大小,GetClientRect 函数用于计算

程序的客户区大小,用于 D3D 设备渲染的宽度和高度。记住 Win32 应用程序有客户区域和非客户区域之分,我们

只渲染客户区域。

清单 2.6:列举的驱动类型和特征级别。

RECT dimensions; GetClientRect( hwnd, &dimensions ); unsigned int width = dimensions.right - dimensions.left; unsigned int height = dimensions.bottom - dimensions.top;

D3D_DRIVER_TYPE driverTypes[] = { D3D_DRIVER_TYPE_HARDWARE, D3D_DRIVER_TYPE_WARP, D3D_DRIVER_TYPE_REFERENCE, D3D_DRIVER_TYPE_SOFTWARE }; unsigned int totalDriverTypes = ARRAYSIZE( driverTypes ); D3D_FEATURE_LEVEL featureLevels[] = { D3D_FEATURE_LEVEL_11_0, D3D_FEATURE_LEVEL_10_1, D3D_FEATURE_LEVEL_10_0 }; unsigned int totalFeatureLevels = ARRAYSIZE( featureLevels );

设备和交换链的创建

下一步骤就是创建交换链。Direct3D 中的交换链是设备中的渲染目标的集合。每个设备至少有一个交换链,而

多个交换链能够被多个设备所创建。一个渲染目标可以有一个已经渲染好显示在屏幕的颜色缓存,一个深度缓存(在

第三章,“2D 的复兴”中讨论),一个模板缓存等。

通常在游戏中,我们有两种颜色缓存,其一是主缓存,其二是辅助缓存,这就是所谓的前向和后向缓存。主缓

存(前向缓存)是显示在屏幕上的,而辅助缓存(后向缓存)被用于下一帧的绘制。

渲染发生的非常快,屏幕能够绘制在显示器完成显示更新之前的结果之上,这就是渲染画面出现不可预测的问

题的原因。在游戏中切换缓存,使得一个用于写入,一个用于显示输出就可以消除这样的问题。在计算机图形学中

这种乒乓技术就是双缓存(也叫页面翻转)。一个交换链能够有多个这样的缓存,D3D 在这些缓存页之间进行翻转。

清单 2.7 描述了交换链的创建。一个交换链的描述被用于我们想创建怎样的交换链。它有如下成员:

缓存页数量(用于页面翻转的主/辅助缓存)。

缓存页的宽度和高度。

缓存格式(这章稍后“格式”那部分有描述)。

刷新率,用于决定刷新显示的频率(使用 60/1 表示 60Hz 的刷新频率)。

窗口句柄(与用 CreateWindow 函数创建的一样)。

一个叫 Windowed 字段的布尔值,用于描述是否 D3D 在全屏模式中应该继续原来的尺寸还是 resize。

取样描述中的取样数量和质量。

清单 2.7:交换链的描述

DXGI_SWAP_CHAIN_DESC swapChainDesc; ZeroMemory( &swapChainDesc, sizeof( swapChainDesc ) ); swapChainDesc.BufferCount = 1; swapChainDesc.BufferDesc.Width = width; swapChainDesc.BufferDesc.Height = height; swapChainDesc.BufferDesc.Format = DXGI_FORMAT_R8G8B8A8_UNORM; swapChainDesc.BufferDesc.RefreshRate.Numerator = 60; swapChainDesc.BufferDesc.RefreshRate.Denominator = 1; swapChainDesc.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT; swapChainDesc.OutputWindow = hwnd; swapChainDesc.Windowed = true; swapChainDesc.SampleDesc.Count = 1; swapChainDesc.SampleDesc.Quality = 0;

取样描述定义了 D3D 中的多重取样性质,多重取样是一种用于像素间的取样和平均渲染来创建在形状颜色间平

滑过渡的技术。我们试图使用多重取样来减少所谓的锯齿边,即阶梯效果。图 2.7 显示了这种阶梯效果(左),和使

用多重取样得到的平滑效果(右)。如果锯齿边很明显,则在游戏中看起来就不美观了。

插图 2.7:锯齿边 vs 平滑显示

交换链中的缓存用法和描述有最多的设置,这些都是很直观的。我们交换链中缓存的用法被设置为

DXGI_USGAE_RENDER_TARGET_OUTPUT,意味着该交换链能够用于输出,即它能够被渲染。

下一步骤就是创建渲染环境,设备和交换链,现在我们已经有关于交换链的描述。Direct3D 设备是一种用于自

身与硬件通信的设备。Direct3D 渲染环境是一种告诉设备怎样绘制的渲染环境,它包括渲染状态和其它的绘制信息。

交换链我们已经讨论过了,是一种设备和环境的渲染目标。渲染设备,环境和交换链见于清单 2.8。Direct3D 设备类

型是 ID3D11Device,渲染环境类型是 ID3D11Context,交换链类型是 IDXGISwapChain。稍后的 Blank Direct3D 窗口

Demo 中的 D3D 设置代码片段就来自于清单 2.8。

清单 2.8:创建 Direct3D 设备,环境和交换链。

ID3D11Device* d3dDevice_; ID3D11DeviceContext* d3dContext_; IDXGISwapChain* swapChain_;

unsigned int creationFlags = 0; #ifdef _DEBUG creationFlags |= D3D11_CREATE_DEVICE_DEBUG; #endif HRESULT result; unsigned int driver = 0; for( driver = 0; driver < totalDriverTypes; ++driver ) { result = D3D11CreateDeviceAndSwapChain( 0, driverTypes[driver], 0, creationFlags, featureLevels, totalFeatureLevels, D3D11_SDK_VERSION, &swapChainDesc, &swapChain_, &d3dDevice_, &featureLevel_, &d3dContext_ ); if( SUCCEEDED( result ) ) { driverType_ = driverTypes[driver]; break; } } if( FAILED( result ) ) { DXTRACE_MSG( "Failed to create the Direct3D device!" ); return false; } ID3D11Texture2D* backBufferTexture; result = swapChain_->GetBuffer( 0, __uuidof( ID3D11Texture2D ), ( LPVOID* )&backBufferTexture ); if( FAILED( result ) ) {

DXTRACE_MSG( "Failed to get the swap chain back buffer!" ); return false; }

交 换 链 , 设 备 环 境 和 渲 染 环 境 能 够 通 过 一 个 Direct3D 函 数 所 全 部 创 建 , 该 函 数 就 是

D3D11CreateDeviceAndSwapChain,或者是调用对每一类型具体的 D3D 函数创建(例如:CreateSwapChain 仅仅创建交

换链)。清单 2.8 中,我们循环每一种驱动类型,尝试依次创建硬件设备,WARP 设备,以及引用设备。如果所有的

尝试都失败了,则我们就不能初始化 Direct3D。D3D11CreateDeviceAndSwapChain 函数也使用参数特征级别数组,如

果至少有一种特征级别以及至少有一种驱动类型存在,则该函数会被成功调用。

D3D11CreateDeviceAndSwapChain 函数的参数值如下:

1.一个用于创建设备的视频适配器(译者注:显卡)的指针。如果传入 NULL,则 D3D 使用默认显卡。如果在

机器上装有多个显卡,就可启用该参数。

2.我们希望创建的驱动设备类型(例如:硬件设备,WARP 设备,软件设备,或参考设备)

3.实现软件渲染设备的动态库句柄。如果使用的驱动设备类型是软件设备,则该参数不能为 NULL。

4.创建标志。D3D 中的创建标志 0 用于我们的游戏发布,而标志 D3D11_CREATE_DEVICE_DEBUG 则允许我

们创建可供调试的设备,在开发中这是比较有用的。

5.我们所希望创建的特征级别,以我们的希望值进行特征的安排。此书中我们只关心 D3D 11 或 Direct3D 10

的创建,当然我们也能够通过 D3D_FEATURE_LEVEL_9_3, D3D_FEATURE_LEVEL_9_2, D3D_FEATURE_LEVEL_9_1 来

创建 Direct3D 9 的目标。

6.特征级别数组中的特征数量。

7.SDK 版本号。因为我们只使用 DirectX 11 SDK,所以书中都是 D3D11_SDK_VERSION。

8.交换链描述对象。

9.设备对象地址(ID3D11-Device 类型)。

10.所选择的特征级别地址,存储特征级别数组中,被创建成功的特征级别。

11.渲染环境地址(ID3D11Context 类型)。

创建渲染目标视图

渲染目标视图是写入联合输出阶段的一种 Direct3D 资源。为了在交换链的向后缓存(辅助缓存)中联合渲染,于

是我们创建渲染目标视图。

在第三章中我们将细致的讨论贴图,但是现在需要知道贴图是一张图像。交换链中的主缓存和辅助缓存其实是

一种颜色贴图,可通过调用交换链的函数 GetBuffer 来获得它的指针。GetBuffer 参数如下:

1.缓存索引(0 给出第一个缓存)。

2.尝试操纵的接口类型。2D 贴图类型就是 ID3D11Texture2D。

3.获得的缓存地址,必须使用 LPVOID 类型参数获得。

对于所获得的缓存指针,我们调用 D3D 设备对象的一个函数 CreateRenderTargetView,来创建渲染目标视图,

其类型就是 ID3D11RenderTargetView。该函数的参数有我们所创建的 2D 贴图视图,渲染目标描述,和我们要创建的

ID3D11RenderTargetView 对象的地址。将渲染目标描述设置为 NULL,将给我们物体表面的 Mip映射等级为 0 的视图。

有关 Mip 映射等级的更多讨论见于第三章。

一旦我们创建好渲染目标视图后,就可以释放交换链的后向缓存指针。因为我们获得一个 COM 对象的引用,

就必须要调用 COM的 Release 函数来减少该对象的一个引用(译者注:当 COM对象的引用数降为 0 时,就会被删除)。

释放步骤是必须要做的,否则将会发生内存泄露。

每当我们想渲染具体的渲染目标时,都必须在任何绘制调用之前设置它,通过调用 OMSetRenderTarget 函数来

完成,该函数是联合输出的一部分(因此 OM output merger 在 OMSetRenderTarget 中)。该函数的输入参数是绑定的

视图数量,渲染目标视图列表,深度/模板视图。在第三章中将讨论深度和模板 stencil 视图。清单 2.9 显示了渲染目

标视图的创建和绑定。

清单 2.9:渲染目标视图的创建和绑定

ID3D11RenderTargetView* backBufferTarget_;

ID3D11Texture2D* backBufferTexture;

result = swapChain_->GetBuffer( 0, __uuidof( ID3D11Texture2D ), ( LPVOID* )&backBufferTexture );

if( FAILED( result ) )

{

DXTRACE_MSG( "Failed to get the swap chain back buffer!" );

return false;

}

result = d3dDevice_->CreateRenderTargetView( backBufferTexture, 0, &backBufferTarget_ );

if( backBufferTexture )

backBufferTexture->Release( );

if( FAILED( result ) )

{

DXTRACE_MSG( "Failed to create the render target view!" );

return false;

}

d3dContext_->OMSetRenderTargets( 1, &backBufferTarget_, 0 );

读者可能注意到清单 2.9 中使用了 DXTRACE_MSG 宏。该宏用于调试,本章后面有关于它和其他的 DirectX Error

处理库的更多讨论。

视口 Viewport

Direct3D 11 迷惑的最后一部分是视口的创建和设置。视口 Viewport 定义为我们要在屏幕上渲染的区域。在单人

或者非分屏多人游戏中它就是整个的屏幕,这种情况下我们只需简单的设置视口的宽高为 D3D 交换链的宽高即可。

对于分屏游戏,我们需要创建两个(译者注:或多个视视口),一个定义为屏幕的上半部分,另一个是下半部分。为

了渲染分屏视图,我们渲染场景时必须渲染一次玩家 1 的视景(perspective),渲染一次玩家 2 的视景。虽然多人游戏

超出了本书的范围,可是有一个关于创建分屏的 Demo 能够在 UltimateGameProgramming 网站上找到。

视口 Viewport 通过填充 D3D11_VIEWPORT 对象来创建,通过调用设备环境的 RSSetViewports 函数来设置它作为

渲染环境。该函数参数有我们设置的视口数量和视口对象列表。清单 2.10 展示了创建和设置全屏视口,其中 X 和 Y

坐标标识屏幕左上角,设置了视口的最小最大深度。

清单 2.10:创建和设置全屏视口。

D3D11_VIEWPORT viewport; viewport.Width = static_cast<float>(width); viewport.Height = static_cast<float>(height); viewport.MinDepth = 0.0f; viewport.MaxDepth = 1.0f; viewport.TopLeftX = 0.0f; viewport.TopLeftY = 0.0f; d3dContext_->RSSetViewports( 1, &viewport );

屏幕的清除和显示

屏幕的渲染发生在几个不同步骤中。第一步通常是清除任何必须要渲染目标的表面。在大多数游戏中它还包括

缓存例如深度缓存清除,在第三章有更多的讨论。在稍后实现的 Blank Direct3D Window Demo 中我们使用一种具体

的颜色来清除渲染目标视图的颜色缓存。这通过调用 D3D 环境的 ClearRenderTargetView 函数来实现。该函数原型如

下:Void ClearRenderTargetView( __in ID3D11RenderTargetView *pRenderTargetView, __in const FLOAT ColorRGBA[ 4 ]);

(注意:在大部分商业游戏中,渲染之前并不一定需要清除颜色缓存,因为天空和环境的渲染会确保每一个像素都会

被覆盖到,从而清除步骤不是必须的。)

可以看到该函数的参数是渲染目标环境和用于清除的颜色值。为了清除屏幕,我们采用一个具体的颜色作为我

们想要的背景遮罩色。该颜色是一个值范围在 0.0 至 1.0 的红,绿,蓝,alpha 的颜色分量数组。其中 0.0 表示该分

量的最小强度,而 1.0 表示该分量达到最大强度。在字节表示中,1.0 表示 255。如果颜色分量的红绿蓝值都为 1.0,

则得到的是白色。第三章有更多关于颜色的讨论。

下一步是绘制场景的几何体。本章中我们不绘制任何几何体,第三章中有关于此主题的细节。

最后一步是调用交换链的 Present 函数将已经渲染好的缓存显示在屏幕上。Present 显示函数原型如下:

HRESULT IDXGISwapChain:: Present( UINT SyncInterval, UINT Flags);该函数的参数是同步区间和显示标识。如果同步区

间是 0 表示立即绘制,1,2,3,4 表示在第 n 个垂直消隐(vertical blanking)之后绘制。一个垂直消隐是当前帧最后一行更

新完毕与下一帧第一行更新时的时间差。计算机显示器设备更新显示像素是垂直一行行的更新。该函数的显示标识,

如 果 是 0 则 输 出 每 个 缓 存 , 是 DXGI_PRESENT_TEST 则 用 于 测 试 目 的 不 输 出 任 何 东 西 , 或 者

DXGI_PRESENT_DO_NOT_SEQUENCE 用于不按照利用垂直消隐同步顺序的输出(译者注:即乱序输出)。对于我们的目

的来说只需要传人两个都为 0 的参数即可得到想要的渲染显示结果。

清单 2.11 显示了一个清除屏幕并且在之上显示视图的例子。在第三章中我们将深入探讨颜色缓存,深度缓存,

用于平滑动画的双缓存等的渲染。本章焦点是学习 Direct3D 的设置。如果执行这段代码,清单 2.11 将会显示深蓝色

的背景。

清单 2.11:清除渲染目标和显示新的渲染场景。

float clearColor[4] = { 0.0f, 0.0f, 0.25f, 1.0f };

d3dContext_->ClearRenderTargetView( backBufferTarget_, clearColor );

swapChain_->Present( 0, 0 );

清理 Cleaning Up

任何 Direct3D 程序的最后一件事情是清理并且释放你所创建的对象。例如在程序开始时,你创建了一个 Direct3D

设备,一个 D3D 渲染环境,一个交换链,以及一个渲染目标视图,当程序结束时,你必须释放这些对象将资源归还

系统进行再利用。COM 对象持有一个引用计数,告诉系统什么时候将它从内存中删除是安全的。通过使用 Release

函数,将会减少对象的引用数,当引用计数为 0 时,系统将对象删除以便回收再利用。

释放 D3D 对象的一个例子见于清单 2.12。在清单中首先检查对象确保不为 NULL,再对它们调用 Release 函数。

以它们所创建的逆序释放对象是一个好的主义。

清单 2.12:释放所创建的 Direct3D 11 对象。

if( backBufferTarget_ ) backBufferTarget_->Release( );

if( swapChain_ ) swapChain_->Release( );

if( d3dContext_ ) d3dContext_->Release( );

if( d3dDevice_ ) d3dDevice_->Release( );

(小提示:在对象调用 Release 函数之前总是检查确保对象非 NULL,可以减少由于无效指针导致你的游戏由于未

定义行为所产生的崩溃。)

格式 Formats

有时你会被要求提供具体的 DXGI 格式。不同格式用于描述不同的事物,比如描述图像的布局,每一颜色使用

的 bits 数,或者对于顶点缓存(第三章)的顶点布局。最常见的是,DXGI 格式用于描述交换链中的缓存布局。DXGI 格

式不是具体的某一类型的数据,只是描述它们是什么结构。

一个 DXGI 格式的例子是,DXGI_FORMAT_R8G8B8A8_UNORM 用于描述数据对于 RGBA 的每一部分用 8bits 来存

储。当定义顶点,使用 DXGI_FORMAT_R32G32B32_FLOAT 格式时,将对于三个部分都用 32bits 来存储。尽管格式可

以是 RGB,它只是描述数据的布局,而不管数据用于何处。

有时你可以看到对于每部分有相同的比特数但是后缀不同的格式。例如 DXGI_FORMAT_R32G32B32A32_FLOAT

和 DXGI_FORMAT_R32G32B32A32_UINT,这表示它们描述的对象的每一部分使用两种格式都使用相同数量的 bit,但

是包含这些 bit 的数据类型不同。这里考虑全部类型的格式。

对于没有声明类型的对象可以用无类型的格式。使用此种格式表明对象的每一部分使用的 bits 数量而不关心所

包含的数据类型,例如 DXGI_FORMAT_R32G32B32A32_TYPELESS 格式。公共格式列表见于表 2.1 中。

表 2.1:Direct3D 公共格式

格式 描述

DXGI_FORMAT_R32G32B32A32_TYPELESS 组成无类型 RGBA 分量的 128 位格式

DXGI_FORMAT_R32G32B32A32_FLOAT 浮点类型 RGBA 分量的 128 位格式

DXGI_FORMAT_R32G32B32A32_UINT 无符号整型 RGBA 分量的 128 位格式

DXGI_FORMAT_R32G32B32A32_SINT 有符号整型 RGBA 分量的 128 位格式

DXGI_FORMAT_R8G8B8A8_TYPELESS 组成无类型 RGBA 分量的 32 位格式

DXGI_FORMAT_R8G8B8A8_UINT 无符号整型 RGBA 分量的 32 位格式

DXGI_FORMAT_R8G8B8A8_SINT 有符号整型 RGBA 分量的 32 位格式

Blank D3D Window Demo

该 Demo 能够在 Chapter2/BlankD3DWindow/目录中找到。此部分我们将使用在上一节获得的 Direct3D 11 的知

识来一步步的创建该 Demo。在这完成的 Demo,其基础部分可以用于本书后面的所有 Demo。

模板框架的设计

安装,关闭,和初始化非具体 Demo 所特有的 D3D 对象是十分简单的,我们将写个新的 Demo 来避免每次都重

写上述相同的代码。有时我们想直接取得 Demo 本身,而必要每次重写标准的 Win32 和 Direct3D 安装代码。尽管这

不是一个很大的决定,但是它避免了每次用粘贴复制的方式的麻烦。

为了利用上述这一点,我们将在同一个源文件中编写所有的 Direct3D 启动和清除的代码,在以后的所有 Demo

中重复使用。为了实现这一点,我们创建一个叫 DX11DemoBase 的基类,而当创建具体的 Demo 时,只需要继承该

基类重写其中的虚拟函数用于具体的 Demo 即可。本部分我们将全部覆盖 BlankDirect3D Demo 的讲解并且创建用于

以后所有 Demo 的一些基础。

Direct3D 类

为什么要使用类?为什么不使用全局变量和函数来实现它们,就像许多其他书籍,材料中的一样?还有很多类

似于上述的问题,它们大多来自于好的和坏的编程实践的比较。

有很多原因使得在编程中使用全局变量看起来是自顶向下编程。事实上使用它们是一种坏的编程习惯,应该避

免使用它们。另一种方案是使用对象作为参数,但是在我们的应用程序中有着大量的对象需要传递给有关的每一个

函数。当然,我们需要创建一个类或者结构来存储它们进行传递,使得最后用一个具体的 Demo 类来继承它是有道

理的,而以 C 方式来做的话是不合理的。

在我们的计划中,Demo 是它自己的一个对象。它能够载入它所需要的内容,渲染它所需要的渲染,更新所需

的更新。它有单独的目的并且执行单一的任务。现在的问题是我们需要我们的基类中的什么?作为开始,我们需要

函数做如下的事情:

1. 初始化 Direct3D

2. 释放在开始时创建的 Direct3D 对象

3. 存储一些非具体 Demo 的公共 Direct3D 对象

4. 提供一种方式载入具体 Demo 的内容

5. 提供卸载具体 Demo 内容的方式

6. 提供每帧更新具体 Demo 的能力

7. 提供可重写的具体 Demo 的渲染函数

通过判断我们已经识别的所需的功能列表,使得创建基类需要具有如下性质:公共的初始化和关闭的函数,虚

拟函数用于载入和卸载具体 Demo 的内容,虚拟函数用于渲染和游戏循环中的每一步的更新。通过在具体 Demo 中

实现这些虚拟函数,从该基类中继承的类能够实现它们自定义的逻辑和行为。DX11DemoBase 基类的定义如清单 2.13

所示:

清单 2.13:Demo 基类的定义

#ifndef _DEMO_BASE_H_

#define _DEMO_BASE_H_

#include<d3d11.h>

#include<d3dx11.h>

#include<DxErr.h>

class Dx11DemoBase

{

public:

Dx11DemoBase();

virtual ~Dx11DemoBase();

bool Initialize( HINSTANCE hInstance, HWND hwnd );

void Shutdown( );

virtual bool LoadContent( );

virtual void UnloadContent( );

virtual void Update( float dt ) = 0;

virtual void Render( ) = 0;

protected:

HINSTANCE hInstance_;

HWND hwnd_;

D3D_DRIVER_TYPE driverType_;

D3D_FEATURE_LEVEL featureLevel_;

ID3D11Device* d3dDevice_;

ID3D11DeviceContext* d3dContext_;

IDXGISwapChain* swapChain_;

ID3D11RenderTargetView* backBufferTarget_;

};

#endif

在清单 2.13 中我们可以看到所有我们需要的 Direct3D 对象以受保护的类成员包含在基类中。如果具体 Demo

需要它们自己的对象,比如附加的渲染目标,只需要在其以继承方式的具体的 Demo 类中添加它们的成员。基类仅

仅包含所有 Demo 所共有的对象。

通过观察基类中的成员函数,我们可以看到只需要给与少量的方法我们就可以创建广泛的 Demo。基类中的构

造函数的工作就是将成员对象设为默认值 NULL,为了做到这一点我们使用构造函数的成员初始化列表,这是一种

在构造函数体之外的初始化成员变量的方法。这种初始化是一种好的编程习惯,比起在对象上拷贝构造函数之前调

用默认构造函数的初始化方法更有效。由于我们的 Direct3D 对象没有构造函数,所以它们这样做是类似的。但是一

般地运行中来初始化所需对象,是一种避免不必要的构造函数调用的好主意。尽管内建类型的初始化在初始化列表

中进行还是在构造函数体内进行其内部工作方式是一样的,因为它们是内建类型而不是对象。

基类的析构函数的任务由 Shutdown 函数完成,来确保当 Demo 超出其作用域后,所有的 Direct3D 对象被正确

的释放。Shutdown 函数调用我们所有的基于 COM 的 Direct3D 对象的 Release 函数进行。在 Shutdown 函数中释放我

们的具体 Demo 内容是十分重要的;因此该函数在释放基类中的公共对象时,首先调用 UnloadContent 函数来释放

具体 Demo 中的内容。

UnloadContent 函数来释放任何继承自基类的具体 Demo 类中的数据。因为并不是所有的 Demo 都需要自己的内

容,例如正在讲解的 BlankWindow Demo,从而该函数不需要一定是纯虚函数。同样的道理用于 LoadContent 函数,

它用于载入具体 Demo 的自己的内容,例如几何体,纹理图像,着色器,音频文件等。(注意:XNA Game Studio 也

是使用同样的主意,每个游戏都继承自 Game base class,并且重写具体的函数来实现各自游戏的行为。XNA 游戏开

发包是构建于 DirectX 之上的游戏开发框架,用于创建 Windows-PC 和 Xbox360 游戏)

Dx11DemoBase 基类的构造,析构,LoadContent,UnloadContent 和 Shutdown 函数实现见于清单 2.14。

清单 2.14:基类部分函数的实现

#include"Dx11DemoBase.h"

Dx11DemoBase::Dx11DemoBase( ) : driverType_( D3D_DRIVER_TYPE_NULL ), featureLevel_( D3D_FEATURE_LEVEL_11_0 ),

d3dDevice_( 0 ), d3dContext_( 0 ), swapChain_( 0 ), backBufferTarget_( 0 )

{}

Dx11DemoBase::~Dx11DemoBase( )

{

Shutdown( );

}

bool Dx11DemoBase::LoadContent( )

{

// Override with demo specifics, if any...

return true;

}

void Dx11DemoBase::UnloadContent( )

{

// Override with demo specifics, if any...

}

void Dx11DemoBase::Shutdown( )

{

UnloadContent( );

if( backBufferTarget_ ) backBufferTarget_->Release( );

if( swapChain_ ) swapChain_->Release( );

if( d3dContext_ ) d3dContext_->Release( );

if( d3dDevice_ ) d3dDevice_->Release( );

backBufferTarget_ = 0;

swapChain_ = 0;

d3dContext_ = 0;

d3dDevice_ = 0;

}

该基类中的最后一个函数是初始化 Initialize 函数,该函数执行本章中之前讲解过的 Direct3D 初始化工作。该初

始化函数中首先声明驱动设备类型硬件设备,WARP 设备,软件设备,和所期望创建的特征级别 Direct3D 11.0,10.1

和 10.0。初始化代码首先尝试在 Direct3D 11.0 中创建硬件设备,如果失败则依次尝试我们所安排的顺序直到找到一

种组合创建成功为止。

下一步是使用已经找到的驱动设备类型和特征级别来创建交换链描述,之后我们将创建交换链的后向(辅助)缓

存的渲染目标视图,创建视口 Viewport,并且调用 LoadContent 函数来载入具体的 Demo 游戏内容。因为我们的基

类 Direct3D 对象的初始化必须发生在具体 Demo 的初始化之前,所以 LoadContent 函数必须最后调用。通过在这包

含 LoadContent 函数我们不必要在类的外面单独调用它。对于在程序退出前 Shutdown 中调用 UnloadContent 函数也

是同样的理由。

Direct3D 11 初始化的全部代码见于清单 2.15。大多数代码都进行了错误检查。

清单 2.15:Dx11DemoBase 基类中的 Initialize 函数。

bool Dx11DemoBase::Initialize( HINSTANCE hInstance, HWND hwnd )

{

hInstance_ = hInstance;

hwnd_ = hwnd;

RECT dimensions;

GetClientRect( hwnd, &dimensions );

unsigned int width = dimensions.right - dimensions.left;

unsigned int height = dimensions.bottom - dimensions.top;

D3D_DRIVER_TYPE driverTypes[] =

{

D3D_DRIVER_TYPE_HARDWARE, D3D_DRIVER_TYPE_WARP,

D3D_DRIVER_TYPE_REFERENCE, D3D_DRIVER_TYPE_SOFTWARE

};

unsigned int totalDriverTypes = ARRAYSIZE( driverTypes );

D3D_FEATURE_LEVEL featureLevels[] =

{

D3D_FEATURE_LEVEL_11_0,

D3D_FEATURE_LEVEL_10_1,

D3D_FEATURE_LEVEL_10_0

};

unsigned int totalFeatureLevels = ARRAYSIZE( featureLevels );

DXGI_SWAP_CHAIN_DESC swapChainDesc;

ZeroMemory( &swapChainDesc, sizeof( swapChainDesc ) );

swapChainDesc.BufferCount = 1;

swapChainDesc.BufferDesc.Width = width;

swapChainDesc.BufferDesc.Height = height;

swapChainDesc.BufferDesc.Format = DXGI_FORMAT_R8G8B8A8_UNORM;

swapChainDesc.BufferDesc.RefreshRate.Numerator = 60;

swapChainDesc.BufferDesc.RefreshRate.Denominator = 1;

swapChainDesc.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT;

swapChainDesc.OutputWindow = hwnd;

swapChainDesc.Windowed = true;

swapChainDesc.SampleDesc.Count = 1;

swapChainDesc.SampleDesc.Quality = 0;

unsigned int creationFlags = 0;

#ifdef _DEBUG

creationFlags |= D3D11_CREATE_DEVICE_DEBUG;

#endif

HRESULT result;

unsigned int driver = 0;

for( driver = 0; driver < totalDriverTypes; ++driver )

{

result = D3D11CreateDeviceAndSwapChain( 0, driverTypes[driver], 0, creationFlags,

featureLevels, totalFeatureLevels,

D3D11_SDK_VERSION, &swapChainDesc, &swapChain_,

&d3dDevice_, &featureLevel_, &d3dContext_ );

if( SUCCEEDED( result ) )

{

driverType_ = driverTypes[driver];

break;

}

}

if( FAILED( result ) )

{

DXTRACE_MSG( "Failed to create the Direct3D device!" );

return false;

}

ID3D11Texture2D* backBufferTexture;

result = swapChain_->GetBuffer( 0, __uuidof( ID3D11Texture2D ), ( LPVOID* )&backBufferTexture );

if( FAILED( result ) )

{

DXTRACE_MSG( "Failed to get the swap chain back buffer!" );

return false;

}

result = d3dDevice_->CreateRenderTargetView( backBufferTexture, 0, &backBufferTarget_ );

if( backBufferTexture )

backBufferTexture->Release( );

if( FAILED( result ) )

{

DXTRACE_MSG( "Failed to create the render target view!" );

return false;

}

d3dContext_->OMSetRenderTargets( 1, &backBufferTarget_, 0 );

D3D11_VIEWPORT viewport;

viewport.Width = static_cast<float>(width);

viewport.Height = static_cast<float>(height);

viewport.MinDepth = 0.0f;

viewport.MaxDepth = 1.0f;

viewport.TopLeftX = 0.0f;

viewport.TopLeftY = 0.0f;

d3dContext_->RSSetViewports( 1, &viewport );

return LoadContent( );

}

Blank Window Demo 类

现在我们已经掌握运行 Blank Direct3D Window Demo 所需的任何知识。为了继承 Dx11DemoBase 基类和调用具

体的 Demo 类,BlankDemo 类的定义见于清单 2.16。

清单 2.16:BlankDemo 继承类的头文件。

#ifndef _BLANK_DEMO_H_

#define _BLANK_DEMO_H_

#include"Dx11DemoBase.h"

class BlankDemo : public Dx11DemoBase

{

public:

BlankDemo( );

virtual ~BlankDemo( );

bool LoadContent( );

void UnloadContent( );

void Update( float dt );

void Render( );

};

#endif

清单代码中 Update 函数有一个叫 dt 的输入参数。我们将会看到这个值在第三章的应用,但是现在需要了解的

是在游戏中我们经常需要实时的更新逻辑,而 dt 参数表示从上一帧已经过去的时间间隔,我们使用它来做基于时

间的更新。现在我们只需要忽略它直到在讨论动画时再提起。

因为 Demo 没有做任何特别的事情,而只是用 Direct3D 清除了屏幕而已,除了渲染函数外,所有的重写函数都

是空的。在渲染 Render 函数中我们只调用了两个 Direct3D 函数,首先调用 ClearRenderTargetView 函数和使用一种

我们给定的颜色来清除屏幕,然后调用 Present 函数来显示新的场景(或者缺乏,因为渲染仅仅是一种颜色)。这些都

是十分基本并且很容易理解的,因为在本章前面部分已经对本 Demo 中所用到的 Direct3D 函数进行了十分详细的讨

论。BlankDemo 的函数实现见于清单 2.17,在这种情况下读者可以根据看到的编写代码了。

清单 2.17:BlankDemo 类的实现。

#include"BlankDemo.h"

BlankDemo::BlankDemo( )

{}

BlankDemo::~BlankDemo( )

{}

bool BlankDemo::LoadContent( )

{

return true;

}

void BlankDemo::UnloadContent( )

{}

void BlankDemo::Update( float dt )

{}

void BlankDemo::Render( )

{

if( d3dContext_ == 0 )

return;

float clearColor[4] = { 0.0f, 0.0f, 0.25f, 1.0f };

d3dContext_->ClearRenderTargetView( backBufferTarget_, clearColor );

swapChain_->Present( 0, 0 );

}

更新应用程序循环

现在我们需要使 Blank Direct3D Window Demo 运行起来。最后一步是更新本章的第一个 Demo 中的 wWinMain

函数,来使用现在的 Demo 类。还记得之前在第一个 Demo 中所添加的注释么?清单 2.18 将看到那些注释已经移除,

取而代之的是响应它们的函数。

清单 2.18:Blank Direct3D Window Demo 中的 main 源文件。

#include<Windows.h>

#include<memory>

#include"BlankDemo.h"

LRESULT CALLBACK WndProc( HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam );

int WINAPI wWinMain( HINSTANCE hInstance, HINSTANCE prevInstance, LPWSTR cmdLine, int cmdShow )

{

UNREFERENCED_PARAMETER( prevInstance );

UNREFERENCED_PARAMETER( cmdLine );

WNDCLASSEX wndClass = { 0 };

wndClass.cbSize = sizeof( WNDCLASSEX ) ;

wndClass.style = CS_HREDRAW | CS_VREDRAW;

wndClass.lpfnWndProc = WndProc;

wndClass.hInstance = hInstance;

wndClass.hCursor = LoadCursor( NULL, IDC_ARROW );

wndClass.hbrBackground = ( HBRUSH )( COLOR_WINDOW + 1 );

wndClass.lpszMenuName = NULL;

wndClass.lpszClassName = "DX11BookWindowClass";

if( !RegisterClassEx( &wndClass ) )

return -1;

RECT rc = { 0, 0, 640, 480 };

AdjustWindowRect( &rc, WS_OVERLAPPEDWINDOW, FALSE );

HWND hwnd = CreateWindowA( "DX11BookWindowClass", "Blank Direct3D 11 Window", WS_OVERLAPPEDWINDOW,

CW_USEDEFAULT, CW_USEDEFAULT, rc.right - rc.left, rc.bottom - rc.top,

NULL, NULL, hInstance, NULL );

if( !hwnd )

return -1;

ShowWindow( hwnd, cmdShow );

std::auto_ptr<Dx11DemoBase> demo(new BlankDemo());

// Demo Initialize

bool result = demo->Initialize( hInstance, hwnd );

if( result == false )

return -1;

MSG msg = { 0 };

while( msg.message != WM_QUIT )

{

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

{

TranslateMessage( &msg );

DispatchMessage( &msg );

}

// Update and Draw

demo->Update( 0.0f );

demo->Render( );

}

// Demo Shutdown

demo->Shutdown( );

return static_cast<int>( msg.wParam );

}

LRESULT CALLBACK WndProc( HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam )

{

PAINTSTRUCT paintStruct;

HDC hDC;

switch( message )

{

case WM_PAINT:

hDC = BeginPaint( hwnd, &paintStruct );

EndPaint( hwnd, &paintStruct );

break;

case WM_DESTROY:

PostQuitMessage( 0 );

break;

default:

return DefWindowProc( hwnd, message, wParam, lParam );

}

return 0;

}

在 wWinMain 函数中我们添加了 7 行代码。首先我们使用 C++标准库中的智能指针 auto_ptr<>来创建一个 Demo

实例。该智能指针能够在当它的作用域结束后或者赋值给另一个智能指针时自动删除自身。作用域可以是整个函数

体,if 语句内部,循环体内部,或者使用一对大括号所创建的区域。

使用智能指针的好处在于我们不需要手动删除所分配的内存。而其真正的好处在于使用智能指针是异常安全的,

这意味着即使是触发一个异常,智能指针也会在栈展开时释放它的数据,也就是即使程序崩溃也没有内存泄露。如

果我们手动删除指针,当异常触发时,将会发生内存泄露。使用内存对象诸如智能指针之类的,是有大量的益处的。

如果你不熟悉它们以及它们的重要性时,请参阅 C++语言的书籍(译者注:推荐阅读《Effective C++》),这方面的知

识已经超出本书的范围。如果你没有了解 C++最近的标准 C++0x,推荐你看看。

最后需要注意的是在 wWinMain 函数中我们返回 MSG 对象的成员 wParam,作为程序的退出代码。因为主函数

需要返回一个整数,我们就在该成员之前施加 C++的 static_cast<>强制转换操作符。

Blank Direct3D Window 运行时的截屏见插图 2.8。本 Demo 使用一种暗颜色清屏,可以与之前的 Demo 截屏做比

较。在该 Demo 中完成的模板文件我们将用于后面的其他 Demo 中,为了创建以后的 Demo 我们只需要继承基类

Dx11DemoBase 并且通过重写 LoadContent,UnloadContent,Update,Render 函数来实现具体的 Demo 逻辑。

插图 2.8:Blank D3D Window Demo 截屏

DirectX 错误处理库

DirectX SDK 提供了一系列的辅助函数和宏来帮助 DirectX 应用程序的调试。为了获得这些辅助函数和宏,工程

必须链接 Dxerr.lib 库和包含 Dxerr.h 头文件。本部分我们将讨论已经存在的不同类型的错误处理函数和宏。

错误处理函数

在 DirectX 错误处理库中有如下三个函数:

TCHAR* DXGetErrorDescription(HRESULT hr)

TCHAR* DXGetErrorString(HRESULT hr)

HRESULT DXTrace(CHAR* strFile, DWORD dwline, HRESULT hr, CHAR* strMsg, BOOL bPopMsgBox)

函数 DXGetErrorDescription 传入参数 HRESULT 来返回错误的值和返回描述错误的字符串。

函数 DXGetErrorString 传入错误的值,返回描述该错误的代码。例如,“D3DERR_DEVICELOST”将会被返回如果

传入与之匹配的错误值,而 DXGetErrorDescription 将会返回它的实际含义。

最后一个函数 DXTrace 将来自于 DXGetErrorString 的错误字符串显示为一个消息。该函数的输入参数是当前文件

名(可是使用 Win32 中的宏__FILE__),行号(可是使用 Win32 中的宏__LINE__),HRESUTL 所持有的错误值,一个将会

被和文件名以及行号一起显示的可选字符串指针,一个决定是否以顶层消息框显示的 bool 类型。

错误处理宏

有三个错误处理宏值得注意,要使用它们程序必须运行在 Debugger 下。这里所列出的宏都是与 DXTrace 函数有

关:DXTRACE_ERR(str, hr) DXTRACE_ERR_MSGBOX(str, hr) DXTRACE_MSG(str) 。

宏 DXTRACE_ERR 用于在调试器中输出消息,这里 str 是你想要显示的跟踪信息,hr 是错误值。而宏

DXTRACE_ERR_MSGBOX 本质上来说是宏 DXTRACE_ERR,只不过是以顶层消息框作为显示输出。最后一个宏

DXTRACE_MSG 只有一个字符串参数,它用于在 VStudio 的窗口调试器中输出。在这些宏中文件名和行号将会和字符

串参数一起显示。

本章我们集中注意的是宏 DXTRACE_MSG,用于在我们的 Direct3D 安装发生错误时显示错误消息。这些宏十分

值得注意,当构建和调试 DirectX 应用程序时它们十分有用。

本章总结

本章覆盖了在 VS2010 中怎样设置和创建 Direct3D 11 窗口。本章末尾我们提供了一套模板文件用于后面的 Demo

创建。如果你从 Direct3D 9 或者更低的版本中来学习 Direct3D 11,你会发现使用 Direct3D 安装和运行是十分简单的。

虽然这里有一定数量的代码,但是它们都十分简单和易于消化。

下一章中我们将在屏幕上渲染 2D 几何体,并且一道讨论纹理映射图像的主题。通过本章我们建立了代码开发

环境;了解了 Win32 和 DirectX 函数以模板代码工作的重要性。

习题

章末习题的答案见于本书的相关网站的附件 A。

1.下列哪一个是 Direct3D 设备类型?

A.硬件设备 B. 软件设备 C. WARP 设备 D. A 和 B 都是 E.以上都是

2.视口 Viewport 是什么?

A.Draws to the monitor

B.定义在窗体上用于绘制的一块区域

C.是一个 3D 照相机

D.以上都不是

3.什么是交换链?

A.Direct3D 设备的别名

B.Direct3D 设备环境的别名

C.A 和 B

D.显示在屏幕上的渲染目标的集合

4.显示已经渲染好的场景使用 Present 方法。Present 方法属于下列哪一对象?

A.Swap chains

B.Direct3D device

C.Direct3D context

D.Render target view

5.什么是渲染目标?

A.用于渲染几何体的对象

B.用于渲染目标输出的纹理贴图

C.交换链的别名

D.以上都不是

6.Blank Window Demo 所支持的特征级别是?

A.Direct3D 11 B.Direct3D 10

C.Direct3D 9 D.A 和 B

E.A 和 B 和 Direct3D 10.1 F.以上都是

7.什么是 WARP 设备?

A.一种快速硬件渲染设备

B.一种低级硬件渲染设备

C.一种快速软件渲染设备

D.以上都不是

8.怎样释放 Direct3D 对象?

A.调用 delete 操作符,就像删除其他 C++动态对象一样。

B.调用 COM 对象的 Release 方法

C.在程序退出时,它们自动被释放

D.使用智能指针 auto_ptr<>

9.怎样创建全屏的 Direct3D 窗口?

A.设置窗口的宽高比屏幕的大

B.只能够用于硬件设备模式

C.必须使用设备所创建的特别的 Direct3D 环境,在全屏应用中

D.设置交换链描述对象的 Windowed 属性为 false

10.DirectX 错误处理库用于何种目的?

A.用于调试输出

B.用于创建异常安全的代码

C.在运行时修正错误

D.在图形显卡中使用类似于 C++中的 try/catch 结构

11.True or False:auto_ptr<>是异常安全的 A.True B.False

12.True or False:WinMain 有 Unicode 类型的参数 A.True B.False

13.True or False:操作符 dynamic_cast<>提供运行时安全 A.True B.False

14.True or False:操作符 static_cast<>用于静态变量的强制转换 A.True B.False

15.True or False:DXTRACE_MSG 显示一个字符串消息框 A.True B.false

第三章 2D 渲染

2 维(2D)游戏,在早期的视频游戏中十分流行,在最近这些年也有大量的此类游戏产生。由于开发相对容易,

众多的独立开发者和专业的游戏制作人兴起了使用 2D 渲染技术的热潮。使用微软的 Xbox Live Arcade 就是一个例子,

许多大受欢迎的游戏都完全是 2D 的,比如 Geometry Wars 2 和 Scott Pilgrim vs. The World。另外,基于 Adobe Flash,

Microsoft Silverlight 和 HTML5 标准的 Web 游戏,以及基于手机设备的游戏例如 iPhone 和 Android,都各有许多 2D

游戏。由于没有昂贵的预算或者需要最新的 3D 技术,开发者很容易想起开发 2D 类型的游戏,这些游戏需要的仅仅

是有趣而已。即使在今天,2D 游戏依然到处存在。

本章内容:

怎样利用 2D 技术来构建一个游戏

贴图纹理是什么,以及怎么使用它们

一种很容易的方式载入纹理图片

怎样创建和使用精灵

2D 游戏的开发

2D 游戏场景类似于只有宽和高的像网格一样的区域。类似于真实世界的网格的例子,它就像在大部分初级和

中级学校教授数学课时使用的网格纸。在游戏画面上,对象位于一个不可见的在 XY 轴的具体位置上的网格上,需

要注意的是 2D 游戏的空间通常是无限的。插图 3.1 显示一个 2D 格子和在其中的一个对象。2D 区域的格子也就是

众所周知的网格平面,该平面是一个可无限延伸的 2D 区域。

插图 3.1:2D 格子的示例。

在 3D 游戏中格子依然存在,不过不仅仅就是 XY 轴,还有一个表示深度的 Z 轴,我们可以想象就是使用立体的

格子代替了平面的格子,这里在其深度上有无限个堆叠的 2D 平面。空间的这些无限细小的平面块组成一个虚拟的

宇宙。一个 3D 格子的例子见于插图 3.2。

较学术地,一个 2D 或 3D 游戏要求像白天和黑夜一样,由于大部分仿真(例如,物理,碰撞和动画等)并且使用

像表面阴影一样的渲染技术。附加更多复杂的数学操作用于各种各样的特效,动画和几何对象,3D 游戏更加需要

大量的资源例如贴图和几何对象,远远超过 2D 游戏中所看到的数量和内存要求。甚至 2D 游戏之间也有不同的需求

(例如,开发的人力资源,存储,CPU 主频,硬件要求等),这依旧是创建专业品质的 2D 游戏的挑战。

插图 3.2:一个 3D 格子的例子

在 2D 游戏中一个游戏的渲染部分一般有如下共同的信息,其每一部分我们都将在本章中深入讨论:

纹理贴图

精灵

纹理贴图

无论是 2D 或 3D 游戏,纹理贴图都是其核心的一部分。纹理贴图是将一幅图片映射到物体表面的意思,它赋予

表面看起来拥有比它本身更复杂的构造。一个例子就是砖块墙壁。代替建模一个真正拥有棱角和裂缝,碎片和其他

真实环境中砖墙拥有的各种特点的砖墙模型,相比之下使用纹理就高效得多,它只需要使用简单的具有砖墙图像的

方块覆盖在物体表面之上即可显示砖墙效果。这就是纹理贴图存在的原因,它们的目的就是在计算机图像中模拟复

杂的表面,而这种在几何体表面看起来的复杂性又不是真实的存在。

一个有砖墙图案的方块没有任何地方有像建模砖墙的 3D 几何体那样的复杂性。实际上,我们讨论的是两个三

角形与成千甚至上百万个三角形复杂性的比较(译者注:之前的有砖墙图案的方块其实是两个三角形组成,而对砖墙

建模的 3D 几何体本质上是由上百万个三角形组成)。对纹理更近一步的讨论,我们甚至能够模拟灯光的改变,通过

结合纹理映射和法线映射(normal mpping)技术就像它与一些模拟的细节相互作用一样。(注意:视频游戏图像是关于

阴影和模拟效果的结果。贴图用来模拟物体表面实际不存在的像素级别的复杂性。)

下面列出了视频游戏中许多不同种类的贴图,它们大部分能够在 3D 游戏中找到。纹理贴图通常包含下列类型:

颜色贴图(Color maps)有时提到的图案贴图(decal maps)用于表面的着色。

镜面贴图(Specular maps)也叫光滑贴图(gloss maps)用于高亮的马赛克。

光照贴图(Light maps)存储预先渲染好的光照和明暗阴影。

环境光遮蔽贴图(Ambient occlusion maps)是高级灯光主题,从暗处移向亮处的表面变化。

阴影贴图(Shadow maps)用于实时阴影的产生和渲染

高度贴图(Height maps)用于多种原因,通常用于几何体的变形效果。

法线贴图(Normal maps)用于在低复杂度的几何体上逐像素着色来模拟高复杂度的几何体直到灯光和视图

的视角相关联。

Alpha 贴图(Alpha maps)用于透明度

立方体贴图(Cube maps)一般用于反射光和环境光

等等。

使用 Photoshop 创建一个颜色贴图图像的例子见于插图 3.3。你还能看到一个贴图类型列表,大部分贴图没有

使用颜色信息,它们大多是与光照和明暗阴影有关。

插图 3.3:在 Photoshop 中创建的一个贴图

技术上来说,实际上用于那些光照的贴图我们应该叫做 look-up 贴图,它们存储一些每个像素级别上的值,在

渲染管线(不一定用于光照,可以用于任何目的)期间被一些算法所使用。例如,一张高度贴图用于代替几何体的变

形,而不是去显示它的明暗变化。一张颜色贴图用于使用一种颜色显示表面明暗变化,这里的 look-up 贴图用于在

环境中基于灯光的性质,当灯光照射到物体表面时改变物体表面的颜色。(注意:一张 look-up 贴图允许我们存储每

个像素一些值用于某些算法,不论它存储的是像素颜色值还是光照时用于计算的一些值等等)

在 2D 游戏中,上述列表我们大多只处理颜色贴图和 alpha 贴图,因为列表中的其余项大多与光照有关,而那

是 3D 游戏中的主题。又因为 32 位图像有 alpha 通道,所以在今天的硬件上 alpha 贴图就没有必要和图像分隔开来。

我们再本章中的“Texture Mapping”部分还将要讨论贴图的主题。图像中的 alpha 通道用于每个点在图像中的具体

透明度。

Sprites 精灵

精灵是一种特殊的对象类型。这里说的精灵是指出现在屏幕上的 2D 图形元素。当然精灵的概念并不是 2D 游戏

所特有,它也存在于 3D 游戏概念中。在 3D 游戏中,精灵用于图形元素的信息统计显示(例如:血条,时间统计,

敌人级别统计等)也用于粒子特效或者其它的方面。在 3D 游戏中使用 2D 元素的粒子见于插图 3.4,3.5,和 3.6。

插图 3.4:在 DirectX SDK 的 Pipes GS Demo 中 2D 元素被渲染在画面顶部

插图 3.5:在 ParticlesGS DirectX SDK Demo 中精灵用于粒子系统

插图 3.6:在 SoftParticles DirectX SDK Demo 中用于创建烟雾效果的半透明粒子

插图 3.5 中 DirectX SDK 中的 ParticlesGS Demo 中的粒子,使用老是正对着照相机的精灵来隐藏它们实际上是平

坦的本质。在插图 3.6 中 SoftParticles DirectX SDK Demo 使用许多聚合在一起的精灵来创建烟雾效果。那些精灵使用

半透明的成分,使得它们很难被肉眼看出是单个的元素,即使是照相机拉近来观察。在插图 3.6 中左边和右边的视

图显示精灵组成的烟雾老是正对着照相机,甚至照相机围绕着它移动。这就是著名的 billboard 精灵。(注意:billboard

精灵一直正对着照相机,无论照相机的角度怎样变化)

一个精灵本质上是一张 2D 形状的贴图,就像是矩形或者正方形一样。在 2D 游戏中,精灵不仅仅表示角色,还

可以表示背景,游戏对象,武器,和任何单个的元素,甚至是能够绘制出来的项。一个简单的例子是在角色扮演类

游戏 Starter Kit(插图 3.7),使用微软的 XNA 游戏开发套件创建诸如角色,道具,背景等所有的精灵,组合在一起来

构建场景。

插图 3.7:使用精灵制作的 2D 游戏(XNA 上的角色扮演类游戏 Starter Kit)

2D 动画实际上是使用一系列的精灵,在某一时刻快速连续的显示它们。这类似于翻书或者是播放动画片的操

作,通过显示动画的很多帧数,这里每一帧显示动画的一个画面。2D 游戏的开发者经常使用著名的 sprite sheets,

它是由许多图片组成,包含一个特定精灵所有帧的画面。

2D 几何

理解怎样创建和控制几何体是精通视频游戏图形编程的关键部分。我们在屏幕上看到的精美的组合在一起的艺

术和数学共同工作来创建模拟游戏者的乐趣。对于刚接触游戏图形学的新手来说,这个部分将会简短的介绍基本的

2D 几何体和 Direct3D 渲染。虽然 Direct3D 与 3D 图形学有极大的关联,但它依旧可以通过图形硬件来高效的渲染

2D 图形。

顶点(Vertex)是什么?

定义在游戏图形中的形状其实是一系列点以及它们之间相互连接的边的集合,它们的内部通过图形硬件渐变着

色。这些 points 就是提到的顶点(vertices),这些线就是边。一个形状被称为线段,如果它由连接两点而成,或者称

为多边形,如果被连接的点有三个或者更多。你可以看看更多的高级渲染主题(本书范围之外的),你毫无疑问的经

常碰到这些术语。插图 3.8 显示了这些术语。

插图 3.8:三角形的各部分

顶点不是专门用来绘制的,而是提供给图形硬件一些必要的信息来学术性的定义一个可渲染表面。当然它也可

以使用 Direct3D API 来作为点来渲染,使用顶点的位置来作为一个点的位置,但是一个真正的顶点是用于图形显卡

的一个数据单位,联合其他的数据来定义一个较大的形状的各种属性。虽然顶点(vertex)和点(point)是两个不同的概

念,但是它们有共同的位置属性。

顶点的含义比点 point 更加丰富。一个顶点实际上是定义一个形状区域的一系列属性。事实上,插图 3.8 中的

点实际上是一个被用于传递给 Direct3D 渲染的三角形的顶点,这里每个点都有用于产生一个效果的许多着色信息。

我们使用顶点来学术的定义形状,图形硬件使用这些性质来绘制一个具体的形状,这里的所提供的所有性质建立在

图形将被怎样着色的效果之上。本书中我们讨论顶点的公共性质有:

位置 Position

颜色 Color

法线向量 Normal vector

贴图坐标 Texture coordinate

在视频游戏中顶点的其它性质包括但不限于:

S-tangent,用于法线映射

Bi-normal,也常用于法线映射

骨骼重量和索引,用于骨骼动画

光照映射坐标

每个点的环境光遮罩因子

等等。

顶点的位置是一个顶点的唯一必要性质。离开位置属性,我们将不能定义一个形状,也就意味着 Direct3D 在它

的缓存中不能绘制任何东西。一个 2D 顶点对于它的位置来说有 X,Y 值,而 3D 顶点有 X,Y 和 Z 值。大多数时候我

们先定义顶点的位置,再来定义其他性质。那些性质是用于创建具体的渲染效果,而随每个顶点的数据需要而定。

例如当产生 UV 贴图映射效果时就需要贴图坐标性质,就像是使用顶点着色器实现动画时骨骼动画信息是必须的一

样。

许多渲染效果的工作使用每个像素的数据代替使用每个顶点的数据,例如逐像素光照和法线映射渲染。但是记

住我们不能从几何体中直接得出每个像素的数据,这些像素的数据是通过插值计算而得来的。插值计算出一点和下

一点之间的值。就拿像素着色器作为一个例子来说:它接受的是从顶点着色器经过插值计算的数据(或者是几何着色

器如果存在的话)。这些插值数据包括位置,颜色,贴图坐标,以及由前一着色器阶段提供的所有其他属性。而像素

着色器被图形硬件锁调用,那些属于形状的像素就被着色。

当我们具体操纵每一像素数据时,它是以贴图图像的形式所提供,例如法线映射贴图用于提供每个像素级别的

法线向量,改变微观下的关照来模拟在形状上的大量的关照细节。另一个例子是传统的贴图映射,用于提供每个像

素的颜色数据来对形状的表面进行着色。

需要注意的是,法线向量(将在第六章中深入讨论)是有方向的。记住,一个向量是一个形状上的一点的属性集,

这里一个向量在 2D 上是有 X 和 Y 轴方向的,在 3D 上是有 X,Y 和 Z 轴方向的。有时你会看到这些术语交换混用,

但是知道它们是不同的这点很重要。这经常发生在代码里面,一个顶点仅仅定义一个位置,一个向量的定义也是一

样的,不同的是你要在代码里面使用它代表的内容(译者注:就是形式相同,含义不同的意思)。例子如下:

struct Vertex2D { float X; float Y; };

struct Vector2D{ float X; float Y; };

在这些情况中,差异在于一个顶点只有位置属性而一个向量用于解释我们的数据,这些结构用于不同的目的,

虽然形式是一样的。又例如一个向量用于表示一粒子弹的某一时刻的运动方向与表示子弹的实际位置的差异一样,

但是在代码中两者 3D 顶点的向量和位置都是用 X,Y,Z 的浮点型值表示。一个向量的大小为 1,就是众所周知的

单位长度向量,在第六章中有更多关于它的讨论。

在 Direct3D 中,我们使用 XNA 数学库表达向量结构。通常大部分程序员会使用向量结构来定义顶点的位置,尽

管一个向量本质上不需要是位置。同样的,意识到这一点是很重要的。一个向量被用于表示虚拟世界中顶点放在原

点时的方向。因为在 3D 空间中原点的 X,Y,Z 分量通常是 0,向量的方向恰好等于点的相对或绝对位置,因此它们之

间能够相互交互。使用向量定义一顶点的属性的例子是如下使用 X,Y 和 Z 值:

struct Vector2D{ float X; float Y; }; struct Vector3D{ float X; float Y; float Z; };

struct Vector3D{ Vector3D position; Vector3D normal; Vector2D textureCoordinate; };

如你所看到的,尽管一个向量并不是一个点,但它可以用来表示点的位置,因为从原点(0, 0, 0)到该点的方向向

量与表示该点的位置的向量相同。换句话说,向量的内容被它所代表的事物说决定。

定义一个三角形

如果我们以一个形状拥有的三个或多个顶点来定义多边形的话,那么有最少顶点数的多边形就是三角形。习惯

上,我们认为一个三角形是由三个点组成的形状,如插图 3.8 所示。

在游戏图形学中我们定义一系列的三角形,这里每个三角形都有它自己的独立形状。这些三角形集合用来定义

网格表面,这里的网格定义为由一群多边形组成的对象。而一群网格集合组成一个模型,这里模型由一个或多个网

格表面所定义。一个例子就是 3D 角色的头部就是一个网格,而头部,躯干,腿和身体的其它部分(网格)组成完整的

角色模型。这些术语对于理解游戏图形学方面是很重要的。一个例子见于插图 3.9(由一些三角形组成的一个网格)

和插图 3.10(一些网格一起构成一个模型)。

插图 3.9:由许多小三角形构成的一个网格表面

插图 3.10:一些网格表面组成的一个模型

一组单个的三角形就是所谓的一个三角形列表,还有其它类别的三角形集合,比如三角形带和三角形扇。一个

三角形带是一个三角形数组,其中前三个顶点定义第一个三角形,而第四个顶点和之前的两个顶点(第二个顶点和第

三个顶点)组成第二个三角形。这意味着在三角形带中增加一个新的三角形只需要增加一个顶点来代替三个顶点即可。

这就允许我们节约内存,这是因为我们不需要太多的内存即可表示更复杂的多边形,但是这也意味着所有三角形必

须有边相连(而三角形列表由单个独立的三角形组成,三角形之间不需要必须相连)。三角形带的例子见于插图 3.11。

一个三角形扇类似于三角形带,但是它是使用第一个顶点和新顶点以及新顶点的前一个顶点来表示新的三角形。

例如(见于插图 3.12),第一个三角形是首先的三个顶点,第二个三角形是第一,三,四个顶点,第三个三角形是第

一,四,五个顶点。

插图 3.11:一个三角形带

插图 3.12:一个三角形扇

我们必须讨论的多边形的另一个表示是使用几何索引。几何索引表示使用一个顶点唯一的数组和使用该数组的

下标索引来定义哪些顶点组成哪个三角形。例如,插图 3.9 中的立方体对象只有 8 个唯一的顶点,它们出现在立方

体的每一个角上。如果我们使用一个三角形列表我们将会有许多重复的顶点,因为我们定义了一个有 36 个顶点的

列表(每个三角形 3 个顶点×每边 2 个三角形×6 边=36)。

使用几何索引表示,在模型拥有成百上千,乃至更多的多边形时收益是巨大的。那些模型,他们通常由有那些

相互共享相同的某一边的大量三角形组成。记住,一条边是由连接两点的线组成,而三角形有三条边。如果我们的

几何索引表示使用 16bits 来表示每个索引顶点,那么一个三角形通过使用 3 个 16bits 的整数来定义,它相当于使用

了 48bits(等于 6 字节)。与使用几何索引相比较,一个 3D 顶点有 3 个浮点数值,每个数轴分量使用 4 字节表示,相

当于使用 12 字节表示一个顶点,36 字节表示每一个三角形。因为几何索引还包括唯一顶点的列表,在多边形数量

不多的情况下,看不到任何的内存节约,直到数量足够多时使用几何索引表示,增加更多的三角形比独立的增加是

节约很多内存的。随着多边形数量的增加,一个模型使用几何索引和使用三角形列表表示的内存消耗是不同的。我

们将会在第六章里讨论 3D 几何体时重新讨论这方面的更多细节。

顶点缓存(Buffers)

一块 buffer 就是一段有具体尺寸的内存。如果你有一个有 100char 类型的数组,那么可以说你有一块 100 字节

的缓存。如果刚刚说的 char 换成是 integers,那么你可以说你有一块整型缓存,该缓存的大小事 400 字节(integer =

4bytes × 100 integer = 400 bytes)。在 Direct3D 的处理中,我们有很多的原因来创建缓存,我们所讨论的第一个原

因就是创建众所周知的顶点缓存。一块顶点缓存是一个类型为 ID3D11Buffer 的 Direct3D 缓存,它用于存储一个网格

模型的所有顶点数据。当我们在Direct3D中创建一块顶点缓存时,我们是在创建一个位于内存的最优位置上的对象,

该最优位置例如是图形硬件的显存,它由硬件的设备驱动所选择。当 Direct3D 渲染我们的对象时,它通过图形总线

来变换这些缓存信息,通过渲染管线执行必要的操作来最终决定几何图形是否被渲染在屏幕上。这里说的“不”是

由于几何图形最终被 Direct3D 判定为不可见的。尝试渲染大量的不可见的几何图形会导致性能下降,大多数的高级

的 3D 商业视频游戏使用一种技术,该技术决定是否几何图形预先可见,并且只提交那些可见或者潜在可见的图形

给图形硬件处理。这是一个高级主题,它属于场景管理部分并且处理多个次主题比如裁剪和划分算法。最快的画几

何图形的方法是不绘制所有的图形,而是只绘制游戏下一帧的所必须的那些可见的图形。

现在来看一下怎样创建一块顶点缓存。假设我们定义顶点只使用它的位置属性,如下:

struct VertexPos { XMFLOAT3 pos; };

XMFLOAT3 是一个内部是包含 X,Y,Z 浮点类型的结构。其名字可知它的结构所代表的事物,这里 XM 表示它来自 XNA

数学库,FLOAT 表示该结构的内部数据类型,3 表示该结构内部有多少个成员。这个结构能够用于表示 3D 点位置,

3D 向量等等,它所表示的内容建立在你使用它的方式上。Direct3D 11 使用 XNA 数学库,而之前的 Direct3D 版本使

用 D3DXVECTOR3 来表示同样的目的。Direct3D 长期以来提供给我们一个高度优化的数学库,那么我们不需要重写它

们,我们将会在第六章中覆盖数学库的公共数据结构和操作的内容。

假设我们已经创建了一个有效的 Direct3D 的设备,我们使用如下代码创建一个简单三角形的顶点缓存:

VertexPos vertices[] =

{

XMFLOAT3( 0.5f, 0.5f, 0.5f ),

XMFLOAT3( 0.5f, -0.5f, 0.5f ),

XMFLOAT3( -0.5f, -0.5f, 0.5f )

};

D3D11_BUFFER_DESC vertexDesc;

ZeroMemory( &vertexDesc, sizeof( vertexDesc ) );

vertexDesc.Usage = D3D11_USAGE_DEFAULT;

vertexDesc.BindFlags = D3D11_BIND_VERTEX_BUFFER;

vertexDesc.ByteWidth = sizeof( VertexPos ) * 3;

D3D11_SUBRESOURCE_DATA resourceData;

ZeroMemory( &resourceData, sizeof( resourceData ) );

resourceData.pSysMem = vertices;

ID3D11Buffer* vertexBuffer;

HRESULT result = d3dDevice_->CreateBuffer( &vertexDesc, &resourceData,&vertexBuffer );

首先需要注意我们定义的顶点列表,它使用三个顶点来定义一个单一的三角形。接下来我们创建了一个缓存描

述对象,其类型是 D3D11_BUFFER_DESC,它用于提供我们所创建的缓存描述细节,这是重要的因为技术上我们能够

创建另一种缓存而不仅仅是顶点缓存。缓存描述有如下的结构和成员:

typedef struct D3D11_BUFFER_DESC {

UINT ByteWidth;

D3D11_USAGE Usage;

UINT BindFlags;

UINT CPUAccessFlags;

UINT MiscFlags;

UINT StructureByteStride;

} D3D11_BUFFER_DESC;

接下来我们创建子资源。子资源用于这种情况,我们使用它来传递顶点数据给缓存创建函数,使得创建的缓存被这

些顶点数据所填充。我们也能够传递 NULL 数据,将会创建一块空的缓存,但是在现在这种情况下我们已经知道我

们想在缓存中存放的数据,于是使用一个 D3D11_SUBRESOURCE_DATA 对象允许我们这样做。该对象有如下的结构和

成员:

typedef struct D3D11_SUBRESOURCE_DATA {

const void* pSysMem;

UINT SysMemPitch;

UINT SysMemSlicePitch;

} D3D11_SUBRESOURCE_DATA;

该结构的 pSysMem 成员是一个已经初始化内存的指针,在现在这种情况是我们发送给缓存填充的内存。

SysMemPitch 和 SysMemSlicePitch 成员用于纹理图像,SysMemPitch 用于决定纹理的每行的开始位置,而

SysMemSlicePitch 用于决定每行的深度,它用于 3D 贴图。

使用缓存描述和子资源数据,我们能够通过简单的调用一个 Direct3D 设备对象函数 CreateBuffer 来创建我们的

缓存。CreateBuffer 有如下的函数原型,接受参数缓存描述,子资源数据(可选),和一个将会通过缓存描述参数被创

建的缓存对象指针。

HRESULT CreateBuffer(const D3D11_BUFFER_DESC* pDesc,

const D3D11_SUBRESOURCE_DATA* pInitialData,

ID3D11Buffer** ppBuffer

);

如果 CreateBuffer 调用成功,我们就能够在缓存中绘制几何图形了。

Input Layout(输入布局)

当我们发送几何图形到显卡上时,我们是发送它的一大块数据。为了让 Direct3D 知道所定义的各种属性,它们

的顺序,和大小,我们使用一种叫做输入布局的东西告诉 API 关于我们所绘制的几何图形的数据的顶点布局。在

Direct3D 中,我们使用一个类型为 D3D11_INPUT_ELEMENT_DESC 的数组来描述一个顶点结构的顶点布局。该类型结

构有如下形式:

typedef struct D3D11_INPUT_ELEMENT_DESC {

LPCSTR SemanticName;

UINT SemanticIndex;

DXGI_FORMAT Format;

UINT InputSlot;

UINT AlignedByteOffset;

D3D11_INPUT_CLASSIFICATION InputSlotClass;

UINT InstanceDataStepRate;

} D3D11_INPUT_ELEMENT_DESC;

第一个元素语义名字(semantic name),是一个描述元素目的的字符串。例如元素作为顶点的位置,则它的语义

就是“POSITION”。我们可以使该元素通过语义“COLOR”用于顶点颜色,通过“NORMAL”用于法线向量等等。语

义使得元素绑定一个 HLSL 着色器作为它的输入或输出变量,本章随后将会讨论。第一个成员是语义索引,一个顶

点有多个元素用于相同的语义但是却值不同,例如如果一个顶点有两种颜色,那么我们能够使用一个语义索引 0 表

示第一种颜色,索引 1 表示第二种颜色。更多示例如,一个顶点有多个贴图坐标,当 UV 贴图和光照贴图同时使用

时就产生了上述情况。第三个成员是元素的格式,例如使用 X,Y 和 Z 浮点值表示一个位置,我们在该顶点所使用的

格式就是 DXGI_FORMAT_R32G32B32_FLOAT,这里 32bits(4 字节)是给 R,G,B 的分量大小。尽管格式分量以 R,G,B 来命

名,但它能够用来表示 X,Y,Z。不要让颜色的概念迷惑了你,实际上这些格式可以用来表示很多而不仅仅是颜色。第

四个成员是输入槽(input slot),它用于寻找指定的顶点。在 Direct3D 中,能够同时绑定和传递多个顶点缓存,我们

使用该成员来寻找元素的指定的顶点缓存索引。第五个参数是字节对齐偏移量,它用来告诉 Direct3D 开始位置的偏

移,当指定了元素的顶点缓存后。第六个成员是输入槽类型,它用于描述是否元素使用每一个顶点或者每一个实例

(逐对象)。逐对象属性处理一个高级主题,叫做实例化,这个技术用于绘制多个有相同网格表面的对象,而使用一

个绘制调用——该技术使得渲染性能得到很大的提升。最后一个成员是 InstanceDataStepRate,它用于告诉 Direct3D

在场景中需要绘制多少个实例。

一个输入布局使用一个类型为 ID3D11InputLayout 的对象。输入布局由调用 Direct3D 设备函数 CreateInputLayout

所创建。该函数的原型如下:

HRESULT CreateInputLayout(

const D3D11_INPUT_ELEMENT_DESC* pInputElementDescs,

UINT NumElements,

const void* pShaderBytecodeWithInputSignature,

SIZE_T BytecodeLength,

ID3D11InputLayout** ppInputLayout

);

第一个参数是一个顶点布局的元素数组,第二个参数是数组元素的数量。第三个参数是有输入标识的已经编译

的顶点着色器代码,它将会被数组的每个元素所验证,第四个参数是着色器字节码的尺寸。顶点着色器的输入标识

必须匹配我们的输入布局,否则该函数的调用将会失败。最后一个参数是该函数所要创建的对象指针。

顶点着色器编译后在 GPU 上执行。顶点着色器在每一个需要在设备上处理的顶点上执行。Direct3D 还支持多种

其他的着色器,第七章中有更多的讨论。Direct3D 要求使用着色器来渲染几何图形,因此我们会在深入讨论着色器

之前就会遇到它们。下面是一个创建顶点着色器的例子,在创建输入布局之前我们需要先创建它们,因为顶点着色

器的标识必须匹配输入布局:

ID3D11VertexShader* solidColorVS;

ID3D11PixelShader* solidColorPS;

ID3D11InputLayout* inputLayout;

ID3DBlob* vsBuffer = 0;

DWORD shaderFlags = D3DCOMPILE_ENABLE_STRICTNESS;

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

shaderFlags |= D3DCOMPILE_DEBUG;

#endif

ID3DBlob* errorBuffer = 0;

HRESULT result;

result = D3DX11CompileFromFile( "sampleShader.fx", 0, 0, "VS_Main", "vs_4_0",

shaderFlags, 0, 0, &vsBuffer, &errorBuffer, 0 );

if( FAILED( result ) )

{

if( errorBuffer != 0 )

{

OutputDebugStringA( ( char* )errorBuffer->GetBufferPointer( ) );

errorBuffer->Release( );

}

return false;

}

if( errorBuffer != 0 )

errorBuffer->Release( );

result = d3dDevice_->CreateVertexShader( vsBuffer->GetBufferPointer( ),

vsBuffer->GetBufferSize( ), 0, &solidColorVS );

if( FAILED( result ) )

{

if( vsBuffer )

vsBuffer->Release( );

return false;

}

首先我们从文本文件中载入顶点着色器代码,将它编译为字节码。你能够选择已经编译好的字节码,或者像我

们刚才那样做由 Direct3D 来编译它,它可被本书后面的 Demo 所接受。编译一个着色器只需调用

D3DX11CompileFormFile 函数,该函数的原型如下:

HRESULT D3DX11CompileFromFile(

LPCTSTR pSrcFile,

const D3D10_SHADER_MACRO* pDefines,

LPD3D10INCLUDE pInclude,

LPCSTR pFunctionName,

LPCSTR pProfile,

UINT Flags1,

UINT Flags2,

ID3DX11ThreadPump* pPump,

ID3D10Blob** ppShader,

ID3D10Blob** ppErrorMsgs,

HRESULT* pHResult

);

该函数的第一个参数是 HLSL 着色器代码的载入和编译路径,第二个参数是在着色器代码中的全局宏,该宏在

HLSL 着色器中的工作方式与 C/C++中的宏一样。在应用程序内定义 HLSL 宏,使用类型 D3D_SHADER_MACRO,一个

例子如定义一个宏名“AGE”,并且它代表 18,用法如下:

const D3D_SHADER_MACRO defineMacros[] =

{

"AGE", "18",

};

第三个参数是一个可选参数,用于处理 HLSL 着色器文件中的#include 语句。这个接口的主要作用就是在包含它

的着色器源代码中打开和关闭文件。第四个参数是你所编译的着色器的入口函数名。一个文件可以包含多个着色器

类型(例如,顶点,像素,几何着色器等)和用于多种目的的多个函数。此参数的重要性在于告诉编译器我们所编译

的着色器哪些潜在的函数为入口函数提供服务。第五个参数是具体的着色器模型,对于我们的目的可以使用 vs_4_0

或者 vs_5_0 来代表使用着色器 4.0 或者 5.0。为了支持着色器模型 5.0,必须需要支持 DirectX 11 的图形硬件单元,

这里使用着色器模型 4.0,只需要支持 DirectX 10 及以上的图形硬件。在第七章有关于着色器模型的更多讨论。第六

个参数是编译标识,指定编译阶段的具体的编译选项。这些标识有如下宏定义:

D3D10_SHADER_AVOID_FLOW_CONTROL——任何时候都禁止流控制

D3D10_SHADER_DEBUG——编译着色器时插入调试信息

D3D10_SHADER_ENABLE_STRICTNESS——禁止过时的语法

D3D10_SHADER_ENABLE_BACKWARDS_COMPATIBILITY——在着色器 4.0 中允许过时的语法

D3D10_SHADER_FORCE_VS_SOFTWARE_NO_OPT——强制顶点着色器在下一个最高的支持版本上编译

D3D10_SHADER_FORCE_PS_SOFTWARE_NO_OPT——强制像素着色器在下一个最高的支持版本上编译

D3D10_SHADER_IEEE_STRICTNESS——在编译器中启用严格 IEEE 标准

D3D10_SHADER_NO_PRESHADER——禁止编译器从静态表达式中退出

D3D10_SHADER_OPTIMIZATION_LEVEL0 (0-3)——设置优化级别,第 0 级不优化,运行最慢,第 3 级完全优化

D3D10_SHADER_PACK_MATRIX_ROW_MAJOR——使用行主序布局的矩阵声明

D3D10_SHADER_PACK_MATRIX_COLUMN_MAJOR——使用列主序布局的矩阵声明

D3D10_SHADER_PARTIAL_PRECISION——强制使用局部精度计算,在某些硬件上会提升性能

D3D10_SHADER_PREFER_FLOW_CONTROL——告诉编译器在任何可能的时候尽量使用流控制

D3D10_SHADER_SKIP_OPTIMIZATION——编译着色器代码时,完全跳过优化阶段

D3D10_SHADER_WARNINGS_ARE_ERRORS——编译时将警告视为错误

第七个参数是特效文件标识,如果我们编译的着色器使用了特效文件就需要设置它,第七章有更多讨论。特效

文件标识能够被设置为如下一个或多个标识:

D3D10_EFFECT_COMPILE_CHILD_EFFECT——允许编译子特效

D3D10_EFFECT_COMPILE_ALLOW_SLOW_OPS——禁止最佳性能模式

D3D10_EFFECT_SINGLE_THREADED——加载特效池(pool)时禁止与其它线程同步

第八个参数是指向一个线程 pump 的指针。若是设置为 NULL,则当编译完成时函数将返回。此参数处理多线程,

在游戏开发中是一个很热门和高级的主题。使用线程允许我们异步加载着色器,调用此函数未返回时,可以继续后

面的代码执行。第九个参数是存放着色器编译完成后字节码的内存地址,它还包括所编译的着色器的任何调试和符

号表信息。第十个参数是存放编译过程中产生的错误和警告信息的内存地址。如果编译过程没有错误发生则该参数

对象为 NULL,如果我们使用它来报告错误我们能够准备它。第十一个参数是线程 pump 的返回值,如果线程 pump(第

八个参数)不为 NULL,则该参数在异步执行完成前必须是一个有效地址。

使用编译好的着色器字节码,我们能够调用 Direct3D 设备的 CreateVertexShader 函数来创建顶点着色器,该函

数参数有编译后的字节码缓存,缓存大小,链接类型的指针,所需创建的顶点着色器对象指针。函数原型如下:

HRESULT CreateVertexShader(

const void* pShaderBytecode,

SIZE_T BytecodeLength,

ID3D11ClassLinkage* pClassLinkage,

ID3D11VertexShader** ppVertexShader

);

接下来就是指定顶点元素的布局。在我们的简单的顶点结构中,我们仅仅使用了顶点的位置属性,只需要使用

“POSITION”语义,并且设置语义索引为 0(在该语义下,它是第一个并且是仅有的一个元素),具体格式是它的 X,Y,Z

值都是 32bit,它的输入槽是 0,字节偏移量是 0,输入槽类型是逐顶点,因为我们的位置用于每一个顶点,和实例

步长比率是 0,因为我们没有使用实例化技术。输入布局的创建调用 CreateInputLayout 完成,之前讨论过的。使用

创建的顶点着色器来创建输入布局的例子如下:

D3D11_INPUT_ELEMENT_DESC vertexLayout[] =

{

{ "POSITION", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 0, D3D11_INPUT_PER_VERTEX_DATA, 0 }

};

unsigned int totalLayoutElements = ARRAYSIZE( vertexLayout );

HRESULT result = d3dDevice_->CreateInputLayout( vertexLayout,

totalLayoutElements, vsBuffer->GetBufferPointer( ),

vsBuffer->GetBufferSize( ), &inputLayout );

vsBuffer->Release( );

if( FAILED( d3dResult ) )

{

return false;

}

仅仅为了完成这事,我们一般还需要载入像素着色器在 Direct3D 10 和 11 上工作。载入像素着色器的一个例子

如下,它看起来像之前的载入顶点着色器的过程:

ID3DBlob* psBuffer = 0;

ID3DBlob* errorBuffer = 0;

HRESULT result;

result = D3DX11CompileFromFile( "sampleShader.fx", 0, 0, "PS_Main", "ps_4_0",

shaderFlags, 0, 0, &psBuffer, &errorBuffer, 0 );

if( FAILED( result ) )

{

if( errorBuffer != 0 )

{

OutputDebugStringA( ( char* )errorBuffer->GetBufferPointer( ) );

errorBuffer->Release( );

}

return false;

}

if( errorBuffer != 0 )

errorBuffer->Release( );

result = d3dDevice_->CreatePixelShader( psBuffer->GetBufferPointer( ),

psBuffer->GetBufferSize( ), 0, &solidColorPS );

psBuffer->Release( );

if( FAILED( result ) )

{

return false;

}

绘制一个 2D 三角形

我们已经所做的所有工作都是为了渲染这个中心。为了在 Direct3D 中渲染几何图形,通常必须准备输入装配,

绑定着色器和其它的资源(例如贴图),绘制每个网格。为了设置输入装配我们首先检查 Direct3D 的 IASetInputLayout,

IASetVertexBuffers 和 IASETPrimitiveTopology。

Direct3D 环境对象的 IASetInputLayout 函数用于绑定我们使用设备函数 CreateInputLayout 所创建的顶点布局。每

次做此事情我们都要考虑使用哪个输入布局来渲染几何图形,该函数只有一个单一的类型为 ID3D11InputLayout 的

对象。而 IASetVertexBuffers 函数用来设置一个或多个顶点缓存,其函数原型如下:

void IASetVertexBuffers(

UINT StartSlot,

UINT NumBuffers,

ID3D11Buffer* const* ppVertexBuffers,

const UINT* pStrides,

const UINT* pOffsets

);

第一个参数是绑定缓存的开始槽,你传递过去的缓存数组中的第一个你要使用的缓存序号。第二个参数是设置

的缓存数量,之后的第三个参数是一个或多个缓存数组。第四个参数是顶点步长,它表示每个顶点的字节数,最后

一个参数是从缓存的开始位置到顶点的第一个元素的开始位置的字节偏移量。第三个和第四个参数必须指定用于每

个顶点缓存的一个值,因此如果有多个缓存它就是一个数组。

函数 IASetPrimitiveTopology 用于告诉 Direct3D 我们所渲染的几何图形的类型。例如渲染一个三角形列表我们将

使用标识 D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST 作为函数的参数,或者我们想渲染三角形带将使用标识

D3D11_PRIMITIVE_TOPOLOGY_TRIANGLESTRIP。这些标识总共有 42 个值,它们大多数处理更加高级的几何体的顶点

控制,该列表见于 DirectX 的文档。

在设置好输入装配之后,我们再来设置着色器。本书稍后,我们将看到怎样应用其它类型的着色器(例如:几何

着色器),但是现在我们注意于顶点和像素着色器。通过调用 Direct3D 设备环境的 VSSetShader 函数来设置顶点着色

器,调用 PSSetShader 来设置像素着色器。这两个函数都采用着色器作为参数,一个类型实例数组指针,和该数组

的大小。我们将在第七章讨论类型实例接口。

一旦我们设置好我们的几何图形的所有必要数据后,最后一步是调用 Draw 函数来绘制它。Direct3D 设备环境

对象的绘制函数采用如下参数顶点数组中的顶点数量,顶点的开始位置(它实际上是你所希望开始绘制的顶点缓存的

偏移量)。渲染几何图形的例子如下:

float clearColor[4] = { 0.0f, 0.0f, 0.25f, 1.0f };

d3dContext_->ClearRenderTargetView( backBufferTarget, clearColor );

unsigned int stride = sizeof( VertexPos );

unsigned int offset = 0;

d3dContext_->IASetInputLayout( inputLayout );

d3dContext_->IASetVertexBuffers( 0, 1, &vertexBuffer_, &stride, &offset );

d3dContext_->IASetPrimitiveTopology( D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST );

d3dContext_->VSSetShader( solidColorVS, 0, 0 );

d3dContext_->PSSetShader( solidColorPS, 0, 0 );

d3dContext_->Draw( 3, 0 );

swapChain_->Present( 0, 0 );

调用交换链的 Present 函数允许我们在屏幕上显示已经渲染好的图像。该函数有两个参数,同步间隔和显示标

识。同步间隔设置为 0 表示立即显示,或者被设置为其它值在多次垂直回扫之后再来显示(例如,3 表示三次垂直回

扫)。显示标识能够是DXGI_PRESENT的任何值,0表示输出每个缓存表示显示一帧。DXGI_PRESENT_DO_NOT_SEQUENCE

表示使用垂直回扫同步输出每个缓存作为一帧,DXGI_PRESENT_TEST 表示现在显示输出(对于测试和错误检查很有帮

助),或者 DXGI_PRESENT_RESTART 告诉驱动丢弃任何发送到 Present 的输出请求。

在第六章我们将会讨论怎样绘制索引几何体,当我们讨论 3D 渲染的时候。索引几何表示不常用于 2D 场景,但

是在 3D 领域它显得特别重要。

2D 三角形 Demo

在本小节我们讨论 Chapter3/Triangle 目录下的 Triangle Demo。这个 Demo 的目的就是使用前小节所讨论的知识

来在屏幕上渲染一个单一的三角形。这个 Demo 从第二章的 Blank Direct3D Window Demo 构建而来。

载入几何图形

本章我们已经讨论了渲染几何图形的顺序,我们需要一个顶点缓存,一个输入布局来描述所使用缓存的顶点布

局,和一个着色器集合。因为 Direct3D 10,着色器是成为渲染图形的一个基本要求,本部分的 Demo 我们指定顶点

和像素着色器,它们在 Demo 中仅仅使用一个固定的颜色渲染表面。本章稍后我们将看到怎样扩展这种效果使得映

射一个贴图图像到物体表面。

Demo 的类 TriangleDemo 来自于头文件 TriangleDemo.h,新增的类成员有 ID3D11VertexShader 类型的

solidColorVS_和 ID3D11PixelShader 类型的变量 solidColorPS_,ID3D11InputLayout 类型的 inputLayout_,和一个

ID3D11Buffer 类型的 vertexBuffer_。该头文件内容如下:

清单 3.1:TriangleDemo 类的头文件

#include"Dx11DemoBase.h"

class TriangleDemo : public Dx11DemoBase

{

public:

TriangleDemo( );

virtual ~TriangleDemo( );

bool LoadContent( );

void UnloadContent( );

void Update( float dt );

void Render( );

private:

ID3D11VertexShader* solidColorVS_;

ID3D11PixelShader* solidColorPS_;

ID3D11InputLayout* inputLayout_;

ID3D11Buffer* vertexBuffer_;

};

顶点结构我们使用一个来自于 XNA 数学库的简单的有三个 float 分量的结构叫 XMFLOAT3。至于 TriangleDemo

类成员,在程序结束时这些对象需要调用它们各自的 Release 方法进行释放。这些函数在 UnloadContent 中执行。清

单 3.2 显示了顶点结构和该类的构造和析构函数,而清单 3.3 显示了 UnLoadContent 函数的内容。

清单 3.2:TriangleDemo 的顶点结构,析构和构造函数

#include"TriangleDemo.h"

#include<xnamath.h>

struct VertexPos

{

XMFLOAT3 pos;

};

TriangleDemo::TriangleDemo( ) : solidColorVS_( 0 ), solidColorPS_( 0 ),

inputLayout_( 0 ), vertexBuffer_( 0 )

{}

TriangleDemo::~TriangleDemo( )

{}

清单 3.3:TriangleDemo 的 UnloadContent 函数

void TriangleDemo::UnloadContent( )

{

if( solidColorVS_ ) solidColorVS_->Release( );

if( solidColorPS_ ) solidColorPS_->Release( );

if( inputLayout_ ) inputLayout_->Release( );

if( vertexBuffer_ ) vertexBuffer_->Release( );

solidColorVS_ = 0;

solidColorPS_ = 0;

inputLayout_ = 0;

vertexBuffer_ = 0;

}

下一个函数是 LoadContent,该函数开始时载入顶点着色器,着色器代码能够在与源文件同目录下的

SolidGreenColor.fx 文件中找到(译者注:可以用记事本打开)。见名知意,该着色器将使用绿色着色图形表面。一旦

顶点着色器源文件被编译,着色器就被 CreateVertexShader 函数所创建,我们能够使用它们创建顶点布局。因为顶

点布局需要验证顶点着色器的标识,我们至少需要将着色器载入内存。

顶点着色器和输入布局创建后,我们接下来创建像素着色器。该段代码的一半在 LoadContent 函数中,见于清

单 3.4。使用 CompileD3DShader 的代码见于清单 3.5,我们分割代码的是由于对于不同的效果载入多个着色器会使

我们编写大量的冗余代码,于是我们能够通过在基类 DX11DemoBase 的成员函数的抽象行为使用其它的高级

Direct3D 代码来消除。

清单 3.4:LoadContent 函数的着色器载入代码

bool TriangleDemo::LoadContent( )

{

ID3DBlob* vsBuffer = 0;

bool compileResult = CompileD3DShader( "SolidGreenColor.fx", "VS_Main", "vs_4_0", &vsBuffer );

if( compileResult == false )

{

MessageBox( 0, "Error loading vertex shader!", "Compile Error", MB_OK );

return false;

}

HRESULT d3dResult;

d3dResult = d3dDevice_->CreateVertexShader( vsBuffer->GetBufferPointer(),

vsBuffer->GetBufferSize( ), 0, &solidColorVS_ );

if( FAILED( d3dResult ) )

{

if( vsBuffer )

vsBuffer->Release( );

return false;

}

D3D11_INPUT_ELEMENT_DESC solidColorLayout[] =

{

{ "POSITION", 0, DXGI_FORMAT_R32G32B32_FLOAT,

0, 0, D3D11_INPUT_PER_VERTEX_DATA, 0 }

};

unsigned int totalLayoutElements = ARRAYSIZE( solidColorLayout );

d3dResult = d3dDevice_->CreateInputLayout( solidColorLayout,

totalLayoutElements, vsBuffer->GetBufferPointer( ),

vsBuffer->GetBufferSize( ), &inputLayout_ );

vsBuffer->Release( );

if( FAILED( d3dResult ) )

{

return false;

}

ID3DBlob* psBuffer = 0;

compileResult = CompileD3DShader( "SolidGreenColor.fx", "PS_Main", "ps_4_0", &psBuffer );

if( compileResult == false )

{

MessageBox( 0, "Error loading pixel shader!", "Compile Error", MB_OK );

return false;

}

d3dResult = d3dDevice_->CreatePixelShader( psBuffer->GetBufferPointer( ),

psBuffer->GetBufferSize( ), 0, &solidColorPS_ );

psBuffer->Release( );

if( FAILED( d3dResult ) )

{

return false;

}

...

}

清单 3.5:CompileShader 函数的实现

bool Dx11DemoBase::CompileD3DShader( char* filePath, char* entry, char*

shaderModel, ID3DBlob** buffer )

{

DWORD shaderFlags = D3DCOMPILE_ENABLE_STRICTNESS;

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

shaderFlags |= D3DCOMPILE_DEBUG;

#endif

ID3DBlob* errorBuffer = 0;

HRESULT result;

result = D3DX11CompileFromFile( filePath, 0, 0, entry, shaderModel,

shaderFlags, 0, 0, buffer, &errorBuffer, 0 );

if( FAILED( result ) )

{

if( errorBuffer != 0 )

{

OutputDebugStringA( ( char* )errorBuffer->GetBufferPointer( ) );

errorBuffer->Release( );

}

return false;

}

if( errorBuffer != 0 )

errorBuffer->Release( );

return true;

}

Loadcontent 函数的第二部分是创建顶点缓存,代码开始定义好一个简单的三角形在 XY 轴上是半个单位长,在

Z 轴上位于 0.5f 的位置使得能够在屏幕上看见,因为如果照相机太近或者在该三角形面的后面,则该三角形将不会

被渲染。顶点列表存储在数组 vertices 中,它提供子资源数据,使得在调用 CreateBuffer 时该数据用于创建实际的顶

点缓存。此部分的代码见于清单 3.6。

清单 3.6:LoadContent 函数的几何图形载入代码

bool TriangleDemo::LoadContent( )

{

...

VertexPos vertices[] =

{

XMFLOAT3( 0.5f, 0.5f, 0.5f ),

XMFLOAT3( 0.5f, -0.5f, 0.5f ),

XMFLOAT3( -0.5f, -0.5f, 0.5f )

};

D3D11_BUFFER_DESC vertexDesc;

ZeroMemory( &vertexDesc, sizeof( vertexDesc ) );

vertexDesc.Usage = D3D11_USAGE_DEFAULT;

vertexDesc.BindFlags = D3D11_BIND_VERTEX_BUFFER;

vertexDesc.ByteWidth = sizeof( VertexPos ) * 3;

D3D11_SUBRESOURCE_DATA resourceData;

ZeroMemory( &resourceData, sizeof( resourceData ) );

resourceData.pSysMem = vertices;

d3dResult = d3dDevice_->CreateBuffer( &vertexDesc,

&resourceData, &vertexBuffer_ );

if( FAILED( d3dResult ) )

{

return false;

}

return true;

}

渲染几何图形

最后两块代码是渲染几何图形和它们的着色器代码。渲染图形的代码见于 TriangleDemo 的 Render 函数,该渲

染函数与本章之前在讨论顶点缓存时的渲染代码几乎相同。该函数只是添加了一个条件判断来确保 Direct3D 设备环

境对象有效。

接下来我们清除渲染目标对象并且设置好输入装配,因为三角形不会移动,我们不必要必须清除渲染目标对象,

但是为了形式的一致性我们还是定义它。输入装配阶段是通过绑定我们创建的输入布局对象,所提供的我们需要绘

制的顶点缓存块和所设置的三角形列表的拓扑结构来设置的。TriangleDemo 的渲染函数见于清单 3.7。

清单 3.7:TriangleDemo 的渲染函数。

void TriangleDemo::Render( )

{

if( d3dContext_ == 0 )

return;

float clearColor[4] = { 0.0f, 0.0f, 0.25f, 1.0f };

d3dContext_->ClearRenderTargetView( backBufferTarget_, clearColor );

unsigned int stride = sizeof( VertexPos );

unsigned int offset = 0;

d3dContext_->IASetInputLayout( inputLayout_ );

d3dContext_->IASetVertexBuffers( 0, 1, &vertexBuffer_, &stride, &offset );

d3dContext_->IASetPrimitiveTopology( D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST );

d3dContext_->VSSetShader( solidColorVS_, 0, 0 );

d3dContext_->PSSetShader( solidColorPS_, 0, 0 );

d3dContext_->Draw( 3, 0 );

swapChain_->Present( 0, 0 );

}

最后来看看着色器代码,顶点着色器是它们的基本形式,它通过传入的顶点位置进行工作然后输出。稍后我们

将控制这些数据来绘制我们的几何对象,但是在这个简单的 Demo 中它只需要直接传出我们所提供的值即可。像素

着色器也是它的基本形式,对于任何需要被着色的像素它只直接返回固定的绿色。来自于像素着色器的颜色是指定

的使用四个浮点类型的值分别是红,绿,蓝和 alpha 通道。在着色器中我们指定颜色值范围是 0.0 至 1.0,它们相当

于使用无符号 char 类型的 0 到 255 值。

顶点着色器的输出时像素着色器的输入,除非在输入装配阶段我们有几何着色器,此处我们没有。像素着色器

的输出是颜色值,这些颜色值被写入到输出缓存中。这些缓存通过使用者调用交换链的 Present 函数来最终显示。

TriangleDemo 的顶点和像素着色器内容见于清单 3.8,该 Demo 运行的截图见于插图 3.13。

清单 3.8:TriangleDemo 的着色器内容

float4 VS_Main( float4 pos : POSITION ) : SV_POSITION

{

return pos;

}

float4 PS_Main( float4 pos : SV_POSITION ) : SV_TARGET

{

return float4( 0.0f, 1.0f, 0.0f, 1.0f );

}

插图 3.13:TriangleDemo 运行截图

纹理映射(Texture Mapping,贴图映射)

正如本章前面所提到的,一个纹理就是映射到我们的图形和实体表面的数据。通常此数据是颜色值,它通过一

种叫做纹理映射的处理,将一张图像映射到物体表面。这些数据也可以表现为其他的信息,例如作为法线映射数据

用于法线映射,作为 alpha 值用于透明度的控制,等等。

纹理贴图,就像游戏中的其它资源一样,将会在运行时载入。因为纹理是 Direct3D 整体的一部分,它提供少量

内建的函数来帮助你有效的处理纹理。开始时,使用函数 D3DX11CreateTextureFromFile 从磁盘加载纹理贴图。该函

数支持多种流行的图像格式,例如 BMP,PNG,和 DDS 等。该函数有六个参数,其原型如下:

HRESULT D3DX11CreateTextureFromFile(

ID3D11Device* pDevice,

LPCTSTR pSrcFile,

D3DX11_IMAGE_LOAD_INFO* pLoadInfo,

ID3DX11ThreadPump* pPump,

ID3D11Resource** ppTexture,

HRESULT* pHResult

);

第一个参数是 Direct3D 设备驱动,该参数必须是有效的。第二个参数是要加载的图像数据文件路径。第三个参

数是图像信息结构,这个一个可选参数允许函数通过我们指定 CPU 的访问标识,内部格式,宽度和高度等,来控制

以何种方式加载纹理图像数据。第四个参数是用于线程 pump,当使用多线程异步加载时使用该参数。第五个参数

是通过该函数加载外部图片所创建的纹理对象的地址。如果函数调用成功,该参数将会持有一个加载好的纹理。最

后一个参数是第四个参数线程 pump 的返回值指针,如果线程 pump 不为 NULL,则该参数在线程返回前必须持有一

块有效的内存。

在 Direct3D 中我们使用 Direct3D 函数加载不同类型的图像文件,其列表如下:

Windows 位图——BMP 格式

联合图像专家组标准——例如 JPEG/JPG

便携式网络图像——PNG

标记图像文件格式——TIFF

图像交换格式——GIF

DirectDraw 外观格式——DDS

Windows 媒体播放格式——WMP

纹理接口(Texture Interfaces)

纹理接口用于管理某些类型的图像数据。在 Direct3D 中主要有三种类型的纹理接口:

ID3D11Texture1D——处理 1D 或者图像条类型的纹理

ID3D11Texture2D——2D 图像数据,它是最常见的纹理资源

ID3D11Texture3D——用于表示体积(3D 纹理)的图像数据

上述这些纹理资源都包含一个或多个子资源。这些子资源用来表现纹理的不同 MIP 级别,下一小节将会讨论。

在你的游戏中使用的最多的纹理将是 2D 类型,它将被转化为 ID3D11Texture2D 资源。编辑器例如 Adobe 的 Photoshop

图像处理软件很常见的用于创建 2D 贴图,然而 1D 贴图是数据数组,被用于在着色器中作为查找表。通常使用 1D

或者 3D 纹理的特性表现是一种高级的特殊的特性,其渲染技术已经超过了一般的简单纹理映射的概念。

一个 2D 贴图使用一个单一的值作为它的贴图坐标。贴图坐标可以被视为一个数组索引在贴图图像中,因此,

2D 贴图使用两个值来表示贴图坐标,就是众知的 TU 和 TV 坐标(或者简写为 U 和 V),而 3D 贴图使用三个值(TU,TV

和 TR)。立方体映射,我们在“贴图细节”将会有简略的讨论,它也使用三个贴图坐标,但是它比 3D 贴图更加的复

杂。

MIP Maps

纹理贴图中的每个像素叫做纹理元素(texel)。一个纹理元素在颜色映射中是一个颜色值,通常在公共图像格式

中表现为 0-255 之间的值。一个 32bit 的图像由 4 个 8bit 的值组成,其分量是红,绿,蓝和 alpha 值。换句话说就是

每个分量使用一个字节表示,一个 32 位的颜色值是 4 字节长度。另一种 RGB 图像,只有三个分量是 24 位大小。

大部分图像编辑器使用颜色值范围 0-255 作为默认值。例如在 Adobe Photoshop 中当创建图像是你能够从颜色

拾取器中选择颜色,其颜色的每个分量就是 0-255(见插图 3.14)。

插图 3.14:在 Adobe Photoshop 中选择一个 32 位的颜色

图像的分辨率描述了它的宽度和高度,若一张图像的分辨率是1024×768意味着有768个宽度,每个宽度有1024

个纹理像素,也就是说该图像表示的一张颜色表有 1024 列和 768 行,这意味着总共有 1024×768=786432 个纹理

元素,如果每个纹理元素使用 4 个字节(32 位),那么该图片总共有 3145728 位,相当于 3072kb,也就是拥有 3Mb

的未压缩数据。

那么什么是 MIP Maps 呢?MIP 分级机制是在相同的纹理下依次向低级别的分辨率版本递减。MIP 分级机制允

许系统依据物体表面所在距离(相对于视点)来转换贴图至合适的分辨率。相对视点越远的对象可以应用较低分辨率

的纹理,因为它们很远而无法看清所有细节。一个 MIP 分级机制的例子见于插图 3.15。

插图 3.15:MIP 分级

插图 3.16:同一个视点观察不同距离的同一个对象

插图 3.16 显示一个对象靠近观察者和远离观察者的比较。对象越来越远离观察者,在屏幕上被着色的像素随着

观察者的远离也越来越少。这意味着,为了表现高品质的画质,我们需要在近处的对象使用高分辨率的图像,但是

当对象越来越远离观察者时,高分辨率的图像不必传递给渲染管线,代替的是使用较低分辨率的图像。

这一点很是重要,因为我们只需要很少的数据进行移动,其渲染性能能够很好的得到提升。如果有许多物体远

离照相机,使用合适的分辨率贴图与使用最高级的分辨率贴图在外观上是没有太大的差别的,而且这是提升性能的

一个关键部分。这个出来过程就是著名的 Mip 映射,使用 D3DX11CreateTextureFromFile 将默认会创建全部的 MIP 分

级链。使用 MIP 分级机制的另一个好处是能够有效的减少混淆走样。

这引入了两个新的术语 MIN 和 MAG。Minification(缩小系数)发生在已经贴图的表面远离观察者时,当表面越来

越远离时,越来越多的纹理元素结合为屏幕上的单个颜色,这是因为来自远离物体的多个纹理元素渲染时使用屏幕

上的同一个像素的原因。纹理的 Magnification(放大系数)现象发生在当已经贴图的物体表面越来越靠近照相机时,

因为屏幕上的多个像素被渲染为物体表面的同一个纹理元素。如果每个纹理元素与每个屏幕像素对应起来,则 MIN

和 MAG 现象就不会发生,但是在 3D 游戏的真实情况是使用多级纹理表面来进行较好的近似。

纹理的细节

有时你需要知道来自载入的纹理的一些必要的信息,比如它的尺寸或像素格式。这些信息可以使用函数

ID3D11Texture2D::GetDesc 来取得。该函数填充一个 D3D11_TEXTURE2D_DESC 的结构。该结构是纹理描述结构并

且指定用于 2D 纹理。Direct3D 还使用结构 D3D11_TEXTURE1D_DESC 和 D3D11_TEXTURE3D_DESC 来分别表示 1D 和

3D 纹理的描述信息。之前的 2D 纹理结构所描述的纹理信息如下:

typedef struct D3D11_TEXTURE2D_DESC {

UINT Width;

UINT Height;

UINT MipLevels;

UINT ArraySize;

DXGI_FORMAT Format;

DXGI_SAMPLE_DESC SampleDesc;

D3D11_USAGE Usage;

UINT BindFlags;

UINT CPUAccessFlags;

UINT MiscFlags;

} D3D11_TEXTURE2D_DESC;

该结构除了 ArraySize 成员每一个都要一个纹理描述的具体上下文。在讨论它们表示之前,我们先讨论一下 cube

maps。一个 cube map 纹理是一个有六个 2D 纹理图片的集合(数组),它们合在一起通常表示一个环境的不同视角的

形式。一个 cube map 通常映射到上,下,左,右,前和后六个方向。如果取 cube map 的信息,该结构的 ArraySize

的值将是 6,因为在数组中有六张纹理图像。一个 cube map 的例子见于插图 3.17。

插图 3.17:一个 cube map 的例子

贴图映射 Demo

在该小节我们已经掌握必要的信息来实现一个贴图映射演示。其还没讨论的细节后面会介绍。贴图映射 Demo

在 Chapter3/TextureMapping 的文件夹中。首先我们构建本章前面来自于 TriangleDemo 的代码。在 TextureDemo 类中

我们增加一个着色器资源视图叫 colorMap_( 类型 ID3D11ShaderResourceView) 和一个采样状态变量叫

colorMapSampler_(类型 ID3D11SamplerState)。

一个着色器资源视图是一个用于访问资源的着色器对象。当我们载入贴图图像到内存时,需要创建一个着色器

资源视图对象,用于通过一个着色器来访那些数据,这就是为什么我们会绑定输入装配的原因。着色器资源视图还

有其他用处,例如提供通用目的的数据使得 DirectCompute 能够并行计算,但是本章我们只注意于纹理贴图。

ID3D11Texture2D 是一个缓存数据,着色器资源视图允许我们在着色器中访问这缓存数据。

一个采样状态允许我们访问一个纹理的采样状态信息。当谈论到怎样创建该对象时我们将会讨论更多,但是一

般的,采样状态允许我们设置属性例如贴图过滤子和地址,我们将会很快讨论到这部分主题。TextureDemo 类见于

清单 3.9。

清单 3.9:来自于 TextureDemo.h 头文件的 TextureDemo 类

class TextureDemo : public Dx11DemoBase

{

public:

TextureDemo( );

virtual ~TextureDemo( );

bool LoadContent( );

void UnloadContent( );

void Update( float dt );

void Render( );

private:

ID3D11VertexShader* solidColorVS_;

ID3D11PixelShader* solidColorPS_;

ID3D11InputLayout* inputLayout_;

ID3D11Buffer* vertexBuffer_;

ID3D11ShaderResourceView* colorMap_;

ID3D11SamplerState* colorMapSampler_;

};

因为我们执行纹理映射,需要更新顶点结构使得包含两个浮点变量用于 TU 和 TV 贴图坐标,只需要使用 XNA

数学库的 XMFLOAT2 即可。

当我们创建输入布局,我们需要在 LoadContent 函数中增加 D3D11_INPUT_ELEMENT_DESC 数组元素用于贴图坐

标。其语法名字是“TEXCOORD”,格式是 DXGI_FORMAT_R32G32_FLOAT(因为我们只使用了两个浮点值)。我们使用

12 作为偏移量,因为之前我们的顶点结构布局是一个 XMFLOAT3 表示位置。因为 XMFLOAT3 是 12 字节大小,我们

的贴图坐标直到 12 字节之后才出现。清单 3.10 显示该 Demo 的顶点结构,LoadContent 和 UnloadContent 函数。

清单 3.10:顶点结构表示和 TextureDemo 的 LoadContent 和 UnloadContent 函数。

struct VertexPos

{

XMFLOAT3 pos;

XMFLOAT2 tex0;

};

bool TextureDemo::LoadContent( )

{

... Load vertex Shader ...

D3D11_INPUT_ELEMENT_DESC solidColorLayout[] =

{

{ "POSITION", 0, DXGI_FORMAT_R32G32B32_FLOAT,

0, 0, D3D11_INPUT_PER_VERTEX_DATA, 0 },

{ "TEXCOORD", 0, DXGI_FORMAT_R32G32_FLOAT,

0, 12, D3D11_INPUT_PER_VERTEX_DATA, 0 }

};

unsigned int totalLayoutElements = ARRAYSIZE( solidColorLayout );

d3dResult = d3dDevice_->CreateInputLayout( solidColorLayout,

totalLayoutElements, vsBuffer->GetBufferPointer( ),

vsBuffer->GetBufferSize( ), &inputLayout_ );

... Load Pixel Shader ...

VertexPos vertices[] =

{

{ XMFLOAT3( 1.0f, 1.0f, 1.0f ), XMFLOAT2( 1.0f, 1.0f ) },

{ XMFLOAT3( 1.0f, -1.0f, 1.0f ), XMFLOAT2( 1.0f, 0.0f ) },

{ XMFLOAT3( -1.0f, -1.0f, 1.0f ), XMFLOAT2( 0.0f, 0.0f ) },

{ XMFLOAT3( -1.0f, -1.0f, 1.0f ), XMFLOAT2( 0.0f, 0.0f ) },

{ XMFLOAT3( -1.0f, 1.0f, 1.0f ), XMFLOAT2( 0.0f, 1.0f ) },

{ XMFLOAT3( 1.0f, 1.0f, 1.0f ), XMFLOAT2( 1.0f, 1.0f ) },

};

... Create Vertex Buffer ...

d3dResult = D3DX11CreateShaderResourceViewFromFile( d3dDevice_,

"decal.dds", 0, 0, &colorMap_, 0 );

if( FAILED( d3dResult ) )

{

DXTRACE_MSG( "Failed to load the texture image!" );

return false;

}

D3D11_SAMPLER_DESC colorMapDesc;

ZeroMemory( &colorMapDesc, sizeof( colorMapDesc ) );

colorMapDesc.AddressU = D3D11_TEXTURE_ADDRESS_WRAP;

colorMapDesc.AddressV = D3D11_TEXTURE_ADDRESS_WRAP;

colorMapDesc.AddressW = D3D11_TEXTURE_ADDRESS_WRAP;

colorMapDesc.ComparisonFunc = D3D11_COMPARISON_NEVER;

colorMapDesc.Filter = D3D11_FILTER_MIN_MAG_MIP_LINEAR;

colorMapDesc.MaxLOD = D3D11_FLOAT32_MAX;

d3dResult = d3dDevice_->CreateSamplerState( &colorMapDesc,

&colorMapSampler_ );

if( FAILED( d3dResult ) )

{

DXTRACE_MSG( "Failed to create color map sampler state!" );

return false;

}

return true;

}

void TextureDemo::UnloadContent( )

{

if( colorMapSampler_ ) colorMapSampler_->Release( );

if( colorMap_ ) colorMap_->Release( );

if( solidColorVS_ ) solidColorVS_->Release( );

if( solidColorPS_ ) solidColorPS_->Release( );

if( inputLayout_ ) inputLayout_->Release( );

if( vertexBuffer_ ) vertexBuffer_->Release( );

colorMapSampler_ = 0;

colorMap_ = 0;

solidColorVS_ = 0;

solidColorPS_ = 0;

inputLayout_ = 0;

vertexBuffer_ = 0;

}

LoadContent 函 数 载 入 我 们 的 贴 图 图 像 , 我 们 使 用 Direct3D 的 实 用 (utility) 函 数

D3DX11CreateShaderResourceViewFromFile 来载入和创建一个着色器资源视图对象。当我们想一次性载入和创建时该

函数还是很有用的。该函数的原型如下,它的参数类似于 D3DX11CreateTextureFromFile:

HRESULT D3DX11CreateShaderResourceViewFromFile(

ID3D11Device* pDevice,

LPCTSTR pSrcFile,

D3DX11_IMAGE_LOAD_INFO* pLoadInfo,

ID3DX11ThreadPump* pPump,

ID3D11ShaderResourceView** ppShaderResourceView,

HRESULT* pHResult

);

在 LoadContent 函数中的最后一个新的部分是创建采样状态。为了创建一个采样状态对象,我们使用 Direct3D 设备

的 CreateSamplerState 函数,它使用采样描述作为参数并且输出所创建的采样状态对象的地址。采样描述结构如下:

typedef struct D3D11_SAMPLER_DESC {

D3D11_FILTER Filter;

D3D11_TEXTURE_ADDRESS_MODE AddressU;

D3D11_TEXTURE_ADDRESS_MODE AddressV;

D3D11_TEXTURE_ADDRESS_MODE AddressW;

FLOAT MipLODBias;

UINT MaxAnisotropy;

D3D11_COMPARISON_FUNC ComparisonFunc;

FLOAT BorderColor[4];

FLOAT MinLOD;

FLOAT MaxLOD;

} D3D11_SAMPLER_DESC;

上述结构的第一个参数是类型为 D3D11_FILTER 的成员,它指定被采样的纹理怎样过滤。纹理过滤描述从源数据

怎样被读取和联合,使得用于着色器。过滤能够提升画质但是在纹理采样时会有更多的性能开销,因为一些过滤类

型是由于读取和结合多个纹理元素值来产生一个在着色器中看到的单一的颜色值。在 Direct3D 11 中可用的过滤类型

如下:

typedef enum D3D11_FILTER {

D3D11_FILTER_MIN_MAG_MIP_POINT

D3D11_FILTER_MIN_MAG_POINT_MIP_LINEAR

D3D11_FILTER_MIN_POINT_MAG_LINEAR_MIP_POINT

D3D11_FILTER_MIN_POINT_MAG_MIP_LINEAR

D3D11_FILTER_MIN_LINEAR_MAG_MIP_POINT

D3D11_FILTER_MIN_LINEAR_MAG_POINT_MIP_LINEAR

D3D11_FILTER_MIN_MAG_LINEAR_MIP_POINT

D3D11_FILTER_MIN_MAG_MIP_LINEAR

D3D11_FILTER_ANISOTROPIC

D3D11_FILTER_COMPARISON_MIN_MAG_MIP_POINT

D3D11_FILTER_COMPARISON_MIN_MAG_POINT_MIP_LINEAR

D3D11_FILTER_COMPARISON_MIN_POINT_MAG_LINEAR_MIP_POINT

D3D11_FILTER_COMPARISON_MIN_POINT_MAG_MIP_LINEAR

D3D11_FILTER_COMPARISON_MIN_LINEAR_MAG_MIP_POINT

D3D11_FILTER_COMPARISON_MIN_LINEAR_MAG_POINT_MIP_LINEAR

D3D11_FILTER_COMPARISON_MIN_MAG_LINEAR_MIP_POINT

D3D11_FILTER_COMPARISON_MIN_MAG_MIP_LINEAR

D3D11_FILTER_COMPARISON_ANISOTROPIC

D3D11_FILTER_TEXT_1BIT

} D3D11_FILTER;

在枚举类型 D3D11_FILTER 中的不同的枚举值,是由于结合纹理的 MIN,MAG 和 MIP 分级机制的不同而引起的。

这些不同的值用于告诉 Direct3D 的内部插值方式,通过检测这些值的不同的 boolean 组合结果。

点采样,也就是最邻近采样,是用于过滤的一种最快的采样方式。它直接从纹理中取得单一的值作为采样结果,

而不进行修改。这个被选取的纹理元素映射在是某像素位置周围最接近该像素的纹理元素。

双线性采样,在被采样点的纹理坐标处进行双线性内插值,除了包围采样的边框附件的那些采样点。这些内插

值(已经双线性采样后的结果)将是在着色器中可见。选择此采样方式是通过过滤(双线性组合)在像素中心周围的最接

近的四个纹理元素完成。这种组合多个附近的纹理元素的方式能够使得结果更加的平滑,该采样方式能够有效减少

在渲染中的人为现象(artifacts,译者注:就是锯齿效果)。

三线性采样,通过在最接近的 MIP 分级纹理中分别进行双线性采样,然后再内插值完成。当一个网格表面从使

用一个 MIP 等级到另一个等级时,该物体表表面将会出现一些明显的变化。而在最邻近的 MIP 等级之间使用插值,

在两个等级之间使用双线性过滤后,能够很大的减少渲染的人工痕迹(译者注:就是使用三线性采样在物体远离或靠

近时,表面纹理能够平滑过渡)。

各向异性过滤,使用三线性采样在不等边四边形上来代替正方形区域。双线性和三线性过滤(采样)能够在当视

线方向垂直于表面时工作得很好,因为是符合正方形采样,但是斜视物体表面时(视线与物体表面夹角为一个锐角),

例如在观察 3D 游戏中的天花板或地面时,将会出现明显的模糊和渲染痕迹。各向异性过滤在用于不同形状的区域

的贴图采样时会将角度考虑进去。

D3D11_SAMPLER_DESC 结构的接下来三个参数用于贴图地址模式。纹理坐标值被指定范围 0.0-1.0 在纹理的各个

尺寸。纹理坐标模式告诉 Direct3D 怎样处理超出该范围的坐标值。纹理坐标模式使用 U,V 和 R 其值如下:

typedef enum D3D11_TEXTURE_ADDRESS_MODE {

D3D11_TEXTURE_ADDRESS_WRAP,

D3D11_TEXTURE_ADDRESS_MIRROR,

D3D11_TEXTURE_ADDRESS_CLAMP,

D3D11_TEXTURE_ADDRESS_BORDER,

D3D11_TEXTURE_ADDRESS_MIRROR_ONCE,

} D3D11_TEXTURE_ADDRESS_MODE;

重复(WRAP)纹理地址将导致贴图重复用于周围。例如,如果你有一个方形区域,你想一张贴图在区域的水平方

向重复显示两次,只需要指定最右边的顶点的 U 贴图坐标分量为 20 即可,见于插图 3.18。这能够使贴图像瓦片一

样覆盖一个单一的表面,它的好处就是只需要使用很少量的数据就可以达到整个覆盖的效果。

插图 3.18:包装贴图坐标

镜像(MIRROR)纹理地址将导致贴图以镜像形式重复(见于插图 3.19),这里 CLAMP 将会简单的使值在 0.0-1.0 的

范围。MIRROR_ONCE(镜像一次)将会使得贴图的镜像形式出现一次,而 MIRROR 将会使用指定的镜像次数。BORDER

地址模式将会设置任何在 0.0-1.0 范围之外的像素一个指定的边界颜色。边界颜色通过 D3D11_SAMPLER_DESC 的另

一个叫做 BorderColor 的成员来指定,它是一个有四个浮点值得数组。

插图 3.19:镜像贴图地址模式

在 D3D11_SAMPLER_DESC 结构中的下一个成员是用于 MIP 的级别细节(level-of-detail,简称 LOD)偏移。该值是

一个通过 Direct3D 使用的 MIP 级别机制的偏移量。例如,如果 Direct3D 指定使用第 2 级别的 MIP 并且偏移量为 3,

则实际上 MIP 使用的是第 5 级别。

跟在 LOD 偏移成员之后的是最大各向异性值,用于各向异性过滤和比较函数。该值范围是 1-16 之间的整数,

它不用于点和双线性过滤。COMPARISON(比较 )函数的返回值用于判定贴图过滤标识的比较。比较标识在

D3D11_COMPARISON_FUNC 中指定,其比较状态的本质是比较两个值之间的等于,大于小于关系。

最后两个成员是用于指定(clamp)最小和最大 MIP 级别范围。例如,如果最大值指定为 1,那么级别为 0 的 MIP

纹理就不会被访问到(注意级别为 0 的 MIP 就是分辨率最高的纹理)。

在渲染函数中的最后的谜题是除着色器之外的部分。为了渲染我们的贴图几何体,我们必须添加贴图资源和设

置采样状态。这可以通过调用函数 PSSetShaderResource 和 PSSetSamplers 来完成,它们被用于在像素着色器中设置

那些项。函数 PSSetShaderResource 原型如下,参数开始槽用于开始插入资源,以及资源数量和输入的资源数组:

void PSSetShaderResources(

UINT StartSlot,

UINT NumViews,

ID3D11ShaderResourceView* const* ppShaderResourceViews

);

函数 PSSetSamplers 的参数类似,有一个开始槽参数,采样描述数量和你所提供的采样描述数组。附加这两个

函数到我们的之前的 Demo 的渲染代码中去,我们准备好了看实际上的效果。为了执行这些效果在着色器中无需修

改。Texture Mapping Demo 的渲染函数代码见于清单 3.11。

清单 3.11:Texture Mapping Demo 的渲染函数。

void TextureDemo::Render( )

{

if( d3dContext_ == 0 )

return;

float clearColor[4] = { 0.0f, 0.0f, 0.25f, 1.0f };

d3dContext_->ClearRenderTargetView( backBufferTarget_, clearColor );

unsigned int stride = sizeof( VertexPos );

unsigned int offset = 0;

d3dContext_->IASetInputLayout( inputLayout_ );

d3dContext_->IASetVertexBuffers( 0, 1, &vertexBuffer_, &stride, &offset );

d3dContext_->IASetPrimitiveTopology( D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST );

d3dContext_->VSSetShader( colorMapVS_, 0, 0 );

d3dContext_->PSSetShader( colorMapPS_, 0, 0 );

d3dContext_->PSSetShaderResources( 0, 1, &colorMap_ );

d3dContext_->PSSetSamplers( 0, 1, &colorMapSampler_ );

d3dContext_->Draw( 6, 0 );

swapChain_->Present( 0, 0 );

}

着色器代码见于清单 3.12,我们有两个新的着色器对象分别是 colorMap_和 colorSampler_。对象 colorMap_是

Texture2D 类型,因为它用于 2D 贴图,而 colorSampler 是 HLSL 高级着色语音的一个类型 SamplerState。为了在我们

提供的渲染函数中的着色器输入中绑定这些对象,我们必须使用 HLSL 注册关键字 register。为了绑定第一个输入贴

图我们使用 t0,这里他表示贴图类型,0 表示使用第一个索引贴图。对于采用状态对象使用 s0 出于同样的原因。因

为我们使用函数 PSSetSamplers 和 PSSetShaderResource 来传递一个数组元素给我们的着色器使用,所以必须将我们

使用的数据索引绑定给每一个 HLSL 变量。因为我们只有一张贴图和一个采样状态,我们只需要使用 t0 和 s0 即可。

在着色器代码中的另一个改变是我们必须更新顶点着色器的输入结构和像素着色器的输入结构来允许使用贴

图坐标。顶点着色器将会获得来自于顶点缓存块的贴图坐标并且直接将它们传递给像素着色器,使得像素着色器来

访问它们。

像素着色器使用这些贴图坐标(记住,它们是顶点之间的内插值数据)在贴图对象上来阅读颜色值。这通过调用

HLSL 着色语言的 Texture2D 对象的 Sample 函数来完成,该函数以输入采样状态对象来使用在贴图坐标上进行采样。

因为我们从 2D 贴图上读取,所以贴图坐标必须是 float2 类型。

清单 3.12:Texture Mapping Demo 的着色器代码

Texture2D colorMap_ : register( t0 );

SamplerState colorSampler_ : register( s0 );

struct VS_Input

{

float4 pos : POSITION;

float2 tex0 : TEXCOORD0;

};

struct PS_Input

{

float4 pos : SV_POSITION;

float2 tex0 : TEXCOORD0;

};

PS_Input VS_Main( VS_Input vertex )

{

PS_Input vsOut = ( PS_Input )0;

vsOut.pos = vertex.pos;

vsOut.tex0 = vertex.tex0;

return vsOut;

}

float4 PS_Main( PS_Input frag ) : SV_TARGET

{

return colorMap_.Sample( colorSampler_, frag.tex0 );

}

到这里后,就可以编译和执行代码了。执行程序的截图如插图 3.20 所示。

插图 3.20:Texture Mapping Demo 程序截图

Sprites 精灵

2D 游戏同样的不能离开精灵,在你开始想汽水或仙女之前,精灵在 2D 游戏中代表角色或道具。每一颗树,宝

藏,或你在屏幕上显示的随机地牢都是使用一个精灵表示。在 2D 游戏编程里,精灵被广泛的使用并且很容易理解。

在 2D 游戏中精灵通常是一张长方形或正方形的贴图,这些精灵共同来构造一个虚拟的世界。

Z-Ordering (Z 排序)

在 2D 游戏中,一些对象作为背景一些对象作为前景,而还有一些对象能够介于两者之间,所有的这些对象就

是著名的层级。一个层级可以认为是一张透明的纸,例如背景层就只有背景精灵渲染在上面,而动作层有所有的角

色,武器,魔法等。这些层级渲染的顺序将会决定哪一些精灵将会在另一些精灵的上面。除了有那些定义好的层级,

还有 Z-ordering(Z 序),它将会处理在对象绘制之前的排序。你所创建的每一个精灵都需要赋值给它一个不同的 Z 序,

用于指出它将会被绘制在哪一个深度上。给定一个假想的深度后,有较小 Z 序值的精灵会被绘制在有较大 Z 序值的

后面,例如任何 Z 序值为 1 的对象将会被绘制在任何 Z 序值为 0 的对象上面,而任何 Z 序值 2 的对象将会绘制在 Z

序值为 0 或 1 的对象上面。

使用层级或 Z 序概念允许我们在 2D 游戏中定义对象的渲染顺序,但是在 3D 游戏中我们通常使用叫做深度测试

(Depth Test)的概念来确保物体表面以正确的顺序显示,在第六章的 3D 游戏中我们有更多的讨论。

硬件深度测试使用一块特别的渲染缓存,叫做深度缓存,它用于决定相对于观察者来说,前面渲染的表面在某

像素点处的深度值是否大于当前渲染的表面在该像素点处的深度值。换句话说,就是如果之前被渲染的表面到观察

者的距离远于当前正在被渲染的表面,则当前表面离照相机更近从而应当被绘制在之前已经渲染好的表面的数据上,

否则如果之前的表面在现在被渲染的表面的前面的话,则现在的表面不需要被渲染,从而相应的像素不需要被更新。

深度测试是一种在 3D 游戏中能够正确渲染对象而不需要基于照相机视图的预排序多边形的方式(我们将会在第

六章讨论)。虽然我们使用 Direct3D 组件渲染,但我们能够在 2D 游戏中按某些对象出现在另一些对象上的要求绘制

精灵,而不需要使用深度测试。通过完全清屏,再在相应的层级中绘制我们的精灵,就能够得到正确的结果。

Sprite Image 精灵图像

精灵最重要的一个方面就是它的图像。传统的说法,每一个精灵需要一张游戏玩家观点下的图片和程序员观点

下的图片。从玩家来看,精灵使用的图片来表达它的目的。例如,表示你的主角的精灵在 RPG 游戏中也许是一幅骑

士图像或甚至是在太空战斗的一艘太空船。你能够选择你喜欢代替图像,但是你的精灵不能离开图像来显示。

这是由于在 2D 游戏早期,精灵图像被直接绘制在渲染目标上而不是使用几何图形的形式,这就是所谓的纹理

位图。在 Direct3D 中我们能够采取绘制 2D 几何图形和纹理映射到该几何图形上的方式,而不是使用过时的 2D 游戏

图形学的知识。纹理位图的本质是将精灵图像的颜色值拷贝到指定位置的渲染目标上。如果你创建一个用于 2D 游

戏的软件渲染 API,你依旧能够使用上述那种过时的方式,但是我们使用 Direct3D 来做更容易达到此种效果,只需

在长方形几何图形上使用 2D 纹理即可。

之前我们学习了怎样从磁盘载入贴图文件,现在我们学习怎样在精灵中使用贴图。

从屏幕上取得精灵

当绘制精灵时,精灵对象需要知道一些该环境中的信息。不仅需要知道它们的每一个位置和在相同层级上各精

灵之间避免碰撞,还需要知道它们绘制的具体区域。这意味着这些精灵对象绘制时需要注意区域的边界。通常这个

区域的大小通过关联的 Direct3D 设备的视口(viewport)所决定。

精灵绘制的区域通过它的变换矩阵所定义,矩阵的更多细节见于第六章,但是现在我们简略的介绍几种在图形

渲染中用到的基本类型的矩阵。矩阵在 Direct3D 中表现为一个 4×4 的方阵。以可视化的观点看它就是作为一张有 4

行 4 列总共 16 个元素的表。这些行列元素允许我们在游戏图形学和模拟仿真中做很多事情。

通常在游戏图形编程中我们首先接触 3 类主要的变换矩阵。第一个是投影变换,第二个是视图变换,第三个是

世界变换。当使用一个矩阵变换点时,可以这种观点看做基于这些矩阵的表示意义来操纵这些点。例如,一个旋转

矩阵用于旋转物体,平移矩阵用于移动物体,缩放矩阵用于缩放物体。但是我们为什么需要这样做呢?

在 3D 空间中的物体通常通过使用 3D 建模应用程序创建,例如使用 Autodesk 的 3D Studio Max。当这些模型和

物体被创建好后,它们所在的空间称之为模型空间。那些点的位置含义是在模型编辑器中导出的模型中的点的位置。

通常模型创建在原点处,像我们之前章节里面的简单三角形和四边形一样(这里原点(0, 0, 0)是模型的中心位置)。

在游戏中我们有一个或多个这样的模型实例,这些模型能够在场景中移动和相互影响。但是所有的这些模型数

据都被定义在各自的模型空间,而它们需要在同一个世界中移动,从而它们的顶点的位置需要更新成一个新的位置。

当我们使用层级或地图编辑器编辑我们的模型的位置或者游戏中模型之间的交互(例如:物理检测,AI 等),我们就

在模型上使用平移,旋转,和缩放矩阵。这就允许我们只需要载入一个模型数据到内存,而分别渲染模型的多个实

例。那么如果我们有一个有 1000 个石头的小行星带,就只需要一个岩石模型和 1000 个代表每个小行星的位置和方

向的矩阵。更近一步使用 Direct3D 的实例化特性,对于 1000 个岩石的变换矩阵的缓存块来说,可以只需执行一次

调用。

简单的说,一个变换矩阵允许我们定义对象的位置和方向。模型通过编辑器(例如 3DMax)来创建在模型空间中。

当我们使用代表位置,缩放和方向的变换矩阵作用在模型的几何数据上时,就能够将模型空间中的几何体变换到世

界空间中。世界空间是一个表示在所有物体周围一个物体相对于其它物体位置的关系的一个环境。

在游戏中,特别是 3D 游戏,照相机扮演一个很大的角色。照相机取向用于几何体来模拟现实照相机的效果。

照相机所代表的矩阵叫做视图矩阵。视图矩阵允许我们将在当前(译者注:世界)空间中的模型变换到照相机的视图

空间。当视图矩阵与世界矩阵结合时,我们就创建了一个单一的做这两者事情的变换,叫做世界-视图矩阵。

为了模拟照相机的位置和方向,我们增加一个投影的概念。一个投影矩阵用于模拟正投影(orthogonal)或透视

(perspective)投影。我们能够通过操纵投影矩阵来实现一些效果,比如缩放一个镜头的效果。

我们在第六章有更多关于投影矩阵的讨论,但是现在我们用上层观点来简述它们。正投影对于 2D 元素来说意

义重大,因为使用正投影会使得物体在渲染时其视景深度不会使用。这意味着如果你有两个单位长度为 10 的盒子,

但是它们在 Z 轴上相距 100 个单位,则使用正投影会使得它们紧挨着显示而与深度无关。正投影不仅用于 2D 游戏,

而且用于 3D 游戏的 2D 界面元素,例如血条,弹药数量,时间,文本等。

透视投影用于在我们渲染物体时增加透视效果。意思是物体远离照相机则在屏幕上显示会越来越小,而靠近照

相机则会显示越来越大。在真实生活中你以不同距离来观察对象就会有这种感觉。你隔一小段距离观察场景,较远

的物体看起来较小。如果你靠近建筑物观察,建筑物的高度远远大于你在一英里外观察它的高度。结合模型,视图

和投影矩阵将会创建一个新的名为模型-视图投影矩阵。这类矩阵通过被顶点着色器使用来变换输入的几何体,产生

模型的最后的位置,它们被用于表示模型的位置和方向,照相机的视图,和投影效果。因此,从技术上来说,我们

使用顶点着色器从模型的局部位置(就是相对位置)产生它们在场景中的真实位置(就是绝对位置)。这是角色动画系统

在骨骼动画上工作得原因,这里的矩阵用于定义通过几何群体定义的骨骼动画的姿势。

在 XNA 数学库中有少量函数用于构建投影矩阵,它们的更多讨论见于第六章。现在我们考察函数

XMMatrixOrthographicOffCenterLH 作为上面论述的开始。该函数用于创建使用左手坐标系(见于第六章)的正投影矩阵。

该函数的返回值是一个 XMMATRIX 结构,这里它用来表示投影矩阵。该函数使用视图的坐标偏移来计算出相对于 X,

Y 轴左上角的用户值。该函数的原型如下:

XMMATRIX XMMatrixOrthographicOffCenterLH(

FLOAT ViewLeft,

FLOAT ViewRight,

FLOAT ViewBottom,

FLOAT ViewTop,

FLOAT NearZ,

FLOAT FarZ

)

该函数的参数定义好投影视口(viewport),这里第一个参数是 X 的最小值,第二个参数是 X 的最大值,第三个参

数是 Y 的最小值,第四个参数是 Y 的最大值。最后两个参数用于设置近裁剪面和远裁剪面(见于第 6 章)。近裁剪平

面将会消除需要渲染在它前面的对象,而远裁剪面用于消除任何在它之后的对象。这就创建了著名的视见体。视见

体是 3D 图形学中的一个重要概念,这里深度被广泛使用。

投影矩阵使得系统知道精灵将在网格(grid)中占的尺寸大小。例如,如果视口是宽 640 像素和高 480 像素,那么

投影矩阵将会限制精灵在视口区域中可见,不在该区域的的精灵将在屏幕上不可见。这是通过 Direct3D 在硬件中处

理而完成。

如果视口的大小改变,或者如果我们表现一些特殊的照相机效果,只需要修改投影矩阵。不必担心这些知识,

我们将在第六章中深入讨论。开始使用这些矩阵时,不必一定要理解它的内部细节,就像你只需一般的了解 XNA 数

学库就可以使用它一样。慢慢的随着你深入使用这些知识来做你所希望的任何事情,则深刻理解那些内部细节才变

得重要起来。

精灵的位置和缩放(Positioning and Scaling Sprites)

现在这些精灵知道它们在环境中的大小,以及它们可能在其中的位置。在一个空间内移动物体叫做平移,就是

将物体从一个位置移动到另一个位置。精灵有两个维度,自然它们能够在两个方向 X,Y 移动,尽管在 Direct3D 中我

们学术的认为在 3D 空间中移动,但不妨碍用于 2D 对象。

如果你想将精灵的位置设置在 640x480 的显示区域,能够将精灵平移至 X,Y 位置为(320,240)来完成。这里事实

上,当使用一个定义屏幕左上角位置为(0,0)的正投影,移动精灵水平方向 320 个像素和垂直方向 240 个像素。当精

灵被平移时,它们是基于一个内部叫做平移点来完成移动。在精灵内部的平移点使用精灵的模型空间的原点作为默

认位置。当在游戏中使用精灵作为角色时,常见于使用精灵的左上角作为平移点来移动。

当移动一个精灵时,需要创建另一种矩阵——平移矩阵,在本章前面有它的简介。平移矩阵一旦定义好后,可

通过Direct3D来设置精灵(或其他任何几何图形)的位置。平移矩阵的创建能够通过函数 XMMatrixTranslation来完成,

其函数原型是:XMMATRIX XMMatrixTranslation( FLOAT OffsetX, FLOAT OffsetY, FLOAT OffsetZ )

该函数的返回值是一个平移矩阵,其参数是 X,Y 和 Z 的平移增量,在第六章中我们将会使用 XNA 数学库函数来

创建旋转和缩放矩阵。

上述虽然是矩阵的一个预览,但是它们的目的很明确。我们创建平移矩阵来移动物体,旋转矩阵来旋转它们,

和使用缩放矩阵对它们缩放(例如放大和缩小)。结合这三类矩阵能够创建世界变换矩阵,而结合视图的世界矩阵和

投影矩阵能够创建出模型-视图投影矩阵,它主要是通过顶点着色器来使用。顶点着色器的目的就是操纵输入的顶点,

而矩阵变换是顶点着色器的一个公共的任务。我们保留照相机和视图的讨论和先忽略视图矩阵,直到第六七八章中

的讨论。

游戏精灵 Demo

在目录 Chapter3/GameSprite 中能够找到 Game Sprite Demo。该 Demo 的目的是创建一个游戏精灵结构,用于显

示精灵的单个实例。我们将编写代码来绘制精灵到屏幕上,这是迈向创建 2D 游戏的第一步。

目前我们很清楚知道创建每个精灵所需要的数据。为了创建一个简单的游戏精灵类需要下列成员:

Position——位置

Rotation——旋转

Scale——缩放

Sprite image——精灵纹理

Vertex buffer——顶点缓存

因为这是一本入门类的书籍,我们将创建一个顶点缓存和纹理应用于每个唯一的精灵,并且将它们渲染在场景

中(例如:多个精灵能够使用同样的纹理和顶点缓存)。在商业级别的游戏中进行大量的绘制调用和诸多的状态改变

将会导致性能问题,我们选择考虑使用成批次的几何图形和使用纹理集合。这些知识属于高级主题,需要更多的

Direct3D 的中级知识来合理的实现。(注意:一个建议是避免过早的考虑优化。只有当瓶颈已经完全确定,并且出现

性能瓶颈的地方有一个很好的性能改进方案时才进行优化。在那些对性能没有负载的代码上或者优先级很低的代码

上进行优化是浪费时间而又徒劳无功的。一句话,就是不要优化其它的代码,当池塘里有更大的鱼的时候(比喻糟糕

性能的代码和瓶颈))。

游戏精灵类相当的简单,它的目的就是当渲染它的时候,使用位置,旋转,和缩放值,来构建精灵的世界矩阵。

一旦该世界矩阵构建好后,使用常量缓存将该矩阵传递给顶点着色器。为了消除载入重复的纹理或者是创建重复的

顶点缓存,在该 Demo 中我们只创建它们一次,将它们应用在每个我们绘制的精灵上。因为我们集中注意于在多个

精灵上的渲染和使用矩阵来放置它们,我们将不会引入必要的概念之外的过度复杂性(译者注:刚刚好就可以)。

游戏精灵类见于清单 3.13。而 Game Sprite Demo 类见于清单 3.14。在 Demo 类中我们有一个用于绘制的精灵资

源数组,一块允许我们传递数据给顶点着色器的常量缓存,一个视图投影矩阵(类型 XMMATRIX),和一个新的混合

状态对象。

清单 3.13:来自头文件 GameSprite.h 中的 GameSprite 类

#include<xnamath.h>

class GameSprite

{

public:

GameSprite( );

virtual ~GameSprite( );

XMMATRIX GetWorldMatrix( );

void SetPosition( XMFLOAT2& position );

void SetRotation( float rotation );

void SetScale( XMFLOAT2& scale );

private:

XMFLOAT2 position_;

float rotation_;

XMFLOAT2 scale_;

};

清单 3.14:来自头文件 GameSpriteDemo.h 的 GameSpriteDemo 类定义

#include"Dx11DemoBase.h"

#include"GameSprite.h"

class GameSpriteDemo : public Dx11DemoBase

{

public:

GameSpriteDemo( );

virtual ~GameSpriteDemo( );

bool LoadContent( );

void UnloadContent( );

void Update( float dt );

void Render( );

private:

ID3D11VertexShader* solidColorVS_;

ID3D11PixelShader* solidColorPS_;

ID3D11InputLayout* inputLayout_;

ID3D11Buffer* vertexBuffer_;

ID3D11ShaderResourceView* colorMap_;

ID3D11SamplerState* colorMapSampler_;

ID3D11BlendState* alphaBlendState_;

GameSprite sprites_[2];

ID3D11Buffer* mvpCB_;

XMMATRIX vpMatrix_;

};

混合状态对象的类型是 ID3D11BlendState。使用它的原因是为了使用 alpha 透明管道来渲染我们的纹理精灵。

其意思是我们必须使用有 alpha 通道的 32bit 的纹理图片。我们使用的贴图见于插图 3.21 中的 Photoshop 中的展示。

常量缓存用于发送模型-视图矩阵给顶点着色器,使得它能够对输入的几何图形进行变换。因为这里没有照相机类,

因此投影矩阵一般不会发生变换除非是窗口的尺寸变了,所以我们能够只计算投影矩阵一次,当我们计算全部的模

型-视图矩阵时在渲染函数中使用投影矩阵,这就是为什么我们有一个 XMMATRIX 类型的成员。

插图 3.21:有透明区域的贴图

创建和渲染游戏精灵

GameSprite.cpp 源文件实现了 GameSprite 类中的成员函数,在该 Demo 中我们简单的用一种方式来存储每一个

精灵的位置,旋转和缩放来构建精灵的世界(模型)矩阵。GameSprite.cpp 内容见于清单 3.15。读者将注意到这里将

每条轴的缩放比例都设置为 1.0f,因为任何小于 1.0f 的设置将会缩小精灵,而大于 1.0f 的设置将会放大精灵。而设

置为 1.0f 会保持精灵的原来的尺寸不变。

清单 3.15:GameSprite.cpp 源文件

#include<d3d11.h>

#include<d3dx11.h>

#include"GameSprite.h"

GameSprite::GameSprite( ) : rotation_( 0 )

{

scale_.x = scale_.y = 1.0f;

}

GameSprite::~GameSprite( )

{}

XMMATRIX GameSprite::GetWorldMatrix( )

{

XMMATRIX translation = XMMatrixTranslation( position_.x, position_.y, 0.0f);

XMMATRIX rotationZ = XMMatrixRotationZ( rotation_ );

XMMATRIX scale = XMMatrixScaling( scale_.x, scale_.y, 1.0f );

return translation * rotationZ * scale;

}

void GameSprite::SetPosition( XMFLOAT2& position )

{

position_ = position;

}

void GameSprite::SetRotation( float rotation )

{

rotation_ = rotation;

}

void GameSprite::SetScale( XMFLOAT2& scale )

{

scale_ = scale;

}

为了渲染游戏精灵,我们通过构建世界矩阵,并且使用函数 VSSetConstantBuffer 将之用于顶点着色器的常量缓

存来绘制每一个游戏精灵,我们还将精灵使用的着色器和贴图绑定,使用精灵资源来渲染几何图形。如果我们想渲

染多个游戏精灵,我们能够对场景中的游戏精灵重复上述过程,不过使用循环更加的简单。GameSprite Demo 的渲

染函数见于清单 3.16。因为每个游戏精灵不同的地方仅仅是它们的模型-视图投影矩阵,我们只需在一次循环的过程

中将它们设置好。每次我们调用 Draw 绘制函数来绘制输出精灵最后通过模型-视图投影矩阵设置的位置。

清单 3.16:Game Sprite Demo 的渲染函数

void GameSpriteDemo::Render( )

{

if( d3dContext_ == 0 )

return;

float clearColor[4] = { 0.0f, 0.0f, 0.25f, 1.0f };

d3dContext_->ClearRenderTargetView( backBufferTarget_, clearColor );

unsigned int stride = sizeof( VertexPos );

unsigned int offset = 0;

d3dContext_->IASetInputLayout( inputLayout_ );

d3dContext_->IASetVertexBuffers( 0, 1, &vertexBuffer_, &stride, &offset );

d3dContext_->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST);

d3dContext_->VSSetShader( solidColorVS_, 0, 0 );

d3dContext_->PSSetShader( solidColorPS_, 0, 0 );

d3dContext_->PSSetShaderResources( 0, 1, &colorMap_ );

d3dContext_->PSSetSamplers( 0, 1, &colorMapSampler_ );

for( int i = 0; i < 2; i++ )

{

XMMATRIX world = sprites_[i].GetWorldMatrix( );

XMMATRIX mvp = XMMatrixMultiply( world, vpMatrix_ );

mvp = XMMatrixTranspose( mvp );

d3dContext_->UpdateSubresource( mvpCB_, 0, 0, &mvp, 0, 0 );

d3dContext_->VSSetConstantBuffers( 0, 1, &mvpCB_ );

d3dContext_->Draw( 6, 0 );

}

swapChain_->Present( 0, 0 );

}

上述渲染代码中我们通过调用 VSSetConstantBuffers 来设置顶点着色器的常量缓存。常量缓存,像其它所有的

DirectX 11 缓存一样是 ID3D11BUFFER 类型,在我们的 LoadContent 函数中创建。之前我们提到,一块常量缓存通过

设置缓存描述对象的 BindFlags 成员为 D3D11_BIND_CONSTANT_BUFFER 来创建。

游戏精灵和它的各种资源在 Demo 的 LoadContent 函数中创建,在 UnloadContent 函数中释放,两者见于清单

3.17。该 Demo 直接从本章之前的 Texture Map Demo 中建立,那么我们省略 LoadContent 函数中来自于之前 Demo

中完全一样的代码部分。

清单 3.17:LoadContent 和 UnloadContent 函数实现

bool GameSpriteDemo::LoadContent( )

{

// ... Previous code from the Texture Map demo...

ID3D11Resource* colorTex;

colorMap_->GetResource( &colorTex );

D3D11_TEXTURE2D_DESC colorTexDesc;

( ( ID3D11Texture2D* )colorTex )->GetDesc( &colorTexDesc );

colorTex->Release( );

float halfWidth = ( float )colorTexDesc.Width / 2.0f;

float halfHeight = ( float )colorTexDesc.Height / 2.0f;

VertexPos vertices[] =

{

{ XMFLOAT3( halfWidth, halfHeight, 1.0f ), XMFLOAT2( 1.0f, 0.0f ) },

{ XMFLOAT3( halfWidth, -halfHeight, 1.0f ), XMFLOAT2( 1.0f, 1.0f ) },

{ XMFLOAT3( -halfWidth, -halfHeight, 1.0f ), XMFLOAT2( 0.0f, 1.0f ) },

{ XMFLOAT3( -halfWidth, -halfHeight, 1.0f ), XMFLOAT2( 0.0f, 1.0f ) },

{ XMFLOAT3( -halfWidth, halfHeight, 1.0f ), XMFLOAT2( 0.0f, 0.0f ) },

{ XMFLOAT3( halfWidth, halfHeight, 1.0f ), XMFLOAT2( 1.0f, 0.0f ) },

};

D3D11_BUFFER_DESC vertexDesc;

ZeroMemory( &vertexDesc, sizeof( vertexDesc ) );

vertexDesc.Usage = D3D11_USAGE_DEFAULT;

vertexDesc.BindFlags = D3D11_BIND_VERTEX_BUFFER;

vertexDesc.ByteWidth = sizeof( VertexPos ) * 6;

D3D11_SUBRESOURCE_DATA resourceData;

ZeroMemory( &resourceData, sizeof( resourceData ) );

resourceData.pSysMem = vertices;

d3dResult = d3dDevice_->CreateBuffer( &vertexDesc, &resourceData,&vertexBuffer_ );

if( FAILED( d3dResult ) )

{

DXTRACE_MSG( "Failed to create vertex buffer!" );

return false;

}

D3D11_BUFFER_DESC constDesc;

ZeroMemory( &constDesc, sizeof( constDesc ) );

constDesc.BindFlags = D3D11_BIND_CONSTANT_BUFFER;

constDesc.ByteWidth = sizeof( XMMATRIX );

constDesc.Usage = D3D11_USAGE_DEFAULT;

d3dResult = d3dDevice_->CreateBuffer( &constDesc, 0, &mvpCB_ );

if( FAILED( d3dResult ) )

{

return false;

}

XMFLOAT2 sprite1Pos( 100.0f, 300.0f );

sprites_[0].SetPosition( sprite1Pos );

XMFLOAT2 sprite2Pos( 400.0f, 100.0f );

sprites_[1].SetPosition( sprite2Pos );

XMMATRIX view = XMMatrixIdentity( );

XMMATRIX projection = XMMatrixOrthographicOffCenterLH( 0.0f, 800.0f,0.0f, 600.0f, 0.1f, 100.0f );

vpMatrix_ = XMMatrixMultiply( view, projection );

D3D11_BLEND_DESC blendDesc;

ZeroMemory( &blendDesc, sizeof( blendDesc ) );

blendDesc.RenderTarget[0].BlendEnable = TRUE;

blendDesc.RenderTarget[0].BlendOp = D3D11_BLEND_OP_ADD;

blendDesc.RenderTarget[0].SrcBlend = D3D11_BLEND_SRC_ALPHA;

blendDesc.RenderTarget[0].DestBlend = D3D11_BLEND_ONE;

blendDesc.RenderTarget[0].BlendOpAlpha = D3D11_BLEND_OP_ADD;

blendDesc.RenderTarget[0].SrcBlendAlpha = D3D11_BLEND_ZERO;

blendDesc.RenderTarget[0].DestBlendAlpha = D3D11_BLEND_ZERO;

blendDesc.RenderTarget[0].RenderTargetWriteMask = 0x0F;

float blendFactor[4] = { 0.0f, 0.0f, 0.0f, 0.0f };

d3dDevice_->CreateBlendState( &blendDesc, &alphaBlendState_ );

d3dContext_->OMSetBlendState( alphaBlendState_, blendFactor, 0xFFFFFFFF );

return true;

}

void GameSpriteDemo::UnloadContent( )

{

if( colorMapSampler_ ) colorMapSampler_->Release( );

if( colorMap_ ) colorMap_->Release( );

if( solidColorVS_ ) solidColorVS_->Release( );

if( solidColorPS_ ) solidColorPS_->Release( );

if( inputLayout_ ) inputLayout_->Release( );

if( vertexBuffer_ ) vertexBuffer_->Release( );

if( mvpCB_ ) mvpCB_->Release( );

if( alphaBlendState_ ) alphaBlendState_->Release( );

colorMapSampler_ = 0;

colorMap_ = 0;

solidColorVS_ = 0;

solidColorPS_ = 0;

inputLayout_ = 0;

The Game Sprite Demo 149

vertexBuffer_ = 0;

mvpCB_ = 0;

alphaBlendState_ = 0;

}

在函数 LoadContent 中,我们做的第一件事事获得已经载入的纹理的入口地址。并且我们获得纹理的宽和高,

这些信息用于顶点缓存创建期间,使得图像的尺寸指示精灵在屏幕上的大小。在我们创建顶点缓存之后,使用我们

从纹理图像中获得的信息(宽和高)来创建常量缓存。因此我们在每一帧中设置常量缓存,直到它的内容被填充之后

才进行渲染。

LoadContent 函数的最后一部分是设置我们的两个精灵实例的位置,创建基于正投影的视图-投影矩阵,还创建

我们的混合状态对象。我们使用混合状态来启用 alpha 透明度,这是一种使用每一个颜色的 alpha 通道来指导怎样

显示该颜色的技术。Alpha 值为 1.0f 表示完全可见,0.0f 表示不可见,而介于之间的值是半透明效果。

为了创建混合状态,我们调用 D3D 设备的 CreateBlendState 函数,并且调用 D3D 设备环境的 OMSetBlendState

函数进行混合状态的设置。CreateBlendState 创建函数有两个参数,其一是一个混合描述子,另一个则是创建出的混

合状态对象的输出地址。OMSetBlendState 函数接受创建出的混合状态对象,一个用于每个颜色通道的混合因子,

一个混合 mask(掩码),后两个参数用于高级混合效果,这里我们只使用它们的默认值。

混合描述子的类型是 D3D11_BLEND_DESC。该结构的一些成员用于混合的高级主题,比如多重采样,alpha 覆盖

等,但是现在我们只需少量的设置用于处理渲染目标的源和目的混合即可。在代码清单中可以看到我们在主渲染目

标上启用了 alpha 混合和设置依赖于源颜色的 alpha 值的混合源和依赖于 1-alpha(alpha 的差值)的混合目标。这就允

许我们使用输入的源颜色 alpha 值和基于 1-alpha 的目标颜色值进行混合。换句话说,就是任何使用 0.0f 的 alpha 值

的源颜色的结果是显示目标颜色,而任何使用 1.0f 的 alpha 值的源颜色的结构就是完全显示自己。任何在 0.0f-1.0f

之间的 alpha 值混合结果是介于源颜色(正在将其渲染到表面的颜色)和目标颜色(已经被渲染到表面的颜色)之间的某

一颜色。

最后一部分的谜团就是 HLSL 着色器代码,见于清单 3.18。该 Demo 的运行时截图见于插图 3.22。在 HLSL 代码

中,我们使用关键字 cbuffer 来设置常量缓存。该关键字允许我们在 HLSL 中创建一个单一的常量缓存,但是常量缓

存中的内容是能够访问到的,就好像它们是定义的全局变量一样,你将看到在顶点着色器中直接使用 mvp_变量。

我们创建的常量缓存与输入寄存器 b0 进行绑定,就像贴图绑定 t0 或者更高的,采样对象绑定 s0 或者更高的一样。

在本 Demo 中的 HLSL 着色器代码与之前代码的主要变化是在顶点着色器中使用模型-视图投影矩阵来变换输入

的顶点。这通过多重采样顶点结合矩阵来完成。已经变换后的顶点通过管线传递给后面的阶段处理。而像素着色器

代码直接使用 TextureMapDemo 中的内容而没有变化。

清单 3.18:GameSpriteDemo 的 HLSL 着色器代码

cbuffer cbChangesPerFrame : register( b0 )

{

matrix mvp_;

};

Texture2D colorMap_ : register( t0 );

SamplerState colorSampler_ : register( s0 );

struct VS_Input

{

float4 pos : POSITION;

float2 tex0 : TEXCOORD0;

};

struct PS_Input

{

float4 pos : SV_POSITION;

float2 tex0 : TEXCOORD0;

};

PS_Input VS_Main( VS_Input vertex )

{

PS_Input vsOut = ( PS_Input )0;

vsOut.pos = mul( vertex.pos, mvp_ );

vsOut.tex0 = vertex.tex0;

return vsOut;

}

float4 PS_Main( PS_Input frag ) : SV_TARGET

{

return colorMap_.Sample( colorSampler_, frag.tex0 );

}

插图 3.22:GameSpriteDemo 程序截图

本章总结

到现在为止,你应该基本的理解了精灵和纹理贴图的工作过程。使用本章所学到的知识,现在你就有可能创建

一个简单的基于精灵概念的 2D 游戏。使用 Direct3D 来绘制 2D 图形只需要在我们的几何图形中用 X 和 Y 表示位置即

可。我们现在能够使用各种向量和 Direct3D 提供的矩阵数学代码来操纵这些 2D 对象,直到符合我们的期望为止。

学术的说,在 Direct3D 中并没有真正的 2D 模型,这里的 2D 模型是通过使用正投影或者只使用几何体的 X 和 Y 轴而

忽略它们的深度值来构建的。在本章中我们使用的是正投影,所有即使这些精灵的深度不同,也不会影响它们在屏

幕上显示的效果。如果我们使用透视投影,它通常用于 3D 渲染,那么深度值对显示有很大的影响,这就开始进入

到真 3D 领域了。

已经学会的知识

在本章中你已经学会了下列知识:

怎样加载纹理

精灵是什么和怎样使用它们

章末习题

答案见于本书的附录 A,可从伴随网站下载。

1. 定义什么是纹理贴图

2. 定义什么是精灵。对于纹理贴图来说,精灵有什么不同。

3. 列出至少 5 种本章中提到的纹理类型。

4. RGB 图像的每个像素使用多少位表示,RGBA 图像呢?

5. 顶点的定义。

6. 三角形的定义。

7. 顶点缓存的目的是什么?Direct3D 缓存对象类型是什么?

8. 列出顶点的至少 5 种属性。

9. 定义一个输入布局和它们在 Direct3D 中怎样使用。

10. 顶点着色器是什么,和怎样使用 Direct3D 函数设置它们?

11. 像素着色器是什么,和怎样使用 Direct3D 函数设置它们?

12. 列出至少三种我们在 Demo 中的渲染函数中使用的输入装配(input assembler)函数。

13. 什么是 MIP 层级机制?

14. 什么是采样状态,和它们在 Direct3D 和 HLSL 中的目的是什么?

15. 什么是混合状态,和在本章中我们使用它们用于何种目的?

16. 定义一个矩阵

17. 使用哪几种矩阵来构建模型-视图投影矩阵,和它们的目的是什么?

18. 什么是贴图访问模式?定义 D3D11_TEXTURE_ADDRESS_MODE 枚举类型的每一种模式。

19. 为了启用 alpha 透明效果,我们需要创建和设置哪些 Direct3D 11 对象?

20. 为什么我们决定使用一个 XMFLOAT4X4 类型代替 XMMATRIX,来作为 GameSpriteDemo 的类成员?

自己动手做实验

选择一种图像编辑器来创建你自己的精灵图像。使用新创建的图像作为纹理贴图来创建第二个精灵对象。

第四章 文本和字体的渲染

当你编写游戏时,文本看起来没有那么重要;它是一个次要元素而不真正的影响着游戏。真的是这样么?当你

完成一个游戏时文本远比你看到的仅仅显示屏幕标题或滚动致谢这些重要。但你需要给玩家说明,游戏指定的目标,

或者让用户知道在他的仓库中还有多少金币,对于这些文本就很重要了。对于用户界面和反馈来说,文本是一个重

要的工具,例如用于弹药统计,玩家血条或护甲值,魔法值等等。

本章中将学到下列知识:

为什么字体和文本对于游戏开发者来说很有用。

字体系统是怎样工作的。

怎样从零开始创建一套简单的字体系统。

怎样绘制文本到屏幕上。

游戏中的文本

从程序员的观点来看,文本是最好的调试工具。你能够增加任何数量的实时信息用于掌握运行时的状态,例如

帧率统计或玩家名字的文本显示。一个显示帧率统计和在游戏中的玩家名字的商业游戏的例子见于插图 4.1 和 4.2。

插图 4.1:在 Epic 游戏 Unreal Development Kit 中的 FPS 统计

插图 4.2:在 Unreal Development Kit 中的游戏控制台

许多游戏都实现了其自身的控制台调试器,用来跟踪和操纵游戏中的不同区域。控制台调试器是一个特殊屏幕,

使用者可以输入在游戏中预先准备好的命令来执行不同的行为。可以想象一下,它使用游戏中的不同命令来修改某

些值和状态,比如显示 FPS 统计数(例如,在 Unreal Tournament 游戏中),输入作弊命令,启用调试统计信息,产生

很多物体,修改游戏特性等。一个使用游戏控制调试器的例子见于插图 4.2。

在 PC 机上的在线竞技游戏,文本以一种普遍的方式用来在不同玩家之间沟通和交流。而执行这种交流方式通

常是按下一个特殊键来完成(例如,键盘上的“T”键),这些交流信息或者用于团队,或者用于对战双方,其信息内

容通常是你自己想发送给其它玩家的文本消息。现在的家庭主机游戏,比如微软的 Xbox360 和索尼的 PS3,在线玩

家都使用麦克风和语音进行交流,但是在 PC 机上的游戏有其历史原因使得使用文本通信作为玩家之间的首要交流

方式。在 Unreal 引擎的简单游戏中使用文本提示的例子见于插图 4.3。(注意:许多家庭主机游戏在屏幕使用一个紧

挨着玩家名字或屏幕名字的图形元素,比如一个讲话者或麦克风图标,来表明有一些玩家正在语音聊天。)

插图 4.3:在 Unreal Development Kit 中的文本提示

增加文本

游戏中的文本有多种实现方式,比如在屏幕上进行几何贴图,使用矢量绘制,甚至仅仅使用标准的消息盒。在

现代游戏开发中,大部分游戏开发库都支持文本渲染,减少开发者自己实现的负担。在 Direct3D 11 之前,DirectX

就已经支持文本渲染了,但是 Direct3D 11 并没有提供之前享有的同样的内建解决方案。现在我们不得不使用依赖于

动态绘制精灵的字体系统来手工进行文本的渲染,这也是这章的主题。下面部分描述字体系统一般是怎样组成的。

贴图字体

一套字体是使用某一绘画风格写就的一系列的字母和符号。例如,在微软 Word 中的 Times New Roman 字体就

与 Verdana 或 Arial 字体的外观不同。换句话说,就是它们有不同的绘画风格。在 2D 和 3D 游戏中,文本通常使用

贴图的几何图形进行绘制,就像我们在第三章中对精灵那样做的一样。正如我们这所描述的,术语字体(font)实际用

于印刷排版,因为由字母和符号组成的字体是一种具体的艺术风格。

排版被定义为在媒体中作为研究,设计和排列的类型。对于网站,印刷,电视传媒等,图形设计者将评判排版

工作作为他们的工作得重要组成部分。排版能够构建或打破一个设计,有很多书籍关注这个主题。游戏中排版依然

很重要,它的使用不能被低估。虽然我们集中于游戏的程序编写,不会被卷入艺术和图形设计方面的事情,但是排

版对于设计师来说就相当的重要了。

贴图字体,也就是位图字体,是将字母预先绘制在贴图图像中的一种字体类型。所有字母被安排在一个网格之

内,每个单一字母占据网格的一个部分。当需要写出单词时,这些单词通过绘制每一个贴图字母来动态构建在屏幕

上。这种方式可以通过使用一系列的单个字母来创建任何单词或你所希望的短语。因为以这种简单自然的方式使用

字体,从而文本的渲染能够十分的快速完成。也因为字体预先绘制在一张纹理上,从而能够通过改变应用的纹理来

修改文本的外观和感觉。

尽管上述方式十分有用,但是贴图字体依旧有一些不足:

1. 贴图字体不能很好的缩放,因为它们是一块纹理的片段,它们只能放大或缩小一点点,否则会被渲染得很

丑陋和变得不可读。

2. 贴图必须支持你可能使用的每一个字母或符号,这会使得贴图文件十分大。

3. 本地化。如果你的游戏支持多种语言,你必须有各种语言字符集的贴图,在语言之间进行本地化翻译。

4. 因为对于每一个字符,它前面和后面的空距不同。当写出某些单词时贴图字体有时会出现奇怪的现象,这

是因为我们经常对每个字符使用相同的空间尺寸,而不是基于字符的实际宽度。例如,字母“P”就比字母

“l”要宽,但是因为它们映射的几何图形(每个字母的格子空间,在排版时作为字母本身的空间)有同样的

尺寸,导致一些字母间的空白不等宽。

5. 首次创建鲁棒性(robust,健壮性)的字体系统是十分具有挑战性的。

贴图字体允许我们以自身需要的方式来构建,并且能够使用不同种类的字体系统。贴图字体也能够用于创建不

够精细的用户界面的外观和感觉。

字体系统的解释

经常有这样的例子,在游戏的图表中硬编码任何需要用到的文本也是有意义的;当你需要动态绘制文本时,就

需要使用一套字体系统,比如当请求玩家输入,显示玩家的聊天(例子见于插图 4.3 中的 Unreal Development Kit),

等等。如果文本不是动态的,其原因可能是使用贴图的方块作为字体系统就足够了。当然原因并不总是那样。

想象一下,你正在玩一款 RPG 游戏,并且你需要与村庄中的居民对话。在硬编码文本系统中,角色所说的是预

先准备好放在一张纹理中,在适当的触发条件下显示出来。换句话说,我们需要准备好用于在游戏中任何可能的对

话交谈的文本贴图。由于有每一次对话的文本,这意味着需要载入大量的文本贴图。

而动态文本系统通过载入在字体中包含的任何字母的单一贴图来构建那些文本字符串。这种方法能够节省使用

所有文本贴图的加载时间和大量内存。在动态系统中,文本通过创建一个与所希望输出的字符的贴图坐标匹配区域

一一对应的方块列表来产生。

使用精灵来创建一套字体系统

Direct3D 11 没有像之前版本那样提供字体和文本渲染的支持。因此我们必须采取手动绘制文本的方式,这可以

通过一个使用动态顶点缓存和贴图映射的例子看到。在目录 Chapter4/Direct3DText 中,可以找到关于文本渲染的例

子——Direct3D Text。

Direct3D Text Demo 将会在第三章的 Texture Mapping Demo 的例子上进行构建。实际上,我们只需要增加一些

代码来将之前 Demo 中的顶点缓存修改为动态缓存,和增加一个函数来将我们的贴图精灵填充到缓存中即可。动态

缓存对于我们需要修改一块缓存中的内容的这种情况来说是很合适的。不推荐多次创建和销毁静态缓存块,特别是

逐帧这样做,你应该使用动态缓存来做这样的任务。

D3DTextDemo 类的定义见于清单 4.1,这里只在 Texture Mapping Demo 类的头文件中添加了一个 DrawString 函

数。该函数接受我们希望显示输出的文本,开始输出的 X,Y 屏幕位置。

清单 4.1:D3DTextDemo 类

#ifndef _D3D_TEXT_DEMO_

#define _D3D_TEXT_DEMO_

#include"Dx11DemoBase.h"

class D3DTextDemo : public Dx11DemoBase

{

public:

D3DTextDemo( );

virtual ~D3DTextDemo( );

bool LoadContent( );

void UnloadContent( );

void Update( float dt );

void Render( );

private:

bool DrawString( char* message, float startX, float startY );

private:

ID3D11VertexShader* solidColorVS_;

ID3D11PixelShader* solidColorPS_;

ID3D11InputLayout* inputLayout_;

ID3D11Buffer* vertexBuffer_;

ID3D11ShaderResourceView* colorMap_;

ID3D11SamplerState* colorMapSampler_;

};

#endif

该 Demo 将以一种直接的方式进行工作,首先,我们载入一幅包含字母 A-Z 的纹理图像。该纹理图像见于插图

4.4,以最左边的字母 A 开始直到最右边的空格为止。最右边的空格用于表示不在 A-Z 之间的无效字符。为了该 Demo

的简单性,我们只支持大写字母,而没有符号和数字。一旦你处理过了该 Demo 怎样工作后,就能够增加支持那些

项,作为一个练习,读者可以尝试着扩展这个简单的文本渲染系统。

贴图渲染算法使用的图片见于插图 4.4,该算法比较简单。对于在字符串中的每个字母,我们将创建一个新的

精灵。第一个精灵的 X,Y 开始位置,作为参数传递给函数 DrawString,则字符串中的每个精灵(字母)将会依次出现。

在贴图图像中每个字母都是 32x32 像素的尺寸,因此有 26 个字母和一个空格的纹理图像的分辨率是 864x32。为了

代码的简单性,在纹理图像中所有字母水平方向依次出现。那么可以想象计算贴图坐标和顶点位置就变得相当容易,

因为我们不必担心多行和符号问题。

回顾一下,我们通过遍历字符串的每个字母来依次创建精灵的这种方式来创建我们的文本几何图形。当我们需

要产生贴图坐标时,使用所需的字母来指导贴图坐标的设置。我们使用纹理图片末尾的空字符来处理无效的字母。

插图 4.4:显示在 DirectX Texture Tool 中的 font.dds 贴图图像

在查看产生文本几何图形的代码之前,我们先看一下清单 4.2 中的 Render 函数。我们使用新的 DrawString 函数

来代替来自于 Texture Mapping Demo 中的函数 d3dContext_->Draw。DrawString 函数将会负责生成文本几何图形并且

渲染它。除了我们使用深蓝色作为背景清除颜色外,代码的其余部分都是与 Texture Mapping Demo 中的相同。

清单 4.2:渲染文本

void D3DTextDemo::Render( )

{

if( d3dContext_ == 0 )

return;

float clearColor[4] = { 0.2f, 0.22f, 0.24f, 1.0f };

d3dContext_->ClearRenderTargetView( backBufferTarget_, clearColor );

unsigned int stride = sizeof( VertexPos );

unsigned int offset = 0;

d3dContext_->IASetInputLayout( inputLayout_ );

d3dContext_->IASetVertexBuffers( 0, 1, &vertexBuffer_, &stride, &offset );

d3dContext_->IASetPrimitiveTopology( D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST );

d3dContext_->VSSetShader( solidColorVS_, 0, 0 );

d3dContext_->PSSetShader( solidColorPS_, 0, 0 );

d3dContext_->PSSetShaderResources( 0, 1, &colorMap_ );

d3dContext_->PSSetSamplers( 0, 1, &colorMapSampler_ );

DrawString( "HELLO WORLD", -0.2f, 0.0f );

swapChain_->Present( 0, 0 );

}

让我们来看一下修改后的 LoadContent 函数,只有一个修改的地方就是我们载入纹理图像文件为 font.dds(代替

Texture Mapping Demo 中的纹理文件 decal.dds)和创建了一块动态顶点缓存。为了创建一块动态顶点缓存,我们需要

执行如下步骤:

1. 设置使用标识为 D3D11_USAGE_DYNAMIC 来允许我们的缓存能够通过 CPU 来动态更新。在之前的 Demo 中

对于静态缓存我们设置的使用标识是 D3D11_USAGE_DEFAULT。

2. 设置 CPU 访问标识为 D3D11_CPU_ACCESS_WRITE。这是为了让 CPU 能够对 GPU 中的资源进行写访问。其它

的访问标识还有读访问和读写访问,但是在本 Demo 中我们不需要 CPU 读任何东西。

3. 当创建顶点缓存时,我们将子资源参数(第二个参数)设置为 NULL,因为我们会对顶点缓存的内容进行动态

更新,从而在创建时不需要设置任何数据。

当动态缓存创建后,我们能够在任何时候对它进行更新。我们对 LoadContent 函数所做出的改变见于清单 4.3。

因为 DrawString 函数将会创建文本几何图形,我们可以将在 Texture Mapping Demo 中的定义两个三角形和将三角形

数据拷贝给 Direct3D 子资源对象的代码删除。在清单 4.3 中需要注意的是,我们设置顶点缓存存储了 24 个精灵(例

如 24 个字母)。读者能够将其改变为自己所希望的字符数。

清单 4.3:LoadContent 函数的更新部分

bool D3DTextDemo::LoadContent( )

{

. . .

d3dResult = D3DX11CreateShaderResourceViewFromFile( d3dDevice_,"font.dds", 0, 0, &colorMap_, 0 );

. . .

D3D11_BUFFER_DESC vertexDesc;

ZeroMemory( &vertexDesc, sizeof( vertexDesc ) );

vertexDesc.Usage = D3D11_USAGE_DYNAMIC;

vertexDesc.CPUAccessFlags = D3D11_CPU_ACCESS_WRITE;

vertexDesc.BindFlags = D3D11_BIND_VERTEX_BUFFER;

const int sizeOfSprite = sizeof( VertexPos ) * 6;

const int maxLetters = 24;

vertexDesc.ByteWidth = sizeOfSprite * maxLetters;

d3dResult = d3dDevice_->CreateBuffer( &vertexDesc, 0, &vertexBuffer_ );

if( FAILED( d3dResult ) )

{

DXTRACE_MSG( "Failed to create vertex buffer!" );

return false;

}

return true;

}

本 Demo 中的最后的函数是 DrawString,该函数来执行所有的任务。函数体代码有点长,我们将其分为几个部

分查看。在第一部分中,我们设置好算法所需要的值,这些值如下:

sizeOfSprite——以字节为单位单个精灵的尺寸。一个精灵是由 6 个顶点组成的两个三角形。在这的这些常量用

来增加代码的可读性。

maxLetters——本 Demo 中能够一次简单的渲染 24 个字母,但是一旦你处理了这个主题后,你能够修改它变得

更加的健壮。

length——我们需要渲染的字符串的长度。

charWidth——单个字母在屏幕上的宽度。

charHeight——单个字母在屏幕上的高度。

texelWidth——单个字母在纹理图像中的宽度。

verticesPreLetter——在每个精灵中存储所有顶点的常量(共两个三角形,六个点)。

如果字符串长超过 24 个字母,我们将对字符串进行截断,只绘制前面 24 个字母,否则我们将依字符串长全部

绘制。DrawString 函数的第一部分以及算法使用的其中的值见于清单 4.4。

清单 4.4:DrawString 函数

bool D3DTextDemo::DrawString( char* message, float startX, float startY )

{

// Size in bytes for a single sprite.

const int sizeOfSprite = sizeof( VertexPos ) * 6;

// Demo’s dynamic buffer set up for max of 24 letters.

const int maxLetters = 24;

int length = strlen( message );

// Clamp for strings too long.

if( length > maxLetters )

length = maxLetters;

// Char’s width on screen.

float charWidth = 32.0f / 800.0f;

// Char’s height on screen.

float charHeight = 32.0f / 640.0f;

// Char’s texel width.

float texelWidth = 32.0f / 864.0f;

// verts per-triangle (3) * total triangles (2) = 6.

const int verticesPerLetter = 6;

. . .

}

为了更新动态缓存中的内容,我们需要调用 Direct3D context(设备环境)的 Map 函数。该 Map 函数参数有需要

映射的缓存,子资源索引(设置为 0,因为我们没有多个子资源),映射类型,映射标识,和将会持有获得的数据的

映射子资源。在本 Demo 中映射类型是 D3D11_MAP_WRITE_DISCARD,它指示 Direct3D 将缓存中的之前的值作为未

定义考虑。对于其它的映射类型,映射标识可以是 D3D11_MAP_FLAG_DO_NOT_WAIT,但是当使用映射类型

D3D11_MAP_WIRTE_DISCARD,则映射标识必须是 0,因为刚才的标识不能用于此种映射类型。

一旦 Map 函数调用成功,我们将能通过传递给函数的最后一个参数——D3D11_MAPPED_SUBRESOURCE 类型的

对象来访问缓存中的内容。为了更新缓存,我们只需要简单的拷贝任何数据给 D3D11_MAPPED_SUBRESOURCE 结构

的 pData 成员。DrawString 函数的映射部分(第二部分)见于清单 4.5。当我们遍历字符串来产生几何图形时,使用 A-Z

的 ASCII 码来完成的代码见于清单 4.6。

清单 4.5:我们的顶点缓存的映射

bool D3DTextDemo::DrawString( char* message, float startX, float startY )

{

. . .

D3D11_MAPPED_SUBRESOURCE mapResource;

HRESULT d3dResult = d3dContext_->Map( vertexBuffer_, 0,D3D11_MAP_WRITE_DISCARD, 0, &mapResource );

if( FAILED( d3dResult ) )

{

DXTRACE_MSG( "Failed to map resource!" );

return false;

}

// Point to our vertex buffer’s internal data.

VertexPos *spritePtr = ( VertexPos* )mapResource.pData;

const int indexA = static_cast<char>( ’A’ );

const int indexZ = static_cast<char>( ’Z’ );

. . .

}

在清单 4.5 中,我们获得一个指针指向子资源的 pData,就可以循环设置需要组装的字符串的几何图案。为了

达成上述目标,我们使用一个 for-loop 的迭代器来遍历字符串。对于每一个字符,我们设置它的 X,Y 的开始位置。

而第一个字符,其 X 位置就是传入给 DrawString 函数的 startX 参数。在遍历字母的过程,我们的本质是想增加每个

字母的宽度到下一个开始位置。通过使用循环索引,我们能够将其与字母的宽度相乘来增加开始位置作为取得当前

字母的开始位置,这种方式允许我们不需要保持前一个字母的X位置,因为这个公式很容易计算出我们所需要的值。

X 的开始位置是精灵的左边,X 的结束位置是精灵的右边,这相当于将 X 的开始位置加上每一个字母的宽度。

一旦我们设置好顶点位置,我们必须同样的设置贴图坐标,贴图坐标也以同样的方式设置,只有稍微的区别。与使

用循环索引来计算顶点位置不同的是,我们使用字母本身来产生贴图坐标,这可通过使用每个字母的 ASCII 码来很

容易计算得出。因为在我们的字体文件 font.dds 纹理中的第一个字母是 A,如果我们将当前字母的 ASCII 码与 A 的

ASCII 码作差,如果当前字母是A则得到 0,如果是 B 则得到 1,等等。我们能够在 DrawString 函数中用 texLookup

函数表示这些值来使用, 就像是我们将循环索引用于创建几何图形的坐标一样。只是循环索引 i以0开始依次递增,

而 texLookup 变量用 0 表示使用第一个字母(A)然后依次递增。

贴图坐标通常范围是 0.0-1.0。我们的开始的 TU 贴图坐标用于第一个字母将会是 0.0。在 DrawString 函数中这种

写法基本一致,但并非必要。贴图坐标和几何图形坐标通过仔细检查是精确匹配的。对于无效的字符,我们将其设

置为 font.dds 纹理图像中的最后一个字符,它只是一个在纹理尾部的 32x32 的空字符。我们能够访问这个空字符,

通过假设当前的字母是 1+Z,因为我们的字体纹理将空字符设置在 Z 图案之后。DrawString 函数产生几何图形的代

码见于清单 4.6。

清单 4.6:在 DrawString 函数中循环创建文本

bool D3DTextDemo::DrawString( char* message, float startX, float startY )

{

. . .

for( int i = 0; i < length; ++i )

{

float thisStartX = startX + ( charWidth * static_cast<float>( i ) );

float thisEndX = thisStartX + charWidth;

float thisEndY = startY + charHeight;

spritePtr[0].pos = XMFLOAT3( thisEndX, thisEndY, 1.0f );

spritePtr[1].pos = XMFLOAT3( thisEndX, startY, 1.0f );

spritePtr[2].pos = XMFLOAT3( thisStartX, startY, 1.0f );

spritePtr[3].pos = XMFLOAT3( thisStartX, startY, 1.0f );

spritePtr[4].pos = XMFLOAT3( thisStartX, thisEndY, 1.0f );

spritePtr[5].pos = XMFLOAT3( thisEndX, thisEndY, 1.0f );

int texLookup = 0;

int letter = static_cast<char>( message[i] );

if( letter < indexA || letter > indexZ )

{

// Grab one index past Z, which is a blank space in the texture.

texLookup = ( indexZ - indexA ) + 1;

}

else

{

// A = 0, B = 1, Z = 25, etc.

texLookup = ( letter - indexA );

}

float tuStart = 0.0f + ( texelWidth * static_cast<float>( texLookup ) );

float tuEnd = tuStart + texelWidth;

spritePtr[0].tex0 = XMFLOAT2( tuEnd, 0.0f );

spritePtr[1].tex0 = XMFLOAT2( tuEnd, 1.0f );

spritePtr[2].tex0 = XMFLOAT2( tuStart, 1.0f );

spritePtr[3].tex0 = XMFLOAT2( tuStart, 1.0f );

spritePtr[4].tex0 = XMFLOAT2( tuStart, 0.0f );

spritePtr[5].tex0 = XMFLOAT2( tuEnd, 0.0f );

spritePtr += 6;

}

. . .

}

在函数 DrawString 中的最后一部分是进行顶点缓存的解映射(unmap,译者注:它的本意是中断,可是这样翻译

不妥)。对于任何已经映射的资源必须通过调用设备环境的 Unmap 函数进行解映射,该函数以需要解映射的缓存和

子资源索引作为参数。使用必须的精灵和更新的缓存来绘制我们的文本,调用 Draw 函数来显示结果。Draw 函数使

用字符串长度的 6 倍数量的顶点缓存,因为每个精灵有六个顶点,而字符串长度表示我们需要绘制的精灵数。函数

DrawString 的最后一部分见于清单 4.7。Direct3D Text Demo 的截屏见于插图 4.5。

清单 4.7:解映射和绘制我们的文本

bool D3DTextDemo::DrawString( char* message, float startX, float startY )

{

. . .

d3dContext_->Unmap( vertexBuffer_, 0 );

d3dContext_->Draw( 6 * length, 0 );

return true;

}

插图 4.5:Direct3D Text Demo 程序截屏

高级主题

在视频游戏中有许多地方使用文本。有一些文本的应用场景是本章中看到的,还有一些可以在其它商业游戏中

看到。虽然超出本章范围的对于使用文本的附加材料,也超出了本书的范围,在 www.UltimateGameProgramming.com

网站上有使用文本的更多的高级主题。如果你感兴趣,这些材料对于你扩展那些超过初学者的等级的知识是十分有

用的。这些材料包括:

实现文本框(和其它的界面元素)

在游戏中的调试控制台

游戏屏幕

在游戏中的统计显示(HUD,译者注:就是时间,帧率,血条,弹药,魔法等值的显示)

游戏中的文本框

文本框听起来像一个简单的概念,但是它们的实现还是需要一些考虑的。文本框不仅是用来输入文本的图形元

素,而且也可以在它上面显示文本。也许这比较简单,但是我们能够增加它的功能,使得当字符串大于文本框的面

积时能够滚动框中的文本。我们需要考虑一个可视化的提示来表明文本框已经准备好接受输入,例如类似于其它文

本编辑器中(包括微软的 Word)的闪烁的垂直线。

游戏中的控制台窗口

游戏中的控制台是另一个很难实现的概念。首先,控制台窗口本身需要存储多行文本,用于接收提交的命令和

系统文本。这通过一个在全屏贴图背景(或者甚至没有背景,直接显示在游戏屏幕上面)的上面显示多行文本来完成。

文本自身最有可能存储在一个数组里,并且该数组存储着从旧到新的最近的文本。

游戏控制台的第二部分是提供一个用于玩家输入的文本框,这已经在之前的小节中描述过——游戏中的文本框。

结合字符数组的显示和文本框,我们就可以创建一个很好的游戏中的控制台显示。但是等等,还有一些事情要做,

对于任何输入到游戏控制台中的文本命令,游戏/游戏引擎必须处理这些命令,执行它们的请求。这可以通过著名

的命令脚本概念来完成,在 www.UltimateGameProgramming 网站中有相关的主题。通过嵌入一个命令脚本处理器,

输入控制台中的命令就能够在游戏中执行。

游戏菜单

游戏的菜单系统远比一个没有任何经验的初学者所假想的那样的实现复杂。游戏的菜单系统是游戏界面的一个

很重要的部分。在主菜单出现之前,欢迎屏幕上通常有开发者和出版者的文字或视频,而主菜单本身由游戏开始菜

单,游戏载入菜单,选项,高分榜等等组成。这些菜单能够以一种非常复杂的方式出现在商业游戏之中,例如插图

4.6 中的 UDK。

插图 4.6:在 Unreal Development Kit 中的游戏菜单界面

其它的界面包括,游戏界面(响应实际的游戏操作),暂停菜单界面,多玩家房间界面,控制安装界面,等等。

下次你玩一款商业游戏时,注意有多少种经常出现的不同的窗口和界面,通常它们的数量是很多的。(注意:微软已

经发布了一个游戏界面 Demo 和在 XNA Game Studio Express 套件上开发的源码,其地址是 http://creators.xna.com)

Heads-Up Display(HUD,游戏统计元素)

Heads-Up Display 是一种特别的显示在玩家界面之上的界面,这里的玩家界面是实际的游戏界面。而 HUD,是

出现在最顶部的图形元素,它们包括时间统计,弹药数量,血条,敌人雷达,等等。一个 HUD 的例子见于插图 4.7。

插图 4.7:Heads-Up Display

章末总结

知道怎样在屏幕上绘制文本是一个很有用的技能。现在,你应该能够很容易使用 Direct3D 字体对象,实现字体

渲染,或者能够扩展你的字体渲染系统。通过操纵文本显示位置,文本能够用于实时调试信息或者作为用户接口的

一部分。

通过使用你已经学到的概念,你能够创建你自己的 HUD 和游戏菜单,一旦你已经熟练这本书之后,你能够探

索文本的更高级的主题。

章节习题

下面问题的答案见于附录 A。

1.贴图字体的别名是什么? A Sprite B Textured sprite C Bitmap font D 以上都不是

2.什么是字体?

A 字母的一种风格 B 文本的字符串

C 精灵的一种类型 D 以上都不是

3.True or False:静态文本的最佳显示方式是使用贴图映射外观,而不是动态创建精灵

A True B False

4.True or False:写或丢弃的映射类型必须使用 D3D11_MAP_FLAG_DO_NOT_WAIT 标识

A True B False

5.True or False:Direct3D 11 有内建的文本渲染

A True B False

动手实验

1.增加基于精灵的字体系统的 alpha 通道

2.重写 Render 函数使得能够将文本绘制为多行

3.在字体系统中增加小写字母和数字

第五章 输入检测和响应

能够与你创建的虚拟世界进行交互是任何一款游戏的关键,交互可以通过键盘,鼠标,或者任何其它的输入设

备。本章中,我们将解释用于执行输入检测的各种操作的好处,和怎样使用它们。

本章内容有:

考察输入检测的各种方法

DirectInput 怎样使你的工作更加容易

怎样检测当前安装的输入设备

怎样使用键盘,鼠标,和操纵杆

怎样使用 Xbox 360 控制器

我需要输入

每一个游戏都需要与玩家进行交互。可能通过这些选择,如键盘,鼠标,dance pad(译者注:游戏室的跳舞机),

或其它输入设备,你的游戏需要以这样一种方式来取得玩家的输入。输入设备能够用于驾驶一辆沿轨道前进的小车,

在游戏的虚拟世界移动你的角色,或其它任何你想象的事情。在 DOS 时代,游戏开发者如果想从键盘取得玩家的击

键,他们除了通过硬件中断的方式几乎没有什么其它选择。那时的标准 C 函数,比如 getchar,用于游戏就太慢了。

开发者需要一种更好的方式——进入基本输入输出系统(BIOS)。BIOS 是计算机上的最低级别的软件。

BIOS 常驻主板的 flash ROM(只读闪存),它来指示计算机怎样为操作系统初始化和准备硬件。而 BIOS 本身,程

序员可以通过汇编语言进行直接的访问。因为 BIOS 知道硬件正在工作的任何事情,所以开发者可以通过询问它来

得到某些信息。BIOS 系统重要的一点是,它一直观察键盘的动态。任何击键都将触发一个硬件中断,来通知系统键

已经被按下。因为这个过程几乎是立即的(译者注:极短时间完成),这可作为从键盘快速和高效的取得击键的一种

方式。

当进入 Windows NT 时代,上述直接从硬件访问键盘的方式被禁止了。Windows 成为了在应用程序和硬件之间

的一堵无形的墙。任何关于系统的信息,都必须通过操作系统来获得,因为操作系统禁止了应用程序直接访问硬件

的能力。Windows 有它自己的一套方式来取得用户的输入,这就是通过消息队列(message queue)来完成。一个消息

队列的例子如下:

MSG msg = {0};

while (WM_QUIT != msg.message)

{

while( PeekMessage( &msg, NULL, 0U, 0U, PM_REMOVE ) == TRUE )

{

TranslateMessage( &msg );

DispatchMessage( &msg );

}

}

消息队列记录着各种事件,比如鼠标移动,键盘输入,或来自于操作系统本身的事件。这种获取事件的方式对

于 Windows 应用程序效率足够了,但是对于游戏来说却不够快。大多数开发者此时转向使用另一个 Windows 函数,

GetAsyncKeyState,来取得他们所需的信息。该函数原型如下,它只有一个虚拟键值作为参数:

SHORT WINAPI GetAsyncKeyState( int vKey );

键盘上的任何实体键都对应着一个虚拟键,其对应列表见于 MSDN 文档。GetAsyncKeyState 函数可以快速检测

键盘上一个键或多个键,甚至鼠标的按键状态。这种收集用户输入的方法,被游戏开发者普遍采用,但是它还有一

个主要的问题——不能收集其它设备的输入,比如游戏手柄,操纵杆等。游戏开发者对于只能支持几种特定的设备

而显得不知所措,因为每个设备都有各自不同的方式来收集和广播输入数据给系统。

一种屏蔽技巧性的方法或设备的巧妙使用的快速取得用户输入的方法,是使用 DirectX SDK 提供的 DirectInput

组件,它提供了公共层来解决上述问题。

输入选项

本章我们将考察使用各种 Win32 函数比如 GetAsyncKeyState,或 DirectInput,和 XInput 来获取输入。DirectInput

允许你的游戏支持各种各样的输入设备,而不必使你必须知道各种设备的底层细节。DirectInput 支持的设备的一个

子集如下:

键盘

鼠标

游戏手柄

操纵杆

方向盘(游戏厅的赛车游戏可以看到)

DirectInput 的最后一次更新在 DirectX 8 中,并且现在微软不建议使用。但是微软的新 API XInput 只支持 Xbox 360

控制器,这意味着现在 XInput 还不支持键盘和鼠标。我们依旧可以使用 DirectInput 处理键盘和鼠标,但是可以使用

各种 Win32 函数来轻松获得那些设备的状态。对于游戏手柄和其它次时代游戏控制器,Windows OS 推荐使用 XInput。

不幸的是,XInput 对于非 Xbox 控制器的支持并不好,如果想支持那些设备,开发者只能求助于 DirectInput,因

为没有提供像处理鼠标和键盘那样的 Win32 函数来处理游戏控制器。因此如果需要支持那些已经存在的设备,而又

XInput 不支持它们,DirectInput 这时就显示出了它依旧可用的价值。

XInput 是一个相对较新的 API,它的第一次出现是在 DirectX 9 的最后子版本(9.0c),用于有效处理 Xbox360 游戏

控制器的输入检测。游戏控制器,不仅包括标准的 Xbox360 游戏手柄,而且也有下列类型:

Arcade sticks

Guitar Hero(吉他英雄游戏)/Rock Band musical instruments

Big button controllers

Dance pads

等等。(译者注:悲了个剧,次时代游戏木有玩过啊,这里有一些可以在现代游戏厅见到)

本章中,我们将考察既使用 Win32 函数也使用 DirectInput 来获得键盘和鼠标的输入状态。我们也将考察怎样通

过 XInput 来检测 Xbox360 控制器的输入状态。

键盘输入

键盘是我们假设 PC 用户至少拥有的两个设备之一。键盘和鼠标对于 PC 游戏和玩家来说,是极其重要的。这一

小节,我们将简略讨论三种获得键盘输入的操作。第一种方式是使用消息队列,第二种是由 Win32 函数比如

GetAsyncKeyState 函数来获得按键状态,第三种就是使用 DirectInput。

Win32 检测键盘输入

本书中我们开发的 Win32 应用程序,一般使用如下窗口回调函数来处理事件:

LRESULT CALLBACK WndProc( HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam )

{

PAINTSTRUCT paintStruct;

HDC hDC;

switch( message )

{

case WM_PAINT:

hDC = BeginPaint( hwnd, &paintStruct );

EndPaint( hwnd, &paintStruct );

break;

case WM_DESTROY:

PostQuitMessage( 0 );

break;

default:

return DefWindowProc( hwnd, message, wParam, lParam );

}

return 0;

}

通过窗口回调函数的方式,我们可以处理大量的事件,其一就是用于键盘行为的 WM_KEYDOWN 和 WM_KEYUP

事件。如下例子,响应 Escape 键按下来退出应用程序:

LRESULT CALLBACK WndProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)

{

PAINTSTRUCT paintStruct;

HDC hDC;

switch( message )

{

case WM_PAINT:

hDC = BeginPaint( hwnd, &paintStruct );

EndPaint( hwnd, &paintStruct );

break;

case WM_DESTROY:

PostQuitMessage( 0 );

break;

case WM_KEYDOWN:

switch(wParam)

{

case VK_ESCAPE:

PostQuitMessage(0);

break;

}

break;

default:

return DefWindowProc( hwnd, message, wParam, lParam );

}

return 0;

}

使用消息队列方式的一个主要问题就是处理太慢。该方法依赖于操作系统发送消息给应用程序,比如当一个键

被按下时。这种方式不是即时的,因为在消息之间有延迟。对于视频游戏来说,还是避免使用消息队列的方式为妙。

一个更好的选择就是当我们需要时,再来获得设备状态(例如,逐帧获取)。通过在 Update 函数中添加如下代码就很

容易完成:

void KeyboardDemo::Update( float dt )

{

if( GetAsyncKeyState( VK_ESCAPE ) )

{

PostQuitMessage( 0 );

}

}

我们可以使用 GetAsyncKeyState 函数来告诉我们是否有键按下。该函数只有唯一参数,即我们想检测的键盘的

实体键对应的虚拟键值,这种方式来检测键盘的输入比较容易。还有使用其它 Win32 函数的方式来做,比如使用

GetKeyState 和 GetKeybordState,它们的函数原型如下:

SHORT WINAPI GetKeyState( int nVirtKey );

BOOL WINAPI GetKeyboardState( PBYTE lpKeyState );

DirectInput 检测键盘输入

DirectInput 的初始化类似于其它 DirectX 组件,要求创建一个 DirectInput 对象和一个输入设备。DirectInput 对象

提供接口来访问 DirectInput 设备。通过所提供的接口,你能够创建一个设备实例,或者枚举系统中安装的设备,或

检测特定设备的状态。

一旦你创建好 DirectInput 对象之后,就必须创建 DirectInput 设备 。你所创建的 DirectInput 设备能够使你访问

指定的输入设备,比如键盘,操纵杆,或其它游戏输入设备。在创建设备之后,你需要取得它的输入访问,就可以

初始化设备,取得该设备的兼容性列表,或读取它的输入。

上述方式也许看起来十分麻烦,仅仅是为了从键盘或游戏手柄中取得几个击键输入,但是与直接访问输入设备

相比,前一种方式将会使你后面的工作变得简单。

现在,通过上述操作后,你就可以访问设备,能够从逐帧中读取它们的输入。例如,如果使用游戏手柄作为输

入设备,你就可以检测到是否玩家按下方向键或预定义的行为键,之后就可以基于这些信息进行响应。此时,你需

要清楚的了解 DirectInput 是怎样从输入设备中获得并且处理数据的过程。我们现在将带你通过分析某些代码来达到

此目的。

创建 DirectInput 对象

注意到,使用 DirectInput 的第一步就是创建 DirectInput 对象,函数 DirectInput8Create 就用于此目的,其原型

如下:

HRESULT WINAPI DirectInput8Create(

HINSTANCE hinst,

DWORD dwVersion,

REFIID riidltf,

LPVOID *ppvOut,

LPUNKNOWN punkOuter );

可以看到需要传递 5 个参数给该函数。

hInst——用于创建 DirectInput 对象的应用程序实例句柄

dwVersion——应用程序所要求的 DirectInput 的版本号,用于该参数的标准值是 DIRECTINPUT_VERSION

riidltf——接口要求标识,可以使用默认值 IID_IDirectInput8 用于该参数

ppvOut——持有创建成功后的 DirectInput 对象的指针

punkOuter——该参数一般设为 NULL

下面是一个代码片段,用来创建一个 DirectInput 对象:

LPDIRECTINPUT8 directInputObject;

HRESULT hr = DirectInput8Create( hInst, DIRECTINPUT_VERSION, IID_IDirectInput8, ( void** )&directInputObject, 0 );

if FAILED( hr )

{

return false;

}

(注意:作为一个提醒,确保调用创建 DirectX 对象后检查其返回值,这可让你知道当对象创建失败后,返回值帮助

你跟踪代码中 bug。假如调用函数 DirectInput8Create 失败,可能表明 DirectX 运行时存在问题,比如运行时没有正

确的安装就会发生该现象)。

前面的代码中首先创建两个变量:hr 和 directInputObject。第一个变量 hr,是标准的 HRESULT 句柄类型,用于

检查函数调用的返回值。第二个变量 directInputObject,用于持有创建成功的 DirectInput 对象。调用函数

DirectInput8Create 之后,马上进行存有返回值 hr 的检查,以确保函数调用成功。

创建 DirectInput 设备

现在,你拥有一个有效的 DirectInput 对象,就可以自由创建设备,可以通过使用 CreateDevice 函数完成:

HRESULT CreateDevice( REFGUID rguid, LPDIRECTINPUTDEVICE *lplpDirectInputDevice, LPUNKNOWN pUnkOuter );

函数 CreateDevice 要求三个参数,第一个参数是 GUID_SysKeyboard 或 GUID_SysMouse 来标识使用键盘或鼠标

设备,第二个参数是创建成功的 DirectInput 设备对象地址,最后一个参数用于处理 COM 接口。大部分应用程序将

最后一个参数设置为 NULL。下面的代码假设你想创建一个用于键盘的 DirectInput 设备:

LPDIRECTINPUTDEVICE8 keyboardDevice;

HRESULT hr = directInputObject ->CreateDevice( GUID_SysKeyboard, &keyboardDevice, 0 );

if FAILED(hr)

{

return false;

}

前面的代码首先创建一个 KeyboardDevice 对象,它是 LPDIRECTINPUTDEVICE8 类型的变量,用于持有创建成功

的 DirectInput 设备。通过 DirectInput 对象来调用 CreateDevice 函数,并且传入 GUID_SysKeyboard 作为第一个参数,

该参数用于指导 CreateDevice 函数创建的设备对象是基于系统键盘的。第二个参数是该函数创建的设备对象,第三

个参数处理 COM 接口,一般设置为 NULL。

CreateDevice 函数调用完成后,键盘设备将持有一个有效的 DirectInput 设备。当然必须检查该函数的返回值来

确保设备的有效性。

设置数据格式

在DirectInput设备创建成功后,你需要设置DirectInput组件将会用于读取设备的输入的数据格式。SetDataFormat

函数需要一个指向 DIDATAFORMAT 结构的指针,作为它的唯一参数:

HRESULT SetDataFormat( LPCDIDATAFORMAT lpdf )

DIDATAFORMAT 结构描述用于 DirectInput 设备的各种元素。该结构定义如下:

typedef struct DIDATAFORMAT

{

DWORD dwSize;

DWORD dwObjSize;

DWORD dwFlags;

DWORD dwDataSize;

DWORD dwNumObjs;

LPDIOBJECTDATAFORMAT rgodf;

} DIDATAFORMAT, *LPDIDATAFORMAT;

如果你想使用一个非标准的输入设备,就必须创建和使用自定义的 DIDATAFORMAT 结构。下面列举了一些用于

公共输入设备的预定义 DIDATAFORMAT 结构:

c_dfDIKeyboard

c_dfDIMouse

c_dfDIJoystick

c_dfDIJoystick2

如果你想使用的输入设备没有在上述预定义中,就必须明确地创建一个 DIDATAFORMAT 对象。大部分公共的输

入设备都不要求这样做,因为它们很少有创建用户自定义数据格式的能力。下面的代码简单的调用 SetDataFormat

函数,来设置用于键盘设备的预定义 DIDATAFORMAT 结构对象:

HRESULT hr = keyboardDevice ->SetDataFormat( &c_dfDIKeyboard );

if FAILED(hr)

{

return false;

}

设置合作级别

合作级别(cooperative level)用于告诉系统,你所创建的输入设备怎样与系统一起工作。输入设备能够被设置为

专用或者非专用的访问。专用访问,意味着只有你的应用程序能够使用该设备,不与其它正在运行的应用程序分享

该设备。在你的游戏全屏化状态下,这点是相当有用的。当一个设备,比如鼠标或键盘,被专用于游戏时,则任何

其它应用程序对于该设备的访问尝试都将失败。

如果你的游戏不介意共享设备,则这种访问模式就是非专用的。当游戏创建设备是非专用访问的,则其它正在

运行的应用程序也可以访问那些设备。当游戏以窗口模式运行在 Windows 桌面时,这种模式就相当重要了。将鼠标

作为非专用输入设备使用,将不会限制其它应用程序对鼠标的输入访问。

每个使用 DirectInput 设备的游戏,都必须设置它使用的设备的合作级别。可以通过函数 SetCooperativeLevel 来

完成,该函数定义如下:

HRESULT SetCooperativeLevel( HWND hwnd, DWORD dwFlags );

上述函数要求两个参数,第一个是要求访问输入设备的窗口句柄,第二个是你要求的访问类型标识。这些标识

有:

DISCL_BACKGROUND——应用程序能够背景访问设备,意思是当游戏窗口不是当前的激活窗口时,其它应用程

序就可以访问该输入设备。

DISCL_EXCLUSIVE——游戏要求完全并且专用控制该设备。

DISCL_FOREGROUND——游戏只要求当游戏窗口是当前激活窗口时才接受输入。如果游戏窗口失去焦点,则对

该窗口的的输入也将停止。

DISCL_NONEXCLUSIVE——输入设备不需要专用化,该设备能被其它程序所共享。

DISCL_NOWINKEY——告诉 DirectInput 禁用键盘上的 Windows 键。

(注意:每个应用程序都必须通过设置 DISCL_BACKGROUND 或 DISCL_FOREGROUND 标识来指定它是否需要前景

或背景访问设备。应用程序也要求设置是否 DISCL_EXCLUSIVE 或 DISCL_NONEXCLUSIVE 标识。而 DISCL_NOWINKEY 标

识则是可选的)。

下述代码简单的设置设备使用 non-exclusive 访问和当应用窗口有焦点时被激活。

hr = keyboardDevice->SetCooperativeLevel( hwnd, DISCL_FOREGROUND | DISCL_NONEXCLUSIVE );

if FAILED(hr)

{

return false;

}

你将注意到函数 SetCooperativeLevel 是 DirectInput 设备接口方法。对象 keyboardDevice 代表通过函数

CreateDevice 创建的 DirectInput 设备对象。传递给例子中的函数 SetCooperativeLevel 的参数包括 hwnd,它表示要求

访问输入设备的窗口句柄,而标识 DISCL_FOREGROUND 和 DISCL_NONEXCLUSIVE 指示 DirectInput 访问设备方式是你

所要求的那样。

获得输入设备(Acquiring the Device)

在你能够从指定的设备中读取输入之前的最后一步操作是“acquiring the device”。当你要求访问输入设备时,

需要通知操作系统,你已经准备好使用和读取该设备。函数 Acquire,是 DirectInput 设备的另一个方法,来执行这

个通知行为。该函数定义如下,没有输入参数,只有一个表面函数是否调用成功的返回值:

HRESULT Acquire( VOID );

下述的简单代码显示显示如何调用该函数:

HRESULT hr = keyboardDevice->Acquire( );

If( FAILED( hr ) )

{

return false;

}

你需要再次注意检查该函数的返回码,确保它调用成功。因为它是从一个输入设备中读取之前的最后一步,检

查返回码来确保设备有效并且已经准备好进行输入。

取得输入(Getting Input)

现在,我们通过 DirectInput 全部完成了一个输入设备的初始化步骤,是时候使用它了。所有设备都使用函数

GetDeviceState 进行读取输入操作,无论该输入设备是键盘,鼠标,还是游戏手柄。该函数原型如下:

HRESULT GetDeviceState( DWORD cbData, LPVOID lpvData );

函数的第一个参数是一个 DWORD 类型的值,它用来持有第二个参数的缓存字节数。第二个参数是一个缓存指

针,持有从设备读取的输入数据。提醒一下,从输入设备读取的数据格式在之前的 SetDataFormat 函数中就已经被

设置。接下来的几个部分将展示怎样通过 DirectInput 在你的应用程序中枚举可用的输入设备。

枚举输入设备(Enumerating Input Devices)

PC 平台的大部分游戏允许使用其它设备,而不仅仅是键盘或鼠标,比如游戏手柄或操纵杆。许多计算机没有默

认那些非标准的设备,那么 DirectInput 不能假设那些设备是存在的。又因为 Windows 允许同时安装多个游戏手柄

或操纵杆,DirectInput 就需要以一种方式来确定有多少个和多少种这些可用的输入设备。DirectInput 在输入设备上

获得必要信息的方法叫做枚举。

就像 Direct3D 能够枚举在系统上安装的显卡和取得它们的性能信息一样,DirectInput 也能够对输入设备做同样

的事情。使用 DirectInput 对象的某些函数,可以取得在系统中的可用的输入设备的数量以及它们的类型和功能列表。

例如,如果你的游戏要求使用一个有模拟操纵杆的游戏手柄,那么你可以枚举已经安装的设备,查看是否有满足你

要求的输入设备。

枚举输入设备的过程要求首先收集一个你需要查看的输入设备列表,其次收集这些设备的具体性能参数。

DirectInput 使用函数 EnumDevice 来收集已安装的设备列表。因为在机器上可能安装多种不同类型的设备,而你并

不对列表中的所有设备感兴趣,函数 EnumDevices 允许你指定需要枚举的设备类型。例如,如果你对键盘和鼠标不

感兴趣,那么可以指定枚举操纵杆设备,EnumDevices 函数将会避免枚举不感兴趣的设备列表。

首先我们解释怎样使用 EnumDevices 函数。该函数原型如下:

HRESULT EnumDevices( DWORD dwDevType, LPDIENUMDEVICESCALLBACK lpCallback, LPVOID pvRef, DWORD dwFlags );

可以看到该函数有四个参数:

dwDevType——该参数用于设置搜索设备时的过滤子。如上所述,你可以指示函数 EnumDevices 只搜索特

定类型的设备。

lpCallback——当搜索系统中的输入设备时,函数 EnumDevices 使用的回调函数,该参数是你定义的回调函

数地址。

pvRef——该参数用于传递数据给回调函数。这里能够使用任何 32 位的值,如果不需要发送数据给回调函

数,则该参数可以设置为 NULL。

dwFlags——最后一个参数是一个 DWORD 类型的值,用于组合一些标识让函数 EnumDevices 知道枚举范围。

参数 dwDevType 能够是下列值之一:

DI8DEVCLASS_ALL——将会使函数返回安装在系统中的所有输入设备列表。

DI8DEVCLASS_DEVICE——该参数将会导致搜索那些不会嵌套在其它类型设备中的设备,比如键盘,鼠标,或游

戏控制器。

DI8DEVCLASS_GAMECTRL——将会导致函数 EnumDevices 只搜索游戏控制器类型的设备,比如游戏手柄或操纵

杆。

DI8DEVCLASS_KEYBOARD——将搜索所有键盘设备(译者注:机器可以安装多个键盘)。

DI8DEVCLASS_POINTER——指示函数 EnumDevices 搜索所有指针类设备,比如鼠标。

参数 dwFlags 可以是下述值之一:

DIEDFL_ALLDEVICES——这是默认值,所有的系统中的设备都将被枚举。

DIEDFL_ATTACHEDONLY——只返回当前附加到系统中的设备。

DIEDFL_FORCEFEEDBACK——只返回支持力感应反馈的设备。

DIEDFL_INCLUDEALIASES——Windows 允许创建设备的别名,这些别名以输入设备的形式出现在系统中,但是它

们代表系统中的其它设备。

DIEDFL_INCLUDEHIDDEN——只返回隐藏的设备。

DIEDFL_INCLUDEPHANTOMS——占位符类型的幻象设备(译者注:原文 Phantom devices are placeholder devices)。

下列简单代码使用函数 EnumDevices 来获取当前附加在系统中的游戏控制器列表:

HRESULT hr;

hr = directInputObject->EnumDevices( DI8DEVCLASS_GAMECTRL, EnumDevicesCallback, NULL, DIEDFL_ATTACHEDONLY ) ;

If( FAILED( hr ) )

return false;

函数 EnumDevices 使用 DI8DEVCLASS_GAMECTRL 来搜索游戏控制器,而标识 DIEDFL_ATTACHEDONLY 表明只搜索

那些系统中的附加设备。第二个参数 EnumDevicesCallback 代表将要接收发现的设备的回调函数。第三个参数被设置

为 NULL,因为不需要发送附加的信息给回调函数。当每一个匹配搜索条件的设备被发现时,都要调用一次

EnumDevices 中设置好的回调函数。例如,如果你搜索系统中的游戏手柄,并且当前有 4 个手柄插入系统,则回调

函数将会总共调用 4 次。

回调函数的目的是给你的应用程序对于每一个硬件设备创建 DirectInput 设备的机会,并且允许你扫描这些设备

的性能。定义在你的代码中的回调函数必须使用固定的格式,例如 DIEnumDevicesCallback 定义:

BOOL CALLBACK DIEnumDevicesCallback( LPCDIDEVICEINSTANCE lpddi, LPVOID pvRef );

上述函数有两个参数,第一个是指向 DIDEVICEINSTANCE 类型的指针,第二个参数是通过函数 EnumDevices 传递

的参数 pvRef。结构 DIDEVICEINSTANCE 持有描述一个输入设备的细节,例如它的 GUID 和它的产品名。当给玩家选

择设备的展示时,上述结构内的信息就很有用了,因为它能够基于设备名来识别设备。该结构定义如下:

typedef struct DIDEVICEINSTANCE {

DWORD dwSize;

GUID guidInstance;

GUID guidProduct;

DWORD dwDevType;

TCHAR tszInstanceName[MAX_PATH];

TCHAR tszProductName[MAX_PATH];

GUID guidFFDriver;

WORD wUsagePage;

WORD wUsage;

} DIDEVICEINSTANCE, *LPDIDEVICEINSTANCE;

dwSize——以字节为单位的结构大小。

guidInstance——设备实例的唯一标识符。

guidProduct——该设备产品的唯一标识符,这可通过设备的制造商来建立该成员。

dwDevType——具体的设备类型。

tszInstanceName[MAX_PATH]——该设备实例的名字(例如,“Joystick 1”)。

tszProductName——产品名

guidFFDriver——该设备驱动唯一标识符

wUsagePage——用于人机交互的代码使用页

wUsage——用于人机交互的代码

函数 DIEnumDevicesCallback 函数返回一个 bool 类型的值,DirectInput 已经定义好两个返回值代替标准的 true

和 false,定义的两个值为 DIENUM_CONTINUEheDIENUM_STOP。这两个值用来控制设备枚举过程,如果你只要求搜

索一个操纵杆设备,则枚举所有安装的操纵杆过程是没有意义的,就应该在找到第一个你所需要的合适的设备后返

回 DIENUM_STOP。

通常,你想要搜集一个所有合适的设备列表来让玩家选择他想使用的设备,可以使用回调机制对每一个合适的

硬件设备创建 DirectInput 设备加入一个列表中,玩家就能够从该列表中选择他们想使用的设备。

下面的简单代码展示了回调函数找到第一个满足 EnumDevices 函数枚举标准的操纵杆设备后就返回的例子:

BOOL CALLBACK EnumDevicesCallback ( const DIDEVICEINSTANCE* pdidInstance, VOID* pContext )

{

HRESULT hr;

hr = directInputObject ->CreateDevice( pdidInstance->guidInstance, &keyboardDevice, 0 );

if( FAILED( hr ) )

{

return DIENUM_CONTINUE;

}

return DIENUM_STOP;

}

代码在回调函数中,首先尝试使用 CreateDevice 函数来获得对设备的访问。如果函数 CreateDevice 失败,则回

调函数返回 DIENUM_CONTINUE,指导输入设备的枚举过程继续。否则,如果 CreateDevice 函数调用成功,则回调

函数返回 DIENUM_STOP,终止枚举过程。

取得设备性能(Getting the Device Capabilities)

在从 EnumDevices 函数中返回一个有效的输入设备后,可以具体检查该设备的各项性能。例如,你也许需要知

道该设备是否支持力感应反馈特性。枚举设备性能的过程十分类似于枚举设备本身。为了取得一项输入设备的具体

细节,能够调用函数 EnumObjects 来完成,就像调用 EnumDevices 函数一样,该函数也使用一个回调过程进行工作:

HRESULT EnumObjects( LPDIENUMDEVICEOBJECTSCALLBACK lpCallback, LPVOID pvRef, DWORD dwFlags );

该函数要求三个参数,第一个是回调函数地址,第二个参数是传递给回调函数的数据,第三个参数是需要枚举

的设备的具体类型标识。函数 EnumObjects 中的回调函数目的是搜集输入设备的各项详细信息。回调函数通过一个

DIDEVICEOBJECTINSTANCE 结构来收集每个设备的详细信息。用于 EnumObjects 函数的 dwFlags 标识是如下值之一:

DIDFT_ABSAXIS——一根绝对轴线(译者注:不知所谓何物,原文:An absolute axis)

DIDFT_ALIAS——用于人机交互的控制标识

DIDFT_ALL——所有对象

DIDFT_AXIS——一根绝对或相对的轴线

DIDFT_BUTTON——一个 push 或 toggle 按钮

DIDFT_COLLECTION——人机接口链接集合

DIDFT_ENUMCOLLECTION(n)——属于链接集合中的一个对象

DIDFT_FFACTUATOR——包含力感应反馈机制的一个对象

DIDFT_FFEFFECTTRIGGER——用于触发力感应反馈效果的一个对象

DIDFT_NOCOLLECTION——不属于人机交互集合中的一个对象

DIDFT_NODATA——一个不产生数据的对象

DIDFT_OUTPUT——一个支持某些输出类型的对象

DIDFT_POV——一个观察点控制器对象

DIDFT_PSHBUTTON——有一个 push 按钮的对象

DIDFT_RELAXIS——一个有一根相对轴线的对象

DIDFT_TGLBUTTON——有一个 toggle 按钮的对象

DIDFT_VENDORDEFINED——一个有供应商扩展性能的对象

在 EnumObjects 中的回调函数必须与下述函数 DIEnumDeviceObjectCallback 签名相同:

BOOL CALLBACK DIEnumDeviceObjectsCallback (LPCDIDEVICEOBJECTINSTANCE lpddoi, LPVOID pvRef );

上述回调函数签名有两个参数,第一个参数是一个类型为 DIDEVICEOBJECTINSTANCE 结构的对象,它持有关于设

备的详细信息,第二个参数是通过 EnumObjects 函数传递给该回调函数的任何值。

结构 DIDEVICEOBJECTINSTANCE 包含丰富的关于设备的信息。它既可以用于设置力感应反馈的限制,也可以帮助

决定设备的具体类型和控制数量。关于该结构的全部解释能够在 DirectInput 文档中找到。

重新取得一个设备(Reacquiring a Device)

有时在游戏过程中可能会出现输入设备的丢失现象。如果你的游戏将设备的合作级别设置为非专用的,则有可

能另一个应用程序使用该设备,限制你对该设备的访问。这种情况,在你继续读取它的输入之前,需要重新尝试获

取该设备。

当访问一个丢失的设备时,函数 GetDeviceState 将会返回 DIERR_INPUTLOST 表明设备已丢失。这种情况下,你

需要在循环中调用 Acquire 函数直到恢复对该设备的使用。下述代码显示一旦设备丢失,怎样重新获得该设备的使

用:

while( 1 )

{

HRESULT hr = keyboardDevice->GetDeviceState( sizeof( buffer ), ( LPVOID )&buffer );

if( FAILED( hr ) )

{

hr = keyboardDevice->Acquire( );

while( hr == DIERR_INPUTLOST )

{

hr = mouseDevice->Acquire( );

}

}

}

(注意:大部分游戏要求多个输入设备,用于多玩家使用。通过创建多个 DirectInput 设备,就能够支持这些分离的

设备)

清除 DirectInput(Cleaning Up DirectInput)

像 Direct3D 一样,当你的应用程序结束时,必须释放定义在其中的 DirectInput 对象。在 DirectInput 对象中,还

必须额外对任何你已经获得控制权的设备执行放弃(unacquire)操作。如果你忘记对所使用的输入设备执行放弃操作,

当你的游戏结束时这些设备将一直会被系统锁定,可能不能被其它设备所使用。

函数 Unacquire 用于释放之前由 DirectInput 取得的设备,其原型如下:

HRESULT Unacquire( VOID );

该函数是 DirectInput 设备提供的一个释放接口。下列代码代码将会正确的放弃输入设备,并且释放 DirectInput

设备和对象:

if( directInputObject )

{

if( keyboardDevice )

{

keyboardDevice->Unacquire( );

keyboardDevice->Release( );

keyboardDevice = 0;

}

directInputObject->Release( );

directInputObject = 0;

}

DirectInput 键盘 Demo (DirectInput Keyboard Demo)

从键盘中读取输入相对比较简单,因为它是默认设备。键盘读取要求一块由 256 个字符数组组成的缓存,该字

符数组持有键盘上的每一个键的状态。每一次从键盘设备中读取,字符数组中能够保留一个或多个键的状态。大部

分游戏都要求在主循环中逐帧读取输入设备。

在你从键盘中读取输入之前,需要以一种简单的方式确定键盘上的哪个键被按下。下面提供宏 KEYDOWN 检查

你所查看的键是否按下来简单的返回 TRUE 或 FALSE:

#define KEYDOWN( name, key ) ( name[key] & 0x80 )

一个从键盘上读取输入的例子如下:

#define KEYDOWN( name, key ) ( name[key] & 0x80 )

char buffer[256];

while ( 1 )

{

keyboardDevice->GetDeviceState( sizeof( buffer ), ( LPVOID )&buffer );

if( KEYDOWN( buffer, DIK_LEFT ) )

{

// Do something with the left arrow

}

if( KEYDOWN( buffer, DIK_UP ) )

{

// Do something with the up arrow

}

}

正如你所看到的,游戏主循环逐帧调用函数 GetDeviceState 将键盘的当前状态填充输入缓存。宏 KEYDOWN 用

于检查指定键的状态。

目录 Chapter5/Keyboard 中有一个展示使用 DirectInput 和 GetAsyncKeyState 取得键盘输入的应用程序 Demo。函

数 GetAsyncKeyState 用于测试是否 Escape 键被按下,如果按下将导致 Demo 的退出。这样做是为了演示的目的,并

且不需要使用一行简单的代码来做。

Keyboard Demo 在第二章的 Triangle Demo 之上构建,在该 Demo 中我们将通过用户按下向上或向下的方向键来

改变三角形的显示颜色。为了完成上述演示,我们将使用一个变量来跟踪当前所选择的颜色,周期性的通过该变量

来改变颜色。头文件 KeyboardDemo.h 见于清单 5.1。

清单 5.1:KeyboardDemo.h 头文件

#include"Dx11DemoBase.h"

class KeyboardDemo : public Dx11DemoBase

{

public:

KeyboardDemo( );

virtual ~KeyboardDemo( );

bool LoadContent( );

void UnloadContent( );

void Update( float dt );

void Render( );

private:

ID3D11VertexShader* customColorVS_;

ID3D11PixelShader* customColorPS_;

ID3D11InputLayout* inputLayout_;

ID3D11Buffer* vertexBuffer_;

ID3D11Buffer* colorCB_;

int selectedColor_;

};

我们将在 HLSL 着色器代码中使用一块常量缓存来存储用于渲染表面的颜色值。常量缓存,就像所有其它 DirectX

11 缓存一样,是 ID3D11BUFFER 结构类型,并且在函数 LoadContent 中创建。在 LoadContent 函数中唯一改变的新代

码就是创建常量缓存,见于清单 5.2。

清单 5.2:增加在函数 LoadContent 尾部的新代码

bool KeyboardDemo::LoadContent( )

{

// . . .Code from the Triangle demo. . .

d3dResult = d3dDevice_->CreateBuffer( &vertexDesc, &resourceData, &vertexBuffer_ );

if( FAILED( d3dResult ) )

{

return false;

}

D3D11_BUFFER_DESC constDesc;

ZeroMemory( &constDesc, sizeof( constDesc ) );

constDesc.BindFlags = D3D11_BIND_CONSTANT_BUFFER;

constDesc.ByteWidth = sizeof( XMFLOAT4 );

constDesc.Usage = D3D11_USAGE_DEFAULT;

d3dResult = d3dDevice_->CreateBuffer( &constDesc, 0, &colorCB_ );

if( FAILED( d3dResult ) )

{

return false;

}

return true;

}

注意到在第三章中,常量缓存的创建是通过设置缓存描述子的 BindFlags 成员为 D3D11_BIND_CONSTANT_BUFFER

来完成。尽管我们能够初始化常量缓存中的数据,但是我们还是将 NULL 传递给函数 CreateBuffer,因为我们会在渲

染函数 Render 中填充它。当然在应用程序退出时,我们需要释放常量缓存持有的数据,这个过程在函数 Unload 中

见于清单 5.3。

清单 5.3:Keyboard Demo 中的 Unload 函数

void KeyboardDemo::UnloadContent( )

{

if( customColorVS_ ) customColorVS_->Release( );

if( customColorPS_ ) customColorPS_->Release( );

if( inputLayout_ ) inputLayout_->Release( );

if( vertexBuffer_ ) vertexBuffer_->Release( );

if( colorCB_ ) colorCB_->Release( );

customColorVS_ = 0;

customColorPS_ = 0;

inputLayout_ = 0;

vertexBuffer_ = 0;

colorCB_ = 0;

}

在图形渲染中,我们使用纯红,纯绿和纯蓝三种颜色来渲染。函数 Update 负责决定是否向上或向下键按下来

将成员变量 selectedColor_设置为 0(红),1(绿),或 2(蓝)。函数中也有一些条件语句来确保该变量在 0-2 之间。更新

函数 Update 见于清单 5.4。

清单 5.4:Keyboard Demo 的更新函数

void KeyboardDemo::Update( float dt )

{

keyboardDevice_->GetDeviceState( sizeof( keyboardKeys_ ), ( LPVOID )&keyboardKeys_ );

// Button press event.

if( GetAsyncKeyState( VK_ESCAPE ) )

{

PostQuitMessage( 0 );

}

// Button up event.

if(KEYDOWN( prevKeyboardKeys_, DIK_DOWN ) && !KEYDOWN( keyboardKeys_, DIK_DOWN ) )

{

selectedColor_--;

}

// Button up event.

if(KEYDOWN( prevKeyboardKeys_, DIK_UP ) && !KEYDOWN( keyboardKeys_, DIK_UP ) )

{

selectedColor_++;

}

memcpy( prevKeyboardKeys_, keyboardKeys_, sizeof( keyboardKeys_ ) );

if( selectedColor_ < 0 ) selectedColor_ = 2;

if( selectedColor_ > 2 ) selectedColor_ = 0;

}

键的状态可以有松开 up,按下 pressed,pushed,或释放 released 这四种状态(译者注:可以通过记录键的前一

次操作和当前操作来判断按键状态)。虽然这些状态看起来相似,但是并不相同。当键处于松开状态时,表明它没有

被按下。当键被按下时,表明该键一直处于按下状态,当点击键时,意思是键之前被按下一次,但是没有一直按下。

而键被释放,则说的是之前它被按下(按下一次,或一直按下),现在该键松开。

当从任何输入设备中检测对按钮的输入动作时,上述分类很重要。在游戏中,如果玩家需要一直按下键来执行

相同的操作,比如赛车游戏中的加速键,那么我们就需要知道是否键被按下。如果玩家必须每次都点击键来执行某

操作,比如 Bungie 的 Halo Reach 游戏中的 DMR 武器射击,那么当按钮一直按下时我们不会去响应其动作,而是当

键按下一次时才执行操作。

测试按钮(译者注,按钮或键可以用以下同样的评判准则)是否松开的简单一条就是“如果它没有按下,则处于

松开状态”。为了测试键是按下状态,我们需要该键的当前状态和前一状态,如果键的前一状态时按下并且当前状

态也是按下(或者说它一直被按下),则表明该键是按下状态。而测试键是否被 pushed,如果键的前一状态是松开,

当前状态是按下,则表明键被按下一次,那么就产生了一个单一按下事件。在下一帧中,如果键还是被按下,则状

态就从 pushed 转到按下状态。这意味着 pushed 只能计数一次,直到玩家松开键后再次 push 该键。最后为了测试

键是松开状态,我们只需要查看该键前一次被按下,当前状态松开即可。因为下一帧的前一状态和当前状态是一样

的,从而键的松开状态只能发生一次。

在清单 5.4 中的 Update 函数中,当用户释放一个方向键时我们来改变颜色。如果我们简单的检查键是否按下,

则颜色将会逐帧的迅速改变。类中的最后一个函数是 Render 渲染函数,在该函数中我们简单的使用颜色值来填充

常量缓存,如果 selectedColor_为 0,1,2 则用红,绿,蓝来填充。渲染函数 Render 见于清单 5.5。常量缓存内容的更

新通过调用 Direct3D 设备环境的 UpdateSubresource 函数来完成。

清单 5.5:渲染函数 Render

void KeyboardDemo::Render( )

{

if( d3dContext_ == 0 )

return;

float clearColor[4] = { 0.0f, 0.0f, 0.25f, 1.0f };

d3dContext_->ClearRenderTargetView( backBufferTarget_, clearColor );

unsigned int stride = sizeof( VertexPos );

unsigned int offset = 0;

d3dContext_->IASetInputLayout( inputLayout_ );

d3dContext_->IASetVertexBuffers( 0, 1, &vertexBuffer_, &stride, &offset );

d3dContext_->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST);

XMFLOAT4 redColor( 1.0f, 0.0f, 0.0f, 1.0f );

XMFLOAT4 greenColor( 0.0f, 1.0f, 0.0f, 1.0f );

XMFLOAT4 blueColor( 0.0f, 0.0f, 1.0f, 1.0f );

if( selectedColor_ == 0 )

{

d3dContext_->UpdateSubresource( colorCB_, 0, 0, &redColor, 0, 0 );

}

else if( selectedColor_ == 1 )

{

d3dContext_->UpdateSubresource( colorCB_, 0, 0, &greenColor, 0, 0 );

}

else

{

d3dContext_->UpdateSubresource( colorCB_, 0, 0, &blueColor, 0, 0 );

}

d3dContext_->VSSetShader( customColorVS_, 0, 0 );

d3dContext_->PSSetShader( customColorPS_, 0, 0 );

d3dContext_->PSSetConstantBuffers( 0, 1, &colorCB_ );

d3dContext_->Draw( 3, 0 );

swapChain_->Present( 0, 0 );

}

该Demo的着色器在文件CustomColor.hlsl中。常量缓存在HLSL中通过关键字 cbuffer被定义为一个特殊的结构,

并且通过输入 b0 来注册(回忆一下通过 t0 来注册纹理,s0 来注册着色器资源视图)。常量缓存中的所有变量和对象

都可以被使用,如果它们是普通的全局变量。在 Keyboard Demo 的着色器中,我们创建一个在常量缓存中 col 名字

的变量,它持有缓存中的颜色值。在像素着色器中,我们简单的返回该变量,其值是我们在程序中设置的用于渲染

的自定义颜色。着色器代码见于清单 5.6。

清单 5.6:Keyboard Demo 中的 CustomColor.hlsl 文件

cbuffer cbChangesPerFrame : register( b0 )

{

float4 col;

};

float4 VS_Main( float4 pos : POSITION ) : SV_POSITION

{

return pos;

}

float4 PS_Main( float4 pos : SV_POSITION ) : SV_TARGET

{

return col;

}

在代码中的最后一部分新的代码是设置和释放 DirectInput,在 DX11DemoBase 头文件和源文件中完成。在头文

件中,我们创建用于 DirectInput 和 DirectInput 键盘的对象,见于清单 5.7,其中也创建了两个 256 字节的数组它们

用于记录键盘的当前状态和前一状态。

清单 5.7:更新 DX11DemoBase.h 头文件

#define KEYDOWN(name, key) ( name[key] & 0x80 )

class Dx11DemoBase

{

public:

Dx11DemoBase();

virtual ~Dx11DemoBase();

bool Initialize( HINSTANCE hInstance, HWND hwnd );

void Shutdown( );

bool CompileD3DShader( char* filePath, char* entry, char* shaderModel, ID3DBlob** buffer );

virtual bool LoadContent( );

virtual void UnloadContent( );

virtual void Update( float dt ) = 0;

virtual void Render( ) = 0;

protected:

HINSTANCE hInstance_;

HWND hwnd_;

D3D_DRIVER_TYPE driverType_;

D3D_FEATURE_LEVEL featureLevel_;

ID3D11Device* d3dDevice_;

ID3D11DeviceContext* d3dContext_;

IDXGISwapChain* swapChain_;

ID3D11RenderTargetView* backBufferTarget_;

LPDIRECTINPUT8 directInput_;

LPDIRECTINPUTDEVICE8 keyboardDevice_;

char keyboardKeys_[256];

char prevKeyboardKeys_[256];

};

DirectInput的初始化代码在Demo基类中的初始化函数 Initialize中完成。这段代码添加在创建视口代码的后面,

见于清单 5.8。

清单 5.8:基类初始化函数 Initialize 中新增的代码

bool Dx11DemoBase::Initialize( HINSTANCE hInstance, HWND hwnd )

{

// . . . Previous code . . .

ZeroMemory( keyboardKeys_, sizeof( keyboardKeys_ ) );

ZeroMemory( prevKeyboardKeys_, sizeof( prevKeyboardKeys_ ) );

result = DirectInput8Create( hInstance_, DIRECTINPUT_VERSION, IID_IDirectInput8, ( void** )&directInput_, 0 );

if( FAILED( result ) )

{

return false;

}

result = directInput_->CreateDevice(GUID_SysKeyboard, &keyboardDevice_, 0);

if( FAILED( result ) )

{

return false;

}

result = keyboardDevice_->SetDataFormat( &c_dfDIKeyboard );

if( FAILED( result ) )

{

return false;

}

result = keyboardDevice_->SetCooperativeLevel( hwnd_, DISCL_FOREGROUND | DISCL_NONEXCLUSIVE );

if( FAILED( result ) )

{

return false;

}

result = keyboardDevice_->Acquire( );

if( FAILED( result ) )

{

return false;

}

return LoadContent( );

}

最后我们必须释放 DirectInput 和键盘设备,在基类中的 Shutdown 函数中完成此任务,见于清单 5.9。

清单 5.9:基类的 Shutdown 函数

void Dx11DemoBase::Shutdown( )

{

UnloadContent( );

if( backBufferTarget_ ) backBufferTarget_->Release( );

if( swapChain_ ) swapChain_->Release( );

if( d3dContext_ ) d3dContext_->Release( );

if( d3dDevice_ ) d3dDevice_->Release( );

if( keyboardDevice_ )

{

keyboardDevice_->Unacquire( );

keyboardDevice_->Release( );

}

if( directInput_ ) directInput_->Release( );

backBufferTarget_ = 0;

swapChain_ = 0;

d3dContext_ = 0;

d3dDevice_ = 0;

keyboardDevice_ = 0;

directInput_ = 0;

}

(译者注:程序截图如下:)

鼠标输入 (Mouse Input)

对于鼠标按钮,我们可以使用消息队列或使用鼠标按钮的虚拟键的按键状态函数来处理。但是就像之前提到的

一样,消息队列对于视频游戏来说是低效的,从而不应该选择这种方式。与像获得鼠标按钮状态一样,我们可以使

用 Win32 函数 GetCursorPos 来获得鼠标的位置,该函数原型如下:

BOOL WINAPI GetCursorPos( LPPOINT lpPoint );

本小节,我们主要关注于使用 DirectInput 来处理鼠标输入。

DirectInput 鼠标输入 (DirectInput Mouse Input)

使用 DirectInput 处理,从鼠标中读取输入十分类似于从键盘中读取输入。其主要的不同就是传给函数

CreateDevice 的 GUID 不同,和用于持有鼠标设备输入的数据结构 DIDATAFORMAT 不同。前面在处理键盘的例子中,

使用 GUID_SysKeyboard 作为第一个参数调用函数 CreateDevice,这里用于鼠标时使用 GUID_SysMouse 代替即可。(注

意:将鼠标设备的合作模式设置为专用,就需要保持 Windows 游标的显示,因为在专用模式下,你需要负责对鼠标

光标的绘制)。

hr = directInputObject->CreateDevice( GUID_SysMouse, &mouseDevice, 0 );

if( FAILED( hr ) )

{

return FALSE;

}

之前 Demo 中函数 SetDataFormat 使用的预定义数据格式是 c_dfDIKeyboard,现在使用鼠标输入设备,该值必须

改为 c_dfIDMouse。

hr = mouseDevice->SetDataFormat( &c_dfDIMouse );

if( FAILED( hr ) )

{

return FALSE;

}

在从鼠标读取输入之前,需要做的一点是增加 DIDATAFORMAT 缓存。正如键盘需要一个由 256 个元素组成的字

符缓存来接收输入数据一样,这里鼠标也需要一个结构为 DIMOUSESTATE 的缓存块。该结构由三个持有鼠标的 X,Y,Z

位置的变量和有持有鼠标状态的四个 BYTE 元素的数组构成。其具体结构如下:

typedef struct DIMOUSESTATE {

LONG lX;

LONG lY;

LONG lZ;

BYTE rgbButtons[4];

} DIMOUSESTATE, *LPDIMOUSESTATE;

前面讨论键盘输入时,我们使用了一个宏来帮助决定是否键盘的某个键被按下,类似的这里也使用一个宏来检

查鼠标按钮的状态。注意,结构中的 X,Y,Z 值并不是表示鼠标的当前位置,而是对于鼠标前一位置的相对位置。例如,

如果你将鼠标向左移动 5 个像素,则 X 就为-5,如果将鼠标向下移动 10 个像素,则 Y 就为 10。

当读取鼠标输入时,你必须保持一个值来跟踪鼠标的前一帧输入,这样才能够正确解释鼠标的动作。随后的代

码段显示了从鼠标设备中读取输入的代码,它同时检测鼠标的动作和按键状态。代码中,我们使用一个自定义宏

BUTTONDOWN 来测试 DIMOUSESTATE 结构,决定鼠标按钮是否按下。如果需要支持四键或多键鼠标,那么就需要使

用 DIMOUSESTATE2 的结构来接收鼠标的输入数据。

#define BUTTONDOWN(name, key) ( name.rgbButtons[key] & 0x80 )

curX = 320;

curY = 240;

while ( 1 )

{

mouseDevice->GetDeviceState( sizeof ( mouseState ), (LPVOID) &mouseState );

if( BUTTONDOWN( mouseState, 0 ) )

{

// Do something with the first mouse button

}

if( BUTTONDOWN( mouseState, 1 ) )

{

// Do something with the up arrow

}

curX += mouseState.lX;

curY += mouseState.lY;

}

可以在目录 Chapter5/Mouse 中找到 Mouse Demo,该 demo 构建在 keyboard Demo 之上,只是增加了一个鼠标

设备。代码实质上是键盘设备代码的拷贝和粘贴,只有少数类似于将用于键盘设备标识改为用于鼠标设备标识的改

变,比如用 c_dfDIMouse 代替 c_dfDIKeyboard。

清单 5.1 记录着 Mouse Demo 中的 Update 函数,鼠标状态中的 lZ 成员持有鼠标滚轮的值,如果鼠标没有滚轮,

则该值一直为 0,你可以增加或减少该值表明鼠标滚轮已经滚动,而其它的两个成员 lX 和 lY 则表示鼠标的 X,Y 位置

的改变量。

清单 5.10:来自于 Mouse Demo 中的 Update 函数

void MouseDemo::Update( float dt )

{

keyboardDevice_->GetDeviceState( sizeof( keyboardKeys_ ), ( LPVOID )&keyboardKeys_ );

mouseDevice_->GetDeviceState( sizeof ( mouseState_ ), ( LPVOID ) &mouseState_ );

// Button press event.

if( GetAsyncKeyState( VK_ESCAPE ) )

{

PostQuitMessage( 0 );

}

// Button up event.

if( KEYDOWN( prevKeyboardKeys_, DIK_DOWN ) && !KEYDOWN( keyboardKeys_, DIK_DOWN ) )

{

selectedColor_—;

}

// Button up event.

if( KEYDOWN( prevKeyboardKeys_, DIK_UP ) && !KEYDOWN( keyboardKeys_, DIK_UP ) )

{

selectedColor_++;

}

if( BUTTONDOWN( mouseState_, 0 ) && !BUTTONDOWN( prevMouseState_, 0 ) )

{

selectedColor_++;

}

if( BUTTONDOWN( mouseState_, 1 ) && !BUTTONDOWN( prevMouseState_, 1 ) )

{

selectedColor_—;

}

mousePosX_ += mouseState_.lX;

mousePosY_ += mouseState_.lY;

mouseWheel_ += mouseState_.lZ;

memcpy( prevKeyboardKeys_, keyboardKeys_, sizeof( keyboardKeys_ ) );

memcpy( &prevMouseState_, &mouseState_, sizeof( mouseState_ ) );

if( selectedColor_ < 0 ) selectedColor_ = 2;

if( selectedColor_ > 2 ) selectedColor_ = 0;

}

清单 5.11 中包含了创建键盘和鼠标设备的代码,以及鼠标设备释放的代码。正如你所看到的,设置鼠标的代码

十分类似于用于键盘的代码。

清单 5.11:鼠标的设置和释放

bool Dx11DemoBase::Initialize( HINSTANCE hInstance, HWND hwnd )

{

// . . .Previous code. . .

ZeroMemory( keyboardKeys_, sizeof( keyboardKeys_ ) );

ZeroMemory( prevKeyboardKeys_, sizeof( prevKeyboardKeys_ ) );

result = DirectInput8Create( hInstance_, DIRECTINPUT_VERSION, IID_IDirectInput8, ( void** )&directInput_, 0 );

if( FAILED( result ) )

{

return false;

}

result = directInput_->CreateDevice(GUID_SysKeyboard, &keyboardDevice_, 0);

if( FAILED( result ) )

{

return false;

}

result = keyboardDevice_->SetDataFormat( &c_dfDIKeyboard );

if( FAILED( result ) )

{

return false;

}

result = keyboardDevice_->SetCooperativeLevel( hwnd_, DISCL_FOREGROUND | DISCL_NONEXCLUSIVE );

if( FAILED( result ) )

{

return false;

}

result = keyboardDevice_->Acquire( );

if( FAILED( result ) )

{

return false;

}

mousePosX_ = mousePosY_ = mouseWheel_ = 0;

result = directInput_->CreateDevice( GUID_SysMouse, &mouseDevice_, 0 );

if( FAILED( result ) )

{

return false;

}

result = mouseDevice_->SetDataFormat( &c_dfDIMouse );

if( FAILED( result ) )

{

return false;

}

result = mouseDevice_->SetCooperativeLevel( hwnd_, DISCL_FOREGROUND | DISCL_NONEXCLUSIVE );

if( FAILED( result ) )

{

return false;

}

result = mouseDevice_->Acquire( );

if( FAILED( result ) )

{

return false;

}

return LoadContent( );

}

void Dx11DemoBase::Shutdown( )

{

UnloadContent( );

if( backBufferTarget_ ) backBufferTarget_->Release( );

if( swapChain_ ) swapChain_->Release( );

if( d3dContext_ ) d3dContext_->Release( );

if( d3dDevice_ ) d3dDevice_->Release( );

if( keyboardDevice_ )

{

keyboardDevice_->Unacquire( );

keyboardDevice_->Release( );

}

if( mouseDevice_ )

{

mouseDevice_->Unacquire( );

mouseDevice_->Release( );

}

if( directInput_ ) directInput_->Release( );

backBufferTarget_ = 0;

swapChain_ = 0;

d3dContext_ = 0;

d3dDevice_ = 0;

keyboardDevice_ = 0;

mouseDevice_ = 0;

directInput_ = 0;

}

XInput——游戏控制器 (XInput—Game Controllers)

XInput 是一个输入 API,并且是 DirectX SDK,XNA 游戏套件和 XDK(Xbox 开发套件)的组成部分。XNA 游戏套件是

一个构建在 DirectX 9 之上的游戏开发工具,用于 Windows PC (Wndows XP SP1 及以上版本)机上的视频游戏和 Xbox

360 视频游戏的开发。XNA 游戏套件可以从微软的官网下下载或从 Xbox360 游戏主机的零售中取得。

XInput 支持广泛的 Xbox 游戏控制器,它既支持 Xbox360 的耳机或听筒的声音输入也支持力感应反馈。Xbox360

游戏控制器兼容 Windows 平台,可以用于 Xbox 游戏主机和 WindowsPC 机两者的视频游戏。Xbox 通过 USB 接口来

支持有线控制器,使用无线控制器适配器来支持无线控制器,无线适配器主要来自于例如 Best Buy 这些零售商。当

多个控制设备用于 Xbox 主机时,唯一的限制是任何时刻只能使用 4 个控制设备。

首先将你使用的 Xbox360 控制器插入 PC 机中,操作系统会自动安装用于刚插入的这些设备的驱动。如果设备

驱动安装失败,你也许要从微软的网站下载这些驱动,或从这些控制器制造商手中取得设备驱动 CD。从零售商那

里买 Xbox360 控制器时,一般会自带驱动 CD。如果你已经有用于你的 Xbox 的有线控制器,你可以考虑从网上下载

这些设备的驱动。可以从网站 www.windowsgaming.com 上下载。

当然,当使用 DirectX SDK 中的 XInput 编写应用程序时,必须链接库 xinput.lib。

设置 XInput (Setting Up XInput)

DirectInput 需要一些安装代码用于完成这个简单的设置任务。XInput 设置过程中,并没有什么特殊的初始化代

码。我们可以通过调用 XInputGetState 函数来检查 Xbox360 控制器是否已经安装在机器上。该函数原型如下:

DWORD XInputGetState(DWORD dwUserIndex, XINPUT_STATE* pState);

该函数有两个参数,第一个是玩家索引,第二个是返回的存储状态的 XINPUT_STATE 对象地址。玩家索引以 0

开始,那么 0 代表第一个玩家,以此类推。控制器将会点亮 Xbox360 附近的指示灯表明该玩家已经接入。通过传入

玩家索引调用上述函数,如果控制器已经插入,则我们将收到返回值 ERROR_SUCCESS 。如果返回

ERROR_DEVICE_NOT_CONNECTED,则表明控制器没有插入插槽。任何其它返回值都表明设备接入存在问题或获取它

的状态有问题。

我们并不必须要设置 XInput,还可以选择禁用它,这可以通过调用 XInputEnable 函数完成,它用于当应用程序

丢失焦点的情况 (如果程序最小化将会丢失焦点)。该函数只有一个参数,用来表明禁用还是启用 XInput。传入 False

禁用 XInput,而传入 true 则启用它。当禁用 XInput 时,调用函数 XInputGetState 将返回中立的数据,就是所有的按

钮都是 up 状态,并且没有输入发生。禁用或启用 XInput 的函数原型如下:

void XInputEnable( BOOL enable );

控制器感应 (Controller Vibrations)

许多 Xbox360 控制器有力感应反馈机制。例如游戏手柄通过内置的左右感应器来导致设备的感应。为了开启控

制器的感应功能,我们必须通过调用函数 XInputSetState 将感应状态发生给控制器。该函数原型如下:

DWORD XInputSetState(DWORD dwUserIndex, XINPUT_VIBRATION* pVibration);

其中的第一个参数是 0-3 的玩家索引,第二个参数是类型为 XINPUT_VIBRATION 的对象,其结构如下:

typedef struct _XINPUT_VIBRATION {

WORD wLeftMotorSpeed;

WORD wRightMotorSpeed;

} XINPUT_VIBRATION, *PXINPUT_VIBRATION;

上述成员 wLeftMotorSpeed 用于设置左侧感应器的速度,wRightMotorSpeed 用于设置右侧感应器的速度。它们

的值范围 0-65535,其 0 表示速度为 0,而 65535 表示最大速度。

XInput 用于输入 (XInput for Input)

通过调用函数 XInputGetState,我可以获得控制器设备的当前状态的所有信息。该函数的最后一个参数是包含

一个结构XINPUT_GAMEPAD作为成员的XINPUT_STATE结构对象的输出地址,而其中XINPUT_GAMEPAD的结构如下:

typedef struct _XINPUT_GAMEPAD {

WORD wButtons;

BYTE bLeftTrigger;

BYTE bRightTrigger;

SHORT sThumbLX;

SHORT sThumbLY;

SHORT sThumbRX;

SHORT sThumbRY;

} XINPUT_GAMEPAD, *PXINPUT_GAMEPAD;

结构中的 wButtons 为位掩码(bitmask)值,就是可以通过使用位操作来对每一个 button 进行测试。有些标识用

于方向垫按钮,操纵杆按钮,外观按钮 (按钮 A,B,X,和 Y),shoudler 按钮 LB 和 RB,和开始和选择按钮(译者注,有

些设备自己也没玩过,不知其贴近的翻译名字)。操纵杆按钮 L3 用于左操纵杆,R3 用于右操纵杆,它们通过压下操

纵杆内部来按下。我们将在 XInput Demo 中展示怎样从外观按钮中检测其输入。

bLeftTrigger 用于左侧的 trigger 按钮(游戏垫控制器上有),而右侧 bRightTrigger 用于右侧 trigger 按钮。这些值的

范围是 0-255,其中 0 表示没有按下,而 255 表示全部按下,中间值表示 trigger 触发按下程度。

其它成员 sThumbLX,sThumbLY,sThumbRX,和 sThumbRY 用于操纵杆的位置,这些值在-32769 到 32767 之间,

例如,左侧杆的 X 轴为-32768 表示杆被移动到最左边,而+32767 表示杆移动到最右边。

该结构 XINPUT_GAMEPAD 可以提供我们所有需要的关于 Xbox360 游戏控制器的输入的消息。

控制器性能 (Controller Capabilities)

有时我们需要知道接入设备的性能。这可以通过调用函数 XInputGetCapabilities 来完成,其原型如下:

DWORD XInputGetCapabilities(

DWORD dwUserIndex,

DWORD dwFlags,

XINPUT_CAPABILITIES* pCapabilities

);

该函数有三个参数,第一个是玩家索引,第二个是设备查找标识,第三个是返回持有设备性能结构对象的地址。

对于查找标识,0 表示所有的设备类型(游戏垫,驾驶方向盘,等),或者可以指定标识 XINPUT_FLAG_GAMEPAD 来限

制只查找游戏垫。现在还只有 XINPUT_FLAG_GAMEPAD 是 XInput 组件唯一支持的标识。

设备性能信息存储在 XINPUT_CAPABILITIES 结构对象中,其具体结构如下:

typedef struct _XINPUT_CAPABILITIES {

BYTE Type;

BYTE SubType;

WORD Flags;

XINPUT_GAMEPAD Gamepad;

XINPUT_VIBRATION Vibration;

} XINPUT_CAPABILITIES, *PXINPUT_CAPABILITIES;

结构中的第一个成员是设备类型,目前只有唯一值 XINPUT_DEVTYPE_GAMEPAD。第二个成员是设备类型的子类

型,可以是 XINPUT_DEVSUBTYPE_ARCADE_STICK,XINPUT_DEVSUBTYPE_GAMEPAD,或 XINPUT_DEVSUBTYPE_WHEEL。

如果该设备不是游戏垫,游戏棒,或驾驶盘,它会被当做游戏垫来对待。Gamepad 成员将会存储游戏垫的按钮状态,

而最后一个成员 Vibration 将会存储设备的当前感应速度。

电池寿命 (Battery Life)

可以通过 XInput 来取得 Xbox 无线设备的电池信息。完成该任务的是 XInput 的函数 GetBatteryInformation,其原

型如下:

DWORD XInputGetBatteryInformation(

DWORD dwUserIndex,

BYTE devType,

XINPUT_BATTERY_INFORMATION* pBatteryInformation

);

函数中第一个参数是玩家索引,第二个参数是设备类型 (0 用于所有设备,或是 XINPUT_FLAG_GAMEPAD),最

后一个参数是存储电池信息的 XINPUT_BATTERY_INFORMATION 类型的对象地址。电池信息类型结构如下:

typedef struct _XINPUT_BATTERY_INFORMATION {

BYTE BatteryType;

BYTE BatteryLevel;

} XINPUT_BATTERY_INFORMATION, *PXINPUT_BATTERY_INFORMATION;

成员 BatteryType 可以是如下值之一:

BATTERY_TYPE_DISCONNECTED——用于未连接设备

BATTERY_TYPE_WIRED——用于插入 USB 接口的有线设备

BATTERY_TYPE_ALKALINE——用于使用碱性电池的设备

BATTERY_TYPE_NIMH——用于使用镍氢化合物电池的设备

BATTERY_TYPE_UNKNOWN——用于未知类型电池的设备

成员 BatteryLevel 可以是如下值之一:

BATTERY_LEVEL_EMPTY——该设备电池电量已经用完

BATTERY_LEVEL_LOW——该设备电池电量很低

BATTERY_LEVEL_MEDIUM——该设备电池电量中等

BATTERY_LEVEL_FULL——该设备电池满电量

按键 (Keystrokes)

XInput提供比调用 XInputGetState函数更一般的方法来获得输入,就是通过调用 XInputGetKeystroke函数来完成。

该函数用于获得接入的游戏垫设备的输入事件,其原型如下:

DWORD XInputGetKeystroke(

DWORD dwUserIndex,

DWORD dwReserved,

PXINPUT_KEYSTROKE pKeystroke

);

函数的第一个参数是玩家索引,第二参数是我们询问的设备类型(0 或 XINPUT_FLAG_GAMEPAD),第三个参数是

按键事件的输出地址,其按键事件结构如下:

typedef struct _XINPUT_KEYSTROKE {

WORD VirtualKey;

WCHAR Unicode;

WORD Flags;

BYTE UserIndex;

BYTE HidCode;

} XINPUT_KEYSTROKE, *PXINPUT_KEYSTROKE;

结构中的 VirtualKey 成员用于存储触发事件的按钮 ID,第二个成员保留并且总是 0,第三个成员是触发事件的

标识,如果按钮按下则标识 XINPUT_KEYSTROKE_KEYDOWN,如果按钮松开则是 XINPUT_KEYSTROKE_KEYUP,如果按

钮一直按下则标识 XINPUT_KEYSTROKE_REPEAT。第四个成员是产生该事件的玩家索引,最后一个成员是 HID 代码,

用于将来的设备。

耳机的声音 (Headset Sound)

许多 Xbox360 控制器都有用于耳机或听筒的接口。这些设备用于通过 Xbox LIVE 进行玩家之间的交流,该 Xbox

LIVE是微软提供的用于Xbox360主机的在线交流服务 (Windows平台上用于游戏的等级物是Windows LIVE)。在XInput

中,我们可以通过函数 XInputGetDSoundAudioDeviceGuids 使用耳机的 speaker 用于音频的输出,其麦克风用于音频

的输入。该函数原型如下:

DWORD XInputGetDSoundAudioDeviceGuids(

DWORD dwUserIndex,

GUID* pDSoundRenderGuid,

GUID* pDSoundCaptureGuid

);

函数中的第一个参数是玩家索引,第二个参数是 DirectSound 渲染 GUID,第三个参数是 DirectSound 捕获 GUID。

GUID 用于耳机或听筒设备渲染(播放)或捕获声音。耳机既有用于声音记录的麦克风也有用于录音重放的 speaker,

在所有的标准 Xbox360 耳机设备中都有这两部分。

使用耳机控制器来工作,需要有 DirectSound 方面的知识,它是 DirectX 中已经过时的音频 API。DirectX SDK 中

有关于在 XInput 中使用 DirectSound 捕获耳机音频的例子。

XInput Demo

目录 Chapter5/XInput 中的 Demo 将展示怎样检测和响应游戏控制器按钮的输入。该 Demo 十分类似于 Keyboard

和 Mouse Demo,其中具体的 XInput 代码可以在 Demo 的 Update 函数中找到。因为 XInput 组件并不需要像 DirectInput

那样显示初始化,所以关于 XInput 的所有代码都在同一个函数中。

来自于头文件 XInputDemo.h 中的 XInput Demo 类见于清单 5.12。像之前的输入 Demo 一样,我们存储设备的前

一状态和当前状态来检测按键事件。

清单 5.12:XInputDemo 类

#include"Dx11DemoBase.h"

#include<XInput.h>

class XInputDemo : public Dx11DemoBase

{

public:

XInputDemo( );

virtual ~XInputDemo( );

bool LoadContent( );

void UnloadContent( );

void Update( float dt );

void Render( );

private:

ID3D11VertexShader* customColorVS_;

ID3D11PixelShader* customColorPS_;

ID3D11InputLayout* inputLayout_;

ID3D11Buffer* vertexBuffer_;

ID3D11Buffer* colorCB_;

int selectedColor_;

XINPUT_STATE controller1State_;

XINPUT_STATE prevController1State_;

};

更新函数 Update 中首先获得当前设备状态,如果函数 XInputGetState 返回值 ERROR_SUCCESS,则表明获得设备

状态成功,则我们知道设备槽 0 表示玩家 1 有设备插入等。Demo 使用控制器的后退按钮来退出程序,使用外观按

钮来改变颜色,X 按钮改为蓝色,A 按钮改为绿色,B 按钮改为红色。Demo 中的 trigger 用于控制左右感应器的感应,

当在 triggers 上的压力越来越大时,设备将会产生感应。更新函数 Update 见于清单 5.13。

清单 5.13:XInputDemo 的 Update 更新函数

void XInputDemo::Update( float dt )

{

unsigned long result = XInputGetState( 0, &controller1State_ );

if( result == ERROR_SUCCESS ) {}

// Button press event.

if( controller1State_.Gamepad.wButtons & XINPUT_GAMEPAD_BACK )

{

PostQuitMessage( 0 );

}

// Button up event.

if( ( prevController1State_.Gamepad.wButtons & XINPUT_GAMEPAD_B ) &&

!( controller1State_.Gamepad.wButtons & XINPUT_GAMEPAD_B ) )

{

selectedColor_ = 0;

}

// Button up event.

if( ( prevController1State_.Gamepad.wButtons & XINPUT_GAMEPAD_A ) &&

!( controller1State_.Gamepad.wButtons & XINPUT_GAMEPAD_A ) )

{

selectedColor_ = 1;

}

// Button up event.

if( ( prevController1State_.Gamepad.wButtons & XINPUT_GAMEPAD_X ) &&

!( controller1State_.Gamepad.wButtons & XINPUT_GAMEPAD_X ) )

{

selectedColor_ = 2;

}

XINPUT_VIBRATION vibration;

WORD leftMotorSpeed = 0;

WORD rightMotorSpeed = 0;

float leftTriggerVal = ( float )controller1State_.Gamepad.bLeftTrigger;

float rightTriggerVal = ( float )controller1State_.Gamepad.bRightTrigger;

if( controller1State_.Gamepad.bLeftTrigger > 0 )

{

leftMotorSpeed = ( WORD )( 65535.0f * ( leftTriggerVal / 255.0f ) );

}

if( controller1State_.Gamepad.bRightTrigger > 0 )

{

rightMotorSpeed = ( WORD )( 65535.0f * ( rightTriggerVal / 255.0f ) );

}

vibration.wLeftMotorSpeed = leftMotorSpeed;

vibration.wRightMotorSpeed = rightMotorSpeed;

XInputSetState( 0, &vibration );

memcpy( &prevController1State_, &controller1State_, sizeof(XINPUT_STATE) );

}

章末总结 (Summary)

输入是任何游戏的一个组成部分,并且在开发周期中应该给予高度的主意。当游戏验收时,处理输入的性能对

游戏来说可以是——成也萧何败萧何。在开发期间如果一直对输入系统投入相当的关注,将会增加玩家的体验。

本章中,你已经看到怎样在 DirectInput 中使用键盘和鼠标工作,以及使用 XInput 来支持广泛的 Xbox360 游戏控

制器。正如你做见到的那样,XInput 是一个更容易使用的 DirectX 库。快速并且简约的用户输入对于任何游戏都是

至关重要的。通过正确地应用 XInput 函数,你将会在游戏中提升用户体验。

你已所学到的 (What You Have Learned )

本章中,你已经学会怎样使用 XInput 从 Xbox360 控制器中取得输入。你应该对下述已经理解:

怎样安装和卸载用于键盘和鼠标的 DirectInput

怎样从 Xbox360 控制器中读取用户输入

为什么需要检查用户是否移除了他的控制器

怎样读取模拟的和数字的控制器

怎样和什么时候使用物理感应

章末习题 (Chapter Questions)

答案见于附录 A。

1. DirectInput 可以用于哪些输入设备?

2. 哪个函数创建 IDirectInput8 接口?

3. 从键盘上读取输入需要哪种类型的缓存?

4. 用于鼠标输入的数据格式类型是什么?

5. 哪个函数用于取得控制器的当前数据?

6. 同一时间可以最多使用多少个控制器?

7. 怎样检测控制器被移除?

8. 怎样禁用控制器?

9. 哪种结构用于收集用户输入?

10. 哪个 XInput 函数允许你设置游戏控制器的物理感应?

动手实验 (On Your Own)

1. 修改 XInput Demo 用于第三章的 GameSprite 类

2. 在第一个练习中重新构建,使得可以使用方向键在屏幕上移动图形

3. 修改 XInput Demo 使得操纵杆基于挤压大小向左触发调整红色 (没有挤压,红色分量为 0,完全挤压红色分

量 100%),向右触发调整蓝色。

第六章 3D 入门

视频游戏对数学要求相当高。任何关于图形学方面,物理方面,碰撞,音频,等等都需要各种各样的数学知识。

无论新手还是有经验的游戏开发者开发游戏,数学这个主题是不可避免的。在平面,二维环境下我们就处理过这种

数学,在第三章中我们的精灵需要水平方向或垂直方向移动,只是这里存在第三维——深度,需要处理。的确,一

些伟大的游戏不仅利用你目前所知道的技术,而且这些最新的游戏都采用 3D 制作。

本章将介绍构建 3D 世界所需要的概念和必要的数学知识。DirectX 提供了一个经过高度优化,轻便 (可用于

Windows 平台和 Xbox360 主机),并且易于使用的数学库 (译者注:即 XNA 库,该库可以单独使用)。尽管你没有通

常看到的在视频游戏中使用数学的经验,但它至少能够帮助你理解数学结构和函数的使用,以及它们所接受的参数。

本章知识概览:

什么是坐标系统,怎样使用它们

怎样在 3D 空间中定义点

向量是什么

矩阵的精彩世界

怎样定位和移动物体

XNA 数学库 (XNA Math)

XNA 不是一个首字母缩略词,XNA 是一个数学库,也是 DirectX 11 SDK 的一部分。之前的 DirectX 版本的数学库

叫做 D3DXMath。如果你链接 DirectX 10 或更早的库,则该数学库在 SDK 中一直可用,但是现在推荐学习和使用 XNA

数学。

XNA 数学不能与 XNA 游戏开发套件混为一谈。XNA 游戏套件是一个 C#框架,允许开发者在 Windows 平台,Zune

媒体设备 (最新支持版本是 XNA Game Studio3.1),WP7 移动设备和 Xbox360 主机上开发游戏。尽管在 DirectX 11 SDK

中数学库叫做 XNA 数学,但它并不是由 C#编写也不是 XNA Game Studio 的一部分。

为了使用 XNA 数学,我们必须包含 xnamath.h 头文件。XNA 数学也在 Xbox SDK 中,可在 Xbox360 上工作。

XNA 实用函数 (Utilities)

XNA 数学有一个实用函数库,用于断言,计算菲涅耳系数和检查 CPU 支持。第一个函数 XMAssert 原型如下:

VOID XMAssert(CONST CHAR* pExpression, CONST CHAR* pFileName, UNIT LineNumber);

菲涅耳系数用于各种高级的图形效果。对于初学者来说,在你将来遇到使用它的时候,它并不像函数。

XMFresnelTerm 的第一个参数是入射角的余弦值,第二个参数是入射角的材料反射指数。XNA 实用部分的最后一个

函数是 XMVerifyCPUSupport,其原型如下:

BOOL XMVerifyCPUSupport( );

在 Windows 平台上,上述函数是通过 Win32 函数 IsProcessorFeaturePresent 来实现的,该 Win32 函数来检查 XNA

数学库运行的必要特性要求。

点 (Points)

任何在一个坐标系统中的位置都可以通过点来表示。一点可以看做空间中的一个无穷小单元。当在空间中定位

一个点时,可以通过每个坐标轴的值来描述。这些值是相应坐标轴上相对于原点的偏移量。例如,2D 空间中的一

点需要两个值表示,一个 X 方向的值和一个 Y 方向的值来描述它的位置,例如<1, 3>可以描述一个点的位置。

对于增加的每条坐标轴,就相应的增加值来表示点。在 3D 空间中,使用三个值,即在 X,Y,Z 轴方向相对于原点

偏移量来描述一个点,比如<1, 2, 4>。点在游戏中可以有多种运用方式,从表示玩家的位置到一个平面上的定位。

尽管每一个点是一个无穷小单元,但是依旧可以表示游戏中的任何物体的位置。之前我们就已经用顶点的位置属性

来表示三角形的点。

向量 (Vectors)

向量被广泛用于 3D 图形学,从描述距离和方向到描述速度。不像点只有一个位置,向量既有方向又有长度(度

量),可以利用向量的这些属性来表示多边形的面的朝向,物体的方向或粒子运动的方向,甚至可以用来仅仅描述位

置。通常,向量被设计为一个箭头形状,有一个头部和尾部来显示方向和长度。向量通常有两种分类:自由向量和

固定向量。

自由向量,可以放置在坐标系统中的任何位置,所有这些位置的自由向量都表示同一个向量。固定向量,要求

(向量起点)固定在坐标系统的原点。这些固定向量的尾部(向量起点)位于原点,而头部(向量终点)可以在空间中的任

何位置。因为所有固定向量的起点都在原点,所有它们可以表示位置概念。这些位置由三个标量值构成。向量中的

元素数量相当于坐标轴的数量,例如,如果坐标系统描述 3D 空间,那么就需要三个分量值来描述空间中的一个位

置。比如,向量<3, 2, 7>相当于在 X 轴上离原点 3 个单位,在 Y 轴上 2 个单位,在 Z 轴上 7 个单位。

向量结构的成员是基本数据类型,比如浮点型,整型等等,并且对于每一个轴结构中都有一个相应的成员。例

如下面的 2D,3D,4D 向量:

struct Vector2

{

float x;

float y;

};

struct Vector3

{

float x;

float y;

float z;

};

struct Vector4

{

float x;

float y;

float z;

float w;

};

当你需要自定义这些类型时,Direct3D 结构中包含了很多各种内置分量的定义。比如在 XNA 数学中,我们可以

用 XMFLOAT2表示2D向量,XMFLOAT3表示 3D向量,和XMFLOAT4表示 4D向量等。还有一些向量类型,比如XMHALF4,

将在本章稍后的“结构和类型”部分中做为一个例子讨论。使用这些 XNA 数学库结构十分有好处,因为它们具有轻

便,高度优化,和易于使用等特点。

通常当使用向量时,你会遇到它们的不同类型,如下有个简单列表:

位置向量(Position Vector)——在坐标系统中用于描述位置属性,其尾部在原点,头部在表示的点的位置处

法线向量(Normal Vector)——垂直于平面的向量,对于决定多边形面的朝向是前向还是后向,很有帮助

单位向量(Unit Vector)——长度为 1 的向量。并不是所有的向量都要求有很大的长度值,比如当创建聚光灯

时,向量的方向才是最重要的

零向量(Zero Vector)——长度为 0 的向量(译者注,其各分量也一定为 0)

XNA 中另一个有效的向量结构是 XMVECTOR,它是一个经过优化的 4D 向量。该结构不仅可以快速映射到硬件

的寄存器中,而且具有轻便和内存对齐的特性。并且 XMVECTOR 的实现是平台独立的,因为有十分高效的函数用于

访问该结构的数据。这些函数包括访问,存储和载入函数,并且以一种轻便的方式安全的访问结构数据。

第一个介绍的函数是载入函数,名为 XMLoadFloat2。该函数有不同的版本用于所有的向量类型,比如 XMLoadInt2,

XMLoadFloat3,XMLoadByte4 等。这些函数的目的是载入相应的向量类型到结构 XMVECTOR 中。当你在代码中使用

这些函数时,这些函数十分有用,以 XMFLOAT2 为例,当想转移结构中的数据到 XMVECTOR 时,使用这些函数可以

在硬件上快速操作。

每一个载入函数都有一个我们想将其中的数据转为 XMVECTOR 中的向量数据地址。例如函数 XMLoadFloat2 以

一个 XMFLOAT2 结构对象作为参数并且返回一个 XMVECTOR 对象,其函数原型如下:

XMVECTOR XMLoadFloat2( CONST XMFLOAT2* pSource );

上述函数的功能就是将一个 XMFLOAT2 对象的数据拷贝至一个 XMVECTOR 对象,而其函数内部使用快速指令来

完成此操作。如果我们想拷贝一个 2D 向量数据到 4D 向量,就像下面操作的那样:

Vector4 dest;

Vector2 src;

dest.x = src.x;

dest.y = src.y;

dest.z = <undefined>;

dest.w = <undefined>;

其它大量的载入函数,见于 SDK 文档。除了载入函数,我们还有存储函数。所有这些存储函数形式都相同,唯

一不同的是它们的第一个参数类型。比如函数 XMStoreFloat2,该函数的第一个参数是将要接收数据的一个 XMFLOAT2

结构对象的地址,第二个参数 XMVECTOR 则是数据拷贝源,该函数原型如下:

VOID XMStoreFloat2( XMFLOAT2* pDestination, XMVECTOR V );

访问函数能够用于取回 XMVECTOR 结构的任何成员的值。这些函数包括 XMVectorGetX,XMVectorGetY,

XMVectorGetZ 和 XMVectorGetW。这些函数都只有一个 XMVECTOR 结构对象作为参数,而返回相应成员的值,这些

函数的原型如下:

FLOAT XMVectorGetX( XMVECTOR V );

FLOAT XMVectorGetY( XMVECTOR V );

FLOAT XMVectorGetZ( XMVECTOR V );

FLOAT XMVectorGetW( XMVECTOR V );

还有一些以第一个参数形式返回相应 XMVECTOR 结构的成员的指针的函数,如下:

VOID XMVectorGetXPtr( FLOAT* x, XMVECTOR V );

VOID XMVectorGetYPtr( FLOAT* y, XMVECTOR V );

VOID XMVectorGetZPtr( FLOAT* z, XMVECTOR V );

VOID XMVectorGetWPtr( FLOAT* w, XMVECTOR V );

除了这些获取相应结构成员的函数,还有一些用于设置成员的函数。例如如下的函数用于设置 XMVECTOR 结构

中的 X 成员:

XMVECTOR XMVectorSetX( XMVECTOR V, FLOAT x );

XMVECTOR XMVectorSetXPtr( XMVECTOR V, CONST FLOAT* x );

向量的运算 (Vector Arithmetic)

我们可以对向量做的大部分基本操作是算术操作。两个向量之间的加,减,乘,除运算,只是对它们的每一个

分量之间做对应的算术操作,就好像是下面对两个 2D 向量之间执行的各种操作一样:

Vector2D a, b, result;

// Adding

result.x = a.x t b.x;

result.y = a.y t b.y;

// Subtracting

result.x = a.x - b.x;

result.y = a.y - b.y;

// Multiplying

result.x = a.x * b.x;

result.y = a.y * b.y;

// Dividing

result.x = a.x / b.x;

result.y = a.y / b.y;

也可以在一个向量和一个标量(浮点类型)之间执行上述运算,或使用重载操作。尽管重载操作符方便于查看代

码,但他们缺乏优化,因为使用此种方式需要执行多个函数(一个用于相等,一个用于操作,还有向量参数的构造函

数的重载等等)和多个载入和存储操作。向量与标量相加,或使用重载操作符的例子如下:

Vector2D a, result;

float value = 100.0f;

// Adding a float

result.x = a.x + value;

result.y = a.y + value;

// Adding as an overloaded operator

result = a + value;

在XNA数学中,我们有函数XMVectorAdd,XMVectorSubtract,XMVectorDivide和XMVectorMultiply来在XMVECTOR

对象之间执行上述相应的算术操作。这些函数使用优化过的指令,其原型如下:

XMVECTOR XMVectorAdd( XMVECTOR V1, XMVECTOR V2 );

XMVECTOR XMVectorSubtract( XMVECTOR V1, XMVECTOR V2 );

XMVECTOR XMVectorDivide( XMVECTOR V1, XMVECTOR V2 );

XMVECTOR XMVectorMultiply( XMVECTOR V1, XMVECTOR V2 );

向量的距离(Distance Between Vectors)

有时需要计算两点之间的距离。这两点可以是有一个点位于原点或固定位置,或任意的两个点。例如,想象一

下你正在创建一款实时的策略游戏。每个怪物都有机会朝着一个共同的目标移动,在 AI 执行期间,需要在这些怪物

之间选择一个向目标移动,那么选择哪一个呢?那么这里计算的距离就派上用场了。通过计算出每个怪物与公共目

标之间的相对距离,AI 就能够选择一个更有优势的怪物来移动。

无论你是计算 2D 或 3D 空间中的距离,其计算本质是一样的。一个例子如下:

Vector2D x1, x2;

float xDistance = x2.x - x1.x;

float yDistance = y2.x - y1.x;

float distance = square_root( xDistance * xDistance + yDistance * yDistance );

该例子中 x2 是第二个向量的 X 轴方向值,x1 是第一个向量的 X 轴方向值。其计算结果是一个表示两点间距离

的标量值。当在 3D 空间中计算两点间的距离时,只需添加一个 z 轴方向的值即可。另一种表述方式是,两向量之

间的距离是两点(译者注:两向量所表示的点)之间的向量的点积的平方根。在随后的标题“点积”部分,我们将详

细的讨论点积概念。两点之间的向量平方根就是向量的长度,就是我们所计算的两向量(分别表示那两点)之间的距

离。

计算向量的长度(Determining the Length of a Vector)

有时需要知道向量的长度,因为向量的长度或度量可以表示游戏中物体的加速度值或速度值。当单位化向量时,

其长度也需要被使用。为了计算向量的长度,需要将该向量的每个分量自乘后相加。这就是所谓的“点积”概念。

再对点积求平方根,即得到向量的长度:

sqrt(vectorX * vectorX + vectorY * vectorY + vectorZ * vectorZ);

Direct3D 提供了一系列的函数来计算向量的长度。调用函数 XMVector2Length 可以计算 2D 向量的长度,而函数

XMVector3Length,XMVector4Length 分别可以计算 3D,4D 向量的长度。还有一些函数用来估计向量的长度或计算

其平方长度。计算平方长度的函数以 Sq 结尾,其本质是执行了长度 X 长度,而估计函数以 Est 结尾用于以牺牲精度

的方式来换取效率。所有的这些 XNA 数学函数如下:

XMVECTOR XMVector2Length( XMVECTOR V );

XMVECTOR XMVector3Length( XMVECTOR V );

XMVECTOR XMVector4Length( XMVECTOR V );

XMVECTOR XMVector2LengthEst( XMVECTOR V );

XMVECTOR XMVector3LengthEst( XMVECTOR V );

XMVECTOR XMVector4LengthEst( XMVECTOR V );

XMVECTOR XMVector2LengthSq( XMVECTOR V );

XMVECTOR XMVector3LengthSq( XMVECTOR V );

XMVECTOR XMVector4LengthSq( XMVECTOR V );

单位化向量(normalize a Vector)

单位化(标准化)向量就是保持向量的方向不变,将向量长度变为 1 的过程。当只需要向量的方向,而其长度不

重要时,这种单位向量就很有用。向量的单位化可以通过将其每个分量除以向量的长度而得到,例如:

Vector3 vec;

float length = Length( vec );

vec.X = vec.X / length;

vec.Y = vec.Y / length;

vec.Z = vec.Z / length;

对每个分量除以向量长度后的向量,其方向不变,但是长度变为 1。在 XNA 数学中,我们可以使用函数

XMVector2Normalize 来将 2D 向量单位化,而函数 XMVector2NormalizeEst 用于计算估计的单位向量(牺牲精度换效率),

还有一些用于 3D 和 4D 向量的单位化函数。这些函数原型如下:

XMVECTOR XMVector2Normalize( XMVECTOR V );

XMVECTOR XMVector3Normalize( XMVECTOR V );

XMVECTOR XMVector4Normalize( XMVECTOR V );

XMVECTOR XMVector2NormalizeEst( XMVECTOR V );

XMVECTOR XMVector3NormalizeEst( XMVECTOR V );

XMVECTOR XMVector4NormalizeEst( XMVECTOR V );

叉积(Cross Product)

向量的叉积运算,用于计算出同时垂直于两向量的另一向量。通常叉积用于计算三角形的法线,可以通过将三

角形的两边做为向量进行叉积运算来获得三角形的法线。一个边向量其本质就是三角形一点 A 到另一点 B 的边作为

向量。叉积就是所谓的向量(矢量)积。

一个计算叉积的例子如下,这里通过将两个向量的分量相互做乘来得到叉积向量:

newVectorX = (vector1Y * vector2Z) –(vector1Z * vector2Y);

newVectorY = (vector1Z * vector2X) –(vector1X * vector2Z);

newVectorZ = (vector1X * vector2Y) –(vector1Y * vector2X);

在 XNA 数学中,可以使用函数 XMVector2Cross,XMVector3Cross,和 XMVector4Cross 用于计算 2D,3D,4D 向

量的叉积,其原型如下:

XMVECTOR XMVector2Cross( XMVECTOR V1, XMVECTOR V2 );

XMVECTOR XMVector3Cross( XMVECTOR V1, XMVECTOR V2 );

XMVECTOR XMVector4Cross( XMVECTOR V1, XMVECTOR V2 );

点积(Dot Product)

最后介绍的向量运算是点积运算。点积也叫做标量积,用于计算两向量之间的角度,还有在计算机图形学中的

其它诸多应用,比如背面剔除,光照等。

背面剔除(Back-face culling)是将模型中的不可见的那些多边形移除,来减少需要绘制的多边形数量的过程。如

果两个向量的夹角小于 90 度,那么点积结果为正值,否则为负值(垂直情况,为 0)。点积的符号用于决定多边形是

前朝向还是后朝向。如果我们使用视图向量和多边形的法线向量做点积运算,即可知道多边形的朝向,从而决定该

多边形是否需要被绘制。当我们使用 Direct3D 时,这些背面剔除操作通常由图形硬件内部处理,而不必我们手动操

作。

我们也可以将点积用于光照,在第七章的“光照”部分我们将讨论其更多的细节。我们将点积用于光照的一种

方式是确定有多少散射光线抵达物体表面。如果光源方向正对着物体则表面接收到的光线最多,如果与物体表面有

个倾角则接收部分光线,如果物体表面在光源背面则接收不到光线。通常可以在物体表面的法线向量与光照方向向

量之间做点积(这里光照向量通过光源位置和物体表面接收光线的点的位置(或接收光线的像素位置)来计算),来决定

物体表面与光照之间的关系。

点积可以通过两向量的各对应分量之积再求和来计算,其结果是一个标量值。一个例子如下:

float dotProduct = vec1.X * vec2.X + vec1.Y * vec2.Y + vec1.Z * vec2.Z;

XNA 数学有函数 XMVector2Dot,XMVector3Dot,XMVector4Dot 用于计算 2D,3D,4D 向量之间的点积。其原

型如下:

XMVECTOR XMVector2Dot( XMVECTOR V1, XMVECTOR V2 );

XMVECTOR XMVector3Dot( XMVECTOR V1, XMVECTOR V2 );

XMVECTOR XMVector4Dot( XMVECTOR V1, XMVECTOR V2 );

尽管点积结果是一个标量值,但在 XNA 数学中以一个 XMVECTOR 向量形式返回,这是因为将标量结果拷贝到

XMVECTOR 结构的各分量的原因。因为在 XNA 数学中使用优化指令集对向量进行流并行处理,所以尽管我们只使用

一个分量结果但依旧返回所有的分量。SIMD 指令每次一条指令处理四个浮点值,因此在这种情况下我们只需要访

问向量中的一个分量做为我们的标量结果。

3D 空间 (3D Space)

任何 3 维世界的基本原理都在你周围的空间中发生着。你桌子上的键盘和显示器,地板上的椅子,所有的这些

都存在于一个 3D 空间。如果你必须通过电话给对方描述这些物体的位置,你会怎么做呢?也许你会描述说你的桌

子在你的前面或说桌子靠近墙面,如果对方完全不知道你房间的信息,从你的描述中,对方会理解吗?对方很可能

不理解,因为对方缺少一个参照点。

坐标系统(Coordinate Systems)

参考点是你和其他人都知道的一个位置。例如,如果参考点选为门口,你就可以这样描述桌子,它在门的左边

距离 10 英尺的地方。当你需要构建一个 3D 世界时,参考点的选取是极为重要的。你需要能够将物体放置在参考点

的相对位置上,使得你和计算机都能够理解。当在 3D 图形学中处理时,该参考点就是坐标系统。一个坐标系统是

一系列通过空间的假想的线,用来描述其中的物体位置。坐标系统的中心叫做原点,也就是参考点的核心概念。空

间中的任何位置都可以被精确的描述为与原点的相对位置。

例如,你可以描述一个物体在原点上面 4 个单位和在原点左边 2 个单位的位置。通过将原点作为参考点,任何

定义在空间中的其他点就都可以被描述。如果你记得第三章中的精灵的工作方式,即屏幕上的任何点都可以用 X 和

Y 坐标解释。由两根相互垂直的轴——水平方向轴和垂直方向轴,组成的坐标系统中 X 和 Y 坐标决定精灵的位置。

插图 6.1 显示了一个 2D 坐标系统的例子。

插图 6.1:一个 2D 坐标系统

当处理三维空间时,需要增加第三根坐标轴——Z 坐标轴。Z 轴从观察者处延伸,给坐标系统一个深度描述。

现在我们有三个维度,宽度,高度,深度,也就是 3 坐标轴。当处理 3D 坐标系统时,你需要注意坐标系统的两个

类别:左手坐标系和右手坐标系。坐标系统的偏手性决定了坐标轴相对于观察者的方向。

左手坐标系统(Left-Handed Coordinate Systems)

左手坐标系统向左延伸 X 轴的正方向,向上延伸 Y 轴的正方向。Z 轴的正方向在左手系中是远离观察者方向。

插图 6.2 显示了一个左手坐标系统。

插图 6.2:左手坐标系统

右手坐标系统(Right-Handed Coordinate Systems)

右手坐标系统延伸 X 和 Y 轴的正方向与左手系相同,但是右手系相对于左手系翻转 X 和 Z 轴的方向(译者注:这

是错误的,图也错了,应该只翻转 Z 轴方向)。Z 轴的正向是朝向观察者,对于 X 轴向右延伸为正,插图 6.3 显示一

个右手坐标系统。

插图 6.3:右手坐标系统

变换(Transformations)

3D 模型大部分都在游戏代码之外创建。例如,如果你创建一个赛车游戏,你也许会在一个 3D 艺术包(译者注:

例如 maya 或 3D max 中创建,再导入游戏)中创建赛车模型。在创建过程中,这些模型由建模师提供,载入到坐标

系统中工作。创建模型时,它所拥有的点集并没有在游戏中定位(译者注:模型创建时使用的是外部工具的坐标系统),

那么当模型最后导入游戏时你怎样将它导向到你的游戏环境中。因为这个原因,你需要对模型进行变换,比如平移,

旋转和缩放,这可以通过几何管线来完成。几何管线是一个过程,它允许你将物体从一个坐标系统变换到另一个坐

标系统。插图 6.4 显示了变换点的过程:

插图 6.4:几何管线

模型制作时,它处于坐标原点(工具坐标系统)。这将导致模型导入游戏时,都将位于游戏场景的默认中心位置。

可是并不是所有载入的模型都需要位于原点,那么你怎样使这些载入的模型位于它们应该所在的位置呢?该问题的

答案就是通过变换。

变换所提及的操作有平移(translating or moving),旋转(rotating),和缩放(scaling)。通过这些操作,你就可以任

意变换物体。这些变换的执行是在模型处理过程的几何管线阶段来做的。

当你载入模型时,它的所有的顶点都位于自己的局部坐标系统中叫做模型空间。模型空间涉及的坐标系统就是

模型相对于局部坐标原点的部分。例如,模型创建时,模型的顶点是以局部坐标的原点为参考而创建的,一个立方

体的边长有两个单位,并且立方体中心位于局部原点,则它的所有顶点都相对于原点各坐标轴就有一个单位。如果

你想将该立方体放置到你的游戏场景中,你需要将该立方体的所有顶点从模型空间变换到游戏场景的全局系统,从

而与场景中的其它物体一起在游戏中使用。这个全局坐标系统就是世界空间,其中的所有物体的参考点都是同一个

固定的原点。将一个物体从模型空间变换到世界空间的过程叫做世界变换。

世界变换(World Transformations)

几何管线的世界变换阶段,以一个位于其自身局部坐标系统的物体为输入,将该物体变换到世界坐标系统中。

世界坐标系统是一个包含 3D 世界中的所有物体的位置和方向的坐标系统。该坐标系统有一个单一固定的原点,所

有变换到该系统的模型都是以该原点为参考。

从模型空间变换到世界空间的过程,通常发生在顶点着色器之内并且当它渲染几何图形时执行该过程。你也可

以在着色器外部变换几何图形,但是要求重新注入(绑定)顶点缓存的动态信息,这并不是一种好的实践方式(特别是

对于静态几何图形来说)。

几何管线的下一个阶段是视图变换。因为通过世界变换后,此时所有的物体都是以单一固定的世界原点为参考,

你只能从该点处来观察它们。为了允许从任意点观察场景,这些物体还必须通过视图变换。

视图变换(View Transformations)

视图变换是将坐标从世界空间变换到视图空间。视图空间所涉及的坐标系统以一个虚拟照相机的位置为参考点

(原点),换句话说,就是在游戏世界中视图变换模拟照相机观察世界空间中的物体,除了图形用户界面(例如:用于

血条,时间统计,弹药数量的屏幕元素,等)。当你选择一个点用于放置你的虚拟照相机观察时,世界空间的坐标需

要再次变换来适应照相机的观察。例如,照相机放置在原点,而此时世界空间本身需要移动到照相机的视图中。

变换时,你的照相机的角度和视图将应用于场景,之后就可以准备屏幕的显示了。

投影变换(Projection Transformations)

视图变换之后,几何管线的下一个步骤是投影变换。投影变换是管线应用(处理)深度的阶段。当你需要将靠近

照相机的物体显示得比远离的物体大一些,就需要创建深度错觉。这种投影就是透视投影,用于 3D 游戏。另一种

投影将会保持所有渲染的物体在它们的不同距离(相对于照相机)上看起来一样大。这种投影就是正投影。还有一种

投影是等角投影,用于以 2D 技术绘制显示 3D 物体,这种投影是对于 3D 显示的一种模拟而不是在图形 API 中实际

进行 3D 处理。我们通常所使用的投影就是透视投影和正投影。

最后,顶点可以在视口中测量(定位)并且投影在 2D 空间。其结果就是在显示器上显示拥有 3D 场景错觉的 2D

图像。

变换物体(Transforming an Object)

到现在为止,你已经知道了变换的概念,但是它们真正用来干什么呢?一个例子,假如你要建模一个城市,该

城市拥有很多房子,办公大楼,和任意散落的少量汽车。现在,你需要载入一个新的汽车模型并且将其添加到场景

中。当模型载入后,通常它位于场景原点并且朝向是错的。为了将它以正确的朝向放置在场景中,载入的汽车模型

需要通过适当的旋转并且将其平移到指定的世界坐标位置。

当载入的物体通过变换管线时,你需要对每个单独的顶点进行变换。在这种情况下变换汽车,是对每个组成汽

车的顶点进行各自的平移和旋转。在每个顶点都完成这些操作后,汽车就以正确的方向出现在了新的位置。这些变

换操作通过将模型中的每个顶点与同一个变换矩阵相乘来完成。

世界矩阵用于世界变换,通过结合作用在具体的游戏物体上的缩放,旋转和平移(位置)矩阵来构成世界矩阵(译

者注:每个物体的世界矩阵并不相同)。尽管我们通常使用这三个矩阵来创建世界矩阵,但是如果某些物体并不需要

其中的一两个过程,你可以不使用它们,因为一个矩阵与单位矩阵结合就像是乘以数字 1,该矩阵并不改变。

结合世界矩阵和视图矩阵将会创建一个新的矩阵,叫做世界-视图矩阵。结合世界-视图矩阵和投影矩阵将会创

建另一个矩阵叫做世界-视图-投影矩阵。该矩阵通常用于顶点着色器中,并且在本书的之前我们已经使用过。

通过变换矩阵变换一个向量,是将向量与矩阵相乘来完成。该操作我们在大多数的 Demo 的顶点着色器中完成,

那里通过使用 HLSL 关键字 mul 来执行相乘操作。在 XNA 数学中,我们用一个 2D 向量作为例子,函数

XMVector2Transform 通过一个矩阵来变换一个向量。还有一个函数版本 XMVector2TransformCoord,也是用于变换一

个向量并且将它变换为 w=1 的情形。第三类变换函数 XMVector2TransformNormal,用于通过除平移部分的其它变换

部分来变换向量。每种类型的函数都有用于 2D,3D,4D 的版本,如下所示:

XMVECTOR XMVector2Transform( XMVECTOR V, XMMATRIX M );

XMVECTOR XMVector3Transform( XMVECTOR V, XMMATRIX M );

XMVECTOR XMVector4Transform( XMVECTOR V, XMMATRIX M );

XMVECTOR XMVector2TransformCoord( XMVECTOR V, XMMATRIX M );

XMVECTOR XMVector3TransformCoord( XMVECTOR V, XMMATRIX M );

XMVECTOR XMVector4TransformCoord( XMVECTOR V, XMMATRIX M );

XMVECTOR XMVector2TransformNormal( XMVECTOR V, XMMATRIX M );

XMVECTOR XMVector3TransformNormal( XMVECTOR V, XMMATRIX M );

XMVECTOR XMVector4TransformNormal( XMVECTOR V, XMMATRIX M );

上述这些函数还有对应的流版本。以这些流函数的 2D 向量版本做为一个例子,其参数包括输出的数组数据地

址,每个输出向量之间的字节数,用于变换的向量输入数组,需变换的向量总数,用于这些向量变换的变换矩阵。

以 2D 流版本函数 XMVector2TransformStream 原型为例:

XMFLOAT4* XMVector2TransformStream(

XMFLOAT4 *pOutputStream,

UINT OutputStride,

CONST XMFLOAT2 *pInputStream,

UINT InputStride,

UINT VectorCount,

XMMATRIX M

);

函数 XMVector2Transform 的另一个变体是 XMVector2TransformNC,这里 NC 表示非 cached 内存(译者注:原文,

non-cached memory)。

矩阵(Matrices)

你也许会在数学课上遇到矩阵,好奇它们能够用于干啥。好,它可以用于这里:游戏编程。矩阵可以将物体从

一个坐标系统变换到另一个坐标系统。通过矩阵变换顶点,可以将顶点从原坐标系统变换到新坐标系统。例如,大

部分建模的物体都在它们自己的局部坐标系统,这意味着所有构成这些物体的顶点都以自己的局部坐标原点为参考

点。为了将这些点放置在公共的世界坐标系统中,需要矩阵对它们进行变换。

当开始学习 3D 图形学时,你会发现矩阵是一个十分容易将你的想法混淆的概念。学习伊始,矩阵和所涉及的

数学会让你感到十分的困难和气馁。这一部分关于矩阵的重要一点是,它们怎样工作(其内部原理)并不是重点,而

是需要着重知道它们可以用来做什么和怎样使用它们。任何人都不应指望你能立即掌握这里的所有的数学知识,幸

运的是,Direct3D 提供了优化过的函数来帮助你完成这些数学工作。如果你想知道矩阵背后的所有细节,互联网上

有大量的很好的数学资源。简单的说,一个矩阵用于表示一个系统的信息,在图形学和游戏编程这种情况下,我们

使用矩阵对象来存储变换向量所必须的信息。

数学上,矩阵(译者注:我们这用到的)是一个 4×4 个位于方格中的数字组成,以行和列的形式排列。矩阵中的

每行表示相应的坐标轴,第一,二,三行分别用于 X,Y,Z 轴。最后一行包含平移变换偏移量。如果你想在代码中

创建自定义的矩阵,可以类似于如下简单的 4×4 数组的浮点数:

float matrix [4][4] =

{

1.0f, 0.0f, 0.0f, 0.0f,

0.0f, 1.0f, 0.0f, 0.0f,

0.0f, 0.0f, 1.0f, 0.0f,

0.0f, 0.0f, 0.0f, 1.0f

};

XNA 数学中,有一些结构用于矩阵表示。比如 XMFLOAT3X3 用于 3×3 规格的矩阵,XMFLOAT4X3 用于 4×3 矩

阵,和 XMFLOAT4X4 用于 4×4 矩阵。以 XMFLOAT3X3 为例,其结构如下:

typedef struct _XMFLOAT3X3

{

union

{

struct

{

FLOAT _11;

FLOAT _12;

FLOAT _13;

FLOAT _21;

FLOAT _22;

FLOAT _23;

FLOAT _31;

FLOAT _32;

FLOAT _33;

};

FLOAT m[3][3];

};

} XMFLOAT3X3;

还有结构 XMMATRIX 表示矩阵,该结构像 XMVECTOR 一样是内存对齐结构,映射到硬件寄存器中工作。在本章

的“结构和类型”小节,我们将会讨论更多的类型。

单位矩阵(The Identity Matrix)

需要注意一个特殊矩阵,叫做单位矩阵,这是一个默认矩阵。它所包含的值对于变换来说,缩放比为 1,没有

旋转,没有平移。单位矩阵通过将矩阵的所有 16 个值置 0 并且再将主对角线置 1 来创建。单位矩阵作为进行所有

矩阵操作的一个起点来使用。一个矩阵乘以单位矩阵,所乘矩阵不变,将单位矩阵来变换物体不会发生平移,旋转

和缩放操作。如下是一个单位矩阵:

float matrix [4][4] =

{

1.0f, 0.0f, 0.0f, 0.0f,

0.0f, 1.0f, 0.0f, 0.0f,

0.0f, 0.0f, 1.0f, 0.0f,

0.0f, 0.0f, 0.0f, 1.0f

};

在 XNA 数学中创建单位矩阵很简单,只需调用函数 XMMatrixIdentity 即可。该函数没有输入参数,其输出就是

一个单位矩阵 XMMATRIX 对象。函数原型:XMMATRIX XMMatrixIdentity( );

缩放矩阵(Matrix Scaling)

变换的一种(译者注:一般三种变换,平移,旋转和缩放)就是可以将一个物体进行缩放。缩放的概念就是通过

一个适当的因子对物体进行放大或缩小操作。例如,如果有一个正方形位于原点,其宽和高都是 2 个单位,现在你

将它的尺寸放大 2 倍,则现在该正方形在每个方向都有 4 个单位长。

还记得单位矩阵的构成么?那些 1 的位置控制着三根轴方向的缩放。当顶点通过该矩阵变换时,它们的 X,Y,Z

值将基于缩放值来改变。通过将单位矩阵的 X 轴位置的 1 设置为 2,Y 轴位置的 1 设置为 3,则通过改变后的矩阵变

换的物体,将在其 X 轴方向物体尺寸放大 2 倍,Y 轴方向放大 3 倍。一个 ScaleMatrix 矩阵的例子如下,变量 scaleX,

scaleY,scaleZ 表示各轴方向上的缩放值:

float ScaleMatrix [4][4] =

{

scaleX, 0.0f, 0.0f, 0.0f,

0.0f, scaleY, 0.0f, 0.0f,

0.0f, 0.0f, scaleZ, 0.0f,

0.0f, 0.0f, 0.0f, 1.0f

};

在 XNA 数学中,可以使用函数 XMMatrixScaling 来创建缩放矩阵。函数的输入参数为 X,Y,Z 方向的缩放因子,并

且返回表示此种缩放效果的 XMMATRIX 矩阵。其函数原型如下:

XMMATRIX XMMatrixScaling( FLOAT ScaleX, FLOAT ScaleY, FLOAT ScaleZ );

平移矩阵(Matrix Translation)

移动(moving)物体的变换叫做平移(translation)。平移通过指定三根轴上的移动增量,可以将物体移动到任何地

方。例如,如果你想将物体向 X 轴的右边移动,只需要给物体一个 X 轴方向的正的移动量即可。为了完成移动操作,

你需要创建一个平移矩阵。

我们再次通过修改单位矩阵,增加其影响平移的变量来完成平移矩阵的创建。查看如下的平移矩阵,变量 moveX,

moveY,和 moveZ 显示各轴方向的移动增量。如果你想将物体向 X 轴正向移动 4 个单位,只需将 moveX 设置为 4。

float matrix [4][4] =

{

1.0f, 0.0f, 0.0f, 0.0f,

0.0f, 1.0f, 0.0f, 0.0f,

0.0f, 0.0f, 1.0f, 0.0f,

moveX, moveY, moveZ, 1.0f

};

在 XNA 数学中,我们通过调用函数 XMMatrixTranslation 来创建平移矩阵,其输入参数为 X,Y,Z 方向的平移量,

函数原型如下:

XMMATRIX XMMatrixTranslation( FLOAT OffsetX, FLOAT OffsetY, FLOAT OffsetZ );

旋转矩阵(Matrix Rotation)

最后我们描述的变换操作是旋转操作。旋转行为是将物体绕某一坐标轴进行转动的行为,旋转可以改变物体在

空间中的方向。例如,如果你想旋转一个行星,可以创建一个旋转矩阵,对行星进行变换。旋转矩阵较之前的变换,

需要多一点的解释,因为矩阵将会基于你所尝试围绕旋转的坐标轴不同而有所改变。

当绕任意轴旋转时,要求你将需要旋转的角度转化为其正弦和余弦值来提供。将这些值放入矩阵,将会使得旋

转适当的角度。当绕 X 轴旋转时,其角度的正弦和余弦值放入矩阵的位置如下所示:

float rotateXMatrix[4][4] =

{

1.0f, 0.0f, 0.0f, 0.0f

0.0f, cosAngle, sinAngle, 0.0f,

0.0f, -sinAngle, cosAngle, 0.0f,

0.0f, 0.0f, 0.0f, 1.0f

};

绕 Y 轴旋转所要求的角度正弦和余弦值在矩阵中的位置与之前的不同。这种情况下,绕 Y 轴旋转角度的正弦和

余弦值影响 X 和 Z 轴方向的值,其旋转的矩阵如下:

float rotateYMatrix[4][4] =

{

cosAngle, 0.0f, -sinAngle, 0.0f,

0.0f, 1.0f, 0.0f, 0.0f,

sinAngle, 0.0f, cosAngle, 0.0f,

0.0f, 0.0f, 0.0f, 1.0f

};

最后一个旋转矩阵是绕 Z 轴旋转的变换矩阵。这种情况下,角度的正弦和余弦值影响 X 和 Y 轴方向,其绕 Z 轴

旋转的矩阵如下:

float rotateZMatrix[4][4] =

{

cosAngle, sinAngle, 0.0f, 0.0f,

-sinAngle, cosAngle, 0.0f, 0.0f,

0.0f, 0.0f, 1.0f, 0.0f,

0.0f, 0.0f, 0.0f, 1.0f

};

在 XNA 数学中,我们有一些操作用于创建一个旋转矩阵。我们可以绕单一的坐标轴旋转,正如我们讨论过的创

建这三个不同的旋转矩阵,可以使用下列函数来完成:

XMMATRIX XMMatrixRotationX( FLOAT Angle );

XMMATRIX XMMatrixRotationY( FLOAT Angle );

XMMATRIX XMMatrixRotationZ( FLOAT Angle );

我们也可以使用 Yaw,Pitch 和 Roll 值来调用如下的 XNA 数学函数进行旋转操作:

XMMATRIX XMMatrixRotationRollPitchYaw( FLOAT Pitch, FLOAT Yaw, FLOAT Roll);

XMMATRIX XMMatrixRotationRollPitchYawFromVector( XMVECTOR Angles);

其中 Pitch 是绕 X 轴的旋转弧度(不是角度),Yaw 是绕 Y 轴旋转的弧度,Roll 是绕 Z 轴旋转的弧度。通过指定这

三个轴旋转的弧度数,我们可以调用函数 XMMatrixRotationRollPitchYaw 来构建一个联合了三个坐标轴方向的旋转矩

阵,该组合矩阵代替了分别使用XMMatrixRotationX等函数创建的各坐标轴方向的旋转矩阵再通过结合的最终矩阵。

旋转的其它操作还包括绕自定义轴和弧度所描述的旋转操作。这可以通过函数 XMMatrixRotationAxis 完成:

XMMATRIX XMMatrixRotationAxis( XMVECTOR Axis, FLOAT Angle );

还有我们可以使用函数 XMMatrixRotationQuaternion 来构建绕一个四元数旋转的矩阵:

XMMATRIX XMMatrixRotationQuaternion( XMVECTOR Quaternion );

四元数是我们在图形学编程中表示旋转的另一个数学对象。四元数是比矩阵更高级的主题,只有当你涉及到高

级角色动画技术,比如使用骨骼(bones, skeleton)的蒙皮动画,才会接触到这一概念。

矩阵的结合(Matrix Concatenation)

你需要学习的关于矩阵的最后一件事情是怎样将它们结合起来。大多数情况,你不仅需要平移物体,也需要将

其进行缩放和旋转操作。多个矩阵可以连乘,或结合,来创造一个单一的矩阵,该矩阵包含之前所有参与计算的矩

阵的能力。这意味着,可以将两矩阵结合为一个矩阵,来代替需要一个用于旋转的矩阵,和一个用于平移的矩阵。

依据平台和内部的实现,在 XNA 数学中我们使用重载乘积操作符。如下的代码展示了重载乘积的用法:

XMMATRIX finalMatrix = rotationMatrix * translationMatrix;

如上代码通过结合旋转矩阵和平移矩阵,创建了一个具有这两种变换的矩阵 finalMatrix。可以在任何物体通过

几何管线时,将该新矩阵应用于物体的变换。需要注意的一点是,注意你的矩阵的相乘顺序。使用上述结果矩阵,

物体首先会进行旋转操作,然后再平移到其指定位置。例如,先将物体绕 Y 轴旋转 90 度,再在 X 轴方向平移 4 个

单位,将导致物体在原点位置旋转并且向右移动 4 个单位。当创建一个世界矩阵时,你需要结合的这些变换矩阵的

顺序一般为 scaling×rotation×translation。

如果上述例子首先对物体进行平移操作,则该物体首先将会向原点右边平移 4 个单位,再绕 Y 轴旋转 90 度。

思考一下下面的想像将会发生什么,如果你伸展你的手臂到你的右边,然后绕你的身体旋转 90 度,那么你的手臂

最后会在哪呢?所以之前的例子调换变换矩阵的顺序后,其位置并不相同,从而当你真正将这些变换矩阵结合使用

时,你需要仔细决定你所需要的矩阵相乘顺序。

在 XNA 数学中,我们可以使用函数 XMMatrixMultiply 进行矩阵相乘操作,该函数以两个需要相乘操作的矩阵作

为输入参数,返回其相乘的结果矩阵。该函数原型如下:

XMMATRIX XMMatrixMultiply( XMMATRIX M1, XMMATRIX M2 );

矩阵结合的另一个术语叫做矩阵相乘。

Cube Demo

本章中,我们将创建第一个真正 3D demo,名为 Cube Demo,可以在目录 Chapter6/CubeDemo 中找到。我们所

提到的“第一个真正的 3D demo”,事实上之前所有的 Demo 在这一点上都创建了单调色(译者注:flat 为单调色模式,

smooth 为平滑色模式,即比如三角形三顶点颜色不同,则单调色模式下三角形内部为某一顶点处颜色,而平滑色

模式下为三顶点的过渡色)或贴图表面(例如,三角形 Demo,精灵 Demo 等)。尽管学术上来说,这些之前的 Demo

也是 3D 的,并且也没有阻止我们利用顶点的 Z 轴,但我们并没有创建一个实际的 3D 网格。在 3D demos 中的第一

个“Hello World”程序就是创建一个 3D 立方体(译者注:比喻句)。

首先我们需要通过添加一个深度/模板(depth/stencil)视图来更新之前的 DirectX 11 基类。该视图与深度和模板测

试有关。一块模板缓存主要用于渲染目标,这里你可以增加或减少通过模板测试的像素在缓存中的值。这些内容牵

扯到一个更高级的渲染主题,即模板阴影卷积(stencil shadow volumes)。

深度测试,如我们再第二章所主意的那样,用于确保物体表面以正确的顺序来绘制和显示,而不需要进行多边

形排序。例如,如果我们有两个三角形,它们有相同的 X 和 Y 坐标,但是在 Z 轴上相差 100 个单位,那么这两个三

角形并没有接触或重复,如果我们先绘制较近的三角形再绘制较远的那个,则较远的那个三角形的颜色数据将会覆

盖较近靠近照相机的那个三角形的颜色数据。这一般不是我们想要的结果,如果我们想正确绘制这些三角形,即较

近的三角形比较远的三角形显示得更靠前,则我们必须花费一些性能开销进行这些多边形排序(译者注:依深度排序)

以使得我们可以按正确的顺序渲染。

但是排序多边形并不是一种低开销的操作,一个场景中通常有成千上万的多边形,并且需要在每一帧都渲染。

深度缓存是一个相对简单的概念,用来消除在输出这些多边形到图形硬件之前的预排序操作。每次一个表面需要渲

染时,它的顶点都需要通过模型-视图投影矩阵进行变换。表面的每一个顶点的深度以及位于表面的像素的内插值深

度,都通过硬件进行计算。深度缓存存储着每一个已经被渲染的像素的深度,通过这些深度来决定(通过硬件完成)

当前正在渲染的像素是否比之前已经渲染的像素更靠近观察者。如果当前正在渲染的像素深度比之前已经渲染的像

素深度更靠近,则该位置的颜色缓存将被更新显示。换句话说,最后渲染到屏幕上的颜色缓存,深度缓存,和其他

附加的目标对象都是由这些通过深度测试的像素来更新。如果像素没有通过深度测试,则只需简单的丢弃即可。(注

意:早期的 3D 游戏,像 Id Software 公司的 Wolfenstein 3D 游戏,在像素渲染到屏幕之前需要进行多边形排序,这

也是二分空间部分树(BSP-Trees)数据结构的一个早期用途)。

我们可以通过一个 ID3D11DepthStencilView 对象来表示深度/模板视图。正如我们已经知道的那样,Direct3D 视

图以一种简单的方式用于硬件来访问资源。这种情况下,深度缓存资源表示一个 2D 贴图,这也就意味着在我们的

基类中需要添加一个 ID3D11DepthStencilView 对象,并且还需要创建一个 ID3D11Texture2D 对象来存储深度缓存的内

容。清单 6.1 显示了更新后的 Dx11DemoBase 基类定义,这里主要是增加了两个指针分别用于持有

ID3D11DepthStencilView 对象和 ID3D11Texture2D 对象。

清单 6.1:包含深度缓存对象的更新后的 Dx11DemoBase 基类

class Dx11DemoBase

{

public:

Dx11DemoBase( );

virtual ~Dx11DemoBase( );

bool Initialize( HINSTANCE hInstance, HWND hwnd );

void Shutdown( );

bool CompileD3DShader( char* filePath, char* entry, char* shaderModel, ID3DBlob** buffer );

virtual bool LoadContent( );

virtual void UnloadContent( );

virtual void Update( float dt ) = 0;

virtual void Render( ) = 0;

protected:

HINSTANCE hInstance_;

HWND hwnd_;

D3D_DRIVER_TYPE driverType_;

D3D_FEATURE_LEVEL featureLevel_;

ID3D11Device* d3dDevice_;

ID3D11DeviceContext* d3dContext_;

IDXGISwapChain* swapChain_;

ID3D11RenderTargetView* backBufferTarget_;

ID3D11Texture2D* depthTexture_;

ID3D11DepthStencilView* depthStencilView_;

};

基类的初始化函数 Initialize 必须更新创建深度缓存。清单 6.2,我们省略本书之前的 Demo 中的重复代码,集中

注意于新添加的代码。新添加的代码包括创建 2D 贴图,创建深度/模板视图,和将视图固定到 Direct3D 组件。

我们创建的 2D 贴图实际上是简单的填充一个有深度缓存必须性质的 D3D11_TEXTURE2D_DESC 对象,来作为深

度缓存。深度缓存必须与我们渲染的宽度和高度相匹配,有一个绑定标识 D3D11_BIND_DEPTH_STENCIL,允许默认

用法,和有格式 DXGI_FORMAT_D24_UNORM_S8_UINT。此格式说明我们使用 24 位作为一个单一的部分来存储深度。

因为深度是一个单一的值,我们不需要 RGB 或 RGBA 贴图图像,只需要创建一个单一分量(R)的贴图即可。一旦我们

创建好深度贴图,就可以创建深度 /模板视图。可以通过填充一个有定义好我们视图所需的性质的

D3D11_DEPTH_STENCIL_VIEW_DESC 对象来完成,这种情况下,我们需要设置一些格式来匹配深度贴图(例子中使用

格式 DXGI_FORMAT_D24_UNORM_S8_UINT)和设置视图维度标识为 D3D11_DSV_DIMENSION_TEXTURE2D。深度/模板

目标对象可以通过调用函数 CreateDepthStencilView 来完成,该函数参数有深度贴图对象,视图描述对象,和将要

存储深度/模板视图对象的指针。清单 6.2 显示了函数 Initialize 中新添加的代码,用于创建深度贴图和创建使用深度

/模板缓存的渲染目标视图。

清单 6.2:更新后的 Initialize 函数

bool Dx11DemoBase::Initialize( HINSTANCE hInstance, HWND hwnd )

{

// ... Previous initialize code ...

D3D11_TEXTURE2D_DESC depthTexDesc;

ZeroMemory( &depthTexDesc, sizeof( depthTexDesc ) );

depthTexDesc.Width = width;

depthTexDesc.Height = height;

depthTexDesc.MipLevels = 1;

depthTexDesc.ArraySize = 1;

depthTexDesc.Format = DXGI_FORMAT_D24_UNORM_S8_UINT;

depthTexDesc.SampleDesc.Count = 1;

depthTexDesc.SampleDesc.Quality = 0;

depthTexDesc.Usage = D3D11_USAGE_DEFAULT;

depthTexDesc.BindFlags = D3D11_BIND_DEPTH_STENCIL;

depthTexDesc.CPUAccessFlags = 0;

depthTexDesc.MiscFlags = 0;

result = d3dDevice_->CreateTexture2D( &depthTexDesc, NULL, &depthTexture_ );

if( FAILED( result ) )

{

DXTRACE_MSG( "Failed to create the depth texture!" );

return false;

}

// Create the depth stencil view

D3D11_DEPTH_STENCIL_VIEW_DESC descDSV;

ZeroMemory( &descDSV, sizeof( descDSV ) );

descDSV.Format = depthTexDesc.Format;

descDSV.ViewDimension = D3D11_DSV_DIMENSION_TEXTURE2D;

descDSV.Texture2D.MipSlice = 0;

result = d3dDevice_->CreateDepthStencilView( depthTexture_, &descDSV, &depthStencilView_ );

if( FAILED( result ) )

{

DXTRACE_MSG( "Failed to create the depth stencil target view!" );

return false;

}

d3dContext_->OMSetRenderTargets( 1, &backBufferTarget_, depthStencilView_ );

D3D11_VIEWPORT viewport;

viewport.Width = static_cast<float>( width );

viewport.Height = static_cast<float>( height );

viewport.MinDepth = 0.0f;

viewport.MaxDepth = 1.0f;

viewport.TopLeftX = 0.0f;

viewport.TopLeftY = 0.0f;

d3dContext_->RSSetViewports( 1, &viewport );

return LoadContent( );

}

因为我们的深度贴图和视图对象消耗资源,所以必须在基类的 Shutdown函数中释放这些对象,代码见清单6.3。

清单 6.3:更新后的 Shutdown 函数

void Dx11DemoBase::Shutdown( )

{

UnloadContent( );

if( depthTexture_ ) depthTexture_->Release( );

if( depthStencilView_ ) depthStencilView_->Release( );

if( backBufferTarget_ ) backBufferTarget_->Release( );

if( swapChain_ ) swapChain_->Release( );

if( d3dContext_ ) d3dContext_->Release( );

if( d3dDevice_ ) d3dDevice_->Release( );

depthTexture_ = 0;

depthStencilView_ = 0;

backBufferTarget_ = 0;

swapChain_ = 0;

d3dContext_ = 0;

d3dDevice_ = 0;

}

我们的 Demo 类叫做 CubeDemo,该 Demo 类将会创建三个常量缓存,分别用于模型,视图,和投影矩阵。虽

然我们只需要这三个矩阵的结合矩阵——模型-视图投影矩阵,但我们依旧分开它们来作为一个使用常量缓存相乘技

术的例子来显示。该部分稍后,你将看到它们与 HLSL 的关系和现代用法的趋势。

在 Demo 中,我们也创建一个索引缓存。第三章中我们注意到一个索引缓存用于指定一个包含具体的三角形顶

点列表的下标来使用。当模型中多边形数量(相互邻接)很多时,用这种方法表示我们的几何对象可以节约大量的内

存。当进行 3D 渲染时,了解怎样创建索引缓存是十分必要的,那么该 Demo 中我们将覆盖这部分知识。

CubeDemo 类见于清单 6.4。它在第三章的 Texture Mapping Demo 之上构建。我们需要在类这一层存储视图和投

影矩阵,因为在该 Demo 中它们从来不会改变,从而不需要在每帧都进行重计算。在第八章中,讨论照相机时,我

们将逐帧在 Update 函数中更新视图矩阵,那里的 Camera Demo 即构建在本 Demo 之上。

清单 6.4:CubeDemo 类

#include"Dx11DemoBase.h"

#include<xnamath.h>

class CubeDemo : public Dx11DemoBase

{

public:

CubeDemo( );

virtual ~CubeDemo( );

bool LoadContent( );

void UnloadContent( );

void Update( float dt );

void Render( );

private:

ID3D11VertexShader* solidColorVS_;

ID3D11PixelShader* solidColorPS_;

ID3D11InputLayout* inputLayout_;

ID3D11Buffer* vertexBuffer_;

ID3D11Buffer* indexBuffer_;

ID3D11ShaderResourceView* colorMap_;

ID3D11SamplerState* colorMapSampler_;

ID3D11Buffer* viewCB_;

ID3D11Buffer* projCB_;

ID3D11Buffer* worldCB_;

XMMATRIX viewMatrix_;

XMMATRIX projMatrix_;

};

我们必须更新载入函数 LoadContent,载入 3D 立方体的顶点缓存来代替原来 Demo 中载入的正方形。通过立方

体的表面三角形的顶点列表,可以逐一指定每个三角形。我们将创建一个索引缓存来指定每个三角形的顶点。如果

你观察这些顶点后,在索引缓存中手工(做为一个练习)指定顶点数组下标,你就能清楚的看到所指定的每个三角形。

创建索引缓存过程与创建顶点缓存如出一辙,只不过我们所使用的绑定标识是 D3D11_BIND_INDEX_BUFFER。最

后 Demo 中的关键一点是,在 LoadContent 函数中,我们创建三个常量缓存,并且将其初始化为我们的视图和投影

矩阵。因为这里我们没有使用照相机,其视图矩阵等于单位矩阵(第 8 章中将会改变)。投影矩阵以透视投影的方式

来创建,通过调用函数 XMMatrixPerspectiveFovLH 来完成。该函数参数有照相机的视图弧度范围,照相机的纵横比,

以及远近裁剪平面。如果你想指定照相机的视图角度范围,必须将这些角度转化为弧度来传递给该函数。照相机的

纵横比就设置为宽度除以高度值即可。

更新后的 LoadContent 函数见于清单 6.5。其中的大块代码用于手工设置三角形的数据。在第八章中,我们将会

学习从文件中载入模型的方式,这些模型文件通常是由建模工具比如 3D Studio Max 导出,对于复杂和有众多细节

的模型这种方式是必须的。

清单 6.5:更新后的 LoadContent 函数

bool CubeDemo::LoadContent( )

{

// ... Previous demo’s LoadContent code ...

VertexPos vertices[] =

{

{ XMFLOAT3( -1.0f, 1.0f, -1.0f ), XMFLOAT2( 0.0f, 0.0f ) },

{ XMFLOAT3( 1.0f, 1.0f, -1.0f ), XMFLOAT2( 1.0f, 0.0f ) },

{ XMFLOAT3( 1.0f, 1.0f, 1.0f ), XMFLOAT2( 1.0f, 1.0f ) },

{ XMFLOAT3( -1.0f, 1.0f, 1.0f ), XMFLOAT2( 0.0f, 1.0f ) },

{ XMFLOAT3( -1.0f, -1.0f, -1.0f ), XMFLOAT2( 0.0f, 0.0f ) },

{ XMFLOAT3( 1.0f, -1.0f, -1.0f ), XMFLOAT2( 1.0f, 0.0f ) },

{ XMFLOAT3( 1.0f, -1.0f, 1.0f ), XMFLOAT2( 1.0f, 1.0f ) },

{ XMFLOAT3( -1.0f, -1.0f, 1.0f ), XMFLOAT2( 0.0f, 1.0f ) },

{ XMFLOAT3( -1.0f, -1.0f, 1.0f ), XMFLOAT2( 0.0f, 0.0f ) },

{ XMFLOAT3( -1.0f, -1.0f, -1.0f ), XMFLOAT2( 1.0f, 0.0f ) },

{ XMFLOAT3( -1.0f, 1.0f, -1.0f ), XMFLOAT2( 1.0f, 1.0f ) },

{ XMFLOAT3( -1.0f, 1.0f, 1.0f ), XMFLOAT2( 0.0f, 1.0f ) },

{ XMFLOAT3( 1.0f, -1.0f, 1.0f ), XMFLOAT2( 0.0f, 0.0f ) },

{ XMFLOAT3( 1.0f, -1.0f, -1.0f ), XMFLOAT2( 1.0f, 0.0f ) },

{ XMFLOAT3( 1.0f, 1.0f, -1.0f ), XMFLOAT2( 1.0f, 1.0f ) },

{ XMFLOAT3( 1.0f, 1.0f, 1.0f ), XMFLOAT2( 0.0f, 1.0f ) },

{ XMFLOAT3( -1.0f, -1.0f, -1.0f ), XMFLOAT2( 0.0f, 0.0f ) },

{ XMFLOAT3( 1.0f, -1.0f, -1.0f ), XMFLOAT2( 1.0f, 0.0f ) },

{ XMFLOAT3( 1.0f, 1.0f, -1.0f ), XMFLOAT2( 1.0f, 1.0f ) },

{ XMFLOAT3( -1.0f, 1.0f, -1.0f ), XMFLOAT2( 0.0f, 1.0f ) },

{ XMFLOAT3( -1.0f, -1.0f, 1.0f ), XMFLOAT2( 0.0f, 0.0f ) },

{ XMFLOAT3( 1.0f, -1.0f, 1.0f ), XMFLOAT2( 1.0f, 0.0f ) },

{ XMFLOAT3( 1.0f, 1.0f, 1.0f ), XMFLOAT2( 1.0f, 1.0f ) },

{ XMFLOAT3( -1.0f, 1.0f, 1.0f ), XMFLOAT2( 0.0f, 1.0f ) },

};

D3D11_BUFFER_DESC vertexDesc;

ZeroMemory( &vertexDesc, sizeof( vertexDesc ) );

vertexDesc.Usage = D3D11_USAGE_DEFAULT;

vertexDesc.BindFlags = D3D11_BIND_VERTEX_BUFFER;

vertexDesc.ByteWidth = sizeof( VertexPos ) * 24;

D3D11_SUBRESOURCE_DATA resourceData;

ZeroMemory( &resourceData, sizeof( resourceData ) );

resourceData.pSysMem = vertices;

d3dResult = d3dDevice_->CreateBuffer( &vertexDesc, &resourceData, &vertexBuffer_ );

if( FAILED( d3dResult ) )

{

DXTRACE_MSG( "Failed to create vertex buffer!" );

return false;

}

WORD indices[] =

{

3, 1, 0, 2, 1, 3,

6, 4, 5, 7, 4, 6,

11, 9, 8, 10, 9, 11,

14, 12, 13, 15, 12, 14,

19, 17, 16, 18, 17, 19,

22, 20, 21, 23, 20, 22

};

D3D11_BUFFER_DESC indexDesc;

ZeroMemory( &indexDesc, sizeof( indexDesc ) );

indexDesc.Usage = D3D11_USAGE_DEFAULT;

indexDesc.BindFlags = D3D11_BIND_INDEX_BUFFER;

indexDesc.ByteWidth = sizeof( WORD ) * 36;

indexDesc.CPUAccessFlags = 0;

resourceData.pSysMem = indices;

d3dResult = d3dDevice_->CreateBuffer( &indexDesc, &resourceData, &indexBuffer_ );

if( FAILED( d3dResult ) )

{

DXTRACE_MSG( "Failed to create index buffer!" );

return false;

}

d3dResult = D3DX11CreateShaderResourceViewFromFile( d3dDevice_, "decal.dds", 0, 0, &colorMap_, 0 );

if( FAILED( d3dResult ) )

{

DXTRACE_MSG( "Failed to load the texture image!" );

return false;

}

D3D11_SAMPLER_DESC colorMapDesc;

ZeroMemory( &colorMapDesc, sizeof( colorMapDesc ) );

colorMapDesc.AddressU = D3D11_TEXTURE_ADDRESS_WRAP;

colorMapDesc.AddressV = D3D11_TEXTURE_ADDRESS_WRAP;

colorMapDesc.AddressW = D3D11_TEXTURE_ADDRESS_WRAP;

colorMapDesc.ComparisonFunc = D3D11_COMPARISON_NEVER;

colorMapDesc.Filter = D3D11_FILTER_MIN_MAG_MIP_LINEAR;

colorMapDesc.MaxLOD = D3D11_FLOAT32_MAX;

d3dResult = d3dDevice_->CreateSamplerState( &colorMapDesc, &colorMapSampler_ );

if( FAILED( d3dResult ) )

{

DXTRACE_MSG( "Failed to create color map sampler state!" );

return false;

}

D3D11_BUFFER_DESC constDesc;

ZeroMemory( &constDesc, sizeof( constDesc ) );

constDesc.BindFlags = D3D11_BIND_CONSTANT_BUFFER;

constDesc.ByteWidth = sizeof( XMMATRIX );

constDesc.Usage = D3D11_USAGE_DEFAULT;

d3dResult = d3dDevice_->CreateBuffer( &constDesc, 0, &viewCB_ );

if( FAILED( d3dResult ) )

{

return false;

}

d3dResult = d3dDevice_->CreateBuffer( &constDesc, 0, &projCB_ );

if( FAILED( d3dResult ) )

{

return false;

}

d3dResult = d3dDevice_->CreateBuffer( &constDesc, 0, &worldCB_ );

if( FAILED( d3dResult ) )

{

return false;

}

viewMatrix_ = XMMatrixIdentity( );

projMatrix_ = XMMatrixPerspectiveFovLH( XM_PIDIV4, 800.0f / 600.0f, 0.01f, 100.0f );

viewMatrix_ = XMMatrixTranspose(viewMatrix_ );

projMatrix_ = XMMatrixTranspose( projMatrix_ );

return true;

}

记住,所有新创建的对象都必须在函数 UnloadContent 中释放,包括我们创建的三个常量缓存和索引缓存,其

释放代码见于清单 6.6。

清单 6.6:更新后的 UnloadContent 函数

void CubeDemo::UnloadContent( )

{

if( colorMapSampler_ ) colorMapSampler_->Release( );

if( colorMap_ ) colorMap_->Release( );

if( solidColorVS_ ) solidColorVS_->Release( );

if( solidColorPS_ ) solidColorPS_->Release( );

if( inputLayout_ ) inputLayout_->Release( );

if( vertexBuffer_ ) vertexBuffer_->Release( );

if( indexBuffer_ ) indexBuffer_->Release( );

if( viewCB_ ) viewCB_->Release( );

if( projCB_ ) projCB_->Release( );

if( worldCB_ ) worldCB_->Release( );

colorMapSampler_ = 0;

colorMap_ = 0;

solidColorVS_ = 0;

solidColorPS_ = 0;

inputLayout_ = 0;

vertexBuffer_ = 0;

indexBuffer_ = 0;

viewCB_ = 0;

projCB_ = 0;

worldCB_ = 0;

}

最后部分的代码是关于 HLSL 着色器和渲染函数的。渲染函数中增加了一行用于清除深度缓存的代码,是通过

调用设备环境函数 ClearDepthStencilView 来完成,该函数参数有深度/模板视图,所清除的地方(这里我们使用标识

D3D11_CLEAR_DEPTH 来清除深度),用于清除深度缓存的介于 0-1 之间的值,和用于清除模板缓存的值。

在渲染之前,我们必须设置索引缓存到输入装配中,因为我们将要使用它。这可以通过调用函数 IASetIndexBuffer

来完成,该函数参数有索引缓存,缓存数据格式,和以字节为单位的开始偏移量。我们用于创建索引缓存的格式是

一个 16 位的无符号整型,其意思是我们必须在函数的数据格式参数中使用 DXGI_FORMAT_R16_UINT。你在索引缓存

中使用的任何格式,都必须通过该函数让 Direct3D 知道如何正确的访问缓存中的数据。

最后一步是绑定我们的常量缓存,然后绘制几何体。我们将每个常量缓存绑定到 b0,b1 和 b2,它在 HLSL 着色

器文件中指定。为了绘制数据,我们不使用 Draw 函数,而是调用 DrawIndexed 函数。该函数参数有索引缓存中的

索引数量,我们绘制的索引数组的开始下标,和基本顶点的位置(在读取顶点缓存之前,每一个索引需要增加的一个

偏移量)。这种方式十分灵活,可以用于绘制部分顶点缓存数据,当你需要在一个很大的顶点缓存中,成批的绘制多

个模型就会遇到这种情况。不过这属于高级的渲染优化主题。更新后的渲染函数见于清单 6.7。

清单 6.7:更新后的 Render 函数

void CubeDemo::Render( )

{

if( d3dContext_ == 0 )

return;

float clearColor[4] = { 0.0f, 0.0f, 0.25f, 1.0f };

d3dContext_->ClearRenderTargetView( backBufferTarget_, clearColor );

d3dContext_->ClearDepthStencilView( depthStencilView_, D3D11_CLEAR_DEPTH, 1.0f, 0 );

unsigned int stride = sizeof( VertexPos );

unsigned int offset = 0;

d3dContext_->IASetInputLayout( inputLayout_ );

d3dContext_->IASetVertexBuffers( 0, 1, &vertexBuffer_, &stride, &offset );

d3dContext_->IASetIndexBuffer( indexBuffer_, DXGI_FORMAT_R16_UINT, 0 );

d3dContext_->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST);

d3dContext_->VSSetShader( solidColorVS_, 0, 0 );

d3dContext_->PSSetShader( solidColorPS_, 0, 0 );

d3dContext_->PSSetShaderResources( 0, 1, &colorMap_ );

d3dContext_->PSSetSamplers( 0, 1, &colorMapSampler_ );

XMMATRIX rotationMat = XMMatrixRotationRollPitchYaw( 0.0f, 0.7f, 0.7f );

XMMATRIX translationMat = XMMatrixTranslation( 0.0f, 0.0f, 6.0f );

XMMATRIX worldMat = rotationMat * translationMat;

worldMat = XMMatrixTranspose( worldMat );

d3dContext_->UpdateSubresource( worldCB_, 0, 0, &worldMat, 0, 0 );

d3dContext_->UpdateSubresource( viewCB_, 0, 0, &viewMatrix_, 0, 0 );

d3dContext_->UpdateSubresource( projCB_, 0, 0, &projMatrix_, 0, 0 );

d3dContext_->VSSetConstantBuffers( 0, 1, &worldCB_ );

d3dContext_->VSSetConstantBuffers( 1, 1, &viewCB_ );

d3dContext_->VSSetConstantBuffers( 2, 1, &projCB_ );

d3dContext_->DrawIndexed( 36, 0, 0 );

swapChain_->Present( 0, 0 );

}

在 HLSL 着色器代码中我们创建的三个常量缓存注册为 b0,b1 和 b2 变量。需要注意的是,这些注册的缓存必

须符合我们在渲染函数 Render 中指定的那样,才能将缓存正确的注册到 HLSL 对象中。我们可以将标识

cbChangesEveryFrame,cbNeverChanges 和 cbChangeOnResize 用于常量缓存。在 DirectX SDK 中还有很多这种通过缓

存的使用频率来对我们的常量缓存对象进行归类,这样就可以在一个特定的更新频率上同时对所有需要更新的缓存

进行更新操作。再次声明,我们也可以像之前 Demos 那样只使用一个常量缓存用于模型-视图投影矩阵。而这里所

使用的关于常量缓存的命名约定,可以允许我们在程序外部很容易看到那些对象的更新频率。

这里的 HLSL 代码本质上与 Texture Mapping Demo 的着色器代码相同,只不过这里需要对输入的顶点进行模型,

视图,投影变换。本 Cube demo 的着色器 HLSL 文件见于清单 6.8。程序运行截图见于插图 6.5。

清单 6.8:CubeDemo 的 HLSL 着色器代码

Texture2D colorMap_ : register( t0 );

SamplerState colorSampler_ : register( s0 );

cbuffer cbChangesEveryFrame : register( b0 )

{

matrix worldMatrix;

};

cbuffer cbNeverChanges : register( b1 )

{

matrix viewMatrix;

};

cbuffer cbChangeOnResize : register( b2 )

{

matrix projMatrix;

};

struct VS_Input

{

float4 pos : POSITION;

float2 tex0 : TEXCOORD0;

};

struct PS_Input

{

float4 pos : SV_POSITION;

float2 tex0 : TEXCOORD0;

};

PS_Input VS_Main( VS_Input vertex )

{

PS_Input vsOut = ( PS_Input )0;

vsOut.pos = mul( vertex.pos, worldMatrix );

vsOut.pos = mul( vsOut.pos, viewMatrix );

vsOut.pos = mul( vsOut.pos, projMatrix );

vsOut.tex0 = vertex.tex0;

return vsOut;

}

float4 PS_Main( PS_Input frag ) : SV_TARGET

{

return colorMap_.Sample( colorSampler_, frag.tex0 );

}

插图 6.5: 3D Cube Demo 程序截图

附加的 XNA 数学主题(Additional XNA Math Topics)

XNA 数学有成百的函数和数据结构。这里仅仅一章是很难覆盖到的,它后面的数学知识牵扯到多本书籍。本章

的剩下部分,我们将看看 XNA 数学的其他方面,这也许对入门级的读者怎样在 DirectX 11 中使用很有帮助。

编译指令(Compiler Directives)

XNA 数学有一个编译指令列表,用于调整 XNA 数学的编译过程和怎样在应用程序中使用。这些编译指令可以给

开发者能够指定他们的程序的不同性能,其列表如下:

_XM_NO_INTRINSICS_

_XM_SSE_INTRINSICS_

_XM_VMX128_INTRINSICS_

XM_NO_ALIGNMENT

XM_NO_MISALIGNED_VECTOR_ACCESS

XM_NO_OPERATOR_OVERLOADS

XM_STRICT_VECTOR4

指令_XM_NO_INTRINSICS_表示不使用 XNA 的内部自定义类型,则当执行 XNA 数学操作时,它只使用标准的浮点

精度。这种行为可以看做是应用程序使用没有特殊优化的 XNA 数学。

_XM_SSE_INTRINSICS_用于启用内部类型 SSE 和 SSE2,在支持该两种类型的平台上。如果该平台不支持,则指

令不起作用。SSE 和 SSE2 是 SIMD(单指令多数据)操作类型,其目的是以指令级别的并行计算(换句话说就是,同一

时间可以允许多个操作用于多块数据)来提升程序性能。在稍后部分的“SIMD”中将会有 SIMD 的更多讨论。SSE 表

示流 SIMD 扩展(Streaming SIMD Extension)。

_XM_VMX128_INTRINSICS_用于定义 Xbox360 主机使用 XNA 的 VMX128,在其它平台上该指令无效。VMX 是另

一种 SIMD 指令。VMX128 是 VMX 的扩展,用于 Xenon 处理器,该处理器可以在 Xbox360 主机内找到。如果游戏的

运行平台是 Xbox360,建议使用该指令。

XM_NO_ALIGNMENT 用于指导编译时不需要数据对齐。例如,使用该指令 XMMATRIX 将不会 16 位(4 字节)对齐。

该指令没有默认定义,因此编译默认启用字节对齐。

XM_NO_MISALIGNED_VECTOR_ACCESS 用于提升 VMX128 指令的性能,当进行联合写内存操作时。该指令只影

响 Xbox360 的 Xenon 处理器。

XM_NO_OPERATOR_OVERLOADS 用于禁用 C++风格的操作符重载。该指令也没有默认定义,但是依旧可以用于

重载操作符的强制省略。

XM_STRICT_VECTOR4 用于决定类型 XMVECTOR 和 XMVECTORI 的 X,Y,Z,W 分量是否可以直接访问(例如,float×Val

= vec.x)。该指令只影响 Xbox360 平台,因为该平台内部使用__vector4 类型,而不影响 Windows 平台,它的内部使

用的是__m128 类型。即使使用该指令,依旧可以使用访问器函数对这些结构的成员进行访问。

常量(Constants)

使用 XNA 数学过程中,其编译常量对于开发者很有用。如下是一个常量列表及它们各自的含义:

XNAMATH_VERSION——用于获取使用的 XNA 数学库版本号

XM_CACHE_LINE_SIZE——XNA 数学库中,用于流操作的 cache 行字节数

XM_PI——表示数学常数圆周率 PI(π)

XM_2PI——2×PI

XM_1DIVPI——1/PI

XM_1DIV2PI——2/PI(译者注:表示怀疑)

XM_PIDIV2——PI/2

XM_PIDIV4——PI/4

XM_PERMUTE_0X——该常量用于创建一个控制向量来控制在函数 XMVectorPermute 中的数据拷贝。使用该常

量的第一个向量的 X 分量将会通过控制向量拷贝到结果向量的指定位置

XM_PERMUTE_0Y——同上,只是第一个向量的 X 分量变为 Y 分量

XM_PERMUTE_0Z——同上

XM_PERMUTE_0W——同上

XM_PERMUTE_1X——使用该常量的,则函数 XMVectorPermute 的第二个参数向量的 X 分量拷贝到结果向量的

指定位置

XM_PERMUTE_1Y——同上

XM_PERMUTE_1Z,XM_PERMUTE_1W——都同上

XM_SELECT_0——用于构造一个控制向量用于控制分量的拷贝,当使用函数 XMVectorSelect 时。通过控制向量

的指定,将第一个向量的分量拷贝至结果向量的索引位置

XM_SELECT_1——同上,只不过是第二个向量的拷贝控制

XM_CRMASK_CR6——用于取得比较结果的 mask 值

XM_CRMASK_CR6TRUE——用于获得比较结果,并且表明是否为真

XM_CRMASK_CR6FALSE——获得比较结果,并且表明是否为假

XM_CRMASK_CR6BOUNDS——用于取得比较结果的 mask,并且表明结果是否输出越界。

宏(Macros)

XNA 数学中的宏,提供了大量用于公共操作的函数功能,比如比较操作,最大值最小值求解,断言,和在全局

范围标记对象。XNA 数学库中的宏完整列表如下:

XMASSERT

XMGLOBALCONST

XMComparisonAllFalse

XMComparisonAllTrue

XMComparisonAllInBounds

XMComparisonAnyFalse

XMComparisonAnyOutOfBounds

XMComparisonAnyTrue

XMComparisonMixed

XMMax

XMMin

宏 XMASSERT 通过断言条件为假,来提供调试信息。该宏的参数是条件表达式,并且包含宏所在的文件和行数

信息。

宏 XMGLOBALCONST 用于指定对象作为“pick-any”全局常量,用于减少数据段的尺寸和消除多个地方声明和使

用的全局常量的重载入。

宏 XMComparisonAllFalse 用于比较所有的分量,如果所有分量比较都为 false,则该宏返回 true

宏 XMComparisonALLTrue 比较所有分量,如果都为 true,则该宏返回 true

宏 XMComparisonAllInBounds 用于测试是否所有的分量都在某一范围,如果所有分量都满足,则该宏返回 true

宏 XMComparisonAnyFalse 返回 true,只要有一个分量比较为 false

宏 XMComparisonAnyOutOfBounds 返回 true,只要有一个分量不在指定范围

宏 XMComparisonAnyTrue 返回 true,只要存在一个分量比较为 true

宏 XMComparisonMixed 返回 true,如果有一些分量比较为真有一些分量比较为假

宏 XMMax 使用操作符<比较两值,并且返回较大值

宏 XMMin 使用操作符<比较两值,并且返回较小值

结构和类型(Structures and Types)

XNA 定义这些结构和类型的目的是,封装它们的功能使得在编程时更易于使用,更轻便,并且允许数据优化。

所有的这些结构和类型都通过头文件 xnamath.h 来暴露。之前我们只简略的接触了少量的向量和矩阵结构,在该部

分中我们将观察所有的这些结构和类型。

首先我们观察如下部分的数据结构:

XMBYTE4

XMBYTEN4

XMCOLOR

XMDEC4

XMDECN4

XMDHEN3

XMDHENN3

结构 XMBYTE4 用于创建含 4 个分量的向量,其中每个分量都是 1 个字节(使用 char 数据类型)。结构 XMBYTEN4

类似于结构 XMBYTE4,只是该结构用于存储归一化输入。当 XMBYTEN4 初始化时,它的输入将会乘以 127.0f 并且缩

放一个 0.0-1.0f 的因子,保证其输入数据在-127.0f 和 127.0f 之间。

结构 XMCOLOR 是一个 32 位的颜色结构,用于存储红色,蓝色,绿色和 alpha 管道值。每个分量都是 8 位,其

结构内部类似于 XMBTYE4。

结构 XMDEC4 对 X,Y,Z 分量都使用 10 位,而对于 W 分量只使用 2 位来存储。该结构存储归一化的值。另一个结

构 XMDHEN3 对 X 分量使用 10 位,Y 和 Z 分量使用 11 位来存储。而结构 XMDHENN3 存储 XMDHEN3 的归一化值。

如果你需要提供给每个分量更多位(总共 32 位)的结构,就可以使用这些数据结构。

接下来的一部分数据结构由两个分量组成:

XMFLOAT2

XMFLOAT2A

XMHALF2

XMSHORT2

XMSHORTN2

XMUSHORT2

XMUSHORTN2

结构 XMFLOAT2 在本章前部分用来表示 2D 浮点型向量。XMFLOAT2A 表示 4 字节对齐的 XMFLOAT2。

结构 XMHALF2 是一个存储一半精度 16 位的浮点 2D 向量(每个部分 16 位代替 32 位浮点数),而 XMSHORT2 表

示存储一半精度的 16 位整数分量的 2D 向量。在 XM 后面的 U 表示这些分量是无符号的,而后面的 N 表示存储的是

归一化的值。因此前缀 U 和后缀 N 同时出现,表示无符号归一化结构。

接下来显示的是所有 3 个分量的结构:

XMFLOAT3

XMFLOAT3A

XMFLOAT3PK

XMFLOAT3SE

XMHEND3

XMHENDN3

XMDHEN3

XMDHENN3

XMU555

XMU565

XMUDHEN3

XMUDHENN3

XMUHEND3

XMUHENDN3

其中的 XMFLOAT3 结构,之前的“Vectors”部分就已经提到过,是每个分量为浮点型的 3 分量结构。XMFLOAT3A

用于 4 字节对齐。XMFLOAT3PK 表示 X,Y 分量使用 11 位,而 Z 分量使用 10 位的结构(译者注:这里原文是 X,Y 各用

10 位,而 Z 用 11 位,这是错误的,可以在 xnamath.h 中看到,并且注意该结构总共才 32 位,是一种压缩结构),

而 XMFLOAT3SE 表示向量的 3 个分量,每个分量都使用 9 位表示基本数,使用 5 位表示指数。

结构 XMDHEN3(它的归一化为 XMDHENN3,这两个结构每个都是 32 位压缩结构)的 X 分量为 10 位,Y 和 Z 分量

为 11 位。结构 XMHEND3 和 XMHENDN3 对于分量 X,Y 都使用 11 位,而分量 Z 使用 10 位表示。其无符号版本是

XMUDHEN3 和 XMUDHENN3。

结构 XMU555 对于 X,Y,Z 分量都使用 5 位表示(译者注:加起来 15 位,还有 1 位表示 w 不使用)。而 XMU565 即

表示 X,Z 用 5 位,而 Y 使用 6 位。

接下来是有四个分量的向量结构:

XMFLOAT4

XMFLOAT4A

XMHALF4

XMICO4

XMICON4

XMSHORT4

XMSHORTN4

XMUBYTE4

XMUBYTEN4

XMUDEC4

XMUDECN4

XMUICO4

XMUICON4

XMUNIBBLE4

XMUSHORT4

XMUSHORTN4

XMXDEC4

XMXDECN4

XMXICO4

XMXICON4

结构 XMFLOAT4 是每个分量都是浮点型的 4 分量向量,在本章的“Vectors”部分已经提到。结构 XMHALF4 是

使用一半精度位的 4D向量,XMSHORT4是使用一半精度位的短整型 4D向量。再次声明,后缀A表示字节对齐(Aligned)

结构,而后缀 N 表示归一化(Normalized)版本。

结构 XMUBYTE4 的各分量使用无符号 char 型,而结构 XMUNIBBLE4 每个分量只使用 4 位。后缀有 ICO 标识的表

示 X,Y,Z 各分量使用 20 位,而 W 分量使用 4 位,共 64 位表示 ICO 结构。而含 DEC 标识的表示对 X,Y,Z 分量各使用 10

位,而 W 分量使用 2 位,共 32 位表示 DEC 结构。而含 XICO 或 XDEC 标识使用有符号数,而 UICO 和 UDEC 表示无

符号数。

接下来的结构列表与矩阵相关:

XMFLOAT3x3

XMFLOAT4x3

XMFLOAT4x3A

XMFLOAT4x4

XMFLOAT4x4A

XMMATRIX

结构 XMFLOAT3X3 表示浮点类型的 3x3 矩阵(共 9 个浮点值),通常用于表示旋转矩阵。XMFLOAT4X3 表示 4X3 矩

阵,而 XMFLOAT4X4 表示 4X4 矩阵。后缀 A 表示字节对齐。XMMATRIX 是行主序的 4X4 字节对齐矩阵,工作时映射

到 4 个向量硬件寄存器中。

最后一个列表是附加的类型,它们如下:

HALF

XMVECTOR

XMVECTORF32

XMVECTORI32

XMVECTORU32

XMVECTORU8

HALF 数据类型,是一个 16 位的浮点型数字,使用 5 位表示指数部分,10 位表示基数部分,还有 1 位为符号位。

结构 XMVECTOR 有 4 个分量的结构,其中每个分量 32 位。该结构可以用于浮点型或整型,并且是对齐优化的,

映射到硬件向量寄存器中工作,该结构在本章的“Vectors”中提到过。

结构 XMVECTORF32 是一个轻便的共 4 个分量,每个分量为 32 位浮点型结构,可以使用 C++初始化语法,使用

浮点型数值来初始化 XMVECTOR 结构。

结构 XMVECTORI32 同上,只是每个分量为 32 位整型。

结构 XMVECTORU32,和 XMVECTORU8 解释与上类似。

附加的函数(Additional Functions)

该小节,我们将简略讨论 XNA 数学中我们还没涉及到的一些附加的有用的函数。这些函数处理颜色,转换和标

量计算。首先我们讨论有关颜色的函数。

处理颜色的函数(Color Functions)

XNA 数学有一系列的函数用于操纵颜色数据。第一个介绍的函数用于调整颜色的对比度,函数名为

XMColorAdjustContrast,其原型如下:

XMVECTOR XMColorAdjustContrast( XMVECTOR C, FLOAT Contrast );

该函数的第一个 XMVECTOR 类型的参数表示一个颜色值,各分量范围为 0.0-1.0f。如果参数 contrast 为 0.0f,则

颜色调整为 50%的灰度,如果参数为 1.0f,则返回原颜色,如果参数为之间的值,则返回一个对比色。

下一个函数是 XMColorAdjustSaturation,用来调整颜色的饱和度,其原型如下:

XMVECTOR XMColorAdjustSaturation( XMVECTOR C, FLOAT Saturation );

该函数的第一个参数也是一个表示颜色的 XMVECTOR 向量值,颜色各分量也在 0.0-1.0f 之间。如果饱和度参数

为 0.0f,则返回一个灰度颜色,如果饱和参数为 1.0f,则返回原颜色。而饱和参数 Saturation 在 0.0-1.0f 之间,则返

回两颜色(灰度颜色和原颜色)之间的线性内插值颜色。

接下来的六个函数用于颜色比较。这些函数用于测试两表示颜色向量之间的关系,相等及不等关系,大于或小

于关系,大于等于或小于等于关系。其返回值为 true/false。这些函数原型如下:

BOOL XMColorEqual( XMVECTOR C1, XMVECTOR C2 );

BOOL XMColorNotEqual( XMVECTOR C1, XMVECTOR C2 );

BOOL XMColorGreater( XMVECTOR C1, XMVECTOR C2 );

BOOL XMColorGreaterOrEqual( XMVECTOR C1, XMVECTOR C2 );

BOOL XMColorLess( XMVECTOR C1, XMVECTOR C2 );

BOOL XMColorLessOrEqual( XMVECTOR C1, XMVECTOR C2 );

接下来两个函数是用于颜色值是否为无穷的比较的函数 XMColorIsInfinite,和查看颜色分量是否有 NaN 定义(不

是一个数字,表示无穷)的函数 XMColorIsNaN,其原型如下:

BOOL XMColorIsInfinite( XMVECTOR C );

BOOL XMColorIsNaN( XMVECTOR C );

接下来的函数 XMColorModulate,用于混合两个颜色。而最后一个介绍的函数 XMColorNegative,用于对一个颜

色取对比值。这两个函数原型如下:

XMVECTOR XMColorModulate( XMVECTOR C1, XMVECTOR C2 );

XMVECTOR XMColorNegative( XMVECTOR C );

上述函数 XMColorNegative 类似于执行以下操作:

XMVECTOR col, result;

result.x = 1.0f - col.x;

result.y = 1.0f - col.y;

result.z = 1.0f - col.z;

result.w = 1.0f - col.w;

使用该函数,黑色将会变换为白色,而白色会变换为黑色。而在 0.0-1.0f 之间的值将会反向它们的明暗对比。

转换函数(Conversion Functions)

XNA 数学函数库中有一类函数用于将数据的一种类型转换为另一种类型。函数 XMConvertFloatToHalf 用于将一

个 32 位浮点型数值转换为一个 HALF 类型的值(一半的精度)。任何异常的浮点数(较大的)例如 NaN,正或负无穷等,

都将转换为 0x7fff,该转换函数原型如下:

HALF XMConvertFloatToHalf( FLOAT Value );

为了转换一个 HALF 类型到浮点类型,可以使用函数:FLOAT XMConvertHalfToFloat( HALF Value );

根据 XNA 数学中该函数的实现(内联实现),可以看到传入 0x7fff 将会转换为浮点值 131008.0f。

如 果 需 要 将 一 个 数 组 的 所 有 值 进 行 转 换 , 我 们 可 以 使 用 函 数 XMConvertFloatToHalfStream 和

XMConvertHalfToFloatStream。这些函数的参数顺序为输出流数组首元素的地址,每个输出值之间的字节跨度,输入

数组的首元素地址,输入元素之间的字节跨度,和需要转换的元素个数。这些函数原型如下:

HALF* XMConvertFloatToHalfStream( HALF* pOutputStream, UINT OutputStride,

CONST FLOAT* pInputStream, UINT InputStride, UINT FloatCount );

FLOAT* XMConvertHalfToFloatStream( FLOAT *pOutputStream, UINT OutputStride,

CONST HALF* pInputStream, UINT InputStride, UINT HalfCount );

另一些有用的函数是度数和弧度之间的相互转化。游戏中,我们有时需要用弧度,而有时需要使用度数。执行

这些转换的函数为 XMConvertToDegrees 和 XMConvertToRadians,这两函数都接收浮点值输入,输出也是浮点值。其

原型如下:

FLOAT XMConvertToDegrees( FLOAT fRadians );

FLOAT XMConvertToRadians( FLOAT fDegrees );

最后要介绍的转换函数用于浮点型向量与整型向量之间的相互转换。函数 XMConvertVectorIntToFloat 和函数

XMConvertVectorUIntToFloat 将会将第一个整型向量参数除以第二个参数 DivExponent 来返回浮点型向量。而函数

XMConvertVectorFloatToInt 和函数 XMConvertVectorFloatToUInt 做类似的事情,只不过除法变成乘法。这些函数原型

如下:

XMVECTOR XMConvertVectorFloatToInt( XMVECTOR VFloat, UINT MulExponent );

XMVECTOR XMConvertVectorFloatToUInt( XMVECTOR VFloat, UINT MulExponent );

XMVECTOR XMConvertVectorIntToFloat( XMVECTOR VInt, UINT DivExponent );

XMVECTOR XMConvertVectorUIntToFloat( XMVECTOR VUInt, UINT DivExponent );

标量函数(Scalar Functions)

接下来要介绍的一类函数是标量函数。这些函数接收一个浮点数参数,并在之上做指定的操作返回结果。这些

函数用于计算余弦值,例如 XMScalarCos,用于计算正弦值,如函数 XMScalarSin,计算反余弦(XMScalarACos)和反正

弦(XMScalarASin),还有计算这些预估值(译者注:后缀 Est 的函数,用于快速计算,以精度换性能)。这些函数原型:

FLOAT XMScalarCos( FLOAT Value );

FLOAT XMScalarCosEst( FLOAT Value );

FLOAT XMScalarSin( FLOAT Value );

FLOAT XMScalarSinEst( FLOAT Value );

FLOAT XMScalarACos( FLOAT Value );

FLOAT XMScalarACosEst( FLOAT Value );

FLOAT XMScalarASin( FLOAT Value );

FLOAT XMScalarASinEst( FLOAT Value );

FLOAT XMScalarSinCos( FLOAT Value );

FLOAT XMScalarSinCosEst( FLOAT Value );

还有一些三角学函数,如 XMScalarModAngle 和 XMScalarNearEqual。第一个函数通过模上 2PI 用于计算一个-PI

到+PI 之间的值。第二个用于测试两值是否近似相等。这两个函数原型如下:

FLOAT XMScalarModAngle( FLOAT Value );

BOOL XMScalarNearEqual( FLOAT S1, FLOAT S2, FLOAT Epsilon )

附加的数学结构和主题(Additional Math Structures And Topics)

XNA 数学有表示颜色,向量,矩阵,平面,和四元数的结构,所有的这些都通过 4D 向量表示,这里 4x4 矩阵

可看做是 4 个行向量表示。关于平面和四元数的更多高级主题,本书中将不再涉及,但是 XNA 数学中有处理它们的

函数。

一张平面可以用一个 4D 向量表示。一张平面是通过两无限延伸的轴表示的无限表面。一张平面可以这样定义,

使用法线向量指定平面的方向(X,Y 和 Z)和从原点到平面的距离值(W)来表示平面。平面一般用于剔除和碰撞检测,

trigger volumes 和其他涉及线的概念。我们不会像渲染三角形,点和线那样渲染平面。

单位四元数可以表示旋转和方向。一个四元数有 4 个浮点值成员,所以可以使用 4D 向量,比如 XMFLOAT4 或

XMVECTOR 表示一个四元数。

游戏中的物理和碰撞检测(Game Physics And Collision Detection)

本章所涉及到的不仅有图形学而且还有游戏物理学。游戏物理学是 3D 游戏开发的一个重要组成部分,当创建

仿真模拟时就必须要用到。游戏中的物理学通常处理有关点质量,刚性物体和柔性物体方面的知识。

点质量是物理对象,它们可以有线性速度和可以更新位置,但是不能用于旋转。对于初学者来说这是最容易的

部分,并且用于游戏中的许多模拟,包括粒子系统。

刚性物体概念,应用与场景中的物体,它们之间有相互作用力,这使得力不仅用于它们的位置和方向,而且影

响它们的角速度。这可以用于像柳条箱或其他物体的爆炸动作,或其它外力作用它们导致在场景中的仿真移动,通

过移动和旋转来与所在世界的相互作用。另一个例子是模拟身体,比如敌人可以通过与环境中的楼梯模型碰撞从而

跌倒的效果。刚性物体不会使形状变形。

柔性物体没有上述两个物理概念这么常用,因为它们模拟的物体可以变形(译者注:比如模拟布料,头发等)。

Epic 的 Unreal 游戏引擎就支持柔性物体。

碰撞检测和检测的反应是指多个物体之间的碰撞和通过它们之间的作用力进行碰撞解析或约束它们的运动。这

样做的好处这些物体从来不会穿入对方的内部,使得它们相互接触时如你的预期的那样作用。游戏中的物理在物体

上面施加力的作用来设计它们在环境(场景)中的行为。碰撞检测允许我们捕获物体,当它们之间会进入对方时,并

且会在这些物体上面施加力的作用,碰撞反馈使得我们可以像现实中那样逼真的模拟碰撞。尽管物理和碰撞检测关

系密切,但它们依旧是用于创建设计者的完全仿真的分开的主题。

章末总结(Summary)

数学是视频游戏开发中的一个重要主题。XNA 数学是 DirectX 11SDK 的一部分,是一个优化过的代码库,可用于

我们自己的处理。本章我们主要覆盖 XNA 数学主题,这在本书中是一个重要部分,该库本身由成百个结构和函数组

成。该库用于我们的处理,是一个十分有用的工具。当被正确使用时,你可以从中获得极大的性能。

你所学到的(What You Have Learned)

学习了向量

学习了矩阵

学习了坐标系统和变换

学习了 XNA 数学中的各种函数和结构

学习了投影知识

章末习题(Chapter Questions)

答案见于附录 A。

1. 叙述向量的定义

2. 矩阵的定义

3. 结构 XMVECTOR 和 XMFLOAT4 之间的区别?

4. 结构 XMFLOAT4 和 XMFLOAT4A 之间的区别?

5. 怎样在 3D 坐标系统中定义一个点?

6. 怎样单位化一个向量?

7. 点积计算的目的是什么?

8. 由一系列相互连接的线组成的原始类型是什么?

9. 什么是单位矩阵?

10. 矩阵连乘的另一术语是什么?

第七章 着色器和特效(Shaders And Effects)

接触 Direct3D 的着色器有一段时间了。顶点着色器和像素着色器给予开发者控制他们数据的任何细节的能力,

当这些数据通过管道的多个处理阶段时。随着 Direct3D 11 发布,着色器的下一代技术也随着发布:着色器模型 5.0。

本章知识概览:

Effect 文件是什么

怎样使用高级着色语言(HLSL)

着色器的种类有哪些

光照的基本知识

HLSL 参考

Direct3D 中的着色器(Shaders In Direct3D)

现在的顶点和像素着色器的能力已经得到很大的提升,有更加丰富的指令,可以访问更多的纹理,并且也正变

得更加复杂。与之前的仅仅改进着色器类型相比,现在 Direct3D 11 还引入了计算着色器,外壳和域着色器。这些新

的着色器与 Direct3D 10 中的几何着色器,顶点着色器,像素着色器一起构成了 Direct3D 11 中的着色器类型,而原

有的着色器早在 DirectX 8 中就开始有了。

可编程着色器的历史(History of Programmable Shaders)

在有着色器之前的图形学编程,使用一个公共的固定算法集合,也就是著名的固定函数管线。为了启用不同的

特征和效果,固定函数管线本质上是提供一种方式来启用或禁用内建的各种状态。这种方式限制了开发人员,因为

这样的图形学 API 编程(例如,Direct3D 和 OpenGL)通过这些 API 控制的方式就固定了,从而没有扩展性。

在 DirectX 8 中,和固定函数算法一起,Direct3D 组件中的图像管线首次提供了可编程的着色器。图像管线就可

以通过装配指令或 HLSL(高级着色语言,High Level Shading Language)进行编程。在 Direct3D 10 中,HLSL 成为了编写

着色器的唯一方式,而装配指令不再支持。并且此时的图形管线成为了 100%可编程的,而固定函数管线已经被完

全移除。

正因为 Direct3D 不再支持固定函数管线,从而导致你必须设计处理顶点和像素的行为。之前使用固定函数管线

处理顶点的方式是将顶点传递给它们来绘制。这种限制方式使得你必须全部控制管线,并且也受管线所支持的功能

限制。这种固定函数管线方式处理光照手段单一,并且需要你设置大量的纹理值。在 Direct3D 中,使用这种方式限

制了你所能够实现的效果。

现在,由于每个顶点都将通过系统处理,你有机会操纵它或者不改变它而直接让它通过。对于像素来说也是一

样。任何将会被系统渲染的像素在输出到屏幕之前,你都有机会将其改变。改变顶点和像素的能力就包含在 Direct3D

的着色器机制中。

Direct3D 中的着色器通过暴露管线中片段的方式,是你可以对其进行动态重组。Direct3D 支持一系列的着色器,

有:顶点着色器,像素着色器,几何着色器,计算着色器,外壳和域着色器。

顶点着色器可以操纵你所预料的事物——顶点。任何通过管线的顶点在输出之前都可以被当前着色器所使用。

同样地,任何像素被渲染输出到屏幕之前,也必须通过像素着色器。几何着色器是 Direct3D 10 中有的一种特殊的着

色器类型,它允许同时操纵多个顶点,控制粒度是对基本几何体进行控制。

Direct3D 11 中的一种新的着色器类型是计算着色器,其硬件要求是至少支持着色器模型 4.0,并且可以在 GPU

上执行通用并行计算。计算着色器可用于从图形到物理,视频编码和解码,等这些方面。由于 GPU 的设计,并不是

所有的任务都适合使用 GPU 来执行,只有那些适合该 GPU 架构的计算任务,才可以利用其相对于 CPU 的额外处理

能力带来性能的提升。

Direct3D 11 中另外两个新着色器类型是外壳和域着色器,随着这两个着色器附加了一个细分表面阶段。该阶段

虽然不可编程,但是这两种着色器可以对它进行补充(外壳和域着色器)。外壳着色器基本上是对输入表面数据转化

为网格控制点的源控制,而域着色器是对每一个顶点进行操作。细分表面的思想是通过表面的定义来控制网格,和

对表面进行动态细分来产生网格的不同层次的细节。这个思想的意思是我们可以借助图形硬件来产生高阶复杂的多

边形模型,而不必实际传输这些高阶多边形细节给图形硬件。这里的动态产生不同层次的细节已经属于高级图形学

编程。

效果文件(Effect Files)

Direct3D 中,有个 Effect 文件的概念。将所创建的着色器与这些文件捆绑在一起就是所谓的一个效果(特效)。大

多数时候,你只是结合顶点和像素着色器来创建某一行为,这叫做技术。一种技术定义了一个渲染效果,而 Effect

文件就是包含很多渲染技术的文件。

当然可以在 Direct3D 11 中一直单独的使用着色器,但是你会发现当将这些单独的着色器放在一起作为一个效果

使用时,会变得十分有用。一个效果是以一种特别的方式将所需的着色器简单的放在一起打包来渲染对象。效果作

为一个单独的对象载入,并且只包含那些必须用到的着色器。通过在你的场景中改变效果文件,你就能够很容易改

变 Direct3D 的渲染方式。效果定义在效果文件之中,以文本格式从磁盘中载入,编译和执行。

尽管你可以在 Direct3D 中任意的使用效果文件,但是也可以不使用它们。本书之前的所有 Demo,每次都是使

用单个的着色器文件。效果文件和我们之前使用的着色器文件不同之处在于,效果不仅仅包含着色器,还有诸如渲

染状态,混合状态等内容。

Effect 文件布局(Effect File Layout)

每个效果文件都包含一个特定的渲染功能集。每个效果,都可以在你绘制场景中的物体时应用,来指示物体绘

制的外观和怎样绘制。例如,你可以创建一个效果专用于贴图对象,或者创建一个效果用于产生明亮的光照或模糊

的光照。对于同一个效果也可以有不同的版本,根据机器性能不同,将某些效果用于低端机器,而另一些用于高端

机器。在使用效果时,它们有着十分广泛的用途。

之前,顶点和像素着色器是被分别载入和应用的。效果将这些着色器结合为一个自包含单元,从而包含多个着

色器的功能。效果由几个不同的部分组成:

外部变量(External Variables)——这些变量的数据来自于程序的调用

输入结构(Input structures)——定义着色器之间传递信息的结构

着色器(Shaders)——着色器代码(一个效果文件可以包含多个着色器)

技术块(Technique blocks)——效果中定义的着色器的使用过程

最简单的 Effect 格式是只包含一个技术块,并且技术块中只有一个着色器,该着色器直接将从顶点结构接收的

输入数据传出。这意味着顶点位置和其它属性都没有改变,直接传入到管线的下一阶段。只有一个简单的像素着色

器,将不执行任何计算直接返回一个单一的颜色值。几何着色器,外壳和域着色器,由于是可选的,就设置为 NULL。

该最简单的效果文件的文本内容如下:

struct VS_OUTPUT

{

float4 Pos : SV_POSITION;

float4 Color : COLOR0;

};

VS_OUTPUT VS( float4 Pos : POSITION )

{

VS_OUTPUT psInput;

psInput.Pos = Pos;

psInput.Color = float4( 1.0f, 1.0f, 0.0f, 1.0f );

return psInput;

}

float4 PS( VS_OUTPUT psInput ) : SV_Target

{

return psInput.Color;

}

technique11 Render

{

pass P0

{

SetVertexShader( CompileShader( vs_4_0, VS( ) ) );

SetGeometryShader( NULL );

SetPixelShader( CompileShader( ps_4_0, PS( ) ) );

}

}

载入一个效果文件(Loading an Effect File)

Effect 通常使用函数 D3DX11CreateEffectFromMemory 从一段缓存中载入。因为该函数载入效果是来自于一段缓

存,所以你需要使用 std::ifstream 读取 Effect 文件,将文件内容传递给载入函数。该函数原型如下:

HRESULT D3DX11CreateEffectFromMemory(

void* pData,

SIZE_T DataLength,

UINT FXFlags,

ID3D11Device* pDevice,

ID3DX11Effect** ppEffect

);

上述函数的第一个参数是 Effect 文件的数据(HLSL 的 Effect 源代码),随后的参数是所包含源代码的字节数,编

译标识,Direct3D 11 设备,和将持有 Effect 对象的 ID3DX11Effect 类型指针。

外部变量和常量缓存(External Variables and Constant Buffers)

大多数效果都需要额外的顶点输入数据,那么这里的外部变量就十分有用了。外部变量是那些当你的效果在应

用程序中可见时,在你的效果文件中声明的变量。这些在效果文件中声明的外部变量可以接收一下诸如当前帧时间,

世界投影,或光照位置的信息,当程序调用时它们可以更新。

随着 Direct3D 10 中的介绍,现在所有的外部变量都属于常量缓存。常量缓存用于程序调用的组合变量,使得可

以优化对它们的访问。常量缓存类似于结构的定义,使用 cbuffer 关键字来创建。如下的小段 HLSL 代码的例子:

cbuffer Variables

{

matrix Projection;

};

常量缓存通常在 Effect 文件的顶部声明,并且不属于其它任何部分。为了易于使用,它可以组合那些都大量访

问的变量。例如,组合在一起的各变量可以每帧分别更新。你也可以创建多个常量缓存。当 Effect 文件载入时,你

可以在程序中将 Effect 变量与外部变量绑定起来。如下代码显示了怎样将外部变量“Projection”绑定到程序中的

ID3DX11EffectMatrixVariable:

ID3DX11EffectMatrixVariable * projMatrixVar = 0;

projMatrixVar = pEffect->GetVariableByName( "Projection" )->AsMatrix( );

projMatrixVar->SetMatrix( ( float* )&finalMatrix );

输入和输出结构(Input and Output Structures)

Effect 文件经常需要在着色器之间传递多个值,为了简单处理,这些值可以包含在一个结构中一起传递。结构

允许多个变量放置在一起,以包裹的形式传递,并且当需要增加新变量时,让所需要做的工作最少。例如,顶点着

色器一般都需要传递像顶点位置,颜色,或法线值到像素着色器中。因为顶点着色器限制只能返回一个单一的值,

所以顶点着色器只需要将这些变量打包进一个结构,将此结构传递个像素着色器即可。像素着色器就能访问该结构

的内部变量。构建一个 VS_OUTPUT 的例子如下:

struct VS_OUTPUT

{

float4 Pos : SV_POSITION;

float4 Color : COLOR0;

};

使用结构这种方法很简单。首先,在顶点着色器中实例化该结构。其次,分别填充该结构的成员变量,并且将

该结构返回。则管线中的下一个着色器将会使用顶点着色器传出 VS_OUTPUT 结构对象作为自身的输入,来访问你

所设置的变量。顶点着色器定义和使用结构的一个例子:

VS_OUTPUT VS( float4 Pos : POSITION, float4 Color : COLOR )

{

VS_OUTPUT psInput;

psInput.Pos = mul( Pos, Projection );

psInput.Color = Color;

return psInput;

}

技术块(Technique Blocks)

Effect 文件将多个着色器功能放入一个单一的块,叫做技术块。一种技术定义了一些物体怎样被绘制。例如,

你可以定义一种技术来支持半透明或不透明的效果。通过技术之间的切换,被绘制的物体将可以从实体变成半透明

物体。技术块的定义通过使用着色器的关键字 technique11 并在随后定义技术:

technique11 Render

{

// technique definition

}

每一种技术都有一套顶点和像素着色器,用于处理由管线传过来的顶点和像素。Effect 允许定义多个技术块,

并且在 Effect 文件中至少需要定义一种技术。每一种技术都可以包含多个 pass 过程。一般的说,大部分定义的技术

都只包含一个 pass 过程,但是需要注意多个 pass 过程可以用于更加复杂的效果。每一个 pass 过程都使用着色器在

硬件上执行不同的特殊效果(特效)。

载入 Effect 文件后,为了使用其中定义的技术,可以获得对其中的技术的访问。技术存储在一个

ID3DX11EffectTechnique 对象中,在后面的顶点布局的定义或渲染时使用。如下代码段显示怎样从 Effect 中创建技术

对象:

ID3DX11EffectTechnique* shadowTech;

shadowTech = effect->GetTechniqueByName( "ShadowMap" );

因为可以创建简单或复杂的渲染技术,这些技术通过其中定义的 pass 过程来表现其效果。每个 pass 过程更新

或改变渲染状态和应用于场景的着色器。因为并不是所有的效果都想通过单一的 pass 过程来表现,所以技术块中允

许我们定义多个 pass 过程。有一些后处理(post-processing)效果,例如域的深度,就需要多个 pass 过程处理。需要

注意的是,使用多个 pass 过程将会导致物体被绘制多次,有时渲染就会变慢。

假如你现在有一个技术对象,并且当你绘制物体时准备使用它。使用该技术要求遍历有效的 pass 过程,并且调

用你的绘制函数。在使用 pass 过程中的着色器绘制之前,技术需要在硬件中进行准备来进行绘制。函数 Apply 用于

设置当前的技术以及它的所有渲染状态和数据。例子如下:

D3DX11_TECHNIQUE_DESC techDesc;

ShadowTech->GetDesc( &techDesc );

for( UINT p = 0; p < techDesc.Passes; p++ )

{

ShadowTech->GetPassByIndex( p )->Apply( 0, d3dContext_ );

// Draw function

}

在 HLSL Effect 文件中创建每个技术需要使用关键字 pass,并且关键字后面跟随一个 pass 级别。Pass 级别是字母

P 后跟随 pass 过程的序号表示。如下例子,有两个 pass 过程,P0 和 P1,为了使技术有效至少需要一个 pass 过程:

technique11 Render

{

pass P0

{

// pass shader definitions

}

pass P1

{

// pass shader definitions

}

}

每个 pass 过程的主要工作就是设置着色器。因为着色器可以用于不同的 pass 过程,所以必须通过函数

SetVertexShader,SetGeometryShader,SetPixelShader 等,来指定具体使用的着色器。

technique11 Render

{

pass P0

{

SetVertexShader( CompileShader( vs_4_0, VS( ) ) );

SetGeometryShader( NULL );

SetPixelShader( CompileShader( ps_4_0, PS( ) ) );

}

}

正如你所看到的,设置着色器的函数包括了调用一个函数 CompileShader。该 HLSL 函数的参数有着色器版本类

型(例如,顶点着色器 5.0 是 vs_5_0),和在 HLSL 文件中你自己编写的着色器主函数进入点。

光栅化状态(Rasterizer States)

Effect 文件允许你在着色器中设置光栅化状态,而不是在应用程序级别来做。你也许可以看到 3D 建模软件以线

框模型显示物体。使用这种方式显示 3D 物体只需使用它们的轮廓即可。这种方式可以让你看到物体是怎样制作的,

这种方式可以比作观察一栋房子的框架,而不是局限于墙的形式。

Direct3D 的默认方式是实体模式,物体表面的绘制是不透明的。这可以通过修改光栅化状态来改变。光栅化状

态指示 Direct3D 在光栅化阶段物体具有怎样的行为,例如采用哪种类型的剔除,像这些多重采样和裁剪特性是否启

用,以及应该选用哪种填充模式。

光栅化状态对象从 ID3D11RasterizerState 接口继承而来,通过调用函数 CreateRasterizerState 来创建,该函数原

型如下:

HRESULT CreateRasterizerState(

const D3D11_RASTERIZER_DESC* pRasterizerDesc,

ID3D11RasterizerState** ppRasterizerState

);

该函数用于在 HLSL 中设置状态,模仿它们不同的应用方面。结构 D3D11_RASTERIZER_DESC 用于定义不同的状

态操作,其结构如下:

typedef struct D3D11_RASTERIZER_DESC {

D3D11_FILL_MODE FillMode;

D3D11_CULL_MODE CullMode;

BOOL FrontCounterClockwise;

INT DepthBias;

FLOAT DepthBiasClamp;

FLOAT SlopeScaledDepthBias;

BOOL DepthClipEnable;

BOOL ScissorEnable;

BOOL MultisampleEnable;

BOOL AntialiasedLineEnable;

} D3D11_RASTERIZER_DESC;

类型为 D3D11_FILL_MODE 的成员,用于控制几何图形的绘制方式。如果使用值 D3D11_FILL_WIREFRAME,则几

何图形将会使用线框模式绘制,如果使用 D3D11_FILL_SOLID,则几何图形将会被绘制为实体。

第二个成员是 D3D11_CULL_MODE 类型的剔除模式。剔除模式指示光栅化时,哪些表面绘制,哪些不需要绘制。

想象一下你现在有一个由多个三角形组成的球面,无论你从哪面观察球,都不可能在同一时刻看到所有的三角形,

只有那些三角形的方向朝你这边的才可以看到。那些在球的背面的三角形叫做背面。基于组成三角形的顶点的定义

顺序,它们组成三角形的围绕方式也不同。组成三角形的顶点的围绕方式定义了三角形的朝向,顺时针或逆时针。

基于不同 3D 物体的类型,尽管你使用同样的围绕方式定义了所有的三角形,从照相机的角度看只需物体进行旋转

操作将导致一些三角形逆向。回到刚才假想的球的情景,从照相机的透视角度看,有一些三角形顺时针,另一些逆

时针。而剔除模型就是指示 Direct3D,哪些三角形可以安全的忽略从而不进行绘制。D3D11_CULL_MODE 类型有三

个枚举值,D3D11_CULL_NONE 不进行剔除,D3D11_CULL_FRONT 剔除所有朝向照相机的多边形,和 D3D11_CULL_BACK

剔除所有的与照相机同向的多边形。通过指定剔除模型,在请求 Direct3D 绘制时,将会有一部分三角形剔除。

如果你想了解上述结构的其它成员的所有细节,可以参照 DirectX SDK 文档。一旦你填充好该结构,就可以调用

函数 CreateRasterizerState 来安全的创建新的光栅化状态。在创建新的光栅化状态之后,在它产生效果之前需要进行

设置操作。可以调用函数 RSSetState 来改变当前激活的光栅化状态,该函数由 ID3D11DeviceContext 接口元素提供,

其函数原型如下:

void RSSetState( ID3D11RasterizerState* pRasterizerState );

高级着色语言(High Level Shading Language)

正如所知,高级着色语言(HLSL)是用来编写着色器的程序语言。其语法和结构十分类似于 C 语言,HLSL 允许你

创建小的着色器程序载入显卡运行。在最新的着色器模型 5.0 中,我们可以使用面向对象编程概念。着色器模型 5.0

是着色器模型 4.0 的超集。该部分我们将简单介绍 HLSL 语法。

变量类型(Variable Types)

HLSL 包含多种可以在 C++中找到的变量类型,例如 int,bool,和 float 类型,同时也包含少量的新类型,如 half,

int1x4,和 float4,这些类型我们已经在第六章中讨论过。

有些变量类型包含多个分量,从而允许你将多个单值放入变量中。例如,类型 float4 允许你在其中存储 4 个 float

值。通过使用这些特殊类型存储值,图形显卡硬件可以对这些数据访问进行优化,确保对它们进行快速存取。

float4 tempFloat = float4(1.0f, 2.0f, 3.0f, 4.0f );

任何包含多个分量的变量,都可以通过操纵单个分量来访问该变量。例如,一个 float3 类型的变量,其内部有

X,Y,Z 三个分量。如下例子,变量 singleFloat 被分量 X 所填充:

float3 newFloat = float3( 0.0f, 1.0f, 2.0f );

float singleFloat = newFloat.x;

任何包含多个分量的变量都可以使用这种方式访问。

语义(Semantics)

语义是一种让着色器知道所结构其中所包含的变量类型对它们进行访优化的一种方式。语义跟随变量声明之后,

有类型如 COLOR0, TEXCOORD0,和 POSITION 等。如下结构,显示两个变量 Pos 和 Color 之后的跟随的语义,来表

明它们的使用方式。

struct VS_OUTPUT

{

float4 Pos : SV_POSITION;

float4 Color : COLOR0;

};

一些公共的语义包括:

SV_POSITION——一个具体的变换位置的 float4 值

NORMAL0——定义一个法线向量

COLOR0——定义一个颜色值

还有一些其他的语义见于 DirectX SDK 文档的 HLSL 章节的完全列表。其中大量的语义末尾跟随一个数字,因为

可能定义多个这样的语义类型。

函数声明(Function Declarations)

HLSL 中的函数定义与其它编程语言中的完全一样:

ReturnValue FunctionName( parameterName : semantic )

{

// function code goes here

}

函数的返回值可以是任何 HLSL 中定义的类型,包括组合类型和 void 空类型。当你定义一个着色器函数的参数

列表时,需要完全指定跟随变量之后的语义标识。当定义函数参数时,还有一些事项需要注意,因为 HLSL 没有具

体的方式用于返回参数列表中的值的引用,这需要通过定义少量的关键字来达成同样的结果。

在参数声明之前,使用关键字 out 可以让编译器知道该变量可以用于输出。另外地,关键字 inout 可以允许变

量既作为输入也作为输出:

void GetColor( out float3 color )

{

color = float3( 0.0f, 1.0f, 1.0f );

}

顶点着色器(Vertex Shaders)

顶点着色器是管线的一个部分,这部分中你可以控制每个需要在系统中处理的顶点。Direct3D 之前的版本,有

另一种处理顶点的方式——使用固定函数管线处理,该方式有一个内建的用于处理顶点的功能集合。现在,最新版

本的 Direct3D 中,你必须自己处理全部的过程。为了这样做,你至少得编写一个简单的顶点着色器。

顶点着色器是三种可以在 Effect 文件中的着色器之一。当物体通过管线传输需要绘制时,它们的顶点会发送到

你的顶点着色器中处理。如果你不想对传入的顶点做任何处理,可以直接将它们传送给像素着色器进行绘制。大多

数情况,你至少需要使用一个世界或投影变换作用这些顶点,以使得它们在正确的空间位置被渲染。

使用顶点着色器,你可以对顶点做诸多的控制,而不仅仅是进行简单的变换。可以将顶点平移到任何坐标轴,

改变它的颜色,或任何其它性质的控制。

像素着色器(Pixel Shaders)

像素着色器可以让你访问任何经过管线输出之前的像素。在像素被绘制到屏幕之前,你有机会改变每个像素的

颜色。某些情况,你只需要简单的返回由顶点或几何着色器传入的像素颜色,但是大多数情况,你需要处理光照或

贴图对像素颜色的影响。

贴图颜色的反转(Texture Color Inversion)

本章我们将创建一个简单的像素着色器用于转化一个渲染表面的颜色,其 Demo 位于 Chapter7/ColorInversion

目录。该 Demo 使用一些与第六章的 CubeDemo 中同样的代码,其不同部分在像素着色器中和我们将使用一个 Effect

文件中的一种技术。

该 effect 效果的目标是使用反向颜色渲染表面。意思是原来是白色渲染,现在使用黑色渲染,而黑色变成白色,

而其它颜色变成其对立面颜色。要表现该效果十分的容易,只需在像素着色器中使用 1 减去原来的颜色,即可反转

颜色。因为 1 减去 1(白色)会变成 0(由白色变为黑色),而 1 减去 0 变为 1(由黑色变为白色)。

HLSL 着色器执行反转颜色的代码见于清单 7.1。该着色器代码与第六章的 CubeDemo 中的代码相同,只是在像

素着色器中使用 1-color 来代替原来代码。注意 SV_TARGET 是一个输出语义,用于指定像素着色器的用于渲染目标

的输出。其 Demo 运行的截屏如图 7.1 所示。

清单 7.1:Color Inversion Demo 的 HLSL 着色器代码

Texture2D colorMap : register( t0 );

SamplerState colorSampler : register( s0 );

cbuffer cbChangesEveryFrame : register( b0 )

{

matrix worldMatrix;

};

cbuffer cbNeverChanges : register( b1 )

{

matrix viewMatrix;

};

cbuffer cbChangeOnResize : register( b2 )

{

matrix projMatrix;

};

struct VS_Input

{

float4 pos : POSITION;

float2 tex0 : TEXCOORD0;

};

struct PS_Input

{

float4 pos : SV_POSITION;

float2 tex0 : TEXCOORD0;

};

PS_Input VS_Main( VS_Input vertex )

{

PS_Input vsOut = ( PS_Input )0;

vsOut.pos = mul( vertex.pos, worldMatrix );

vsOut.pos = mul( vsOut.pos, viewMatrix );

vsOut.pos = mul( vsOut.pos, projMatrix );

vsOut.tex0 = vertex.tex0;

return vsOut;

}

float4 PS_Main( PS_Input frag ) : SV_TARGET

{

return 1.0f - colorMap.Sample( colorSampler, frag.tex0 );

}

technique11 ColorInversion

{

pass P0

{

SetVertexShader( CompileShader( vs_5_0, VS_Main() ) );

SetGeometryShader( NULL );

SetPixelShader( CompileShader( ps_5_0, PS_Main() ) );

}

}

插图 7.1:Color Inversion Demo 程序截屏

Demo 类使用了一个类型为 ID3DX11Effect 的 effect 对象(见清单 7.2),和用于载入 effect 的代码见于清单 7.3 中

的函数 LoadContent,这里只显示用于载入 effect 的代码和用于创建输入布局的代码,因为函数的其余部分代码与

CubeDemo 中的一样。

清单 7.2:Color Inversion Demo 的类定义

#include"Dx11DemoBase.h"

#include<xnamath.h>

#include<d3dx11effect.h>

class ColorInversionDemo : public Dx11DemoBase

{

public:

ColorInversionDemo( );

virtual ~ColorInversionDemo( );

bool LoadContent( );

void UnloadContent( );

void Update( float dt );

void Render( );

private:

ID3DX11Effect* effect_;

ID3D11InputLayout* inputLayout_;

ID3D11Buffer* vertexBuffer_;

ID3D11Buffer* indexBuffer_;

ID3D11ShaderResourceView* colorMap_;

ID3D11SamplerState* colorMapSampler_;

XMMATRIX viewMatrix_;

XMMATRIX projMatrix_;

};

清单 7.3:Color Inversion 的载入函数 LoadContent 的最新代码

bool ColorInversionDemo::LoadContent( )

{

ID3DBlob* buffer = 0;

bool compileResult = CompileD3DShader( "ColorInversion.fx", 0,"fx_5_0", &buffer );

if( compileResult == false )

{

DXTRACE_MSG( "Error compiling the effect shader!" );

return false;

}

HRESULT d3dResult;

d3dResult = D3DX11CreateEffectFromMemory( buffer->GetBufferPointer( ),

buffer->GetBufferSize( ), 0, d3dDevice_, &effect_ );

if( FAILED( d3dResult ) )

{

DXTRACE_MSG( "Error creating the effect shader!" );

if( buffer )

buffer->Release( );

return false;

}

D3D11_INPUT_ELEMENT_DESC solidColorLayout[] =

{

{ "POSITION", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 0,

D3D11_INPUT_PER_VERTEX_DATA, 0 },

{ "TEXCOORD", 0, DXGI_FORMAT_R32G32_FLOAT, 0, 12,

D3D11_INPUT_PER_VERTEX_DATA, 0 }

};

unsigned int totalLayoutElements = ARRAYSIZE( solidColorLayout );

ID3DX11EffectTechnique* colorInvTechnique;

colorInvTechnique = effect_->GetTechniqueByName( "ColorInversion" );

ID3DX11EffectPass* effectPass = colorInvTechnique->GetPassByIndex( 0 );

D3DX11_PASS_SHADER_DESC passDesc;

D3DX11_EFFECT_SHADER_DESC shaderDesc;

effectPass->GetVertexShaderDesc( &passDesc );

passDesc.pShaderVariable->GetShaderDesc(passDesc.ShaderIndex, &shaderDesc);

d3dResult = d3dDevice_->CreateInputLayout( solidColorLayout,

totalLayoutElements, shaderDesc.pBytecode,

shaderDesc.BytecodeLength, &inputLayout_ );

buffer->Release( );

if( FAILED( d3dResult ) )

{

DXTRACE_MSG( "Error creating the input layout!" );

return false;

}

. . .

}

清单 7.3 中,我们使用之前的编译函数 CompileD3DShaper 来编译 Effect 文件,这里我们没有指定一个函数入口

点名字,而是使用“fx_5_0”,这里 fx 代表 Effect 文件,就好比“vs_5_0”表示顶点着色器,“ps_5_0”表示像素着

色器一样。

当我们创建输入布局时,必须使用一个 Effect 文件中的顶点着色器来指定具体的输入布局。为了这样做,首先

我们获得指向一项技术的指针,该 technique(技术)有我们希望用于输入布局的具体的顶点着色器,而该指针运行我

们访问 technique 的 passes 过程。因为每个过程能够使用不同的顶点着色器,所以我们必须获得指向用于输入布局

的 pass 过程指针。通过该 pass 过程,我们可以调用函数 GetVertexShaderDesc 来获得用于该 pass 过程的顶点着色器

的描述对象,随后调用该对象的函数 GetShaderDesc 就可以取得顶点着色器的字节码和字节大小。通过取得的顶点

着色器字节码和字节大小就可以创建输入布局。

最后一个有修改代码的函数,允许使用该 Demo 中的 Effect 文件的是渲染代码,见于清单 7.4。在函数 Render

中,我们可以通过使用不同的 Effect 的变量对象来设置着色器中的变量,例如 ID3DX11EffectShaderResourceVariable

用于着色器的资源变量,ID3DX11EffectSamplerVariable 用于采样对象,而 ID3DX11EffectMatrixVariable 用于矩阵,等

等。

为了获得变量的指针,我们可以调用函数 GetVariableByName(或 GetVariableByIndex)得到。可以调用后缀函数

“AsType”将变量类型转化为我们知道的类型。例如,“AsShaderResource”将变量转为一个着色器资源,“AsSampler”

将变量转为一个采样对象,“AsMatrix”将变量转为一个矩阵对象,等等。

一旦我们获得这些变量的指针,我们就可以调用变量的函数对它进行数据绑定 (例如,调用变量类型

ID3DX11EffectMatrixVariable 的函数 SetMatrix 来将矩阵数据设置给它)。一旦我们设置好着色器变量,我们就可以获

得用于渲染的 technique 对象指针,并且遍历 technique 的 pass 过程来绘制网格的几何图形。渲染函数 Render 见于

清单 7.4。

清单 7.4:Color Inversion Demo 的渲染函数

void ColorInversionDemo::Render( )

{

if( d3dContext_ == 0 )

return;

float clearColor[4] = { 0.0f, 0.0f, 0.25f, 1.0f };

d3dContext_->ClearRenderTargetView( backBufferTarget_, clearColor );

d3dContext_->ClearDepthStencilView( depthStencilView_, D3D11_CLEAR_DEPTH, 1.0f, 0 );

unsigned int stride = sizeof( VertexPos );

unsigned int offset = 0;

d3dContext_->IASetInputLayout( inputLayout_ );

d3dContext_->IASetVertexBuffers( 0, 1, &vertexBuffer_, &stride, &offset );

d3dContext_->IASetIndexBuffer( indexBuffer_, DXGI_FORMAT_R16_UINT, 0 );

d3dContext_->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST);

XMMATRIX rotationMat = XMMatrixRotationRollPitchYaw( 0.0f, 0.7f, 0.7f );

XMMATRIX translationMat = XMMatrixTranslation( 0.0f, 0.0f, 6.0f );

XMMATRIX worldMat = rotationMat * translationMat;

ID3DX11EffectShaderResourceVariable* colorMap;

colorMap = effect_->GetVariableByName( "colorMap" )->AsShaderResource( );

colorMap->SetResource( colorMap_ );

ID3DX11EffectSamplerVariable* colorMapSampler;

colorMapSampler = effect_->GetVariableByName("colorSampler")->AsSampler( );

colorMapSampler->SetSampler( 0, colorMapSampler_ );

ID3DX11EffectMatrixVariable* worldMatrix;

worldMatrix = effect_->GetVariableByName( "worldMatrix" )->AsMatrix( );

worldMatrix->SetMatrix( ( float* )&worldMat );

ID3DX11EffectMatrixVariable* viewMatrix;

viewMatrix = effect_->GetVariableByName( "viewMatrix" )->AsMatrix( );

viewMatrix->SetMatrix( ( float* )&viewMatrix_ );

ID3DX11EffectMatrixVariable* projMatrix;

projMatrix = effect_->GetVariableByName( "projMatrix" )->AsMatrix( );

projMatrix->SetMatrix( ( float* )&projMatrix_ );

ID3DX11EffectTechnique* colorInvTechnique;

colorInvTechnique = effect_->GetTechniqueByName( "ColorInversion" );

D3DX11_TECHNIQUE_DESC techDesc;

colorInvTechnique->GetDesc( &techDesc );

for( unsigned int p = 0; p < techDesc.Passes; p++ )

{

ID3DX11EffectPass* pass = colorInvTechnique->GetPassByIndex( p );

if( pass != 0 )

{

pass->Apply( 0, d3dContext_ );

d3dContext_->DrawIndexed( 36, 0, 0 );

}

}

swapChain_->Present( 0, 0 );

}

最后需要注意的是:Direct3D 11 没有在像其它 include 和 lib 同样目录下提供的支持 Effect 的代码。你可以在

DirectX SDK 目录下的 Samples\C++\Effects11\Inc 中找到 d3dx11effect.h,和在 Samples\C++\Effects 11 中用于构建的

Effects11_2010.sln 的解决方案(假设你用的是 VS2010)。该解决方案用于构建在 Direct3D 中链接使用 Effect 文件的静

态库。这的确是首先额外做的一些工作,但是为了使用 Direct3D 11 这必须要做,并且只需做一次,以后的所有工程

都可以使用。

Color Shifting

接下来我们要创建另一个简单的像素着色器 Effect,用于改变渲染表面的颜色成分,该 Demo 位于

Chapter7/ColorShift 目录。该 Demo 的代码同样沿袭于第六章的 Cube Demo,其不同的地方就是使用像素着色器。

该 Effect 效果,我们只是对贴图的采样颜色分量进行简单的置换,使得 Red = Blue,Blue = Green,和 Green = Red。

为了在像素着色器中达到目的,首先需要取得贴图采样颜色,我们通过创建一个 float4 对象来存储新的置换后的颜

色。该 Demo 的 Effect 文件见于清单 7.5,其程序截图见于插图 7.2。

清单 7.5:Color Shift Demo 的 HLSL 着色器

Texture2D colorMap : register( t0 );

SamplerState colorSampler : register( s0 );

cbuffer cbChangesEveryFrame : register( b0 )

{

matrix worldMatrix;

};

cbuffer cbNeverChanges : register( b1 )

{

matrix viewMatrix;

};

cbuffer cbChangeOnResize : register( b2 )

{

matrix projMatrix;

};

struct VS_Input

{

float4 pos : POSITION;

float2 tex0 : TEXCOORD0;

};

struct PS_Input

{

float4 pos : SV_POSITION;

float2 tex0 : TEXCOORD0;

};

PS_Input VS_Main( VS_Input vertex )

{

PS_Input vsOut = ( PS_Input )0;

vsOut.pos = mul( vertex.pos, worldMatrix );

vsOut.pos = mul( vsOut.pos, viewMatrix );

vsOut.pos = mul( vsOut.pos, projMatrix );

vsOut.tex0 = vertex.tex0;

return vsOut;

}

float4 PS_Main( PS_Input frag ) : SV_TARGET

{

float4 col = colorMap.Sample( colorSampler, frag.tex0 );

float4 finalCol;

finalCol.x = col.y;

finalCol.y = col.z;

finalCol.z = col.x;

finalCol.w = 1.0f;

return finalCol;

}

technique11 ColorShift

{

pass P0

{

SetVertexShader( CompileShader( vs_5_0, VS_Main() ) );

SetGeometryShader( NULL );

SetPixelShader( CompileShader( ps_5_0, PS_Main() ) );

}

}

插图 7.2:Color Shift Demo 程序截图

Multitexturing

最后一个 Demo,我们创建多重纹理效果,该 Demo 位于 Chapter7/Multitexture 目录。多重纹理效果是在一个

物体表面显示 2 张纹理图像。我们可以通过对两者的纹理图像采样,使用 color1×color2 作为最终的像素颜色值。

多重纹理是一个十分有用的技术。有时我们需要表现诸如光照贴图,阴影贴图,细节贴图等,就可以一次使用

多张纹理图像来获得最终效果。

在多重纹理 Demo 中,我们载入两张纹理图像,并且都传递给我们的着色器,就像之前处理一张纹理那样。清

单 7.6 显示在 MultiTextureDemo 类中添加了用于处理第二张纹理的资源视图。Demo 中,我们只需简单的拷贝用于

第 1 张纹理的代码,处理第 2 张纹理。在着色器中,我们简单的对两张纹理图像采样,并且返回颜色乘积作为最终

渲染效果。注意一点:在 HLSL 文件中,第二张纹理使用 t1 变量绑定,第一张使用 t0 变量绑定,见于清单 7.7。程

序截图见于插图 7.3。

插图 7.3:Multitexture Demo 截图

清单 7.6:增加了第二张贴图的 Multitexture Demo 类

class MultiTextureDemo : public Dx11DemoBase

{

public:

MultiTextureDemo( );

virtual ~MultiTextureDemo( );

bool LoadContent( );

void UnloadContent( );

void Update( float dt );

void Render( );

private:

ID3DX11Effect* effect_;

ID3D11InputLayout* inputLayout_;

ID3D11Buffer* vertexBuffer_;

ID3D11Buffer* indexBuffer_;

ID3D11ShaderResourceView* colorMap_;

ID3D11ShaderResourceView* secondMap_;

ID3D11SamplerState* colorMapSampler_;

XMMATRIX viewMatrix_;

XMMATRIX projMatrix_;

};

清单 7.7:Multitexture Demo 的 HLSL 源代码

Texture2D colorMap : register( t0 );

Texture2D secondMap : register( t1 );

SamplerState colorSampler : register( s0 );

cbuffer cbChangesEveryFrame : register( b0 )

{

matrix worldMatrix;

};

cbuffer cbNeverChanges : register( b1 )

{

matrix viewMatrix;

};

cbuffer cbChangeOnResize : register( b2 )

{

matrix projMatrix;

};

struct VS_Input

{

float4 pos : POSITION;

float2 tex0 : TEXCOORD0;

};

struct PS_Input

{

Pixel Shaders 313

float4 pos : SV_POSITION;

float2 tex0 : TEXCOORD0;

};

PS_Input VS_Main( VS_Input vertex )

{

PS_Input vsOut = ( PS_Input )0;

vsOut.pos = mul( vertex.pos, worldMatrix );

vsOut.pos = mul( vsOut.pos, viewMatrix );

vsOut.pos = mul( vsOut.pos, projMatrix );

vsOut.tex0 = vertex.tex0;

return vsOut;

}

float4 PS_Main( PS_Input frag ) : SV_TARGET

{

float4 col = colorMap.Sample( colorSampler, frag.tex0 );

float4 col2 = secondMap.Sample( colorSampler, frag.tex0 );

return col * col2;

}

几何着色器(Geometry Shaders)

几何着色器比之前我们所使用的其它着色器复杂一点点。它不像顶点和像素着色器,几何着色器可以输出比输

入多或少一些。而顶点着色器必须输入一个单一的顶点并且输出一个单一的顶点,像素着色器类似这样。而几何着

色器,可以移除或增加这些通过该阶段的管线的顶点。如果你想基于一些标准来裁剪几何图形,这一点是很有用的,

或者你想通过细分曲面(tessellation)来增加物体的细节(分辨率)。

Effect 文件中,几何着色器在顶点和像素着色器阶段之间。因为几何着色器是可选的,所以你时常看到在 effect

的 technique 块中将其设置为 NULL 值。当需要几何着色器时,可以像顶点和像素着色器一样设置其几何着色器。

下面代码给出了几何着色器的工作示例。它包含完整的几何着色器函数,并且附加有使用的结构和常量缓存。

下面的几何着色器的目的就是从顶点列表中接收输入的单个顶点,产生一个完全的三角形发送给像素着色器:

cbuffer TriangleVerts

{

float3 triPositions[3] =

{

float3( -0.25, 0.25, 0 ),

float3( 0.25, 0.25, 0 ),

float3( -0.25, -0.25, 0 )

};

};

struct VS_OUTPUT

{

float4 Pos : SV_POSITION;

float4 Color : COLOR0;

};

[maxvertexcount(3)]

void GS( point VS_OUTPUT input[1], inout TriangleStream<VS_OUTPUT> triangleStream )

{

VS_OUTPUT psInput;

for( int i = 0; i < 3; i++ )

{

float3 position = triPositions[i];

position = position + input[0].Pos;

psInput.Pos = mul( float4( position, 1.0f ), Projection );

psInput.Color = input[0].Color;

triangleStream.Append( psInput );

}

}

几何着色器函数声明(Geometry Shader Function Declaration)

几何着色器的声明与之前的顶点和像素着色器声明有稍微的不同。代替之前的用于函数目的的返回类型,该着

色器的输出顶点也在参数列表中,着色器自身返回类型为 void。每个几何着色器都需要指明它返回的顶点数量,并

且通过在函数上方使用关键字maxvertexcount来指定。下述函数的含义就是返回单一的三角形,所有需要三个顶点:

[maxvertexcount(3)]

void GS( point VS_OUTPUT input[1], inout TriangleStream<VS_OUTPUT> triangleStream )

几何着色器有两个参数,第一个是输入参数——顶点数组。传输到该着色器中的几何图形类型是基于在应用程

序代码中所使用的拓扑结构。因此上述示例使用一个顶点列表,传入到该着色器中的几何图形类型是顶点,并且它

们都只是数组中的一项。如果应用程序使用一个三角形列表,则其第一个参数类型就为 triangle,则数组中每一项

有三个顶点。

第二个参数是一个流对象。流对象是一个用于输出的顶点列表,并且将其结果传入下一个着色器阶段。该顶点

列表所使用的结构格式必须是与像素着色器输入格式一样。在几何着色器中所创建的几何图形类型,可以使用三种

流对象:PointStream,TriangleStream 和 LineStream。

当向流对象中增加顶点时,有时需要在末尾以带状形式创建。此时,可以调用函数 restartstrip 来完成。当用于

创建一系列的内部相连的三角形时,该方式就十分有用。

几何着色器的说明(The Geometry Shader Explained)

前面的几何着色器示例中,着色器通过传递来的点位置(point)列表产生三个顶点(vertices)用于组成三角形。通

过初始化顶点的位置属性和结合在 triPositions 变量中的顶点位置来创建顶点。该 triPositions 变量持有用于创建三角

形位置的三个点位置列表。

因为着色器创建的每个三角形都要求三个顶点,所以在着色器内部遍历三次,来产生三角形的新的顶点。而三

角形的最终位置则需要通过一个投影矩阵变换来产生。在每个最终顶点创建后就增加到三角形流对象中。

光照介绍(Introduction to Lighting)

该部分,我们将快速的浏览一下 Direct3D 中的光照主题。这部分的 Demo 是基于后面第八章中的 Models Demo,

来显示光照在复杂物体上的效果,但是我们会延迟对 Demo 中模型载入部分的讲解,直到读者学习到下一章的

“Models”部分。本小节,我们将讨论光照的产生以及使用着色器取得光照。一旦你结束第八章的学习,你就可以

看到该 Demo 的代码是怎样联系在一起的。

游戏中的实时光照在着色器中的表现,就是光照方程在顶点着色器中进行逐顶点光照执行,而在像素着色器中

表现为逐像素光照。因为通常像素比顶点多,这些像素彼此之间十分靠近,所以在像素着色器上执行光照方程得到

的光照品质会比顶点着色器上获得的更加杰出。

现在有一定数量的算法用于表现光照。该小节,我们只使用一种简单的算法来表现光照效果,该方法基于之前

Direct3D 版本的固定函数管线的标准的光照模型。

通常,计算机图形学中的渲染方程是极其复杂的,数学技巧性很强,并且你还需要做一些研究,如果你开始迈

向更高级的光照主题,比如全局光照和阴影。但是本章中,我们通常只考虑光照方程的三个最常用的部分。这就是

众所周知的环境(ambient)光,散射(diffuse)光,和镜面(specular)光这三项。因为这是一本入门书籍,我们将以最容

易理解的方式介绍这些概念,使得读者至少可以在之后使用。

环境光(The Ambient Term)

顾名思义,环境光用于模拟光照在周围环境中的反射效果,在物体表面进行着色的现象。这是一个光照方程的

一个基本项,尽管它只是一个用于增加总光照效果的颜色值,可以使用 float4 表示,例如(0.3f,0.3f,0.3f,1.0f)。

实际上,该基本项只不过是让一个实体颜色在场景中轻微的发亮,看上去的效果十分的不真实——其例子见于插图

7.4。

插图 7.4:只有环境光

更复杂的光照方程,例如各种各样的全局光照算法,和其它的一些技术比如环境遮挡,可以表现反射光照的高

度真实和复杂的效果,但是这些方法不用于实时模拟。

散射光(The Diffuse Term)

光照方程的散射项部分,用来模拟光照在物体表面和观察者眼睛之间的反射效果。这与之前的光照首先在环境

中的物体之间反射并最后对物体表面进行着色的效果是不同的,散射光照项科研用来模拟多种不同的现象,例如全

局光照的混合,柔和的阴影等等。

散射项表示光照强度部分,用于调制表面颜色(或者表面纹理,其散射项光照颜色有点像纯白色。)的明暗变化,

很适合用在实时游戏中。先不谈距离这一影响光照强度的因素,如果光照直接朝向物体,则物体接收到的光照强度

为全部光照;如果光照方向朝向物体背面,则物体不受光照影响;如果光照方向与物体有一个角度,物体表面则接

收部分光照,并且随角度减少,光照将平行于物体表面。其效果见于插图 7.5。

插图 7.5:光照与物体表面有一定的角度

为了表现散射光照效果,我们可以使用一个基于计算两向量点积概念的方程。我们取得物体表面的法线向量和

光照方向向量,进行点积运算就给出了散射光强度。表面的法线向量可以使用所组成的三角形的法线向量表示,而

光照向量可以通过计算灯光位置和接收光照的顶点的位置(或像素的位置,当进行逐像素光照时)之差来得到。

重新观察一下插图 7.5 可得,如果灯光的位置在接收光照顶点的正上方,则光照向量和表面法线向量之间的点

积等于 1.0。因为它们的移动方向都朝向对方(你可以通过第六章中的数学部分的知识来计算点积)。如果灯光在物体

的背面,则向量方向与之前的反向,将导致点积结果小于 0。所有其它的角度的点积结果在上界(1.0)和平行物体表

面的情形(0.0)之间时,将会有一个光照强度的变化。我们可以直接通过散射项的值与物体表面颜色相乘,来表现散

射光照对物体表面的影响。其散射光照方程如下:

float diffuse = clamp( dot( normal, lightVec ), 0.0, 1.0 );

如果我们使用的光照颜色仅仅是表现纯光照强度(译者注:颜色分量都相同,表现为一个单值),则有如下方程:

float diffuse = clamp( dot( normal, lightVec ), 0.0, 1.0 );

float4 diffuseLight = lightColor * diffuse;

如果将散射光照颜色应用到物体表面颜色中,假设物体表面的颜色仅仅是来自于一个颜色贴图,则方程如下:

float diffuse = clamp( dot( normal, lightVec ), 0.0, 1.0 );

float4 diffuseLight = lightColor * diffuse;

float4 finalColor = textureColor * diffuseLight;

我们可以固定散射项,因为我们认为任何小于或等于 0.0 的散射项会使得当将散射项部分与光照颜色相乘时,

或者之间作为表面颜色,会使得最后的散射颜色为黑色,这表示没有散射颜色。我们不想由于散射项的负值以一种

我们不期望的形式影响光照方程的其它部分。一个只有散射光照的例子见于插图 7.6。

镜面光(Specular Term)

镜面光照类似于散射光照,只不过镜面光照模拟灯光的镜面反射,即光线在物体表面反射并且进入眼睛。散射

光照发生在粗糙的物体表面(漫反射),即当放大表面时有很多的崎岖不平的地方,将会导致光照反射到任意的方向。

这就是为什么无论你是否旋转一个发生漫反射的物体或从任意角度观察它,其光照强度基本一样。

而镜面光照,发生在光滑的物体表面,它将导致光线反射到其镜面的方向。在光滑的表面,可以观察到更加刺

眼(强烈光照)的光照反射。一个例子就是发亮的光滑金属表面。由于该表面十分光滑,将导致光线反射比漫反射更

加强烈。计算机图形学中,这种现象可以创建所看到的光亮的物体。在非光亮或非光滑的物体表面,镜面光照效果

很低或者不存在。当我们模拟物体表面时,必须正确的混合散射光照和镜面光照,来创建真实可信的效果。例如,

一块面包就没有一个金属球那么光亮。在现实生活中的镜子是如此的光滑使得我们可以在镜子表面看到我们自己完

整的镜像。另一个例子是,观察软饮料瓶子,可以发现如果你在手中旋转瓶子,则高亮部分看起来像是在旋转表面

移动和光源好像在改变一样。

插图 7.6:只有散射光照

从散射光照和镜面光照的描述中可以看到,散射光照独立于观察视角,而镜面光照则依赖于观察视角。这意味

着表现镜面光照的方程将使用照相机向量来代替散射光照中的光照方向向量。照相机方向向量可以如下计算:

float4 cameraVec = cameraPosition – vertexPosition;

从照相机方向向量中,我们可以创建之前提到过的 half vector。可以使用如下方程计算镜面光照的影响:

float3 halfVec = normalize( lightVec + cameraVec );

float specularTerm = pow( saturate( dot( normal, halfVec ) ), 25 );

只使用镜面光照的一个例子见于插图 7.7。

插图 7.7:只使用镜面光照

结合三种光照(Putting It All Together)

Lighting Demo 可以在 Chapter7/Lighting 目录中找到。在该 Demo 中,我们在像素着色器中使用环境光照,散射

光照和镜面光照三部分的合成效果来表现真实的光照现象。该 Demo 本质上是在第八章中的 Models Demo 之上增加

光照效果。注意本章中我们只讨论着色器部分,下一章中再来讨论 Demo 是怎样从文件中加载和渲染 3D 模型的。

用于表现光照效果的 HLSL 着色器代码见于清单 7.8。在顶点着色器中,我们变换输入的顶点的位置,来计算输

出顶点的位置,通过 3×3 的世界矩阵来完成变换。我们这的标准变换是因为最后真正的标准变换依赖于物体的旋

转操作。必须通过标准变换来获得正确的结果。

清单 7.8:光照部分的 HLSL 着色器代码

Texture2D colorMap : register( t0 );

SamplerState colorSampler : register( s0 );

cbuffer cbChangesEveryFrame : register( b0 )

{

matrix worldMatrix;

};

cbuffer cbNeverChanges : register( b1 )

{

matrix viewMatrix;

};

cbuffer cbChangeOnResize : register( b2 )

{

matrix projMatrix;

};

cbuffer cbCameraData : register( b3 )

{

float3 cameraPos;

};

struct VS_Input

{

float4 pos : POSITION;

float2 tex0 : TEXCOORD0;

float3 norm : NORMAL;

};

struct PS_Input

{

float4 pos : SV_POSITION;

float2 tex0 : TEXCOORD0;

float3 norm : NORMAL;

float3 lightVec : TEXCOORD1;

float3 viewVec : TEXCOORD2;

};

PS_Input VS_Main( VS_Input vertex )

{

PS_Input vsOut = ( PS_Input )0;

float4 worldPos = mul( vertex.pos, worldMatrix );

vsOut.pos = mul( worldPos, viewMatrix );

vsOut.pos = mul( vsOut.pos, projMatrix );

vsOut.tex0 = vertex.tex0;

vsOut.norm = mul( vertex.norm, (float3x3)worldMatrix );

vsOut.norm = normalize( vsOut.norm );

float3 lightPos = float3( 0.0f, 500.0f, 50.0f );

vsOut.lightVec = normalize( lightPos - worldPos );

vsOut.viewVec = normalize( cameraPos - worldPos );

return vsOut;

}

float4 PS_Main( PS_Input frag ) : SV_TARGET

{

float3 ambientColor = float3( 0.2f, 0.2f, 0.2f );

float3 lightColor = float3( 0.7f, 0.7f, 0.7f );

float3 lightVec = normalize( frag.lightVec );

float3 normal = normalize( frag.norm );

float diffuseTerm = clamp( dot( normal, lightVec ), 0.0f, 1.0f );

float specularTerm = 0;

if( diffuseTerm > 0.0f )

{

float3 viewVec = normalize( frag.viewVec );

float3 halfVec = normalize( lightVec + viewVec );

specularTerm = pow( saturate( dot( normal, halfVec ) ), 25 );

}

float3 finalColor = ambientColor + lightColor *

diffuseTerm + lightColor * specularTerm;

return float4( finalColor, 1.0f );

}

在像素着色器中,我们使用一个常量表示环境项部分,而执行 N 与 L 的点积(表面法线 Normal 和光照 Light 向

量)来得到散射项部分,而执行 N 与 H 的点积(法线向量和半 half 矢量)来得到镜面项部分。通过这三个部分之和来得

到最终的光照颜色。注意这里我们假设使用白色表示散射颜色和镜面颜色,如果你想使用其它颜色代替,可以自行

修改来查看输出效果。作为一个额外的练习,你需要使用常量缓存变量来表示光照颜色,照相机位置,和灯光位置,

使得可以通过键盘来操纵这些值,查看它们在实时环境改变的效果。

章末总结(Summary)

你现在至少熟悉了着色器编程的基本概念和使用它们的好处。学习着色器编程的最好的方式是不断的操纵(修改,

玩耍)你已经写好着色器代码,来查看修改后的效果。有时一个微小的改变可以有意想不到的效果。本章提供了一个

关于着色器知识的简略的参考。HLSL 着色器语言可以做很多事情,要想精通 HLSL 就需要不断的练习和实验。

你所学到的(What You Have Learned)

怎样编写顶点,像素,几何着色器

怎样使用 HLSL

在场景中怎样使用光照

章末习题(Chapter Questions)

答案见于附录 A。

1. 使用什么函数载入 Effect 文件?

2. 什么是 HLSL?

3. 几何着色器的目的是什么?

4. 域和外壳着色器的目的是什么?

5. 光栅化能够操纵的两种模型是什么?

6. 定义语义 semantics 这个概念

7. 什么是计算着色器,可以使用计算着色器的最低 DirectX 版本是哪个版本?

8. 定义 HLSL 的技术 techniques 和过程 passes。

9. 什么是固定函数管线?How does Direct3D 11 make use of it?

10. 细分曲面单元有何目的?

自己动手(On Your Own)

1. 实现一个有单一的 technique 和 pass 的 Effect 文件,并且用它渲染一个物体。

2. 重构之前所有的“On Your Own”中的 Demo,在 demo 中使用单一的 Effect 文件代替之前的着色器。对于

每个 effect 创建不同的 technique 块。而在应用程序里面,允许用户通过方向键切换用于渲染的 technique。

3. 修改光照 Demo,允许可以通过键盘上的方向键来移动光源的位置。为了达成目标,你需要通过一个常量

缓存发送光源位置给着色器。

第八章 Direct3D 中的照相机和模型(Cameras And Models In Direct 3D)

3D 场景中的照相机主题经常被忽略。游戏场景中照相机是一个十分重要的参与者,并且以现在的游戏眼光来

看,照相机已经相当的复杂。游戏制作的成功与否,照相机这个部分是十分的关键,因为如果照相机做得很烂,玩

家很容易给一个很差的差评。

本章中,我们简略探讨两个主题,其一就是创建两种不同类型的照相机,其二就是展示怎样从文件中载入 3D

模型。

本章你将学到的知识:

怎样创建一个 look-at 照相机

怎样创建一个弧度旋转照相机

怎样冲 OBJ 格式文件中载入模型

Direct3D 中的照相机(Cameras In Direct3D)

游戏中,我们创建一个视图矩阵来代表虚拟的照相机。该视图矩阵,可以使用 XNA 数学中的函数

XMMatrixLookAtLH(右手坐标系版本为 XMMatrixLookAtRH)来创建,其原型如下:

XMMATRIX XMMatrixLookAtLH(

XMVECTOR EyePosition,

XMVECTOR FocusPosition,

XMVECTOR UpDirection

);

函数 XMMatrixLookAtLH 和函数 XMMatrixLookToLH 的区别就是,前一个函数的视角方向为一个朝向,而后一个

函数的观察目标为一个固定的点。当构建照相机时,我们的想法就是在游戏更新时控制照相机的各项性质,而当需

要从照相机的透视视角绘制时,我们产生三个向量传递给创建照相机的函数。

本章中,我们将创建固定的 look-at 照相机和一个围绕固定观察点旋转的照相机。

Look-At Camera Demo

固定的照相机(指观察方向固定)相对比较直观,它们就是放置在一个位置,并且朝一个方向观察。这里有两种

类型的固定照相机:位置固体的照相机和位置可以移动的照相机。位置固定的照相机是位于一个固定的位置。而动

态固定照相机是在游戏中的位置可以改变——例如游戏 Halo Reach,当玩家坠入悬崖时,其追逐照相机(译者注:玩

家视角)就变成固定照相机形式,其实就是玩家通过某一平面被游戏认定为已经死亡。

我们将会创建一个简单的固定照相机,即一个最基本的照相机系统。该 Demo 位于 Chapter8/LookAtCamera 目

录。Look-at 照相机需要一个自身位置,观察目标位置,和观察的方向。因为通常定义观察方向向上(0, 1, 0),所以

我们只需要一个照相机自身位置和观察目标的位置,就可以创建 LookAtCamera 类。该类的定义见清单 8.1。

清单 8.1:LookAtCamera.h 头文件

#include<xnamath.h>

class LookAtCamera

{

public:

LookAtCamera( );

LookAtCamera( XMFLOAT3 pos, XMFLOAT3 target );

void SetPositions( XMFLOAT3 pos, XMFLOAT3 target );

XMMATRIX GetViewMatrix( );

private:

XMFLOAT3 position_;

XMFLOAT3 target_;

XMFLOAT3 up_;

};

LookAtCamera 类十分的短小。第一个默认构造函数将所有成员向量的各分量初始化为 0,除了观察方向向量,

因为它被固定为(0, 1, 0)。如果我们使用该类的默认构造函数,我们将看到的视图与之前所有的 Demo 中一样。第二

个构造函数将会设置我们的照相机的位置和观察目标位置,而其成员函数 SetPositions 也用于同样的目的。

当使渲染场景时,为了使用照相机,我们通过调用成员函数 GetViewMatrix 来获得由函数 XMMatrixLookAtLH 设

置的照相机自身位置,观察目标位置,和观察方向向量的视图矩阵。因为 XMMatrixLookAtLH 函数要求的参数类型

为 XMVCTOR,我们使用函数 XMLoadFloat3 将 XMFLOAT3 类型转化为 XMVECTOR。如果我们想使用 XMMVECTOR 作

为我们的成员变量,我们必须考虑类的内存对齐,需要将代码以一种特定的顺序安排才可以通过正确的编译。

LookAtCamera 类的实现见于清单 8.2。

清单 8.2:LookAtCamera 类的实现

#include<d3d11.h>

#include"LookAtCamera.h"

LookAtCamera::LookAtCamera( ) : position_( XMFLOAT3( 0.0f, 0.0f, 0.0f ) ),

target_( XMFLOAT3( 0.0f, 0.0f, 0.0f ) ), up_( XMFLOAT3( 0.0f, 1.0f, 0.0f ) )

{}

LookAtCamera::LookAtCamera( XMFLOAT3 pos, XMFLOAT3 target ) :

position_( pos ), target_( target ), up_( XMFLOAT3( 0.0f, 1.0f, 0.0f ) )

{}

void LookAtCamera::SetPositions( XMFLOAT3 pos, XMFLOAT3 target )

{

position_ = pos;

target_ = target;

}

XMMATRIX LookAtCamera::GetViewMatrix( )

{

XMMATRIX viewMat = XMMatrixLookAtLH( XMLoadFloat3( &position_ ),

XMLoadFloat3( &target_ ), XMLoadFloat3( &up_ ) );

return viewMat;

}

Demo 的主类是 CameraDemo,构建于第六章的 3D CubeDemo 之上。不同之处在于,这里我们添加了固定照相

机到 Demo 中,从而在渲染之前需要从照相机处获得视图矩阵。有新添加的照相机类 CameraDemo 见于清单 8.3。

清单 8.3:CameraDemo 类

#include"Dx11DemoBase.h"

#include"LookAtCamera.h"

class CameraDemo : public Dx11DemoBase

{

public:

CameraDemo( );

virtual ~CameraDemo( );

bool LoadContent( );

void UnloadContent( );

void Update( float dt );

void Render( );

private:

ID3D11VertexShader* solidColorVS_;

ID3D11PixelShader* solidColorPS_;

ID3D11InputLayout* inputLayout_;

ID3D11Buffer* vertexBuffer_;

ID3D11Buffer* indexBuffer_;

ID3D11ShaderResourceView* colorMap_;

ID3D11SamplerState* colorMapSampler_;

ID3D11Buffer* viewCB_;

ID3D11Buffer* projCB_;

ID3D11Buffer* worldCB_;

XMMATRIX projMatrix_;

LookAtCamera camera_;

};

我们将在载入资源函数 LoadContent 中设置照相机。该 Demo 中的照相机位于 X 轴 3 个单位,Y 轴 3 个单位,Z

轴-12 个单位。这个位置允许物体出现在照相机的视野中,并且物体轻微的在屏幕上方,观察时有个小小的角度。

函数 LoadContent 见于清单 8.4。

清单 8.4:在 LoadContent 函数中设置照相机

bool CameraDemo::LoadContent( )

{

// ... Previous demo’s code ...

XMMATRIX projection = XMMatrixPerspectiveFovLH( XM_PIDIV4,

800.0f / 600.0f, 0.01f, 100.0f );

projection = XMMatrixTranspose( projection );

XMStoreFloat4x4( &projMatrix_, projection );

camera_.SetPositions( XMFLOAT3( 3.0f, 3.0f, -12.0f ),

XMFLOAT3( 0.0f, 0.0f, 0.0f ) );

return true;

}

最后一部分新代码在渲染函数 Render 中,这里我们调用照相机的成员函数 GetViewMatrix 来取得视图矩阵,并

且将其传递给视图矩阵常量缓存。这是与第六章的 3D Cube Demo 中不同的地方。Look-At Camera Demo 的截屏见于

插图 8.1。

清单 8.5:渲染函数 Render 中使用照相机

void CameraDemo::Render( )

{

if( d3dContext_ == 0 )

return;

float clearColor[4] = { 0.0f, 0.0f, 0.25f, 1.0f };

d3dContext_->ClearRenderTargetView( backBufferTarget_, clearColor );

d3dContext_->ClearDepthStencilView( depthStencilView_,

D3D11_CLEAR_DEPTH, 1.0f, 0 );

unsigned int stride = sizeof( VertexPos );

unsigned int offset = 0;

d3dContext_->IASetInputLayout( inputLayout_ );

d3dContext_->IASetVertexBuffers( 0, 1, &vertexBuffer_, &stride, &offset );

d3dContext_->IASetIndexBuffer( indexBuffer_, DXGI_FORMAT_R16_UINT, 0 );

d3dContext_->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST);

d3dContext_->VSSetShader( solidColorVS_, 0, 0 );

d3dContext_->PSSetShader( solidColorPS_, 0, 0 );

d3dContext_->PSSetShaderResources( 0, 1, &colorMap_ );

d3dContext_->PSSetSamplers( 0, 1, &colorMapSampler_ );

XMMATRIX worldMat = XMMatrixIdentity( );

worldMat = XMMatrixTranspose( worldMat );

XMMATRIX viewMat = camera_.GetViewMatrix( );

viewMat = XMMatrixTranspose( viewMat );

d3dContext_->UpdateSubresource( worldCB_, 0, 0, &worldMat, 0, 0 );

d3dContext_->UpdateSubresource( viewCB_, 0, 0, &viewMat, 0, 0 );

d3dContext_->UpdateSubresource( projCB_, 0, 0, & projMatrix_, 0, 0 );

d3dContext_->VSSetConstantBuffers( 0, 1, &worldCB_ );

d3dContext_->VSSetConstantBuffers( 1, 1, &viewCB_ );

d3dContext_->VSSetConstantBuffers( 2, 1, &projCB_ );

d3dContext_->DrawIndexed( 36, 0, 0 );

swapChain_->Present( 0, 0 );

}

插图 8.1:Look-At Camera Demo 程序截图

Arc-Ball Camera Demo

下一个照相机 Demo 是 arc-ball 照相机。这种照相机擅长编辑或观察游戏中的目标物体,和照相机需要在目标

物体周围进行球面旋转行为。该 Demo 的代码见于目录 Chapter8/ArcBallCamera。

该 Demo 中,我们只需做少量的事情。因为观察目标位置就是照相机的焦点位置。照相机自身的位置将会围绕

目标物体旋转,这意味着该位置在函数 GetViewMatrix 中需要计算。

我们需要为该类型的照相机设置的另一个性质是照相机与观察物体之间的距离和绕 X 轴旋转的控制。距离的作

用类似于缩放效果,允许我们靠近或远离目标物体的位置。而旋转控制允许我们旋转 180 度,从而可以从旋转的起

点旋转到它的反方向位置。

清单 8.6 显示了 ArcCamera 类。其成员有当前照相机与观察物体之间的距离,以及我们限制的照相机与物体位

置之间的最远和最近距离,和 X,Y 轴的旋转值。我们也可以增加旋转弧度的最大最小值来限制照相机的旋转。

清单 8.6:ArcCamera 类

#include<xnamath.h>

class ArcCamera

{

public:

ArcCamera( );

void SetDistance(float distance, float minDistance, float maxDistance);

void SetRotation( float x, float y, float minY, float maxY );

void SetTarget( XMFLOAT3& target );

void ApplyZoom( float zoomDelta );

void ApplyRotation( float yawDelta, float pitchDelta );

XMMATRIX GetViewMatrix( );

private:

XMFLOAT3 position_;

XMFLOAT3 target_;

float distance_, minDistance_, maxDistance_;

float xRotation_, yRotation_, yMin_, yMax_;

};

ArcCamera 类有一个默认构造函数,用于初始化观察目标位置为原点,而自身位置离目标位置两个单位,设置

旋转限制为 180 度(-90 到 90)。该限制允许我们可以在照相机旋转到下方之前,可以移动到最上方的位置。因为成

员函数 GetViewMatrix 将会计算照相机的位置,从而构造函数中只是简单的给一个默认值来代替其真实的位置。

另一个成员函数是 SetDistance,它将设置照相机的当前距离(与观察物体之间),也可以设置新的最小和最大距

离,函数 SetRotation 可以设置当前的 X 和 Y 的旋转值,也可以设置它们的旋转限制,而函数 SetTarget 用于设置观

察位置。这些函数的实现见于清单 8.7。

清单 8.7:ArcCamera 类中部分成员函数的实现

ArcCamera::ArcCamera( ) : target_( XMFLOAT3( 0.0f, 0.0f, 0.0f ) ),

position_( XMFLOAT3( 0.0f, 0.0f, 0.0f ) )

{

SetDistance( 2.0f, 1.0f, 10.0f );

SetRotation( 0.0f, 0.0f, -XM_PIDIV2, XM_PIDIV2 );

}

void ArcCamera::SetDistance( float distance, float minDistance, float maxDistance )

{

distance_ = distance;

minDistance_ = minDistance;

maxDistance_ = maxDistance;

if( distance_ < minDistance_ ) distance_ = minDistance_;

if( distance_ > maxDistance_ ) distance_ = maxDistance_;

}

void ArcCamera::SetRotation( float x, float y, float minY, float maxY )

{

xRotation_ = x;

yRotation_ = y;

yMin_ = minY;

yMax_ = maxY;

if( yRotation_ < yMin_ ) yRotation_ = yMin_;

if( yRotation_ > yMax_ ) yRotation_ = yMax_;

}

void ArcCamera::SetTarget( XMFLOAT3& target )

{

target_ = target;

}

接下来介绍用于移动的函数。首先是函数 ApplyZoom,该函数用于增加或减少照相机与观察点之间的距离,其

结果会限制在我们设置的最近和最远距离之间。函数 ApplyRotation 类似于同样的事情,只不过是操纵照相机绕 X

轴旋转,使得照相机向上移动或向下移动,并且也受到设置的旋转限制的约束。这些函数都接受所控制的参数的改

变量,这意味着这些控制参数会与输入的变量相加来改变值,而不是设置这些改变量为绝对距离或旋转。这允许我

们构建照相机的假想改变,直到通过函数 GetViewMatrix 来计算出最后的视图矩阵。

函数 GetViewMatrix,以及函数 ApplyZoom 和 ApplyRotation,见于清单 8.8,实现十分的明了,并且借助于 XNA

数学来完成。首先我们在局部空间创建一个位置变量,并且将在 Zoom 代码中调用该变量。假如没有发生旋转的话,

则该缩放位置就是我们照相机的最终位置。使用 XNA 数学和矩阵,通常用于我们将局部位置变换到世界空间的真实

位置,必须在照相机上执行旋转操作。我们旋转照相机,其实只是绕观察目标旋转照相机的位置而已。如果观察目

标有局部位置(0, 0, 0),并且我们的照相机的局部位置为(0, 0, distance),那么如果要变换我们的照相机到正确的世界

位置,我们必须先平移观察目标到到目标位置,再通过照相机的旋转矩阵绕局部位置旋转,最后平移已经旋转的照

相机位置到指定位置。平移只是一个简单的附加向量。

最后一步是,我们必须计算照相机的朝向。这可以简单的创建一个局部空间的朝向向量(0, 1, 0),再通过照相机

的旋转矩阵对它进行旋转,从而得到世界空间中的朝向向量。我们通过计算好的照相机位置,观察目标位置,和向

上的朝向,传递给函数 XMMatrixLookAtLH 来创建 arc-ball 照相机的视图矩阵。旋转矩阵可以通过调用函数

XMMatrixRotationRollPitchYaw(译者注:第六章中有介绍)来创建,该函数参数为 yaw,pitch,和 roll,返回创建好的

旋转矩阵。我们应用 X 和 Y 轴方向的旋转值给该函数,其余的创建工作将由 XNA 数学完成。

清单 8.8:函数 ApplyZoom,ApplyRotation,GetViewMatrix 的实现。

void ArcCamera::ApplyZoom( float zoomDelta )

{

distance_ += zoomDelta;

if( distance_ < minDistance_ ) distance_ = minDistance_;

if( distance_ > maxDistance_ ) distance_ = maxDistance_;

}

void ArcCamera::ApplyRotation( float yawDelta, float pitchDelta )

{

xRotation_ += yawDelta;

yRotation_ += pitchDelta;

if( xRotation_ < yMin_ ) xRotation_ = yMin_;

if( xRotation_ > yMax_ ) xRotation_ = yMax_;

}

XMMATRIX ArcCamera::GetViewMatrix( )

{

XMVECTOR zoom = XMVectorSet( 0.0f, 0.0f, distance_, 1.0f );

XMMATRIX rotation = XMMatrixRotationRollPitchYaw( xRotation_,

-yRotation_, 0.0f );

zoom = XMVector3Transform( zoom, rotation );

XMVECTOR pos = XMLoadFloat3( &position_ );

XMVECTOR lookAt = XMLoadFloat3( &target_ );

pos = lookAt + zoom;

XMStoreFloat3( &position_, pos );

XMVECTOR up = XMVectorSet( 0.0f, 1.0f, 0.0f, 1.0f );

up = XMVector3Transform( up, rotation );

XMMATRIX viewMat = XMMatrixLookAtLH( pos, lookAt, up );

return viewMat;

}

该 Demo 与 Look-At Camera Demo 大体上相同,少数不同地方是使用 ArcCamera 代替之前的照相机(见于清单 8.9)

和只指定照相机的默认距离带代替之前的照相机安装步骤,因为构造函数已经给予我们所需的值(见于清单 8.10)。

清单 8.9:Arc Camera Demo 的主类

#include"Dx11DemoBase.h"

#include"ArcCamera.h"

#include<XInput.h>

class CameraDemo2 : public Dx11DemoBase

{

public:

CameraDemo2( );

virtual ~CameraDemo2( );

bool LoadContent( );

void UnloadContent( );

void Update( float dt );

void Render( );

private:

ID3D11VertexShader* solidColorVS_;

ID3D11PixelShader* solidColorPS_;

ID3D11InputLayout* inputLayout_;

ID3D11Buffer* vertexBuffer_;

ID3D11Buffer* indexBuffer_;

ID3D11ShaderResourceView* colorMap_;

ID3D11SamplerState* colorMapSampler_;

ID3D11Buffer* viewCB_;

ID3D11Buffer* projCB_;

ID3D11Buffer* worldCB_;

XMMATRIX projMatrix_;

ArcCamera camera_;

XINPUT_STATE controller1State_;

XINPUT_STATE prevController1State_;

};

清单 8.10:在 LoadContent 函数中修改的照相机安装代码只有一行

camera_.SetDistance( 6.0f, 4.0f, 20.0f );

Arc Camera Demo 不仅构建于 Look-At Camera Demo(该 demo 是从第六章的 3D Cube demo 中修改而来)之上,而

且也借鉴了第五章中的 XInput Demo。在该 Demo 中,我们使用了 XInput 和一个 Xbox360 控制器来控制我们的照相

机围绕目标位置旋转观察。这些工作在 Update 函数中完成,见于清单 8.11。

更新函数 Update 首先获得设备的状态,如果设备没有接入 Xbox,则我们不能获得任何信息。接下来增加可由

控制器的 Back 按钮退出应用程序的代码。该代码部分不是必要的,但是是一个很好的尝试。接下来检查是否 B 按

钮按下,如果按下则将照相机慢慢远离观察目标;如果 A 按钮按下,则照相机将慢慢靠近观察目标。更新函数中,

使用 right thumb-stick(译者注:没有玩过 Xbox,sorry)来旋转照相机。这可以通过一个相当简单的方法来完成,如果

thumb-stick的 X和 Y 轴移动了一个有意义的量,则我们将 yaw(Y轴旋转)和 pitch(X 轴旋转)增加或减少一个适当的量。

Demo 中可以看到,要 sticks 至少移动 1000 个单位旋转才有所改变,因为如果使用太小的量就产生旋转会导致 stick

太灵敏。Arc Camera Demo 程序截图见插图 8.2。

清单 8.11:Demo 的更新函数 Update

void CameraDemo2::Update( float dt )

{

unsigned long result = XInputGetState( 0, &controller1State_ );

if( result != ERROR_SUCCESS )

{

return;

}

// Button press event.

if( controller1State_.Gamepad.wButtons & XINPUT_GAMEPAD_BACK )

{

PostQuitMessage( 0 );

}

// Button up event.

if( ( prevController1State_.Gamepad.wButtons & XINPUT_GAMEPAD_B ) &&

!( controller1State_.Gamepad.wButtons & XINPUT_GAMEPAD_B ) )

{

camera_.ApplyZoom( -1.0f );

}

// Button up event.

if( ( prevController1State_.Gamepad.wButtons & XINPUT_GAMEPAD_A ) &&

!( controller1State_.Gamepad.wButtons & XINPUT_GAMEPAD_A ) )

{

camera_.ApplyZoom( 1.0f );

}

float yawDelta = 0.0f;

float pitchDelta = 0.0f;

if( controller1State_.Gamepad.sThumbRY < -1000 ) yawDelta = -0.001f;

else if( controller1State_.Gamepad.sThumbRY > 1000 ) yawDelta = 0.001f;

if( controller1State_.Gamepad.sThumbRX < -1000 ) pitchDelta = -0.001f;

else if( controller1State_.Gamepad.sThumbRX > 1000 ) pitchDelta = 0.001f;

camera_.ApplyRotation( yawDelta, pitchDelta );

memcpy( &prevController1State_, &controller1State_, sizeof( XINPUT_STATE ) );

}

插图 8.2:Arc Camera Demo 截图

网格和模型(Meshes And Models)

本书中,我们已经创建过的最复杂的模型是一个立方体,它是通过我们手动指定其几何结构来完成的(第六章的

3D Cube Demo)。这个立方体其本质就是一个网格,也是你所创建的最简单的封闭体积的 3D 物体。一张网格,正如

你所回忆的那样,是一张有一个或多个多边形,材质,贴图等的几何物体。而一个模型,是多张网格的集合,它们

一起组成更大更多细节的实体。比如轮子,身体,门,窗等,这些由很多网格表面组成的模型。

手动指定这些最终物体的几何结构会是一个大问题,因为手动创建它们太复杂了,所以我们必须借助工具来创

建它们。该部分我们就简略介绍怎样从文件和创建这些模型的工具中载入模型。本章的最后一个 Demo 是 Models

Demo,可以在 Chapter8/Models 目录中找到。该 Demo 直接构建在本章前部分的 Arc Camera Demo 之上。

OBJ 文件格式(The OBJ File Format)

Wavefront 公司(译者注:就是 Alias Wavefront 公司,其大名鼎鼎的 Maya 软件就是该公司开发的,后该公司被

Autodesk 公司收购)制定的 OBJ 文件格式是一种文本文件,可以通过任何文本编辑器打开和编辑。此种格式相当简

单,它的布局首先通常是顶点列表,随后纹理坐标,顶点法线向量,和三角形索引。文件中每一行都是一块不同的

信息,每行开始字母表面后面部分所代表的含义。例如,注释行使用#符号作为开始字母,如下:

# 1104 triangles in group

顶点位置行的开始字母为 v。其后跟随 X,Y,Z 位置,以空格符分开。例子如下:v 0.000000 2.933333 -0.000000

纹理贴图坐标的开始字母为 vt,其后跟随两个浮点数值,而顶点的法线向量开始字母为 vn。例子如下:

vt 1.000000 0.916667

vn 0.000000 -1.000000 0.000000

三角形信息的开始行字母为 f,其后跟随三组数字。每组数字有三个索引值,以斜杠分开,这三个索引值分别

是顶点索引,贴图索引和法线索引。因为每个顶点位置,贴图坐标和法线在文件中是唯一的,所以 OBJ 中的对象信

息都是不同的,就像使用几何索引方式一样。当使用几何索引时,我们的一个索引用于表示一个顶点的所有属性(例

如,位置,贴图坐标,等),而这里在 OBJ 文件中,对于每个顶点属性使用分开的索引表示。一个 OBJ 文件中的三

角形索引表示如下,每个索引之间用”/”分开,而每组索引之间用空格分开:f 2/1/1 3/2/2 4/3/3

下面还有一些其它的 OBJ 文件的关键字。关键字 mtllib 用于指定在网格上使用的材质文件:mtllib Sphere.mtl

关键字 usemtl 用于指定随后的网格使用从 mtllib 文件中载入的指定材质:usemtl Material01

而关键字 g 表面开始一个新的网格:g Sphere02

从文件中读取符号(Reading Tokens from a File)

OBJ 文件是一种简单的文本文件。我们所需要读取的每块信息都在它的每一行上,这意味着我们需要编写代码

来解析文件中的每行文本,并且划分出文本中的更小的信息片段,再从这些片段中提取所需的信息。为了完成该工

作,我们创建一个简单的类做解析工作,返回文本文件中的信息。该类名 TokenStream,可以在 Chapter8/Models

目录中找到。清单 8.12 显示了 TokenStream.h 头文件。

清单 8.12:头文件 TokenStream.h

class TokenStream

{

public:

TokenStream( );

void ResetStream( );

void SetTokenStream( char* data );

bool GetNextToken( std::string* buffer, char* delimiters, int totalDelimiters );

bool MoveToNextLine( std::string *buffer );

private:

int startIndex_, endIndex_;

std::string data_;

};

TokenStream 对象将会存储文件的数据并且读取所在位置的当前片段信息。我们很快就可以看到这是怎么做的。

首先考察一下清单 8.13 中的构造函数,ResetStream 重置流函数和 SetTokenStream 函数。构造函数和 ResetStream

函数将当前读取位置设置为文件第一行,而函数 SetTokenStream 将会设置数据成员变量来存储文件中的文本。

清单 8.13:构造函数,ResetStream 和 SetTokenStream 函数

TokenStream::TokenStream( )

{

ResetStream( );

}

void TokenStream::ResetStream( )

{

startIndex_ = endIndex_ = 0;

}

void TokenStream::SetTokenStream( char *data )

{

ResetStream( );

data_ = data;

}

接下来是 TokenStream.cpp 文件中的两个辅助函数。这些函数简单的测试是否一个字符是定界符。一个定界字

符是分割文本的标记。拿如下文本为例,我们可以看到每个单词由一个空格分开,该空格就是定界符。

“Hello world. How are you?”

第一个函数 isValidIdentifier 简单的检查一个字符是否为数字,字母或者符号。这通常作为一个默认检查,而其

重载版本将会检查字符是否在一个定界符数组中。如果你使用该 Demo 来打开 spheres.obj 文件,你将看到该文件中

只有这些定界符:新行号符,空格,和/。函数 isValidIdentifier 见于清单 8.14。

清单 8.14:函数 isValidIdentifier

bool isValidIdentifier( char c )

{

// Ascii from ! to ~.

if( ( int )c > 32 && ( int )c < 127 )

return true;

return false;

}

bool isValidIdentifier( char c, char* delimiters, int totalDelimiters )

{

if( delimiters == 0 || totalDelimiters == 0 )

return isValidIdentifier( c );

for( int i = 0; i < totalDelimiters; i++ )

{

if( c == delimiters[i] )

return false;

}

return true;

}

接下来考察的函数是 GetNextToken。该函数循环检查文本,直到抵达一个定界符为止。一旦找到一个定界符,

就可以使用其开始索引(开始读取的位置)和结束索引来标识一个符号。该符号作为第一个参数返回,返回的是该符

号的首地址。其函数返回类型为 bool,表明是否取到了一个新的符号(可以用来检查是否抵达缓存数据尾部)。其函

数如下:

清单 8.15:函数 GetNextToken

bool TokenStream::GetNextToken( std::string* buffer, char* delimiters, int totalDelimiters )

{

startIndex_ = endIndex_;

bool inString = false;

int length = ( int )data_.length( );

if( startIndex_ >= length - 1 )

return false;

while( startIndex_ < length && isValidIdentifier( data_[startIndex_],

delimiters, totalDelimiters ) == false )

{

startIndex_++;

}

endIndex_ = startIndex_ + 1;

if( data_*startIndex_+ == ’"’ )

inString = !inString;

if( startIndex_ < length )

{

while( endIndex_ < length && ( isValidIdentifier(

data_[endIndex_], delimiters, totalDelimiters ) || inString == true ) )

{

if( data_*endIndex_+ == ’"’ )

inString = !inString;

endIndex_++;

}

if( buffer != NULL )

{

int size = ( endIndex_ - startIndex_ );

int index = startIndex_;

buffer->reserve( size + 1 );

buffer->clear( );

for( int i = 0; i < size; i++ )

{

buffer->push_back( data_[index++] );

}

}

return true;

}

return false;

}

接下来最后一个函数是 MoveToNextLine 函数,它将当前读取位置移动到下一行数据。并且我们返回移动后的数

据行头指针,这是因为我们的数据是一个连续的字符数组并且需要为下一行解析做准备,或者读取当前位置的剩余

部分。其函数如下:

清单 8.16:MoveToNextLine 函数

bool TokenStream::MoveToNextLine( std::string* buffer )

{

int length = ( int )data_.length( );

if( startIndex_ < length && endIndex_ < length )

{

endIndex_ = startIndex_;

while( endIndex_ < length && ( isValidIdentifier( data_[endIndex_] ) ||

data_*endIndex_+ == ’ ’ ) )

{

endIndex_++;

}

if( ( endIndex_ - startIndex_ ) == 0 )

return false;

if( endIndex_ - startIndex_ >= length )

return false;

if( buffer != NULL )

{

int size = ( endIndex_ - startIndex_ );

int index = startIndex_;

buffer->reserve( size + 1 );

buffer->clear( );

for( int i = 0; i < size; i++ )

{

buffer->push_back( data_[index++] );

}

}

}

else

{

return false;

}

endIndex_++;

startIndex_ = endIndex_ + 1;

return true;

}

从 OBJ 文件中载入网格(Loading Meshes from OBJ Files)

实际载入 OBJ 文件的类是 ObjModel。该类使用 TokenStream 类来解析数据并且利用解析后的信息创建三角形列

表。OBJ 文件有顶点位置,贴图坐标,和法线向量,那么我们的 ObjModel 类将会存储其每种数据的指针,如清单

8.17 所示。

清单 8.17:ObjModel 类

class ObjModel

{

public:

ObjModel( );

~ObjModel( );

void Release( );

bool LoadOBJ( char *fileName );

float *GetVertices() { return vertices_; }

float *GetNormals() { return normals_; }

float *GetTexCoords() { return texCoords_; }

int GetTotalVerts() { return totalVerts_; }

private:

float *vertices_;

float *normals_;

float *texCoords_;

int totalVerts_;

};

载入函数 LoadOBJ(见清单 8.18 和 8.19)比其名字本身更加简单。该函数首先打开文件并且确定文件的字节数。

它将文件读入一块临时缓存并且将其传递给 TokenStream 对象。首先 TokenStream 对象通过调用 MoveToNextLine 函

数来定位数据函数,其次解析每一行数据为我们检查的具体信息。

当解析一行时,首先检查行首字母来表明该行的信息类型。如果以 v 开始表明后面是顶点位置,如果以 vt 开始

表明其后是纹理坐标,如果以 vn 开始表示后接顶点法线向量。我们使用空格定界符来定界每一个部分。

如果我们从文件中读取到一个表面,即出现在关键字 f 之后,那么我们需要另一个 TokenStream 对象来使用空

格和/字符作为定界符。

清单 8.18:LoadOBJ 函数前半部分

bool ObjModel::LoadOBJ( char *fileName )

{

std::ifstream fileStream;

int fileSize = 0;

fileStream.open( fileName, std::ifstream::in );

if( fileStream.is_open( ) == false )

return false;

fileStream.seekg( 0, std::ios::end );

fileSize = ( int )fileStream.tellg( );

fileStream.seekg( 0, std::ios::beg );

if( fileSize <= 0 )

return false;

char *buffer = new char[fileSize];

if( buffer == 0 )

return false;

memset( buffer, ’\0’, fileSize );

TokenStream tokenStream, lineStream, faceStream;

std::string tempLine, token;

fileStream.read( buffer, fileSize );

tokenStream.SetTokenStream( buffer );

delete[] buffer;

tokenStream.ResetStream( );

std::vector<float> verts, norms, texC;

std::vector<int> faces;

char lineDelimiters[2] = { ’\n’, ’ ’ -;

while( tokenStream.MoveToNextLine( &tempLine ) )

{

lineStream.SetTokenStream( ( char* )tempLine.c_str( ) );

tokenStream.GetNextToken( 0, 0, 0 );

if( !lineStream.GetNextToken( &token, lineDelimiters, 2 ) )

continue;

if( strcmp( token.c_str( ), "v" ) == 0 )

{

lineStream.GetNextToken( &token, lineDelimiters, 2 );

verts.push_back( ( float )atof( token.c_str( ) ) );

lineStream.GetNextToken( &token, lineDelimiters, 2 );

verts.push_back( ( float )atof( token.c_str( ) ) );

lineStream.GetNextToken( &token, lineDelimiters, 2 );

verts.push_back( ( float )atof( token.c_str( ) ) );

}

else if( strcmp( token.c_str( ), "vn" ) == 0 )

{

lineStream.GetNextToken( &token, lineDelimiters, 2 );

norms.push_back( ( float )atof( token.c_str( ) ) );

lineStream.GetNextToken( &token, lineDelimiters, 2 );

norms.push_back( ( float )atof( token.c_str( ) ) );

lineStream.GetNextToken( &token, lineDelimiters, 2 );

norms.push_back( ( float )atof( token.c_str( ) ) );

}

else if( strcmp( token.c_str( ), "vt" ) == 0 )

{

lineStream.GetNextToken( &token, lineDelimiters, 2 );

texC.push_back( ( float )atof( token.c_str( ) ) );

lineStream.GetNextToken( &token, lineDelimiters, 2 );

texC.push_back( ( float )atof( token.c_str( ) ) );

}

else if( strcmp( token.c_str( ), "f" ) == 0 )

{

char faceTokens*3+ = , ’\n’, ’ ’, ’/’ -;

std::string faceIndex;

faceStream.SetTokenStream( ( char* )tempLine.c_str( ) );

faceStream.GetNextToken( 0, 0, 0 );

for( int i = 0; i < 3; i++ )

{

faceStream.GetNextToken( &faceIndex, faceTokens, 3 );

faces.push_back( ( int )atoi( faceIndex.c_str( ) ) );

faceStream.GetNextToken( &faceIndex, faceTokens, 3 );

faces.push_back( ( int )atoi( faceIndex.c_str( ) ) );

faceStream.GetNextToken( &faceIndex, faceTokens, 3 );

faces.push_back( ( int )atoi( faceIndex.c_str( ) ) );

}

}

else if( strcmp( token.c_str( ), "#" ) == 0 )

{

int a = 0;

int b = a;

}

token*0+ = ’\0’;

}

一旦我们取得了这些数据,我们就需要使用这些信息来创建几何体的三角形列表。我们不直接在 OBJ 文件中使

用这些信息,因为这些索引定义了每个属性,而不是每个顶点。一旦我们创建很方便在 Direct3D 中处理的信息,就

返回真,并且释放所有的临时数据。LoadOBJ 函数的第二部分如下:

清单 8.19:函数 LoadOBJ 的下半部分

{

// "Unroll" the loaded obj information into a list of triangles.

int vIndex = 0, nIndex = 0, tIndex = 0;

int numFaces = ( int )faces.size( ) / 9;

totalVerts_ = numFaces * 3;

vertices_ = new float[totalVerts_ * 3];

if( ( int )norms.size( ) != 0 )

{

normals_ = new float[totalVerts_ * 3];

}

if( ( int )texC.size( ) != 0 )

{

texCoords_ = new float[totalVerts_ * 2];

}

for( int f = 0; f < ( int )faces.size( ); f+=3 )

{

vertices_[vIndex + 0] = verts[( faces[f + 0] - 1 ) * 3 + 0];

vertices_[vIndex + 1] = verts[( faces[f + 0] - 1 ) * 3 + 1];

vertices_[vIndex + 2] = verts[( faces[f + 0] - 1 ) * 3 + 2];

vIndex += 3;

if(texCoords_)

{

texCoords_[tIndex + 0] = texC[( faces[f + 1] - 1 ) * 2 + 0];

texCoords_[tIndex + 1] = texC[( faces[f + 1] - 1 ) * 2 + 1];

tIndex += 2;

}

if(normals_)

{

normals_[nIndex + 0] = norms[( faces[f + 2] - 1 ) * 3 + 0];

normals_[nIndex + 1] = norms[( faces[f + 2] - 1 ) * 3 + 1];

normals_[nIndex + 2] = norms[( faces[f + 2] - 1 ) * 3 + 2];

nIndex += 3;

}

}

verts.clear( );

norms.clear( );

texC.clear( );

faces.clear( );

return true;

}

最后的代码考察一下 LoadContent函数。当我们载入OBJ模型时,我们创建一个新的ObjModel对象,叫做 LoadOBJ,

并且使用该对象创建的属性指针来填充我们的顶点结构数组。一旦这些信息填入我们的顶点缓存,就可以作为一个

普通的三角形列表来进行渲染,则我们的模型就会出现在屏幕上。你可以尝试使用不同复杂度的模型来替换该 Demo

中的 sphere 模型。载入顶点缓存的具体代码见于清单 8.20。程序截图见于插图 8.3。

清单 8.20:LoadContent 函数中载入顶点缓存的代码

// Load the models from the file.

ObjModel objModel;

if( objModel.LoadOBJ( "sphere.obj" ) == false )

{

DXTRACE_MSG( "Error loading 3D model!" );

return false;

}

totalVerts_ = objModel.GetTotalVerts( );

VertexPos* vertices = new VertexPos[totalVerts_];

float* vertsPtr = objModel.GetVertices( );

float* texCPtr = objModel.GetTexCoords( );

for( int i = 0; i < totalVerts_; i++ )

{

vertices[i].pos = XMFLOAT3( *(vertsPtr + 0), *(vertsPtr + 1), *(vertsPtr + 2) );

vertsPtr += 3;

vertices[i].tex0 = XMFLOAT2( *(texCPtr + 0), *(texCPtr + 1) );

texCPtr += 2;

}

D3D11_BUFFER_DESC vertexDesc;

ZeroMemory( &vertexDesc, sizeof( vertexDesc ) );

vertexDesc.Usage = D3D11_USAGE_DEFAULT;

vertexDesc.BindFlags = D3D11_BIND_VERTEX_BUFFER;

vertexDesc.ByteWidth = sizeof( VertexPos ) * totalVerts_;

D3D11_SUBRESOURCE_DATA resourceData;

ZeroMemory( &resourceData, sizeof( resourceData ) );

resourceData.pSysMem = vertices;

d3dResult = d3dDevice_->CreateBuffer( &vertexDesc, &resourceData, &vertexBuffer_ );

if( FAILED( d3dResult ) )

{

DXTRACE_MSG( "Failed to create vertex buffer!" );

return false;

}

delete[] vertices;

objModel.Release( );

插图 8.3:Models Demo 截图

高级主题(Advanced Topics)

这章仅仅是开始接触到 3D 视频游戏场景的皮毛。尽管这些主题可以迅速成为十分高级的主题,但是你依旧可

以对它们做各种尝试。只需考察本章早期的照相机代码就可以表明,只需再多做一点点工作,你就可以创建一个照

相机系统来允许你创建用于不同游戏类型的广泛的视图。

该部分,我们花费一些时间来讨论一些主题,甚至作为一个初学者,你也可以马上对这些领域进行探索。只需

要一些优质的艺术素材,你甚至可以创建一些十分优秀的 Demo 或者使用这些思想作为基础来创建游戏。

复杂的照相机(Complex Cameras)

本章我们已经接触了两种照相机类型。第一类照相机是一个简单的固定照相机,它可以直接提供所创建的

look-at 视图矩阵。这种照相机有它自己存在的目的,但是但是它不足以作为一个单独类型的照相机在 Direct3D 中讨

论。第二类照相机有用一点点,当我们需要观察我们的 3D 物体时。Arc 照相机允许我们自由的绕 X 或 Y 轴旋转。许

多 3D 模型编辑器使用类似的照相机模式,这里目标视图位置成为了强制视图,而照相机的位置则基于围绕的观察

物体动态改变。

下面还有更多的照相机类型,用于创建不同的 3D 类型游戏。随后的清单列举了公共的 3D 照相机系统:

First-persion:第一人称视角

Free(ghost):自由视角

Chase(third-persion):第三人称视角

Flight:飞行模式视角

Scripted:

AI:

Framing:

第一人称视角的照相机广泛用于第一人称射击游戏。使用第一人称照相机,玩家通过游戏人物的眼睛来透视投

影观察周围环境。在 FPS(第一人称射击游戏,first-person shooters)游戏中,玩家的武器和角色的部分身体可以看见,

还有一些界面元素,例如帮助瞄准目标的十字准线圈。Epic Game 的 UDK(插图 8.4)就是一个使用第一人称照相机的

例子。

自由视角照相机,也就是著名的鬼影(ghost)照相机,可以在周围环境的任意轴线自由移动。这种照相机通常用

于 FPS 游戏中的观众视角,例如 UDK 中的简单 demo(插图 8.5),或者重播模式,例如 Bungie 的 Halo Reach Theater,

等等。尽管自由视角照相机可以或者不需要经由碰撞检测与游戏世界进行物理交互(译者注:如果没有经过碰撞检测,

则该照相机模式可以看到物体的内部),该类照相机在场景中移动时只有十分少量的限制规则。

插图 8.4:UDK 示例 Demo 中的第一人称视角

插图 8.5:自由照相机模式在 Epic 的 UDK 示例

插图 8.6:第三人称照相机在 UDK 中的示例

第三人称照相机是一种捕捉场景中物体的照相机类型。这种照相机类型通常用于第三人称游戏(见插图 8.6),飞

行游戏等,玩家的照相机通常在角色的后面,这些照相机有阻尼效应,使得如果照相机被附着在杆上,照相机逐渐

的捕捉旋转而不是移动。

飞行游戏中,我们可以使用多种不同类型的照相机一起工作。驾驶舱内的视图使用第一人称照相机,在游戏重

放时使用固定照相机,使用自由视角照相机来观察导弹和火箭炮,使用第三人称照相机来在角色(飞机等)后面观察

飞行视角。在用于 XNA 游戏开发包(http://create.msdn.com)的修改后的第三人称照相机版本(插图 8.7)示例 Demo

中,第三人称照相机也可以变成 Arc 照相机,当玩家使用右边游戏操纵杆绕飞行器转动,而左边的控制器将会使其

变回第三人称视角。

Scripted 和人工智能视角照相机通常被其它角色控制。对于 scripted 照相机,我们可以编写实时照相机的移动

和向后播放。Scripting 照相机,通常伴随着移动和游戏中物体和模型的动画,使得可以创建实时的,游戏中的电影

场景,也就是场景剪辑。

插图 8.7:从第三人称视角到 Arc 视角

许多游戏也在玩游戏期间使用多种类型的照相机。例如,一些游戏需要基于玩家所在的当前环境来转换第一人

称和第三人称视角,游戏中的分屏视图可以有多种照相机用于不同玩家,渲染在屏幕的不同区域,还有些游戏给你

选项来决定使用哪种视角(例如插图 8.8 中的 XNA 的 Ship Game Starter Kit)。

XNA 是一个类似于 DirectX SDK 的微软游戏开发框架,所有允许使用 SDK 截图,就像使用 DirectX SDK 中的截图

一样。

插图 8.8:XNA 的 Ship 游戏中基于玩家选择改变照相机视图

3D 层级文件(3D Level Files)

从文件中载入 3D 几何体的第一步就是载入环境实体。一个环境有多种方面,下面列举一些:

Skies:天空

Water:水流

Terrain(land):陆地

Building:建筑物

Vehicles:交通工具

Characters:角色

Weapons:武器

Power-ups:能量

Trigger volumes(例如,触发一个事件的区域,例如场景剪辑)

Environment props(例如,岩石,树木,草地,灌木丛等)

Objective game props(例如,标志旗,小山位置等)

等等

在你的虚拟世界中可以拥有广泛的不同类型的物体,其中有一些是不可见的。现在的游戏,游戏分层通过手工

指定太大,经常是使用场景编辑器来完成,即地图或者层级编辑器。

创建地图编辑器不是一件容易的任务并且经常需要与具体的游戏结合。表示游戏层级的文件格式通常也与具体

的游戏高度相关。作为一个例子,考察一下简单的示例文件,它存储了用于 3D 模型的位置,旋转和缩放信息。如

下所示:

Level Level1

{

PlayerStart 0,0,-100 0,0,0 1,1,1

WeaponStart Pistol

Weapons

{

Pistol -421,66,932 0,90,0 1,1,1

Sniper 25,532,235 0,0,0 1,1,1

RocketLauncher 512,54,336 0,0,0, 1,1,1

...

}

Enemies

{

...

}

Scripts

{

...

}

Triggers

{

...

}

}

上述例子中,想象一下当游戏开始时,函数 PlayerStart 指定玩家在游戏世界中的开始位置,旋转度,和缩放比;

而函数 WeaponStart 则指定玩家所拥有的武器。设想一下所有的武器和它们在游戏世界中的位置都定义在 Weapons

块中,所有的敌人则定义在 Enemies 块中,和脚本事件和触发区域定义在各自的块中。如果游戏层次布局开始时载

入脚本,当玩家进入不可见的触发区域中将会触发事件或剧情(例如,打开一扇门,或者是播放一段 CG 动画等)。

即使在这些最基本和想象中的层级文件中,你也可以看到许多游戏的共同细节,并且我们可以列举游戏中的物

体的足够的细节。虽然这不是任何一种真实的格式,但你可以给单件物体或者一群物体以足够的特性和信息使得它

们变得十分复杂。在辅助应用程序(译者注:地图或场景编辑器)中创建这些物体和编辑它们可以使得制作层级文件

更加的容易和更有效率。现在,许多游戏公司都使用辅助的艺术工具,比如 3D Max 或者 Maya 来不仅创建单个的

对象而且也创建层级或场景数据文件,来在游戏中载入。

章末总结(Summary)

本章的目标是介绍了两个简单的 3D 照相机,可以帮助你学习怎样创建更多的照相机类型。本章还展示了怎样

从 OBJ 文件中载入模型,OBJ 文件格式被众多的 3D 建模程序支持。OBJ 文件是一种简单的文本格式,其语法也相对

简洁明了。

你所学到的(What You Have Learned)

怎样创建 look-at(固定模式)照相机

怎样创建 arc-ball 照相机

怎样从 OBJ 文件中载入网格

章末习题(Chapter Questions)

习题答案见于附录 A

1. 什么是 look-at 照相机?

2. 本章我们讨论了哪两种照相机类型?

3. 什么是 arc-ball 照相机?

4. True or False:OBJ 文件是一种 3D 模型的二进制文件

5. 描述什么是 token stream。

索引(Index)