dx12 龙书第一章学习笔记 -- 向量代数

1.向量与坐标系

向量:兼具大小和方向的量

同一个向量v在不同的坐标系中有着不同的坐标表示 -- 向量对应的坐标总是相对于某一参考系而言,我们需要知道如何将向量坐标在不同的框架之间进行转换

dx12中涉及顶点空间->世界空间->摄影空间的坐标系转换,其转换矩阵可以通过dx12提供的函数获得,也可以自行构造

Direct3D采用左手坐标系 -- 右手换成左手即可(四指:+x 四指弯曲:+y 大拇指:+z)

2.向量运算

3D向量基本运算:①相等②加减法③标量乘法④normalizing(规范化)⑤正交投影

⑥点积 dot product:结果:数字

注意,根据点积的性质,可以得到:

点积的几何意义:dx12中大量使用单位向量,点积用于求夹角余弦值

⑦叉积 cross product:结果:向量(正交于u、v) - 3D向量

可以明显看出:

w的方向确定:uv总是按两者间较小的角度弯曲四指,根据左手定则,大拇指方向为w的方向

叉积公式对于左右手坐标系而言都是一样的,区别在于左右手坐标系的z轴方向不同,所以导致相同坐标在图中表示不同

3.正交化

正交化:如果向量集{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轴)

4.d3d的数学库 -- DirectXMath

该数学库采用了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)

  • 向量叉乘不符合交换律(b×a方向朝下),符合结合律,分配律。​​​​​​​
  • 向量点乘符合交换律,结合律,分配律。

你可能感兴趣的:(DirectX,笔记,学习,c++,游戏)