DX12学习笔记——第一章 向量代数

向量在计算机图形学,碰撞检测,物理模拟各个方面都扮演了重要的角色,而这几个方面都是现代视频游戏的常用组件。本书这方面的论述比较非正式,偏实用。

目标

  1. 学习向量几何上和代数上的表示;
  2. 向量操作及其几何意义;
  3. 熟悉向量的函数和DirectXMath库的类。

1.1 向量

向量表示一个包含大小和方向的量。比如力、位移,速度。几何里用带方向的线段表示向量。向量跟位置无关。当两个向量的长度,指向的方向相同则两个向量相同。

1.1.1 向量和坐标系统

我们可以给向量定义几何操作以解决问题。计算机不能几何地操作向量,所以我们用代数代表向量。我们在空间中引入了3D坐标系统。平移向量,使向量的尾巴和坐标原点重合(我们说此时,该向量在标准位置上),通过指定向量头部的坐标来唯一标识一个向量。
但是,一个向量相对于A坐标系的坐标和其相对于B的坐标是不同的,即不同坐标系下,同一个向量的坐标不同。我们用坐标定义一个向量的时候,这些坐标都是相对于所使用的坐标系。
向量和点都可以通过所使用坐标系的坐标进行描述,但二者是不同的,一个点代表3D空间里的位置,而向量代表一个长度和方向。

1.1.2 左手坐标系和右手坐标系

Direct3D用左手坐标系。伸出左手,手指向正x轴伸展,然后将手指向y轴弯曲,你的拇指大概指向正z轴。
伸出右手,进行相同操作,则为右手坐标系。

1.1.3 基本向量操作

  1. 相等:两个向量相等,当且仅当他们所有的对应项都相等。
  2. 加法:两个向量相加,将他们的所有对应项相加。两者维数需要相同。
  3. 标量乘法:一个标量和一个向量相乘,将向量的每一项和这个数相乘。
  4. 减法:向量加法和向量乘法结合计算加法:u − v = u + (−1 · v) = u + (−v) = (ux − vx, uy − vy, uz − vz).
    零向量的每个项都是0.
    这几个操作的几何意义:
    在这里插入图片描述

1.2 长度和单位向量

向量长度即代表向量的线段的长度。
有时候我们不需要知道向量长度,因为我们只用向量来表示纯粹的方向,对于这种只需要方向的向量,我们想让它长度为1,。我们把使向量成为单位向量的操作叫归一化向量。通过把向量的每一项除以它的长度实现归一化。
获取向量长度,2维向量用勾股定理,3维向量用两次勾股定理,以此类推。

1.3 点乘

点乘是向量乘法的一种形式,得到一个标量值。u = (ux, uy, uz),v = (vx, vy, vz),则点乘这样定义:
在这里插入图片描述
其几何意义:
在这里插入图片描述
其中,theta角为u,v之间最小的加减,在0到pi之间,为锐角或钝角。
向量点乘为向量的长度乘以他们之间角度的cos值。如果两者都是单位向量,则其点乘为两者间夹角的余弦值。

  1. 如果点乘为0,则两个向量相交;
  2. 点乘大于0,则夹角为锐角;
  3. 点乘小于0,则夹角为钝角。
    cos值可以通过点乘除以向量的长度得到。
    正交投影:
    在这里插入图片描述

1.3.1 正交化

向量集是正交的当且仅当所有向量互相正交(几何中的每个向量都和集合中的其他任意一个向量正交)且为单位向量。
 我们经常需要正交化一个集合,使其标准正交化。在3d图形学中,我们经常以一个标准正交基开始,由于数学精度的问题,这个集合逐渐变成非标准正交的了。

假设有两个向量,v0,v1,我们想要将其正交化为一个标准正交的集合w0,w1.首先,让w0=v0,改变v1,让他正交于w0,(通过减去v1在w0方向上的部分):
w1 = v1 - proj(w0)(v1)
这样我们就得到了w0,w1这个互相正交的向量集合,下一步据说将它们归一化为单位长度。
3维向量和2d方法差不多。w1和w2正交后,我们让w3和w1,w2正交,
w2 = v2 - proj(w0)(v2)-proj(w1)(v2)
我们得到互相正交的向量集合后,构建标准正交集合的最后一步是归一化w0,w1,w2,使他们长度为1.
总结,从输入集中选择一个向量vi,加到正交集合中,减去vi在已经放入到正交集合的其他所有向量方向上的部分,保证新得到的向量正交于已经在正交集合中的所有其他向量。

