DirectX 12 3D 游戏开发实战
上QQ阅读APP看书,第一时间看更新

第1章 向量代数

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

学习目标:

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

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

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

1.1 向量

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

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

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

1.1.1 向量与坐标系

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

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

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


注意 ..\..\Doc1.files\image001.png 

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


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

图1.4 同一向量\boldsymbol{v}在不同的标架中有着不同的坐标

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


注意 ..\..\Doc1.files\image001.png 

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


1.1.2 左手坐标系与右手坐标系

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

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

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

1.1.3 向量的基本运算

现在通过坐标来表示向量的相等、加法运算、标量乘法运算和减法运算的定义。对于这4种定义,设有向量\textbf{\textit{u}} = (u_{x}, u_{y}, u_{z})和向量\textbf{\textit{v}} = (v_{x}, v_{y}, v_{z})

1.两个向量相等,当且仅当它们的对应分量分别相等。即\boldsymbol{u}=\boldsymbol{v} ,当且仅当u_{x} = v_{x}u_{y} = v_{y}u_{z} = v_{z}

2.向量的加法即令两个向量的对应分量分别相加:\textbf{\textit{u}} + \textbf{\textit{v}} = (u_{x} + v_{x}, u_{y} + v_{y}, u_{z} + v_{z})。注意,只有同维的向量之间才可以进行加法运算。

3.向量可以与标量(即实数)相乘,所得到的结果仍是一个向量。例如,设k为一个标量,则k\textbf{\textit{u}} = (ku_{x}, ku_{y}, ku_{z})。这种运算叫作标量乘法(scalar multiplication)。

4.向量减法可以通过向量加法和标量乘法表示,即\textbf{\textit{u}} - \textbf{\textit{v}} = \textbf{\textit{u}} + (-1\cdot\textbf{\textit{v}}) = \textbf{\textit{u}} + (-\textbf{\textit{v}}) = (u_{x} - v_{x}, u_{y} - v_{y}, u_{z} - v_{z})

例1.1

设向量\textbf{\textit{u}} = (1, 2, 3)\textbf{\textit{v}} = (1, 2, 3)\textbf{\textit{w}} = (3, 0, -2)及标量k = 2。那么,

1.\textbf{\textit{u}} + \textbf{\textit{w}} = (1, 2, 3) + (3, 0, -2) = (4, 2, 1)

2.\textbf{\textit{u}} = \textbf{\textit{v}}

3.\textbf{\textit{u}} - \textbf{\textit{v}} = \textbf{\textit{u}} + (-\textbf{\textit{v}}) = (1, 2, 3) + (-1, -2, -3) = (0, 0, 0) = \textbf{\textit{0}}

4.k\textbf{\textit{w}} = 2(3, 0, -2) = (6, 0, -4)

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

例1.2

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

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

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

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

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

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

1.2 长度和单位向量

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

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

首先来看位于平面xz中以xz为直角边,以a为斜边所构成的直角三角形。根据毕达哥拉斯定理,有a=\sqrt{{{x}^{2}}+{{z}^{2}}}。接下来再看以ay为直角边,以||\boldsymbol{u}||为斜边所围成的直角三角形。再次运用毕达哥拉斯定理,便能得出下列计算向量模的公式:

\left \| \textbf{\textit{u}} \right \|=\sqrt{{{y}^{2}}+{{a}^{2}}}=\sqrt{{{y}^{2}}+{{(\sqrt{{{x}^{2}}+{{z}^{2}}})}^{2}}}=\sqrt{{{x}^{2}}+{{y}^{2}}+{{z}^{2}}}  (1.1)

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

\hat{\textbf{\textit{u}}}=\frac{\textbf{\textit{u}}}{||\textbf{\textit{u}}||}=\left( \frac{x}{||\textbf{\textit{u}}||},\frac{y}{||\textbf{\textit{u}}||},\frac{z}{||\textbf{\textit{u}}||} \right)  (1.2)

为了验证公式的正确性,下面计算\hat{\boldsymbol{u}}的长度:

