39. 游戏数学基础

 矢量是游戏开发中涉及到的最基本的对象。矢量有多种形式,可以是2D、3D或4D等。3D矢量是一个包含3个浮点数的结构,每个值代表矢量中不同的轴。这些值分别是x、y和z轴,可用于描述三维空间。矢量用于描述3D空间中的方向,它可能也是最常用的数学对象。

       矩阵是游戏开发中第二个最常用的数学对象。矩阵主要用于将矢量从一个坐标系转换到另一个坐标系,还可用于旋转和平移。矩阵由浮点数的2D数组组成。3×3矩阵由3行、3列共9个元素组成,4×4矩阵是由4行、4列共16个浮点元素组成的2D数组。

四元组用于描述旋转。四元组是由4个浮点数w、x、y、z构成的结构,像4D矢量。虽然矩阵也可用于旋转,但使用四元组更好,因为四元组只有4个浮点数,而矩阵有16个浮点数(对4×4矩阵而言)。这意味着存储四元组所需的空间比存储矩阵所需的空间少,另外四元组的数学操作也要少些。这使得四元组的计算速度更块。在处理旋转时,四元组同样比矩阵更平滑。

       射线用于描述位置和方向。射线有原点,也就是射线开始的位置,方向就是射线指向的地方。射线包括两个矢量,一个代表位置,一个代表方向。通常,射线用于碰撞检测.

平面是在区域上无限扩展的网格。可以将平面当作是沿地无限延伸的面。平面无限窄,而且没有边界。另一方面,多边形是封闭的区域,它有边界,而且大小有限。为了让多边形更像是多边形,它就要包含3个或更多的点。如果有两条连接的直线,那么除了两条直线外,什么也没有创建。将3条线段连接到一起就会形成一个三角形,这是计算机图形学中最常用的一种形状。

       物理学是个很庞大的领域。本书并没有足够的篇幅来详细介绍。本书实现的物理学可浓缩为3D场景中的重力和碰撞检测。

8.2 矢量数学和回顾
       
如前所述,矢量是游戏开发中涉及的最基本对象。3D矢量保存在3D空间中指定位置的3个轴上。这些轴分别是x轴、y轴和z轴,分别代表空间中的宽度、高度和深度。这些值可对3D环境中的任意物体定位。3D矢量是一个包含三个浮点数的结构。多数人使用标号为x、y、z的变量指定这三个值,而其他人使用三个浮点数构成的数组来指定。

// Example 1
struct Vector3D
{
float x, y, z;
};

// Example 2
struct Vector3D
{
float v[3];
};

  

void VecAdd(Vector3D v1, Vector3D v2, Vector3D &out)
{
out.x = v1.x + v2.x;
out.y = v1.y + v2.y;
out.z = v1.z + v2.z;
}

void VecSub(Vector3D v1, Vector3D v2, Vector3D &out)
{
out.x = v1.x - v2.x;
out.y = v1.y - v2.y;
out.z = v1.z - v2.z;
}

void VecMultiply(Vector3D v1, Vector3D v2, Vector3D &out)
{
out.x = v1.x * v2.x;
out.y = v1.y * v2.y;
out.z = v1.z * v2.z;
}

void VecDivide(Vector3D v1, Vector3D v2, Vector3D &out)
{
out.x = v1.x / v2.x;
out.y = v1.y / v2.y;
out.z = v1.z / v2.z;
}

void VecEquals(Vector3D v1, Vector3D &out)
{
out.x = v1.x;
out.y = v1.y;
out.z = v1.z;
}

  有的操作还可以实现游戏开发中很重要的内容。这些操作包括计算矢量长度(即幅度),计算两个矢量的点积,计算两个矢量的叉积及矢量归一化。

       同样可以通过将一个矢量的分量乘上另一个矢量的分量,并将乘积结果相加来计算矢量的长度。为了的得到矢量长度,可以使用执行速度很慢的sqrt()函数。该函数并没有那么糟糕,因为函数中只包含了sqrt()函数一次。但要牢记的是,平方根函数是CPU上执行速度最慢的一个函数。

float VecLength(Vector3D v1)
{
return sqrt(v1.x*v1.x + v1.y*v1.y + v1.z*v1.z);
}

     两个矢量的点积用于度量两个方向的差。点积又称标量积,在游戏开发中的应用较广,尤其是在手动光照算法中得到了大量的应用。将所有对应分量的乘积相加到一起就得到了点积结果。这与计算矢量长度很相似,除了其中用到了平方根操作之外。程序清单8.5给出了计算两个矢量点积的示例。