1.4 叉乘

向量数学的乘法的第二种形式为叉乘。叉乘结果会得到第三个向量,叉乘只针对于3d向量,没有2D叉乘。两个向量u,v的叉乘得到第三个向量w,w和u,v都正交。即:
w = u X v = (uyvz - uzvy, uzvx - uxvz, uxvy - uyvx)
用右手坐标系法则,伸出右手,手指指向u向量的方向,向v方向弯曲手指,拇指指向w方向,w=uXv。
叉乘不符合交换律。

1.4.1 假的2D叉乘

叉乘允许我们找到正交于所给3d向量的向量。2d中一般不会有这种情况,但是给定2d向量u(ux,uy),找到垂直于u的向量v很有用。从图中看,v(-uy,ux)。正面很直接
u. v = (ux, uy ) . (vx, vy) = -uxuy + uxuy = 0
同时,u垂直-v。

1.4.2 用叉乘标准正交化

向量集合v0, v1, v2几乎正交,但由于数学精度误差积累导致不正交,我们可以用叉乘对其进行标准正交化。

  1. 设w0为v0正交化的值,w1为v1归一化的值;
  2. w2为w0和v1叉乘值,再归一化;那么,w2与w1和w0正交;
  3. w1为w0和w2叉乘再归一化。得到的w1,w2,w0互相正交
    v0和w0方向一样,只是长度变了;而w1,w2和v1,v2方向可能不一样。我们需要根据应用场景不同,选择改变方向的向量。后面我们用3个正交向量代表相机的方向,v2代表相机看的方向。当正交化这3个向量时,我们不希望改变相机看的方向,所以我们从v2开始,改变v0和v1来正交化向量。

1.5 点

我们用位于标准位置的向量来表示坐标。我们称为坐标向量,这时,我们关心的只是向量箭头的坐标,而不是方向和长度。
这样的弊端是,我们做向量操作对于点没有意义。而另一方面,一些操作可以扩展到点上,比如,p点到q点的差是从p指向q的向量。点p加上向量v得到q,跟p经过位移v得到q一样的。
在这里插入图片描述
所以,我们可以用向量代表点在坐标系中的位置,就不需要再定义点的操作了,因为向量代数已经有这些内容了。

1.6 DirectX数学向量

windows8及以上,DirectX数学是用于Direct3D应用的一个3D数学库,是windows SDK的一部分。这个库用了SSE2指令集。128比特宽度的simd(单指令多数据)寄存器,simd指令可以在一个指令中对4个32比特float或int32进行操作。这对于向量计算特别有用:u+v=(ux+vx, uy+vy, uz+vz)
我们把对应的部分相加。通过用simd,我们能用一个simd指令进行向量计算而不是4个标量运算。向量的2个或3个组件的计算也可以用simd,将不用的组件设为0,忽略它们。
要用DirectX数学库的话,要添加DirectXMath.h和DirectXPackedVector.h,前者在DirectX命名空间,后者在DirectX::PackedVector命名空间。x86平台需要开启SSE2(Project Properties > Configuration Properties > C/C++ >
Code Generation > Enable Enhanced Instruction Set),每个平台要开启快速浮点模型(Project Properties >
Configuration Properties > C/C++ > Code Generation > Floating Point Model).。

1.6.1 向量类型

在DirectX数学中,核心向量类型是XMVECTOR,它会映射到SIMD硬件寄存器。它是一个128比特类型,可以处理4个32位float用一个simd指令。sse2可用时,XMVECTOR是这样定义的:
typedef __m128 XMVECTOR;
__m128是一个特殊的SIMD类型。在进行计算时,向量必须是这个类型才能利用SIMD的优势。
使用方式:

  1. 局部和全局变量用XMVECTOR类型;
  2. 类数据成员使用XMFLOAT2, XMFLOAT3和XMFLOAT4类型;
  3. 进行计算前用加载函数 将XMFLOATn转换位XMVECTOR类型;
  4. 用XMVECTOR实例进行计算;
  5. 用存储函数将XMVECTOR转换为XMFLOATn类型。

