Attention: Here be dragons

This is the latest (unstable) version of this documentation, which may document features not available in or compatible with released stable versions of Godot.

贝塞尔、曲线和路径

贝塞尔曲线是一种自然几何形状的数学近似. 我们用它们来代表一个曲线, 含有尽可能少的信息, 保持高水平的灵活性.

不像抽象的数学概念, 贝塞尔曲线是为工业设计. 它们是图形软件行业中的流行工具.

它们依赖于 插值, 我们在上一篇文章中看到, 如何结合多个步骤来创建平滑的曲线. 为了更好地理解贝塞尔曲线的工作原理, 我们从最简单的形式开始: 二次贝塞尔曲线.

二次贝塞尔曲线

取三个点, 这是建立二次贝塞尔曲线所需的最小值:

../../_images/bezier_quadratic_points.png

要在它们之间画一条曲线,我们首先使用 0 到 1 之间的值,在由这三个点构成的两个线段的每个顶点上逐步插值。当我们把 t 值从 0 变成 1 时,就得到了两个沿着线段移动的点。

func _quadratic_bezier(p0: Vector2, p1: Vector2, p2: Vector2, t: float):
    var q0 = p0.lerp(p1, t)
    var q1 = p1.lerp(p2, t)

然后,我们插值 q0q1,以获得沿着曲线移动的单点 r

var r = q0.lerp(q1, t)
return r

这种类型的曲线就被称为二次贝塞尔曲线。

../../_images/bezier_quadratic_points2.gif

(图像来源: 维基百科)

三次贝塞尔曲线

基于前面的例子, 我们可以通过在四个点之间插值得到更多的控制.

../../_images/bezier_cubic_points.png

首先我们使用一个带有四个参数的函数,以 p0p1p2p3 四个点作为输入:

func _cubic_bezier(p0: Vector2, p1: Vector2, p2: Vector2, p3: Vector2, t: float):

我们对每两个点进行线性插值, 将它们减少到三个:

var q0 = p0.lerp(p1, t)
var q1 = p1.lerp(p2, t)
var q2 = p2.lerp(p3, t)

然后我们把这三个点缩减为两个点:

var r0 = q0.lerp(q1, t)
var r1 = q1.lerp(q2, t)

然后到一个:

var s = r0.lerp(r1, t)
return s

这里给出了完整的函数:

func _cubic_bezier(p0: Vector2, p1: Vector2, p2: Vector2, p3: Vector2, t: float):
    var q0 = p0.lerp(p1, t)
    var q1 = p1.lerp(p2, t)
    var q2 = p2.lerp(p3, t)

    var r0 = q0.lerp(q1, t)
    var r1 = q1.lerp(q2, t)

    var s = r0.lerp(r1, t)
    return s

结果将是在所有四个点之间的平滑曲线插值:

../../_images/bezier_cubic_points.gif

(图像来源: 维基百科)

备注

三次贝塞尔插值在三维中也是一样的,只需使用 Vector3 代替 Vector2

添加控制点

在三次贝塞尔的基础上,我们可以通过改变两个点的工作方式来自由地控制曲线的形状。我们不使用 p0p1p2p3,而是将它们存储为:

  • point0 = p0:是第一个点,即源

  • control0 = p1 - p0:是相对于第一个控制点的向量

  • control1 = p3 - p2:是相对于第二个控制点的向量

  • point1 = p3:是第二个点,即终点

使用这种方式, 有两个点和两个控制点, 它们是各自点的相对向量. 如果你以前用过图形或动画软件, 这可能看起来很熟悉:

../../_images/bezier_cubic_handles.png

这就是图形软件如何向用户呈现贝塞尔曲线, 以及它们在Godot引擎内的工作原理.

Curve2D、Curve3D、Path 以及 Path2D

有两个包含曲线的对象:Curve3DCurve2D(分别用于 3D 和 2D)。

它们可以包含几个点,允许更长的路径。也可以将它们设置为节点:Path3DPath2D(分别用于 3D 和 2D):

../../_images/bezier_path_2d.png

然而它们的使用方法可能不是很直观,下面是对贝塞尔曲线最常见用例的描述。

估值

直接估值也是一种选择,不过在大多数情况下都不是很有用。贝塞尔曲线最大的缺点是,如果你以恒定的速度沿着它走,从 t = 0t = 1,实际的插值不会以恒定的速度移动。速度也是根据点 p0p1p2p3 之间距离插值出来的,无法使用简单的数学方法以恒定的速度通过曲线。

让我们用下面的伪代码举个例子:

var t = 0.0

func _process(delta):
    t += delta
    position = _cubic_bezier(p0, p1, p2, p3, t)
../../_images/bezier_interpolation_speed.gif

如你所见,即便 t 在匀速递增,圆点的速度还是在不断变化的(以像素每秒为单位)。这也使贝塞尔难以做到任何实际的开箱即用。

绘制

绘制贝塞尔(或基于曲线的对象)是很常见的用例,但这也不容易。几乎在任何情况下,贝塞尔曲线需要被转换成某种线段。然而,这通常很困难,除非创建大量线段。

原因是曲线的某些部分(具体来说是拐角)可能需要大量的点,而其他部分可能不需要:

../../_images/bezier_point_amount.png

另外,如果两个控制点都是 0,0(请记住它们是相对向量),贝塞尔曲线就是一条直线(因此绘制大量的点会很浪费)。

在绘制贝塞尔曲线之前,需要进行细分。这通常使用递归或分治函数来完成,该函数将曲线分割,直到曲率量小于某个阈值。

Curve 类通过 Curve2D.tessellate() 函数(接收可选的递归 stages 和角度 tolerance 参数)提供该功能。这样,基于曲线绘制某些东西就更容易了。

遍历

曲线的最后一个常见用途是遍历它们。由于之前提到的恒定速度,这也很困难。

为了使这更容易,需要将曲线烘焙成等距点。这样,它们可以用常规插值来近似(可以使用立方选项进一步优化)。为此,只需将 Curve3D.sample_baked() 方法与 Curve2D.get_baked_length() 一起使用。对其中任何一个的第一次调用都会在内部烘焙曲线。

那么,可以使用以下伪代码来完成恒定速度的遍历:

var t = 0.0

func _process(delta):
    t += delta
    position = curve.sample_baked(t * curve.get_baked_length(), true)

然后输出将以恒定速度移动:

../../_images/bezier_interpolation_baked.gif