||\hat{\textbf{\textit{u}}}||=\sqrt{{{\left( \frac{x}{||\textbf{\textit{u}}||} \right)}^{2}}+{{\left( \frac{y}{||\textbf{\textit{u}}||} \right)}^{2}}+{{\left( \frac{z}{||\textbf{\textit{u}}||} \right)}^{2}}}=\frac{\sqrt{{{x}^{2}}+{{y}^{2}}+{{z}^{2}}}}{\sqrt{||\textbf{\textit{u}}|{{|}^{2}}}}={\frac{||\textbf{\textit{u}}||}{||\textbf{\textit{u}}||}=1}

由此可见,\hat{\boldsymbol{u}}确实是一个单位向量(unit vector)。

例1.3

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

\left \| \textbf{\textit{v}} \right \|=\frac{\textbf{\textit{v}}}{\left \| \textbf{\textit{v}} \right \|}=\left( -\frac{1}{\sqrt{26}},\frac{3}{\sqrt{26}},\frac{4}{\sqrt{26}} \right) 

为了验证\hat{\textbf{\textit{v}}}是单位向量,我们计算其长度:

\left \| \textbf{\textit{v}} \right \|=\sqrt{{{\left( -\frac{1}{\sqrt{26}} \right)}^{2}}+{{\left( \frac{3}{\sqrt{26}} \right)}^{2}}+{{\left( \frac{4}{\sqrt{26}} \right)}^{2}}}=\sqrt{\frac{1}{26}+\frac{9}{26}+\frac{16}{26}}={\sqrt{1}=1}

1.3 点积

点积(dot product,亦称数量积或内积)是一种计算结果为标量值的向量乘法运算,因此有时也称为标量积(scalar product)。设向量\textbf{\textit{u}} = (u_{x}, u_{y}, u_{z})\textbf{\textit{v}} = (v_{x}, v_{y}, v_{z}),则点积的定义为:

\textbf{\textit{u}}\cdot \textbf{\textit{v}}={{u}_{x}}{{v}_{x}}+{{u}_{y}}{{v}_{y}}+{{u}_{z}}{{v}_{z}}  (1.3)

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

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

\textbf{\textit{u}}\cdot \textbf{\textit{v}}=\left \| \textbf{\textit{u}} \right \|\: \left \| \textbf{\textit{v}} \right \|\cos \theta   (1.4)

其中,\theta 是向量\boldsymbol{u}与向量\boldsymbol{v}之间的夹角,0\leqslant \theta \leqslant \pi ,如图1.9所示。式(1.4)表明,两向量的点积为:两向量夹角的余弦值乘以这两个向量的模。特别地,如果向量\boldsymbol{u}和向量\boldsymbol{v}都是单位向量,那么\boldsymbol{u}\cdot \boldsymbol{v}就等于两向量夹角的余弦值,即\textbf{\textit{u}}\cdot \textbf{\textit{v}}=\textrm{cos}\, \theta

图1.9 图a中,向量\boldsymbol{u}与向量\boldsymbol{v}之间的夹角\theta是一个锐角;图b中,向量\boldsymbol{u}与向量\boldsymbol{v}之间的夹角\theta是一个钝角。每当讨论两个向量之间的夹角时,我们提及的总是较小的那个角,即角\theta总是满足0\leqslant \theta \leqslant \pi

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

1.如果\textbf{\textit{u}}\cdot \textbf{\textit{v}}=0,那么\textbf{\textit{u}}\bot \textbf{\textit{v}}(即两个向量正交)。

2.如果\textbf{\textit{u}}\cdot \textbf{\textit{v}}> 0,那么两向量之间的夹角\theta小于90°(即两向量间的夹角为一锐角)。

3.如果\textbf{\textit{u}}\cdot \textbf{\textit{v}}< 0,那么两向量之间的夹角\theta大于90°(即两向量间的夹角为一钝角)。


注意 ..\..\Doc1.files\image001.png 

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


例1.4

设向量\textbf{\textit{u}} = (1, 2, 3)\textbf{\textit{v}} = (-4, 0, -1)。计算向量\boldsymbol{u}\boldsymbol{v}之间的夹角。

先来计算:

\textbf{\textit{u}}\cdot \textbf{\textit{v}}=(1,\ 2,\ 3)\cdot (-4,\ 0,\ -1)=-4-3=-7

现在,运用式(1.4)得到\theta

\cos \theta =\frac{\textbf{\textit{u}}\cdot \textbf{\textit{v}}}{\left \| \textbf{\textit{u}} \right \|\ \left \| \textbf{\textit{v}} \right \|}=\frac{-7}{\sqrt{14}\cdot \sqrt{17}}

\theta ={{\cos }^{-1}}\frac{-7}{\sqrt{14}\cdot \sqrt{17}}\approx {{117}^{\circ }}

例1.5

考虑图1.10。给出向量\boldsymbol{v}单位向量\boldsymbol{n},请借助点积公式求出用\boldsymbol{v}\boldsymbol{n}表示向量\boldsymbol{p}的公式。

图1.10 向量\boldsymbol{v}在单位向量\boldsymbol{n}上的正交投影(orthogonal projection)

首先,观察图示可以得知存在一标量k,使得\boldsymbol{p} = k\boldsymbol{n} ;而且,因为我们假设\left \|\textbf{\textit{n}} \right \|=1,所以有\left \| \textbf{\textit{p}} \right \|=\left \| k\textbf{\textit{n}} \right \|=\left \| k \parallel \mid {\textbf{\textit{n}}}\right \|=\left \| k \right \|。注意,k可能是负值,当且仅当\boldsymbol{p}\boldsymbol{n}的方向相反。利用三角函数,我们有k=\left \| \textbf{\textit{v}} \right \|\cos \theta;因此,\textbf{\textit{p}}=k\textbf{\textit{n}}=(\left \| \textbf{\textit{v}} \right \|\cos \theta )\textbf{\textit{n}}。又由于\boldsymbol{n}是单位向量,便可以用另一种方法来表示:

\textbf{\textit{p}}=(\left \| \textbf{\textit{v}} \right \|\cos \theta )\textbf{\textit{n}}=(\left \| \textbf{\textit{v}} \right \|\cdot 1\cos \theta )\textbf{\textit{n}}=(\left \| \textbf{\textit{v}} \right \|\left \| \textbf{\textit{n}} \right \|\cos \theta )\textbf{\textit{n}}=(\textbf{\textit{v}}\cdot \textbf{\textit{n}})\textbf{\textit{n}}

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

\textbf{\textit{p}} = \text{proj}_{n}(\textbf{\textit{v}})  

如果将\boldsymbol{v}看作是一个力,便可认为\boldsymbol{p}是力\boldsymbol{v}在方向\boldsymbol{n}上的分力。同理,向量\textbf{\textit{w}} = \text{perp}_{n}(\textbf{\textit{v}}) = \textbf{\textit{v}} - \textbf{\textit{p}}是作用力\boldsymbol{v}\boldsymbol{n}的正交方向上的分力(这就是用\text{perp}_{n}(\textbf{\textit{v}})来表示“垂直”的原因)。观察到\textbf{\textit{v}} = \textbf{\textit{p}} + \textbf{\textit{w}} = \text{proj}_{n}(\textbf{\textit{v}}) + \text{perp}_{n}(\textbf{\textit{v}}),这就是说,可以将向量\boldsymbol{v}分解成两个互相正交的向量\boldsymbol{p}\boldsymbol{w}之和。

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

\textbf{\textit{p}}=\text{proj}_{\textbf{\textit{n}}}(\textbf{\textit{v}})=\left( \textbf{\textit{v}}\cdot \frac{\textbf{\textit{n}}}{\left \| \textbf{\textit{n}} \right \|} \right)\frac{\textbf{\textit{n}}}{\left \| \textbf{\textit{n}} \right \|}=\frac{(\textbf{\textit{v}}\cdot \textbf{\textit{n}})}{\left \| \textbf{\textit{n}} \right \|^{2}}\textbf{\textit{n}}