1.6.2 加载和存储函数

从XMFLOATn到XMVECTOR,用加载函数:

// Loads XMFLOAT2 into XMVECTOR
XMVECTOR XM_CALLCONV XMLoadFloat2(const XMFLOAT2
*pSource);
// Loads XMFLOAT3 into XMVECTOR
XMVECTOR XM_CALLCONV XMLoadFloat3(const XMFLOAT3
*pSource);
// Loads XMFLOAT4 into XMVECTOR
XMVECTOR XM_CALLCONV XMLoadFloat4(const XMFLOAT4
*pSource);

从XMVECTOR类型存储为XMFLOATn类型,用存储函数:

// Loads XMVECTOR into XMFLOAT2
void XM_CALLCONV XMStoreFloat2(XMFLOAT2
*pDestination, FXMVECTOR V);
// Loads XMVECTOR into XMFLOAT3
void XM_CALLCONV XMStoreFloat3(XMFLOAT3
*pDestination, FXMVECTOR V);
// Loads XMVECTOR into XMFLOAT4
void XM_CALLCONV XMStoreFloat4(XMFLOAT4
*pDestination, FXMVECTOR V);

设置XMVECTOR的一个组件用下面的set和get方法:

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寄存器上的函数而不是在堆栈上。以这种方式传递的参数的个数依赖于平台和编译器。所以,为了平台无关,我们用FXMVECTOR, GXMVECTOR, HXMVECTOR和CMXVECTOR来传递XMVECTOR参数,这些类型会基于平台和编译器转换为正确的类型。另外,XM_CALLCONV必须在函数名前进行定义,以保证正确的调用惯例被使用(依赖于平台和编译器)。
传递XMVECTOR参数的规则如下:

  1. 前三个XMVECTOR为FXMVECTOR类型;
  2. 第四个为GXMVECTOR类型;
  3. 第5和第6为HXMVECTOR类型;
  4. 其他为CXMVECTOR类型。

下面,我们说明在支持__fastcall调用惯例和__vectorcall调用惯例的32位windows编译器上,这些类型是如何定义的:
前3个通过寄存器传递,剩下的在栈上

  // 32-bit Windows __fastcall passes first 3
    XMVECTOR arguments
    // via registers, the remaining on the stack.
    typedef const XMVECTOR FXMVECTOR;
    typedef const XMVECTOR& GXMVECTOR;
    typedef const XMVECTOR& HXMVECTOR;
    typedef const XMVECTOR& CXMVECTOR;

前6个通过寄存器传递,剩下的在栈上

 // 32-bit Windows __vectorcall passes first 6
    XMVECTOR arguments
    // via registers, the remaining on the stack.
    typedef const XMVECTOR FXMVECTOR;
    typedef const XMVECTOR GXMVECTOR;
    typedef const XMVECTOR HXMVECTOR;
    typedef const XMVECTOR& CXMVECTOR;

其他平台的定义可查看文档。这些规则的一个例外是构造函数,DirectXMath建议前三个函数用FXMVECTOR类型,6个以外的用CXMVECTOR类型。另外,构造函数不要用XM_CALLCONV。

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

在XMVECTOR参数中间可以有非XMVECTOR参数。这些XMVECTOR使用的规则和计数方式跟中间的非XMVECTOR参数不存在一样。
这些传递XMVECTOR参数的规则应用于input参数,output参数(XMVECTOR&或XMVECTOR*)不会用SSE/SSE2寄存器,当作非XMVECTOR参数对待。

1.6.4 常量向量

常量XMVECTOR实例应该用XMVECTORF32类型,比如:

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,它是一个16字节对齐的结构,有XMVECTOR的转换操作,这样定义的:

// Conversion types for constants
__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常量:

XMVECTORU32:
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 杂项

DirectX数学库定义了以下的常量,用来近似涉及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); }

最大最小函数:

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 set函数

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

1.6.8 向量函数

