【Godot4.2】Godot中的贝塞尔曲线

概述

贝塞尔曲线是一种常见的曲线形式,你可以在很多地方看到它的身影,比如PS的钢笔工具,Godot中的Path2DCurve2D等。

贝塞尔曲线求解本身涉及向量的线性插值,好在Vector2已经提供了lerp()方法。
本篇就介绍二次和三次贝塞尔曲线的基础原理,并写出相应的点求取函数。

二次贝塞尔曲线

在实际上手绘制之前,让我们先来理解一下贝塞尔曲线的求点原理与本质——向量线性插值。
1161912546.jpg
在平面上有三个点ABC

  • AB相连,形成一个向量AB⃗\vec{AB}AB,BC相连,形成另一个向量BC⃗\vec{BC}BC
  • AB⃗\vec{AB}ABBC⃗\vec{BC}BC同步进行0.01.0插值,设插值变量为t
  • 在插值的每一时刻,会从AB⃗\vec{AB}ABBC⃗\vec{BC}BC上各获得一个点DE
    1161914558.jpg
  • 连接DE,对DE⃗\vec{DE}DE进行0.01.0插值,而且插值与此时的t一致。则获得一个点F
  • 也就是说,同步对AB⃗\vec{AB}ABBC⃗\vec{BC}BCDE⃗\vec{DE}DE进行0.01.0插值。

1161916361.jpg
DE⃗\vec{DE}DE插值获取的所有点F连起来就是一条由A到B的贝塞尔曲线。整个插值过程也就是官方文档中的这张动图:

2121793576.gif
所以二次贝塞尔曲线是同时进行三个向量插值获得的点的集合。

二次贝塞尔曲线点求取函数

基于原理,很容易就可以写出一个求二阶贝塞尔曲线的函数。

# 求两点之间的二阶贝塞尔曲线点集合
func l2_bezier_curve(
	p1:Vector2,p2:Vector2, # 起止点
	ctl:Vector2,           # 控制点
	steps:=10              # 插值次数,值越大,曲线越平滑
) -> PackedVector2Array:
	var points:PackedVector2Array
	for i in range(steps+1):
		var t = i/float(steps)
		var l1 = p1.lerp(ctl,t)
		var l2 = ctl.lerp(p2,t)
		var p = l1.lerp(l2,t)
		points.append(p)
	return points

其中:

  • p1p2是曲线的起点和终点
  • ctl是曲线的控制点
  • steps是插值的步数,步数越大,曲线上点越多,曲线越平滑

测试代码:

extends Node2D
var curve:PackedVector2Array

var p1:=Vector2(200,200)
var p2:=Vector2(400,200)
var ctl:Vector2

func _process(delta: float) -> void:
	ctl = get_global_mouse_position()
	curve =  l2_bezier_curve(p1,p2,ctl)
	queue_redraw()

func _draw() -> void:
	
	draw_polyline(curve,Color.AQUAMARINE,1)   # 绘制路径
	draw_dashed_line(p1,ctl,Color.WHITE,1)
	draw_dashed_line(p2,ctl,Color.WHITE,1)
	draw_circle(p1,3,Color.ORANGE_RED)
	draw_circle(p2,3,Color.ORANGE_RED)
	draw_circle(ctl,3,Color.ORANGE_RED)

测试效果:
请添加图片描述

三次贝塞尔曲线

平面上四个点A、B、C、D:

  • 分别组成三个向量AB⃗\vec{AB}ABBC⃗\vec{BC}BCCD⃗\vec{CD}CD
  • 在三个向量上同步插值获得三个点E、F、G
  • EF和FG相连,组成向量EF⃗\vec{EF}EFFG⃗\vec{FG}FG
  • EF⃗\vec{EF}EFFG⃗\vec{FG}FG上同步插值获得点H和I
  • EF⃗\vec{EF}EF上同步插值获得点J
  • 整个同步插值过程获得的点J的集合,顺序相连,绘制处的就是三次贝塞尔曲线
    在这里插入图片描述

动态过程如下(也就是官方文档的动图):

-1227113256.gif

三阶贝塞尔曲线点求取函数

# 求两点之间的三阶贝塞尔曲线点集合
func l3_bezier_curve(
	p1:Vector2,p2:Vector2,                # 起止点
	ctl_1:=Vector2(),ctl_2:=Vector2(),    # 控制点
	steps:=10,                            # 插值次数,值越大,曲线越平滑
) -> PackedVector2Array:
	var points:PackedVector2Array = []
	# 求曲线点集
	for i in range(steps+1):
		var p = p1.bezier_interpolate(p1+ctl_1,p2+ctl_2,p2,i/float(steps))
		points.append(p)
	return points

其中:

  • p1p2是曲线的起点和终点
  • ctl1是起点的出控制点ctl2是终点的入控制点,注意这两个控制点的坐标是相对于起点和终点
  • steps是插值的步数,步数越大,曲线上点越多,曲线越平滑

测试代码:

extends Node2D
var curve:PackedVector2Array

var p1:=Vector2(200,200)
var p2:=Vector2(400,200)
var ctl1:=Vector2(0,50)
var ctl2:=Vector2(50,0)

func _process(delta: float) -> void:
	ctl2 = get_global_mouse_position() - p2
	curve =  l3_bezier_curve(p1,p2,ctl1,ctl2)
	queue_redraw()

func _draw() -> void:
	
	draw_polyline(curve,Color.AQUAMARINE,1)   # 绘制路径
	draw_dashed_line(p1,p1+ctl1,Color.WHITE,1)
	draw_dashed_line(p2,p2+ctl2,Color.WHITE,1)
	draw_circle(p1,3,Color.ORANGE_RED)
	draw_circle(p2,3,Color.ORANGE_RED)
	draw_circle(p1+ctl1,3,Color.ORANGE_RED)
	draw_circle(p2+ctl2,3,Color.ORANGE_RED)

测试效果:

请添加图片描述

导数

Vector2类型提供了一个名叫bezier_derivative()的方法,用来求贝塞尔曲线上t处的“导数”。

经过实际测试,这个所谓的“导数”返回一个点,连接贝塞尔曲线上t处的点与该点,刚好是一个切线段

我们以下面的代码进行测试:

extends Node2D

var p1 = Vector2(100,100)   # 起点
var p2 = Vector2(200,200)   # 终点
var ctl_1 = Vector2(50,0)  # 控制点1
var ctl_2 = Vector2(50,0)  # 控制点2

var points:PackedVector2Array = []   # 曲线点集合
var ds:PackedVector2Array = []       # 曲线点导数集合
var steps = 100;                     # 点的数目,越多曲线越平滑

var curve_color:= Color.WHITE       # 曲线绘制颜色
var ctl_color:= Color.AQUAMARINE    # 控制点和连线绘制颜色

func _ready() -> void:
	# 求曲线点集
	for i in range(steps+1):
		var p = p1.bezier_interpolate(p1+ctl_1,p2-ctl_2,p2,i/float(steps))
		points.append(p)
		var d = p1.bezier_derivative(p1+ctl_1,p2-ctl_2,p2,i/float(steps))
		ds.append(d)


func _draw() -> void:
	draw_polyline(points,curve_color,1)
	var i = 0
	draw_line(points[i],points[i]+ds[i],ctl_color,1)
	print(points[i]," ",points[i]+ds[i])

其中i是指曲线上点的索引,不同的i可以从points[i]中获取代表在i/float(steps)处的点。

以下是一些i值下对应点与“导数”点连线的情况:

在这里插入图片描述

将极坐标点函数运用于贝塞尔控制点

# 极坐标点函数 - 通过角度和长度定义一个点
func pVector2(angle:float = 0.0,length:float =0.0) -> Vector2:
	var dir = Vector2.RIGHT.rotated(deg_to_rad(angle))
	return dir * length

Vector2很难直观的表达方向和距离信息,pVector2则可以,所以在设定贝塞尔控制点时,可以使用极坐标点函数。

总结

本文历经修改,从最初的冗长删减为至今的模样,也总结出了二阶和三阶贝塞尔曲线点的求取函数,基本完成其使命。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

巽星石

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值