正交化

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

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

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

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

{{\textbf{\textit{w}}}_{1}}={{\textbf{\textit{v}}}_{1}}-{\text{proj}_{\textbf{\textit{w}}0}}({{\textbf{\textit{v}}}_{1}})

图1.11 2D正交化处理

图1.12 3D正交化处理

接下来,通过令\textbf{\textit{v}}_{2}依次减去它在\textbf{\textit{w}}_{0}方向与\textbf{\textit{w}}_{1}方向上的分量(投影),使之同时正交于\textbf{\textit{w}}_{0}\textbf{\textit{w}}_{1}

现在我们就得到了所有元素都彼此正交的向量集{\textbf{\textit{w}}_{0}, \textbf{\textit{w}}_{1}, \textbf{\textit{w}}_{2}\};最后一步是通过将\boldsymbol{w}_0\boldsymbol{w}_1\boldsymbol{w}_2规范化为单位向量来构建一个规范正交集。

对于具有n个向量的一般集合\left {\textbf{\textit{v}}_{0}, \cdots ,\textbf{\textit{v}}_{n-1} \right \}而言,为了将其正交化为规范正交集\left {\textbf{\textit{w}}_{0}, \cdots ,\textbf{\textit{w}}_{n-1} \right \},我们就要使用格拉姆施密特正交化(Gram-Schmidt Orthogonalization)方法进行处理。

基本步骤:设\textbf{\textit{w}}_{0} = \textbf{\textit{v}}_{0}

对于1\leqslant i\leqslant n-1,令 {{\textbf{\textit{w}}}_{i}}={{\textbf{\textit{v}}}_{i}}-\sum\limits_{j=0}^{i-1}{{\text{proj}_{{{\textbf{\textit{w}}}_{j}}}}({{\textbf{\textit{v}}}_{i}})}

规范化步骤:令{{\textbf{\textit{w}}}_{i}}=\frac{{{\textbf{\textit{w}}}_{i}}}{||{{\textbf{\textit{w}}}_{i}}||}

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

1.4 叉积

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

\textbf{\textit{w}}=\textbf{\textit{u}}\times \textbf{\textit{v}}=({{u}_{y}}{{v}_{z}}-u_{z}^{{}}v_{y}^{{}},{{u}_{z}}{{v}_{x}}-{{u}_{x}}{{v}_{z}},{{u}_{x}}{{v}_{y}}-{{u}_{y}}{{v}_{x}})  (1.5)


注意 ..\..\Doc1.files\image001.png 

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


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

例1.6

设向量\textbf{\textit{u}} = (2, 1, 3)和向量\textbf{\textit{v}} = (2, 0, 0)。计算\textbf{\textit{w}}=\textbf{\textit{u}}\times \textbf{\textit{v}}\textbf{\textit{z}}=\textbf{\textit{v}}\times \textbf{\textit{u}},并验证向量\boldsymbol{w}既正交于向量\boldsymbol{u}又正交于向量\boldsymbol{v}。运用式(1.5),有:

\begin{align*}\textbf{\textit{w}}&=\textbf{\textit{u}}\times \textbf{\textit{v}}\\ &=(2,\ 1,\ 3)\ \times \ (2,\ 0,\ 0)\\ &=(1\cdot 0-3\cdot 0,\ 3\cdot 2-2\cdot 0,\ 2\cdot 0-1\cdot 2)\\ &=(0,\ 6,\ -2)\end{align*}

以及

\begin{align*}\textbf{\textit{z}}&=\textbf{\textit{v}}\times \textbf{\textit{u}}\\ &=(2,\ 0,\ 0)\ \times \ (2,\ 1,\ 3)\\ &=(0\cdot 3-0\cdot 1,\ 0\cdot 2-2\cdot 3,\ 2\cdot 1-0\cdot 2)\\ &=(0,\ -6,\ 2)\end{align*}

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

