向量数学

前言

本教程是一个简短而实用的线性代数介绍,因为它适用于游戏开发。线性代数是研究向量及其用途的学科。向量在 2D 和 3D 开发中都有许多应用,Godot 对它们的应用也非常广泛。要成为一名优秀的游戏开发者,对向量数学的理解是必不可少的。

备注

本教程不是线性代数的正式教科书。我们将只关注它如何应用于游戏开发。要更全面地了解数学,请参见 https://www.khanacademy.org/math/linear-algebra

坐标系(2D)

在 2D 空间中,使用水平轴(x)和垂直轴(y)定义坐标。2D 空间中的特定位置会被写成一对值,如 (4, 3)

../../_images/vector_axis1.png

备注

如果您是计算机图形学的新手,可能会觉得很奇怪,y 轴的正方向朝下而不是向上(您在数学课上学到的就像那样)。然而,这在大多数计算机图形应用程序中是常见的。

2D 平面上的任何位置都可以用一对数字来表示。然而,我们也可以将位置 (4, 3) 看作是从 (0, 0) 点或原点出发的偏移。画一个箭头从原点指向点:

../../_images/vector_xy1.png

这是一个向量。向量表示许多有用的信息。除了告诉我们点在 (4, 3) 之外,我们还可以把它看成角度 θ 和长度(或幅度)m。在这种情况下,箭头是一个位置向量——它表示空间中相对于原点的位置。

关于向量, 需要考虑的一个重要点是它们仅表示 相对 方向和大小. 没有一个向量的位置的概念. 以下两个向量相同:

../../_images/vector_xy2.png

这两个向量都表示向右4个单位, 在某个起始点以下3个单位. 不管您在平面上画哪个向量, 它总是代表一个相对的方向和大小.

向量运算

您可以使用任意一种方法,x和y坐标或角度和大小, 来引用向量, 但是为了方便起见, 程序员通常使用坐标表示法. 例如, 在Godot中, 原点是屏幕左上角, 因此要将一个名为 Node2D 的2D节点向右400像素, 向下300像素, 请使用以下代码:

GDScriptC#

  1. $Node2D.position = Vector2(400, 300)
  1. var node2D = GetNode<Node2D>("Node2D");
  2. node2D.Position = new Vector2(400, 300);

Godot支持 Vector2Vector3 分别用于2D和3D. 本文讨论的数学规则同样适用于这两种类型.

成员访问

向量的各个组成部分可以直接通过名称访问.

GDScriptC#

  1. # create a vector with coordinates (2, 5)
  2. var a = Vector2(2, 5)
  3. # create a vector and assign x and y manually
  4. var b = Vector2()
  5. b.x = 3
  6. b.y = 1
  1. // create a vector with coordinates (2, 5)
  2. var a = new Vector2(2, 5);
  3. // create a vector and assign x and y manually
  4. var b = new Vector2();
  5. b.x = 3;
  6. b.y = 1;

向量加法

当两个向量相加或相减时, 相应的分量相加:

GDScriptC#

  1. var c = a + b # (2, 5) + (3, 1) = (5, 6)
  1. var c = a + b; // (2, 5) + (3, 1) = (5, 6)

我们也可以通过在第一个向量的末尾加上第二个向量来直观地看到这一点:

../../_images/vector_add1.png

注意,做 a + b 的加法和 b + a 得到的结果是一样的。

标量乘法

备注

向量表示方向和幅度。仅表示幅值的值称为标量

向量可以乘以标量

GDScriptC#

  1. var c = a * 2 # (2, 5) * 2 = (4, 10)
  2. var d = b / 3 # (3, 6) / 3 = (1, 2)
  1. var c = a * 2; // (2, 5) * 2 = (4, 10)
  2. var d = b / 3; // (3, 6) / 3 = (1, 2)

../../_images/vector_mult1.png

备注

向量乘以标量不会改变它的方向,只会改变它的幅值。这就是缩放向量的方法。

实际应用

让我们看看向量加法和减法的两种常见用法.

移动

向量可以表示具有大小和方向的任何量。典型的例子有:位置、速度、加速度、力。在这幅图像中,在第 1 步的飞船的位置向量为 (1,3),速度向量为 (2,1)。速度向量表示船舶每一步移动的距离。通过将速度加到当前位置,我们可以求出第 2 步的位置。

../../_images/vector_movement1.png

小技巧

速度测量单位时间内位置的变化。新的位置是通过在前一个位置上增加速度来找到的。

指向目标

在这个场景中,您有一辆坦克,坦克希望让炮塔指向机器人。把机器人的位置减去坦克的位置就得到了从坦克指向机器人的向量。

../../_images/vector_subtract2.png

小技巧

要找到从 A 指向 B 的向量,请使用 B - A

单位向量

大小1 的向量称为单位向量,有时也被称为方向向量法线。当您需要记录方向时就可以使用单位向量。

归一化

归一化 一个向量意味着将其长度缩减到 1 , 并保留其方向. 其方法是将每个分量除以其幅度. 由于这是一个很常见的操作, Vector2Vector3 提供了归一化的方法:

GDScriptC#

  1. a = a.normalized()
  1. a = a.Normalized();

警告

因为归一化需要除以向量的长度, 所以不能对长度为 0 的向量进行归一化. 试图这样做会导致错误.

反射

单位向量的一种常见用法是表示法线。法向量是垂直于表面的单位向量,定义了表面的方向。它们通常用于照明、碰撞和涉及表面的其他操作。