float VecDotProduct(Vector3D v1, Vector3D v2)
{
return v1.x*v2.x + v1.y*v2.y + v1.z*v2.z;
}

  叉积又名为矢量积,它是一个新矢量,通过查找与两个矢量正交的矢量而计算得到该矢量。这种特性在游戏开发中有很多用途,经常会看到它的使用。使用叉积的示例之一就是计算多边形的法线(多边形面朝的方向)。通过获取两个矢量中每个相反分量之间的差,可以计算出叉积。这意味着为了得到叉积的x值,必须将第一个矢量的y分量和第二个矢量的z分量相乘,然后减去第一个矢量的z分量和第二个矢量的y分量的乘积结果。程序清单8.6给出了计算每个分量叉积结果的方法。叉积又名外积。

void VecCrossProduct(Vector3D v1, Vector3D v2, Vector3D &out)
{
out.x = (v1.y*v2.z) - (v1.z*v2.y);
out.y = (v1.z*v2.x) - (v1.x*v2.z);
out.z = (v1.x*v2.y) - (v1.y*v2.x);
}

  将矢量归一化为单位长度,也就是说想要矢量具有相同的方向,而且矢量长度为1。这意味着计算矢量长度时,其长度为1。这在许多游戏开发程序中得到了应用,常用于光照中。

       为了归一化矢量,所要做的全部工作是得到矢量长度,然后对每个分量根据长度进行缩放。换句话说,使用计算矢量长度的相同代码得到长度,然后再用该长度除以每个分量。这样就可以对矢量进行缩放,矢量长度就等于1,由此可以得到归一化的矢量。程序清单8.7给出了具体的代码实现。

void VecNormalize(Vector3D v1, Vector3D &out)
{
float inv_length = 1 / VecLength(v1);
out.x = v1.x * inv_length;
out.y = v1.y * inv_length;
out.z = v1.z * inv_length;
}

  

Direct3D矢量
       
可以选择使用Direct3D结构和函数以实现矢量数学。这样做很好,因为为了使用矢量数学,而无需完全理解或对其编码,同样还可以很大程度地提高程序性能。因为Microsoft公司的代码做了很好的优化,而且效率非常高。

   Direct3D矢量包括2D、3D和4D三个版本。诚如所知,每一维都确定了矢量结构中的轴数。程序清单8.8给出了这几类矢量的示例。

D3DXVECTOR2 vec2;
D3DXVECTOR3 vec3;
D3DXVECTOR4 vec4;

  Direct3D矢量函数如预期一样工作。这些函数的好处就在于它们经过了专业优化,使用它们没有任何问题,而这都根据情况而定。如果正在编写只和Direct3D打交道的代码,尤其是如果读者数学功底不是很好,那么Direct3D矢量函数也许就是读者要考虑使用的对象。

D3DXVECTOR3 v1, v2, result;
float val;

D3DXVec3Add(
&result, &v1, &v2);
D3DXVec3Subtract(
&result, &v1, &v2);

val
= D3DXVec3Length(&v1);
val
= D3DXVec3Dot(&v1, &v2);
D3DXVec3Cross(
&result, &v1, &v2);
D3DXVec3Normalize(
&result, &v1);

  矩阵数学

       在计算机图形学中,诸如矩阵这样的对象可以控制物体的位置和旋转。矩阵是浮点数的2D数组,可以对矢量进行平移、旋转、缩放和变换。从根本上讲,矩阵是由行和列构成的表。矩阵有多种形式,如3×3、3×4和4×4。这里矩阵的数分别代表2D数组包含的行数和列数。例如,4×4矩阵由16个元素组成,因为4×4矩阵有4行,4列(4×4=16)。

       和其他如矢量这样的数学结构相比,矩阵的可视化表示比较困难。实际上,可以试着将矩阵当成对象考虑,它可以通过如旋转这样的操作更改或改变矢量。在3D游戏编程中,大部分工作都要和4×4矩阵打交道。对矩阵同样可以实现和矢量相类似的操作,诸如矩阵和其他矩阵或矢量的乘法。例如,在将已经旋转过的矩阵和一组矢量相乘时,得到的结果矢量将以在矩阵中指定的相同量旋转。用一个旋转了90°的矩阵乘上微个对象几何图形,那么该几何图形同样旋转90°。

 做矩阵乘法运算时,主要是将各个矩阵合并为一个矩阵。因此,可以用一个矩阵存储旋转数据,另一个矩阵存储平移矩阵。然后在准备使用这两个矩阵时,可以将它们合并成一个矩阵。然后可以使用这个最终矩阵乘上所有的矢量对象,以实现所有的旋转和平移,从而得到最终几何图形的位置和方向。用矩阵乘上矢量可以实现对原始几何图形的操作,并将其转换成新的表现形式。这是矩阵最常用的一点,读者可以做一些超出本书研究范围的工作。对矢量和矩阵做乘法,就是对矢量做变换。

       如前所述,4×4矩阵是一个由16个浮点数组成的结构。如果使用的是类,那么同样可以指定旋转、缩放、平移及变换矢量的成员函数,同样可以指定一个函数,计算两个不同的矩阵乘法。程序清单8.10给出了在简单C++类中实现的方法。

 1 class CMatrix4x4
