十五、附录 D:C++ 数学

如果没有对数学,尤其是几何的基本理解,就无法开发电子游戏。

本附录涵盖了本书网站http://www.apress.com/9781430258308上的示例所提供的基本数学课程。

向量

矢量有两个用途:表示位移和方向。

游戏中的向量有三种不同的形式:二维(2D)、3D 和 4D 同质向量。这本书只利用了 3D 和 4D 矢量。

清单 D-1 显示了 3D Vector3类的类声明。

清单 D-1。Vector3阶级宣言

class Vector3
{
public:
       float m_x;
       float m_y;
       float m_z;

       Vector3();
       Vector3(const float x, const float y, const float z);
       virtual Vector3();

       void       Set(const Vector3& in);
       void       Multiply(const float scalar);
       void       Divide(const float scalar);
       void       Add(const Vector3& in);
       void       Subtract(const Vector3& in);
       void       Negate();
       float       Length() const;
       float       LengthSquared() const;
       void       Normalize();
       void       GetNormal(Vector3& normal);

       Vector3& operator=(const Vector3& in);
       Vector3& operator=(const Vector4& in);

       float       Dot(const Vector3& in) const;
       Vector3     Cross(const Vector3& in) const;
};

存储在Vector3类中的数据由三个浮点数表示,分别对应三个基本轴:x、y 和 z。

然后,我们有了一组可以用来处理Vector3类的方法。清单 D-2 包含了Vector3的构造函数和析构函数。

清单 D-2。 Vector3构造函数和析构函数

Vector3::Vector3()
       :       m_x(0.0f)
       ,       m_y(0.0f)
       ,       m_z(0.0f)
{
}

Vector3::Vector3(const float x, const float y, const float z)
       :       m_x(x)
       ,       m_y(y)
       ,       m_z(z)
{
}

Vector3::Vector3()
{
}

我们还有一个方法,Set ( 列出 D-3 ),它覆盖了 vector 中的值。

清单 D-3。 Vector3::Set

void Vector3::Set(const Vector3& in) {
       m_x = in.m_x;
       m_y = in.m_y;
       m_z = in.m_z;
}

向量可以被浮点数乘除。这具有延长或缩短向量的效果。例如,将一个向量乘以二,向量的长度就会加倍。清单 D-4 展示了这些方法。

清单 D-4。 Vector3 MultiplyDivide

void Vector3::Multiply(const float scalar) {
       m_x *= scalar;
       m_y *= scalar;
       m_z *= scalar;
}

void Vector3::Divide(const float scalar) {
       float divisor = 1.0f / scalar;
       m_x *= divisor;
       m_y *= divisor;
       m_z *= divisor;
}

向量也可以与其他向量相加或相减。清单 D-5 显示了AddSubtract方法。

清单 D-5。 Vector3 AddSubtract

void Vector3::Add(const Vector3& in) {
       m_x += in.m_x;
       m_y += in.m_y;
       m_z += in.m_z;
}

void Vector3::Subtract(const Vector3& in) {
       m_x -= in.m_x;
       m_y -= in.m_y;
       m_z -= in.m_z;
}

我们对向量进行的另一个常见操作是求它们的反。清单 D-6 显示了实现这一点的方法。

清单 D-6。 矢量 3:: Negate

void Vector3::Negate()
{
       m_x = -m_x;
       m_y = -m_y;
       m_z = -m_z;
}

我们使用毕达哥拉斯定理来计算向量的长度。计算三角形斜边长度的函数如下:

斜边长度= √( x2+y2+z2)

毕达哥拉斯定理的标准实现包括平方根。计算平方根可能是一个昂贵的操作;因此,我们还实现了一个计算向量平方长度的方法。我们可以通过比较两个向量的平方长度来确定一个向量比另一个向量长还是短。清单 D-7 显示了LengthLengthSquared方法。

清单 D-7。 矢量 3 LengthLengthSquared

float Vector3::Length() const
{
       return sqrt((m_x*m_x) + (m_y*m_y) + (m_z*m_z));
}

float Vector3::LengthSquared() const
{
       return (m_x*m_x) + (m_y*m_y) + (m_z*m_z);
}

我们在游戏编程中使用的向量的一个常见变体是单位法向量。单位法线是一个向量,它代表一个方向,长度为 1。这很有用,因为我们可以利用法线在其他情况下有单位长度的事实。清单 D-8 显示了从Vector3对象获取法向量所必需的代码。

清单 D-8。 矢量 3 NormalizeGetNormal

void Vector3::Normalize()
{
       Divide(Length());
}

void Vector3::GetNormal(Vector3& normal)
{
       normal = *this;
       normal.Normalize();
}

点(或标量)积是对两个向量执行的运算。点积最常见的用途之一是计算这些向量之间的角度。点积的细节将在第 9 章介绍。清单 D-9 描述了Vector3::Dot方法。

清单 D-9。 Vector3:【圆点】

float Vector3::Dot(const Vector3& in) const
{
       return (m_x * in.m_x) + (m_y * in.m_y) + (m_z * in.m_z);
}

叉积用于计算垂直于两个输入向量的新向量。这也在第 9 章中有更详细的介绍;然而,代码显示在清单 D-10 中。

清单 D-10。 Vector3::十字

Vector3 Vector3::Cross(const Vector3& in) const
{
       return Vector3(
               (m_y * in.m_z) - (m_z * in.m_y),
               (m_z * in.m_x) - (m_x * in.m_z),
               (m_x * in.m_y) - (m_y * in.m_x));
}

4D 向量覆盖了与 3D 向量基本相同的操作,只增加了一点点,w 分量。该组件用于确定位移矢量和法向矢量之间的差异。当 4D 向量的 w 分量被设置为 1 时,我们表示该向量是位置向量,当它为 0 时,我们表示它是方向向量。

当我们将向量乘以 4×4 变换矩阵时,这种变化的意义就显现出来了。当 w 分量设置为 0 时,向量将不会被 4×4 矩阵的位置元素平移。

矩阵

3D 游戏编程中使用矩阵来表示变换信息。矩阵可以包含适合于增大或减小对象尺寸、旋转对象以及在 3D 空间中平移对象的信息。清单 D-11 包含了Matrix4类的类声明。

清单 D-11。Matrix4阶级宣言

class Matrix4
{
public:
       enum Rows
       {
              X,
              Y,
              Z,
              W,
              NUM_ROWS
       };

       float m_m[16];

       Matrix4();
       virtual Matrix4();

       void Identify();
       Vector3 Transform(const Vector3& in) const;
       Vector3 TransformTranspose(const Vector3& in) const;
       Vector4 Multiply(const Vector4& in) const;
       void RotateAroundX(float radians);
       void RotateAroundY(float radians);
       void RotateAroundZ(float radians);
       void Multiply(const Matrix4& in, Matrix4& out) const;

       Matrix4 Transpose() const;

       Matrix4& operator=(const Matrix3& in);
       Matrix4& operator=(const Matrix4& in);

       Vector4 GetRow(const Rows row) const;
};

清单显示,我们的矩阵包含 16 个浮点值,可以表示为一组 4 个向量,每个向量包含 4 个元素,这给出了我们的 4×4 矩阵。我们已经定义了一个枚举Rows,来表示矩阵的行。

单位矩阵

一种特殊类型的矩阵是对角矩阵。这是一个只设置对角线值的矩阵。单位矩阵中的每个对角线值都是 1。单位矩阵表示当另一个矩阵或向量相乘时保持不变的矩阵。清单 D-12 展示了方法Identify,我们用它来设置一个矩阵为单位矩阵。

清单 D-12。 Matrix4::识别

void Matrix4::Identify()
{
       m_m[0] = 1.0f;
       m_m[1] = 0.0f;
       m_m[2] = 0.0f;
       m_m[3] = 0.0f;
       m_m[4] = 0.0f;
       m_m[5] = 1.0f;
       m_m[6] = 0.0f;
       m_m[7] = 0.0f;
       m_m[8] = 0.0f;
       m_m[9] = 0.0f;
       m_m[10] = 1.0f;
       m_m[11] = 0.0f;
       m_m[12] = 0.0f;
       m_m[13] = 0.0f;
       m_m[14] = 0.0f;
       m_m[15] = 1.0f;
}

旋转矩阵

可以创建围绕每个主轴旋转的旋转矩阵。清单 D-13 显示了创建绕 x、y 和 z 轴旋转的矩阵所需的代码。

清单 D-13。 Matrix4旋转矩阵创建方法

void Matrix4::RotateAroundX(float radians)
{
       m_m[0] = 1.0f; m_m[1] = 0.0f; m_m[2] = 0.0f;
       m_m[4] = 0.0f; m_m[5] = cos(radians); m_m[6] = sin(radians);
       m_m[8] = 0.0f; m_m[9] = -sin(radians); m_m[10] = cos(radians);
}

void Matrix4::RotateAroundY(float radians)
{
       m_m[0] = cos(radians); m_m[1] = 0.0f; m_m[2] = -sin(radians);
       m_m[4] = 0.0f; m_m[5] = 1.0f; m_m[6] = 0.0f;
       m_m[8] = sin(radians); m_m[9] = 0.0f; m_m[10] = cos(radians);
}

void Matrix4::RotateAroundZ(float radians)
{
       m_m[0] = cos(radians); m_m[1] = sin(radians); m_m[2] = 0.0f;
       m_m[4] = -sin(radians); m_m[5] = cos(radians); m_m[6] = 0.0f;
       m_m[8] = 0.0f; m_m[9] = 0.0f; m_m[10] = 1.0f;
}

乘法矩阵

矩阵运算可以合并;这个过程被称为串联,是通过矩阵相乘来实现的。值得注意的是,连接矩阵时的操作顺序很重要。旋转对象然后平移它,与平移对象然后旋转相比,会产生不同的结果。Matrix4::Multiply清单 D-14 中有描述。

清单 D-14。 Matrix4::Multiply

void Matrix4::Multiply(const Matrix4& in, Matrix4& out) const
{
       assert(this != &in && this != &out && &in != &out);
       out.m_m[0]
              = (m_m[0] * in.m_m[0]) +
                (m_m[1] * in.m_m[4]) +
                (m_m[2] * in.m_m[8]) +
                (m_m[3] * in.m_m[12]);

       out.m_m[1]
              = (m_m[0] * in.m_m[1]) +
                (m_m[1] * in.m_m[5]) +
                (m_m[2] * in.m_m[9]) +
                (m_m[3] * in.m_m[13]);

       out.m_m[2]
              = (m_m[0] * in.m_m[2]) +
                (m_m[1] * in.m_m[6]) +
                (m_m[2] * in.m_m[10]) +
                (m_m[3] * in.m_m[14]);

       out.m_m[3]
              = (m_m[0] * in.m_m[3]) +
                (m_m[1] * in.m_m[7]) +
                (m_m[2] * in.m_m[11]) +
                (m_m[3] * in.m_m[15]);

       out.m_m[4]
              = (m_m[4] * in.m_m[0]) +
                (m_m[5] * in.m_m[4]) +
                (m_m[6] * in.m_m[8]) +
                (m_m[7] * in.m_m[12]);

       out.m_m[5]
              = (m_m[4] * in.m_m[1]) +
                (m_m[5] * in.m_m[5]) +
                (m_m[6] * in.m_m[9]) +
                (m_m[7] * in.m_m[13]);

       out.m_m[6]
              = (m_m[4] * in.m_m[2]) +
                (m_m[5] * in.m_m[6]) +
                (m_m[6] * in.m_m[10]) +
                (m_m[7] * in.m_m[14]);

       out.m_m[7]
              = (m_m[4] * in.m_m[3]) +
                (m_m[5] * in.m_m[7]) +
                (m_m[6] * in.m_m[11]) +
                (m_m[7] * in.m_m[15]);

       out.m_m[8]
              = (m_m[8] * in.m_m[0]) +
                (m_m[9] * in.m_m[4]) +
                (m_m[10] * in.m_m[8]) +
                (m_m[11] * in.m_m[12]);

       out.m_m[9]
              = (m_m[8] * in.m_m[1]) +
                (m_m[9] * in.m_m[5]) +
                (m_m[10] * in.m_m[9]) +
                (m_m[11] * in.m_m[13]);

       out.m_m[10]
              = (m_m[8] * in.m_m[2]) +
                (m_m[9] * in.m_m[6]) +
                (m_m[10] * in.m_m[10]) +
                (m_m[11] * in.m_m[14]);

       out.m_m[11]
              = (m_m[8] * in.m_m[3]) +
                (m_m[9] * in.m_m[7]) +
                (m_m[10] * in.m_m[11]) +
                (m_m[11] * in.m_m[15]);

       out.m_m[12]
              = (m_m[12] * in.m_m[0]) +
                (m_m[13] * in.m_m[4]) +
                (m_m[14] * in.m_m[8]) +
                (m_m[15] * in.m_m[12]);

       out.m_m[13]
              = (m_m[12] * in.m_m[1]) +
                (m_m[13] * in.m_m[5]) +
                (m_m[14] * in.m_m[9]) +
                (m_m[15] * in.m_m[13]);

       out.m_m[14]
              = (m_m[12] * in.m_m[2]) +
                (m_m[13] * in.m_m[6]) +
                (m_m[14] * in.m_m[10]) +
                (m_m[15] * in.m_m[14]);

       out.m_m[15]
              = (m_m[12] * in.m_m[3]) +
                (m_m[13] * in.m_m[7]) +
                (m_m[14] * in.m_m[11]) +
                (m_m[15] * in.m_m[15]);
}

矩阵乘法是一个开销很大的过程,因为成员矩阵的每一行和输入矩阵的每一列在每个元素处相交,它们被用作向量,并且在每个位置计算点积。

