坐标系规定
在之后的学习中,一般使用左手坐标系,+x,+y,+z分别指向右方、上方、前方。
多坐标系
世界坐标系:协议某个点为原点,其他所有点都有具体不变的坐标,能够用世界坐标系描述其他坐标系的位置,而不能使用更大的、外部的坐标系来描述世界坐标系。
物体坐标系:和物体相关联的坐标系,与某个物体有互动关系。例如我的杯子在左边,我的电脑在我前面。
摄像机坐标系:摄像机即是观察者,可以看作一个特殊的物体。在渲染中,为了节省资源,将摄像机可见的内容渲染。
惯性坐标系:原点与物体坐标系重合,但轴平行于世界坐标系的轴。从物体坐标系转换到惯性坐标系只需要旋转,从从惯性坐标系转换到世界坐标系只需要平移。可把惯性坐标系当作物体坐标系与世界坐标系的中转站,方便用于表达3D世界中事物本身的关系和与全局的关系。
向量
向量的两个属性:大小和指向。物理中的速度即是向量。
向量和点的关系:从原点开始向向量[x,y,z]代表的位移移动,会到达点(x,y,z)的位置,所以在数学中向量和点等价,但概念不同。任意一点都可以用从原点开始的向量表达。
向量运算
向量的模:简单说就是向量的大小或长度。
单位向量:标准化向量或法线。
标准化向量:
向量与标量相乘:
向量加减:
向量距离:
向量点乘(优先级高于加减法):
点乘结果描述了两个向量之间的“相似“程度,结果越大,两个向量越接近。如果a和b中任意为0,则结果为0。
向量叉乘(仅适用于3D向量):
叉乘得到的结果垂直于原来两个向量。
a×b的长度等于向量的大小与向量夹角sin值的积,如下:
备注:
- 如果a、b平行或任意一个为0,结果为0。叉乘对零向量的解释为:它平行于任意其他向量,而点乘的解释是和其他任何向量垂直。
- 叉乘的方向:将a的头和b的尾相接,并检查从a到b是顺时针还是逆时针。在左手坐标系中,如果a和b呈顺时针,那么a×b指向平面向外方向,如果a和b逆时针,a×b指向平面内方向。
- 叉乘的运算优先级和点乘一样,乘法在加减法之前。当点乘和叉乘在一起时,叉乘优先计算:a·b×c=a·(b×c)。标量和向量间不能叉乘。
公式(提取几个容易忘的):
||a||>=0 向量大小非负
||a||+||b||>=||a+b|| 向量加法的三角形法则
a·b=b·a 点乘交换律
||a||=sqrt(a·a) 用点乘定义向量大小
点乘对标量以及向量的加减法有交换律、结合律和分配律。
a×a=0 任意向量与自身的叉乘等于零向量
a×b=-(b×a) 叉乘逆交换律
a×b=(-a)×(-b) 叉乘的操作数同时变负得到相同结果
k(a×b)=(ka)×b=a×(kb) 标量乘法对茶城的结合律
a×(b+c)=a×b+a×c 叉乘对向量加法的分配律
a·(a×b)=0 叉乘与另一向量的叉乘再点乘该向量本身等于零
向量类
以上内容用C++表达如下:
//include<math.h>
class Vector3 {
public:
float x, y, z;
//构造函数
//默认构造函数
Vector3() {}
//复制构造函数
Vector3(const Vector3 &a) :x(a.x), y(a.y), z(a.z) {}
//带参数的构造函数,用三个值完成初始化
Vector3(float nx, float ny, float nz) :x(nx), y(ny), z(nz) {}
//标准对象操作
//重载赋值运算符
Vector3 &operator=(const Vector3 &a)const {
x = a.x;
y = a.y;
z = a.z;
return *this;
}
//重载==运算符
bool operator==(const Vector3 &a)const {
return x == a.x&&y == a.y&&z == a.z;
}
bool operator!=(const Vector3 &a)const {
return x != a.x || y != a.y || z != a.z;
}
//向量运算
//零向量
void zero() {
x = y = z = 0.0f;
}
//重载一元"-"运算符
Vector3 operator-()const {
return Vector3(-x, -y, -z);
}
//重载二元“+”“-”运算符
Vector3 operator + (const Vector3 &a)const {
return Vector3(x + a.x, y + a.y, z + a.z);
}
Vector3 operator -(const Vector3 &a)const {
return Vector3(x - a.x, yy - a.y, z - a.z);
}
//与标量乘除法
Vector3 operator *(float n)const {
return Vector3(n*x, n*y, n*z);
}
Vector3 operator /(float n)const {
float oneOvernN = 1.0f / n;//不对n=0进行检查
return Vector3(oneOvernN*x, oneOvernN*y, oneOvernN*z);
}
//重载自反运算符
Vector3 &operator +=(const Vector3 &a) {
x += a.x;
y += a.y;
z += a.z;
return *this;
}
Vector3 &operator -=(const Vector3 &a) {
x -= a.x;
y -= a.y;
z -= a.z;
return *this;
}
Vector3 &operator *=(float n) {
x *= n;
y *= n;
z *= n;
return *this;
}
Vector3 &operator /=(float n) {
x /= n;
y /= n;
z /= n;
return *this;
}
//向量标准化
void normalize() {
float magSq = x*x + y*y + z*z;
if (magSq > 0.0f) {//检查除零
float oneOverMag = 1.0f / sqrt(magSq);
x *= oneOverMag;
y *= oneOverMag;
z *= oneOverMag;
}
}
//向量点乘,重载标准的乘法运算符
//此处返回一个标量,而与标量乘除法返回一个向量
float operator*(const Vector3 &a)const {
return x*a.x + y*a.y + z*a.z;
}
//非成员函数
//求向量模
inline float vectorMag(const Vector3 &a) {
return sqrt(a.x*a.x + a.y*a.y + a.z*a.z);
}
//计算两向量叉乘
inline Vector3 crossProduct(const Vector3 &a, const Vector3 &b) {
return Vector3(
a.y*b.z - a.z*b.y,
a.z*b.x - a.x*b.z,
a.x*b.y - a.y*b.x
);
}
//标量左乘
inline Vector3 product (float k, const Vector3 &v) {
return Vector3(l*v.x, k*v.x, k*v.z);
}
//计算两点间距离
inline float distance(const Vector3 &a, const Vector &b) {
float dx = a.x - b.x;
float dy = a.y - b.y;
float dz = a.z - b.z;
return sqrt(dx*dx + dy*dy + dz*dz);
}
//全局变量
//提供一个全局零向量
extern const Vector3 kZeroVector;
}
备注:
- 之所以使用float而不是double,是因为使用32位的double节省了大量资源,在3D图形中,节省的资源可以获得更好的性能。
- 为了保持程序简洁及运行速度,重载使用多的运算符以及使用inline关键字指定内联函数。
- 使用const成员函数:保证代码没有副作用,以防不小心改变对象。
- 使用const引用参数:以传值的方式传参会调用一次构造函数,传const引用形式是传值,实际上的是传地址,避免调用构造函数,提高效率。此外,如果函数不是内联的,传值方式比传址方式需要更多的堆栈空间和更长的参数压栈时间。
- 成员函数(如zero())和非成员函数(如vectrMag()):对于只接受一个vector实参的函数,实际上都可以设计。非成员函数不包含隐式this指针的普通函数。为了易懂,使用非成员函数。
- 不做缺省初始化,节省资源。Vector3型变量需要手动初始化。
- 不使用虚函数,同样,为了节省资源和提高效率。
- 不适用protected和private进行信息屏蔽。对于向量类,信息屏蔽不合适。向量类仅存储三个值,如使用getX(),setX()类的函数会让代码变得更复杂,使代码失去简洁和效率。
- 如上两个部分所言,在数学中,点和向量相当,所以不需要创建“点”类。
- 最重要的,保持代码的简洁,节省资源,加快运行效率。