为了证明向量\boldsymbol{w}既正交于向量\boldsymbol{u}又正交于向量\boldsymbol{v},我们需要用到1.3节中的结论:如果\textbf{\textit{u}}\cdot \textbf{\textit{v}}=0,那么\textbf{\textit{u}}\bot \textbf{\textit{v}}(即两个向量彼此正交)。由于:

\textbf{\textit{w}}\cdot \textbf{\textit{u}}=(0,6,-2)\cdot (2,1,3)=0\cdot 2+6\cdot 1+(-2)\cdot 3=0

以及

\textbf{\textit{w}}\cdot \textbf{\textit{v}}=(0,6,-2)\cdot (2,0,0)=0\cdot 2+6\cdot 0+(-2)\cdot 0=0

由此可以推断出:向量\boldsymbol{w}既正交于向量\boldsymbol{u},也正交于向量\boldsymbol{v}

1.4.1 2D向量的伪叉积

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

图1.14 向量\boldsymbol{u}的2D伪叉积计算结果是正交于\boldsymbol{u}的向量\boldsymbol{v}

\textbf{\textit{u}}\cdot \textbf{\textit{v}}=({{u}_{x}},{{u}_{y}})\cdot (-{{u}_{y}},{{u}_{x}})=-{{u}_{x}}{{u}_{y}}+{{u}_{y}}{{u}_{x}}=0

因此,\textbf{\textit{u}}\bot \textbf{\textit{v}}。同时,不难看出\textbf{\textit{u}}\cdot -\textbf{\textit{v}}={{u}_{x}}{{u}_{y}}+{{u}_{y}}(-{{u}_{x}})=0,所以亦可知\textbf{\textit{u}}\bot - \textbf{\textit{v}}

1.4.2 通过叉积来进行正交化处理

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

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

1. 令{{\textbf{\textit{w}}}_{0}}=\frac{{{v}_{0}}}{||{{v}_{0}}||}

2.令{{\textbf{\textit{w}}}_{2}}=\frac{{{w}_{0}}\times {{v}_{1}}}{||{{w}_{0}}\times {{v}_{1}}||}

3.令{{\boldsymbol{w}}_{1}}={{\boldsymbol{w}}_{2}}\times {{\boldsymbol{w}}_{0}},根据练习14可知:由于{{\boldsymbol{w}}_{2}}\bot {{\boldsymbol{w}}_{0}}且||\boldsymbol{w}_2|| = ||\boldsymbol{w}_0|| = 1,因此||{{\boldsymbol{w}}_{2}}\times {{\boldsymbol{w}}_{0}}||=1。所以,我们最后也就不再需要对它进行规范化处理了。

此时,向量集\left{\boldsymbol{w}_{0}, \boldsymbol{w}_{1}, \boldsymbol{w}_{2}\right\}是规范正交的。


注意 ..\..\Doc1.files\image001.png 

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


1.5 点

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

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

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

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


注意 ..\..\Doc1.files\image001.png 

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


1.6 利用DirectXMath库进行向量运算

对于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对此均有支持。

1.6.1 向量类型

在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.对于类中的数据成员,使用XMFLOAT2、XMFLOAT3和XMFLOAT4类型。

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

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

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

1.6.2 加载方法和存储方法

用下面的方法将数据从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);

1.6.3 参数的传递

为了提高效率,可以将XMVECTOR类型的值作为函数的参数,直接传送至SSE/SSE2寄存器(register)里,而不存于栈(stack)内。以此方式传递的参数数量取决于用户使用的平台(例如,32位的Windows系统、64位的Windows系统及Windows RT系统所能传递的参数数量都各不相同)和编译器。因此,为了使代码更具通用性,不受具体平台、编译器的影响,我们将利用FXMVECTOR、GXMVECTOR、HXMVECTOR和CXMVECTOR类型来传递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类型的参数一致。

1.6.4 常向量

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

1.6.5 重载运算符

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

1.6.6 杂项

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

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

1.6.7 Setter函数

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 示例程序输出的结果

