向量:兼具大小和方向的量
同一个向量v在不同的坐标系中有着不同的坐标表示 -- 向量对应的坐标总是相对于某一参考系而言,我们需要知道如何将向量坐标在不同的框架之间进行转换
dx12中涉及顶点空间->世界空间->摄影空间的坐标系转换,其转换矩阵可以通过dx12提供的函数获得,也可以自行构造
Direct3D采用左手坐标系 -- 右手换成左手即可(四指:+x 四指弯曲:+y 大拇指:+z)
3D向量基本运算:①相等②加减法③标量乘法④normalizing(规范化)⑤正交投影
⑥点积 dot product:结果:数字
注意,根据点积的性质,可以得到:
点积的几何意义:dx12中大量使用单位向量,点积用于求夹角余弦值
⑦叉积 cross product:结果:向量(正交于u、v) - 3D向量
可以明显看出:
w的方向确定:uv总是按两者间较小的角度弯曲四指,根据左手定则,大拇指方向为w的方向
叉积公式对于左右手坐标系而言都是一样的,区别在于左右手坐标系的z轴方向不同,所以导致相同坐标在图中表示不同
正交化:如果向量集{v0,v1,...}中的每个向量都是相互正交的且具有单位长度,那么我们称此几何是规范正交的
①格拉姆--施密特正交化:将向量集{v0,v1...}处理称为规范正交的向量集{w0,w1...}的方法 -- 具体做法:在将给定集合内的向量vi添加到规范正交集中时,需要令vi减去它在现有规范正交集中其他向量{w0,...,wi-1}方向上的分量(投影)
举个例子:{v0,v1,v2},要得到{w0,w1,w2},首先固定v0为w0,接下来处理v1->w1,要让w1正交于{w0},v1减去v1对于w0方向的投影,因此得到w1,以此类推,对于v2,v2要分别减去w0的投影、w1的投影,得到w2 -- 最后再normalize所有向量
②叉积正交化法 -- 3D
具体步骤:v0->w0(normalize),w2=(w0×v1).normalized(),此时w2垂直于v0v1的平面,最后,w1=w2×w0
-- 我们可以发现,这两种方法有一个共同点,那就是首先固定了向量w0,也就是没有改变向量v0的方向,这一点在表示摄像机空间时尤为重要,因为我们不希望改变摄像机的观察方向(+z轴)
该数学库采用了SIMD流指令扩展2(SSE2)指令集,借助128位宽的单指令多数据(SIMD)寄存器,利用一条SIMD指令即可同时对4个32位浮点数或整数进行运算 -- 比如4D向量(3D向量就忽略最后一维度)加法,通过一条指令即可对4个分量同时进行加法运算
为了使用DirectXMath库,需要添加头文件:
#include
#include // 相关数据结构
using namespace DirectX; // -- DirectXMath.h
using namespace DirectX::PackedVector; // -- DirectXPackedVector.h
// 除此之外不需要任何其他库文件.所有代码的实现都内联在头文件中
Visual Studio相关配置:
①对于x86平台:需要启用SSE2指令集(Project Properties(工程属性)->Configuration Properties(配置属性)->C/C++->Code Generation(代码生成)->Enable Enhanced Instruction Set(启用增强指令集))
②对于x64平台,我们不必开启SSE2指令集,因为所有x64 CPU对此都有支持
另外,我们还应启用快速浮点模式/fp:fast(Project Properties(工程属性)->Configuration Properties(配置属性)->C/C++->Code Generation(代码生成)->Floating Point Model(浮点模型))
1.向量类型:
核心的向量类型:XMVECTOR
一种不透明的数据类型,不同平台有不同的实现方式
XMVECTOR是一种可移植类型,用于表示四个 32 位浮点或整数组件的向量,每个组件以最佳方式对齐并映射到硬件向量寄存器。
typedef __m128 XMVECTOR; // __m128是一种特殊的SIMD类型
XMVECTOR类型的数据需要按16字节对齐,这对于局部变量和全局变量而言都是自动实现的
至于类中的数据成员,建议分别使用XMFLOAT2,XMFLOAT3,XMFLOAT4类型加以代替
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 XMFLOAT& Float4)
{
x = Float4.x;
y = Float4.y;
z = Float4.z;
w = Float4.w;
return *this;
}
}
我们使用XMFLOAT4不能发挥SIMD技术的高效特性,需要将这些类的实例转换为XMVECTOR类型
XMFLOATn -- 加载函数 --> XMVECTOR
XMVECTOR -- 存储函数 --> XMFLOATn
通过XMVECTOR进行计算,通过XMFLOATn来获取分量值
①加载方法:XMFLOATn -> XMVECTOR
将数据从XMFLOATn类型中加载到XMVECTOR,因为XMVECTOR内部有4个向量组件,所以其实这种加载方式是选择性填充,存储方式同理
XMVECTOR XM_CALLCONV XMLoadFloat4(const XMFLOAT4 *pSource); // 2、3同理
②存储方法:XMVECTOR -> XMFLOATn
void XM_CALLCONV XMStoreFloat4(XMFLOAT4 *pDestination, FXVECTOR V); // 2、3同理
③其他方法:
float XM_CALLCONV XMVectorGetX(FXMVECTOR V); // 获取(存储)x分量 y、z同理
XMVECTOR XM_CALLCONV XMVectorSetX(FXMVECTOR V, float x); // 加载x分量 y、z同理
为了提高效率,可以将XMVECTOR类型的值作为函数的参数,直接传送到SSE/SSE2寄存器里,而不存于栈内。以此方式传递的参数数量取决于用户使用的平台和编译器。因此为了使代码更具通用性,我们将利用FXMVECTOR、GXMVECTOR、HXMECTOR和CXMVECTOR类型来传递XMVECTOR类型的参数。基于特定的平台和编译器,它们会被自动地定义为适当的类型。
传递XMVECTOR参数的规则如下:
前3个XMVECTOR参数的类型都是:FXMVECTOR
第4个XMVECTOR参数:GXMVECTOR
第5、6个XMVECTOR参数:HXMVECTOR
其余的XMVECTOR参数:CXMVECTOR
此外,一定要把调用约定注解XM_CALLCONV加在函数名之前,它会根据编译器的版本确定出对应的调用约定属性
※常向量:XMVECTORF32类型,数学库提供将它转换至XMVECTOR类型的运算符
重载运算符:-- 之前提到,我们是使用XMVECTOR类型进行计算
1.两个XMVECTOR:
+ - * /
+= -= *= /=
2.XMVECTOR与float:
* *= / /=
杂项:与有关的数学常量近似值
const float XM_PI = 3.1415926f;
const float XM_2PI = 6.283185307f;
const float XM_1DIVPI = 0.318309886f;
// XM_1DIV2PI XM_PIDIV2 XM_PIDIV4
内联函数实现弧度和角度的互相转换:
inline float XMConvertToRadians(float fDegrees)
{ return fDegrees * (XM_PI/180.0f); } // 角度转弧度
inline float XMConvertToDegrees(float fRadians)
{ return fRadians * (180.f / XM_PI ); }
求出两数间较大值和较小值的函数:
template inline T XMMin(T a,T b) { return (a inline T XMMax(T a,T b) { return (a>b) ? a : b; }
设置函数 -- 设置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);
// 注意:XMVECTOR本身就是四维的
因为XMVECTOR类型的实例可以用其他同类型实例初始化,所以可以使用设置函数间接初始化一个XMVECTOR类型
向量函数 -- 向量运算:
// 主要讨论3D 也有2D和4D版本
// 返回||v|| 模
XMVECTOR XM_CALLCONV XMVector3Length(FXMVECTOR V);
// 返回||v||^2 模的平方
XMVECTOR XM_CALLCONV XMVector3LengthSq(FXMVECTOR V);
// 返回V1·V2 点乘
XMVECTOR XM_CALLCONV XMVector3Dot(FXMVECTOR V1,FXMVECTOR V2);
// 返回V1×V2 叉乘
XMVECTOR XM_CALLCONV XMVector3Cross(FXMVECTOR V1,FXMVECTOR V2);
// 返回v/||v|| 标准化
XMVECTOR XM_CALLCONV XMVector3Normalize(FXMVECTOR V);
// 返回v1与v2之间的夹角
XMVECTOR XM_CALLCONV XMVector3AngleBetweenVectors(FXMVECTOR V1,FXMVECTOR V2);
// 还有函数:XMVector3Equal(V1,V2) XMVector3NotEqual(V1,V2) -- 返回值bool
// 矢量拆分为平行且垂直于法线的分量
void XM_CALLCONV XMVector3ComponentsFromNormal(
XMVECTOR* pParallel, // proj_n(v)
XMVECTOR* pPerpendicular, // prep_n(v)
FXMVECTOR V,FXMVECTOR Normal
);
注意:点乘函数虽然结果本应该是float类型,但仍然返回XMVECTOR(把这个float值映射到XMVECTOR的每个向量上),能保证SIMD向量与标量的混合运算次数降到最低
目前接触来说,向量计算有关库函数返回值没有float类型的,有返回void bool XMVECTOR类型的
浮点数计算判等问题:
浮点数计算会存在误差,结果不可能完全相等,我们通过比较两个浮点数是否近似相等加以解决
具体做法:定义一个Epsilon常量(const float Epsilon = 0.001f),作为容差,如果两个浮点数之差小于Epsilon,则认为相等
// DirectXMath库提供近似判等函数:
XMFINLINE bool XM_CALLCONV XMVector3NearEqual(
FXMVECTOR U, FXMVECTOR V,
FXMVECTOR Epsilon
);
代码示例:
#include // XMVerifyCPUSupport()函数
#include
#include
#include
using namespace DirectX;
using namespace DirectX::PackedVector;
using namespace std;
ostream& XM_CALLCONV operator<<(ostream& os, FXMVECTOR v)
{
XMFLOAT3 temp;
XMStoreFloat3(&temp, v);
// 要想输出w的值,可以通过XMStoreFloat4来装载FXMVECTOR类型实例
os << "(" << temp.x << "," << temp.y << "," << temp.z << ")";
return os;
}
// 施密特正交化函数:
void XM_CALLCONV GramSchmidt(XMVECTOR* vArr, int numArr)
{
XMVECTOR proj, prep;
for (int i = 0; i < numArr; i++)
{
XMVECTOR projAll = XMVectorZero();
for (int j = 0; j < i; j++) {
XMVector3ComponentsFromNormal(&proj, &prep, vArr[i], vArr[j]); // 此函数遇到的问题,normal方向相同,但大小不同,得到的proj居然不同?
projAll += proj;
}
vArr[i] -= projAll;
vArr[i] = XMVector3Normalize(vArr[i]);
}
}
int main()
{
cout.setf(ios_base::boolalpha);
// 检查是否支持SSE2指令集
if (!XMVerifyCPUSupport())
{
cout << "direct math not supported" << endl;
return 0;
}
XMVECTOR v1 = XMVectorZero();
cout << v1 << endl;
XMVECTOR v2 = XMVectorSet(1, 2, 3, 0);
cout << v2 << endl;
XMVECTOR v3 = XMVectorReplicate(2);
cout << v3 << endl;
XMVECTOR dotValue = XMVector3Dot(v2, v3);
cout << dotValue << endl;
XMVECTOR crossValue = XMVector3Cross(v2, v3);
cout << crossValue << endl;
XMVECTOR lengthValue = XMVector3Length(v2);
cout << lengthValue << endl;
XMVECTOR value = v2 * 2;
cout << value << endl;
// 课后习题18
cout << "------施密特正交化------" << endl;
XMVECTOR v4 = XMVectorSet(1, 0, 0, 0);
XMVECTOR v5 = XMVectorSet(1, 5, 0, 0);
XMVECTOR v6 = XMVectorSet(2, 1, -4, 0);
XMVECTOR vArr[3] = { v4, v5, v6 };
GramSchmidt(vArr, 3);
for (auto v : vArr) {
cout << v << endl;
}
}
// 注意:将XMVECTOR类型作为FXMVECTOR类型进行函数参数传递
⭐遗留问题:FXMVECTOR和XMVECTOR使用场景的区分
一些运算规律:矩阵的转置、逆与伴随运算的运算规律 - cloneycs - 博客园 (cnblogs.com)