DirectXMath
XMVECTOR 向量
DirectXMath中的向量以XMVECTOR类型存储,其定义如下:
也就是说,XMVECTOR实为使用typedef定义的_m128类型的别名。它是一种特殊的SIMD类型。
Single Instruction, Multiple Data
单指令多数据流,为了高并发数学运算设计的硬件加速数据类型,允许在同一时钟周期中处理多个数据。
内存对齐
由于SIMD寄存器一次读取128位(16字节),它会要求内存对齐,即在内存中的位置必须是16的倍数。
为了不在运行时因为地址不对齐报错,一种常用的解决方案是使用 XMFLOATn 系列类型(如 XMFLOAT3, XMFLOAT4, XMFLOAT4X4)进行存储,在计算时通过XMLoad...(&XMFLOATn)方法将数据加载为SIMD类型,计算结束后再用XMStore...(&XMVECTOR,XMFLOATn)方法把数据存回去。
访问和修改
SIMD类型的数据不允许像结构体一样使用类似point.x的方式直接进行直接访问或修改,DirectXMath提供了以下方法:
- 读取值:使用
XMVectorGetX(v)、XMVectorGetY(v)、XMVectorGetZ(v)和XMVectorGetW(v)读取v的单个分量,得到一个浮点数返回值;使用XMStoreFloat4((XMFLOAT4*)data, v)一次性读取v的四个分量到长度为4的浮点数数组data中。 - 修改值:一种方法是使用
XMVectorSet(x,y,z,w)重新设置值,另一种则是使用XMVectorSet...(v, flaot)将v的指定分量设置为指定浮点数。 - 复制值:使用
XMVectorSplat...(v)将v的四个分量都设置为指定分量的值,并返回这个新的对象。 - 重组值:使用
XMVectorSwizzle<x, y, z, w>(v)将v的不同分量交换位置并返回交换后的对象,其中x、y、z和w是$0-3$之间的正整数,表示对应位置的值原本在v中的索引。如,XMVectorSwizzle<1, 0, 3, 2>(v)表示将v的x和y分量交换位置,z和w分量交换位置。
以上所有方法不是原地修改,而是回返回一个新的对象。
此外,尽管DirectXMath提供了以上方法对XMVECTOR对象修改,但在实践中仍然不建议频繁进出寄存器修改XMVECTOR对象,而是如上文所说,在普通数据需要计算时加载为XMVECTOR对象,计算完成后再存回去。
XM_CALLCONV
在C++中,传递大型对象通常用引用const T&,传递小对象(如int)直接传值。但XMVECTOR较为特殊,它是SIMD类型,可以直接映射到CPU的寄存器上。从效率来看,直接传值会是更优的选择。然而,能够以值传递的参数数量会因为平台(x86,x64)和编译器设置有所不同,DirectXMath定义了一套规则来处理需要接收多个向量的方法传参的方式,这就是XM_CALLCONV。
其规则大体如下:
- 声明函数时,应在函数名前加上
XM_CALLCONV; - 前三个
XMVECTOR参数类型为FXMVECTOR,表示通过寄存器传递。 - 第四个
XMVECTOR参数类型为GXMVECTOR,表示根据编译器决定通过堆栈还是寄存器传递。 - 第五、六个
XMVECTOR参数类型为HXMVECTOR,表示根据编译器决定通过堆栈还是寄存器传递。 - 任何额外的
XMVECTOR参数类型为CXMVECTOR,表示通过堆栈传递。
需要注意:
- 以上规则只适用于
XMVECTOR参数,如果函数的参数列表中包括其它类型的参数,它们不会受影响,也不纳入计数。 - 以上关于参数类型的规则只在声明函数的形参列表时应用,也就是说它们只表示形参类型;输出参数不会使用寄存器,因此返回类型不需要改变;另,在调用函数时只需正常传入
XMVECTOR对象名即可。 其本质上都是XMVECTOR的别名,不是什么其它类型,只会影响编译时传参的方式。
XMVECTORF32 常量向量
如果要定义一个需要16字节对齐的常量向量,应该使用XMVECTORF32类型,它可以以下方式进行初始化:
XMVECTORF32重载了转换运算符,允许在需要XMVETOR类型的位置使用XMVECTORF32。
原理上来说,XMVECTORF32使用联合体进行存储值,其定义如下:
点击展开代码
__declspec(align(16)) struct XMVECTORF32 //声明16位地址对齐的结构体
{
//使用联合体存值,允许以为数组初始化的方式去初始化
union{
float f[4];
XMVECTOR v;
};
//隐式类型转换,在float和XMVECTOR之间转换
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
};
运算符重载
DirectXMath重载了XMVECTOR的运算符,使得运算符合向量运算的规则;几乎所有运算符重载都遵循了XM_CALLCONV。
点击展开运算符重载说明
- 一元运算符 这些运算符只作用于向量本身。
| 运算符 | 函数原型 | 功能描述 |
|---|---|---|
正号 + |
operator+ (FXMVECTOR V) |
返回向量本身(通常无实际作用,仅为了对称)。 |
负号 - |
operator- (FXMVECTOR V) |
取反。将向量的四个分量全部取负值 ($x, y, z, w \to -x, -y, -z, -w$)。 |
- 向量与向量之间的二元运算符 这类运算符在两个向量之间进行逐分量运算。
| 运算符 | 函数原型 | 功能描述 |
|---|---|---|
加法 + |
operator+ (V1, V2) |
对应分量相加:$(x_1+x_2, y_1+y_2, z_1+z_2, w_1+w_2)$。 |
减法 - |
operator- (V1, V2) |
对应分量相减:$(x_1-x_2, y_1-y_2, z_1-z_2, w_1-w_2)$。 |
乘法 \* |
operator\* (V1, V2) |
注意:这是分量相乘,不是点积或叉积。结果为 $(x_1x_2, y_1y_2, z_1z_2, w_1w_2)$。 |
除法 / |
operator/ (V1, V2) |
对应分量相除:$(x_1/x_2, y_1/y_2, z_1/z_2, w_1/w_2)$。 |
- 向量与标量之间的二元运算符 这类运算符在一个向量和一个标量之间运算。
| 运算符 | 函数原型 | 功能描述 |
|---|---|---|
乘法 \* |
operator* (V, S) / operator* (S, V) |
每个分量都乘以 $S$。支持 v * s 和 s * v 两种写法。 |
除法 / |
operator/ (V, S) |
每个分量都除以 $S$。 |
Dot&Cross 点积与叉积
点积
数学上点积的计算公式为$\mathbf{u} \cdot \mathbf{v} = u_x v_x + u_y v_y + u_z v_z = |\mathbf{u}| |\mathbf{v}| \cos $,其中$ \theta$是两个向量之间的夹角,结果是一个标量。 在DirectXMath中,使用以下方法计算点积:
它接收两个XMVECTOR对象,返回一个XMVECTOR对象。
也就是说,尽管在数学定义上点积的结果是一个标量,在DirectXMath的实践中通常会将它作为四个分量相同的向量进行处理。这是因为从效率上来说,保持数据在结束运算前始终为SIMD类型是更好的选择。
叉积
数学上叉积的计算公式为$\mathbf{w} = \mathbf{u} \times \mathbf{v}$,得到一个垂直于这两个向量的新向量。 在DirectXMath中,使用以下方法计算叉积:
它接收两个XMVECTOR对象,返回一个XMVECTOR对象。