DirectX提供以下函数来进行不同的向量操作,我们以3D为例,2维和4维也是一样的,只是将2或4替换了3.
向量长度:
XMVECTOR XM_CALLCONV XMVector3Length(FXVECTOR V);
长度平方
XMVECTOR XM_CALLCONV XMVector3LengthSq( //Returns ||v||2
FXMVECTOR V); // Input v
点乘
XMVECTOR XM_CALLCONV XMVector3Dot( // Returns
v1·v2
FXMVECTOR V1, // Input v1
FXMVECTOR V2); // Input v2
叉乘
XMVECTOR XM_CALLCONV XMVector3Cross( // Returns
v1 × v2
FXMVECTOR V1, // Input v1
FXMVECTOR V2); // Input v2
归一化
XMVECTOR XM_CALLCONV XMVector3Normalize( //
Returns v/||v||
FXMVECTOR V); // Input v
正交向量
XMVECTOR XM_CALLCONV XMVector3Orthogonal( //
Returns a vector orthogonal to v
FXMVECTOR V); // Input v
俩向量之间的角度
XMVECTOR XM_CALLCONV
XMVector3AngleBetweenVectors( // Returns the
angle between v1 and v2
FXMVECTOR V1, // Input v1
FXMVECTOR V2); // Input v2
v和法线得到的几个向量
void XM_CALLCONV XMVector3ComponentsFromNormal(
XMVECTOR* pParallel, // Returns projn(v)
XMVECTOR* pPerpendicular, // Returns perpn(v)
FXMVECTOR V, // Input v
FXMVECTOR Normal); // Input n
俩向量是否相等
bool XM_CALLCONV XMVector3Equal( // Returns v1
= v2
FXMVECTOR V1, // Input v1
FXMVECTOR V2); // Input v2
是否不等
bool XM_CALLCONV XMVector3NotEqual( // Returns
v1 ≠ v2
FXMVECTOR V1, // Input v1
FXMVECTOR V2); // Input v2
注意,哪些即使返回标量的值的操作返回的也是XMVECTOR类型,比如点乘。标量结果在结果的每个组件重复。比如叉乘计算返回的向量是(v1 · v2, v1 · v2,v1 · v2,v1 · v2)。这样做的结果是最小化标量和SIMD向量操作的混合。保持所有东西SIMD直到计算完成效率更高。
DirectX数学库提供了一些估计的函数,精确度小但计算很快。比如

XMVECTOR XM_CALLCONV XMVector3LengthEst( //
Returns estimated ||v||
FXMVECTOR V); // Input v
XMVECTOR XM_CALLCONV XMVector3NormalizeEst( //
Returns estimated v/||v||
FXMVECTOR V); // Input v

1.6.9 浮点数误差

当比较浮点数的时候,需要考虑其误差。将浮点数乘n次,误差会累积。我们说两个浮点数近似相等,当他们之间距离小于一个很小的常量Epsilon时。DirectX数学库提供了XMVECTOR3NearEqual函数,允许两个向量在Epsilon范围内存在误差。

1.7 总结

  1. 向量用来模拟有长度和方向的物理量。一般我们用带箭头的线段表示向量。当向量平移,尾巴和坐标系的原点对齐时,我们称它在标准位置。在标准位置的向量可以通过指定它在坐标系的坐标来线性地表示。
  2. 向量间操作:加法,减法,标量乘法,长度,正交化,点乘,叉乘,判断是否相等等。
  3. 用SIMD操作的代码里可以使用DirectX数学里的XMVECTOR有效率的描述向量。类的数据成员我们用XMFLOAT2, XMFLOAT3,XMFLOAT4类型表示,然后通过加载和存储函数在XMFLOATn和XMVECTOR类型间相互转换。需要初始化的常量向量一般用XMVECTORF32类型表示。
  4. 为了提高效率,XMVECTOR可以当作参数在SSE/SSE2寄存器上传递函数而不是在栈上。为了平台无关,我们用FXMVECTOR, GXMVECTOR, HXMVECTOR和CXMVECTOR传递XMVECTOR参数。规则是:前3个用FXMVECTOR, 第4个用GXMVECTOR, 第5,6个用HXMVECTOR, 其他的用XCMVECTOR.
  5. XMVECTOR重载了一些数学运算符做向量加法,减法,标量乘法运算。另外,DirectX数学库还提供了计算向量长度,向量平方长度,向量点乘,叉乘,归一化等操作。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值