DirectX 12 3D 游戏开发实战

978-7-115-47921-1
作者:
译者: 王陈
编辑: 李瑾

图书目录:

详情

本书是畅销书作者的最新力作,该作者在3D开发领域拥有丰富的经验和积累,本书容纳了作者多年来的重要经验。书中通过三个部分来讲解如何使用DirectX12进行3D游戏的开发,从基础开始,由浅入深地引导读者进行学习,通过阅读本书,读者能够快速掌握这一工具。

图书摘要

版权信息

书名:DirectX 12 3D 游戏开发实战

ISBN:978-7-115-47921-1

本书由人民邮电出版社发行数字版。版权所有,侵权必究。

您购买的人民邮电出版社电子书仅供您个人使用,未经授权,不得以任何方式复制和传播本书内容。

我们愿意相信读者具有这样的良知和觉悟,与我们共同保护知识产权。

如果购买者有侵权行为,我们可能对该用户实施包括但不限于关闭该帐号等维权措施,并可能追究法律责任。



著    [美] 弗兰克•D.卢娜(Frank D. Luna)

译    王 陈

责任编辑 罗子超

人民邮电出版社出版发行  北京市丰台区成寿寺路11号

邮编 100164  电子邮件 315@ptpress.com.cn

网址 http://www.ptpress.com.cn

读者服务热线:(010)81055410

反盗版热线:(010)81055315


Simplified Chinese language edition copyright ©2018 by Post & Telecom Press.

All rights reserved.

Introduction to 3D Game Programming with DirectX 12 by Frank D. Luna.

Copyright ©2016 Mercury Learning and Information,Inc.

本书中文简体版由Mercury Learning and Information公司授权人民邮电出版社出版。未经出版者书面许可,对本书的任何部分不得以任何方式或任何手段复制和传播。

版权所有,侵权必究。


Direct3D是微软公司DirectX SDK集成开发包中的重要组成部分,是编写高性能3D图形应用程序的渲染库,适用于多媒体、娱乐、即时3D动画等广泛和实用的3D图形计算领域。

本书围绕交互式计算机图形学这一主题展开,着重介绍Direct3D的基础知识和着色器编程的方法,并介绍了如何利用Direct3D来实现各种有趣的技术与特效,旨在为读者学习更高级的图形技术奠定坚实的基础。本书包括3部分内容。第一部分介绍必备的数学知识,涵盖向量代数、矩阵代数和变换等内容。这是贯穿全书的数学工具,是读者需要掌握的基础内容。第二部分重点介绍Direct3D的基础知识,展示用Direct3D来实现绘图任务的基本概念与技术,如渲染流水线、纹理贴图、混合、曲面细分等。第三部分则利用Direct3D来实现各种有趣的特效,如实例化与视锥体剔除、阴影贴图、环境光遮蔽等。

本书适合希望通过Direct3D来学习3D编程的C++中级程序员阅读,也可供已对Direct3D有一定了解或具有非DirectX API使用经验的3D程序员参考。

谨以此书献给我的侄辈们——

Marrick、Hans、Max、Anna、Augustus、Presley以及Elyse



Direct3D 12是一款为运行在现代图形硬件上的各种Windows 10平台(Windows桌面版、手机版和Xbox One)编写高性能3D图形应用程序的渲染库。Direct3D也是一种底层库,这也就意味着此种应用程序接口(API)与其下层控制的图形硬件模块关系更为紧密[1]。Direct3D的主要用户大多来自游戏产业,他们驾驭Direct3D来构建更加高端的渲染引擎。同时,它亦应用于如医药产业、科学可视化以及虚拟建筑漫游等行业,用来实现高性能的3D图形交互功能。另外,由于当今每一部新的个人电脑都已配备了现代图形设备,因此,非3D应用也开始逐步把计算密集型的工作移交至显卡来执行,以充分发挥其中GPU(Graphics Processing Unit,图形处理器)的计算能力。这就是众所周知的GPU通用计算(general purpose GPU computing)技术。对此,Direct3D也提供了用于编写GPU通用计算程序的计算着色器API。尽管Direct3D 12程序通常以原生的C++语言进行编写,但SharpDX团队正在致力于.NET包装器版的开发,因此,用户也可以从托管应用程序中来访问这一强大的3D图形API。

本书围绕交互式计算机图形学这个主题展开,关注于通过Direct3D 12来进行游戏的开发。读者将从中学到Direct3D的基础知识以及着色器编程的方法。阅读完本书之后,读者就可以继续学习更加高级的图形技术了。本书共分为3个主要部分。第一部分讲解了本书后续要用到的数学知识。第二部分展示如何用Direct3D来实现基本绘图任务,例如初始化Direct3D,定义3D几何图形,设置摄像机,光照,纹理,混合技术,模板技术,曲面细分技术,创建顶点、像素、几何图形以及计算着色器。第三部分则主要是利用Direct3D来实现各种有趣的技术与特效,例如动画角色网格、拾取技术、环境贴图、法线贴图、阴影贴图以及环境光遮蔽技术。

初学者最好按先后顺序通读全书。书中章节是按照由浅入深、逐步递进的顺序组织而成的。这样一来,读者便不会因过陡的学习曲线而如堕烟海。一般来讲,特定篇章中所用的技术与概念往往在之前的章节中有所交代。因此,读者最好在掌握了欲学习章节之前的所有内容后再继续前行。当然,有一定经验的读者可直接挑选感兴趣的部分进行阅读。

最后,部分读者可能会不禁琢磨:读完本书之后,究竟能够开发出何种类型的游戏来呢?这里对此给出的解释是:您最好亲自粗略地阅览此书,看看其中大概都在讲些什么内容。据此,基于本书所讲的技术知识再结合自己的聪明才智,至于能够开发出哪类游戏作品,想必这答案读者也就自会了然于胸了。

本书主要适合以下3类读者:

1.希望通过Direct3D最新版本来学习3D图形学编程的C++中级程序员。

2.具有非DirectX API(如OpenGL)使用经验,并希望学习Direct3D编程方面知识的3D程序员。

3.具有一定的Direct3D使用经验,并希望学习Direct3D最新版本的程序员。

需要强调的是,本书为重点介绍Direct3D 12、着色器编程以及3D游戏编程的读物,而并非是讨论一般计算机程序设计的读物。因此,读者需要具备下列预备知识:

1.高中程度的数学知识,比如代数、三角学以及(数学)函数等。

2.Visual Studio相关的使用技能,比如如何创建项目、为项目添加文件以及指定需要链接的外部库等。

3.中级C++编程技能以及数据结构知识,比如熟练地运用指针、数组、运算符重载、链表、继承、多态等。

4.熟悉使用Win32 API进行Windows编程还是很有必要的,可谓是学习本书的基础。但这一条并非是强制性要求,因为本书附录A中提供了Win32编程的相关入门知识。

下面是进行Direct3D 12编程的必备条件:

1.Windows 10操作系统。

2.Visual Studio 2015开发环境或其后续版本。

3.一款支持Direct3D 12的显卡(本书中的演示程序都已通过Geforce GTX 760平台的测试)。

Direct3D是一种规模庞大的API,将其所有的细节都在一本书中体现是不切实际的。因此,为了获得更为深入的API信息,学习DirectX SDK[2]文档的查阅方法势在必行。DirectX SDK在MSDN上的最新文档为《Direct3D 12 Programming Guide》,即《Direct3D 12编程指南》。

图1所示的是在线文档的截图。

DirectX文档涵盖了DirectX API的方方面面,因此,它是一种不可或缺的参考资料。然而,由于此文档对预备知识的讲解并不深入且假设读者对此有一定认识,因而导致它无法成为初学者最佳的学习工具。但是,随着DirectX每个新版本的发布,该文档也在日益完善中。

换言之,这个文档主要还是用作参考。假设用户碰到一个与DirectX有关的数据类型或函数,如函数ID3D12Device::CreateCommittedResource,并希望获取更多与之相关的信息,就可以方便地在该文档中搜索它,比如本示例中的函数(见图2),以得到更为细致的描述。

图1 DirectX文档中的《Direct3D 12编程指南》

图2 获取函数的相关文档


注意  

在本书中,我们会不时地指导读者去阅览文档以获取更多的有关细节。


我们还建议读者研究一下官方提供的Direct3D 12演示程序。

微软官方可能还会在此陆续增添更多的例程。除此之外,读者还可以去NVIDIA、AMD以及Intel的官方网站上查找与Direct3D 12有关的示例。

尽管我们努力遵循Direct3D 12的最佳实践,力图写出高效的代码,但本书中每个样例的主要目标还是为了阐述Direct3D中的基本概念以及演示图形编程技术。应当明确的是,写出最优代码并非本书最终目的,而且过分优化还可能导致原本意图明晰的代码变得含混不清,反而适得其反。希望读者将这一点铭记于心,尤其是在将书中例程代码合并到自己的项目中时,因此在此过程中,您可能为了追求程序更高的效率而重构代码。再者,为了把注意力集中在Direct3D API上,我们还在Direct3D之上构建了一层轻量级的框架。这就意味着我们很可能会在源代码中,以硬编码的数值与定义其他内容的方式来令程序得以运行。类似地,在大型的3D应用程序中,可能要在Direct3D的基础之上实现一款渲染引擎。但本书的主旨却是Direct3D API,而非设计渲染引擎。

读者可以登录本书的网站(www.d3dcoder.net和www.merclearning.com),以获取本书相关材料。在前者中,读者可以找到本书内所有例程的完整源代码以及项目文件。也可通过异步社区本书页面获取(www.epubit.com)。在大多数情况下,DirectX程序往往比较庞大,以至于不宜全部列入书中。因此,只得在书中嵌入与所讲内容密切相关的代码片段。为此,我们极力建议读者在学习相关的例程代码时去一睹它的全貌(为了便于读者学习,我们已将演示程序的规模尽量减小)。一般说来,在阅读过特定章节,并研究完所附演示代码后,读者应当能够自行独立地实现该章节中所述的例程。但事实上,一种更快捷的学习方法是在参考书籍和示例代码的同时,尝试着以自己的方式实现相关程序。

通过双击项目文件(.vcxproj)或解决方案文件(.sln)就可以方便地打开本书的演示程序。接下来,我们将详述如何通过Visual Studio 2015 (VS15)以本书的例程框架从头开始创建并构建一个项目。在此,我们以第6章中的“Box”(立方体)演示程序为例。

首先,读者需要下载本书所用的源代码并将其保存在硬盘的某个文件夹之中。为了便于讨论,假设这个文件夹的路径为C:\d3d12book。在这里可以看到一系列文件夹,其中含有对应章节的例程项目。读者可能会注意到有个名为“Common”的文件夹,其中包含所有演示项目中都要复用的公共代码。现在便可以在源代码文件夹中新建一个文件夹,用来存放我们自己的例程,例如C:\d3d12book\MyDemos。随后,我们将基于本书中的例程框架在该文件夹中创建一个新的项目。

注意  

事实上,读者自己设置的目录结构大可不必如此,这只不过是本书例程的结构而已。如果读者希望按自己的意愿来设置源代码文件,可以将演示项目放在任何地方,只要使Visual Studio能找到Common目录中的源代码即可。


首先运行VS15,接着在主菜单中依次选择File(文件)→New(新建)→Project(项目),如图3所示。

图3 创建一个新项目

在弹出的New Project(新项目)对话框(如图4所示)左侧Visual C++项目类型的树形控件中选择Visual C++Win32,再于右侧选择Win32 Project(Win32项目)。接下来,给项目起个名称,并指定项目文件夹的保存位置。别忘了取消默认选中的Create directory for solution(为解决方案创建目录)复选框。随后单击OK(确定)按钮。

接着,又会弹出一个新的对话框。其左侧有Overview(概述)和Application Settings(应用程序设置)两个选项。选择Application Settings,便会出现如图5所示的对话框。在这里,需要确保选择Windows application(Windows应用程序)选项和Empty project(空项目)复选框,之后再单击Finish(完成)按钮。至此,我们已成功创建了一个空的Win32项目,但在构建DirectX项目例程之前,我们还有一些事情需要做。

图4 新项目的相关设置

图5 应用程序的相关设置

通过在源代码文件Common/d3dApp.h中使用#pragma预处理指令来链接所需的库文件,如:

// 链接所需的d3d12库
#pragma comment(lib, "d3dcompiler.lib")
#pragma comment(lib, "D3D12.lib")
#pragma comment(lib, "dxgi.lib")

对于创建演示程序而言,该预处理指令使我们免于打开项目属性页面并在连接器配置项下指定附加依赖库。

