英文原文地址:《Vector math》
向量数学(Vector math) 导论本教程是一个针对线性代数(linear algebra)在游戏开发中应用的一个简短的实践性教程。线性代数是一个研究向量及其应用的学科。在2D及3D游戏开发中,向量有着非常多的应用,Godot引擎对它的使用也非常广泛。所以,想成为一个牛逼的开发者,对向量数学的深入理解是必不可少的。
注意:本文并非正式的线性代数教科书。我们将仅关注其在游戏开发中的应用。想更全面地了解数学知识,请参见线性代数在线教程
坐标系(2D)在2D空间中,使用水平轴(x
)以及垂直轴(y
)来定义坐标系。并且使用形如(4,3)
的一个数值对来表示2D空间中的某个位置。
注意:如果你是一个计算机图形学的初学者,那么你可能很疑惑,y
轴的方向竖直向下而不是向上,和你曾经在数学课上学的不一样。然而,这种形式在计算机图形应用里是非常常见的。
在2D平面上的任何一个位置可以用一个数值对表示。然而,我们也可以把这个位置(4,3)
看作是一个从(0,0)
点或原点的偏移量(offset)。画一个从原点到这个点的箭头:
这就是一个向量。向量代表着很多有用的信息。除了告诉我们一个点在(4,3)
位置,我们还可以把它视为角θ
和长度(或幅度)m
。在这种情况下,这个箭头是一个位置向量 - 它表示相对于原点的一个空间位置。
关于向量的一个要点是,它们仅表示相对方向和大小。一个向量的位置是没有意义的。下面的两个向量是等价的:
这两个向量都表示从某个起始点向右偏移4个单位向下偏移3个单位。不管你在平面的哪个位置画这个向量,它总是便是一个相对方向和大小。
向量运算你可以把向量理解成x
和y
坐标也可以把它理解为角度和长度,但方便起见,程序员一般使用坐标记法。例如,Godot中,原点是屏幕的左上角,所以可以用如下代码将一个名为Node2D
的2D节点放在向右400像素向下300像素的位置:
$Node2D.position = Vector2(400, 300)
Godot针对2D和3D应用场景分别支持2维向量(Vector2)和3维向量(Vector3)。本文中讨论的数学规则对这两种类型均有效。
- 成员访问
向量的独立分量可以直接用名字访问。
# create a vector with coordinates (2, 5)
var a = Vector2(2, 5)
# create a vector and assign x and y manually
var b = Vector2()
b.x = 3
b.y = 1
- 向量加法
两个向量相加或相减,即对应分量的相加或相减:
var c = a + b # (2, 5) + (3, 1) = (5, 6)
我们可以通过将第二个向量添加在第一个向量的末端来可视化地表现这个运算:
注意,a+b
的结果和b+a
的结果是一致的。
- 标量乘法(Scalar multiplication)
注意,向量拥有方向和长度,一个仅拥有长度的值称为标量(scalar)。
一个向量可以乘以一个标量:
var c = a * 2 # (2, 5) * 2 = (4, 10)
var d = b / 3 # (3, 6) / 3 = (1, 2)
注意:用一个向量乘以一个标量,仅会改变向量的长度,而不会改变其方向。我们可以以此来缩放一个向量。
实际应用让我们看两个向量加减的常见用法。
- 移动
向量可以表示任意具有长度和方向的量。典型的例子有:位置,速度,加速度和力。下图中,第1步时飞船的位置向量是(1,3)
且速度向量是(2,1)
。速度向量表示飞船每一步能移动多远。我们可以通过将当前位置加上速度获取到第2步时的位置。
提示:速度衡量在一个单位时间内位置的变化量。新的位置通过速度与前一时刻位置相加获得。
- 指向一个目标
在这个情景中,你想让坦克的炮塔指向机器人。只要从机器人的位置减去坦克的位置就可以获得从坦克指向机器人的向量。
提示:想要获得A
到B
的向量,就用B-A
。
长度为1
的向量被称为单位向量(unit vector)。单位向量有时也作为**方向向量(direction vector)或者法线(normal)**被提及。当你想记录一个方向的时候,单位向量是非常有用的。
标准化意思是保持一个向量方向不变将它的长度缩为1.通过对每一个分量都除以其长度可以对其标准化。
var a = Vector2(2, 4)
var m = sqrt(a.x*a.x + a.y*a.y) # get magnitude "m" using the Pythagorean theorem
a.x /= m
a.y /= m
因为这是一个常用运算,所以vector2
和vector3
都提供了标准化的方法:
a = a.normalized()
警告:标准化运算需要除以向量长度,因此你不能对一个长度为0
的向量标准化。如果尝试这样做会产生错误。
单位向量的一个常见用法就是指示法线(normal)。法向量是垂直于一个面的单位向量,用于定义面的方向。它们经常被用于光照,碰撞以及其它与面相关的运算。
例如,试想一下,有一个运动的球,我们希望它能够从一面墙或其它对象弹起:
因为这是一个水平的面,所以它的法向量是(0,-1)
。当球碰撞时,我们取它的剩余运动(即,当它撞击表面时剩余的量),并且使用法线将其反射。在Godot中,Vector2
类中有一个bounce()
函数可以处理这件事。以下是一个基于KinematicBody2D
节点的GDScript的代码例子,以再现上面的图解:
# object "collision" contains information about the collision
var collision = move_and_collide(velocity * delta)
if collision:
var reflect = collision.remainder.bounce(collision.normal)
velocity = velocity.bounce(collision.normal)
move_and_collide(reflect)
点乘(dot product)
点乘是向量数学里最重要的概念之一,但是却经常被误解。点乘是两个向量间的运算,运算结果是一个标量。与即有大小又有方向的向量不同,标量只有大小。
点乘公式有两种常见形式:
和
然而绝大多数情况下,最好还是使用内置函数。注意,两个向量的顺序是无关紧要的:
var c = a.dot(b)
var d = b.dot(a) # 二者等效
在和单位向量一起使用时,点乘最为有用。此时公式一(经过约分)就只剩下一个cosθ
。这意味着,我们可以使用点乘来获取两个向量夹角的信息:
当使用单位向量时,结果永远介于-1
(180°)和 1
(0°)之间。
我们以此为依据来检测一个对象是否朝向另一个对象。如下图所示,玩家P
正要躲避僵尸A
和B
。假设僵尸的视野是180°,它们能看到玩家么?
绿色的箭头fA
和fB
是表示僵尸朝向的单位向量,蓝色的半圆表示它的视野。对于僵尸A
,我们通过P-A
获取它指向玩家的方向向量AP
,并对AP
标准化。如果这个向量AP
和僵尸的面部朝向向量间的夹角小于90°,则僵尸可以看到玩家。使用代码将如下所示:
var AP = (P - A).normalized()
if AP.dot(fA) > 0:
print("A sees P!")
叉乘(cross product)
和点乘一样,叉乘也是两个向量间的运算。不同的是,叉乘的结果是垂直于这两个向量的向量。它的长度取决于这两个向量的相对角度。如果两个向量是平行的,它们叉乘的结果将是一个零向量(null vector)。
叉乘的计算方法如下所示:
var c = Vector3()
c.x = (a.y * b.z) - (a.z * b.y)
c.y = (a.z * b.x) - (a.x * b.z)
c.z = (a.x * b.y) - (a.y * b.x)
在Godot中,我们可以使用内置方法:
var c = a.cross(b)
注意:在叉乘中,向量的顺序是起作用的。a.cross(b)
和b.cross(a)
的结果不同。两个向量会指向相反的方向。
叉乘的一个常见用法是求一个3D空间中的平面或表面的面法线(surface normal)。已知一个三角形ABC
,使用向量相减求得两个边AB
和AC
。再对这两个变相量进行叉乘,AB x AC
求得一个与两个边都垂直的向量即面法线。
以下是求面法线的函数:
func get_triangle_normal(a, b, c):
# find the surface normal given 3 vertices
var side1 = b - a
var side2 = c - a
var normal = side1.cross(side2)
return normal
指向目标
在上面介绍点乘的部分,我们看到如何用点乘来求两个向量间的夹角。然而,在3D中,这是不够的。我们还要知道以哪个轴为轴心旋转。我们可以通过计算当前朝向于目标方向之间的叉乘来计算,计算的结果即为旋转轴向。
更多内容要获取更多关于Godot向量数学的内容,请参阅如下文章:
Advanced vector math
Matrices and transforms