游戏开发中的贝塞尔曲线,曲线和路径
贝塞尔曲线是自然几何形状的数学近似。我们使用它们来表示一条曲线,该曲线具有尽可能少的信息并具有很高的灵活性。
与更抽象的数学概念不同,贝塞尔曲线是为工业设计而创建的。它们是图形软件行业中流行的工具。
它们依赖于插值(我在上一篇文章中提过),结合了多个步骤以创建平滑曲线。为了更好地了解贝塞尔曲线的工作原理,让我们从其最简单的形式开始:二次贝塞尔曲线。
二次贝塞尔曲线
取三点,这是二次贝塞尔曲线起作用的最低要求:
为了在它们之间绘制一条曲线,我们首先使用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
这种类型的曲线称为二次贝塞尔曲线。
(图片来源:*)
三次贝塞尔曲线
在前面的示例的基础上,我们可以通过在四个点之间进行插值来获得更多控制。
我们首先使用的功能与四个参数取四点作为输入, 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
结果将是在所有四个点之间插值的平滑曲线:
(图片来源:*)
注意
三次贝塞尔曲线插值在3D中的效果相同,只是使用Vector3 代替Vector2。
添加控制点
以立方贝塞尔曲线为基础,我们可以更改两个点的工作方式以*控制曲线的形状。而是具有p0
,p1
,p2
和p3
,我们将它们存储为:
-
point0 = p0
:是第一点,来源 -
control0 = p1 - p0
:相对于第一个控制点的向量 -
control1 = p3 - p2
:是相对于第二个控制点的向量 -
point1 = p3
:是第二点,目的地
这样,我们有两个点和两个控制点,它们是相对于各个点的相对向量。如果您以前使用过图形或动画软件,则可能看起来很熟悉:
这就是图形软件如何向用户显示Bezier曲线,以及它们在Godot中的工作方式和外观。
Curve2D,Curve3D,路径和Path2D
有两个包含曲线的对象:Curve3D和Curve2D(分别用于3D和2D)。
它们可以包含多个点,从而可以使用更长的路径。也可以将它们设置为节点:Path和Path2D(也分别用于3D和2D):
但是,使用它们可能并不十分明显,因此以下是Bezier曲线最常见用例的描述。
评估
仅评估它们可能是一种选择,但是在大多数情况下,它不是很有用。 贝塞尔曲线的最大缺点是,如果以恒定速度从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)
如您所见,即使t以恒定速度增加,圆的速度(以每秒像素为单位)也会变化。这使得贝塞尔曲线难以在开箱即用的情况下使用。
画画
绘制贝塞尔曲线(或基于曲线的对象)是一种非常常见的用例,但这也不容易。在几乎任何情况下,贝塞尔曲线都需要转换为某种线段。但是,这通常很困难,而又不创建大量的代码。
原因是曲线的某些部分(特别是拐角)可能需要大量的点,而其他部分可能不需要:
此外,如果两个控制点都是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)
然后,输出将以恒定速度移动: