贝塞尔, 曲线和路径

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

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

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

二次贝塞尔曲线

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

../../_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)

然后, 我们插值 "q0" 和 "q1", 以获得沿着曲线移动的单点 "r".

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

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

../../_images/bezier_quadratic_points2.gif

(图像来源: 维基百科)

三次贝塞尔曲线

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

../../_images/bezier_cubic_points.png

首先我们使用一个带有四个参数的函数, 以四个点作为输入,"p0","p1","p2" 和 "p3":

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

(图像来源: 维基百科)

注解

三次贝塞尔插值在三维中也是一样的, 只需使用 "三维向量" 而不是 "二维向量".

添加控制点

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

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

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

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

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

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

../../_images/bezier_cubic_handles.png

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

二维曲线, 三维曲线, 路径和二维路径

有两个对象包含曲线 Curve3DCurve2D (分别代表3D和2D).

它们可以包含几个点, 允许更长的路径. 也可以将它们设置为节点: 路径二维路径 (在三维和二维内都适用):

../../_images/bezier_path_2d.png

然而使用它们, 可能不是很明显, 下面是对贝塞尔曲线最常见用例的描述.

评估

评估它们可能是一种选择, 但在大多数情况下, 它不是很有用. 贝塞尔曲线最大的缺点是如果你以恒定的速度穿过它们, 从"t = 0"到"t = 1", 实际的插值不会以恒定的速度移动. 速度也是点 "p0" , "p1" , "p2" , "p3" 之间距离的插值, 没有一个简单的数学方法以恒定的速度通过曲线.

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

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 参数). 这样一来, 基于曲线画东西就比较容易了.

遍历

最后曲线最常见的用例是遍历. 因为之前提到关于匀速的内容, 这也是困难的.

To make this easier, the curves need to be baked into equidistant points. This way, they can be approximated with regular interpolation (which can be improved further with a cubic option). To do this, just use the Curve.interpolate_baked() method together with Curve2D.get_baked_length(). The first call to either of them will bake the curve internally.

匀速遍历, 然后, 可以用下面的伪代码:

var t = 0.0

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

并且输出, 然后匀速移动:

../../_images/bezier_interpolation_baked.gif