2 {
3 public:
4 CMatrix4x4();
5
6 void Identity();
7
8 void operator=(CMatrix4x4 &m);
9 CMatrix4x4 operator*(CMatrix4x4 &m);
10 Vector3D operator*(Vector3D &v);
11
12 void Translate(float x, float y, float z);
13 void Rotate(double angle, int x, int y, int z);
14
15 float matrix[16];
16 };

  

 1 void CMatrix4x4::Identity()
2 {
3 memset(this, 0, sizeof(CMatrix4x4));
4 matrix[0] = 1.0f;
5 matrix[5] = 1.0f;
6 matrix[10] = 1.0f;
7 matrix[15] = 1.0f;
8 }
9
10
11 void CMatrix4x4::operator =(CMatrix4x4 &m)
12 {
13 memcpy(this, &m, sizeof(m));
14 }
15
16 CMatrix4x4 CMatrix4x4::operator *(CMatrix4x4 &m)
17 {
18 float *m = matrix;
19 float *m2 = m.matrix;
20
21 return CMatrix4x4(m[0] * m2[0] + m[4] * m2[1] +
22 m[8] * m2[2] + m[12] * m2[3],
23 m[1] * m2[0] + m[5] * m2[1] +
24 m[9] * m2[2] + m[13] * m2[3],
25 m[2] * m2[0] + m[6] * m2[1] +
26 m[18] * m2[2] + m[14] * m2[3],
27 m[3] * m2[0] + m[7] * m2[1] +
28 m[11] * m2[2] + m[15] * m2[3],
29 m[0] * m2[4] + m[4] * m2[5] +
30 m[8] * m2[6] + m[12] * m2[7],
31 m[1] * m2[4] + m[5] * m2[5] +
32 m[9] * m2[6] + m[13] * m2[7],
33 m[2] * m2[4] + m[6] * m2[5] +
34 m[10] * m2[6] + m[14] * m2[7],
35 m[3] * m2[4] + m[7] * m2[5] +
36 m[11] * m2[6] + m[15] * m2[7],
37 m[0] * m2[8] + m[4] * m2[9] +
38 m[8] * m2[10] + m[12] * m2[11],
39 m[1] * m2[8] + m[5] * m2[9] +
40 m[9] * m2[10] + m[13] * m2[11],
41 m[2] * m2[8] + m[6] * m2[9] +
42 m[10] * m2[10] + m[14] * m2[11],
43 m[3] * m2[8] + m[7] * m2[9] +
44 m[11] * m2[10] + m[15] * m2[11],
45 m[0] * m2[12] + m[4] * m2[13] +
46 m[8] * m2[4] + m[12] * m2[15],
47 m[1] * m2[12] + m[5] * m2[13] +
48 m[9] * m2[4] + m[13] * m2[15],
49 m[2] * m2[12] + m[6] * m2[13] +
50 m[10] * m2[4] + m[14] * m2[15],
51 m[3] * m2[12] + m[7] * m2[13] +
52 m[11] * m2[4] + m[15] * m2[15]);
53 }
54
55 /*
56 3D空间中矩阵的平移只是代码中要做的很简单的工作。所要做的全部工作就是使用想要平移矩阵的x、y、z值替换矩阵的最后一行。平移的x分量保存在第12个数组元素中,y分量保存在第13个数组元素中,z分量保存在第14个数组元素中。
57 */
58
59 void CMatrix4x4::Translate(float x, float y, float z)
60 {
61 matrix[12] = x;
62 matrix[13] = y;
63 matrix[14] = z;
64 matrix[15] = 1;
65 }
66
67
68 void CMatrix4x4::Rotate(double angle, int x, int y, int z)
69 {
70 float sine = (float)sin(angle);
71 float cosine = (float)cos(angle);
72
73 if(x)
74 {
75 matrix[5] = cosine;
76 matrix[6] = sine;
77 matrix[9] = -sine;
78 matrix[10] = cosine;
79 }
80 if(y)
81 {
82 matrix[0] = cosine;
83 matrix[2] = -sine;
84 matrix[8] = sine;
85 matrix[10] = cosine;
86 }
87 if(z)
88 {
89 matrix[0] = cosine;
90 matrix[1] = sine;
91 matrix[4] = -sine;
92 matrix[5] = cosine;
93 }
94 }

  Direct3D矩阵

       Direct3D中包含了类似于矢量对象的内置矩阵对象和函数。这些对象类型为D3DXMATRIX,它是针对16个字节排成一行的矩阵,读者可以使用D3DXMATRIX16。然后,可以用不同的D3D函数完成矩阵相关操作,如使用D3DXMatrixIdentity()函数创建单位矩阵,使用D3DXMatrixTranslation()函数实现平移。矩阵平移可以通过D3DXMatrixRotateAxis()函数实现。如果想围绕任意轴旋转,使用D3DXMatrixRotationYawPitchRoll()函数即可。可以根据偏航、倾斜和滚动量进行旋转,使用D3DXMatrixRotationX()、D3DXMatrixRotationY()和D3DXMatrixRotationZ()函数即可实现矢量按照各自的轴旋转。

       Direct3D矩阵像Direct3D矢量一样已经优化过。如果在游戏和引擎中只计划使用Direct3D,但开发人员没有任何矩阵数学背景知识和使用经验,也许可以考虑使用Direct3D矩阵。但如果不介意自己编写矩阵处理代码,而且如果想学习这方面的内容,那么可以考虑编写自己的实现代码。这样做可以了解矩阵的本质,有助于将来的学习。如果经证明Direct3D矩阵的处理速度总是比自己编写的代码的处理速度快,那么最好使用Direct3D矩阵。

  四元组数学
       