1.6.8 向量函数

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(  // 返回v1≠v
  FXMVECTOR V1,                      // 输入向量v1
  FXMVECTOR V2);                     // 输入向量v2


注意 ..\..\Doc1.files\image001.png 

可以看到,即使在数学上计算的结果是标量(如点积k=\boldsymbol{v}_1\cdot \boldsymbol{v}_2),但这些函数所返回的类型依旧是XMVECTOR,而得到的标量结果则被复制到XMVECTOR中的各个分量之中。例如点积,此函数返回的向量为(\boldsymbol{v}_1\cdot \boldsymbol{v}_2,\boldsymbol{v}_1\cdot \boldsymbol{v}_2,\boldsymbol{v}_1\cdot \boldsymbol{v}_2,\boldsymbol{v}_1\cdot \boldsymbol{v}_2)。这样做的原因之一是:将标量和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 示例程序的输出结果


注意 ..\..\Doc1.files\image001.png 

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

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


1.6.9 浮点数误差

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

#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.7 小结

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

2.假设有向量\boldsymbol{u}=\left(u_{x}, u_{y}, u_{z}\right)和向量\boldsymbol{v}=\left(v_{x}, v_{y}, v_{z}\right),那么就能对它们进行下列向量计算。

(a)加法运算: \boldsymbol{u}+\boldsymbol{v}=\left(u_{x}+v_{x}, u_{y}+v_{y}, u_{z}+v_{z}\right)

(b)减法运算:\boldsymbol{u}-\boldsymbol{v}=\left(u_{x}-v_{x}, u_{y}-v_{y}, u_{z}-v_{z}\right)

(c)标量乘法运算:\quad k \boldsymbol{u}=\left(k u_{x}, k u_{y}, k u_{z}\right)

(d)向量长度:\|\boldsymbol{u}\|=\sqrt{x^{2}+y^{2}+z^{2}}

(e)规范化: \quad \hat{\boldsymbol{u}}=\frac{\boldsymbol{u}}{\|\boldsymbol{u}\|}=\left(\frac{x}{\|\boldsymbol{u}\|}, \frac{y}{\|\boldsymbol{u}\|}, \frac{z}{\|\boldsymbol{u}\|}\right)

(f)点积:\quad \boldsymbol{u} \cdot \boldsymbol{v}=\|\boldsymbol{u}\|\|\boldsymbol{v}\| \cos \boldsymbol{\theta}=u_{x} v_{x}+u_{y} v_{y}+u_{z} v_{z}

(g)叉积:\quad \boldsymbol{u} \times \boldsymbol{v}=\left(u_{y} v_{z}-u_{z} v_{y}, u_{z} v_{x}-u_{x} v_{z}, u_{x} v_{y}-u_{y} v_{x}\right)

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

4.为了提高效率,当XMVECTOR类型的值被当作参数传入函数时,可以直接存入SSE/SSE2寄存器中而不是栈上。要令代码与平台无关,我们将使用FXMVECTOR、GXMVECTOR、HXMVECTOR和CXMVECTOR类型来传递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.8 练习

1.设向量\textbf{\textit{u}} = (1, 2)和向量\textbf{\textit{v}} = (3, -4)。写出下列各式的演算过程,并在2D坐标系内画出相应的向量。

(a)\textbf{\textit{u}} + \textbf{\textit{v}}

(b)\textbf{\textit{u}} - \textbf{\textit{v}}

(c)2\boldsymbol{u}+\frac{1}{2}\boldsymbol{v}

(d)-2\textbf{\textit{u}} + \textbf{\textit{v}}

2.设向量\boldsymbol{u}= (-1, 3, 2)和向量\boldsymbol{v} = (3, -4, 1)。写出下列问题的解答过程。

(a)\textbf{\textit{u}} + \textbf{\textit{v}}

(b)\textbf{\textit{u}} - \textbf{\textit{v}}

(c)3\textbf{\textit{u}} + 2\textbf{\textit{v}}

(d)-2\textbf{\textit{u}} + \textbf{\textit{v}}

3.本习题展示了向量代数与实数所共有的一些计算性质(注意,以下清单中所列举的性质并不完整)。假设有向量\textbf{\textit{u}} = (u_{x}, u_{y}, u_{z})\textbf{\textit{v}} = (v_{x}, v_{y}, v_{z})\textbf{\textit{w}} = (w_{x}, w_{y}, w_{z}),另有标量ck,请证明下列向量性质。

(a)\textbf{\textit{u}} + \textbf{\textit{v}} = \textbf{\textit{v}} + \textbf{\textit{u}}(加法交换律)

(b)\textbf{\textit{u}} + (\textbf{\textit{v}} + \textbf{\textit{w}}) = (\textbf{\textit{u}} + \textbf{\textit{v}}) + \textbf{\textit{w}}(加法结合律)

(c)(ck)\textbf{\textit{u}}=c(k\textbf{\textit{u}})(标量乘法的结合律)

(d)k(\textbf{\textit{u}} + \textbf{\textit{v}}) = k\textbf{\textit{u}} + k\textbf{\textit{v}}(分配律1)

(e)\textbf{\textit{u}}(k + c) = k\textbf{\textit{u}} + c\textbf{\textit{u}}(分配律2)


提示 ..\Doc1.files\image001.png

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

\begin{aligned}(c k) \boldsymbol{u} &=(c k)\left(u_{x}, u_{y}, u_{z}\right) \\&=\left((c k) u_{x},(c k) u_{y},(c k) u_{z}\right) \\&=\left(c\left(k u_{x}\right), c\left(k u_{y}\right), c\left(k u_{z}\right)\right) \\&=c\left(k u_{x}, k{u}_{y}, k u_{z}\right) \\&=c(k \boldsymbol{u})\end{aligned}


4.根据等式2[(1, 2, 3) - \textbf{\textit{x}}] - (-2, 0, 4) = -2(1, 2, 3),求其中的向量\boldsymbol{x}

5.设向量\boldsymbol{u}= (-1, 3, 2)和向量\boldsymbol{v}= (3, -4, 1)。对\boldsymbol{u}\boldsymbol{v}进行规范化处理。

6.设k为标量,向量\textbf{\textit{u}} = (u_{x}, u_{y}, u_{z})。求证||k\textbf{\textit{u}}|| = ||k||||\textbf{\textit{u}}||

7.下列各组向量中,\boldsymbol{u}\boldsymbol{v}之间的夹角是直角、锐角还是钝角?

(a)\textbf{\textit{u}} = (1, 1, 1)\textbf{\textit{v}} = (2, 3, 4)

(b)\textbf{\textit{u}} = (1, 1, 0)\textbf{\textit{v}} = (-2, 2, 0)

(c)\textbf{\textit{u}} = (-1, -1, -1)\textbf{\textit{v}} = (3, 1, 0)

8.设向量\boldsymbol{u}= (-1, 3, 2)和向量\boldsymbol{v}= (3, -4, 1)。计算\boldsymbol{u}\boldsymbol{v}之间的夹角\theta

9.设向量\textbf{\textit{u}} = (u_{x}, u_{y}, u_{z})\textbf{\textit{v}} = (v_{x}, v_{y}, v_{z})\textbf{\textit{w}} = (w_{x}, w_{y}, w_{z}),且ck为标量。证明下列点积性质。

(a)\boldsymbol{u} \cdot \boldsymbol{v}=\boldsymbol{v} \cdot \boldsymbol{u}

(b)\boldsymbol{u} \cdot(\boldsymbol{v}+\boldsymbol{w})=\boldsymbol{u} \cdot \boldsymbol{v}+\boldsymbol{u} \cdot \boldsymbol{w}

(c)k(\boldsymbol{u} \cdot \boldsymbol{v})=(k \boldsymbol{u}) \cdot \boldsymbol{v}=\boldsymbol{u} \cdot(k \boldsymbol{v})