至此,项目已经配置完成。现在来为它添加源代码并对其进行构建。首先,将“Box”演示程序的源代码BoxApp.cpp以及Shaders文件夹(位于d3d12book\Chapter 6 Drawing in Direct3D\Box)复制到工程目录之中。

待复制完上述文件之后,我们以下列步骤来将源代码添加到当前的项目之中。

1.右键单击解决方案资源管理器下的项目名称,在弹出的下拉菜单中依次选择Add(添加)→Existing Item(现有项),将文件BoxApp.cpp添加到项目中。

2.右键单击解决方案资源管理器下的项目名称,在弹出的下拉菜单中逐步选择Add→Existing Item,前往读者放置本书Common文件夹的位置,并将此文件夹中所有的.h/.cpp文件都添加到项目之中。现在,方案资源管理器看起来应当与图6相同。

3.再次右键单击解决方案资源管理器下的项目名称,从菜单中选择Properties(属性)。再从Configuration Properties(配置属性)→General(常规)选项卡下,将Target Platform Version(目标平台版本)设置为版本10.x[4],以令目标平台为Windows 10。接着单击Apply(应用)按钮。

4.大功告成!源代码文件现都已位于项目之中,读者可以在主菜单中选择Debug(调试)→Start Debugging(开始调试)进行编译、链接以及执行该演示程序。应用程序的执行效果应当与图7所示的一致。

图6 添加“Box”例程所需源代码之后的解决方案资源管理器

图7 “Box”演示程序的效果


注意  

Common目录下的大量代码都是构建本书例程的基石。所以,建议读者先不必忙于查看这些代码。待读到了本书中与之相关的章节后,再研究它们也不迟。


[1] 尤其是到了Direct3D 12,更像Mantle等API那样实现了前所未有的更底层的硬件抽象,削减驱动层的工作,转交给开发者负责,从而令图形的处理流程更加“智能”,使用起来犹如贴地飞行的“快感”。

[2] DirectX包罗系列与多媒体以及游戏开发有关的API,因此Direct3D只是DirectX的一个子集。详细信息请见《DirectX Graphics and Graming》(ee663274)。本书则侧重Direct3D的讲解。

[3] 采用Visual Studio 2017的读者可以参考《Visual Studio中的使用C++的DirectX游戏开发》一文。

[4] 其中的“x”对应于构建项目时所采用的具体SDK版本。