旋转是游戏编程中最常用的操作之一。旋转用于改变物体方向,或是饶着场景中某个轴或具体的点转动,并且可以移动骨骼动画中要用到的骨架点。正因为旋转如此重要,尤其是在游戏开发中,所以提高它们的精确度、速度和效率具有很重要的意义。例如,就骨骼动画而言,存储效率和速度对系统也许是最重要的两个要素,这是由系统的复杂性和存储旋转数据所需的内存就决定的。

 曾有一段时间使用矩阵实现旋转,出现过问题。当存储矩阵时,每个4×4矩阵都需要16个浮点数。现在这通常并不是什么问题,因为内存非常便宜而且充裕。但如果是在一个内存有限的设备上,那么可用的字节数就变的异常珍贵。这些设备可以是移动设备,如手机、PDA和手持游戏机等。另一个问题或潜在的问题是,在使用矩阵时速度就会成为问题。在CPU上要完成的操作越多,完成任务所耗费的时间就越多。就精度而言,使用矩阵的主要问题出现在旋转操作上。有一种名为gimble lock的问题将导致矩阵无法准确计算旋转结果。该问题会产生无法预期的视觉结果,这样会导致在旋转物体时出现一堆问题。在按照多个轴将矩阵旋转到某个轴开始照向错误方向的点时,就会发生gimble lock情况。这通常会出现在几个旋转后,而且会导致错误结果。

       四元组是4D数学对象。四元组并不是4D矢量,但能当作4D矢量对待。四元组使用w、x、y、z取代了x、y、z和w。当谈及四元组的表述时,每个成分的顺序并没有什么影响。虽然读者可以对它们的编码处理,但顺序实际上没有什么影响。四元组是复数的扩展,或者在本例中,它们中的三个是复数。这些复数可以使用常用的比例数加上称为虚数部分的数描述。