(d)\boldsymbol{v} \cdot \boldsymbol{v}=\|\boldsymbol{v}\|^{2}

(e)\mathbf{0} \cdot \boldsymbol{v}=0


提示 ..\Doc1.files\image001.png

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

\begin{aligned}\boldsymbol{v} \cdot \boldsymbol{v} &=v_{x} v_{x}+v_{y} v_{y}+v_{z} v_{z} \\&=v_{x}^{2}+v_{y}^{2}+v_{z}^{2} \\&=(\sqrt{v_{x}^{2}+v_{y}^{2}+v_{z}^{2}})^{2} \\&=\|\boldsymbol{v}\|^{2}\end{aligned}


10.利用余弦定理(c^{2}=a^{2}+b^{2}-2 a b \cos \boldsymbol{\theta},其中abc分别是三角形3条边的边长,\thetaab之间的夹角)来证明:


提示 ..\Doc1.files\image001.png

参考图1.9,设c^{2}=\|\boldsymbol{u}-\boldsymbol{v}\|^{2} a^{2}=\|\boldsymbol{u}\|^以及b^{2}=\| \boldsymbol {v} \|^{2} ,再运用上一个习题中得到的点积性质即可。


11.设向量\boldsymbol{n} = (-2, 1)。将向量\boldsymbol{g}=(0,-9.8) 分解为两个相互正交的向量之和,使它们一个平行于\boldsymbol{n}、一个正交于\boldsymbol{n}。最后,在同一2D坐标系中画出这些向量。

12.设向量\boldsymbol{u}= (-2, 1, 4)和向量\boldsymbol{v} = (3, -4, 1)。求向量\boldsymbol{w}=\boldsymbol{u} \times \boldsymbol{v},再证明\boldsymbol{w} \cdot \boldsymbol{u}=0\boldsymbol{w} \cdot \boldsymbol{v}=0

13.设\boldsymbol{A}=(0,0,0)\boldsymbol{B}=(0, 1, 3)\boldsymbol{C}=(5, 1, 0)三点在某坐标系中定义了一个三角形。求出一正交于此三角形的向量。


提示 ..\Doc1.files\image001.png

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


14.证明\|\boldsymbol{u} \times \boldsymbol{v}\|=\|\boldsymbol{u}\|\|\boldsymbol{v}\| \sin \theta


提示 ..\Doc1.files\image001.png

\|\boldsymbol{u}\|\|\boldsymbol{v}\| \sin \theta一侧开始证明,先利用三角恒等式\textrm{cos}^{2}\, \theta+\textrm{sin}^{2}\, \theta=1\Rightarrow \textrm{sin} \, \theta=\sqrt{1-\textrm{cos}^{2}\, \theta},再运用式(1.4)。


15.证明:由向量\boldsymbol{u}和向量\boldsymbol{v}张成的平行四边形面积为||u\times v||,如图1.21所示[8]

图1.21 由向量\boldsymbol{u}和向量\boldsymbol{v}张成的平行四边形。此平行四边形的底为||\boldsymbol{v}||且高为h

16.举例证明:存在3D向量\boldsymbol{u}\boldsymbol{v}\boldsymbol{w},满足\boldsymbol{u} \times(\boldsymbol{v} \times \boldsymbol{w}) \neq(\boldsymbol{u} \times \boldsymbol{v}) \times \boldsymbol{w}。这说明叉积一般不满足结合律。


提示 ..\Doc1.files\image001.png

考虑这个简单的向量组合:\boldsymbol{i}=(1, 0, 0)\boldsymbol{j}=(0, 1, 0)\boldsymbol{k}=(0, 0, 1)


17.证明两个非零且相互平行向量的叉积为零向量,即\boldsymbol{u} \times k \boldsymbol{u}=\mathbf{0}


提示 ..\Doc1.files\image001.png

直接利用叉积定义即可。


18.利用格拉姆—施密特正交化方法,令向量集{(1,0,0),(1,5,0),(2,1,-4)\}规范正交化。

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] 私以为平行四边形的高h应垂直于向量v

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