十五、附录 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 Multiply
和Divide
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 显示了Add
和Subtract
方法。
清单 D-5。 Vector3 Add
和Subtract
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 显示了Length
和LengthSquared
方法。
清单 D-7。 矢量 3 Length
和LengthSquared
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 Normalize
和GetNormal
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
类的最后一个方法是Transform
和TransformTranspose
方法。这些方法用于将向量乘以矩阵。清单 D-16 包含了实现这一点的代码。
清单 D-16。 Matrix4 Transform
和TransformTranspose
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-18 中BuildPlane
的代码。
清单 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;
}
IsInFront
的Vector4
版本从Vector4
的m_x
、m_y
和m_z
字段构造一个Vector3
,并调用Vector3
版本。
IsInFront
的Vector3
版本简单地计算点积或法线和所提供的点,并确定它是否大于平面常数。如果它更大,那么我们知道这个点在平面的前面。
版权属于:月萌API www.moonapi.com,转载请注明出处