本书由异步社区出品,社区(https://www.epubit.com/)为您提供相关资源和后续服务。

本书提供如下资源:

要获得以上配套资源,请在异步社区本书页面中点击,跳转到下载界面,按提示进行操作即可。注意:为保证购书读者的权益,该操作会给出相关提示,要求输入提取码进行验证。

如果您是教师,希望获得教学配套资源,请在社区本书页面中直接联系本书的责任编辑。

作者和编辑尽最大努力来确保书中内容的准确性,但难免会存在疏漏。欢迎您将发现的问题反馈给我们,帮助我们提升图书的质量。

当您发现错误时,请登录异步社区,按书名搜索,进入本书页面,点击“提交勘误”,输入勘误信息,点击“提交”按钮即可。本书的作者和编辑会对您提交的勘误进行审核,确认并接受后,您将获赠异步社区的100积分。积分可用于在异步社区兑换优惠券、样书或奖品。

我们的联系邮箱是contact@epubit.com.cn。

如果您对本书有任何疑问或建议,请您发邮件给我们,并请在邮件标题中注明本书书名,以便我们更高效地做出反馈。

如果您有兴趣出版图书、录制教学视频,或者参与图书翻译、技术审校等工作,可以发邮件给我们;有意出版图书的作者也可以到异步社区在线提交投稿(直接访问www.epubit.com/selfpublish/submission即可)。

如果您是学校、培训机构或企业,想批量购买本书或异步社区出版的其他图书,也可以发邮件给我们。

如果您在网上发现有针对异步社区出品图书的各种形式的盗版行为,包括对图书全部或部分内容的非授权传播,请您将怀疑有侵权行为的链接发邮件给我们。您的这一举动是对作者权益的保护,也是我们持续为您提供有价值的内容的动力之源。

“异步社区”是人民邮电出版社旗下IT专业图书社区,致力于出版精品IT技术图书和相关学习产品,为作译者提供优质出版服务。异步社区创办于2015年8月,提供大量精品IT技术图书和电子书,以及高品质技术文章和视频课程。更多详情请访问异步社区官网https://www.epubit.com。

“异步图书”是由异步社区编辑团队策划出版的精品IT专业图书的品牌,依托于人民邮电出版社近30年的计算机图书出版积累和专业编辑团队,相关图书在封面上印有异步图书的LOGO。异步图书的出版领域包括软件开发、大数据、AI、测试、前端、网络技术等。

异步社区

微信服务号


在此,我要对审阅本书早期版本的Rod Lopez、Jim Leiterman、Hanley Leung、Rick Falck、Tybon Wu、Tuomas Sandroos、Eric Sandegren、Jay Tennant与William Goschnick表示感谢。向现存于本书网站上、运用于演示程序中的3D模型以及纹理的制作者Tyler Drinkard致以谢意。我还要感谢Dale E. La Force、Adam Hoult、Gary Simmons、James Lambers以及William Chin,他们曾给予我极大的帮助。另外,亦感激为我提供了DirectX 12 beta版本的Matt Sandy,以及耐心解答用户们在使用beta版时遇到的种种问题的DirectX团队。最后,我还要感谢Mercury Learning and Information出版社的全体工作人员,尤其是出版人David Pallai以及项目经理Jennifer Blaney,是他们直接促成了本书顺利的出版发行。


“世上之事,无数学则不可解。”

罗杰•培根(《大著作》第四部分,第一章《第一个区别》,1267年)

电子游戏试图向玩家呈现出一个虚拟的世界。然而,计算机从本质上来讲却是一种处理数据的精密仪器。那么问题来了:如何用计算机来表达游戏中虚拟的场景呢?解决的办法就是完全运用数学的方式来描述场景空间以及其中物体的交互。因此,数学在电子游戏的开发中起着至关重要的基础性作用。

在讲述必备知识的第一部分中,我们将介绍穿插于全书的数学工具。重点是向量(vector,物理学和工程学中亦常译为“矢量”)、坐标系(coordinate system)、矩阵(matrix)及其变换(transformation),这些工具将广泛用于本书的所有例程之中。除了对这些数学知识进行讲解以外,我们还将纵览由DirectX数学库所提供的相关类与函数,并示范它们的用法。

请注意,这些主题仅论述了本书后续需要掌握的一些基础内容,而有关电子游戏所需的数学知识却不止于此。对于期望学习更多与游戏相关数学知识的读者,我们推荐[Verth04]和[Lengyel02]。

第1章“向量代数”向量(vector)也许是计算机游戏中最基础的数学对象,没有之一了。例如,我们可以用向量表示位置、位移、方向、速度与力。在这一章中,我们将学习向量及其运算法则。

第2章“矩阵代数”矩阵(matrix)为变换提供了一种高效且紧凑的简化表达方式。在这一章中,我们将熟悉矩阵及其运算定义。

第3章“变换”这一章将考察缩放、旋转和平移这三种基本的几何变换。我们利用这些变换来操纵空间中的3D物体。另外,我们还将讲解坐标变换,以此在不同的坐标系之间转换几何体的坐标表示。


向量在计算机图形学、碰撞检测和物理模拟中扮演着关键的角色,而这几方面又正是构成现代电子游戏的常见组成部分。本书的讲述风格主要趋于实践而非严格化的数学推理,如需要查阅专业的3D游戏或3D图形学数学书籍,可参考[Verth04]一书。需要强调的是,本章研究向量的主要目的在于使读者理解本书中所有例程里向量的用法。

学习目标:

1.学习向量在几何学和数学中的表示方法。

2.了解向量的运算定义及其在几何学中的应用。

3.熟悉DirectXMath库中与向量有关的类和方法。

向量(vector)是一种兼具大小(也称为模,magnitude)和方向的量。具有这两种属性的量皆称为向量值物理量(vector-valued quantity)。与向量值物理量相关的例子有作用力(在特定方向上施加的力——力的大小即为向量的模)、位移(质点沿净方向[1]移动的距离)和速度(速率和方向)。这样一来,向量就能用于表示力、位移和速度。另外,有时也用向量单指方向,例如玩家在3D游戏里的视角方向、一个多边形的朝向、一束光线的传播方向以及它照射在某表面后的反射方向等。

首先用几何方法来描述向量的数学特征:通过图像中的一条有向线段即可表示一个向量(见图1.1),其中,线段长度代表向量的模,箭头的指向代表向量的方向。我们可以注意到:向量的绘制位置之于其自身是无足轻重的,因为改变某向量的位置并不会对其大小或方向这两个属性造成任何影响。因此,我们说:两个向量相等,当且仅当它们的长度相等且方向相同。所以,图1.1a中的向量和向量相等,因为它们的长度相等且方向相同。事实上,由于位置对于向量是无关紧要的,所以我们总是能在平移一个向量的同时又完全不改变它的几何意义(因为平移操作既不影响它的长度,也不改变它的方向)。显而易见,我们可以将向量完全平移到向量处(反之亦可),使两者完全重合,分毫不差——由此即可证明它们是相等的。现给出一个实例,图1.1b中的向量和向量向两只蚂蚁分别发出指示:令它们从各自所处的两个不同点,点和点,向北爬行10米。这样一来,我们就能根据蚂蚁的爬行路线,再次得到两个相等的向量。此时,这两个向量与位置信息无关,仅简单地指挥蚂蚁们如何从它们所处的位置爬行移动。在本例中,蚂蚁们被指示向北(方向)移动10米(长度)。

图1.1 向量的实例
(a)绘制在2D平面上的向量 (b)这两个向量指挥蚂蚁们向北移动10米

现在来定义向量实用的几何运算,它能解决与向量值物理量有关的问题。然而,由于计算机无法直接处理以几何方法表示的向量,所以需要寻求一种用数学表示向量的方法加以代替。在这里,我们引入一种3D空间坐标系,通过平移操作使向量的尾部都位于原点(见图1.2)。接着,我们就能凭借向量头部的坐标来确定该向量,并将它记作,如图1.3所示。现在就能以计算机程序中的3个浮点数来表示一个向量了。

图1.2 平移向量,使它的尾部与坐标系的原点重合。当一个向量的尾部位于原点时,称该向量位于标准位置(standard position)

图1.3 一个向量在某3D坐标系中的坐标


注意  

如果在2D空间里进行开发工作,则改用2D坐标系即可。此时,向量只有两个坐标分量:。在这种情况下,计算机程序中仅用两个浮点数就能表示一个向量。


请考虑图1.4,该图展示了向量以及空间中两组不同的标架(frame)[2]。我们可以平移向量,将它分别置于两组标架中的标准位置。显而易见的是,向量在标架中的坐标与它在标架中的坐标是不同的。换句话说,同一个向量在不同的坐标系中有着不同的坐标表示。

图1.4 同一向量在不同的标架中有着不同的坐标

与此类似的还有温度。水的沸点为100℃或212°F(华氏度)[3]。沸水的物理温度是不变的,与温标无关(也就是说,不能因为采用不同的温标而使其沸点降低),但是我们却可以根据所用的温标来为同一温度赋予不同的标量值。类似地,对于向量来说,它的方向和模都表现在对应的有向线段上,不会更改;只有在改变描述它的参考系时,其坐标才会相应地改变。这一点是很重要的,因为这意味着:每当我们根据坐标来确定一个向量时,其对应的坐标总是相对于某一参考系而言的。在3D计算机图形学中,我们通常会用到较多的参考系。因此,我们需要记录向量在每一种坐标系中的对应坐标。另外,我们也需要知道如何将向量坐标在不同的标架之间进行转换。


注意  

可以看出,标架中的向量和点都能够用坐标来表示。但是它们的意义却是截然不同的:在3D空间中,点仅表示位置,而向量却表示着大小与方向。我们将在1.5节中对点展开进一步的讨论。


Direct3D采用的是左手坐标系(left-handed coordinate system)。如果我们伸出左手,并拢手指,假设它们指向的是轴的正方向,再弯曲四指指向轴的正方向,则最后伸直拇指的方向大约就是轴的正方向[4]。图1.5详细展示了左手坐标系与右手坐标系(right-handed coordinate system)的区别。

现在来看右手坐标系。如果伸出右手,并拢手指,假设它们指向的是轴的正方向,再弯曲四指指向轴的正方向,那么,最后伸直拇指的方向大约就是轴的正方向。

图1.5 图的左侧展示的是左手坐标系,可以看出其中的坐标轴正方向指向本书页面内; 图的右侧展示的是右手坐标系,其坐标轴正方向则指向页面外

现在通过坐标来表示向量的相等、加法运算、标量乘法运算和减法运算的定义。对于这4种定义,设有向量和向量

1.两个向量相等,当且仅当它们的对应分量分别相等。即 ,当且仅当

2.向量的加法即令两个向量的对应分量分别相加:。注意,只有同维的向量之间才可以进行加法运算。

3.向量可以与标量(即实数)相乘,所得到的结果仍是一个向量。例如,设为一个标量,则。这种运算叫作标量乘法(scalar multiplication)。

4.向量减法可以通过向量加法和标量乘法表示,即

例1.1

设向量及标量。那么,

1.;

2.;

3.;

4.

第三组运算的不同之处在于其中有个叫作零向量(zero-vector)的特殊向量,它的所有分量都为0,可直接将它简记作

例1.2

为了使配图绘制起来更为方便,我们在此例中将围绕2D向量进行讨论。其计算方式与3D向量的方法一致,只不过2D向量少了一个分量而已。

1.设向量,那么该如何在几何学的角度上对进行比较呢?我们注意到,。绘出向量(见图1.6a),可以观察到,向量的方向与向量正好相反,并且长度是向量的1/2。由此可知,把一个向量的系数变为其相反数,就相当于在几何学中“翻转”此向量的方向,而且对向量进行标量乘法即为对其长度进行缩放。

2.设向量,则。图1.6b展示了向量加法运算的几何意义:把向量进行平移,使尾部头部重合。此时,向量与向量的和即:以的尾部为起点、以平移后的头部为终点所作的向量(如果令向量的位置保持不变,平移向量,使的尾部与的头部重合也能得到同样的结果。在这种情况下, + 的和就可以表示为以的尾部为起点、以平移后的头部为终点所作的向量)。可以看出,向量的加法运算与物理学中不同作用力合成合力的规则是一致的。如果有两个力(两个向量)作用在同一方向上,则将在这个方向上产生更大的合力(更长的向量);如果有两个力(两个向量)作用于彼此相反的方向上,那么便会产生更小的合力(更短的向量),如图1.7所示。

3.设向量,则。图1.6c展示了向量减法运算的几何意义。从本质上讲,的差值仍是一个向量,该向量自的头部始至的头部终。如果我们将看作两个点,那么得到的是一个从点指向点的向量;这种解释方式的重点在于使我们找出向量的方向。同时,不难看出,在把看作点的时候,的长度也就是“点到点的距离”。

图1.6 向量运算的几何意义
(a)标量乘法的几何意义 (b)向量加法的几何意义 (c)向量减法的几何意义

图1.7 作用在球上的两个作用力。利用向量加法将两者合成为一个合力

向量大小(亦称为模)的几何意义是对应有向线段的长度,用双竖线表示(例如||||代表向量的模)。现给出向量,我们希望用代数的方法计算它的模。3D向量的模可通过运用两次毕达哥拉斯定理[5]得出,如图1.8所示。

图1.8 运用两次毕达哥拉斯定理便能得出3D向量的模

首先来看位于平面中以为直角边,以为斜边所构成的直角三角形。根据毕达哥拉斯定理,有。接下来再看以为直角边,以||||为斜边所围成的直角三角形。再次运用毕达哥拉斯定理,便能得出下列计算向量模的公式:

  (1.1)

在某些情况下,我们并不关心向量的长度,仅用它来表示方向。对此,我们希望使该向量的长度为1。把一个向量的长度变为单位长度称为向量的规范化[6](normalizing)处理。具体实现方法是,将向量的每个分量分别除以该向量的模:

  (1.2)

为了验证公式的正确性,下面计算的长度:

由此可见,确实是一个单位向量(unit vector)。

例1.3

对向量 = (−1, 3, 4)进行规范化处理。我们能求出。因此,

 

为了验证是单位向量,我们计算其长度:

点积(dot product,亦称数量积或内积)是一种计算结果为标量值的向量乘法运算,因此有时也称为标量积(scalar product)。设向量,则点积的定义为:

  (1.3)

可见,点积就是向量间对应分量的乘积之和。

点积的定义并没有明显地体现出其几何意义。但是我们却能根据余弦定理(law of cosines,参见练习10)找到二向量点积的几何关系:

  (1.4)

其中,是向量与向量之间的夹角,,如图1.9所示。式(1.4)表明,两向量的点积为:两向量夹角的余弦值乘以这两个向量的模。特别地,如果向量和向量都是单位向量,那么就等于两向量夹角的余弦值,即

图1.9 图a中,向量与向量之间的夹角是一个锐角;图b中,向量与向量之间的夹角是一个钝角。每当讨论两个向量之间的夹角时,我们提及的总是较小的那个角,即角总是满足

式(1.4)给出了一些有用的点积几何性质:

1.如果,那么(即两个向量正交)。

2.如果,那么两向量之间的夹角小于90°(即两向量间的夹角为一锐角)。

3.如果,那么两向量之间的夹角大于90°(即两向量间的夹角为一钝角)。


注意  

“正交”(orthogonal)与“垂直”(perpendicular)实为同义词。


例1.4

设向量。计算向量之间的夹角。

先来计算:

现在,运用式(1.4)得到

例1.5

考虑图1.10。给出向量单位向量,请借助点积公式求出用表示向量的公式。

图1.10 向量在单位向量上的正交投影(orthogonal projection)

首先,观察图示可以得知存在一标量,使得;而且,因为我们假设,所以有。注意,可能是负值,当且仅当的方向相反。利用三角函数,我们有;因此,。又由于是单位向量,便可以用另一种方法来表示:

特别是这里证明了:当是单位向量时,,顺带也解释了在这种情况下的几何意义。我们称为向量落在向量上的正交投影(orthogonal projection),通常将它表示为:

  

如果将看作是一个力,便可认为是力在方向上的分力。同理,向量是作用力的正交方向上的分力(这就是用来表示“垂直”的原因)。观察到,这就是说,可以将向量分解成两个互相正交的向量之和。

如果不具有单位长度,就先对它进行规范化处理,使之成为单位向量。通过把向量替换为单位向量,即可得到更具一般性的投影公式:

如果向量集中的每个向量都是互相正交(集合内的任一向量都与集合中的其他所有向量相互正交)且皆具单位长度,那么我们就称此集合是规范正交(orthonormal)的。有时我们会接到一个近乎(但并不完全)规范正交的集合。这时,一个常见的工作就是通过正交化手段,使之成为规范正交集。例如,我们有时会在3D计算机图形学中用到规范正交集,但是由于处理过程中数值精度的问题,它会随之逐步变为非规范正交集。这时就要用到正交化这一手段了。我们下面将主要围绕这种问题的2D和3D情况展开探讨(也就是说,集合内只有2个或3个向量的情况)。

先来考察相对简单的2D情况吧。假设我们有向量集合,现欲将它正交化为图1.11中所示的正交集。首先设,通过使减去它在上的分量(投影)来令它正交于

此时,我们便得到了一个元素互相正交的向量集合;最后一步是构建一个规范正交集,将向量规范化为单位向量即可。

3D情况与2D情况的处理方法相似,但是步骤更多。假设有向量集,现希望将它正交化为正交集,过程如图1.12所示。首先使,通过令减去它在方向上的分量,让它正交于

图1.11 2D正交化处理

图1.12 3D正交化处理

接下来,通过令依次减去它在方向与方向上的分量(投影),使之同时正交于

现在我们就得到了所有元素都彼此正交的向量集;最后一步是通过将规范化为单位向量来构建一个规范正交集。

对于具有个向量的一般集合而言,为了将其正交化为规范正交集,我们就要使用格拉姆施密特正交化(Gram-Schmidt Orthogonalization)方法进行处理。

基本步骤:设

对于,令

规范化步骤:令

再次重申,从直观上来说,在将给定集合内的向量添加到规范正交集中时,我们需要令减去它在现有规范正交集中其他向量方向上的分量(投影),这样方可确保新加入规范正交集的向量与该集合中的其他向量互相正交。

向量乘法的第二种形式是叉积(cross product,亦称向量积、外积)。与计算结果为标量的点积不同,叉积的计算结果亦为向量。此外,只有3D向量的叉积有定义(不存在2D向量叉积)。假设3D向量的叉积得到的是另一个向量,则与向量彼此正交。也就是说,向量既正交于,也正交于,如图1.13所示。如果,那么叉积的计算方法为:

  (1.5)


注意  

若实际采用的是右手坐标系,则遵守右手拇指法则(right-hand-thumb rule,有的文献也称之为右手定则):如果伸出右手并拢手指,令它们指向第一个向量的方向,再以的角度弯曲四指,使之指向向量的方向,那么,最后伸直拇指的方向大约为向量的方向。


图1.13 两个3D向量的叉积得到的是:既正交于也正交于的向量。如果伸出左手,使并拢的左手手指指向向量的方向,再以的角度弯曲四指,使之指向向量的方向,
那么最后伸直的大拇指约略指向的即为的方向。
这就是所谓的左手拇指法则(left-hand-thumb rule,有的文献也称之为左手定则)

例1.6

设向量和向量。计算,并验证向量既正交于向量又正交于向量。运用式(1.5),有:

以及

根据计算结果可以明确地得出一项结论:一般来说,即向量的叉积不满足交换律。事实上,我们同时也能够证明,这正是叉积的反交换律。叉积所得的向量可以通过左手拇指法则来加以确认。伸出左手,如果并拢手指指向的为参与叉积运算第一个向量的方向,再弯曲四指指向参与叉积运算第二个向量的方向(总是按两者间较小的夹角弯曲四指。如果无法做到,四指需要向手背方向旋转,则说明手心要转到背对方向,拇指最终指向相反方向),那么伸直的拇指方向即为所求叉积的向量方向,如图1.13所示。

为了证明向量既正交于向量又正交于向量,我们需要用到1.3节中的结论:如果,那么(即两个向量彼此正交)。由于:

以及

由此可以推断出:向量既正交于向量,也正交于向量

我们刚刚证明了:通过叉积可以求出与两个指定3D向量正交的向量。在 2D 空间中虽然不存在这种情况,但是若给定一个2D向量,我们还是能通过与3D向量叉积相似的方法,求出与正交的向量。图1.14从几何角度展示了满足上述条件的向量。形式上的证明也比较简洁:

图1.14 向量的2D伪叉积计算结果是正交于的向量

因此,。同时,不难看出,所以亦可知

在1.3.1节中,我们曾探讨了可以使向量集正交化的方法:格拉姆施密特正交化方法。对于3D情况来讲,还存在另外一种与叉积有关的策略,可使近乎规范正交的向量集完全正交化。但若受数值精度误差累积的影响,也许会导致其成为非规范正交集。图1.15中几何图示所对照的叉积处理流程如下。

图1.15 通过叉积来进行正交化处理3D正交化处理。

1. 令。

2.令。

3.令,根据练习14可知:由于且|||| = |||| = 1,因此。所以,我们最后也就不再需要对它进行规范化处理了。

此时,向量集是规范正交的。


注意  

在上面的示例中,我们首先令,这意味着将向量转换到向量时并未改变方向——仅缩放了的长度而已。但是,向量与向量的方向却可以分别不同于向量和向量。对于特定的应用来说,不改变集合中某个向量的方向也许是件很重要的事。例如,在本书后面,我们会利用3个规范正交向量来表示摄像机(camera)的朝向,而其中的第三个向量描述的正是摄像机的观察方向。在对这些向量进行正交化处理的过程中,我们通常并不希望改变此摄像机的观察方向。所以,我们会运用上面的算法,在第一步中处理向量,再通过修改向量和向量来使它们正交化。


到目前为止,我们一直都在讨论向量,却还没有对位置的概念进行任何描述。然而,在3D程序中是需要我们来指明位置关系的,例如3D几何体的位置和3D虚拟摄像机的位置等。在一个坐标系中,通过一个处于标准位置的向量(见图1.16)就能表示出3D空间中的特定位置,我们称这种向量为位置向量(position vector)。在这种情况下,向量箭头的位置才是值得关注的主要特征,而方向和大小都是无足轻重的。“位置向量”和“点”这两个术语可以互相替代,这是因为一个位置向量足以确定一个点。

然而,用向量表示点也有副作用,在代码中则更为明显,因为部分向量运算对点来说是没有意义的。例如,两点之和的意义何在?但从另一方面来讲,一些运算却可以在点上得到推广。如,可以将两个点的差定义为由点指向点的向量。同样,也可以定义点与向量相加,其意义为:令点沿向量位移而得到点。由于我们用向量来表示坐标系中的点,所以除了刚刚讨论过的几类与点有关的运算外便无须再做其他额外的工作,这是因为利用向量代数的框架就足以解决点的描述问题了,详见图1.17。

图1.16 由原点延伸至目标点的位置向量,用它即可描述目标点在坐标系中的位置 

图1.17 图a通过的两点之差来定义由点指向点的向量。图b中点与向量的和可以定义为:使点沿着向量位移而得到点


注意  

其实还有一种通过几何方式来定义的多点之间的特殊和,即仿射组合(affine combination),这种运算的过程就像求取诸点的加权平均值。


对于Windows 8及其以上版本来讲,DirectXMath(其前身为XNA Math数学库,DirectXMath正是基于此而成)是一款为Direct3D应用程序量身打造的3D数学库,而它也自此成为了Windows SDK的一部分。该数学库采用了SIMD流指令扩展2(Streaming SIMD Extensions 2,SSE2)指令集。借助128位宽的单指令多数据(Single Instruction Multiple Data,SIMD)寄存器,利用一条SIMD指令即可同时对4个32位浮点数或整数进行运算。这对于向量运算带来的益处是不言而喻的。例如,若见到如下的向量加法:

我们按普通的计算方式只能对分量逐个相加。而通过SIMD技术,我们就可以仅用一条SIMD加法指令来取代4条普通的标量指令,从而直接计算出4D向量的加法结果。如果只需要进行3D数据运算,我们仍然可以使用SIMD技术,但是要忽略第4个坐标分量。类似地,对于2D运算,则应忽略第3、4个坐标分量。

我们并不会对DirectXMath库进行全面的介绍,而只是针对本书需要的关键部分进行讲解。关于此库的所有细节,可以参考它的在线文档[DirectXMath]。对于希望了解如何开发一个优秀的SIMD向量库,乃至希望深入理解DirectXMath库设计原理的读者,我们在这里推荐一篇文章《Designing Fast Cross-Platform SIMD Vector Libraries(设计快速的跨平台SIMD向量库)》[Oliveira 2010]。

为了使用DirectXMath库,我们需要向代码中添加头文件#include <DirectXMath.h>,而为了一些相关的数据类型还要加入头文件#include <DirectXPackedVector.h>。除此之外并不需要其他的库文件,因为所有的代码都以内联的方式实现在头文件里。DirectXMath.h文件中的代码都存在于DirectX命名空间之中,而DirectXPackedVector.h文件中的代码则都位于DirectX::PackedVector命名空间以内。另外,针对x86平台,我们需要启用SSE2指令集(Project Properties工程属性)→Configuration Properties配置属性)→C/C++→Code Generation代码生成)→Enable Enhanced Instructon Set启用增强指令集))。对于所有的平台,我们还应当启用快速浮点模型/fp:fast(Project Properties工程属性)→Configuration Properties配置属性)→C/C++Code Generation代码生成)→Floating Point Model浮点模型))。而对于x64平台来说,我们却不必开启SSE2指令集,这是因为所有的x64 CPU对此均有支持。

在DirectXMath库中,核心的向量类型是XMVECTOR,它将被映射到SIMD硬件寄存器。通过SIMD指令的配合,利用这种具有128位的类型能一次性处理4个32位的浮点数。在开启SSE2后,此类型在x86和x64平台的定义是:

typedef __m128 XMVECTOR;

这里的__m128是一种特殊的SIMD类型(定义见xmmintrin.h)。在计算向量的过程中,必须通过此类型才可充分地利用SIMD技术。正如前文所述,我们将通过SIMD技术来处理2D和3D向量运算,而计算过程中用不到的向量分量则将它置零并忽略。

XMVECTOR类型的数据需要按16字节对齐,这对于局部变量和全局变量而言都是自动实现的。至于类中的数据成员,建议分别使用XMFLOAT2(2D向量)、XMFLOAT3(3D向量)和XMFLOAT4(4D向量)类型来加以代替。这些结构体的定义如下[7]

struct XMFLOAT2
{
  float x;
  float y;

  XMFLOAT2() {}
  XMFLOAT2(float _x, float _y) : x(_x), y(_y) {}
  explicit XMFLOAT2(_In_reads_(2) const float *pArray) : 
    x(pArray[0]), y(pArray[1]) {}

  XMFLOAT2& operator= (const XMFLOAT2& Float2) 
  { x = Float2.x; y = Float2.y; return *this; }
};


struct XMFLOAT3
{
  float x;
  float y;
  float z;

  XMFLOAT3() {}
  XMFLOAT3(float _x, float _y, float _z) : x(_x), y(_y), z(_z) {}
  explicit XMFLOAT3(_In_reads_(3) const float *pArray) : 
    x(pArray[0]), y(pArray[1]), z(pArray[2]) {}

  XMFLOAT3& operator= (const XMFLOAT3& Float3) 
  { x = Float3.x; y = Float3.y; z = Float3.z; return *this; }
};

struct XMFLOAT4
{
  float x;
  float y;
  float z;
  float w;

  XMFLOAT4() {}
  XMFLOAT4(float _x, float _y, float _z, float _w) : 
    x(_x), y(_y), z(_z), w(_w) {}
  explicit XMFLOAT4(_In_reads_(4) const float *pArray) : 
    x(pArray[0]), y(pArray[1]), z(pArray[2]), w(pArray[3]) {}

  XMFLOAT4& operator= (const XMFLOAT4& Float4) 
  { x = Float4.x; y = Float4.y; z = Float4.z; w = Float4.w; return 
    *this; }
};

但是,如果直接把上述这些类型用于计算,却依然不能充分发挥出SIMD技术的高效特性。为此,我们还需要将这些类型的实例转换为XMVECTOR类型。转换的过程可以通过DirectXMath库的加载函数(loading function)实现。相反地,DirectXMath库也提供了用来将XMVECTOR类型转换为XMFLOATn类型的存储函数(storage function)。

总结一下:

1.局部变量或全局变量用XMVECTOR类型。

2.对于类中的数据成员,使用XMFLOAT2XMFLOAT3XMFLOAT4类型。

3.在运算之前,通过加载函数将XMFLOATn类型转换为XMVECTOR类型。

4.用XMVECTOR实例来进行运算。

5.通过存储函数将XMVECTOR类型转换为XMFLOATn类型。

用下面的方法将数据从XMFLOATn类型加载到XMVECTOR类型:

// 将数据从XMFLOAT2类型中加载到XMVECTOR类型
XMVECTOR XM_CALLCONV XMLoadFloat2(const XMFLOAT2 *pSource);

// 将数据从XMFLOAT3类型中加载到XMVECTOR类型
XMVECTOR XM_CALLCONV XMLoadFloat3(const XMFLOAT3 *pSource);

// 将数据从XMFLOAT4类型中加载到XMVECTOR类型
XMVECTOR XM_CALLCONV XMLoadFloat4(const XMFLOAT4 *pSource);

用下面的方法可将数据从XMVECTOR类型存储到XMFLOATn类型:

// 将数据从XMVECTOR类型中存储到XMFLOAT2类型
void XM_CALLCONV XMStoreFloat2(XMFLOAT2 *pDestination, FXMVECTOR V);

// 将数据从XMVECTOR类型中存储到XMFLOAT3类型
void XM_CALLCONV XMStoreFloat3(XMFLOAT3 *pDestination, FXMVECTOR V);

// 将数据从XMVECTOR类型中存储到XMFLOAT4类型
void XM_CALLCONV XMStoreFloat4(XMFLOAT4 *pDestination, FXMVECTOR V);

当我们只希望从XMVECTOR实例中得到某一个向量分量或将某一向量分量转换为XMVECTOR类型时,相关的存取方法如下:

float XM_CALLCONV XMVectorGetX(FXMVECTOR V);
float XM_CALLCONV XMVectorGetY(FXMVECTOR V);
float XM_CALLCONV XMVectorGetZ(FXMVECTOR V);
float XM_CALLCONV XMVectorGetW(FXMVECTOR V);

XMVECTOR XM_CALLCONV XMVectorSetX(FXMVECTOR V, float x);
XMVECTOR XM_CALLCONV XMVectorSetY(FXMVECTOR V, float y);
XMVECTOR XM_CALLCONV XMVectorSetZ(FXMVECTOR V, float z);
XMVECTOR XM_CALLCONV XMVectorSetW(FXMVECTOR V, float w);

为了提高效率,可以将XMVECTOR类型的值作为函数的参数,直接传送至SSE/SSE2寄存器(register)里,而不存于栈(stack)内。以此方式传递的参数数量取决于用户使用的平台(例如,32位的Windows系统、64位的Windows系统及Windows RT系统所能传递的参数数量都各不相同)和编译器。因此,为了使代码更具通用性,不受具体平台、编译器的影响,我们将利用FXMVECTORGXMVECTORHXMVECTORCXMVECTOR类型来传递XMVECTOR类型的参数。基于特定的平台和编译器,它们会被自动地定义为适当的类型。此外,一定要把调用约定注解XM_CALLCONV加在函数名之前,它会根据编译器的版本确定出对应的调用约定属性。

传递XMVECTOR参数的规则如下:

1.前3个XMVECTOR参数应当用类型FXMVECTOR

2.第4个XMVECTOR参数应当用类型GXMVECTOR

3.第5、6个XMVECTOR参数应当用类型HXMVECTOR

4.其余的XMVECTOR参数应当用类型CXMVECTOR

下面详解这些类型在32位Windows平台和编译器(编译器需要支持__fastcall和新增的__vectorcall调用约定)上的定义:

// 在32位的Windows系统上,编译器将根据__fastcall调用约定将前3个
// XMVECTOR参数传递到寄存器中,而把其余参数都存在栈上
typedef const XMVECTOR FXMVECTOR;
typedef const XMVECTOR& GXMVECTOR;
typedef const XMVECTOR& HXMVECTOR;
typedef const XMVECTOR& CXMVECTOR;

// 在32位的Windows系统上,编译器将通过__vectorcall调用约定将前6个
// XMVECTOR参数传递到寄存器中,而把其余参数均存在栈上
typedef const XMVECTOR FXMVECTOR;
typedef const XMVECTOR GXMVECTOR;
typedef const XMVECTOR HXMVECTOR;
typedef const XMVECTOR& CXMVECTOR;

对于这些类型在其他平台的定义细节,可参见DirectXMath库文档中“Library Internals(库的内部细节)”下的“Calling Conventions(调用约定)”部分[DirectXMath]。构造函数(constructor)方法对于这些规则来讲却是个例外。[DirectXMath]建议,在编写构造函数时,前3个XMVECTOR参数用FXMVECTOR类型,其余XMVECTOR参数则用CXMVECTOR类型。另外,对于构造函数不要使用XM_CALLCONV注解。

以下示例截取自DirectXMath库的源代码:

inline XMMATRIX XM_CALLCONV XMMatrixTransformation(
  FXMVECTOR ScalingOrigin, 
  FXMVECTOR ScalingOrientationQuaternion,
  FXMVECTOR Scaling, 
  GXMVECTOR RotationOrigin, 
  HXMVECTOR RotationQuaternion, 
  HXMVECTOR Translation);

此函数有6个XMVECTOR参数,根据参数传递法则,前3个参数用FXMVECTOR类型,第4个参数用GXMVECTOR类型,第5个和第6个参数则用HXMVECTOR类型。

XMVECTOR类型的参数之间,我们也可以掺杂其他非XMVECTOR类型的参数。此时,XMVECTOR参数的规则依然适用,而在统计XMVECTOR参数的数量时,会对其他类型的参数视若无睹。例如,在下列函数中,前3个XMVECTOR参数的类型依旧为FXMVECTOR,第4个XMVECTOR参数的类型仍为GXMVECTOR

inline XMMATRIX XM_CALLCONV XMMatrixTransformation2D(
  FXMVECTOR ScalingOrigin, 
  float   ScalingOrientation, 
  FXMVECTOR Scaling, 
  FXMVECTOR RotationOrigin, 
  float   Rotation, 
  GXMVECTOR Translation);

传递XMVECTOR参数的规则仅适用于“输入”参数。“输出”的XMVECTOR参数(即XMVECTOR&XMVECTOR*)则不会占用SSE/SSE2寄存器,所以它们的处理方式与非XMVECTOR类型的参数一致。

XMVECTOR类型的常量实例应当用XMVECTORF32类型来表示。在DirectX SDK中的CascadedShadowMaps11示例内就可见到这种类型的应用:

static const XMVECTORF32 g_vHalfVector = { 0.5f, 0.5f, 0.5f, 0.5f };
static const XMVECTORF32 g_vZero = { 0.0f, 0.0f, 0.0f, 0.0f };

XMVECTORF32 vRightTop = {
vViewFrust.RightSlope,
vViewFrust.TopSlope,
1.0f,1.0f
};

XMVECTORF32 vLeftBottom = {
vViewFrust.LeftSlope,
vViewFrust.BottomSlope,
1.0f,1.0f
};

基本上,在我们运用初始化语法的时候就要使用XMVECTORF32类型。

XMVECTORF32是一种按16字节对齐的结构体,数学库中还提供了将它转换至XMVECTOR类型的运算符。其定义如下:

// 将常向量转换为其他类型的运算符
__declspec(align(16)) struct XMVECTORF32
{
  union
  {
    float f[4];
    XMVECTOR v;
  };

  inline operator XMVECTOR() const { return v; }
  inline operator const float*() const { return f; }
#if !defined(_XM_NO_INTRINSICS_) && defined(_XM_SSE_INTRINSICS_)
  inline operator __m128i() const { return _mm_castps_si128(v); }
  inline operator __m128d() const { return _mm_castps_pd(v); }
#endif
};

另外,也可以通过XMVECTORU32类型来创建由整型数据构成的XMVECTOR常向量:

static const XMVECTORU32 vGrabY = {
0x00000000,0xFFFFFFFF,0x00000000,0x00000000
};

XMVECTOR类型针对向量的加法运算、减法运算和标量乘法运算,都分别提供了对应的重载运算符。

XMVECTOR  XM_CALLCONV   operator+ (FXMVECTOR V);
XMVECTOR  XM_CALLCONV   operator- (FXMVECTOR V);

XMVECTOR&  XM_CALLCONV   operator+=(XMVECTOR& V1, FXMVECTOR V2);
XMVECTOR&  XM_CALLCONV   operator-= (XMVECTOR& V1, FXMVECTOR V2);
XMVECTOR&  XM_CALLCONV   operator*= (XMVECTOR& V1, FXMVECTOR V2);
XMVECTOR&  XM_CALLCONV   operator/= (XMVECTOR& V1, FXMVECTOR V2);

XMVECTOR&  operator*= (XMVECTOR& V, float S);
XMVECTOR&  operator/= (XMVECTOR& V, float S);

XMVECTOR  XM_CALLCONV   operator+ (FXMVECTOR V1, FXMVECTOR V2);
XMVECTOR  XM_CALLCONV   operator- (FXMVECTOR V1, FXMVECTOR V2);
XMVECTOR  XM_CALLCONV   operator* (FXMVECTOR V1, FXMVECTOR V2);
XMVECTOR  XM_CALLCONV   operator/ (FXMVECTOR V1, FXMVECTOR V2);
XMVECTOR  XM_CALLCONV   operator* (FXMVECTOR V, float S);
XMVECTOR  XM_CALLCONV   operator* (float S, FXMVECTOR V);
XMVECTOR  XM_CALLCONV   operator/ (FXMVECTOR V, float S);

DirectXMath库定义了一组与有关的常用数学常量近似值:

const float XM_PI    =   3.141592654f;
const float XM_2PI   =   6.283185307f;
const float XM_1DIVPI    =  0.318309886f;
const float XM_1DIV2PI   =  0.159154943f;
const float XM_PIDIV2    =  1.570796327f;
const float XM_PIDIV4    =  0.785398163f;

另外,它用下列内联函数实现了弧度和角度间的互相转化:

inline float XMConvertToRadians(float fDegrees)
{ return fDegrees * (XM_PI / 180.0f); }
inline float XMConvertToDegrees(float fRadians)
{ return fRadians * (180.0f / XM_PI); }

DirectXMath库还定义了求出两个数间较大值及较小值的函数:

template<class T> inline T XMMin(T a, T b) { return (a < b) ? a : b; }
template<class T> inline T XMMax(T a, T b) { return (a > b) ? a : b; }

DirectXMath库提供了下列函数,以设置XMVECTOR类型中的数据:

// 返回零向量0
XMVECTOR XM_CALLCONV XMVectorZero();

// 返回向量(1, 1, 1, 1)
XMVECTOR XM_CALLCONV XMVectorSplatOne();

// 返回向量(x, y, z, w)
XMVECTOR XM_CALLCONV XMVectorSet(float x, float y, float z, float w);

// 返回向量(Value, Value, Value, Value)
XMVECTOR XM_CALLCONV XMVectorReplicate(float Value);

// 返回向量(vx, vx, vx, vx) 
XMVECTOR XM_CALLCONV XMVectorSplatX(FXMVECTOR V);

// 返回向量(vy, vy, vy, vy) 
XMVECTOR XM_CALLCONV XMVectorSplatY(FXMVECTOR V);

// 返回向量(vz, vz, vz, vz) 
XMVECTOR XM_CALLCONV XMVectorSplatZ(FXMVECTOR V);

下列的示例程序详细地解释了上面大多数函数的用法:

#include <windows.h> // 为了使XMVerifyCPUSupport函数返回正确值
#include <DirectXMath.h>
#include <DirectXPackedVector.h>
#include <iostream>
using namespace std;
using namespace DirectX;
using namespace DirectX::PackedVector;

// 重载"<<"运算符,这样就可以通过cout函数输出XMVECTOR对象
ostream& XM_CALLCONV operator<<(ostream& os, FXMVECTOR v)
{
  XMFLOAT3 dest;
  XMStoreFloat3(&dest, v);

  os << "(" << dest.x << ", " << dest.y << ", " << dest.z << ")";
  return os;
}

int main()
{
  cout.setf(ios_base::boolalpha);

  // 检查是否支持SSE2指令集 (Pentium4, AMD K8及其后续版本的处理器)
  if (!XMVerifyCPUSupport())
  {
    cout << "directx math not supported" << endl;
    return 0;
  }

  XMVECTOR p = XMVectorZero();
  XMVECTOR q = XMVectorSplatOne();
  XMVECTOR u = XMVectorSet(1.0f, 2.0f, 3.0f, 0.0f);
  XMVECTOR v = XMVectorReplicate(-2.0f);
  XMVECTOR w = XMVectorSplatZ(u);

  cout << "p = " << p << endl;
  cout << "q = " << q << endl;
  cout << "u = " << u << endl;
  cout << "v = " << v << endl;
  cout << "w = " << w << endl;

  return 0;
}

上述示例程序的输出结果如图1.18所示。

图1.18 示例程序输出的结果

DirectXMath库提供了下面的函数来执行各种向量运算。我们主要围绕3D向量的运算函数进行讲解,类似的运算还有2D和4D版本。除了表示维度的数字不同以外,这几种版本的函数名皆同。

XMVECTOR XM_CALLCONV XMVector3Length(      // 返回||v||
  FXMVECTOR V);                            // 输入向量v

XMVECTOR XM_CALLCONV XMVector3LengthSq(    //返回||v||2
  FXMVECTOR V);                            // 输入向量v

XMVECTOR XM_CALLCONV XMVector3Dot(         // 返回v1·v
  FXMVECTOR V1,                            // 输入向量v1
  FXMVECTOR V2);                           // 输入向量v2

XMVECTOR XM_CALLCONV XMVector3Cross(       // 返回v1×v2
  FXMVECTOR V1,                            // 输入向量v1
  FXMVECTOR V2);                           // 输入向量v2

XMVECTOR XM_CALLCONV XMVector3Normalize(   // 返回v/||v||
  FXMVECTOR V);                            // 输入向量v

XMVECTOR XM_CALLCONV XMVector3Orthogonal(  // 返回一个正交于v的向量
  FXMVECTOR V);                            // 输入向量v

XMVECTOR XM_CALLCONV
XMVector3AngleBetweenVectors(             // 返回v1和v2之间的夹角
  FXMVECTOR V1,                           // 输入向量v1
  FXMVECTOR V2);                          // 输入向量v2

void XM_CALLCONV XMVector3ComponentsFromNormal(
  XMVECTOR* pParallel,                   // 返回projn(v)
  XMVECTOR* pPerpendicular,              // 返回perpn(v)
  FXMVECTOR V,                           // 输入向量v
  FXMVECTOR Normal);                     // 输入规范化向量n

bool XM_CALLCONV XMVector3Equal(     // 返回v1 == v2?
  FXMVECTOR V1,                      // 输入向量v1
  FXMVECTOR V2);                     // 输入向量v2

bool XM_CALLCONV XMVector3NotEqual(  // 返回v1v
  FXMVECTOR V1,                      // 输入向量v1
  FXMVECTOR V2);                     // 输入向量v2


注意  

可以看到,即使在数学上计算的结果是标量(如点积),但这些函数所返回的类型依旧是XMVECTOR,而得到的标量结果则被复制到XMVECTOR中的各个分量之中。例如点积,此函数返回的向量为()。这样做的原因之一是:将标量和SIMD向量的混合运算次数降到最低,使用户除了自定义的计算之外全程都使用SIMD技术,以提升计算效率。


下面的程序演示了如何使用上述大部分函数,其中还示范了一些重载运算符的用法:

#include <windows.h> // 为了使XMVerifyCPUSupport函数返回正确值
#include <DirectXMath.h>
#include <DirectXPackedVector.h>
#include <iostream>
using namespace std;
using namespace DirectX;
using namespace DirectX::PackedVector;

// 对"<<"运算符进行重载,这样就可以通过cout函数输出XMVECTOR对象
ostream& XM_CALLCONV operator<<(ostream& os, FXMVECTOR v)
{
  XMFLOAT3 dest;
  XMStoreFloat3(&dest, v);

  os << "(" << dest.x << ", " << dest.y << ", " << dest.z << ")";
  return os;
}

int main()
{
  cout.setf(ios_base::boolalpha);

  // 检查是否支持SSE2指令集 (Pentium4, AMD K8及其后续版本的处理器)
  if (!XMVerifyCPUSupport())
  {
    cout << "directx math not supported" << endl;
    return 0;
  }

  XMVECTOR n = XMVectorSet(1.0f, 0.0f, 0.0f, 0.0f);
  XMVECTOR u = XMVectorSet(1.0f, 2.0f, 3.0f, 0.0f);
  XMVECTOR v = XMVectorSet(-2.0f, 1.0f, -3.0f, 0.0f);
  XMVECTOR w = XMVectorSet(0.707f, 0.707f, 0.0f, 0.0f);

  // 向量加法:利用XMVECTOR类型的加法运算符+  
  XMVECTOR a = u + v;

  // 向量减法:利用XMVECTOR类型的减法运算符- 
  XMVECTOR b = u - v;

  // 标量乘法:利用XMVECTOR类型的标量乘法运算符* 
  XMVECTOR c = 10.0f*u;

  // ||u||
  XMVECTOR L = XMVector3Length(u);

  // d = u / ||u||
  XMVECTOR d = XMVector3Normalize(u);

  // s = u dot v
  XMVECTOR s = XMVector3Dot(u, v);

  // e = u x v 
  XMVECTOR e = XMVector3Cross(u, v);

  // 求出proj_n(w)和perp_n(w)
  XMVECTOR projW;
  XMVECTOR perpW;
  XMVector3ComponentsFromNormal(&projW, &perpW, w, n);

  // projW + perpW == w?
  bool equal = XMVector3Equal(projW + perpW, w) != 0;
  bool notEqual = XMVector3NotEqual(projW + perpW, w) != 0;

  // projW与perpW之间的夹角应为90度
  XMVECTOR angleVec = XMVector3AngleBetweenVectors(projW, perpW);
  float angleRadians = XMVectorGetX(angleVec);
  float angleDegrees = XMConvertToDegrees(angleRadians);

  cout << "u             = " << u << endl;
  cout << "v             = " << v << endl;
  cout << "w             = " << w << endl;
  cout << "n             = " << n << endl;
  cout << "a = u + v     = " << a << endl;
  cout << "b = u - v     = " << b << endl;
  cout << "c = 10 * u    = " << c << endl;
  cout << "d = u / ||u|| = " << d << endl;
  cout << "e = u x v     = " << e << endl;
  cout << "L = ||u||     = " << L << endl;
  cout << "s = u.v       = " << s << endl;
  cout << "projW         = " << projW << endl;
  cout << "perpW         = " << perpW << endl;
  cout << "projW + perpW == w = " << equal << endl;
  cout << "projW + perpW != w = " << notEqual << endl;
  cout << "angle         = " << angleDegrees << endl;

  return 0;
}

上述示例程序的输出结果如图1.19所示。

图1.19 示例程序的输出结果


注意  

DirectXMath库也提供了一些估算方法,精度低但速度快。如果愿意为了速度而牺牲一些精度,则可以使用它们。下面是两个估算方法的例子。

XMVECTOR XM_CALLCONV XMVector3LengthEst(     // 返回估算值||v||
  FXMVECTOR V);                              // 输入v
 
XMVECTOR XM_CALLCONV XMVector3NormalizeEst(  // 返回估算值v/||v||
  FXMVECTOR V);                             // 输入v


在用计算机处理与向量有关的工作时,我们应当了解以下的内容。在比较浮点数时,一定要注意浮点数存在的误差。我们认为相等的两个浮点数可能会因此而有细微的差别。例如,已知在数学上规范化向量的长度为1,但是在计算机程序中的表达上,向量的长度只能接近于1。此外,在数学中,对于任意实数。但是,当只能在数值上逼近1时,随着幂的增加,所求近似值的误差也在逐渐增大。由此可见,数值误差是可积累的。下面这个小程序可印证这些观点:

#include <windows.h> // 为了使XMVerifyCPUSupport函数返回正确值
#include <DirectXMath.h>
#include <DirectXPackedVector.h>
#include <iostream>
using namespace std;
using namespace DirectX;
using namespace DirectX::PackedVector;

int main()
{
  cout.precision(8);

  // 检查是否支持SSE2指令集 (Pentium4, AMD K8及其后续版本的处理器)
  if (!XMVerifyCPUSupport())
  {
    cout << "directx math not supported" << endl;
    return 0;
  }

  XMVECTOR u = XMVectorSet(1.0f, 1.0f, 1.0f, 0.0f);
  XMVECTOR n = XMVector3Normalize(u);

  float LU = XMVectorGetX(XMVector3Length(n));

  // 在数学上,此向量的长度应当为1。在计算机中的数值表达上也是如此吗?
  cout << LU << endl;
  if (LU == 1.0f)
    cout << "Length 1" << endl;
  else
    cout << "Length not 1" << endl;

  // 1的任意次方都是1。但是在计算机中,事实确实如此吗?
  float powLU = powf(LU, 1.0e6f);
  cout << "LU^(10^6) = " << powLU << endl;
}

上述示例程序的输出结果如图1.20所示。

图1.20 示例程序输出的结果

为了弥补浮点数精确性上的不足,我们通过比较两个浮点数是否近似相等来加以解决。在比较的时候,我们需要定义一个Epsilon常量,它是个非常小的值,可为误差留下一定的“缓冲”余地。如果两个数相差的值小于Epsilon,我们就说这两个数是近似相等的。换句话说,Epsilon是针对浮点数的误差问题所指定的容差(tolerance)。下面的函数解释了如何利用Epsilon来检测两个浮点数是否相等:

const float Epsilon = 0.001f;
bool Equals(float lhs, float rhs)
{
    // lhs和rhs相差的值是否小于EPSILON?
    return fabs(lhs - rhs) < Epsilon ? true : false;
}

对此,DirectXMath库提供了XMVector3NearEqual函数,用于以Epsilon作为容差,测试比较的向量是否相等:

// 返回 
//  abs(U.x – V.x) <= Epsilon.x && 
//  abs(U.y – V.y) <= Epsilon.y &&
//  abs(U.z – V.z) <= Epsilon.z
XMFINLINE bool XM_CALLCONV XMVector3NearEqual(
  FXMVECTOR U, 
  FXMVECTOR V, 
  FXMVECTOR Epsilon);

1.向量可以用来模拟同时具有大小和方向的物理量。在几何学上,我们用有向线段表示向量。当向量平移至尾部与所在坐标系原点恰好重合的位置时,向量位于标准位置。一旦向量处于标准位置,我们便可以用向量头部相对于坐标系的坐标来作为它的数学描述。

2.假设有向量和向量,那么就能对它们进行下列向量计算。

(a)加法运算:

(b)减法运算:

(c)标量乘法运算:

(d)向量长度:

(e)规范化:

(f)点积:

(g)叉积:

3.用DirectXMath库的XMVECTOR类型来描述向量,这样就可以在代码中利用SIMD技术进行高效的运算。对于类中的数据成员来说,要使用XMFLOAT2XMFLOAT3XMFLOAT4这些类表示向量,并通过加载和存储方法令数据在XMVECTOR类型与XMFLOATn类型之间互相转化。另外,在使用常向量的初始化语法时,应当采用XMVECTORF32类型。

4.为了提高效率,当XMVECTOR类型的值被当作参数传入函数时,可以直接存入SSE/SSE2寄存器中而不是栈上。要令代码与平台无关,我们将使用FXMVECTORGXMVECTORHXMVECTORCXMVECTOR类型来传递XMVECTOR参数。传递XMVECTOR参数的规则为:前3个XMVECTOR参数应当用FXMVECTOR类型,第4个XMVECTOR参数用GXMVECTOR类型,第5个和第6个XMVECTOR参数用HXMVECTOR类型,而其余的XMVECTOR类型参数则用CXMVECTOR类型。

5.XMVECTOR类重载了一些运算符用来实现向量的加法、减法和标量乘法。另外,DirectXMath库还提供了下面一些实用的函数,用于计算向量的模、模的平方、两个向量的点积、两个向量的叉积以及对向量进行规范化处理:

  XMVECTOR XM_CALLCONV XMVector3Length(FXMVECTOR V);
  XMVECTOR XM_CALLCONV XMVector3LengthSq(FXMVECTOR V);
  XMVECTOR XM_CALLCONV XMVector3Dot(FXMVECTOR V1, FXMVECTOR V2);
  XMVECTOR XM_CALLCONV XMVector3Cross(FXMVECTOR V1, FXMVECTOR V2);
  XMVECTOR XM_CALLCONV XMVector3Normalize(FXMVECTOR V);


1.设向量和向量。写出下列各式的演算过程,并在2D坐标系内画出相应的向量。

(a)

(b)

(c)

(d)

2.设向量和向量。写出下列问题的解答过程。

(a)

(b)

(c)

(d)

3.本习题展示了向量代数与实数所共有的一些计算性质(注意,以下清单中所列举的性质并不完整)。假设有向量,另有标量,请证明下列向量性质。

(a)(加法交换律)

(b)(加法结合律)

(c)(标量乘法的结合律)

(d)(分配律1)

(e)(分配律2)


提示 

仅利用向量运算的定义和实数的性质即可完成证明。例如,


4.根据等式,求其中的向量

5.设向量和向量。对进行规范化处理。

6.设为标量,向量。求证

7.下列各组向量中,之间的夹角是直角、锐角还是钝角?

(a)

(b)

(c)

8.设向量和向量。计算之间的夹角

9.设向量,且为标量。证明下列点积性质。

(a)

(b)

(c)

(d)

(e)


提示 

仅利用前文介绍的各种定义即可证明,例如,


10.利用余弦定理(,其中分别是三角形3条边的边长,之间的夹角)来证明:

提示 

参考图1.9,设以及,再运用上一个习题中得到的点积性质即可。


11.设向量。将向量 分解为两个相互正交的向量之和,使它们一个平行于、一个正交于。最后,在同一2D坐标系中画出这些向量。

12.设向量和向量。求向量,再证明

13.设三点在某坐标系中定义了一个三角形。求出一正交于此三角形的向量。


提示 

先求出位于三角形任意两条边上的两个向量,再对它们进行叉积运算即可。


14.证明。


提示 

一侧开始证明,先利用三角恒等式,再运用式(1.4)。


15.证明:由向量和向量张成的平行四边形面积为,如图1.21所示[8]

图1.21 由向量和向量张成的平行四边形。此平行四边形的底为||||且高为

16.举例证明:存在3D向量,满足。这说明叉积一般不满足结合律。


提示 

考虑这个简单的向量组合:


17.证明两个非零且相互平行向量的叉积为零向量,即


提示 

直接利用叉积定义即可。


18.利用格拉姆—施密特正交化方法,令向量集规范正交化。

19.思考下面的程序及其输出结果(见图1.22)。猜测其中每个XMVector*函数的功能。然后在DirectXMath文档中,查阅每个函数的相关信息[9]

#include <windows.h> // 为了使用XMVerifyCPUSupport函数返回正确值
#include <DirectXMath.h>
#include <DirectXPackedVector.h>
#include <iostream>
using namespace std;
using namespace DirectX;
using namespace DirectX::PackedVector;

// 重载"<<"运算符,这样便可以使用cout输出XMVECTOR对象
ostream& XM_CALLCONV operator<<(ostream& os, FXMVECTOR v)
{
  XMFLOAT4 dest;
  XMStoreFloat4(&dest, v);

  os << "(" << dest.x << ", " << dest.y << ", " 
     << dest.z << ", " << dest.w << ")";
  return os;
}

int main()
{
  cout.setf(ios_base::boolalpha);

  // 检查是否支持SSE2指令集 (Pentium4, AMD K8及其后续版本的处理器)
  if (!XMVerifyCPUSupport())
  {
    cout << "directx math not supported" << endl;
    return 0;
  }

  XMVECTOR p = XMVectorSet(2.0f, 2.0f, 1.0f, 0.0f);
  XMVECTOR q = XMVectorSet(2.0f, -0.5f, 0.5f, 0.1f);
  XMVECTOR u = XMVectorSet(1.0f, 2.0f, 4.0f, 8.0f);
  XMVECTOR v = XMVectorSet(-2.0f, 1.0f, -3.0f, 2.5f);
  XMVECTOR w = XMVectorSet(0.0f, XM_PIDIV4, XM_PIDIV2, XM_PI);

  cout << "XMVectorAbs(v)        = " << XMVectorAbs(v) << endl;
  cout << "XMVectorCos(w)        = " << XMVectorCos(w) << endl;
  cout << "XMVectorLog(u)        = " << XMVectorLog(u) << endl;
  cout << "XMVectorExp(p)        = " << XMVectorExp(p) << endl;

  cout << "XMVectorPow(u, p)     = " << XMVectorPow(u, p) << endl;
  cout << "XMVectorSqrt(u)       = " << XMVectorSqrt(u) << endl;

  cout << "XMVectorSwizzle(u, 2, 2, 1, 3) = "
       << XMVectorSwizzle(u, 2, 2, 1, 3) << endl;
  cout << "XMVectorSwizzle(u, 2, 1, 0, 3) = "
       << XMVectorSwizzle(u, 2, 1, 0, 3) << endl;

  cout << "XMVectorMultiply(u, v)     = " << XMVectorMultiply(u, v) << endl;
  cout << "XMVectorSaturate(q)        = " << XMVectorSaturate(q) << endl;
  cout << "XMVectorMin(p, v)          = " << XMVectorMin(p, v) << endl;
  cout << "XMVectorMax(p, v)          = " << XMVectorMax(p, v) << endl;

  return 0;
}

图1.22 上述程序输出的结果

[1] “净”的对应英文为net,大抵表示为最终合成的总效果,如净方向即质点在不同力的作用下所移动的方向(也就是这几个作用力的合力方向)。后文同。

[2] 本书中所使用的术语“标架”(frame)、“参考系”(frame of reference)、“空间”(space)和“坐标系”(coordinate system)皆表示相同的意义。——原作者注

[3] 准确地说还应考虑到气压因素。

[4] 这里所讲的都是推断坐标系各轴大致方向的办法,所谓“弯曲四指”意即找寻与“弯曲之前”垂直的坐标轴。下同。

[5] 毕达哥拉斯定理即勾股定理。西方文献常称勾股定理为毕达哥拉斯定理。

[6] 看到normalize这个词的各种译法就让我咬牙切齿!这个词在不同的学科里有着不同的译法,就算是同一学科的不同文献、不同词典的译法也是各异,如标准化、归一化、正常化、规格化、正态化、单位化……而且现在各种讨论中大多是统计学方面的,不同人给出的解释也各有差异,刨根问底也找不出图形学方面的译法。这就与“向量”相似,译作“矢量”也可,而且用这两种译法的书籍皆有。故现以数学文献和主流网站上的译法为准,基本上称区间、范围为“归一化”,名词向量或空间等译作“规范化”。当然,也有我水平不足之嫌。写这段话的目的其实就是希望读者不要过分拘泥于名词译法,个人以为只要在查看各种文献、与他人交流知道彼此在谈什么即可,其他地方也是如此。

[7] DirectXMath.h头文件会随着DirectXMath库版本的更新而变更,因此可能会有若干细节与读者所用的版本不符。DirectXMath库中的其他文件也存在这种情况。此库当前的主要维护人是Chuck Walbourn。读者可以访问Chuck Walbourn的博客或GitHub网站以获得最新信息。

[8] 私以为平行四边形的高应垂直于向量

[9] 注意XMVectorCos函数的输出结果。


在计算机3D图形学中,我们利用矩阵简洁地描述几何体的变换,例如缩放、旋转和平移。除此之外,还可借助矩阵将点或向量的坐标在不同的标架之间进行转换。在本章中,我们将探索与矩阵有关的数学知识。

学习目标:

1.理解矩阵及其相关运算的定义。

2.探究为何能将向量与矩阵的乘法视为一种线性组合。

3.学习单位矩阵、转置矩阵、行列式以及矩阵的逆等概念。

4.逐步熟悉DirectXMath库中提供的关于矩阵计算的类与函数的子集。

一个规模为矩阵(matrix),是由列实数所构成的矩形阵列。[1]行数和列数的乘积表示了矩阵的维度。矩阵中的数字则称作元素(element)或(entry)。通过双下标表示法指定元素的行和列就可以确定出对应的矩阵元素,表示的是矩阵中第行、第列的元素。

例2.1

考察下列矩阵:

1.是一个矩阵,是一个矩阵,是一个矩阵,是一个矩阵。

2.我们通过双下标表示法 =−5将矩阵中第4行第2列的元素指定为−5,并以表示矩阵中第2行第1列的这一元素。

3.是两种特殊矩阵,分别只由一行元素或一列元素构成。由于它们常用于以矩阵的形式来表示一个向量(例如,我们可以自由地交替使用(, , )与[, , ]这两种向量记法),因此有时候也分别称它们为行向量或列向量。观察可知,在表示行向量和列向量的矩阵元素时不必再采用双下标——使用单下标记法即可。

在某些情况下,我们倾向于把矩阵的每一行都看作一个向量。例如,可以把矩阵写作:

其中,。 在这种表达方式中,第一个索引表示特定的行,第二个索引“*”表示该行的整个行向量。而且,对于矩阵的列也有类似的定义:

其中

在这种表达方法中,第二个索引表示特定的列,第一个索引“*”表示该列的整个列向量。

现在来定义矩阵相等、加法运算、标量乘法运算和减法运算。

1.两个矩阵相等,当且仅当这两个矩阵的对应元素相等。为了加以比较,两者必有相同的行数和列数。

2.两个矩阵的加法运算,即将两者对应的元素相加。同理,只有行数和列数都分别相同的两个矩阵相加才有意义。

3.矩阵的标量乘法就是将一个标量依次与矩阵内的每个元素相乘。

4.利用矩阵的加法和标量乘法可以定义出矩阵的减法,即

例2.2

那么,

(i)

(ii)

(iii)

(iv)

由于在矩阵的加法和标量乘法的运算过程中,是以元素为单位展开计算的,所以它们实际上也分别从实数运算中继承了下列性质:

1.          加法交换律

2.   加法结合律

3.        标量乘法对矩阵加法的分配律

4.        矩阵乘法对标量加法的分配律

如果是一个矩阵,是一个矩阵,那么,两者乘积的结果是一个规模为的矩阵。矩阵中第行、第列的元素,由矩阵的第个行向量与矩阵的第个列向量的点积求得,即:

  (2.1)

要注意的是,为了使矩阵乘积有意义,矩阵中的列数与矩阵中的行数必须相同。也就是说,矩阵中行向量的维数(可认为是分量的个数)与矩阵中列向量的维数要一致。如果二者的维数不同,那么式(2.1)中的点积运算没有意义。

例2.3

因为矩阵的行向量维数为2,矩阵的列向量维数为3,所以乘积无定义。不妨这样想,由于2D向量不能与3D向量进行点积计算,因此,矩阵中的第一个行向量与矩阵中的第一个列向量也就无法开展点积运算。

例2.4

由于矩阵的列数与矩阵的行数相同,可首先指出乘积是有意义的(其结果是一个矩阵)。根据式(2.1)可以得到:

我们还可以发现乘积却没有意义,因为矩阵的列数不等于矩阵的行数。这表明,矩阵的乘法一般不满足交换律,即

考虑下列向量与矩阵的乘法运算:

可以观察到:该例中,的计算结果是一个规模为的行向量。现在运用式(2.1)即可得到:

因此,

  (2.2)

式(2.2)实为一种线性组合(linear combination),这意味着向量与矩阵的乘积就相当于:向量给定的标量系数与矩阵中各行向量的线性组合。注意,尽管我们只展示了行向量与矩阵的乘法,但是这个结论却具有一般性。也就是说,对于一个行向量与一个矩阵,我们总可得到所给出的标量系数与中诸行向量的线性组合

  (2.3)

矩阵的乘法运算具有一些很有用的代数性质。例如,矩阵乘法对矩阵加法的分配律以及。除此之外,我们还会不时地用到矩阵乘法的结合律,可借此来决定矩阵乘法的计算顺序:

转置矩阵(transpose matrix)指的是将原矩阵的行与列进行互换所得到的新矩阵。所以,根据一个矩阵可得到一个规模为的转置矩阵。我们将矩阵的转置矩阵记作

例2.5

求出下列3个矩阵的转置矩阵:

上面提到,通过互换矩阵的行和列来求出转置矩阵,于是得到:

转置矩阵具有下列实用性质:

1.

2.

3.

4.

5.

单位矩阵(identity matrix)比较特殊,是一种主对角线上的元素均为1,其他元素都为0的方阵。

例如,下列依次是规模为的单位矩阵:

单位矩阵是矩阵的乘法单位元(multiplicative identity)。即如果矩阵,矩阵,而的单位矩阵,那么

换句话说,任何矩阵与单位矩阵相乘,得到的依然是原矩阵。我们可以将单位矩阵看作是矩阵中的“数字1”。特别地,如果是一个方阵,那么它与单位矩阵的乘法满足交换律:

例2.6

以及,证明

运用式(2.1)得:

所以是正确的。

例2.7

= [−1, 2]且。验证

应用式(2.1),可得:

另外可以看出,我们无法计算乘积,因为此矩阵乘法是无定义的。

行列式是一种特殊的函数,它以一个方阵作为输入,并输出一个实数。方阵的行列式通常表示为det 。我们可以从几何的角度来解释行列式。行列式反映了在线性变换下,(维多面体)体积变化的相关信息[2]。另外,行列式也应用于解线性方程组的克莱姆法则(Cramer’s Rule,亦称克莱默法则)。然而,我们在此学习行列式的主要目的是:利用它推导出求逆矩阵的公式(第2.7节的主题)。此外,行列式还可以用于证明:方阵是可逆的当且仅当。这个结论很实用,因为它为我们确认矩阵的可逆性提供了一种行之有效的计算工具。不过在定义行列式之前,我们先要介绍一下余子阵的概念。

指定一个的矩阵余子阵(minor matrix)[3].即为从中去除第行和第列的矩阵。

例2.8

求出下列矩阵的余子阵

去除矩阵的第一行和第一列,得到为:

去除矩阵的第二行和第二列,得到为:

去除矩阵的第一行和第三列,得到为:

矩阵的行列式有一种递归定义。例如,一个矩阵的行列式要根据矩阵的行列式来定义,而矩阵的行列式要靠矩阵的行列式来定义,最后,矩阵的行列式则依赖于矩阵的行列式来定义(矩阵的行列式被简单地定义为)。

为一个矩阵。那么,当时,我们定义:

  (2.4)

对照余子阵的定义可知,对于矩阵来说,其相应的行列式公式为:

对于矩阵来说,其行列式计算公式为:

对于矩阵,其行列式计算公式为:

在3D图形学中,主要使用矩阵。因此,我们不再继续推导的行列式公式。

例2.9

求矩阵的行列式。

我们有

为一个矩阵。乘积称为元素的代数余子式(cofactor of )。如果为矩阵中的每个元素分别计算出,并将它置于矩阵中第行、第列的相应位置,那么将获得矩阵的代数余子式矩阵(cofactor matrix of ):

若取矩阵的转置矩阵,将得到矩阵的伴随矩阵(adjoint matrix of ),记作:

  (2.5)

在下一节中,我们将学习利用带有伴随矩阵的公式来计算逆矩阵。

矩阵代数不存在除法运算的概念[4],但是却另外定义了一种矩阵乘法的逆运算。下面总结了与矩阵逆运算有关的关键信息。

1.只有方阵才具有逆矩阵。因此,当提到逆矩阵时,我们便假设要处理的是一个方阵。

2.矩阵的逆也是一个矩阵,并表示为

3.不是每个方阵都有逆矩阵。存在逆矩阵的方阵称为可逆矩阵(invertible matrix),不存在逆矩阵的方阵称作奇异矩阵(singular matrix)。

4.可逆矩阵的逆矩阵是唯一的。

5.矩阵与其逆矩阵相乘将得到单位方阵:。可以发现,矩阵与其逆矩阵的乘法运算满足交换律。

另外,可以利用逆矩阵来解矩阵方程。例如,设矩阵方程,且已知,求。假设矩阵是可逆的(即存在),我们就能解得。过程如下:

    

    方程两端各乘以

       根据可逆矩阵的定义,有

        根据单位矩阵的定义,有

在任何一本大学水平的线性代数教科书里,都可以找到求逆矩阵公式的推导过程,这里也就不再赘述了。此公式由原矩阵的伴随矩阵和行列式构成:

  (2.6)

例2.10

推导矩阵的逆矩阵通式,并利用此式求出矩阵的逆矩阵。

已知

因此,

现在运用此公式来求矩阵的逆矩阵:

为了核实结果,我们来验证


注意 

对于规模较小的矩阵(及其以下规模的矩阵)来说,运用伴随矩阵的方法将得到不错的计算效率。但针对规模更大的矩阵而言,就要使用诸如高斯消元法(Gaussian elimination,也作高斯消去法)等其他手段。由于我们关注于3D计算机图形学中所涉及的具有特殊形式的矩阵,因此也就提前确定出了它们的求逆矩阵公式。这样一来,我们便无须在求常用的逆矩阵上浪费CPU资源了,继而也就极少会在代码中运用式(2.6)。


我们以下列“矩阵乘积的逆”这一实用的代数性质,为此节画上句号:

该性质假设矩阵与矩阵都是可逆的,而且皆为同维方阵。为了证明是乘积的逆,我们必须证实以及。证明过程如下:

为了对点与向量进行变换,就要借助行向量以及矩阵。相关原因将在下一章中细述。目前,我们只需把注意力集中在DirectXMath库中常用于表示矩阵的数据类型。

DirectXMath以定义在DirectXMath.h头文件中的XMMATRIX类来表示矩阵(为了叙述清晰起见,这里进行了若干细节上的调整):

#if (defined(_M_IX86) || defined(_M_X64) || defined(_M_ARM)) && 
defined(_XM_NO_INTRINSICS_)
struct XMMATRIX
#else
__declspec(align(16)) struct XMMATRIX
#endif
{
  // 利用4个XMVECTOR来表示矩阵,借此使用SIMD技术
  XMVECTOR r[4];

  XMMATRIX() {}

  // 通过指定4个行向量来初始化矩阵
  XMMATRIX(FXMVECTOR R0, FXMVECTOR R1, FXMVECTOR R2, CXMVECTOR R3) 
    { r[0] = R0; r[1] = R1; r[2] = R2; r[3] = R3; }

  // 通过指定16个矩阵元素来初始化矩阵
  XMMATRIX(float m00, float m01, float m02, float m03,
       float m10, float m11, float m12, float m13,
       float m20, float m21, float m22, float m23,
       float m30, float m31, float m32, float m33);

  // 通过含有16个浮点数元素的数组来初始化矩阵
  explicit XMMATRIX(_In_reads_(16) const float *pArray);

  XMMATRIX&  operator= (const XMMATRIX& M) 
    { r[0] = M.r[0]; r[1] = M.r[1]; r[2] = M.r[2]; r[3] = M.r[3]; 
    return *this; }

  XMMATRIX  operator+ () const { return *this; }
  XMMATRIX  operator- () const;

  XMMATRIX&  XM_CALLCONV   operator+= (FXMMATRIX M);
  XMMATRIX&  XM_CALLCONV   operator-= (FXMMATRIX M);
  XMMATRIX&  XM_CALLCONV   operator*= (FXMMATRIX M);
  XMMATRIX&  operator*= (float S);
  XMMATRIX&  operator/= (float S);

  XMMATRIX  XM_CALLCONV   operator+ (FXMMATRIX M) const;
  XMMATRIX  XM_CALLCONV   operator- (FXMMATRIX M) const;
  XMMATRIX  XM_CALLCONV   operator* (FXMMATRIX M) const;
  XMMATRIX  operator* (float S) const;
  XMMATRIX  operator/ (float S) const;

  friend XMMATRIX   XM_CALLCONV   operator* (float S, FXMMATRIX M);
};

综上所述,XMMATRIX由4个XMVECTOR实例所构成,并借此来使用SIMD技术。此外,XMMATRIX类还为矩阵计算提供了多种重载运算符。

除了各种构造方法之外,还可以使用XMMatrixSet函数来创建XMMATRIX实例:

XMMATRIX XM_CALLCONV XMMatrixSet(
  float m00, float m01, float m02, float m03,
  float m10, float m11, float m12, float m13,
  float m20, float m21, float m22, float m23,
  float m30, float m31, float m32, float m33);

就像通过XMFLOAT2 (2D),XMFLOAT3 (3D)和XMFLOAT4 (4D)来存储类中不同维度的向量一样,DirectXMath文档也建议我们用XMFLOAT4X4来存储类中的矩阵类型数据成员。

struct XMFLOAT4X4
{
  union
  {
    struct
    {
      float _11, _12, _13, _14;
      float _21, _22, _23, _24;
      float _31, _32, _33, _34;
      float _41, _42, _43, _44;
    };
    float m[4][4];
  };

  XMFLOAT4X4() {}
  XMFLOAT4X4(float m00, float m01, float m02, float m03,
             float m10, float m11, float m12, float m13,
             float m20, float m21, float m22, float m23,
             float m30, float m31, float m32, float m33);
  explicit XMFLOAT4X4(_In_reads_(16) const float *pArray);

  float    operator() (size_t Row, size_t Column) const { return m[Row][Column]; }
  float&   operator() (size_t Row, size_t Column) { return m[Row][Column]; }

  XMFLOAT4X4& operator=(const XMFLOAT4X4& Float4x4);
};

通过下列方法将数据从XMFLOAT4X4内加载到XMMATRIX中:

inline XMMATRIX XM_CALLCONV 
XMLoadFloat4x4(const XMFLOAT4X4* pSource);

通过下列方法将数据从XMMATRIX内存储到XMFLOAT4X4中:

inline void XM_CALLCONV 
XMStoreFloat4x4(XMFLOAT4X4* pDestination, FXMMATRIX M);

DirectXMath库包含了下列与矩阵相关的实用函数:

XMMATRIX XM_CALLCONV XMMatrixIdentity();   // 返回单位矩阵I

bool XM_CALLCONV XMMatrixIsIdentity(       // 如果M是单位矩阵则返回true
    FXMMATRIX M);                          // 输入矩阵M

XMMATRIX XM_CALLCONV XMMatrixMultiply(     // 返回矩阵乘积AB
    FXMMATRIX A,                           // 输入矩阵A
    CXMMATRIX B);                          // 输入矩阵B

XMMATRIX XM_CALLCONV XMMatrixTranspose(    // 返回MT
    FXMMATRIX M);                          // 输入矩阵M

XMVECTOR XM_CALLCONV XMMatrixDeterminant(  // 返回(det M, det M, det M, det M)
    FXMMATRIX M);                          // 输入矩阵M

XMMATRIX XM_CALLCONV XMMatrixInverse(      // 返回M-1
    XMVECTOR* pDeterminant,                // 输入(det M, det M, det M, det M)
    FXMMATRIX M);                          // 输入矩阵M

在声明具有XMMATRIX参数的函数时,除了要注意1个XMMATRIX应计作4个XMVECTOR参数这一点之外,其他的规则与传入XMVECTOR类型的参数时(见1.6.3节)相一致。假设传入函数的XMVECTOR参数不超过两个,则第一个XMMATRIX参数应当为FXMMATRIX类型,其余的XMMATRIX参数均应为CXMMATRIX类型。下面的代码展示了在32位Windows平台和编译器(编译器需支持__fastcall以及新增的__vectorcall调用约定)的环境下,这些类型的定义:

// 在32位的Windows系统上,__fastcall调用约定通过寄存器传递前3个XMVECTOR参数,其余的
//参数则存在堆栈上
typedef const XMMATRIX& FXMMATRIX;
typedef const XMMATRIX& CXMMATRIX;

// 在32位的Windows系统上,__vectorcall调用约定通过寄存器传递前 6个 XMVECTOR实参,其余的
// 参数则存在堆栈上
typedef const XMMATRIX FXMMATRIX;
typedef const XMMATRIX& CXMMATRIX;

可以看出,在32位Windows操作系统上的__fastcall调用约定中,XMMATRIX类型的参数是不能传至SSE/SSE2寄存器的,因为这些寄存器此时只支持3个XMVECTOR参数传入。而XMMATRIX参数却是由4个XMVECTOR构成,所以矩阵类型的数据只能通过堆栈来加以引用。至于这些类型在其他平台上的定义详情,可见DirectXMath文档[DirectXMath]中“Library Internals”(库的内部细节)下的“Calling Conventions”(调用约定)部分。构造函数方法对于这些规则来说是一个特例。[DirectXMath]建议用户总是在构造函数中采用CXMMATRIX类型来获取XMMATRIX参数,而且对于构造函数也不要使用XM_CALLCONV约定注解。

下列代码提供了一些XMMATRIX类的使用范例,其中包括了上一小节中介绍的大多数函数。

#include <windows.h> // 为了使XMVerifyCPUSupport函数返回正确值
#include <DirectXMath.h>
#include <DirectXPackedVector.h>
#include <iostream>
using namespace std;
using namespace DirectX;
using namespace DirectX::PackedVector;

// 重载"<<"运算符,这样就可以利用cout输出XMVECTOR和XMMATRIX对象
ostream& XM_CALLCONV operator << (ostream& os, FXMVECTOR v)
{
  XMFLOAT4 dest;
  XMStoreFloat4(&dest, v);

  os << "(" << dest.x << ", " << dest.y << ", " << dest.z << ", " << dest.w << ")";
  return os;
}

ostream& XM_CALLCONV operator << (ostream& os, FXMMATRIX m)
{
  for (int i = 0; i < 4; ++i)
  {
    os << XMVectorGetX(m.r[i]) << "\t";
    os << XMVectorGetY(m.r[i]) << "\t";
    os << XMVectorGetZ(m.r[i]) << "\t";
    os << XMVectorGetW(m.r[i]);
    os << endl;
  }
  return os;
}

int main()
{
  // 检查是否支持SSE2指令集 (Pentium4, AMD K8及其后续版本的处理器)
  if (!XMVerifyCPUSupport())
  {
    cout << "directx math not supported" << endl;
    return 0;
  }

  XMMATRIX A(1.0f, 0.0f, 0.0f, 0.0f,
             0.0f, 2.0f, 0.0f, 0.0f,
             0.0f, 0.0f, 4.0f, 0.0f,
             1.0f, 2.0f, 3.0f, 1.0f);

  XMMATRIX B = XMMatrixIdentity();
  XMMATRIX C = A * B;

  XMMATRIX D = XMMatrixTranspose(A);

  XMVECTOR det = XMMatrixDeterminant(A);
  XMMATRIX E = XMMatrixInverse(&det, A);

  XMMATRIX F = A * E;

  cout << "A = " << endl << A << endl;
  cout << "B = " << endl << B << endl;
  cout << "C = A*B = " << endl << C << endl;
  cout << "D = transpose(A) = " << endl << D << endl;
  cout << "det = determinant(A) = " << det << endl << endl;
  cout << "E = inverse(A) = " << endl << E << endl;
  cout << "F = A*E = " << endl << F << endl;

  return 0;
}

上述范例程序的输出结果如图2.1所示。

图2.1 范例程序输出的结果

1.矩阵是一个由列实数所构成的矩形阵列。两个同维矩阵相等,当且仅当它们对应的元素分别相等。两个同维矩阵的加法运算,由这两个矩阵对应的元素相加来实现。标量与矩阵的乘法运算是将标量与矩阵中的每个元素分别相乘。

2.如果是一个矩阵,且为一个矩阵,那么两者乘积的结果是一个规模为的矩阵。矩阵中第行、第列的元素,由矩阵中的第个行向量与矩阵中的第个列向量进行点积运算得出,即

3.矩阵乘法不满足交换律(即一般来说,),但是却满足结合律()。

4.转置矩阵由原矩阵互换行与列来求得。所以,矩阵的转置矩阵为矩阵。我们将矩阵的转置矩阵表示为

5.单位矩阵是一种除主对角线上的元素为1外,其他元素均为0的方阵。

6.行列式是一种特殊的函数,向它传入一个方阵便会计算出一个对应的实数。方阵是可逆的,当且仅当。行列式常常用于计算逆矩阵。

7.矩阵与其逆矩阵的乘积结果为单位矩阵,即。如果一个矩阵是可逆的,则此矩阵的逆矩阵是唯一的。只有方阵才可能有逆矩阵,即便是方阵也未必可逆。逆矩阵可由公式来计算,其中是伴随矩阵(即矩阵的代数余子式矩阵的转置矩阵)。

8.我们在编写代码时,用DirectXMath中的XMMATRIX类型来表示矩阵,以此来发挥SIMD技术高效的运算能力。但对于类中的数据成员,我们则要以XMFLOAT4X4类型来加以表示,并通过加载(XMLoadFloat4x4)和存储(XMStoreFloat4x4)方法,使数据在XMMATRIX类型与XMFLOAT4X4类型之间互相转换。XMMATRIX类重载了一些算数运算符,使矩阵可以实现加法运算、减法运算、矩阵乘法运算和标量乘法运算。此外,DirectXMath库还提供了下列实用的矩阵函数,用于计算单位矩阵、矩阵乘积、转置矩阵、行列式以及逆矩阵:

  XMMATRIX XM_CALLCONV XMMatrixIdentity();  
  XMMATRIX XM_CALLCONV XMMatrixMultiply(FXMMATRIX A, CXMMATRIX B);
  XMMATRIX XM_CALLCONV XMMatrixTranspose(FXMMATRIX M);
  XMVECTOR XM_CALLCONV XMMatrixDeterminant(FXMMATRIX M);
  XMMATRIX XM_CALLCONV XMMatrixInverse(XMVECTOR* pDeterminant, 
    FXMMATRIX M);

1.求解下列矩阵方程中的矩阵

2.计算下列矩阵的乘积:

 (a)

 (b)

 (c)

3.计算下列矩阵的转置矩阵:

 (a)

 (b)

 (c)

4.将下列线性组合写作向量与矩阵乘积的形式:

 (a)

 (b)

5.证明

6.证明

7.证明向量的叉积可以用矩阵的乘积来表示:

8.设矩阵,那么,请问矩阵的逆矩阵吗?

9.设矩阵,那么,请问矩阵的逆矩阵吗?

10.求下列矩阵的行列式:

11.求下列矩阵的逆矩阵:

12.下列矩阵是可逆矩阵吗?

13.假设矩阵是可逆矩阵,证明

14.所有的线性代数书籍都会证明这一性质。设皆为矩阵,并假设是可逆的,试根据与上述性质来证明

15.证明2D矩阵的行列式得到的是:由向量与向量张成的平行四边形的有向面积。如果向量以逆时针方向旋转角能与向量重合,则结果为正,否则为负。

16.求由下列向量张成的平行四边形面积:

 (a)

 (b)

17.设,且。证明。这个结论说明了矩阵之间的乘法运算满足结合律。(事实上,只要矩阵的乘法有意义,任意规模的矩阵乘法都满足结合律。)

18.编写一个计算机程序,使之在不借助DirectXMath库的情况下(仅用C++中的二维数组(array of arrays))就可以计算矩阵的转置矩阵。

19.编写一个计算机程序,在不使用DirectXMath库的情况下(仅用C++中的二维数组),使它可以计算出矩阵的行列式及其逆矩阵。

[1] 准确来讲,矩阵中的元素并非仅为实数。

[2] 这个定义并不十分准确。例如,在二维情况的线性变换下,二阶行列式反映的是平行四边形有向面积的变化。参见本章练习15。

[3] 这一小节中的部分数学术语在不同的文献中会有些差别,此处以常见文献中的译法为主。读者应以具体的定义为准。

[4] 可参见22.2.6节。


相关图书

Python面向对象编程:构建游戏和GUI
Python面向对象编程:构建游戏和GUI
精通游戏测试(第3版)
精通游戏测试(第3版)
罗布乐思开发官方指南 从入门到实践
罗布乐思开发官方指南 从入门到实践
游戏引擎原理与实践 卷2 高级技术
游戏引擎原理与实践 卷2 高级技术
游戏数值设计
游戏数值设计
游戏引擎原理与实践 卷1 基础框架
游戏引擎原理与实践 卷1 基础框架

相关文章

相关课程