向量
第一章 向量代数
向量与坐标系
定义
一种兼具大小和方向的量
坐标系
下图展现了向量v以及控件中俩组不同的标架(frame)
每当我们根据坐标来确定一个向量时,其对应的坐标总是相当于某一个参考系而言的。在3D计算图形学中,我们通常会用到较多的参考系。因此我们需要记录向量的每一种坐标系中对应的坐标。另外我们也需要知道如何将向量坐标在不同标架之间进行转换。
Direct3D采用的是左手坐标系,左手坐标系Z轴指向屏幕里,也就是眼睛看向方向,右手坐标系Z轴指向屏幕外,见下俩图。
向量的基本运算
点积是一种计算结果为标量值的向量乘法运算,因此有些时候也称标量积。设向量 u = ( u x , u y , u z ) u=(u_x,u_y,u_z) u=(ux,uy,uz), v = ( v x , v y , v z ) v=(v_x,v_y,v_z) v=(vx,vy,vz),则点积的定义为:
可见,点积就是向量间对应分类的乘积之和。
点积的定义并没有明显地体现出其几何意义。但是我们却能根据余弦定理找到向量点积的几何关系:
u
⋅
v
=
∣
∣
u
∣
∣
∣
∣
v
∣
∣
c
o
s
θ
\qquad \qquad \qquad \qquad \qquad \qquad \qquad \qquad \qquad u·v=\mid \mid u \mid \mid \, \mid \mid v \mid \mid cos\theta
u⋅v=∣∣u∣∣∣∣v∣∣cosθ
如果
u
u
u与
v
v
v是单位向量的话,那么有:
u
⋅
v
=
c
o
s
θ
\qquad \qquad \qquad \qquad \qquad \qquad \qquad \qquad \qquad u·v=cos\theta
u⋅v=cosθ
此时我们就能通过点积计算得到向量的夹角的余弦值,也就能得到夹角度数。
正交投影投影线垂直于投影面的投影。
正交化一个集合里的向量都与一向量正交,且这个集合的向量都为单位向量,那我们将此集合称为规范正交,而将普通向量集正交化为规范正交集的过程叫做正交化。而这个过程就要使用格拉姆——施密特正交化(Gram-Schmidt Orthogonalization)方法进行处理。规范化步骤:
w
i
=
w
i
∣
∣
w
i
∣
∣
w_i=\frac{w_i}{||w_i||}
wi=∣∣wi∣∣wi。
叉积(亦称向量积、外积)是向量的第二种乘法形式。与计算结果为标量的点积不同,叉积的计算结果亦为向量。叉积的计算方法为:
w
=
u
×
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
)
\qquad \qquad \qquad \qquad \qquad \qquad \qquad \qquad \qquad w=u×v=(u_yv_z-u_zv_y,u_zv_x-u_xv_z,u_xv_y-u_yv_x)
w=u×v=(uyvz−uzvy,uzvx−uxvz,uxvy−uyvx)
若实际上采用的是左手坐标系,那么伸出左手,四指头伸直指向向量
u
u
u,四指做握拳动作,四指弯曲的方向则是向量
v
v
v的方向,那么此时,大拇指指向的方向则是向量
w
w
w的方向。(注:如若此处用的是右手坐标系,那么用右手做同样操作即可),这个方法叫左手拇指法则,有的文献也称之为左手定则。
利用DirectXMath库对向量运算
DirectXMath使用环境
对于Windows8及其以上版本来讲,DirectXMath(其前身为XNA Math数据库,DirectXMath正是基于此而成)是一款Direct3D应用程序量身打造的3D数学库,而它也自此成为了WindowsSDK的一部分。该数据库采用了SIMD流指令扩展2(Streaming SIMD
Extensions2,SSE2)指令集。借助128位宽的单指令多数据(single instruction multiple data,SIMD)寄存器,利用一条指令SIMD指令即可同时对4个32位浮点数或整数进行运算。这对于向量运算带来的收益是不言而喻的。
为了使用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 Instruction 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)(采用联合的形式,这样用法的好处是可以同一段地址空间,做不同变量长度的字节数组使用)
typedef union __declspec(intrin_type) __declspec(align(16)) __m128 {
float m128_f32[4];
unsigned __int64 m128_u64[2];
__int8 m128_i8[16];
__int16 m128_i16[8];
__int32 m128_i32[4];
__int64 m128_i64[2];
unsigned __int8 m128_u8[16];
unsigned __int16 m128_u16[8];
unsigned __int32 m128_u32[4];
} __m128;
对于类中的数据成员建议分别使用XMFLOAT2(2D),XMFLOAT2(3D),XMFLOAT2(4D)
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
类型转换为XMVECTn
类型的存储函数。(想了很久不清楚它这里为什么要分别用俩种类型互相转换,而不是直接用XMVECTOR
直接定义,我自己趋向的结果是类型明确,加上此程序开发者不关心其他类型的冗余数据,如果有什么官方解释,麻烦请留言,谢谢)。
用下面的方法将数据类型从XMFLOATn
类型加载到XMVECTOR
类型 :
XMVECTOR XM_CALLCONV XMLoadFloat2(const XMFLOAT2 *pSource);
XMVECTOR XM_CALLCONV XMLoadFloat3(const XMFLOAT3 *pSource);
XMVECTOR XM_CALLCONV XMLoadFloat4(const XMFLOAT4 *pSource);
用下面的方法将数据类型从XMVECTOR
类型存储到XMFLOATn
类型:
void XM_CALLCONV XMStoreFloat2(XMFLOAT2 *pDestination, FXVECTOR V);
void XM_CALLCONV XMStoreFloat3(XMFLOAT3 *pDestination, FXVECTOR V);
void XM_CALLCONV XMStoreFloat4(XMFLOAT4 *pDestination, FXVECTOR 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寄存器里,而不存于栈内。以此方式传递的参数数量取决于用户使用的平台和编译器。因此为了使代码更具通用性,不受具体平台、编译器的影响,我们将利用FXMVECTOR
、GXMVECTOR
、HXMECTOR
和CXMVECTOR
类型来传递XMVECTOR
类型的参数。基于特定的平台和编译器,它们会被自动地定义为适当的类型。
传递XMVECTOR
参数的规则如下:
在32位的Windows系统上,编译器将根据__vectorcall
调用约定将前3个,XMVECTOR参数传递到寄存器中,而把其余参数都存于栈上。
在64位的Windows系统上,编译器将根据__fastcall
调用约定将前6个,`XMVECTOR参数传递到寄存器中,而把其余参数都存于栈上。
前3个XMVECTOR
参数的类型都是应当用类型:FXMVECTOR
;
第4个XMVECTOR
参数应当用类型:GXMVECTOR
;
第5、6个XMVECTOR
参数应当用类型:HXMVECTOR
;
其余的XMVECTOR
参数应当用类型:CXMVECTOR
;
构造函数对于这些规则来说是个例外,在编写时,前三个XMVECTOR
参数用FXMVECTOR
类型,其余用CXMVECTOR
类型,另外对于构造函数,不要用XM_CALLCONV
注解。
下面是截取自DirectXMath
源码库:
inline XMMATRIX XM_CALLCONV XMMatrixTransformation(
FXMVECTOR ScalingOrigin,
FXMVECTOR ScalingOrientationQuaternion, .
FXMVECTOR Scaling,//第1~3个参数用FXMVECTOR
GXMVECTOR RotationOrigin,//第4个参数用GXMVECTOR
HXMVECTOR RotationQuaternion,
HXMVECTOR Translation);//5~6个参数用CXMVECTOR
当我们在这些参数之间掺杂其他不是XMVECTOR
参数类型,此规则依旧可用:
inline XMMATRIX XM_CALLCONV XMMatrixTransformation2D(
FXMVECTOR ScalingOrigin,
float ScalingOrientation,
FXMVECTOR Scaling,
FXMVECTOR RotationOrigin,
float Rotation,
GXMVECTOR Translation);//第4个XMVECTOR参数
传递XMVECTOR
参数的规则仅适用“输入”参数。“输出”的XMVECTOR
参数则不会占用SSE/SSE2寄存器,所以他们的处理方式与非XMVECTOR
类型的参数一致。
常向量
XMVECTOR
类型的惨了实例应当用XMVECTORF32
类型来表示。在DirectX SDK中就可以看到这种实例的运用:
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
转换成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库定义了一组与 π \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; }
Setter函数
DirectXMath库提供了下列函数,以设置XMVECTOR类型中的数据:
// Returns the zero vector 0
XMVECTOR XM_CALLCONV XMVectorZero();
// Returns the vector (1, 1, 1, 1)
XMVECTOR XM_CALLCONV XMVectorSplatOne();
// Returns the vector (x, y, z, w)
XMVECTOR XM_CALLCONV XMVectorSet(float x, float y,
float z, float w);
// Returns the vector (s, s, s, s)
XMVECTOR XM_CALLCONV XMVectorReplicate(float Value);
// Returns the vector (vx, vx, vx, vx)
XMVECTOR XM_CALLCONV XMVectorSplatX(FXMVECTOR V);
// Returns the vector (vy, vy, vy, vy)
XMVECTOR XM_CALLCONV XMVectorSplatY(FXMVECTOR V);
// Returns the vector (vz, vz, vz, vz)
XMVECTOR XM_CALLCONV XMVectorSplatZ(FXMVECTOR V);
实例程序:
#include <windows.h> //
#include <DirectXMath.h>
#include <DirectXPackedVector.h>
#include
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;
}
运行结果:
向量函数
DirectXMath库提供了下面的函数来执行向量的各种运算,类似的有2D、3D、4D,下面的就是3D的部分函数。
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);
XMVECTOR XM_CALLCONV XMVector3Orthogonal(FXMVECTOR V);
XMVECTOR XM_CALLCONV XMVector3AngleBetweenVectors(FXMVECTOR V1,FXMVECTOR V2);
void XM_CALLCONV XMVector3ComponentsFromNormal(XMVECTOR* pParallel,XMVECTOR* pPerpendicular,FXMVECTOR V,FXMVECTOR Normal);
bool XM_CALLCONV XMVector3Equal(FXMVECTOR V1,FXMVECTOR V2);
bool XM_CALLCONV XMVector3NotEqual(FXMVECTOR V1,FXMVECTOR V2);
下面Demo演示一些函数与重载的使用:
#include <windows.h> // for XMVerifyCPUSupport
#include <DirectXMath.h>
#include <DirectXPackedVector.h>
#include <iostream>
using namespace std;
using namespace DirectX;
using namespace DirectX::PackedVector;
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);
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 a = u + v;
XMVECTOR b = u - v;
XMVECTOR c = 10.0f * u;
XMVECTOR L = XMVector3Length(u);
XMVECTOR d = XMVector3Normalize(u);
XMVECTOR s = XMVector3Dot(u, v);
XMVECTOR e = XMVector3Cross(u, v);
XMVECTOR projW;
XMVECTOR perpW;
XMVector3ComponentsFromNormal(&projW, &perpW, w, n);
bool equal = XMVector3Equal(projW + perpW, w) != 0;
bool notEqual = XMVector3NotEqual(projW + perpW, w)
!= 0;
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,但是在计算机程序中的表达上,向量的长度只能接近于1。此外,在数学中,对于任意实数 p p p有 1 p = 1 1^p=1 1p=1。但是,当只能在数值上逼近1时,随着幂 p p p的增加,所求近似值的误差也在逐渐增大,由此可见,数值误差时可以积累的。下面这个小程序可以印证这些观点:
#include <windows.h>
#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));
cout << LU << endl;
if (LU == 1.0f)
//在数学上,此向量的长度应当为1.
cout << "Length 1" << endl;
else
cout << "Length not 1" << endl;
float powLU = powf(LU, 1.0e6f);
cout << "LU ^ (10 ^ 6) = " << powLU << endl;
}
运算结果:
因此为了弥补浮点数精确性上的不足,我们通过比较俩个浮点数是否近似相等来加以解决。在比较的时候,我们需要定义一个Epsilon
常量,我们就说这俩个数是近似相等的。换句话说,Epsilon
是针对浮点数的误差问题所指定的容差。下面函数解释了如何利用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
作为容差,测试比较的向量是否相等:
// Returns
// 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);