贝塞尔,曲线和路径

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

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

它们依赖于:ref:插值<doc_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)

然后,我们插值“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)

And to one:

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

Here is the full function:

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

(图像来源:维基百科)

注解

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

Adding control points

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

  • “point0 = p0”:是第一个点,即源
  • “control0 = p1 - p0”:是相对于第一个控制点的向量
  • “control1 = p3 - p2”:是相对于第二个控制点的向量
  • “point1 = p3”:是第二个点,即终点

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

../../_images/bezier_cubic_handles.png

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

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

There are two objects that contain curves: Curve3D and Curve2D (for 3D and 2D respectively).

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

../../_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”(请记住它们是相对向量),贝塞尔曲线就是一条直线(所以画很多点就是在浪费时间)。

Before drawing Bezier curves, tessellation is required. This is often done with a recursive or divide and conquer function that splits the curve until the curvature amount becomes less than a certain threshold.

*曲线*类通过:ref:`二维曲线.细分曲面() <class_Curve2D_method_tessellate>`提供了这个特性。(其接收可选的递归“阶段”和角度“公差”参数)。这样,根据曲线画东西就容易多了。

遍历

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

为了更简单,这些曲线需要被“烘焙”成等距点。这样,它们就可以用正则插值逼近(其可以通过立方选项进一步改进)。要做到这一点,只需使用:ref:曲线.插值_烘培()<class_Curve_method_interpolate_baked>`方法结合:ref:`二维曲线.获取_烘焙_长度()<class_Curve2D_method_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