高等向量数学

平面

单位向量的点积还有一个有趣的性质。请想象一个垂直于这个向量(且经过原点)的平面。平面会将整个空间划分为正(在平面上方)和负(在平面下方)两部分,并且(与普遍的看法相反)你也可以在 2D 中进行这样的数学运算:

../../_images/tutovec10.png

垂直于表面的单位向量称为单位法向量(因此描述的是表面的朝向),不过通常会简称为法线。平面、3D 几何体等场合中都会用到法线(用来确定面或顶点的属于哪一侧)。法线是一种单位向量,因为用途才被称为法线。(就像我们说坐标 (0,0) 是“原点”一样!)。

The plane passes by the origin and the surface of it is perpendicular to the unit vector (or normal). The side the vector points to is the positive half-space, while the other side is the negative half-space. In 3D this is exactly the same, except that the plane is an infinite surface (imagine an infinite, flat sheet of paper that you can orient and is pinned to the origin) instead of a line.

到平面的距离

现在平面是什么就很清楚了,让我们再回到点积上。单位向量和任何空间点之间的点积(是的,这次我们在向量和位置之间进行点乘),将返回从该点到平面的距离

var distance = normal.dot(point)

但返回的不止是距离的绝对值,如果点位于负半空间,那么这个距离也是负的:

../../_images/tutovec11.png

这样我们就能够知道点位于平面的哪一侧。

脱离原点

我知道你在想什么!到目前为止还算不错,但真正的平面在空间中无处不在,并不一定要经过原点。你想要的是真正平面,你现在就想行动起来。

请记住,平面不仅仅是将空间一分为二,这两个空间还有极性。也就是说,如果两个平面完全重合,它们的正负半空间可以相反。

明确了这一点,我们就可以将完整的平面描述为法线 N与原点的距离标量 D。这样用 N 和 D 就可以表示我们的平面了。例如:

../../_images/tutovec12.png

对于 3D 空间中的平面,Godot 提供了 Plane 内置类型来处理这些计算。

基本上,N 和 D 可以表示空间中的任何平面,无论是 2D 还是 3D(取决于 N 的维数),两者的数学运算相同。它与之前相同,但 D 是从原点到平面的距离,沿 N 方向行进。例如,假设你想要到达平面上的某个点,只需执行以下操作:

var point_in_plane = N*D

这将拉伸(调整大小)法线向量并使其接触平面。这个数学运算可能看起来很混乱,但实际上比看起来要简单得多。如果我们想再次知道从点到平面的距离,可以以相同方法,但要调整距离:

var distance = N.dot(point) - D

也可以用内置函数执行同样的计算:

var distance = plane.distance_to(point)

这同样会返回一个正或负的距离。

还可以通过同时对 N 和 D 取负来反转平面的极性。这样,平面的位置不变,但正负半空间倒置:

N = -N
D = -D

Godot 还在 Plane 中实现了该运算。因此,使用以下格式将按预期工作:

var inverted_plane = -plane

所以,请记住,平面的主要实际用途是我们可以计算到平面的距离。那么,什么时候计算从点到平面的距离有用呢?让我们看一些例子。

在 2D 中构造平面

平面不会凭空出现,必须先进行构造。在 2D 空间中构造平面很简单:只需要法线(单位向量)和某一个点,或者空间中任意两点都可以完成。

在法线和点的情况下,由于法线已经被计算出来,大部分计算工作都已完成。因此,只需根据法线和点的点积计算 D 即可。

var N = normal
var D = normal.dot(point)

For two points in space, there are actually two planes that pass through them, sharing the same space but with normal pointing to the opposite directions. To compute the normal from the two points, the direction vector must be obtained first, and then it needs to be rotated 90 degrees to either side:

# Calculate vector from `a` to `b`.
var dvec = point_a.direction_to(point_b)
# Rotate 90 degrees.
var normal = Vector2(dvec.y, -dvec.x)
# Alternatively (depending the desired side of the normal):
# var normal = Vector2(-dvec.y, dvec.x)

剩余步骤与前例相同。point_a 和 point_b 都可以用于计算,毕竟两者位于同一个平面内:

var N = normal
var D = normal.dot(point_a)
# this works the same
# var D = normal.dot(point_b)

在 3D 空间中构造平面更加复杂,下文会进一步解释。

平面的一些示例

以下是平面的用途示例。假设有一个多边形。比如矩形、梯形、三角形或任何没有面向内弯曲的多边形。

对于多边形的每段,我们计算经过该段的平面。一旦我们有了平面列表,我们就可以做一些有趣的事情,例如检查某个点是否在多边形内。

我们遍历所有平面,如果我们能找到一个到该点的距离为正的平面,那么该点就在多边形外部。如果我们找不到,那么该点就在多边形内部。

../../_images/tutovec13.png

代码应该是这样的:

var inside = true
for p in planes:
	# check if distance to plane is positive
	if (p.distance_to(point) > 0):
		inside = false
		break # with one that fails, it's enough

很酷吧?但还会更好!再多花点功夫,类似的逻辑也会让我们知道两个凸多边形何时重叠。这被称为分离轴定理(或 SAT),大多数物理引擎都使用它来检测碰撞。

对于一个点,只要检查是否有一个平面返回正距离,就足以判断该点是否在外部。对于另一个多边形,我们必须找到一个平面,使另一个多边形所有到它的距离都返回为正。先使用 A 的平面对 B 的点进行检查,然后使用 B 的平面对 A 的点进行检查:

../../_images/tutovec14.png

代码应该是这样的:

var overlapping = true

for p in planes_of_A:
	var all_out = true
	for v in points_of_B:
		if (p.distance_to(v) < 0):
			all_out = false
			break

	if (all_out):
		# a separating plane was found
		# do not continue testing
		overlapping = false
		break

if (overlapping):
	# only do this check if no separating plane
	# was found in planes of A
	for p in planes_of_B:
		var all_out = true
		for v in points_of_A:
			if (p.distance_to(v) < 0):
				all_out = false
				break

		if (all_out):
			overlapping = false
			break

if (overlapping):
	print("Polygons Collided!")

如你所见,平面非常有用,而这只是冰山一角。你可能想知道非凸多边形会发生什么。通常只需将凹多边形分割成较小的凸多边形,或使用诸如 BSP(现在很少使用)之类的技术即可解决。

3D 碰撞检测

这是另一个奖励,是对耐心坚持看完这篇长篇教程的奖励。这是另一条智慧。这可能不是直接使用案例(Godot 已经很好地完成了碰撞检测),但几乎所有物理引擎和碰撞检测库都在使用它 :)

还记得将 2D 凸形转换为 2D 平面数组对于碰撞检测很有用吗?你可以检测某个点是否位于任何凸形内,或者两个 2D 凸形是否重叠。

嗯,这在 3D 中也适用,如果两个 3D 多面体发生碰撞,你将无法找到分离平面。如果找到了分离平面,则这两个形状肯定没有发生碰撞。

稍微回顾一下,分离平面意味着多边形 A 的所有顶点都在平面的一侧,而多边形 B 的所有顶点都在另一侧。该平面总是多边形 A 或多边形 B 的面平面之一。

不过,在 3D 中,这种方法存在问题,因为在某些情况下可能找不到分离平面。以下是这种情况的一个示例:

../../_images/tutovec22.png

为了避免这种情况,一些额外的平面需要作为分隔器被测试,这些平面是多边形 A 的边和多边形 B 的边的叉积

../../_images/tutovec23.png

所以,最终的算法是这样的:

var overlapping = true

for p in planes_of_A:
	var all_out = true
	for v in points_of_B:
		if (p.distance_to(v) < 0):
			all_out = false
			break

	if (all_out):
		# a separating plane was found
		# do not continue testing
		overlapping = false
		break

if (overlapping):
	# only do this check if no separating plane
	# was found in planes of A
	for p in planes_of_B:
		var all_out = true
		for v in points_of_A:
			if (p.distance_to(v) < 0):
				all_out = false
				break

		if (all_out):
			overlapping = false
			break

if (overlapping):
	for ea in edges_of_A:
		for eb in edges_of_B:
			var n = ea.cross(eb)
			if (n.length() == 0):
				continue

			var max_A = -1e20 # tiny number
			var min_A = 1e20 # huge number

			# we are using the dot product directly
			# so we can map a maximum and minimum range
			# for each polygon, then check if they
			# overlap.

			for v in points_of_A:
				var d = n.dot(v)
				max_A = max(max_A, d)
				min_A = min(min_A, d)

			var max_B = -1e20 # tiny number
			var min_B = 1e20 # huge number

			for v in points_of_B:
				var d = n.dot(v)
				max_B = max(max_B, d)
				min_B = min(min_B, d)

			if (min_A > max_B or min_B > max_A):
				# not overlapping!
				overlapping = false
				break

		if (not overlapping):
			break

if (overlapping):
   print("Polygons collided!")

更多信息

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

如果你需要进一步的解释,你可以看看 3Blue1Brown 的绝佳的系列视频《线性代数的本质》:http://www.bilibili.com/video/BV1ys411472E?p=2