矩阵转置

图形编程中的另一个常见操作是计算矩阵的转置。这是通过切换矩阵的行和列来实现的,方法如清单 D-15 中的所示。

清单 D-15。 Matrix4:: Transpose

Matrix4 Matrix4::Transpose() const
{
       Matrix4 out;
       out.m_m[0]       = m_m[0];
       out.m_m[1]       = m_m[4];
       out.m_m[2]       = m_m[8];
       out.m_m[3]       = m_m[12];
       out.m_m[4]       = m_m[1];
       out.m_m[5]       = m_m[5];
       out.m_m[6]       = m_m[9];
       out.m_m[7]       = m_m[13];
       out.m_m[8]       = m_m[2];
       out.m_m[9]       = m_m[6];
       out.m_m[10]      = m_m[10];
       out.m_m[11]      = m_m[14];
       out.m_m[12]      = m_m[3];
       out.m_m[13]      = m_m[7];
       out.m_m[14]      = m_m[11];
       out.m_m[15]      = m_m[15];
}

矩阵的转置被证明在图形编程中是有用的,因为正交旋转矩阵的转置也是它的逆。正交矩阵在第 6 章中讨论。

变换向量

组成Matrix4类的最后一个方法是TransformTransformTranspose方法。这些方法用于将向量乘以矩阵。清单 D-16 包含了实现这一点的代码。

清单 D-16。 Matrix4 TransformTransformTranspose

Vector3 Matrix4::Transform(const Vector3& in) const
{
       return Vector3((m_m[0] * in.m_x) + (m_m[1] * in.m_y) + (m_m[2] * in.m_z),
               (m_m[4] * in.m_x) + (m_m[5] * in.m_y) + (m_m[6] * in.m_z),
               (m_m[6] * in.m_x) + (m_m[7] * in.m_y) + (m_m[8] * in.m_z));
}

Vector3 Matrix4::TransformTranspose(const Vector3& in) const
{
       return Vector3((m_m[0] * in.m_x) + (m_m[3] * in.m_y) + (m_m[6] * in.m_z),
               (m_m[1] * in.m_x) + (m_m[4] * in.m_y) + (m_m[7] * in.m_z),
               (m_m[2] * in.m_x) + (m_m[5] * in.m_y) + (m_m[8] * in.m_z));
}

飞机

平面用来分隔空间。它们是平的,无限延伸。平面对于构建形状以确定对象位于空间内部还是外部非常有用。在本书中,平面被用来实现第八章中的视图截锥剔除。清单 D-17 展示了Plane类的类声明。

清单 D-17。Plane阶级宣言

class Plane
{
private:
       Vector3      m_normal;
       float        m_d;

public:
       Plane();
       Plane(const Vector3& point, const Vector3& normal);
       Plane();

       void BuildPlane(const Vector3& point, const Vector3& normal);

       bool IsInFront(const Vector4& point) const;
       bool IsInFront(const Vector3& point) const;
};

我们的Plane课很基础。我们只需要建立一个平面并检验一个点是否在平面前面的方法。如果一个点不在平面前面,我们就知道它是否在平面后面。接受Vector3参数的构造函数简单地调用了BuildPlane,所以我们来看看清单 D-18BuildPlane的代码。

清单 D-18。 位面::BuildPlane

void Plane::BuildPlane(const Vector3& point, const Vector3& normal)
{
       m_normal = normal;
       m_normal.Normalize();
       m_d      = m_normal.Dot(point);
}

BuildPlane使用三角学计算平面常数 d。第 9 章详细介绍了点积。我们知道点积的结果是两个向量的长度乘以向量间夹角的余弦。在BuildPlane中,我们对法向量进行归一化,以确保该向量的长度为 1。这意味着我们点积的结果是点向量的长度乘以两者夹角的余弦。这就给出了法线和点矢量之间的直角三角形的相邻边的长度。这个长度是平面从原点沿着法向量方向的距离。

我们现在可以用这个来确定其他点是在平面的前面还是后面。我们在IsInFront方法中这样做,我们在清单 D-19 中展示了这个方法。

清单 D-19。 平面::IsInFront

bool Plane::IsInFront(const Vector4& point) const
{
       return IsInFront(Vector3(point.m_x, point.m_y, point.m_z));
}

bool Plane::IsInFront(const Vector3& point) const
{
       return m_normal.Dot(point) >= m_d;
}

IsInFrontVector4版本从Vector4m_xm_ym_z字段构造一个Vector3,并调用Vector3版本。

IsInFrontVector3版本简单地计算点积或法线和所提供的点,并确定它是否大于平面常数。如果它更大,那么我们知道这个点在平面的前面。