曾有一段时间使用矩阵实现旋转,出现过问题。当存储矩阵时,每个4×4矩阵都需要16个浮点数。现在这通常并不是什么问题,因为内存非常便宜而且充裕。但如果是在一个内存有限的设备上,那么可用的字节数就变的异常珍贵。这些设备可以是移动设备,如手机、PDA和手持游戏机等。另一个问题或潜在的问题是,在使用矩阵时速度就会成为问题。在CPU上要完成的操作越多,完成任务所耗费的时间就越多。就精度而言,使用矩阵的主要问题出现在旋转操作上。有一种名为gimble lock的问题将导致矩阵无法准确计算旋转结果。该问题会产生无法预期的视觉结果,这样会导致在旋转物体时出现一堆问题。在按照多个轴将矩阵旋转到某个轴开始照向错误方向的点时,就会发生gimble lock情况。这通常会出现在几个旋转后,而且会导致错误结果。

       四元组是4D数学对象。四元组并不是4D矢量,但能当作4D矢量对待。四元组使用w、x、y、z取代了x、y、z和w。当谈及四元组的表述时,每个成分的顺序并没有什么影响。虽然读者可以对它们的编码处理,但顺序实际上没有什么影响。四元组是复数的扩展,或者在本例中,它们中的三个是复数。这些复数可以使用常用的比例数加上称为虚数部分的数描述。

       使用四元组有多个好处。例如,它们的连接速度要比矩阵快,因为它们需要的数学运算较少。四元组要比矩阵小,只需要4个浮点数;而4×4矩阵需要16个浮点数,这使得四元组只有4×4矩阵大小的1/4。对四元组内插同样会更平滑,会得到更准确的旋转,而不会受到gimble lock的影响。

四元组复习
       四元组可以用一个实数和4个虚数表示。四元组的w分量是实部,其他的是虚部。描述四元组的方程看上去如下所示:
q = w + xi + yj + zk

       四元组乘法是不可互换的,所以不能仅将该四元组的每个分量乘上另一个四元组的对应分量。两个四元组相乘用下面的公式计算:
q1 * q2 = (w1*w2 - x1*x2 - y1*y2 - z1*z2)   +
    (w1*x2 + x1*w2 + y1*z2 - z1*y2)i +
    (w1*y2 + x1*z2 + y1*w2 - z1*x2)j +
    (w1*z2 + x1*y2 + y1*x2 - z1*w2)k

       四元组可用一个类表示,这个类的成员变量分别是w、x、y、z。可以对四元组实现多种操作,如乘法、加法、减法、计算四元组量值(长度)、点积、叉积、球形内插和旋转。

 1 class CQuaternion 
2 {
3 public:
4 CQuaternion();
5 CQuaternion(float xAxis, float yAxis,
6 float zAxis, float wAxis);
7
8 void operator=(const CQuaternion &q);
9 CQuaternion operator*(const CQuaternion &q);
10 CQuaternion operator+(const CQuaternion &q);
11
12 void CreateQuatFromAxis(CVector3 &a, float radians);
13
14 float Length();
15 void Normal();
16
17 CQuaternion Conjugate();
18 CQuaternion CrossProduct(const CQuaternion &q);
19
20 void Rotatef(float amount, float xAxis,
21 float yAxis, float zAxis);
22 void RotationRadiansf(double X, double Y, double Z);
23
24 void CreateMatrix(float *pMatrix);
25
26 void Slerp(const CQuaternion &q1, const CQuaternion &q2, float t);
27
28 float w, x, y, z;
29 };

  使用一个名为D3DXQUATERNION的结构就可以创建Direct3D四元组结构。Direct3D同样还包含了一系列用于操纵四元组的函数,如D3DXQuaternionDot()函数将返回两个四元组的点积,D3DXQuaternionLength()函数计算得到四元组的长度,而D3DXQuaternionMultiply()函数则将两个四元组相乘。查看DirectX SDK文档可以得到完整的函数列表文档。Direct3D的四元组结构如程序清单8.18所示。

typedef struct D3DXQUATERNION {

    FLOAT x;

    FLOAT y;

    FLOAT z;

    FLOAT w;

} D3DXQUATERNION;

    射线是由两个矢量组成的简单结构。一个矢量代表原点,而另一个矢量代表射线朝向的方向。射线方向是单位长度法线,它代表射线照向无限远的方向。这意味着如果射线朝向一个物体,那么无论它们两个相距多远,都会和物体交汇。指定射线最小长度的方法有很多,但从实用角度而言,射线应该是无限长的。
struct stRay
{
    Vector3D m_origin;       // 射线原点
    Vector3D m_direction;    // 射线照射的方向
}
   射线主要用于碰撞检测。在第9章介绍碰撞检测这个主题时会讨论射线的使用方法。目前要注意的是,所有的射线都有长度和单位长度归一化后的方向。

    

你可能感兴趣的:(游戏)