例如, 假设我们有一个移动的球, 我们想从墙上或其他物体上弹回来:

../../_images/vector_reflect1.png

因为这是一个水平曲面,所以曲面法线的值为 (0, -1) . 当球碰撞时,我们取它的原运动方向(当它撞到表面时剩余的量), 以法线为角平分线反射它。在Godot中, Vector2 类有一个 bounce() 方法来处理这个问题。这是上图的 GDScript 示例,使用 KinematicBody2D

GDScriptC#

  1. # object "collision" contains information about the collision
  2. var collision = move_and_collide(velocity * delta)
  3. if collision:
  4. var reflect = collision.remainder.bounce(collision.normal)
  5. velocity = velocity.bounce(collision.normal)
  6. move_and_collide(reflect)
  1. // KinematicCollision2D contains information about the collision
  2. KinematicCollision2D collision = MoveAndCollide(_velocity * delta);
  3. if (collision != null)
  4. {
  5. var reflect = collision.Remainder.Bounce(collision.Normal);
  6. _velocity = _velocity.Bounce(collision.Normal);
  7. MoveAndCollide(reflect);
  8. }

点积

点积是向量数学中最重要的概念之一,但经常被误解。点积是对两个向量的操作,返回一个标量。与同时包含大小和方向的向量不同,标量值只有大小。

点积公式有两种常见形式:

../../_images/vector_dot1.png

以及

../../_images/vector_dot2.png

然而, 在大多数情况下, 使用内置方法是最容易的. 注意, 两个向量的顺序并不重要:

GDScriptC#

  1. var c = a.dot(b)
  2. var d = b.dot(a) # These are equivalent.
  1. float c = a.Dot(b);
  2. float d = b.Dot(a); // These are equivalent.

当与单位向量一起使用时,点积是最有用的,使得第一个公式减少到仅有 cosθ。这意味着我们可以使用点积来告诉我们关于两个向量之间的角度的一些信息:

../../_images/vector_dot3.png

当使用单位向量, 结果总是会在 -1 (180°) 和 1 (0°) 之间.

面向问题

我们可以利用这个事实来检测一个物体是否面向另一个物体. 在下图中, 游戏角色 P 试图避开丧尸 AB . 假设一个丧尸的视野是 180° , 他们能看到游戏角色吗?

../../_images/vector_facing2.png

绿色箭头 fAfB单位向量 , 代表僵尸的朝向, 蓝色半圆代表其视野. 对于僵尸 A , 我们用 P - A 找到指向玩家的方向向量 AP , 并进行归一化处理, 不过,Godot有一个辅助方法可以做到这一点, 叫做 direction_to . 如果这个向量和面对的向量之间的角度小于90°, 那么僵尸就可以看到玩家.

在代码中是这样的:

GDScriptC#

  1. var AP = A.direction_to(P)
  2. if AP.dot(fA) > 0:
  3. print("A sees P!")
  1. var AP = A.DirectionTo(P);
  2. if (AP.Dot(fA) > 0)
  3. {
  4. GD.Print("A sees P!");
  5. }

叉积

和点积一样,叉积也是对两个向量的运算。但是,叉乘积的结果是一个方向与两个向量垂直的向量。它的大小取决于相对角度,如果两个向量是平行的,那么叉积的结果将是一个空向量。

../../_images/vector_cross1.png ../../_images/vector_cross2.png

叉积是这样计算的:

GDScriptC#

  1. var c = Vector3()
  2. c.x = (a.y * b.z) - (a.z * b.y)
  3. c.y = (a.z * b.x) - (a.x * b.z)
  4. c.z = (a.x * b.y) - (a.y * b.x)
  1. var c = new Vector3();
  2. c.x = (a.y * b.z) - (a.z * b.y);
  3. c.y = (a.z * b.x) - (a.x * b.z);
  4. c.z = (a.x * b.y) - (a.y * b.x);

使用 Godot,您可以使用内置的方法:

GDScriptC#

  1. var c = a.cross(b)
  1. var c = a.Cross(b);

备注

在叉积中,顺序很重要。a.cross(b)b.cross(a) 的结果不一样,会得到指向相反的向量。

法线计算

叉积的一种常用方法是在 3D 空间中求平面或曲面的表面法向量。如果有三角形 ABC,我们可以用向量减法找到两条边 ABAC。通过叉乘,AB x AC 得到一个垂直于这两个向量的向量:表面法向量。

下面是一个计算三角形法线的函数:

GDScriptC#

  1. func get_triangle_normal(a, b, c):
  2. # find the surface normal given 3 vertices
  3. var side1 = b - a
  4. var side2 = c - a
  5. var normal = side1.cross(side2)
  6. return normal
  1. Vector3 GetTriangleNormal(Vector3 a, Vector3 b, Vector3 c)
  2. {
  3. // find the surface normal given 3 vertices
  4. var side1 = b - a;
  5. var side2 = c - a;
  6. var normal = side1.Cross(side2);
  7. return normal;
  8. }

指向目标

在上面的点积部分,我们看到如何用它来查找两个向量之间的角度。然而在 3D 中,这些信息还不够。我们还需要知道在围绕什么轴旋转。我们可以通过计算当前面对的方向和目标方向的叉积来查找。由此得到的垂直向量就是旋转轴。

更多信息

有关在Godot中使用向量数学的更多信息, 请参阅以下文章: