贝塞尔曲线和路径

贝塞尔曲线是自然几何形状的数学近似。我们使用它们来表示一个尽可能少的信息和高度灵活性的曲线。

与更抽象的数学概念不同,贝塞尔曲线是为工业设计而创建的。它们是图形软件行业中的流行工具。

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

二次贝塞尔

取三个点,二次贝塞尔工作所需的最小值:

../../_images/bezier_quadratic_points.png

为了在它们之间绘制一条曲线,我们首先使用从0到1的值,在由三个点组成的两个分段的两个顶点上逐渐插值。当我们改变 t 从0到1。

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

然后我们插入 q0q1 获得单点 r 沿着曲线移动。

var r = q0.linear_interpolate(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.linear_interpolate(p1, t)
var q1 = p1.linear_interpolate(p2, t)
var q2 = p2.linear_interpolate(p3, t)

然后我们取三点,把它们减少到两个:

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

一方面:

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

以下是完整功能:

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

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

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

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

../../_images/bezier_cubic_points.gif

(图片来源:维基百科)

注解

三次贝塞尔插值在3D中的效果相同,只需使用 Vector3 而不是 Vector2 .

添加控制点

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

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

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

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

  • point1 = p3 :是第二个点,目的地

这样,我们有两个点和两个控制点,它们是各自点的相对矢量。如果您以前使用过图形或动画软件,这可能看起来很熟悉:

../../_images/bezier_cubic_handles.png

这就是图形软件如何向用户呈现贝塞尔曲线,以及它们在Godot中的工作和外观。

曲线2d,曲线3d,路径和路径2d

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

它们可以包含多个点,允许更长的路径。也可以将它们设置为节点: PathPath2D (同样适用于3D和2D):

../../_images/bezier_path_2d.png

然而,使用它们可能并不十分明显,因此下面是对贝塞尔曲线最常见用例的描述。

评价

仅仅评估它们可能是一种选择,但在大多数情况下,它不是很有用。贝塞尔曲线的最大缺点是,如果你以恒定的速度穿过它们, t = 0t = 1 ,实际插值将 not 以恒定速度移动。速度也是点之间距离的插值。 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 (记住它们是相对向量),贝塞尔曲线只是一条直线(因此绘制大量的点是浪费的)。

在绘制贝塞尔曲线之前, 曲面细分 是必需的。这通常是通过一个递归或分而治之的函数来完成的,该函数将分割曲线,直到曲率量小于某个阈值。

这个 曲线 类通过 Curve2D.tessellate() 功能(接收可选 stages 递归和角度 tolerance 参数)。这样,基于曲线绘制东西更容易。

遍历

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

为了使这更容易,曲线需要 变成等距离的点。通过这种方法,可以用规则插值(使用立方选项可以进一步改进)来近似它们。为此,只需使用 Curve.interpolate_baked() 方法与 Curve2D.get_baked_length() . 对其中任何一个的第一个调用都将在内部烘焙曲线。

然后,可以使用以下伪代码以恒定速度进行遍历:

var t = 0.0

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

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

../../_images/bezier_interpolation_baked.gif