贝塞尔、曲线和路径

贝塞尔曲线是一种自然几何形状的数学近似. 我们用它们来代表一个曲线, 含有尽可能少的信息, 保持高水平的灵活性. 不像抽象的数学概念, 贝塞尔曲线是为工业设计. 它们是图形软件行业中的流行工具. 插值, 我们在上一篇文章中看到, 如何结合多个步骤来创建平滑的曲线. 为了更好地理解贝塞尔曲线的工作原理, 我们从最简单的形式开始: 二次贝塞尔曲线.

二次贝塞尔曲线

取三个点, 这是建立二次贝塞尔曲线所需的最小值: 为了在它们之间画一条曲线, 我们首先在由这三个点构成的两个线段的每个顶点上逐步插值, 使用0到1之间的值. 当我们把 “t” 值从0变成1时, 就给出两个沿着线段移动的点. GDScript C#

  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)
  1. private Vector2 QuadraticBezier(Vector2 p0, Vector2 p1, Vector2 p2, float t){ Vector2 q0 = p0.LinearInterpolate(p1, t); Vector2 q1 = p1.LinearInterpolate(p2, t);}

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

  1. var r = q0.linear_interpolate(q1, t)return r
  1. Vector2 r = q0.LinearInterpolate(q1, t);return r;

二次贝塞尔 曲线. (图像来源: 维基百科)

三次贝塞尔曲线

基于前面的例子, 我们可以通过在四个点之间插值得到更多的控制. 首先我们使用一个带有四个参数的函数, 以四个点作为输入,”p0”,”p1”,”p2” 和 “p3”: GDScript C#

  1. func _cubic_bezier(p0: Vector2, p1: Vector2, p2: Vector2, p3: Vector2, t: float):
  1. public Vector2 CubicBezier(Vector2 p0, Vector2 p1, Vector2 p2, Vector2 p3, float t){}

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

  1. var q0 = p0.linear_interpolate(p1, t)var q1 = p1.linear_interpolate(p2, t)var q2 = p2.linear_interpolate(p3, t)
  1. Vector2 q0 = p0.LinearInterpolate(p1, t);Vector2 q1 = p1.LinearInterpolate(p2, t);Vector2 q2 = p2.LinearInterpolate(p3, t);

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

  1. var r0 = q0.linear_interpolate(q1, t)var r1 = q1.linear_interpolate(q2, t)
  1. Vector2 r0 = q0.LinearInterpolate(q1, t);Vector2 r1 = q1.LinearInterpolate(q2, t);

然后到一个: GDScript C#

  1. var s = r0.linear_interpolate(r1, t)return s
  1. Vector2 s = r0.LinearInterpolate(r1, t);return s;

这里给出了完整的函数: GDScript C#

  1. 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
  1. private Vector2 CubicBezier(Vector2 p0, Vector2 p1, Vector2 p2, Vector2 p3, float t){ Vector2 q0 = p0.LinearInterpolate(p1, t); Vector2 q1 = p1.LinearInterpolate(p2, t); Vector2 q2 = p2.LinearInterpolate(p3, t); Vector2 r0 = q0.LinearInterpolate(q1, t); Vector2 r1 = q1.LinearInterpolate(q2, t); Vector2 s = r0.LinearInterpolate(r1, t); return s;}

结果将是在所有四个点之间的平滑曲线插值: (图像来源: 维基百科) 注解 三次贝塞尔插值在三维中也是一样的, 只需使用 “三维向量” 而不是 “二维向量”.

添加控制点

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

  • “point0 = p0”: 是第一个点, 即源
  • “control0 = p1 - p0”: 是相对于第一个控制点的向量
  • “control1 = p3 - p2”: 是相对于第二个控制点的向量
  • “point1 = p3”: 是第二个点, 即终点 使用这种方式, 有两个点和两个控制点, 它们是各自点的相对向量. 如果你以前用过图形或动画软件, 这可能看起来很熟悉: 这就是图形软件如何向用户呈现贝塞尔曲线, 以及它们在Godot引擎内的工作原理.

    Curve2D、Curve3D、Path 以及 Path2D

    Curve3D 和 Curve2D(分别代表 3D 和 2D)。 Path 和 Path2D(在 3D 和 2D 内都适用): 然而它们的使用方法可能不是很直观,下面是对贝塞尔曲线最常见用例的描述。

    估值

    t = 0t = 1,实际的插值不会以恒定的速度移动。速度也是根据点 p0p1p2p3 之间距离插值出来的,无法使用简单的数学方法以恒定的速度通过曲线。 让我们用下面的伪代码举个简单的例子: GDScript C#
    1. var t = 0.0func _process(delta): t += delta position = _cubic_bezier(p0, p1, p2, p3, t)
    1. private float _t = 0.0f;public override void _Process(float delta){ _t += delta; Position = CubicBezier(p0, p1, p2, p3, _t);}
    t 在匀速递增,圆点的速度还是在不断变化的(以像素每秒为单位)。这也使贝塞尔难以做到任何实际的开箱即用。

    绘制

    绘制贝塞尔(或基于曲线的对象)是很常见的用例, 但这也不容易. 几乎在任何情况下, 贝塞尔曲线需要被转换成某种线段. 这通常很难, 然而, 并没有创建非常高数量的线段. 原因是曲线的某些部分(具体来说是角落)可能需要相当多的点, 而其他部分不一定: 另外, 如果两个控制点都是 “0,0”(请记住它们是相对向量), 贝塞尔曲线就是一条直线(所以画很多点就是在浪费时间). 细分 . 这通常是用递归函数或除法函数来完成的, 它可以分割曲线, 直到曲率变得小于某个阈值. Curve 类通过 Curve2D.tessellate() 函数来提供该功能(函数接收可选的 stages 递归和角度 tolerance 参数). 这样一来, 基于曲线画东西就比较容易了.

    遍历

    最后曲线最常见的用例是遍历. 因为之前提到关于匀速的内容, 这也是困难的. 烘焙 成若干等距的点。这样就可以用常规的插值操作(还可以使用立方选项进一步优化)来进行近似估值了。要实现这样的效果,只需调用 Curve.interpolate_baked() 和 Curve2D.get_baked_length() 方法。首次调用两者之中的任意方法都会触发内部对曲线的烘焙。 匀速遍历, 然后, 可以用下面的伪代码: GDScript C#
    1. var t = 0.0func _process(delta): t += delta position = curve.interpolate_baked(t * curve.get_baked_length(), true)
    1. private float _t = 0.0f;public override void _Process(float delta){ _t += delta; Position = curve.InterpolateBaked(_t * curve.GetBakedLength(), true);}
    并且输出, 然后匀速移动: