Skip to content

DirectXMath

XMVECTOR 向量

DirectXMath中的向量以XMVECTOR类型存储,其定义如下:

typedef __m128 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的不同分量交换位置并返回交换后的对象,其中xyzw是$0-3$之间的正整数,表示对应位置的值原本在v中的索引。如,XMVectorSwizzle<1, 0, 3, 2>(v)表示将vxy分量交换位置,zw分量交换位置。

以上所有方法不是原地修改,而是回返回一个新的对象。

此外,尽管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 v={1.0f,2.0f,3.0f,4.0f};

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 * ss * 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 XM_CALLCONV XMVector3Dot(FXMVECTOR V1, FXMVECTOR V2);

它接收两个XMVECTOR对象,返回一个XMVECTOR对象。 也就是说,尽管在数学定义上点积的结果是一个标量,在DirectXMath的实践中通常会将它作为四个分量相同的向量进行处理。这是因为从效率上来说,保持数据在结束运算前始终为SIMD类型是更好的选择。

叉积

数学上叉积的计算公式为$\mathbf{w} = \mathbf{u} \times \mathbf{v}$,得到一个垂直于这两个向量的新向量。 在DirectXMath中,使用以下方法计算叉积:

XMVECTOR XM_CALLCONV XMVector3Cross(FXMVECTOR V1, FXMVECTOR V2);

它接收两个XMVECTOR对象,返回一个XMVECTOR对象。